From 750878d6680d46a9da4c8a85f61506c3659462ce Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 9 Jan 2018 16:26:03 +0100 Subject: [PATCH] homogenization of templates: Docker --- autogen/gentemplates/gen.go | 362 ++++--- integration/docker_test.go | 2 +- provider/docker/config.go | 151 +-- provider/docker/config_container.go | 210 +++- .../docker/config_container_docker_test.go | 781 +++++++++++--- .../docker/config_container_swarm_test.go | 889 +++++++++------- provider/docker/config_service.go | 162 ++- provider/docker/config_service_test.go | 999 ++++++++++++++++-- provider/label/label.go | 24 +- provider/label/names.go | 14 +- templates/docker.tmpl | 362 ++++--- 11 files changed, 2786 insertions(+), 1170 deletions(-) diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index 6acb8d804..7b5e00cd8 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -235,267 +235,291 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} [backends] {{range $backendName, $backend := .Backends}} - {{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}} - {{$servers := index $backendServers $backendName}} - {{range $serverName, $server := $servers}} - {{if hasServices $server}} - {{$services := getServiceNames $server}} - {{range $serviceIndex, $serviceName := $services}} - [backends.backend-{{getServiceBackend $server $serviceName}}.servers.service-{{$serverName}}] - url = "{{getServiceProtocol $server $serviceName}}://{{getIPAddress $server}}:{{getServicePort $server $serviceName}}" - weight = {{getServiceWeight $server $serviceName}} + {{ $servers := index $backendServers $backendName }} + {{range $serverName, $server := $servers }} + {{if hasServices $server }} + {{ $services := getServiceNames $server }} + {{range $serviceIndex, $serviceName := $services }} + [backends.backend-{{ getServiceBackendName $server $serviceName }}.servers.service-{{ $serverName }}] + url = "{{ getServiceProtocol $server $serviceName }}://{{ getIPAddress $server }}:{{ getServicePort $server $serviceName }}" + weight = {{ getServiceWeight $server $serviceName }} {{end}} {{else}} - [backends.backend-{{$backendName}}.servers.server-{{$server.Name | replace "/" "" | replace "." "-"}}] - url = "{{getProtocol $server}}://{{getIPAddress $server}}:{{getPort $server}}" - weight = {{getWeight $server}} + [backends.backend-{{ $backendName }}.servers.server-{{ $server.Name | replace "/" "" | replace "." "-" }}] + url = "{{ getProtocol $server }}://{{ getIPAddress $server }}:{{ getPort $server }}" + weight = {{ getWeight $server }} {{end}} {{end}} {{end}} [frontends] -{{range $frontend, $containers := .Frontends}} +{{range $frontendName, $containers := .Frontends }} {{$container := index $containers 0}} - {{if hasServices $container}} - {{$services := getServiceNames $container}} + {{if hasServices $container }} + {{ $services := getServiceNames $container }} - {{range $serviceIndex, $serviceName := $services}} - [frontends."frontend-{{getServiceBackend $container $serviceName}}"] - backend = "backend-{{getServiceBackend $container $serviceName}}" - priority = {{getServicePriority $container $serviceName}} - passHostHeader = {{getServicePassHostHeader $container $serviceName}} - passTLSCert = {{getServicePassTLSCert $container $serviceName}} + {{range $serviceIndex, $serviceName := $services }} + {{ $ServiceFrontendName := getServiceBackendName $container $serviceName }} - entryPoints = [{{range getServiceEntryPoints $container $serviceName}} + [frontends."frontend-{{ $ServiceFrontendName }}"] + backend = "backend-{{ $ServiceFrontendName }}" + priority = {{ getServicePriority $container $serviceName }} + passHostHeader = {{ getServicePassHostHeader $container $serviceName }} + passTLSCert = {{ getServicePassTLSCert $container $serviceName }} + + entryPoints = [{{range getServiceEntryPoints $container $serviceName }} "{{.}}", {{end}}] - {{if getServiceWhitelistSourceRange $container $serviceName}} - whitelistSourceRange = [{{range getServiceWhitelistSourceRange $container $serviceName}} + {{ $whitelistSourceRange := getServiceWhitelistSourceRange $container $serviceName }} + {{if $whitelistSourceRange }} + whitelistSourceRange = [{{range $whitelistSourceRange }} "{{.}}", {{end}}] {{end}} - basicAuth = [{{range getServiceBasicAuth $container $serviceName}} + basicAuth = [{{range getServiceBasicAuth $container $serviceName }} "{{.}}", {{end}}] - {{if hasServiceRedirect $container $serviceName}} - [frontends."frontend-{{getServiceBackend $container $serviceName}}".redirect] - entryPoint = "{{getServiceRedirectEntryPoint $container $serviceName}}" - regex = "{{getServiceRedirectRegex $container $serviceName}}" - replacement = "{{getServiceRedirectReplacement $container $serviceName}}" + {{ $redirect := getServiceRedirect $container $serviceName }} + {{if $redirect }} + [frontends."frontend-{{ $ServiceFrontendName }}".redirect] + entryPoint = "{{ $redirect.EntryPoint }}" + regex = "{{ $redirect.Regex }}" + replacement = "{{ $redirect.Replacement }}" {{end}} - {{ if hasServiceErrorPages $container $serviceName }} - [frontends."frontend-{{getServiceBackend $container $serviceName}}".errors] - {{ range $pageName, $page := getServiceErrorPages $container $serviceName }} - [frontends."frontend-{{getServiceBackend $container $serviceName}}".errors.{{$pageName}}] - status = [{{range $page.Status}} + {{ $errorPages := getServiceErrorPages $container $serviceName }} + {{if $errorPages }} + [frontends."frontend-{{ $ServiceFrontendName }}".errors] + {{ range $pageName, $page := $errorPages }} + [frontends."frontend-{{ $ServiceFrontendName }}".errors.{{ $pageName }}] + status = [{{range $page.Status }} "{{.}}", {{end}}] - backend = "{{$page.Backend}}" - query = "{{$page.Query}}" + backend = "{{ $page.Backend }}" + query = "{{ $page.Query }}" {{end}} {{end}} - {{ if hasServiceRateLimits $container $serviceName }} - [frontends."frontend-{{getServiceBackend $container $serviceName}}".rateLimit] - extractorFunc = "{{ getRateLimitsExtractorFunc $container $serviceName }}" - [frontends."frontend-{{getServiceBackend $container $serviceName}}".rateLimit.rateSet] - {{ range $limitName, $rateLimit := getServiceRateLimits $container $serviceName }} - [frontends."frontend-{{getServiceBackend $container $serviceName}}".rateLimit.rateSet.{{ $limitName }}] - period = "{{ $rateLimit.Period }}" - average = {{ $rateLimit.Average }} - burst = {{ $rateLimit.Burst }} + {{ $rateLimit := getServiceRateLimit $container $serviceName }} + {{if $rateLimit }} + [frontends."frontend-{{ $ServiceFrontendName }}".rateLimit] + extractorFunc = "{{ $rateLimit.ExtractorFunc }}" + [frontends."frontend-{{ $ServiceFrontendName }}".rateLimit.rateSet] + {{range $limitName, $limit := $rateLimit.RateSet }} + [frontends."frontend-{{ $ServiceFrontendName }}".rateLimit.rateSet.{{ $limitName }}] + period = "{{ $limit.Period }}" + average = {{ $limit.Average }} + burst = {{ $limit.Burst }} {{end}} {{end}} - [frontends."frontend-{{getServiceBackend $container $serviceName}}".routes."service-{{$serviceName | replace "/" "" | replace "." "-"}}"] - rule = "{{getServiceFrontendRule $container $serviceName}}" + {{ $headers := getServiceHeaders $container $serviceName }} + {{if $headers }} + [frontends."frontend-{{ $ServiceFrontendName }}".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 }} - {{if hasServiceRequestHeaders $container $serviceName}} - [frontends."frontend-{{getServiceBackend $container $serviceName}}".headers.customRequestHeaders] - {{range $k, $v := getServiceRequestHeaders $container $serviceName}} - {{$k}} = "{{$v}}" + {{if $headers.AllowedHosts }} + AllowedHosts = [{{range $headers.AllowedHosts }} + "{{.}}", + {{end}}] + {{end}} + + {{if $headers.HostsProxyHeaders }} + HostsProxyHeaders = [{{range $headers.HostsProxyHeaders }} + "{{.}}", + {{end}}] + {{end}} + + {{if $headers.CustomRequestHeaders }} + [frontends."frontend-{{ $ServiceFrontendName }}".headers.customRequestHeaders] + {{range $k, $v := $headers.CustomRequestHeaders }} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + + {{if $headers.CustomResponseHeaders }} + [frontends."frontend-{{ $ServiceFrontendName }}".headers.customResponseHeaders] + {{range $k, $v := $headers.CustomResponseHeaders }} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + + {{if $headers.SSLProxyHeaders }} + [frontends."frontend-{{ $ServiceFrontendName }}".headers.SSLProxyHeaders] + {{range $k, $v := $headers.SSLProxyHeaders }} + {{$k}} = "{{$v}}" + {{end}} {{end}} {{end}} - {{if hasServiceResponseHeaders $container $serviceName}} - [frontends."frontend-{{getServiceBackend $container $serviceName}}".headers.customResponseHeaders] - {{range $k, $v := getServiceResponseHeaders $container $serviceName}} - {{$k}} = "{{$v}}" - {{end}} - {{end}} + [frontends."frontend-{{ $ServiceFrontendName }}".routes."service-{{ $serviceName | replace "/" "" | replace "." "-" }}"] + rule = "{{ getServiceFrontendRule $container $serviceName }}" {{end}} ## end range services {{else}} - [frontends."frontend-{{$frontend}}"] - backend = "backend-{{getBackend $container}}" - priority = {{getPriority $container}} - passHostHeader = {{getPassHostHeader $container}} - passTLSCert = {{getPassTLSCert $container}} + [frontends."frontend-{{ $frontendName }}"] + backend = "backend-{{ getBackendName $container }}" + priority = {{ getPriority $container }} + passHostHeader = {{ getPassHostHeader $container }} + passTLSCert = {{ getPassTLSCert $container }} - entryPoints = [{{range getEntryPoints $container}} + entryPoints = [{{range getEntryPoints $container }} "{{.}}", {{end}}] - {{if getWhitelistSourceRange $container}} - whitelistSourceRange = [{{range getWhitelistSourceRange $container}} + {{ $whitelistSourceRange := getWhitelistSourceRange $container}} + {{if $whitelistSourceRange }} + whitelistSourceRange = [{{range $whitelistSourceRange }} "{{.}}", {{end}}] {{end}} - basicAuth = [{{range getBasicAuth $container}} + basicAuth = [{{range getBasicAuth $container }} "{{.}}", {{end}}] - {{if hasRedirect $container}} - [frontends."frontend-{{$frontend}}".redirect] - entryPoint = "{{getRedirectEntryPoint $container}}" - regex = "{{getRedirectRegex $container}}" - replacement = "{{getRedirectReplacement $container}}" + {{ $redirect := getRedirect $container }} + {{if $redirect }} + [frontends."frontend-{{ $frontendName }}".redirect] + entryPoint = "{{ $redirect.EntryPoint }}" + regex = "{{ $redirect.Regex }}" + replacement = "{{ $redirect.Replacement }}" {{end}} - {{ if hasErrorPages $container }} - [frontends."frontend-{{$frontend}}".errors] - {{ range $pageName, $page := getErrorPages $container }} - [frontends."frontend-{{$frontend}}".errors.{{ $pageName }}] - status = [{{range $page.Status}} + {{ $errorPages := getErrorPages $container }} + {{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 $container }} - [frontends."frontend-{{$frontend}}".rateLimit] - extractorFunc = "{{ getRateLimitsExtractorFunc $container }}" - [frontends."frontend-{{$frontend}}".rateLimit.rateSet] - {{ range $limitName, $rateLimit := getRateLimits $container }} - [frontends."frontend-{{$frontend}}".rateLimit.rateSet.{{ $limitName }}] - period = "{{ $rateLimit.Period }}" - average = {{ $rateLimit.Average }} - burst = {{ $rateLimit.Burst }} + {{ $rateLimit := getRateLimit $container }} + {{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 $container}} - [frontends."frontend-{{$frontend}}".headers] - {{if hasSSLRedirectHeaders $container}} - SSLRedirect = {{getSSLRedirectHeaders $container}} - {{end}} - {{if hasSSLTemporaryRedirectHeaders $container}} - SSLTemporaryRedirect = {{getSSLTemporaryRedirectHeaders $container}} - {{end}} - {{if hasSSLHostHeaders $container}} - SSLHost = "{{getSSLHostHeaders $container}}" - {{end}} - {{if hasSTSSecondsHeaders $container}} - STSSeconds = {{getSTSSecondsHeaders $container}} - {{end}} - {{if hasSTSIncludeSubdomainsHeaders $container}} - STSIncludeSubdomains = {{getSTSIncludeSubdomainsHeaders $container}} - {{end}} - {{if hasSTSPreloadHeaders $container}} - STSPreload = {{getSTSPreloadHeaders $container}} - {{end}} - {{if hasForceSTSHeaderHeaders $container}} - ForceSTSHeader = {{getForceSTSHeaderHeaders $container}} - {{end}} - {{if hasFrameDenyHeaders $container}} - FrameDeny = {{getFrameDenyHeaders $container}} - {{end}} - {{if hasCustomFrameOptionsValueHeaders $container}} - CustomFrameOptionsValue = "{{getCustomFrameOptionsValueHeaders $container}}" - {{end}} - {{if hasContentTypeNosniffHeaders $container}} - ContentTypeNosniff = {{getContentTypeNosniffHeaders $container}} - {{end}} - {{if hasBrowserXSSFilterHeaders $container}} - BrowserXSSFilter = {{getBrowserXSSFilterHeaders $container}} - {{end}} - {{if hasContentSecurityPolicyHeaders $container}} - ContentSecurityPolicy = "{{getContentSecurityPolicyHeaders $container}}" - {{end}} - {{if hasPublicKeyHeaders $container}} - PublicKey = "{{getPublicKeyHeaders $container}}" - {{end}} - {{if hasReferrerPolicyHeaders $container}} - ReferrerPolicy = "{{getReferrerPolicyHeaders $container}}" - {{end}} - {{if hasIsDevelopmentHeaders $container}} - IsDevelopment = {{getIsDevelopmentHeaders $container}} - {{end}} + {{ $headers := getHeaders $container }} + {{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 }} - {{if hasAllowedHostsHeaders $container}} - AllowedHosts = [{{range getAllowedHostsHeaders $container}} + {{if $headers.AllowedHosts }} + AllowedHosts = [{{range $headers.AllowedHosts }} "{{.}}", {{end}}] {{end}} - {{if hasHostsProxyHeaders $container}} - HostsProxyHeaders = [{{range getHostsProxyHeaders $container}} + {{if $headers.HostsProxyHeaders }} + HostsProxyHeaders = [{{range $headers.HostsProxyHeaders }} "{{.}}", {{end}}] {{end}} - {{if hasRequestHeaders $container}} - [frontends."frontend-{{$frontend}}".headers.customRequestHeaders] - {{range $k, $v := getRequestHeaders $container}} + {{if $headers.CustomRequestHeaders }} + [frontends."frontend-{{ $frontendName }}".headers.customRequestHeaders] + {{range $k, $v := $headers.CustomRequestHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} - {{if hasResponseHeaders $container}} - [frontends."frontend-{{$frontend}}".headers.customResponseHeaders] - {{range $k, $v := getResponseHeaders $container}} + {{if $headers.CustomResponseHeaders }} + [frontends."frontend-{{ $frontendName }}".headers.customResponseHeaders] + {{range $k, $v := $headers.CustomResponseHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} - {{if hasSSLProxyHeaders $container}} - [frontends."frontend-{{$frontend}}".headers.SSLProxyHeaders] - {{range $k, $v := getSSLProxyHeaders $container}} + {{if $headers.SSLProxyHeaders }} + [frontends."frontend-{{ $frontendName }}".headers.SSLProxyHeaders] + {{range $k, $v := $headers.SSLProxyHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} {{end}} - [frontends."frontend-{{$frontend}}".routes."route-frontend-{{$frontend}}"] - rule = "{{getFrontendRule $container}}" + [frontends."frontend-{{ $frontendName }}".routes."route-frontend-{{ $frontendName }}"] + rule = "{{ getFrontendRule $container }}" {{end}} diff --git a/integration/docker_test.go b/integration/docker_test.go index 0cd61ecfc..75eb118d6 100644 --- a/integration/docker_test.go +++ b/integration/docker_test.go @@ -184,7 +184,7 @@ func (s *DockerSuite) TestDockerContainersWithOneMissingLabels(c *check.C) { defer os.Remove(file) // Start a container with some labels labels := map[string]string{ - label.TraefikFrontendValue: "my.super.host", + "traefik.frontend.value": "my.super.host", } s.startContainerWithLabels(c, "swarm:1.0.0", labels, "manage", "token://blabla") diff --git a/provider/docker/config.go b/provider/docker/config.go index feb8cba59..cfcb35b77 100644 --- a/provider/docker/config.go +++ b/provider/docker/config.go @@ -14,117 +14,78 @@ import ( func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.Configuration { var DockerFuncMap = template.FuncMap{ "getDomain": getFuncStringLabel(label.TraefikDomain, p.Domain), + "getSubDomain": getSubDomain, "isBackendLBSwarm": isBackendLBSwarm, // FIXME dead ? // Backend functions - "getIPAddress": p.getIPAddress, - "getPort": getPort, - "getWeight": getFuncStringLabel(label.TraefikWeight, label.DefaultWeight), - "getProtocol": getFuncStringLabel(label.TraefikProtocol, label.DefaultProtocol), - "hasHealthCheckLabels": hasFunc(label.TraefikBackendHealthCheckPath), - "getHealthCheckPath": getFuncStringLabel(label.TraefikBackendHealthCheckPath, ""), - "getHealthCheckPort": getFuncIntLabel(label.TraefikBackendHealthCheckPort, label.DefaultBackendHealthCheckPort), - "getHealthCheckInterval": getFuncStringLabel(label.TraefikBackendHealthCheckInterval, ""), - "hasCircuitBreakerLabel": hasFunc(label.TraefikBackendCircuitBreakerExpression), + "getIPAddress": p.getIPAddress, + "getPort": getPort, + "getWeight": getFuncIntLabel(label.TraefikWeight, label.DefaultWeightInt), + "getProtocol": getFuncStringLabel(label.TraefikProtocol, label.DefaultProtocol), + "getMaxConn": getMaxConn, + "getHealthCheck": getHealthCheck, + "getCircuitBreaker": getCircuitBreaker, + "getLoadBalancer": getLoadBalancer, + + // TODO Deprecated [breaking] + "hasCircuitBreakerLabel": hasFunc(label.TraefikBackendCircuitBreakerExpression), + // TODO Deprecated [breaking] "getCircuitBreakerExpression": getFuncStringLabel(label.TraefikBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression), - "hasLoadBalancerLabel": hasLoadBalancerLabel, - "getLoadBalancerMethod": getFuncStringLabel(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), - "hasMaxConnLabels": hasMaxConnLabels, - "getMaxConnAmount": getFuncInt64Label(label.TraefikBackendMaxConnAmount, math.MaxInt64), - "getMaxConnExtractorFunc": getFuncStringLabel(label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc), - "getSticky": getSticky, - "hasStickinessLabel": hasFunc(label.TraefikBackendLoadBalancerStickiness), - "getStickinessCookieName": getFuncStringLabel(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName), + // TODO Deprecated [breaking] + "hasLoadBalancerLabel": hasLoadBalancerLabel, + // TODO Deprecated [breaking] + "getLoadBalancerMethod": getFuncStringLabel(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), + // TODO Deprecated [breaking] + "hasMaxConnLabels": hasMaxConnLabels, + // TODO Deprecated [breaking] + "getMaxConnAmount": getFuncInt64Label(label.TraefikBackendMaxConnAmount, math.MaxInt64), + // TODO Deprecated [breaking] + "getMaxConnExtractorFunc": getFuncStringLabel(label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc), + // TODO Deprecated [breaking] + "getSticky": getSticky, + // TODO Deprecated [breaking] + "hasStickinessLabel": hasFunc(label.TraefikBackendLoadBalancerStickiness), + // TODO Deprecated [breaking] + "getStickinessCookieName": getFuncStringLabel(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName), // Frontend functions - "getBackend": getBackend, - "getPriority": getFuncStringLabel(label.TraefikFrontendPriority, label.DefaultFrontendPriority), - "getPassHostHeader": getFuncStringLabel(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), - "getPassTLSCert": getFuncBoolLabel(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), - "getEntryPoints": getFuncSliceStringLabel(label.TraefikFrontendEntryPoints), - "getBasicAuth": getFuncSliceStringLabel(label.TraefikFrontendAuthBasic), - "getWhitelistSourceRange": getFuncSliceStringLabel(label.TraefikFrontendWhitelistSourceRange), - "getFrontendRule": p.getFrontendRule, - "hasRedirect": hasRedirect, - "getRedirectEntryPoint": getFuncStringLabel(label.TraefikFrontendRedirectEntryPoint, label.DefaultFrontendRedirectEntryPoint), - "getRedirectRegex": getFuncStringLabel(label.TraefikFrontendRedirectRegex, ""), - "getRedirectReplacement": getFuncStringLabel(label.TraefikFrontendRedirectReplacement, ""), - "hasErrorPages": hasErrorPages, - "getErrorPages": getErrorPages, - "hasRateLimits": hasFunc(label.TraefikFrontendRateLimitExtractorFunc), - "getRateLimitsExtractorFunc": getFuncStringLabel(label.TraefikFrontendRateLimitExtractorFunc, ""), - "getRateLimits": getRateLimits, - // Headers - "hasHeaders": hasHeaders, - "hasRequestHeaders": hasFunc(label.TraefikFrontendRequestHeaders), - "getRequestHeaders": getFuncMapLabel(label.TraefikFrontendRequestHeaders), - "hasResponseHeaders": hasFunc(label.TraefikFrontendResponseHeaders), - "getResponseHeaders": getFuncMapLabel(label.TraefikFrontendResponseHeaders), - "hasAllowedHostsHeaders": hasFunc(label.TraefikFrontendAllowedHosts), - "getAllowedHostsHeaders": getFuncSliceStringLabel(label.TraefikFrontendAllowedHosts), - "hasHostsProxyHeaders": hasFunc(label.TraefikFrontendHostsProxyHeaders), - "getHostsProxyHeaders": getFuncSliceStringLabel(label.TraefikFrontendHostsProxyHeaders), - "hasSSLRedirectHeaders": hasFunc(label.TraefikFrontendSSLRedirect), - "getSSLRedirectHeaders": getFuncBoolLabel(label.TraefikFrontendSSLRedirect, false), - "hasSSLTemporaryRedirectHeaders": hasFunc(label.TraefikFrontendSSLTemporaryRedirect), - "getSSLTemporaryRedirectHeaders": getFuncBoolLabel(label.TraefikFrontendSSLTemporaryRedirect, false), - "hasSSLHostHeaders": hasFunc(label.TraefikFrontendSSLHost), - "getSSLHostHeaders": getFuncStringLabel(label.TraefikFrontendSSLHost, ""), - "hasSSLProxyHeaders": hasFunc(label.TraefikFrontendSSLProxyHeaders), - "getSSLProxyHeaders": getFuncMapLabel(label.TraefikFrontendSSLProxyHeaders), - "hasSTSSecondsHeaders": hasFunc(label.TraefikFrontendSTSSeconds), - "getSTSSecondsHeaders": getFuncInt64Label(label.TraefikFrontendSTSSeconds, 0), - "hasSTSIncludeSubdomainsHeaders": hasFunc(label.TraefikFrontendSTSIncludeSubdomains), - "getSTSIncludeSubdomainsHeaders": getFuncBoolLabel(label.TraefikFrontendSTSIncludeSubdomains, false), - "hasSTSPreloadHeaders": hasFunc(label.TraefikFrontendSTSPreload), - "getSTSPreloadHeaders": getFuncBoolLabel(label.TraefikFrontendSTSPreload, false), - "hasForceSTSHeaderHeaders": hasFunc(label.TraefikFrontendForceSTSHeader), - "getForceSTSHeaderHeaders": getFuncBoolLabel(label.TraefikFrontendForceSTSHeader, false), - "hasFrameDenyHeaders": hasFunc(label.TraefikFrontendFrameDeny), - "getFrameDenyHeaders": getFuncBoolLabel(label.TraefikFrontendFrameDeny, false), - "hasCustomFrameOptionsValueHeaders": hasFunc(label.TraefikFrontendCustomFrameOptionsValue), - "getCustomFrameOptionsValueHeaders": getFuncStringLabel(label.TraefikFrontendCustomFrameOptionsValue, ""), - "hasContentTypeNosniffHeaders": hasFunc(label.TraefikFrontendContentTypeNosniff), - "getContentTypeNosniffHeaders": getFuncBoolLabel(label.TraefikFrontendContentTypeNosniff, false), - "hasBrowserXSSFilterHeaders": hasFunc(label.TraefikFrontendBrowserXSSFilter), - "getBrowserXSSFilterHeaders": getFuncBoolLabel(label.TraefikFrontendBrowserXSSFilter, false), - "hasContentSecurityPolicyHeaders": hasFunc(label.TraefikFrontendContentSecurityPolicy), - "getContentSecurityPolicyHeaders": getFuncStringLabel(label.TraefikFrontendContentSecurityPolicy, ""), - "hasPublicKeyHeaders": hasFunc(label.TraefikFrontendPublicKey), - "getPublicKeyHeaders": getFuncStringLabel(label.TraefikFrontendPublicKey, ""), - "hasReferrerPolicyHeaders": hasFunc(label.TraefikFrontendReferrerPolicy), - "getReferrerPolicyHeaders": getFuncStringLabel(label.TraefikFrontendReferrerPolicy, ""), - "hasIsDevelopmentHeaders": hasFunc(label.TraefikFrontendIsDevelopment), - "getIsDevelopmentHeaders": getFuncBoolLabel(label.TraefikFrontendIsDevelopment, false), + "getBackend": getBackendName, // TODO Deprecated [breaking] replaced by getBackendName + "getBackendName": getBackendName, + "getPriority": getFuncIntLabel(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), + "getPassHostHeader": getFuncBoolLabel(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getPassTLSCert": getFuncBoolLabel(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), + "getEntryPoints": getFuncSliceStringLabel(label.TraefikFrontendEntryPoints), + "getBasicAuth": getFuncSliceStringLabel(label.TraefikFrontendAuthBasic), + "getWhitelistSourceRange": getFuncSliceStringLabel(label.TraefikFrontendWhitelistSourceRange), + "getFrontendRule": p.getFrontendRule, + + "getRedirect": getRedirect, + "getErrorPages": getErrorPages, + "getRateLimit": getRateLimit, + "getHeaders": getHeaders, // Services - "hasServices": hasServices, - "getServiceNames": getServiceNames, - "getServiceBackend": getServiceBackend, + "hasServices": hasServices, + "getServiceNames": getServiceNames, + "getServiceBackend": getServiceBackendName, // TODO Deprecated [breaking] replaced by getServiceBackendName + "getServiceBackendName": getServiceBackendName, // Services - Backend server functions "getServicePort": getServicePort, "getServiceProtocol": getFuncServiceStringLabel(label.SuffixProtocol, label.DefaultProtocol), "getServiceWeight": getFuncServiceStringLabel(label.SuffixWeight, label.DefaultWeight), // Services - Frontend functions "getServiceEntryPoints": getFuncServiceSliceStringLabel(label.SuffixFrontendEntryPoints), - "getServiceWhitelistSourceRange": getFuncServiceSliceStringLabel(label.TraefikFrontendWhitelistSourceRange), + "getServiceWhitelistSourceRange": getFuncServiceSliceStringLabel(label.SuffixFrontendWhitelistSourceRange), "getServiceBasicAuth": getFuncServiceSliceStringLabel(label.SuffixFrontendAuthBasic), "getServiceFrontendRule": p.getServiceFrontendRule, - "getServicePassHostHeader": getFuncServiceStringLabel(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeader), + "getServicePassHostHeader": getFuncServiceBoolLabel(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeaderBool), "getServicePassTLSCert": getFuncServiceBoolLabel(label.SuffixFrontendPassTLSCert, label.DefaultPassTLSCert), - "getServicePriority": getFuncServiceStringLabel(label.SuffixFrontendPriority, label.DefaultFrontendPriority), - "hasServiceRedirect": hasServiceRedirect, - "getServiceRedirectEntryPoint": getFuncServiceStringLabel(label.SuffixFrontendRedirectEntryPoint, label.DefaultFrontendRedirectEntryPoint), - "getServiceRedirectReplacement": getFuncServiceStringLabel(label.SuffixFrontendRedirectReplacement, ""), - "getServiceRedirectRegex": getFuncServiceStringLabel(label.SuffixFrontendRedirectRegex, ""), - "hasServiceRequestHeaders": hasFuncServiceLabel(label.SuffixFrontendRequestHeaders), - "getServiceRequestHeaders": getFuncServiceMapLabel(label.SuffixFrontendRequestHeaders), - "hasServiceResponseHeaders": hasFuncServiceLabel(label.SuffixFrontendResponseHeaders), - "getServiceResponseHeaders": getFuncServiceMapLabel(label.SuffixFrontendResponseHeaders), - "hasServiceErrorPages": hasServiceErrorPages, - "getServiceErrorPages": getServiceErrorPages, - "hasServiceRateLimits": hasFuncServiceLabel(label.SuffixFrontendRateLimitExtractorFunc), - "getServiceRateLimits": getServiceRateLimits, + "getServicePriority": getFuncServiceIntLabel(label.SuffixFrontendPriority, label.DefaultFrontendPriorityInt), + + "getServiceRedirect": getServiceRedirect, + "getServiceErrorPages": getServiceErrorPages, + "getServiceRateLimit": getServiceRateLimit, + "getServiceHeaders": getServiceHeaders, } // filter containers filteredContainers := fun.Filter(func(container dockerData) bool { @@ -143,7 +104,7 @@ func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.C serviceNames[container.ServiceName] = struct{}{} } } - backendName := getBackend(container) + backendName := getBackendName(container) backends[backendName] = container servers[backendName] = append(servers[backendName], container) } diff --git a/provider/docker/config_container.go b/provider/docker/config_container.go index 560d3c5ba..867dcbc0f 100644 --- a/provider/docker/config_container.go +++ b/provider/docker/config_container.go @@ -2,6 +2,7 @@ package docker import ( "context" + "math" "strconv" "strings" @@ -99,21 +100,7 @@ func (p Provider) getIPAddress(container dockerData) string { return "" } -func hasLoadBalancerLabel(container dockerData) bool { - method := label.Has(container.Labels, label.TraefikBackendLoadBalancerMethod) - sticky := label.Has(container.Labels, label.TraefikBackendLoadBalancerSticky) - stickiness := label.Has(container.Labels, label.TraefikBackendLoadBalancerStickiness) - cookieName := label.Has(container.Labels, label.TraefikBackendLoadBalancerStickinessCookieName) - return method || sticky || stickiness || cookieName -} - -func hasMaxConnLabels(container dockerData) bool { - mca := label.Has(container.Labels, label.TraefikBackendMaxConnAmount) - mcef := label.Has(container.Labels, label.TraefikBackendMaxConnExtractorFunc) - return mca && mcef -} - -func getBackend(container dockerData) string { +func getBackendName(container dockerData) string { if value := label.GetStringValue(container.Labels, label.TraefikBackend, ""); len(value) != 0 { return provider.Normalize(value) } @@ -154,27 +141,95 @@ func getSubDomain(name string) string { return strings.Replace(strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1), "_", "-", -1) } -// TODO: Deprecated -// Deprecated replaced by Stickiness -func getSticky(container dockerData) string { - if label.Has(container.Labels, label.TraefikBackendLoadBalancerSticky) { - log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) - } - - return label.GetStringValue(container.Labels, label.TraefikBackendLoadBalancerSticky, "false") -} - func isBackendLBSwarm(container dockerData) bool { return label.GetBoolValue(container.Labels, labelBackendLoadBalancerSwarm, false) } -func hasRedirect(container dockerData) bool { - return label.Has(container.Labels, label.TraefikFrontendRedirectEntryPoint) || - label.Has(container.Labels, label.TraefikFrontendRedirectReplacement) && label.Has(container.Labels, label.TraefikFrontendRedirectRegex) +func getMaxConn(container dockerData) *types.MaxConn { + amount := label.GetInt64Value(container.Labels, label.TraefikBackendMaxConnAmount, math.MinInt64) + extractorFunc := label.GetStringValue(container.Labels, label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc) + + if amount == math.MinInt64 || len(extractorFunc) == 0 { + return nil + } + + return &types.MaxConn{ + Amount: amount, + ExtractorFunc: extractorFunc, + } } -func hasErrorPages(container dockerData) bool { - return label.HasPrefix(container.Labels, label.Prefix+label.BaseFrontendErrorPage) +func getLoadBalancer(container dockerData) *types.LoadBalancer { + if !label.HasPrefix(container.Labels, label.TraefikBackendLoadBalancer) { + return nil + } + + method := label.GetStringValue(container.Labels, label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod) + + lb := &types.LoadBalancer{ + Method: method, + Sticky: getSticky(container), + } + + if label.GetBoolValue(container.Labels, label.TraefikBackendLoadBalancerStickiness, false) { + cookieName := label.GetStringValue(container.Labels, label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName) + lb.Stickiness = &types.Stickiness{CookieName: cookieName} + } + + return lb +} + +// TODO: Deprecated +// replaced by Stickiness +// Deprecated +func getSticky(container dockerData) bool { + if label.Has(container.Labels, label.TraefikBackendLoadBalancerSticky) { + log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) + } + + return label.GetBoolValue(container.Labels, label.TraefikBackendLoadBalancerSticky, false) +} + +func getCircuitBreaker(container dockerData) *types.CircuitBreaker { + circuitBreaker := label.GetStringValue(container.Labels, label.TraefikBackendCircuitBreakerExpression, "") + if len(circuitBreaker) == 0 { + return nil + } + return &types.CircuitBreaker{Expression: circuitBreaker} +} + +func getHealthCheck(container dockerData) *types.HealthCheck { + path := label.GetStringValue(container.Labels, label.TraefikBackendHealthCheckPath, "") + if len(path) == 0 { + return nil + } + + port := label.GetIntValue(container.Labels, label.TraefikBackendHealthCheckPort, label.DefaultBackendHealthCheckPort) + interval := label.GetStringValue(container.Labels, label.TraefikBackendHealthCheckInterval, "") + + return &types.HealthCheck{ + Path: path, + Port: port, + Interval: interval, + } +} + +func getRedirect(container dockerData) *types.Redirect { + if label.Has(container.Labels, label.TraefikFrontendRedirectEntryPoint) { + return &types.Redirect{ + EntryPoint: label.GetStringValue(container.Labels, label.TraefikFrontendRedirectEntryPoint, ""), + } + } + + if label.Has(container.Labels, label.TraefikFrontendRedirectRegex) && + label.Has(container.Labels, label.TraefikFrontendRedirectReplacement) { + return &types.Redirect{ + Regex: label.GetStringValue(container.Labels, label.TraefikFrontendRedirectRegex, ""), + Replacement: label.GetStringValue(container.Labels, label.TraefikFrontendRedirectReplacement, ""), + } + } + + return nil } func getErrorPages(container dockerData) map[string]*types.ErrorPage { @@ -182,46 +237,76 @@ func getErrorPages(container dockerData) map[string]*types.ErrorPage { return label.ParseErrorPages(container.Labels, prefix, label.RegexpFrontendErrorPage) } -func getRateLimits(container dockerData) map[string]*types.Rate { +func getRateLimit(container dockerData) *types.RateLimit { + extractorFunc := label.GetStringValue(container.Labels, label.TraefikFrontendRateLimitExtractorFunc, "") + if len(extractorFunc) == 0 { + return nil + } + prefix := label.Prefix + label.BaseFrontendRateLimit - return label.ParseRateSets(container.Labels, prefix, label.RegexpFrontendRateLimit) + limits := label.ParseRateSets(container.Labels, prefix, label.RegexpFrontendRateLimit) + + return &types.RateLimit{ + ExtractorFunc: extractorFunc, + RateSet: limits, + } } -func hasHeaders(container dockerData) bool { - for key := range container.Labels { - if strings.HasPrefix(key, label.TraefikFrontendHeaders) { - return true - } +func getHeaders(container dockerData) *types.Headers { + headers := &types.Headers{ + CustomRequestHeaders: label.GetMapValue(container.Labels, label.TraefikFrontendRequestHeaders), + CustomResponseHeaders: label.GetMapValue(container.Labels, label.TraefikFrontendResponseHeaders), + SSLProxyHeaders: label.GetMapValue(container.Labels, label.TraefikFrontendSSLProxyHeaders), + AllowedHosts: label.GetSliceStringValue(container.Labels, label.TraefikFrontendAllowedHosts), + HostsProxyHeaders: label.GetSliceStringValue(container.Labels, label.TraefikFrontendHostsProxyHeaders), + STSSeconds: label.GetInt64Value(container.Labels, label.TraefikFrontendSTSSeconds, 0), + SSLRedirect: label.GetBoolValue(container.Labels, label.TraefikFrontendSSLRedirect, false), + SSLTemporaryRedirect: label.GetBoolValue(container.Labels, label.TraefikFrontendSSLTemporaryRedirect, false), + STSIncludeSubdomains: label.GetBoolValue(container.Labels, label.TraefikFrontendSTSIncludeSubdomains, false), + STSPreload: label.GetBoolValue(container.Labels, label.TraefikFrontendSTSPreload, false), + ForceSTSHeader: label.GetBoolValue(container.Labels, label.TraefikFrontendForceSTSHeader, false), + FrameDeny: label.GetBoolValue(container.Labels, label.TraefikFrontendFrameDeny, false), + ContentTypeNosniff: label.GetBoolValue(container.Labels, label.TraefikFrontendContentTypeNosniff, false), + BrowserXSSFilter: label.GetBoolValue(container.Labels, label.TraefikFrontendBrowserXSSFilter, false), + IsDevelopment: label.GetBoolValue(container.Labels, label.TraefikFrontendIsDevelopment, false), + SSLHost: label.GetStringValue(container.Labels, label.TraefikFrontendSSLHost, ""), + CustomFrameOptionsValue: label.GetStringValue(container.Labels, label.TraefikFrontendCustomFrameOptionsValue, ""), + ContentSecurityPolicy: label.GetStringValue(container.Labels, label.TraefikFrontendContentSecurityPolicy, ""), + PublicKey: label.GetStringValue(container.Labels, label.TraefikFrontendPublicKey, ""), + ReferrerPolicy: label.GetStringValue(container.Labels, label.TraefikFrontendReferrerPolicy, ""), } - return false + + if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() { + return nil + } + + return headers +} + +// Deprecated +func hasLoadBalancerLabel(container dockerData) bool { + method := label.Has(container.Labels, label.TraefikBackendLoadBalancerMethod) + sticky := label.Has(container.Labels, label.TraefikBackendLoadBalancerSticky) + stickiness := label.Has(container.Labels, label.TraefikBackendLoadBalancerStickiness) + cookieName := label.Has(container.Labels, label.TraefikBackendLoadBalancerStickinessCookieName) + return method || sticky || stickiness || cookieName +} + +// Deprecated +func hasMaxConnLabels(container dockerData) bool { + mca := label.Has(container.Labels, label.TraefikBackendMaxConnAmount) + mcef := label.Has(container.Labels, label.TraefikBackendMaxConnExtractorFunc) + return mca && mcef } // Label functions -func getFuncInt64Label(labelName string, defaultValue int64) func(container dockerData) int64 { - return func(container dockerData) int64 { - return label.GetInt64Value(container.Labels, labelName, defaultValue) - } -} - -func getFuncMapLabel(labelName string) func(container dockerData) map[string]string { - return func(container dockerData) map[string]string { - return label.GetMapValue(container.Labels, labelName) - } -} - func getFuncStringLabel(labelName string, defaultValue string) func(container dockerData) string { return func(container dockerData) string { return label.GetStringValue(container.Labels, labelName, defaultValue) } } -func getFuncIntLabel(labelName string, defaultValue int) func(container dockerData) int { - return func(container dockerData) int { - return label.GetIntValue(container.Labels, labelName, defaultValue) - } -} - func getFuncBoolLabel(labelName string, defaultValue bool) func(container dockerData) bool { return func(container dockerData) bool { return label.GetBoolValue(container.Labels, labelName, defaultValue) @@ -234,6 +319,19 @@ func getFuncSliceStringLabel(labelName string) func(container dockerData) []stri } } +func getFuncIntLabel(labelName string, defaultValue int) func(container dockerData) int { + return func(container dockerData) int { + return label.GetIntValue(container.Labels, labelName, defaultValue) + } +} + +func getFuncInt64Label(labelName string, defaultValue int64) func(container dockerData) int64 { + return func(container dockerData) int64 { + return label.GetInt64Value(container.Labels, labelName, defaultValue) + } +} + +// Deprecated func hasFunc(labelName string) func(container dockerData) bool { return func(container dockerData) bool { return label.Has(container.Labels, labelName) diff --git a/provider/docker/config_container_docker_test.go b/provider/docker/config_container_docker_test.go index deb6bd5c5..bea44da54 100644 --- a/provider/docker/config_container_docker_test.go +++ b/provider/docker/config_container_docker_test.go @@ -18,16 +18,19 @@ import ( func TestDockerBuildConfiguration(t *testing.T) { testCases := []struct { + desc string containers []docker.ContainerJSON expectedFrontends map[string]*types.Frontend expectedBackends map[string]*types.Backend }{ { + desc: "when no container", containers: []docker.ContainerJSON{}, expectedFrontends: map[string]*types.Frontend{}, expectedBackends: map[string]*types.Backend{}, }, { + desc: "when basic container configuration", containers: []docker.ContainerJSON{ containerJSON( name("test"), @@ -63,24 +66,16 @@ func TestDockerBuildConfiguration(t *testing.T) { }, }, { + desc: "when container has label 'enable' to false", containers: []docker.ContainerJSON{ containerJSON( - name("test1"), + name("test"), labels(map[string]string{ - label.TraefikBackend: "foobar", - label.TraefikFrontendEntryPoints: "http,https", - label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", - label.TraefikFrontendRedirectEntryPoint: "https", - }), - ports(nat.PortMap{ - "80/tcp": {}, - }), - withNetwork("bridge", ipv4("127.0.0.1")), - ), - containerJSON( - name("test2"), - labels(map[string]string{ - label.TraefikBackend: "foobar", + label.TraefikEnable: "false", + label.TraefikPort: "666", + label.TraefikProtocol: "https", + label.TraefikWeight: "12", + label.TraefikBackend: "foobar", }), ports(nat.PortMap{ "80/tcp": {}, @@ -88,162 +83,71 @@ func TestDockerBuildConfiguration(t *testing.T) { withNetwork("bridge", ipv4("127.0.0.1")), ), }, - expectedFrontends: map[string]*types.Frontend{ - "frontend-Host-test1-docker-localhost-0": { - Backend: "backend-foobar", - PassHostHeader: true, - EntryPoints: []string{"http", "https"}, - BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, - Redirect: &types.Redirect{ - EntryPoint: "https", - }, - Routes: map[string]types.Route{ - "route-frontend-Host-test1-docker-localhost-0": { - Rule: "Host:test1.docker.localhost", - }, - }, - }, - "frontend-Host-test2-docker-localhost-1": { - Backend: "backend-foobar", - PassHostHeader: true, - EntryPoints: []string{}, - BasicAuth: []string{}, - Routes: map[string]types.Route{ - "route-frontend-Host-test2-docker-localhost-1": { - Rule: "Host:test2.docker.localhost", - }, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-foobar": { - Servers: map[string]types.Server{ - "server-test1": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - "server-test2": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - }, - CircuitBreaker: nil, - }, - }, - }, - { - containers: []docker.ContainerJSON{ - containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikBackend: "foobar", - label.TraefikFrontendEntryPoints: "http,https", - label.TraefikBackendMaxConnAmount: "1000", - label.TraefikBackendMaxConnExtractorFunc: "somethingelse", - label.TraefikBackendLoadBalancerMethod: "drr", - label.TraefikBackendCircuitBreakerExpression: "NetworkErrorRatio() > 0.5", - }), - ports(nat.PortMap{ - "80/tcp": {}, - }), - withNetwork("bridge", ipv4("127.0.0.1")), - ), - }, - expectedFrontends: map[string]*types.Frontend{ - "frontend-Host-test1-docker-localhost-0": { - Backend: "backend-foobar", - PassHostHeader: true, - EntryPoints: []string{"http", "https"}, - BasicAuth: []string{}, - Routes: map[string]types.Route{ - "route-frontend-Host-test1-docker-localhost-0": { - Rule: "Host:test1.docker.localhost", - }, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-foobar": { - Servers: map[string]types.Server{ - "server-test1": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - }, - CircuitBreaker: &types.CircuitBreaker{ - Expression: "NetworkErrorRatio() > 0.5", - }, - LoadBalancer: &types.LoadBalancer{ - Method: "drr", - }, - MaxConn: &types.MaxConn{ - Amount: 1000, - ExtractorFunc: "somethingelse", - }, - }, - }, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, }, { + desc: "when all labels are set", containers: []docker.ContainerJSON{ containerJSON( name("test1"), 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", - }), - ports(nat.PortMap{ - "80/tcp": {}, - }), - withNetwork("bridge", ipv4("127.0.0.1")), - ), - }, - expectedFrontends: map[string]*types.Frontend{ - "frontend-Host-test1-docker-localhost-0": { - EntryPoints: []string{}, - BasicAuth: []string{}, - PassHostHeader: true, - Backend: "backend-foobar", - Routes: map[string]types.Route{ - "route-frontend-Host-test1-docker-localhost-0": { - Rule: "Host:test1.docker.localhost", - }, - }, - Errors: map[string]*types.ErrorPage{ - "foo": { - Status: []string{"404"}, - Query: "foo_query", - Backend: "foobar", - }, - "bar": { - Status: []string{"500", "600"}, - Query: "bar_query", - Backend: "foobar", - }, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-foobar": { - Servers: map[string]types.Server{ - "server-test1": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - }, - }, - }, - }, - { - containers: []docker.ContainerJSON{ - containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikBackend: "foobar", + label.TraefikFrontendRateLimitExtractorFunc: "client.ip", label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", @@ -259,14 +163,76 @@ func TestDockerBuildConfiguration(t *testing.T) { ), }, expectedFrontends: map[string]*types.Frontend{ - "frontend-Host-test1-docker-localhost-0": { - EntryPoints: []string{}, - BasicAuth: []string{}, - PassHostHeader: true, - Backend: "backend-foobar", + "frontend-Host-traefik-io-0": { + EntryPoints: []string{ + "http", + "https", + }, + Backend: "backend-foobar", Routes: map[string]types.Route{ - "route-frontend-Host-test1-docker-localhost-0": { - Rule: "Host:test1.docker.localhost", + "route-frontend-Host-traefik-io-0": { + 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{ @@ -284,24 +250,48 @@ func TestDockerBuildConfiguration(t *testing.T) { }, }, }, + Redirect: &types.Redirect{ + EntryPoint: "https", + Regex: "", + Replacement: "", + }, }, }, expectedBackends: map[string]*types.Backend{ "backend-foobar": { Servers: map[string]types.Server{ "server-test1": { - URL: "http://127.0.0.1:80", - Weight: 0, + URL: "https://127.0.0.1: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", + }, }, }, }, } - for caseID, test := range testCases { + for _, test := range testCases { test := test - t.Run(strconv.Itoa(caseID), func(t *testing.T) { + t.Run(test.desc, func(t *testing.T) { t.Parallel() var dockerDataList []dockerData for _, cont := range test.containers { @@ -848,7 +838,7 @@ func TestDockerGetFrontendRule(t *testing.T) { } } -func TestDockerGetBackend(t *testing.T) { +func TestDockerGetBackendName(t *testing.T) { testCases := []struct { container docker.ContainerJSON expected string @@ -881,7 +871,7 @@ func TestDockerGetBackend(t *testing.T) { t.Run(strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() dData := parseContainer(test.container) - actual := getBackend(dData) + actual := getBackendName(dData) if actual != test.expected { t.Errorf("expected %q, got %q", test.expected, actual) } @@ -1014,6 +1004,313 @@ func TestDockerGetPort(t *testing.T) { } } +func TestDockerGetMaxConn(t *testing.T) { + testCases := []struct { + desc string + container docker.ContainerJSON + expected *types.MaxConn + }{ + { + desc: "should return nil when no max conn labels", + container: containerJSON( + name("test1"), + labels(map[string]string{})), + expected: nil, + }, + { + desc: "should return nil when no amount label", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikBackendMaxConnExtractorFunc: "client.ip", + })), + expected: nil, + }, + { + desc: "should return default when no empty extractorFunc label", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikBackendMaxConnExtractorFunc: "", + label.TraefikBackendMaxConnAmount: "666", + })), + expected: &types.MaxConn{ + ExtractorFunc: "request.host", + Amount: 666, + }, + }, + { + desc: "should return a struct when max conn labels are set", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikBackendMaxConnExtractorFunc: "client.ip", + label.TraefikBackendMaxConnAmount: "666", + })), + expected: &types.MaxConn{ + ExtractorFunc: "client.ip", + Amount: 666, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := getMaxConn(dData) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestDockerGetCircuitBreaker(t *testing.T) { + testCases := []struct { + desc string + container docker.ContainerJSON + expected *types.CircuitBreaker + }{ + { + desc: "should return nil when no CB labels", + container: containerJSON( + name("test1"), + labels(map[string]string{})), + expected: nil, + }, + { + desc: "should return a struct CB when CB labels are set", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikBackendCircuitBreakerExpression: "NetworkErrorRatio() > 0.5", + })), + expected: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := getCircuitBreaker(dData) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestDockerGetLoadBalancer(t *testing.T) { + testCases := []struct { + desc string + container docker.ContainerJSON + expected *types.LoadBalancer + }{ + { + desc: "should return nil when no LB labels", + container: containerJSON( + name("test1"), + labels(map[string]string{})), + expected: nil, + }, + { + desc: "should return a struct when labels are set", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikBackendLoadBalancerMethod: "drr", + label.TraefikBackendLoadBalancerSticky: "true", + label.TraefikBackendLoadBalancerStickiness: "true", + label.TraefikBackendLoadBalancerStickinessCookieName: "foo", + })), + expected: &types.LoadBalancer{ + Method: "drr", + Sticky: true, + Stickiness: &types.Stickiness{ + CookieName: "foo", + }, + }, + }, + { + desc: "should return a nil Stickiness when Stickiness is not set", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikBackendLoadBalancerMethod: "drr", + label.TraefikBackendLoadBalancerSticky: "true", + label.TraefikBackendLoadBalancerStickinessCookieName: "foo", + })), + 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() + + dData := parseContainer(test.container) + + actual := getLoadBalancer(dData) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestDockerGetRedirect(t *testing.T) { + testCases := []struct { + desc string + container docker.ContainerJSON + expected *types.Redirect + }{ + { + desc: "should return nil when no redirect labels", + container: containerJSON( + name("test1"), + labels(map[string]string{})), + expected: nil, + }, + { + desc: "should use only entry point tag when mix regex redirect and entry point redirect", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikFrontendRedirectEntryPoint: "https", + label.TraefikFrontendRedirectRegex: "(.*)", + label.TraefikFrontendRedirectReplacement: "$1", + }), + ), + expected: &types.Redirect{ + EntryPoint: "https", + }, + }, + { + desc: "should return a struct when entry point redirect label", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikFrontendRedirectEntryPoint: "https", + }), + ), + expected: &types.Redirect{ + EntryPoint: "https", + }, + }, + { + desc: "should return a struct when regex redirect labels", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikFrontendRedirectRegex: "(.*)", + label.TraefikFrontendRedirectReplacement: "$1", + }), + ), + expected: &types.Redirect{ + Regex: "(.*)", + Replacement: "$1", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := getRedirect(dData) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestDockerGetRateLimit(t *testing.T) { + testCases := []struct { + desc string + container docker.ContainerJSON + expected *types.RateLimit + }{ + { + desc: "should return nil when no rate limit labels", + container: containerJSON( + name("test1"), + labels(map[string]string{})), + expected: nil, + }, + { + desc: "should return a struct when rate limit labels are defined", + container: containerJSON( + name("test1"), + 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", + })), + 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", + container: containerJSON( + name("test1"), + 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", + })), + expected: nil, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := getRateLimit(dData) + + assert.Equal(t, test.expected, actual) + }) + } +} + func TestGetErrorPages(t *testing.T) { testCases := []struct { desc string @@ -1069,3 +1366,145 @@ func TestGetErrorPages(t *testing.T) { }) } } + +func TestDockerGetHealthCheck(t *testing.T) { + testCases := []struct { + desc string + container docker.ContainerJSON + expected *types.HealthCheck + }{ + { + desc: "should return nil when no health check labels", + container: containerJSON( + name("test1"), + labels(map[string]string{})), + expected: nil, + }, + { + desc: "should return nil when no health check Path label", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikBackendHealthCheckPort: "80", + label.TraefikBackendHealthCheckInterval: "6", + })), + expected: nil, + }, + { + desc: "should return a struct when health check labels are set", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikBackendHealthCheckPath: "/health", + label.TraefikBackendHealthCheckPort: "80", + label.TraefikBackendHealthCheckInterval: "6", + })), + 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() + + dData := parseContainer(test.container) + + actual := getHealthCheck(dData) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestDockerGetHeaders(t *testing.T) { + testCases := []struct { + desc string + container docker.ContainerJSON + expected *types.Headers + }{ + { + desc: "should return nil when no custom headers options are set", + container: containerJSON( + name("test1"), + labels(map[string]string{})), + expected: nil, + }, + { + desc: "should return a struct when all custom headers options are set", + container: containerJSON( + name("test1"), + 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", + }), + ), + 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() + + dData := parseContainer(test.container) + + actual := getHeaders(dData) + + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/provider/docker/config_container_swarm_test.go b/provider/docker/config_container_swarm_test.go index 366cd76b5..09111673e 100644 --- a/provider/docker/config_container_swarm_test.go +++ b/provider/docker/config_container_swarm_test.go @@ -4,7 +4,9 @@ import ( "reflect" "strconv" "testing" + "time" + "github.com/containous/flaeg" "github.com/containous/traefik/provider/label" "github.com/containous/traefik/types" docker "github.com/docker/docker/api/types" @@ -13,171 +15,56 @@ import ( "github.com/stretchr/testify/require" ) -func TestSwarmGetFrontendName(t *testing.T) { +func TestSwarmBuildConfiguration(t *testing.T) { testCases := []struct { - service swarm.Service - expected string - networks map[string]*docker.NetworkResource + desc string + services []swarm.Service + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + networks map[string]*docker.NetworkResource }{ { - service: swarmService(serviceName("foo")), - expected: "Host-foo-docker-localhost-0", - networks: map[string]*docker.NetworkResource{}, + desc: "when no container", + services: []swarm.Service{}, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + networks: map[string]*docker.NetworkResource{}, }, { - service: swarmService(serviceLabels(map[string]string{ - label.TraefikFrontendRule: "Headers:User-Agent,bat/0.1.0", - })), - expected: "Headers-User-Agent-bat-0-1-0-0", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceLabels(map[string]string{ - label.TraefikFrontendRule: "Host:foo.bar", - })), - expected: "Host-foo-bar-0", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceLabels(map[string]string{ - label.TraefikFrontendRule: "Path:/test", - })), - expected: "Path-test-0", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService( - serviceName("test"), - serviceLabels(map[string]string{ - label.TraefikFrontendRule: "PathPrefix:/test2", - }), - ), - expected: "PathPrefix-test2-0", - networks: map[string]*docker.NetworkResource{}, - }, - } - - for serviceID, test := range testCases { - test := test - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dData := parseService(test.service, test.networks) - provider := &Provider{ - Domain: "docker.localhost", - SwarmMode: true, - } - actual := provider.getFrontendName(dData, 0) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } - }) - } -} - -func TestSwarmGetFrontendRule(t *testing.T) { - testCases := []struct { - service swarm.Service - expected string - networks map[string]*docker.NetworkResource - }{ - { - service: swarmService(serviceName("foo")), - expected: "Host:foo.docker.localhost", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceName("bar")), - expected: "Host:bar.docker.localhost", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceLabels(map[string]string{ - label.TraefikFrontendRule: "Host:foo.bar", - })), - expected: "Host:foo.bar", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceLabels(map[string]string{ - label.TraefikFrontendRule: "Path:/test", - })), - expected: "Path:/test", - networks: map[string]*docker.NetworkResource{}, - }, - } - - for serviceID, test := range testCases { - test := test - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dData := parseService(test.service, test.networks) - provider := &Provider{ - Domain: "docker.localhost", - SwarmMode: true, - } - actual := provider.getFrontendRule(dData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } - }) - } -} - -func TestSwarmGetBackend(t *testing.T) { - testCases := []struct { - service swarm.Service - expected string - networks map[string]*docker.NetworkResource - }{ - { - service: swarmService(serviceName("foo")), - expected: "foo", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceName("bar")), - expected: "bar", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceLabels(map[string]string{ - label.TraefikBackend: "foobar", - })), - expected: "foobar", - networks: map[string]*docker.NetworkResource{}, - }, - } - - for serviceID, test := range testCases { - test := test - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dData := parseService(test.service, test.networks) - actual := getBackend(dData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } - }) - } -} - -func TestSwarmGetIPAddress(t *testing.T) { - testCases := []struct { - service swarm.Service - expected string - networks map[string]*docker.NetworkResource - }{ - { - service: swarmService(withEndpointSpec(modeDNSSR)), - expected: "", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService( - withEndpointSpec(modeVIP), - withEndpoint(virtualIP("1", "10.11.12.13/24")), - ), - expected: "10.11.12.13", + desc: "when basic container configuration", + services: []swarm.Service{ + swarmService( + serviceName("test"), + serviceLabels(map[string]string{ + label.TraefikPort: "80", + }), + withEndpointSpec(modeVIP), + withEndpoint(virtualIP("1", "127.0.0.1/24")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-test-docker-localhost-0": { + Backend: "backend-test", + PassHostHeader: true, + EntryPoints: []string{}, + BasicAuth: []string{}, + Routes: map[string]types.Route{ + "route-frontend-Host-test-docker-localhost-0": { + Rule: "Host:test.docker.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test": { + Servers: map[string]types.Server{ + "server-test": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + }, + }, networks: map[string]*docker.NetworkResource{ "1": { Name: "foo", @@ -185,71 +72,257 @@ func TestSwarmGetIPAddress(t *testing.T) { }, }, { - service: swarmService( - serviceLabels(map[string]string{ - labelDockerNetwork: "barnet", - }), - withEndpointSpec(modeVIP), - withEndpoint( - virtualIP("1", "10.11.12.13/24"), - virtualIP("2", "10.11.12.99/24"), + desc: "when container has label 'enable' to false", + services: []swarm.Service{ + swarmService( + serviceName("test1"), + serviceLabels(map[string]string{ + label.TraefikEnable: "false", + label.TraefikPort: "666", + label.TraefikProtocol: "https", + label.TraefikWeight: "12", + label.TraefikBackend: "foobar", + }), + withEndpointSpec(modeVIP), + withEndpoint(virtualIP("1", "127.0.0.1/24")), ), - ), - expected: "10.11.12.99", + }, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, networks: map[string]*docker.NetworkResource{ "1": { - Name: "foonet", + Name: "foo", }, - "2": { - Name: "barnet", + }, + }, + { + desc: "when all labels are set", + services: []swarm.Service{ + swarmService( + serviceName("test1"), + serviceLabels(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", + }), + withEndpointSpec(modeVIP), + withEndpoint(virtualIP("1", "127.0.0.1/24")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-traefik-io-0": { + EntryPoints: []string{ + "http", + "https", + }, + Backend: "backend-foobar", + Routes: map[string]types.Route{ + "route-frontend-Host-traefik-io-0": { + 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-test1": { + URL: "https://127.0.0.1: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", + }, + }, + }, + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foo", }, }, }, } - for serviceID, test := range testCases { + for _, test := range testCases { test := test - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Run(test.desc, func(t *testing.T) { t.Parallel() - dData := parseService(test.service, test.networks) + var dockerDataList []dockerData + for _, service := range test.services { + dData := parseService(service, test.networks) + dockerDataList = append(dockerDataList, dData) + } + provider := &Provider{ - SwarmMode: true, + Domain: "docker.localhost", + ExposedByDefault: true, + SwarmMode: true, } - actual := provider.getIPAddress(dData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } - }) - } -} -func TestSwarmGetPort(t *testing.T) { - testCases := []struct { - service swarm.Service - expected string - networks map[string]*docker.NetworkResource - }{ - { - service: swarmService( - serviceLabels(map[string]string{ - label.TraefikPort: "8080", - }), - withEndpointSpec(modeDNSSR), - ), - expected: "8080", - networks: map[string]*docker.NetworkResource{}, - }, - } + actualConfig := provider.buildConfiguration(dockerDataList) + require.NotNil(t, actualConfig, "actualConfig") - for serviceID, test := range testCases { - test := test - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dData := parseService(test.service, test.networks) - actual := getPort(dData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } + assert.EqualValues(t, test.expectedBackends, actualConfig.Backends) + assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends) }) } } @@ -388,161 +461,6 @@ func TestSwarmTraefikFilter(t *testing.T) { } } -func TestSwarmLoadDockerConfig(t *testing.T) { - testCases := []struct { - services []swarm.Service - expectedFrontends map[string]*types.Frontend - expectedBackends map[string]*types.Backend - networks map[string]*docker.NetworkResource - }{ - { - services: []swarm.Service{}, - expectedFrontends: map[string]*types.Frontend{}, - expectedBackends: map[string]*types.Backend{}, - networks: map[string]*docker.NetworkResource{}, - }, - { - services: []swarm.Service{ - swarmService( - serviceName("test"), - serviceLabels(map[string]string{ - label.TraefikPort: "80", - }), - withEndpointSpec(modeVIP), - withEndpoint(virtualIP("1", "127.0.0.1/24")), - ), - }, - expectedFrontends: map[string]*types.Frontend{ - "frontend-Host-test-docker-localhost-0": { - Backend: "backend-test", - PassHostHeader: true, - EntryPoints: []string{}, - BasicAuth: []string{}, - Routes: map[string]types.Route{ - "route-frontend-Host-test-docker-localhost-0": { - Rule: "Host:test.docker.localhost", - }, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-test": { - Servers: map[string]types.Server{ - "server-test": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - }, - CircuitBreaker: nil, - LoadBalancer: nil, - }, - }, - networks: map[string]*docker.NetworkResource{ - "1": { - Name: "foo", - }, - }, - }, - { - services: []swarm.Service{ - swarmService( - serviceName("test1"), - serviceLabels(map[string]string{ - label.TraefikPort: "80", - label.TraefikBackend: "foobar", - label.TraefikFrontendEntryPoints: "http,https", - label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", - label.TraefikFrontendRedirectEntryPoint: "https", - }), - withEndpointSpec(modeVIP), - withEndpoint(virtualIP("1", "127.0.0.1/24")), - ), - swarmService( - serviceName("test2"), - serviceLabels(map[string]string{ - label.TraefikPort: "80", - label.TraefikBackend: "foobar", - }), - withEndpointSpec(modeVIP), - withEndpoint(virtualIP("1", "127.0.0.1/24")), - ), - }, - expectedFrontends: map[string]*types.Frontend{ - "frontend-Host-test1-docker-localhost-0": { - Backend: "backend-foobar", - PassHostHeader: true, - EntryPoints: []string{"http", "https"}, - BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, - Redirect: &types.Redirect{ - EntryPoint: "https", - }, - Routes: map[string]types.Route{ - "route-frontend-Host-test1-docker-localhost-0": { - Rule: "Host:test1.docker.localhost", - }, - }, - }, - "frontend-Host-test2-docker-localhost-1": { - Backend: "backend-foobar", - PassHostHeader: true, - EntryPoints: []string{}, - BasicAuth: []string{}, - Routes: map[string]types.Route{ - "route-frontend-Host-test2-docker-localhost-1": { - Rule: "Host:test2.docker.localhost", - }, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-foobar": { - Servers: map[string]types.Server{ - "server-test1": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - "server-test2": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - }, - CircuitBreaker: nil, - LoadBalancer: nil, - }, - }, - networks: map[string]*docker.NetworkResource{ - "1": { - Name: "foo", - }, - }, - }, - } - - for caseID, test := range testCases { - test := test - t.Run(strconv.Itoa(caseID), func(t *testing.T) { - t.Parallel() - var dockerDataList []dockerData - for _, service := range test.services { - dData := parseService(service, test.networks) - dockerDataList = append(dockerDataList, dData) - } - - provider := &Provider{ - Domain: "docker.localhost", - ExposedByDefault: true, - SwarmMode: true, - } - - actualConfig := provider.buildConfiguration(dockerDataList) - require.NotNil(t, actualConfig, "actualConfig") - - assert.EqualValues(t, test.expectedBackends, actualConfig.Backends) - assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends) - }) - } -} - func TestSwarmTaskParsing(t *testing.T) { testCases := []struct { service swarm.Service @@ -647,3 +565,244 @@ func TestSwarmGetFuncStringLabel(t *testing.T) { }) } } + +func TestSwarmGetFrontendName(t *testing.T) { + testCases := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(serviceName("foo")), + expected: "Host-foo-docker-localhost-0", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Headers:User-Agent,bat/0.1.0", + })), + expected: "Headers-User-Agent-bat-0-1-0-0", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Host:foo.bar", + })), + expected: "Host-foo-bar-0", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Path:/test", + })), + expected: "Path-test-0", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService( + serviceName("test"), + serviceLabels(map[string]string{ + label.TraefikFrontendRule: "PathPrefix:/test2", + }), + ), + expected: "PathPrefix-test2-0", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dData := parseService(test.service, test.networks) + provider := &Provider{ + Domain: "docker.localhost", + SwarmMode: true, + } + actual := provider.getFrontendName(dData, 0) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestSwarmGetFrontendRule(t *testing.T) { + testCases := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(serviceName("foo")), + expected: "Host:foo.docker.localhost", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceName("bar")), + expected: "Host:bar.docker.localhost", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Host:foo.bar", + })), + expected: "Host:foo.bar", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Path:/test", + })), + expected: "Path:/test", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dData := parseService(test.service, test.networks) + provider := &Provider{ + Domain: "docker.localhost", + SwarmMode: true, + } + actual := provider.getFrontendRule(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestSwarmGetBackendName(t *testing.T) { + testCases := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(serviceName("foo")), + expected: "foo", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceName("bar")), + expected: "bar", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikBackend: "foobar", + })), + expected: "foobar", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dData := parseService(test.service, test.networks) + actual := getBackendName(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestSwarmGetIPAddress(t *testing.T) { + testCases := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(withEndpointSpec(modeDNSSR)), + expected: "", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService( + withEndpointSpec(modeVIP), + withEndpoint(virtualIP("1", "10.11.12.13/24")), + ), + expected: "10.11.12.13", + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foo", + }, + }, + }, + { + service: swarmService( + serviceLabels(map[string]string{ + labelDockerNetwork: "barnet", + }), + withEndpointSpec(modeVIP), + withEndpoint( + virtualIP("1", "10.11.12.13/24"), + virtualIP("2", "10.11.12.99/24"), + ), + ), + expected: "10.11.12.99", + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foonet", + }, + "2": { + Name: "barnet", + }, + }, + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dData := parseService(test.service, test.networks) + provider := &Provider{ + SwarmMode: true, + } + actual := provider.getIPAddress(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestSwarmGetPort(t *testing.T) { + testCases := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService( + serviceLabels(map[string]string{ + label.TraefikPort: "8080", + }), + withEndpointSpec(modeDNSSR), + ), + expected: "8080", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dData := parseService(test.service, test.networks) + actual := getPort(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} diff --git a/provider/docker/config_service.go b/provider/docker/config_service.go index ee720b4c6..72af79b01 100644 --- a/provider/docker/config_service.go +++ b/provider/docker/config_service.go @@ -45,14 +45,14 @@ func checkServiceLabelPort(container dockerData) error { serviceLabels := make(map[string]struct{}) for lbl := range container.Labels { // Get all port service labels - portLabel := label.PortRegexp.FindStringSubmatch(lbl) + portLabel := extractServicePort(lbl) if len(portLabel) > 0 { serviceLabelPorts[portLabel[0]] = struct{}{} } // Get only one instance of all service names from service labels - servicesLabelNames := label.ServicesPropertiesRegexp.FindStringSubmatch(lbl) + servicesLabelNames := label.FindServiceSubmatch(lbl) - if len(servicesLabelNames) > 0 && !strings.HasPrefix(lbl, label.TraefikFrontend) { + if len(servicesLabelNames) > 0 { serviceLabels[strings.Split(servicesLabelNames[0], ".")[1]] = struct{}{} } } @@ -72,12 +72,21 @@ func checkServiceLabelPort(container dockerData) error { return err } +func extractServicePort(labelName string) []string { + if strings.HasPrefix(labelName, label.TraefikFrontend+".") || + strings.HasPrefix(labelName, label.TraefikBackend+".") { + return nil + } + + return label.PortRegexp.FindStringSubmatch(labelName) +} + // Extract backend from labels for a given service and a given docker container -func getServiceBackend(container dockerData, serviceName string) string { +func getServiceBackendName(container dockerData, serviceName string) string { if value, ok := getServiceLabels(container, serviceName)[label.SuffixFrontendBackend]; ok { return provider.Normalize(container.ServiceName + "-" + value) } - return provider.Normalize(container.ServiceName + "-" + getBackend(container) + "-" + serviceName) + return provider.Normalize(container.ServiceName + "-" + getBackendName(container) + "-" + serviceName) } // Extract port from labels for a given service and a given docker container @@ -88,54 +97,103 @@ func getServicePort(container dockerData, serviceName string) string { return getPort(container) } -func hasServiceRedirect(container dockerData, serviceName string) bool { +func getServiceRedirect(container dockerData, serviceName string) *types.Redirect { serviceLabels := getServiceLabels(container, serviceName) - if len(serviceLabels) == 0 { - return false + + if hasStrictServiceLabel(serviceLabels, label.SuffixFrontendRedirectEntryPoint) { + return &types.Redirect{ + EntryPoint: getStrictServiceStringValue(serviceLabels, label.SuffixFrontendRedirectEntryPoint, label.DefaultFrontendRedirectEntryPoint), + } } - return label.Has(serviceLabels, label.SuffixFrontendRedirectEntryPoint) || - label.Has(serviceLabels, label.SuffixFrontendRedirectRegex) && label.Has(serviceLabels, label.SuffixFrontendRedirectReplacement) -} + if hasStrictServiceLabel(serviceLabels, label.SuffixFrontendRedirectRegex) && + hasStrictServiceLabel(serviceLabels, label.SuffixFrontendRedirectReplacement) { + return &types.Redirect{ + Regex: getStrictServiceStringValue(serviceLabels, label.SuffixFrontendRedirectRegex, ""), + Replacement: getStrictServiceStringValue(serviceLabels, label.SuffixFrontendRedirectReplacement, ""), + } + } -func hasServiceErrorPages(container dockerData, serviceName string) bool { - serviceLabels := getServiceLabels(container, serviceName) - return label.HasPrefix(serviceLabels, label.BaseFrontendErrorPage) + return getRedirect(container) } func getServiceErrorPages(container dockerData, serviceName string) map[string]*types.ErrorPage { serviceLabels := getServiceLabels(container, serviceName) - return label.ParseErrorPages(serviceLabels, label.BaseFrontendErrorPage, label.RegexpBaseFrontendErrorPage) + + if label.HasPrefix(serviceLabels, label.BaseFrontendErrorPage) { + return label.ParseErrorPages(serviceLabels, label.BaseFrontendErrorPage, label.RegexpBaseFrontendErrorPage) + } + + return getErrorPages(container) } -func getServiceRateLimits(container dockerData, serviceName string) map[string]*types.Rate { +func getServiceRateLimit(container dockerData, serviceName string) *types.RateLimit { serviceLabels := getServiceLabels(container, serviceName) - return label.ParseRateSets(serviceLabels, label.BaseFrontendRateLimit, label.RegexpBaseFrontendRateLimit) + + if hasStrictServiceLabel(serviceLabels, label.SuffixFrontendRateLimitExtractorFunc) { + extractorFunc := getStrictServiceStringValue(serviceLabels, label.SuffixFrontendRateLimitExtractorFunc, label.DefaultBackendMaxconnExtractorFunc) + return &types.RateLimit{ + ExtractorFunc: extractorFunc, + RateSet: label.ParseRateSets(serviceLabels, label.BaseFrontendRateLimit, label.RegexpBaseFrontendRateLimit), + } + } + + return getRateLimit(container) +} + +func getServiceHeaders(container dockerData, serviceName string) *types.Headers { + serviceLabels := getServiceLabels(container, serviceName) + + headers := &types.Headers{ + CustomRequestHeaders: getServiceMapValue(container, serviceLabels, serviceName, label.SuffixFrontendRequestHeaders), + CustomResponseHeaders: getServiceMapValue(container, serviceLabels, serviceName, label.SuffixFrontendResponseHeaders), + SSLProxyHeaders: getServiceMapValue(container, serviceLabels, serviceName, label.SuffixFrontendHeadersSSLProxyHeaders), + AllowedHosts: getServiceSliceValue(container, serviceLabels, label.SuffixFrontendHeadersAllowedHosts), + HostsProxyHeaders: getServiceSliceValue(container, serviceLabels, label.SuffixFrontendHeadersHostsProxyHeaders), + STSSeconds: getServiceInt64Value(container, serviceLabels, label.SuffixFrontendHeadersSTSSeconds, 0), + SSLRedirect: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersSSLRedirect, false), + SSLTemporaryRedirect: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersSSLTemporaryRedirect, false), + STSIncludeSubdomains: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersSTSIncludeSubdomains, false), + STSPreload: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersSTSPreload, false), + ForceSTSHeader: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersForceSTSHeader, false), + FrameDeny: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersFrameDeny, false), + ContentTypeNosniff: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersContentTypeNosniff, false), + BrowserXSSFilter: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersBrowserXSSFilter, false), + IsDevelopment: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersIsDevelopment, false), + SSLHost: getServiceStringValue(container, serviceLabels, label.SuffixFrontendHeadersSSLHost, ""), + CustomFrameOptionsValue: getServiceStringValue(container, serviceLabels, label.SuffixFrontendHeadersCustomFrameOptionsValue, ""), + ContentSecurityPolicy: getServiceStringValue(container, serviceLabels, label.SuffixFrontendHeadersContentSecurityPolicy, ""), + PublicKey: getServiceStringValue(container, serviceLabels, label.SuffixFrontendHeadersPublicKey, ""), + ReferrerPolicy: getServiceStringValue(container, serviceLabels, label.SuffixFrontendHeadersReferrerPolicy, ""), + } + + if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() { + return nil + } + + return headers } // Service label functions -func getFuncServiceMapLabel(labelSuffix string) func(container dockerData, serviceName string) map[string]string { - return func(container dockerData, serviceName string) map[string]string { - return getServiceMapLabel(container, serviceName, labelSuffix) - } -} - func getFuncServiceSliceStringLabel(labelSuffix string) func(container dockerData, serviceName string) []string { return func(container dockerData, serviceName string) []string { - return getServiceSliceStringLabel(container, serviceName, labelSuffix) + serviceLabels := getServiceLabels(container, serviceName) + return getServiceSliceValue(container, serviceLabels, labelSuffix) } } func getFuncServiceStringLabel(labelSuffix string, defaultValue string) func(container dockerData, serviceName string) string { return func(container dockerData, serviceName string) string { - return getServiceStringLabel(container, serviceName, labelSuffix, defaultValue) + serviceLabels := getServiceLabels(container, serviceName) + return getServiceStringValue(container, serviceLabels, labelSuffix, defaultValue) } } func getFuncServiceBoolLabel(labelSuffix string, defaultValue bool) func(container dockerData, serviceName string) bool { return func(container dockerData, serviceName string) bool { - return getServiceBoolLabel(container, serviceName, labelSuffix, defaultValue) + serviceLabels := getServiceLabels(container, serviceName) + return getServiceBoolValue(container, serviceLabels, labelSuffix, defaultValue) } } @@ -145,44 +203,42 @@ func getFuncServiceIntLabel(labelSuffix string, defaultValue int) func(container } } -func hasFuncServiceLabel(labelSuffix string) func(container dockerData, serviceName string) bool { - return func(container dockerData, serviceName string) bool { - return hasServiceLabel(container, serviceName, labelSuffix) - } +func hasStrictServiceLabel(serviceLabels map[string]string, labelSuffix string) bool { + value, ok := serviceLabels[labelSuffix] + return ok && len(value) > 0 } -func hasServiceLabel(container dockerData, serviceName string, labelSuffix string) bool { - value, ok := getServiceLabels(container, serviceName)[labelSuffix] - if ok && len(value) > 0 { - return true +func getServiceStringValue(container dockerData, serviceLabels map[string]string, labelSuffix string, defaultValue string) string { + if value, ok := serviceLabels[labelSuffix]; ok { + return value } - return label.Has(container.Labels, label.Prefix+labelSuffix) + return label.GetStringValue(container.Labels, label.Prefix+labelSuffix, defaultValue) } -func getServiceMapLabel(container dockerData, serviceName string, labelSuffix string) map[string]string { - if value, ok := getServiceLabels(container, serviceName)[labelSuffix]; ok { +func getStrictServiceStringValue(serviceLabels map[string]string, labelSuffix string, defaultValue string) string { + if value, ok := serviceLabels[labelSuffix]; ok { + return value + } + return defaultValue +} + +func getServiceMapValue(container dockerData, serviceLabels map[string]string, serviceName string, labelSuffix string) map[string]string { + if value, ok := serviceLabels[labelSuffix]; ok { lblName := label.GetServiceLabel(labelSuffix, serviceName) return label.ParseMapValue(lblName, value) } return label.GetMapValue(container.Labels, label.Prefix+labelSuffix) } -func getServiceSliceStringLabel(container dockerData, serviceName string, labelSuffix string) []string { - if value, ok := getServiceLabels(container, serviceName)[labelSuffix]; ok { +func getServiceSliceValue(container dockerData, serviceLabels map[string]string, labelSuffix string) []string { + if value, ok := serviceLabels[labelSuffix]; ok { return label.SplitAndTrimString(value, ",") } return label.GetSliceStringValue(container.Labels, label.Prefix+labelSuffix) } -func getServiceStringLabel(container dockerData, serviceName string, labelSuffix string, defaultValue string) string { - if value, ok := getServiceLabels(container, serviceName)[labelSuffix]; ok { - return value - } - return label.GetStringValue(container.Labels, label.Prefix+labelSuffix, defaultValue) -} - -func getServiceBoolLabel(container dockerData, serviceName string, labelSuffix string, defaultValue bool) bool { - if rawValue, ok := getServiceLabels(container, serviceName)[labelSuffix]; ok { +func getServiceBoolValue(container dockerData, serviceLabels map[string]string, labelSuffix string, defaultValue bool) bool { + if rawValue, ok := serviceLabels[labelSuffix]; ok { value, err := strconv.ParseBool(rawValue) if err == nil { return value @@ -201,6 +257,16 @@ func getServiceIntLabel(container dockerData, serviceName string, labelSuffix st return label.GetIntValue(container.Labels, label.Prefix+labelSuffix, defaultValue) } +func getServiceInt64Value(container dockerData, serviceLabels map[string]string, labelSuffix string, defaultValue int64) int64 { + if rawValue, ok := serviceLabels[labelSuffix]; ok { + value, err := strconv.ParseInt(rawValue, 10, 64) + if err == nil { + return value + } + } + return label.GetInt64Value(container.Labels, label.Prefix+labelSuffix, defaultValue) +} + func getServiceLabels(container dockerData, serviceName string) label.ServicePropertyValues { return label.ExtractServiceProperties(container.Labels)[serviceName] } diff --git a/provider/docker/config_service_test.go b/provider/docker/config_service_test.go index 67677d1f5..3a6307f18 100644 --- a/provider/docker/config_service_test.go +++ b/provider/docker/config_service_test.go @@ -4,7 +4,9 @@ import ( "reflect" "strconv" "testing" + "time" + "github.com/containous/flaeg" "github.com/containous/traefik/provider/label" "github.com/containous/traefik/types" docker "github.com/docker/docker/api/types" @@ -15,24 +17,25 @@ import ( func TestDockerServiceBuildConfiguration(t *testing.T) { testCases := []struct { + desc string containers []docker.ContainerJSON expectedFrontends map[string]*types.Frontend expectedBackends map[string]*types.Backend }{ { + desc: "when no container", containers: []docker.ContainerJSON{}, expectedFrontends: map[string]*types.Frontend{}, expectedBackends: map[string]*types.Backend{}, }, { + desc: "simple configuration", containers: []docker.ContainerJSON{ containerJSON( name("foo"), labels(map[string]string{ - "traefik.service.port": "2503", - "traefik.service.frontend.entryPoints": "http,https", - "traefik.service.frontend.auth.basic": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", - "traefik.service.frontend.redirect.entryPoint": "https", + "traefik.service.port": "2503", + "traefik.service.frontend.entryPoints": "http,https", }), ports(nat.PortMap{ "80/tcp": {}, @@ -45,10 +48,7 @@ func TestDockerServiceBuildConfiguration(t *testing.T) { Backend: "backend-foo-foo-service", PassHostHeader: true, EntryPoints: []string{"http", "https"}, - BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, - Redirect: &types.Redirect{ - EntryPoint: "https", - }, + BasicAuth: []string{}, Routes: map[string]types.Route{ "service-service": { Rule: "Host:foo.docker.localhost", @@ -69,6 +69,178 @@ func TestDockerServiceBuildConfiguration(t *testing.T) { }, }, { + desc: "when all labels are set", + containers: []docker.ContainerJSON{ + containerJSON( + name("foo"), + labels(map[string]string{ + label.Prefix + "service." + label.SuffixPort: "666", + label.Prefix + "service." + label.SuffixProtocol: "https", + label.Prefix + "service." + label.SuffixWeight: "12", + + label.Prefix + "service." + label.SuffixFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.Prefix + "service." + label.SuffixFrontendEntryPoints: "http,https", + label.Prefix + "service." + label.SuffixFrontendPassHostHeader: "true", + label.Prefix + "service." + label.SuffixFrontendPassTLSCert: "true", + label.Prefix + "service." + label.SuffixFrontendPriority: "666", + label.Prefix + "service." + label.SuffixFrontendRedirectEntryPoint: "https", + label.Prefix + "service." + label.SuffixFrontendRedirectRegex: "nope", + label.Prefix + "service." + label.SuffixFrontendRedirectReplacement: "nope", + label.Prefix + "service." + label.SuffixFrontendWhitelistSourceRange: "10.10.10.10", + + label.Prefix + "service." + label.SuffixFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.Prefix + "service." + label.SuffixFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.Prefix + "service." + label.SuffixFrontendHeadersSSLProxyHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.Prefix + "service." + label.SuffixFrontendHeadersAllowedHosts: "foo,bar,bor", + label.Prefix + "service." + label.SuffixFrontendHeadersHostsProxyHeaders: "foo,bar,bor", + label.Prefix + "service." + label.SuffixFrontendHeadersSSLHost: "foo", + label.Prefix + "service." + label.SuffixFrontendHeadersCustomFrameOptionsValue: "foo", + label.Prefix + "service." + label.SuffixFrontendHeadersContentSecurityPolicy: "foo", + label.Prefix + "service." + label.SuffixFrontendHeadersPublicKey: "foo", + label.Prefix + "service." + label.SuffixFrontendHeadersReferrerPolicy: "foo", + label.Prefix + "service." + label.SuffixFrontendHeadersSTSSeconds: "666", + label.Prefix + "service." + label.SuffixFrontendHeadersSSLRedirect: "true", + label.Prefix + "service." + label.SuffixFrontendHeadersSSLTemporaryRedirect: "true", + label.Prefix + "service." + label.SuffixFrontendHeadersSTSIncludeSubdomains: "true", + label.Prefix + "service." + label.SuffixFrontendHeadersSTSPreload: "true", + label.Prefix + "service." + label.SuffixFrontendHeadersForceSTSHeader: "true", + label.Prefix + "service." + label.SuffixFrontendHeadersFrameDeny: "true", + label.Prefix + "service." + label.SuffixFrontendHeadersContentTypeNosniff: "true", + label.Prefix + "service." + label.SuffixFrontendHeadersBrowserXSSFilter: "true", + label.Prefix + "service." + label.SuffixFrontendHeadersIsDevelopment: "true", + + label.Prefix + "service." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: "404", + label.Prefix + "service." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: "foobar", + label.Prefix + "service." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: "foo_query", + label.Prefix + "service." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: "500,600", + label.Prefix + "service." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: "foobar", + label.Prefix + "service." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: "bar_query", + + label.Prefix + "service." + label.SuffixFrontendRateLimitExtractorFunc: "client.ip", + label.Prefix + "service." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", + label.Prefix + "service." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", + label.Prefix + "service." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", + label.Prefix + "service." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", + label.Prefix + "service." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", + label.Prefix + "service." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-foo-foo-service": { + Backend: "backend-foo-foo-service", + EntryPoints: []string{ + "http", + "https", + }, + 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: "", + }, + + Routes: map[string]types.Route{ + "service-service": { + Rule: "Host:foo.docker.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foo-foo-service": { + Servers: map[string]types.Server{ + "service-0": { + URL: "https://127.0.0.1:666", + Weight: 12, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "several containers", containers: []docker.ContainerJSON{ containerJSON( name("test1"), @@ -158,9 +330,9 @@ func TestDockerServiceBuildConfiguration(t *testing.T) { ExposedByDefault: true, } - for caseID, test := range testCases { + for _, test := range testCases { test := test - t.Run(strconv.Itoa(caseID), func(t *testing.T) { + t.Run(test.desc, func(t *testing.T) { t.Parallel() var dockerDataList []dockerData for _, container := range test.containers { @@ -177,77 +349,6 @@ func TestDockerServiceBuildConfiguration(t *testing.T) { } } -func TestDockerGetFuncMapLabel(t *testing.T) { - serviceName := "myservice" - fakeSuffix := "frontend.foo" - fakeLabel := label.Prefix + fakeSuffix - - testCases := []struct { - desc string - container docker.ContainerJSON - suffixLabel string - expectedKey string - expected map[string]string - }{ - { - desc: "fallback to container label value", - container: containerJSON(labels(map[string]string{ - fakeLabel: "X-Custom-Header: ContainerRequestHeader", - })), - suffixLabel: fakeSuffix, - expected: map[string]string{ - "X-Custom-Header": "ContainerRequestHeader", - }, - }, - { - desc: "use service label instead of container label", - container: containerJSON(labels(map[string]string{ - fakeLabel: "X-Custom-Header: ContainerRequestHeader", - label.GetServiceLabel(fakeLabel, serviceName): "X-Custom-Header: ServiceRequestHeader", - })), - suffixLabel: fakeSuffix, - expected: map[string]string{ - "X-Custom-Header": "ServiceRequestHeader", - }, - }, - { - desc: "use service label with an empty value instead of container label", - container: containerJSON(labels(map[string]string{ - fakeLabel: "X-Custom-Header: ContainerRequestHeader", - label.GetServiceLabel(fakeLabel, serviceName): "X-Custom-Header: ", - })), - suffixLabel: fakeSuffix, - expected: map[string]string{ - "X-Custom-Header": "", - }, - }, - { - desc: "multiple values", - container: containerJSON(labels(map[string]string{ - fakeLabel: "X-Custom-Header: MultiHeaders || Authorization: Basic YWRtaW46YWRtaW4=", - })), - suffixLabel: fakeSuffix, - expected: map[string]string{ - "X-Custom-Header": "MultiHeaders", - "Authorization": "Basic YWRtaW46YWRtaW4=", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - dData := parseContainer(test.container) - - values := getFuncServiceMapLabel(test.suffixLabel)(dData, serviceName) - - assert.EqualValues(t, test.expected, values) - }) - } -} - func TestDockerGetFuncServiceStringLabel(t *testing.T) { testCases := []struct { container docker.ContainerJSON @@ -337,6 +438,387 @@ func TestDockerGetFuncServiceSliceStringLabel(t *testing.T) { } } +func TestDockerGetServiceStringValue(t *testing.T) { + testCases := []struct { + desc string + container docker.ContainerJSON + serviceLabels map[string]string + labelSuffix string + defaultValue string + expected string + }{ + { + desc: "should use service label when label exists in service labels", + container: containerJSON( + name("test1"), + labels(map[string]string{ + "traefik.foo": "bir", + })), + serviceLabels: map[string]string{ + "foo": "bar", + }, + labelSuffix: "foo", + defaultValue: "fail", + expected: "bar", + }, + { + desc: "should use container label when label doesn't exist in service labels", + container: containerJSON( + name("test1"), + labels(map[string]string{ + "traefik.foo": "bir", + })), + serviceLabels: map[string]string{ + "fo": "bar", + }, + labelSuffix: "foo", + defaultValue: "fail", + expected: "bir", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := getServiceStringValue(dData, test.serviceLabels, test.labelSuffix, test.defaultValue) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestDockerHasStrictServiceLabel(t *testing.T) { + testCases := []struct { + desc string + serviceLabels map[string]string + labelSuffix string + expected bool + }{ + { + desc: "should return false when service don't have label", + serviceLabels: map[string]string{}, + labelSuffix: "", + expected: false, + }, + { + desc: "should return true when service have label", + serviceLabels: map[string]string{ + "foo": "bar", + }, + labelSuffix: "foo", + expected: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := hasStrictServiceLabel(test.serviceLabels, test.labelSuffix) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestDockerGetStrictServiceStringValue(t *testing.T) { + testCases := []struct { + desc string + serviceLabels map[string]string + labelSuffix string + defaultValue string + expected string + }{ + { + desc: "should return a string when the label exists", + serviceLabels: map[string]string{ + "foo": "bar", + }, + labelSuffix: "foo", + expected: "bar", + }, + { + desc: "should return a string when the label exists and value empty", + serviceLabels: map[string]string{ + "foo": "", + }, + labelSuffix: "foo", + defaultValue: "cube", + expected: "", + }, + { + desc: "should return the default value when the label doesn't exist", + serviceLabels: map[string]string{}, + labelSuffix: "foo", + defaultValue: "cube", + expected: "cube", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getStrictServiceStringValue(test.serviceLabels, test.labelSuffix, test.defaultValue) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestDockerGetServiceMapValue(t *testing.T) { + testCases := []struct { + desc string + container docker.ContainerJSON + serviceLabels map[string]string + serviceName string + labelSuffix string + expected map[string]string + }{ + { + desc: "should return when no labels", + container: containerJSON( + name("test1"), + labels(map[string]string{})), + serviceLabels: map[string]string{}, + serviceName: "soo", + labelSuffix: "foo", + expected: nil, + }, + { + desc: "should return a map when label exists", + container: containerJSON( + name("test1"), + labels(map[string]string{ + "traefik.foo": "bir:fii", + })), + serviceLabels: map[string]string{ + "foo": "bar:foo", + }, + serviceName: "soo", + labelSuffix: "foo", + expected: map[string]string{ + "Bar": "foo", + }, + }, + { + desc: "should return a map when label exists (fallback to container labels)", + container: containerJSON( + name("test1"), + labels(map[string]string{ + "traefik.foo": "bir:fii", + })), + serviceLabels: map[string]string{ + "fo": "bar:foo", + }, + serviceName: "soo", + labelSuffix: "foo", + expected: map[string]string{ + "Bir": "fii", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := getServiceMapValue(dData, test.serviceLabels, test.serviceName, test.labelSuffix) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestDockerGetServiceSliceValue(t *testing.T) { + testCases := []struct { + desc string + container docker.ContainerJSON + serviceLabels map[string]string + labelSuffix string + expected []string + }{ + { + desc: "should return nil when no label", + container: containerJSON( + name("test1"), + labels(map[string]string{})), + serviceLabels: map[string]string{}, + expected: nil, + }, + { + desc: "should return a slice when label", + container: containerJSON( + name("test1"), + labels(map[string]string{ + "traefik.foo": "bor, byr, ber", + })), + serviceLabels: map[string]string{ + "foo": "bar, bir, bur", + }, + labelSuffix: "foo", + expected: []string{"bar", "bir", "bur"}, + }, + { + desc: "should return a slice when label (fallback to container labels)", + container: containerJSON( + name("test1"), + labels(map[string]string{ + "traefik.foo": "bor, byr, ber", + })), + serviceLabels: map[string]string{ + "fo": "bar, bir, bur", + }, + labelSuffix: "foo", + expected: []string{"bor", "byr", "ber"}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := getServiceSliceValue(dData, test.serviceLabels, test.labelSuffix) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestDockerGetServiceBoolValue(t *testing.T) { + testCases := []struct { + desc string + container docker.ContainerJSON + serviceLabels map[string]string + labelSuffix string + defaultValue bool + expected bool + }{ + { + desc: "should return default value when no label", + container: containerJSON( + name("test1"), + labels(map[string]string{})), + serviceLabels: map[string]string{}, + labelSuffix: "foo", + defaultValue: true, + expected: true, + }, + { + desc: "should return a bool when label", + container: containerJSON( + name("test1"), + labels(map[string]string{ + "traefik.foo": "false", + })), + serviceLabels: map[string]string{ + "foo": "true", + }, + labelSuffix: "foo", + expected: true, + }, + { + desc: "should return a bool when label (fallback to container labels)", + container: containerJSON( + name("test1"), + labels(map[string]string{ + "traefik.foo": "true", + })), + serviceLabels: map[string]string{ + "fo": "false", + }, + labelSuffix: "foo", + expected: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := getServiceBoolValue(dData, test.serviceLabels, test.labelSuffix, test.defaultValue) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestDockerGetServiceInt64Value(t *testing.T) { + testCases := []struct { + desc string + container docker.ContainerJSON + serviceLabels map[string]string + labelSuffix string + defaultValue int64 + expected int64 + }{ + { + desc: "should return default value when no label", + container: containerJSON( + name("test1"), + labels(map[string]string{})), + serviceLabels: map[string]string{}, + labelSuffix: "foo", + defaultValue: 666, + expected: 666, + }, + { + desc: "should return a int64 when label", + container: containerJSON( + name("test1"), + labels(map[string]string{ + "traefik.foo": "20", + })), + serviceLabels: map[string]string{ + "foo": "10", + }, + labelSuffix: "foo", + expected: 10, + }, + { + desc: "should return a int64 when label (fallback to container labels)", + container: containerJSON( + name("test1"), + labels(map[string]string{ + "traefik.foo": "20", + })), + serviceLabels: map[string]string{ + "fo": "10", + }, + labelSuffix: "foo", + expected: 20, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := getServiceInt64Value(dData, test.serviceLabels, test.labelSuffix, test.defaultValue) + + assert.Equal(t, test.expected, actual) + }) + } +} + func TestDockerCheckPortLabels(t *testing.T) { testCases := []struct { container docker.ContainerJSON @@ -387,7 +869,7 @@ func TestDockerCheckPortLabels(t *testing.T) { } } -func TestDockerGetServiceBackend(t *testing.T) { +func TestDockerGetServiceBackendName(t *testing.T) { testCases := []struct { container docker.ContainerJSON expected string @@ -425,7 +907,7 @@ func TestDockerGetServiceBackend(t *testing.T) { t.Run(strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() dData := parseContainer(test.container) - actual := getServiceBackend(dData, "myservice") + actual := getServiceBackendName(dData, "myservice") if actual != test.expected { t.Errorf("expected %q, got %q", test.expected, actual) } @@ -507,7 +989,356 @@ func TestDockerGetServicePort(t *testing.T) { } } -func TestGetServiceErrorPages(t *testing.T) { +func TestDockerGetServiceRedirect(t *testing.T) { + service := "rubiks" + + testCases := []struct { + desc string + container docker.ContainerJSON + expected *types.Redirect + }{ + { + desc: "should return nil when no redirect labels", + container: containerJSON( + name("test1"), + labels(map[string]string{})), + expected: nil, + }, + { + desc: "should use only entry point tag when mix regex redirect and entry point redirect", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.Prefix + service + "." + label.SuffixFrontendRedirectEntryPoint: "https", + label.Prefix + service + "." + label.SuffixFrontendRedirectRegex: "(.*)", + label.Prefix + service + "." + label.SuffixFrontendRedirectReplacement: "$1", + }), + ), + expected: &types.Redirect{ + EntryPoint: "https", + }, + }, + { + desc: "should return a struct when entry point redirect label", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.Prefix + service + "." + label.SuffixFrontendRedirectEntryPoint: "https", + }), + ), + expected: &types.Redirect{ + EntryPoint: "https", + }, + }, + { + desc: "should return a struct when entry point redirect label (fallback to container labels)", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikFrontendRedirectEntryPoint: "https", + }), + ), + expected: &types.Redirect{ + EntryPoint: "https", + }, + }, + { + desc: "should return a struct when regex redirect labels", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.Prefix + service + "." + label.SuffixFrontendRedirectRegex: "(.*)", + label.Prefix + service + "." + label.SuffixFrontendRedirectReplacement: "$1", + }), + ), + expected: &types.Redirect{ + Regex: "(.*)", + Replacement: "$1", + }, + }, + { + desc: "should return a struct when regex redirect labels (fallback to container labels)", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikFrontendRedirectRegex: "(.*)", + label.TraefikFrontendRedirectReplacement: "$1", + }), + ), + expected: &types.Redirect{ + Regex: "(.*)", + Replacement: "$1", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := getServiceRedirect(dData, service) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestDockerGetServiceHeaders(t *testing.T) { + service := "rubiks" + + testCases := []struct { + desc string + container docker.ContainerJSON + expected *types.Headers + }{ + { + desc: "should return nil when no custom headers options are set", + container: containerJSON( + name("test1"), + labels(map[string]string{})), + expected: nil, + }, + { + desc: "should return a struct when all custom headers options are set", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.Prefix + service + "." + label.SuffixFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.Prefix + service + "." + label.SuffixFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.Prefix + service + "." + label.SuffixFrontendHeadersSSLProxyHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.Prefix + service + "." + label.SuffixFrontendHeadersAllowedHosts: "foo,bar,bor", + label.Prefix + service + "." + label.SuffixFrontendHeadersHostsProxyHeaders: "foo,bar,bor", + label.Prefix + service + "." + label.SuffixFrontendHeadersSSLHost: "foo", + label.Prefix + service + "." + label.SuffixFrontendHeadersCustomFrameOptionsValue: "foo", + label.Prefix + service + "." + label.SuffixFrontendHeadersContentSecurityPolicy: "foo", + label.Prefix + service + "." + label.SuffixFrontendHeadersPublicKey: "foo", + label.Prefix + service + "." + label.SuffixFrontendHeadersReferrerPolicy: "foo", + label.Prefix + service + "." + label.SuffixFrontendHeadersSTSSeconds: "666", + label.Prefix + service + "." + label.SuffixFrontendHeadersSSLRedirect: "true", + label.Prefix + service + "." + label.SuffixFrontendHeadersSSLTemporaryRedirect: "true", + label.Prefix + service + "." + label.SuffixFrontendHeadersSTSIncludeSubdomains: "true", + label.Prefix + service + "." + label.SuffixFrontendHeadersSTSPreload: "true", + label.Prefix + service + "." + label.SuffixFrontendHeadersForceSTSHeader: "true", + label.Prefix + service + "." + label.SuffixFrontendHeadersFrameDeny: "true", + label.Prefix + service + "." + label.SuffixFrontendHeadersContentTypeNosniff: "true", + label.Prefix + service + "." + label.SuffixFrontendHeadersBrowserXSSFilter: "true", + label.Prefix + service + "." + label.SuffixFrontendHeadersIsDevelopment: "true", + }), + ), + 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, + }, + }, + { + desc: "should return a struct when all custom headers options are set (fallback to container labels)", + container: containerJSON( + name("test1"), + 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", + }), + ), + 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() + + dData := parseContainer(test.container) + + actual := getServiceHeaders(dData, service) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestDockerGetServiceRateLimit(t *testing.T) { + service := "rubiks" + + testCases := []struct { + desc string + container docker.ContainerJSON + expected *types.RateLimit + }{ + { + desc: "should return nil when no rate limit labels", + container: containerJSON( + name("test1"), + labels(map[string]string{})), + expected: nil, + }, + { + desc: "should return a struct when rate limit labels are defined", + container: containerJSON( + name("test1"), + labels(map[string]string{ + label.Prefix + service + "." + label.SuffixFrontendRateLimitExtractorFunc: "client.ip", + label.Prefix + service + "." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", + label.Prefix + service + "." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", + label.Prefix + service + "." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", + label.Prefix + service + "." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", + label.Prefix + service + "." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", + label.Prefix + service + "." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", + })), + 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", + container: containerJSON( + name("test1"), + 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", + })), + expected: nil, + }, + { + desc: "should return a struct when rate limit labels are defined (fallback to container labels)", + container: containerJSON( + name("test1"), + 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", + })), + 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, + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := getServiceRateLimit(dData, service) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestDockerGetServiceErrorPages(t *testing.T) { service := "courgette" testCases := []struct { desc string diff --git a/provider/label/label.go b/provider/label/label.go index 3cd99c590..bdc82c1c0 100644 --- a/provider/label/label.go +++ b/provider/label/label.go @@ -19,11 +19,14 @@ const ( // Default values const ( - DefaultWeight = "0" + DefaultWeight = "0" // TODO [breaking] use int value + DefaultWeightInt = 0 // TODO rename to DefaultWeight DefaultProtocol = "http" - DefaultPassHostHeader = "true" + DefaultPassHostHeader = "true" // TODO [breaking] use bool value + DefaultPassHostHeaderBool = true // TODO rename to DefaultPassHostHeader DefaultPassTLSCert = false - DefaultFrontendPriority = "0" + DefaultFrontendPriority = "0" // TODO [breaking] int value + DefaultFrontendPriorityInt = 0 // TODO rename to DefaultFrontendPriority DefaultCircuitBreakerExpression = "NetworkErrorRatio() > 1" DefaultFrontendRedirectEntryPoint = "" DefaultBackendLoadBalancerMethod = "wrr" @@ -226,13 +229,24 @@ func HasPrefix(labels map[string]string, prefix string) bool { return false } +// FindServiceSubmatch split service label +func FindServiceSubmatch(name string) []string { + matches := ServicesPropertiesRegexp.FindStringSubmatch(name) + if matches == nil || + strings.HasPrefix(name, TraefikFrontend+".") || + strings.HasPrefix(name, TraefikBackend+".") { + return nil + } + return matches +} + // ExtractServiceProperties Extract services labels func ExtractServiceProperties(labels map[string]string) ServiceProperties { v := make(ServiceProperties) for name, value := range labels { - matches := ServicesPropertiesRegexp.FindStringSubmatch(name) - if matches == nil || strings.HasPrefix(name, TraefikFrontend) { + matches := FindServiceSubmatch(name) + if matches == nil { continue } diff --git a/provider/label/names.go b/provider/label/names.go index 1331b1f32..2f4f7899b 100644 --- a/provider/label/names.go +++ b/provider/label/names.go @@ -17,10 +17,11 @@ const ( SuffixBackendHealthCheckPath = "backend.healthcheck.path" SuffixBackendHealthCheckPort = "backend.healthcheck.port" SuffixBackendHealthCheckInterval = "backend.healthcheck.interval" - SuffixBackendLoadBalancerMethod = "backend.loadbalancer.method" - SuffixBackendLoadBalancerSticky = "backend.loadbalancer.sticky" - SuffixBackendLoadBalancerStickiness = "backend.loadbalancer.stickiness" - SuffixBackendLoadBalancerStickinessCookieName = "backend.loadbalancer.stickiness.cookieName" + SuffixBackendLoadBalancer = "backend.loadbalancer" + SuffixBackendLoadBalancerMethod = SuffixBackendLoadBalancer + ".method" + SuffixBackendLoadBalancerSticky = SuffixBackendLoadBalancer + ".sticky" + SuffixBackendLoadBalancerStickiness = SuffixBackendLoadBalancer + ".stickiness" + SuffixBackendLoadBalancerStickinessCookieName = SuffixBackendLoadBalancer + ".stickiness.cookieName" SuffixBackendMaxConnAmount = "backend.maxconn.amount" SuffixBackendMaxConnExtractorFunc = "backend.maxconn.extractorfunc" SuffixFrontend = "frontend" @@ -58,7 +59,6 @@ const ( SuffixFrontendRule = "frontend.rule" SuffixFrontendRuleType = "frontend.rule.type" SuffixFrontendWhitelistSourceRange = "frontend.whitelistSourceRange" - SuffixFrontendValue = "frontend.value" TraefikDomain = Prefix + SuffixDomain TraefikEnable = Prefix + SuffixEnable TraefikPort = Prefix + SuffixPort @@ -73,6 +73,7 @@ const ( TraefikBackendHealthCheckPath = Prefix + SuffixBackendHealthCheckPath TraefikBackendHealthCheckPort = Prefix + SuffixBackendHealthCheckPort TraefikBackendHealthCheckInterval = Prefix + SuffixBackendHealthCheckInterval + TraefikBackendLoadBalancer = Prefix + SuffixBackendLoadBalancer TraefikBackendLoadBalancerMethod = Prefix + SuffixBackendLoadBalancerMethod TraefikBackendLoadBalancerSticky = Prefix + SuffixBackendLoadBalancerSticky TraefikBackendLoadBalancerStickiness = Prefix + SuffixBackendLoadBalancerStickiness @@ -90,8 +91,7 @@ const ( TraefikFrontendRedirectRegex = Prefix + SuffixFrontendRedirectRegex TraefikFrontendRedirectReplacement = Prefix + SuffixFrontendRedirectReplacement TraefikFrontendRule = Prefix + SuffixFrontendRule - TraefikFrontendRuleType = Prefix + SuffixFrontendRuleType - TraefikFrontendValue = Prefix + SuffixFrontendValue + TraefikFrontendRuleType = Prefix + SuffixFrontendRuleType // k8s only TraefikFrontendWhitelistSourceRange = Prefix + SuffixFrontendWhitelistSourceRange TraefikFrontendHeaders = Prefix + SuffixFrontendHeaders TraefikFrontendRequestHeaders = Prefix + SuffixFrontendRequestHeaders diff --git a/templates/docker.tmpl b/templates/docker.tmpl index 86144341c..ad57bf229 100644 --- a/templates/docker.tmpl +++ b/templates/docker.tmpl @@ -2,267 +2,291 @@ [backends] {{range $backendName, $backend := .Backends}} - {{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}} - {{$servers := index $backendServers $backendName}} - {{range $serverName, $server := $servers}} - {{if hasServices $server}} - {{$services := getServiceNames $server}} - {{range $serviceIndex, $serviceName := $services}} - [backends.backend-{{getServiceBackend $server $serviceName}}.servers.service-{{$serverName}}] - url = "{{getServiceProtocol $server $serviceName}}://{{getIPAddress $server}}:{{getServicePort $server $serviceName}}" - weight = {{getServiceWeight $server $serviceName}} + {{ $servers := index $backendServers $backendName }} + {{range $serverName, $server := $servers }} + {{if hasServices $server }} + {{ $services := getServiceNames $server }} + {{range $serviceIndex, $serviceName := $services }} + [backends.backend-{{ getServiceBackendName $server $serviceName }}.servers.service-{{ $serverName }}] + url = "{{ getServiceProtocol $server $serviceName }}://{{ getIPAddress $server }}:{{ getServicePort $server $serviceName }}" + weight = {{ getServiceWeight $server $serviceName }} {{end}} {{else}} - [backends.backend-{{$backendName}}.servers.server-{{$server.Name | replace "/" "" | replace "." "-"}}] - url = "{{getProtocol $server}}://{{getIPAddress $server}}:{{getPort $server}}" - weight = {{getWeight $server}} + [backends.backend-{{ $backendName }}.servers.server-{{ $server.Name | replace "/" "" | replace "." "-" }}] + url = "{{ getProtocol $server }}://{{ getIPAddress $server }}:{{ getPort $server }}" + weight = {{ getWeight $server }} {{end}} {{end}} {{end}} [frontends] -{{range $frontend, $containers := .Frontends}} +{{range $frontendName, $containers := .Frontends }} {{$container := index $containers 0}} - {{if hasServices $container}} - {{$services := getServiceNames $container}} + {{if hasServices $container }} + {{ $services := getServiceNames $container }} - {{range $serviceIndex, $serviceName := $services}} - [frontends."frontend-{{getServiceBackend $container $serviceName}}"] - backend = "backend-{{getServiceBackend $container $serviceName}}" - priority = {{getServicePriority $container $serviceName}} - passHostHeader = {{getServicePassHostHeader $container $serviceName}} - passTLSCert = {{getServicePassTLSCert $container $serviceName}} + {{range $serviceIndex, $serviceName := $services }} + {{ $ServiceFrontendName := getServiceBackendName $container $serviceName }} - entryPoints = [{{range getServiceEntryPoints $container $serviceName}} + [frontends."frontend-{{ $ServiceFrontendName }}"] + backend = "backend-{{ $ServiceFrontendName }}" + priority = {{ getServicePriority $container $serviceName }} + passHostHeader = {{ getServicePassHostHeader $container $serviceName }} + passTLSCert = {{ getServicePassTLSCert $container $serviceName }} + + entryPoints = [{{range getServiceEntryPoints $container $serviceName }} "{{.}}", {{end}}] - {{if getServiceWhitelistSourceRange $container $serviceName}} - whitelistSourceRange = [{{range getServiceWhitelistSourceRange $container $serviceName}} + {{ $whitelistSourceRange := getServiceWhitelistSourceRange $container $serviceName }} + {{if $whitelistSourceRange }} + whitelistSourceRange = [{{range $whitelistSourceRange }} "{{.}}", {{end}}] {{end}} - basicAuth = [{{range getServiceBasicAuth $container $serviceName}} + basicAuth = [{{range getServiceBasicAuth $container $serviceName }} "{{.}}", {{end}}] - {{if hasServiceRedirect $container $serviceName}} - [frontends."frontend-{{getServiceBackend $container $serviceName}}".redirect] - entryPoint = "{{getServiceRedirectEntryPoint $container $serviceName}}" - regex = "{{getServiceRedirectRegex $container $serviceName}}" - replacement = "{{getServiceRedirectReplacement $container $serviceName}}" + {{ $redirect := getServiceRedirect $container $serviceName }} + {{if $redirect }} + [frontends."frontend-{{ $ServiceFrontendName }}".redirect] + entryPoint = "{{ $redirect.EntryPoint }}" + regex = "{{ $redirect.Regex }}" + replacement = "{{ $redirect.Replacement }}" {{end}} - {{ if hasServiceErrorPages $container $serviceName }} - [frontends."frontend-{{getServiceBackend $container $serviceName}}".errors] - {{ range $pageName, $page := getServiceErrorPages $container $serviceName }} - [frontends."frontend-{{getServiceBackend $container $serviceName}}".errors.{{$pageName}}] - status = [{{range $page.Status}} + {{ $errorPages := getServiceErrorPages $container $serviceName }} + {{if $errorPages }} + [frontends."frontend-{{ $ServiceFrontendName }}".errors] + {{ range $pageName, $page := $errorPages }} + [frontends."frontend-{{ $ServiceFrontendName }}".errors.{{ $pageName }}] + status = [{{range $page.Status }} "{{.}}", {{end}}] - backend = "{{$page.Backend}}" - query = "{{$page.Query}}" + backend = "{{ $page.Backend }}" + query = "{{ $page.Query }}" {{end}} {{end}} - {{ if hasServiceRateLimits $container $serviceName }} - [frontends."frontend-{{getServiceBackend $container $serviceName}}".rateLimit] - extractorFunc = "{{ getRateLimitsExtractorFunc $container $serviceName }}" - [frontends."frontend-{{getServiceBackend $container $serviceName}}".rateLimit.rateSet] - {{ range $limitName, $rateLimit := getServiceRateLimits $container $serviceName }} - [frontends."frontend-{{getServiceBackend $container $serviceName}}".rateLimit.rateSet.{{ $limitName }}] - period = "{{ $rateLimit.Period }}" - average = {{ $rateLimit.Average }} - burst = {{ $rateLimit.Burst }} + {{ $rateLimit := getServiceRateLimit $container $serviceName }} + {{if $rateLimit }} + [frontends."frontend-{{ $ServiceFrontendName }}".rateLimit] + extractorFunc = "{{ $rateLimit.ExtractorFunc }}" + [frontends."frontend-{{ $ServiceFrontendName }}".rateLimit.rateSet] + {{range $limitName, $limit := $rateLimit.RateSet }} + [frontends."frontend-{{ $ServiceFrontendName }}".rateLimit.rateSet.{{ $limitName }}] + period = "{{ $limit.Period }}" + average = {{ $limit.Average }} + burst = {{ $limit.Burst }} {{end}} {{end}} - [frontends."frontend-{{getServiceBackend $container $serviceName}}".routes."service-{{$serviceName | replace "/" "" | replace "." "-"}}"] - rule = "{{getServiceFrontendRule $container $serviceName}}" + {{ $headers := getServiceHeaders $container $serviceName }} + {{if $headers }} + [frontends."frontend-{{ $ServiceFrontendName }}".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 }} - {{if hasServiceRequestHeaders $container $serviceName}} - [frontends."frontend-{{getServiceBackend $container $serviceName}}".headers.customRequestHeaders] - {{range $k, $v := getServiceRequestHeaders $container $serviceName}} - {{$k}} = "{{$v}}" + {{if $headers.AllowedHosts }} + AllowedHosts = [{{range $headers.AllowedHosts }} + "{{.}}", + {{end}}] + {{end}} + + {{if $headers.HostsProxyHeaders }} + HostsProxyHeaders = [{{range $headers.HostsProxyHeaders }} + "{{.}}", + {{end}}] + {{end}} + + {{if $headers.CustomRequestHeaders }} + [frontends."frontend-{{ $ServiceFrontendName }}".headers.customRequestHeaders] + {{range $k, $v := $headers.CustomRequestHeaders }} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + + {{if $headers.CustomResponseHeaders }} + [frontends."frontend-{{ $ServiceFrontendName }}".headers.customResponseHeaders] + {{range $k, $v := $headers.CustomResponseHeaders }} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + + {{if $headers.SSLProxyHeaders }} + [frontends."frontend-{{ $ServiceFrontendName }}".headers.SSLProxyHeaders] + {{range $k, $v := $headers.SSLProxyHeaders }} + {{$k}} = "{{$v}}" + {{end}} {{end}} {{end}} - {{if hasServiceResponseHeaders $container $serviceName}} - [frontends."frontend-{{getServiceBackend $container $serviceName}}".headers.customResponseHeaders] - {{range $k, $v := getServiceResponseHeaders $container $serviceName}} - {{$k}} = "{{$v}}" - {{end}} - {{end}} + [frontends."frontend-{{ $ServiceFrontendName }}".routes."service-{{ $serviceName | replace "/" "" | replace "." "-" }}"] + rule = "{{ getServiceFrontendRule $container $serviceName }}" {{end}} ## end range services {{else}} - [frontends."frontend-{{$frontend}}"] - backend = "backend-{{getBackend $container}}" - priority = {{getPriority $container}} - passHostHeader = {{getPassHostHeader $container}} - passTLSCert = {{getPassTLSCert $container}} + [frontends."frontend-{{ $frontendName }}"] + backend = "backend-{{ getBackendName $container }}" + priority = {{ getPriority $container }} + passHostHeader = {{ getPassHostHeader $container }} + passTLSCert = {{ getPassTLSCert $container }} - entryPoints = [{{range getEntryPoints $container}} + entryPoints = [{{range getEntryPoints $container }} "{{.}}", {{end}}] - {{if getWhitelistSourceRange $container}} - whitelistSourceRange = [{{range getWhitelistSourceRange $container}} + {{ $whitelistSourceRange := getWhitelistSourceRange $container}} + {{if $whitelistSourceRange }} + whitelistSourceRange = [{{range $whitelistSourceRange }} "{{.}}", {{end}}] {{end}} - basicAuth = [{{range getBasicAuth $container}} + basicAuth = [{{range getBasicAuth $container }} "{{.}}", {{end}}] - {{if hasRedirect $container}} - [frontends."frontend-{{$frontend}}".redirect] - entryPoint = "{{getRedirectEntryPoint $container}}" - regex = "{{getRedirectRegex $container}}" - replacement = "{{getRedirectReplacement $container}}" + {{ $redirect := getRedirect $container }} + {{if $redirect }} + [frontends."frontend-{{ $frontendName }}".redirect] + entryPoint = "{{ $redirect.EntryPoint }}" + regex = "{{ $redirect.Regex }}" + replacement = "{{ $redirect.Replacement }}" {{end}} - {{ if hasErrorPages $container }} - [frontends."frontend-{{$frontend}}".errors] - {{ range $pageName, $page := getErrorPages $container }} - [frontends."frontend-{{$frontend}}".errors.{{ $pageName }}] - status = [{{range $page.Status}} + {{ $errorPages := getErrorPages $container }} + {{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 $container }} - [frontends."frontend-{{$frontend}}".rateLimit] - extractorFunc = "{{ getRateLimitsExtractorFunc $container }}" - [frontends."frontend-{{$frontend}}".rateLimit.rateSet] - {{ range $limitName, $rateLimit := getRateLimits $container }} - [frontends."frontend-{{$frontend}}".rateLimit.rateSet.{{ $limitName }}] - period = "{{ $rateLimit.Period }}" - average = {{ $rateLimit.Average }} - burst = {{ $rateLimit.Burst }} + {{ $rateLimit := getRateLimit $container }} + {{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 $container}} - [frontends."frontend-{{$frontend}}".headers] - {{if hasSSLRedirectHeaders $container}} - SSLRedirect = {{getSSLRedirectHeaders $container}} - {{end}} - {{if hasSSLTemporaryRedirectHeaders $container}} - SSLTemporaryRedirect = {{getSSLTemporaryRedirectHeaders $container}} - {{end}} - {{if hasSSLHostHeaders $container}} - SSLHost = "{{getSSLHostHeaders $container}}" - {{end}} - {{if hasSTSSecondsHeaders $container}} - STSSeconds = {{getSTSSecondsHeaders $container}} - {{end}} - {{if hasSTSIncludeSubdomainsHeaders $container}} - STSIncludeSubdomains = {{getSTSIncludeSubdomainsHeaders $container}} - {{end}} - {{if hasSTSPreloadHeaders $container}} - STSPreload = {{getSTSPreloadHeaders $container}} - {{end}} - {{if hasForceSTSHeaderHeaders $container}} - ForceSTSHeader = {{getForceSTSHeaderHeaders $container}} - {{end}} - {{if hasFrameDenyHeaders $container}} - FrameDeny = {{getFrameDenyHeaders $container}} - {{end}} - {{if hasCustomFrameOptionsValueHeaders $container}} - CustomFrameOptionsValue = "{{getCustomFrameOptionsValueHeaders $container}}" - {{end}} - {{if hasContentTypeNosniffHeaders $container}} - ContentTypeNosniff = {{getContentTypeNosniffHeaders $container}} - {{end}} - {{if hasBrowserXSSFilterHeaders $container}} - BrowserXSSFilter = {{getBrowserXSSFilterHeaders $container}} - {{end}} - {{if hasContentSecurityPolicyHeaders $container}} - ContentSecurityPolicy = "{{getContentSecurityPolicyHeaders $container}}" - {{end}} - {{if hasPublicKeyHeaders $container}} - PublicKey = "{{getPublicKeyHeaders $container}}" - {{end}} - {{if hasReferrerPolicyHeaders $container}} - ReferrerPolicy = "{{getReferrerPolicyHeaders $container}}" - {{end}} - {{if hasIsDevelopmentHeaders $container}} - IsDevelopment = {{getIsDevelopmentHeaders $container}} - {{end}} + {{ $headers := getHeaders $container }} + {{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 }} - {{if hasAllowedHostsHeaders $container}} - AllowedHosts = [{{range getAllowedHostsHeaders $container}} + {{if $headers.AllowedHosts }} + AllowedHosts = [{{range $headers.AllowedHosts }} "{{.}}", {{end}}] {{end}} - {{if hasHostsProxyHeaders $container}} - HostsProxyHeaders = [{{range getHostsProxyHeaders $container}} + {{if $headers.HostsProxyHeaders }} + HostsProxyHeaders = [{{range $headers.HostsProxyHeaders }} "{{.}}", {{end}}] {{end}} - {{if hasRequestHeaders $container}} - [frontends."frontend-{{$frontend}}".headers.customRequestHeaders] - {{range $k, $v := getRequestHeaders $container}} + {{if $headers.CustomRequestHeaders }} + [frontends."frontend-{{ $frontendName }}".headers.customRequestHeaders] + {{range $k, $v := $headers.CustomRequestHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} - {{if hasResponseHeaders $container}} - [frontends."frontend-{{$frontend}}".headers.customResponseHeaders] - {{range $k, $v := getResponseHeaders $container}} + {{if $headers.CustomResponseHeaders }} + [frontends."frontend-{{ $frontendName }}".headers.customResponseHeaders] + {{range $k, $v := $headers.CustomResponseHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} - {{if hasSSLProxyHeaders $container}} - [frontends."frontend-{{$frontend}}".headers.SSLProxyHeaders] - {{range $k, $v := getSSLProxyHeaders $container}} + {{if $headers.SSLProxyHeaders }} + [frontends."frontend-{{ $frontendName }}".headers.SSLProxyHeaders] + {{range $k, $v := $headers.SSLProxyHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} {{end}} - [frontends."frontend-{{$frontend}}".routes."route-frontend-{{$frontend}}"] - rule = "{{getFrontendRule $container}}" + [frontends."frontend-{{ $frontendName }}".routes."route-frontend-{{ $frontendName }}"] + rule = "{{ getFrontendRule $container }}" {{end}}