diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index 00e877b2e..e64ad0d94 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -1081,195 +1081,171 @@ func templatesKvTmpl() (*asset, error) { return a, nil } -var _templatesMarathonTmpl = []byte(`{{$apps := .Applications}} +var _templatesMarathonTmpl = []byte(`{{ $apps := .Applications }} [backends] -{{range $app := $apps}} -{{range $serviceIndex, $serviceName := getServiceNames $app}} +{{range $app := $apps }} +{{range $serviceIndex, $serviceName := getServiceNames $app }} + {{ $backendName := getBackend $app $serviceName}} - [backends."{{getBackend $app $serviceName }}"] + [backends."{{ $backendName }}"] - {{ if hasCircuitBreakerLabels $app }} - [backends."{{getBackend $app $serviceName }}".circuitBreaker] - expression = "{{getCircuitBreakerExpression $app }}" + {{ $circuitBreaker := getCircuitBreaker $app }} + {{if $circuitBreaker }} + [backends."{{ $backendName }}".circuitBreaker] + expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{ if hasLoadBalancerLabels $app }} - [backends."{{getBackend $app $serviceName }}".loadBalancer] - method = "{{getLoadBalancerMethod $app }}" - sticky = {{getSticky $app}} - {{if hasStickinessLabel $app}} - [backends."{{getBackend $app $serviceName }}".loadBalancer.stickiness] - cookieName = "{{getStickinessCookieName $app}}" + {{ $loadBalancer := getLoadBalancer $app }} + {{if $loadBalancer }} + [backends."{{ $backendName }}".loadBalancer] + method = "{{ $loadBalancer.Method }}" + sticky = {{ $loadBalancer.Sticky }} + {{if $loadBalancer.Stickiness }} + [backends."{{ $backendName }}".loadBalancer.stickiness] + cookieName = "{{ $loadBalancer.Stickiness.CookieName }}" {{end}} {{end}} - {{ if hasMaxConnLabels $app }} - [backends."{{getBackend $app $serviceName }}".maxConn] - amount = {{getMaxConnAmount $app }} - extractorFunc = "{{getMaxConnExtractorFunc $app }}" + {{ $maxConn := getMaxConn $app }} + {{if $maxConn }} + [backends."{{ $backendName }}".maxConn] + extractorFunc = "{{ $maxConn.ExtractorFunc }}" + amount = {{ $maxConn.Amount }} {{end}} - {{ if hasHealthCheckLabels $app }} - [backends."{{getBackend $app $serviceName }}".healthCheck] - path = "{{getHealthCheckPath $app }}" - port = {{getHealthCheckPort $app}} - interval = "{{getHealthCheckInterval $app }}" + {{ $healthCheck := getHealthCheck $app }} + {{if $healthCheck }} + [backends."{{ $backendName }}".healthCheck] + path = "{{ $healthCheck.Path }}" + port = {{ $healthCheck.Port }} + interval = "{{ $healthCheck.Interval }}" + {{end}} + + {{range $serverName, $server := getServers $app $serviceName }} + [backends."{{ $backendName }}".servers."{{ $serverName }}"] + url = "{{ $server.URL }}" + weight = {{ $server.Weight }} {{end}} {{end}} - - {{range $task := $app.Tasks}} - {{range $serviceIndex, $serviceName := getServiceNames $app}} - - [backends."{{getBackend $app $serviceName}}".servers."server-{{$task.ID | replace "." "-"}}{{getServiceNameSuffix $serviceName }}"] - url = "{{getProtocol $app $serviceName}}://{{getBackendServer $task $app}}:{{getPort $task $app $serviceName}}" - weight = {{getWeight $app $serviceName}} - - {{end}} - {{end}} - {{end}} [frontends] -{{range $app := $apps}} -{{range $serviceIndex, $serviceName := getServiceNames .}} +{{range $app := $apps }} +{{range $serviceIndex, $serviceName := getServiceNames $app }} + {{ $frontendName := getFrontendName $app $serviceName }} - [frontends."{{ getFrontendName $app $serviceName }}"] - backend = "{{getBackend $app $serviceName}}" - priority = {{getPriority $app $serviceName}} - passHostHeader = {{getPassHostHeader $app $serviceName}} - passTLSCert = {{getPassTLSCert $app $serviceName}} + [frontends."{{ $frontendName }}"] + backend = "{{ getBackend $app $serviceName }}" + priority = {{ getPriority $app $serviceName }} + passHostHeader = {{ getPassHostHeader $app $serviceName }} + passTLSCert = {{ getPassTLSCert $app $serviceName }} - entryPoints = [{{range getEntryPoints $app $serviceName}} + entryPoints = [{{range getEntryPoints $app $serviceName }} "{{.}}", {{end}}] - {{if getWhitelistSourceRange $app $serviceName}} - whitelistSourceRange = [{{range getWhitelistSourceRange $app $serviceName}} + {{ $whitelistSourceRange := getWhitelistSourceRange $app $serviceName }} + {{if $whitelistSourceRange }} + whitelistSourceRange = [{{range $whitelistSourceRange }} "{{.}}", {{end}}] {{end}} - basicAuth = [{{range getBasicAuth $app $serviceName}} + basicAuth = [{{range getBasicAuth $app $serviceName }} "{{.}}", {{end}}] - {{if hasRedirect $app $serviceName}} - [frontends."{{ getFrontendName $app $serviceName }}".redirect] - entryPoint = "{{getRedirectEntryPoint $app $serviceName}}" - regex = "{{getRedirectRegex $app $serviceName}}" - replacement = "{{getRedirectReplacement $app $serviceName}}" + {{ $redirect := getRedirect $app $serviceName }} + {{if $redirect }} + [frontends."{{ $frontendName }}".redirect] + entryPoint = "{{ $redirect.EntryPoint }}" + regex = "{{ $redirect.Regex }}" + replacement = "{{ $redirect.Replacement }}" {{end}} - {{ if hasErrorPages $app $serviceName }} - [frontends."{{ getFrontendName $app $serviceName }}".errors] - {{ range $pageName, $page := getErrorPages $app $serviceName }} - [frontends."{{ getFrontendName $app $serviceName }}".errors.{{ $pageName }}] - status = [{{range $page.Status}} - "{{.}}", - {{end}}] - backend = "{{$page.Backend}}" - query = "{{$page.Query}}" + {{ $errorPages := getErrorPages $app $serviceName }} + {{if $errorPages }} + [frontends."{{ $frontendName }}".errors] + {{range $pageName, $page := $errorPages }} + [frontends."{{ $frontendName }}".errors.{{ $pageName }}] + status = [{{range $page.Status }} + "{{.}}", + {{end}}] + backend = "{{ $page.Backend }}" + query = "{{ $page.Query }}" {{end}} {{end}} - {{ if hasRateLimits $app $serviceName }} - [frontends."{{ getFrontendName $app $serviceName }}".rateLimit] - extractorFunc = "{{ getRateLimitsExtractorFunc $app $serviceName }}" - [frontends."{{ getFrontendName $app $serviceName }}".rateLimit.rateSet] - {{ range $limitName, $rateLimit := getRateLimits $app $serviceName }} - [frontends."{{ getFrontendName $app $serviceName }}".rateLimit.rateSet.{{ $limitName }}] - period = "{{ $rateLimit.Period }}" - average = {{ $rateLimit.Average }} - burst = {{ $rateLimit.Burst }} + {{ $rateLimit := getRateLimit $app $serviceName }} + {{if $rateLimit }} + [frontends."{{ $frontendName }}".rateLimit] + extractorFunc = "{{ $rateLimit.ExtractorFunc }}" + [frontends."{{ $frontendName }}".rateLimit.rateSet] + {{ range $limitName, $limit := $rateLimit.RateSet }} + [frontends."{{ $frontendName }}".rateLimit.rateSet.{{ $limitName }}] + period = "{{ $limit.Period }}" + average = {{ $limit.Average }} + burst = {{ $limit.Burst }} {{end}} {{end}} - {{if hasHeaders $app $serviceName }} - [frontends."{{ getFrontendName $app $serviceName }}".headers] - {{if hasSSLRedirectHeaders $app $serviceName}} - SSLRedirect = {{getSSLRedirectHeaders $app $serviceName}} - {{end}} - {{if hasSSLTemporaryRedirectHeaders $app $serviceName}} - SSLTemporaryRedirect = {{getSSLTemporaryRedirectHeaders $app $serviceName}} - {{end}} - {{if hasSSLHostHeaders $app $serviceName}} - SSLHost = "{{getSSLHostHeaders $app $serviceName}}" - {{end}} - {{if hasSTSSecondsHeaders $app $serviceName}} - STSSeconds = {{getSTSSecondsHeaders $app $serviceName}} - {{end}} - {{if hasSTSIncludeSubdomainsHeaders $app $serviceName}} - STSIncludeSubdomains = {{getSTSIncludeSubdomainsHeaders $app $serviceName}} - {{end}} - {{if hasSTSPreloadHeaders $app $serviceName}} - STSPreload = {{getSTSPreloadHeaders $app $serviceName}} - {{end}} - {{if hasForceSTSHeaderHeaders $app $serviceName}} - ForceSTSHeader = {{getForceSTSHeaderHeaders $app $serviceName}} - {{end}} - {{if hasFrameDenyHeaders $app $serviceName}} - FrameDeny = {{getFrameDenyHeaders $app $serviceName}} - {{end}} - {{if hasCustomFrameOptionsValueHeaders $app $serviceName}} - CustomFrameOptionsValue = "{{getCustomFrameOptionsValueHeaders $app $serviceName}}" - {{end}} - {{if hasContentTypeNosniffHeaders $app $serviceName}} - ContentTypeNosniff = {{getContentTypeNosniffHeaders $app $serviceName}} - {{end}} - {{if hasBrowserXSSFilterHeaders $app $serviceName}} - BrowserXSSFilter = {{getBrowserXSSFilterHeaders $app $serviceName}} - {{end}} - {{if hasContentSecurityPolicyHeaders $app $serviceName}} - ContentSecurityPolicy = "{{getContentSecurityPolicyHeaders $app $serviceName}}" - {{end}} - {{if hasPublicKeyHeaders $app $serviceName}} - PublicKey = "{{getPublicKeyHeaders $app $serviceName}}" - {{end}} - {{if hasReferrerPolicyHeaders $app $serviceName}} - ReferrerPolicy = "{{getReferrerPolicyHeaders $app $serviceName}}" - {{end}} - {{if hasIsDevelopmentHeaders $app $serviceName}} - IsDevelopment = {{getIsDevelopmentHeaders $app $serviceName}} - {{end}} + {{ $headers := getHeaders $app $serviceName }} + {{if $headers }} + [frontends."{{ $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 $app $serviceName}} - AllowedHosts = [{{range getAllowedHostsHeaders $app $serviceName}} - "{{.}}", - {{end}}] - {{end}} + {{if $headers.AllowedHosts }} + AllowedHosts = [{{range $headers.AllowedHosts }} + "{{.}}", + {{end}}] + {{end}} - {{if hasHostsProxyHeaders $app $serviceName}} - HostsProxyHeaders = [{{range getHostsProxyHeaders $app $serviceName}} - "{{.}}", - {{end}}] - {{end}} + {{if $headers.HostsProxyHeaders }} + HostsProxyHeaders = [{{range $headers.HostsProxyHeaders }} + "{{.}}", + {{end}}] + {{end}} - {{if hasRequestHeaders $app $serviceName}} - [frontends."{{ getFrontendName $app $serviceName }}".headers.customRequestHeaders] - {{range $k, $v := getRequestHeaders $app $serviceName}} - {{$k}} = "{{$v}}" + {{if $headers.CustomRequestHeaders }} + [frontends."{{ $frontendName }}".headers.customRequestHeaders] + {{range $k, $v := $headers.CustomRequestHeaders }} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + + {{if $headers.CustomResponseHeaders }} + [frontends."{{ $frontendName }}".headers.customResponseHeaders] + {{range $k, $v := $headers.CustomResponseHeaders }} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + + {{if $headers.SSLProxyHeaders }} + [frontends."{{ $frontendName }}".headers.SSLProxyHeaders] + {{range $k, $v := $headers.SSLProxyHeaders }} + {{$k}} = "{{$v}}" + {{end}} {{end}} {{end}} - {{if hasResponseHeaders $app $serviceName}} - [frontends."{{ getFrontendName $app $serviceName }}".headers.customResponseHeaders] - {{range $k, $v := getResponseHeaders $app $serviceName}} - {{$k}} = "{{$v}}" - {{end}} - {{end}} - - {{if hasSSLProxyHeaders $app $serviceName}} - [frontends."{{ getFrontendName $app $serviceName }}".headers.SSLProxyHeaders] - {{range $k, $v := getSSLProxyHeaders $app $serviceName}} - {{$k}} = "{{$v}}" - {{end}} - {{end}} - {{end}} - - [frontends."{{ getFrontendName $app $serviceName }}".routes."route-host{{$app.ID | replace "/" "-"}}{{getServiceNameSuffix $serviceName }}"] - rule = "{{getFrontendRule $app $serviceName}}" + [frontends."{{ $frontendName }}".routes."route-host{{ $app.ID | replace "/" "-" }}{{ getServiceNameSuffix $serviceName }}"] + rule = "{{ getFrontendRule $app $serviceName }}" {{end}} {{end}} diff --git a/provider/label/label.go b/provider/label/label.go index bdc82c1c0..771c50899 100644 --- a/provider/label/label.go +++ b/provider/label/label.go @@ -93,6 +93,14 @@ func GetBoolValue(labels map[string]string, labelName string, defaultValue bool) return defaultValue } +// GetBoolValueP get bool value associated to a label +func GetBoolValueP(labels *map[string]string, labelName string, defaultValue bool) bool { + if labels == nil { + return defaultValue + } + return GetBoolValue(*labels, labelName, defaultValue) +} + // GetIntValue get int value associated to a label func GetIntValue(labels map[string]string, labelName string, defaultValue int) int { if rawValue, ok := labels[labelName]; ok { @@ -229,6 +237,14 @@ func HasPrefix(labels map[string]string, prefix string) bool { return false } +// HasPrefixP Check if a value is associated to a less one label with a prefix +func HasPrefixP(labels *map[string]string, prefix string) bool { + if labels == nil { + return false + } + return HasPrefix(*labels, prefix) +} + // FindServiceSubmatch split service label func FindServiceSubmatch(name string) []string { matches := ServicesPropertiesRegexp.FindStringSubmatch(name) diff --git a/provider/marathon/builder_test.go b/provider/marathon/builder_test.go index 0f821e46b..6d9a34157 100644 --- a/provider/marathon/builder_test.go +++ b/provider/marathon/builder_test.go @@ -50,7 +50,7 @@ func constraint(value string) func(*marathon.Application) { } } -func labelWithService(key, value string, serviceName string) func(*marathon.Application) { +func withServiceLabel(key, value string, serviceName string) func(*marathon.Application) { if len(serviceName) == 0 { panic("serviceName can not be empty") } @@ -121,20 +121,35 @@ func readinessCheckResult(taskID string, ready bool) func(*marathon.Application) } } +func withTasks(tasks ...marathon.Task) func(*marathon.Application) { + return func(application *marathon.Application) { + for _, task := range tasks { + tu := task + application.Tasks = append(application.Tasks, &tu) + } + } +} + // Functions related to building tasks. func task(ops ...func(*marathon.Task)) marathon.Task { - t := marathon.Task{ + t := &marathon.Task{ ID: testTaskName, // The vast majority of tests expect the task state to be TASK_RUNNING. State: string(taskStateRunning), } for _, op := range ops { - op(&t) + op(t) } - return t + return *t +} + +func withTaskID(id string) func(*marathon.Task) { + return func(task *marathon.Task) { + task.ID = id + } } func localhostTask(ops ...func(*marathon.Task)) marathon.Task { diff --git a/provider/marathon/config.go b/provider/marathon/config.go index 7fb5ffc80..873516f8a 100644 --- a/provider/marathon/config.go +++ b/provider/marathon/config.go @@ -17,6 +17,8 @@ import ( "github.com/gambol99/go-marathon" ) +const defaultService = "" + func (p *Provider) buildConfiguration() *types.Configuration { var MarathonFuncMap = template.FuncMap{ "getBackend": p.getBackend, @@ -24,87 +26,60 @@ func (p *Provider) buildConfiguration() *types.Configuration { "getSubDomain": p.getSubDomain, // see https://github.com/containous/traefik/pull/1693 // Backend functions - "getBackendServer": p.getBackendServer, - "getPort": getPort, - "getWeight": getFuncStringService(label.SuffixWeight, label.DefaultWeight), - "getProtocol": getFuncStringService(label.SuffixProtocol, label.DefaultProtocol), - "hasCircuitBreakerLabels": hasFunc(label.TraefikBackendCircuitBreakerExpression), + "getBackendServer": p.getBackendServer, + "getPort": getPort, + "getCircuitBreaker": getCircuitBreaker, + "getLoadBalancer": getLoadBalancer, + "getMaxConn": getMaxConn, + "getHealthCheck": getHealthCheck, + "getServers": p.getServers, + + // TODO Deprecated [breaking] + "getWeight": getFuncIntService(label.SuffixWeight, label.DefaultWeightInt), + // TODO Deprecated [breaking] + "getProtocol": getFuncStringService(label.SuffixProtocol, label.DefaultProtocol), + // TODO Deprecated [breaking] + "hasCircuitBreakerLabels": hasFunc(label.TraefikBackendCircuitBreakerExpression), + // TODO Deprecated [breaking] "getCircuitBreakerExpression": getFuncString(label.TraefikBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression), - "hasLoadBalancerLabels": hasLoadBalancerLabels, - "getLoadBalancerMethod": getFuncString(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), - "getSticky": getSticky, - "hasStickinessLabel": hasFunc(label.TraefikBackendLoadBalancerStickiness), - "getStickinessCookieName": getFuncString(label.TraefikBackendLoadBalancerStickinessCookieName, ""), - "hasMaxConnLabels": hasMaxConnLabels, - "getMaxConnExtractorFunc": getFuncString(label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc), - "getMaxConnAmount": getFuncInt64(label.TraefikBackendMaxConnAmount, math.MaxInt64), - "hasHealthCheckLabels": hasFunc(label.TraefikBackendHealthCheckPath), - "getHealthCheckPath": getFuncString(label.TraefikBackendHealthCheckPath, ""), - "getHealthCheckPort": getFuncInt(label.TraefikBackendHealthCheckPort, label.DefaultBackendHealthCheckPort), - "getHealthCheckInterval": getFuncString(label.TraefikBackendHealthCheckInterval, ""), + // TODO Deprecated [breaking] + "hasLoadBalancerLabels": hasLoadBalancerLabels, + // TODO Deprecated [breaking] + "getLoadBalancerMethod": getFuncString(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), + // TODO Deprecated [breaking] + "getSticky": getSticky, + // TODO Deprecated [breaking] + "hasStickinessLabel": hasFunc(label.TraefikBackendLoadBalancerStickiness), + // TODO Deprecated [breaking] + "getStickinessCookieName": getFuncString(label.TraefikBackendLoadBalancerStickinessCookieName, ""), + // TODO Deprecated [breaking] + "hasMaxConnLabels": hasMaxConnLabels, + // TODO Deprecated [breaking] + "getMaxConnExtractorFunc": getFuncString(label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc), + // TODO Deprecated [breaking] + "getMaxConnAmount": getFuncInt64(label.TraefikBackendMaxConnAmount, math.MaxInt64), + // TODO Deprecated [breaking] + "hasHealthCheckLabels": hasFunc(label.TraefikBackendHealthCheckPath), + // TODO Deprecated [breaking] + "getHealthCheckPath": getFuncString(label.TraefikBackendHealthCheckPath, ""), + // TODO Deprecated [breaking] + "getHealthCheckInterval": getFuncString(label.TraefikBackendHealthCheckInterval, ""), // Frontend functions - "getPassHostHeader": getFuncStringService(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeader), - "getPassTLSCert": getFuncBoolService(label.SuffixFrontendPassTLSCert, label.DefaultPassTLSCert), - "getPriority": getFuncStringService(label.SuffixFrontendPriority, label.DefaultFrontendPriority), - "getEntryPoints": getFuncSliceStringService(label.SuffixFrontendEntryPoints), - "getFrontendRule": p.getFrontendRule, - "getFrontendName": p.getFrontendName, - "getBasicAuth": getFuncSliceStringService(label.SuffixFrontendAuthBasic), - "getServiceNames": getServiceNames, - "getServiceNameSuffix": getServiceNameSuffix, - "getWhitelistSourceRange": getFuncSliceStringService(label.SuffixFrontendWhitelistSourceRange), - "hasRedirect": hasRedirect, - "getRedirectEntryPoint": getFuncStringService(label.SuffixFrontendRedirectEntryPoint, label.DefaultFrontendRedirectEntryPoint), - "getRedirectRegex": getFuncStringService(label.SuffixFrontendRedirectRegex, ""), - "getRedirectReplacement": getFuncStringService(label.SuffixFrontendRedirectReplacement, ""), - "hasErrorPages": hasPrefixFuncService(label.BaseFrontendErrorPage), - "getErrorPages": getErrorPages, - "hasRateLimits": hasFuncService(label.SuffixFrontendRateLimitExtractorFunc), - "getRateLimitsExtractorFunc": getFuncStringService(label.SuffixFrontendRateLimitExtractorFunc, ""), - "getRateLimits": getRateLimits, - // Headers - "hasHeaders": hasPrefixFuncService(label.TraefikFrontendHeaders), - "hasRequestHeaders": hasFuncService(label.SuffixFrontendRequestHeaders), - "getRequestHeaders": getFuncMapService(label.SuffixFrontendRequestHeaders), - "hasResponseHeaders": hasFuncService(label.SuffixFrontendResponseHeaders), - "getResponseHeaders": getFuncMapService(label.SuffixFrontendResponseHeaders), - "hasAllowedHostsHeaders": hasFuncService(label.SuffixFrontendHeadersAllowedHosts), - "getAllowedHostsHeaders": getFuncSliceStringService(label.SuffixFrontendHeadersAllowedHosts), - "hasHostsProxyHeaders": hasFuncService(label.SuffixFrontendHeadersHostsProxyHeaders), - "getHostsProxyHeaders": getFuncSliceStringService(label.SuffixFrontendHeadersHostsProxyHeaders), - "hasSSLRedirectHeaders": hasFuncService(label.SuffixFrontendHeadersSSLRedirect), - "getSSLRedirectHeaders": getFuncBoolService(label.SuffixFrontendHeadersSSLRedirect, false), - "hasSSLTemporaryRedirectHeaders": hasFuncService(label.SuffixFrontendHeadersSSLTemporaryRedirect), - "getSSLTemporaryRedirectHeaders": getFuncBoolService(label.SuffixFrontendHeadersSSLTemporaryRedirect, false), - "hasSSLHostHeaders": hasFuncService(label.SuffixFrontendHeadersSSLHost), - "getSSLHostHeaders": getFuncStringService(label.SuffixFrontendHeadersSSLHost, ""), - "hasSSLProxyHeaders": hasFuncService(label.SuffixFrontendHeadersSSLProxyHeaders), - "getSSLProxyHeaders": getFuncMapService(label.SuffixFrontendHeadersSSLProxyHeaders), - "hasSTSSecondsHeaders": hasFuncService(label.SuffixFrontendHeadersSTSSeconds), - "getSTSSecondsHeaders": getFuncInt64Service(label.SuffixFrontendHeadersSTSSeconds, 0), - "hasSTSIncludeSubdomainsHeaders": hasFuncService(label.SuffixFrontendHeadersSTSIncludeSubdomains), - "getSTSIncludeSubdomainsHeaders": getFuncBoolService(label.SuffixFrontendHeadersSTSIncludeSubdomains, false), - "hasSTSPreloadHeaders": hasFuncService(label.SuffixFrontendHeadersSTSPreload), - "getSTSPreloadHeaders": getFuncBoolService(label.SuffixFrontendHeadersSTSPreload, false), - "hasForceSTSHeaderHeaders": hasFuncService(label.SuffixFrontendHeadersForceSTSHeader), - "getForceSTSHeaderHeaders": getFuncBoolService(label.SuffixFrontendHeadersForceSTSHeader, false), - "hasFrameDenyHeaders": hasFuncService(label.SuffixFrontendHeadersFrameDeny), - "getFrameDenyHeaders": getFuncBoolService(label.SuffixFrontendHeadersFrameDeny, false), - "hasCustomFrameOptionsValueHeaders": hasFuncService(label.SuffixFrontendHeadersCustomFrameOptionsValue), - "getCustomFrameOptionsValueHeaders": getFuncStringService(label.SuffixFrontendHeadersCustomFrameOptionsValue, ""), - "hasContentTypeNosniffHeaders": hasFuncService(label.SuffixFrontendHeadersContentTypeNosniff), - "getContentTypeNosniffHeaders": getFuncBoolService(label.SuffixFrontendHeadersContentTypeNosniff, false), - "hasBrowserXSSFilterHeaders": hasFuncService(label.SuffixFrontendHeadersBrowserXSSFilter), - "getBrowserXSSFilterHeaders": getFuncBoolService(label.SuffixFrontendHeadersBrowserXSSFilter, false), - "hasContentSecurityPolicyHeaders": hasFuncService(label.SuffixFrontendHeadersContentSecurityPolicy), - "getContentSecurityPolicyHeaders": getFuncStringService(label.SuffixFrontendHeadersContentSecurityPolicy, ""), - "hasPublicKeyHeaders": hasFuncService(label.SuffixFrontendHeadersPublicKey), - "getPublicKeyHeaders": getFuncStringService(label.SuffixFrontendHeadersPublicKey, ""), - "hasReferrerPolicyHeaders": hasFuncService(label.SuffixFrontendHeadersReferrerPolicy), - "getReferrerPolicyHeaders": getFuncStringService(label.SuffixFrontendHeadersReferrerPolicy, ""), - "hasIsDevelopmentHeaders": hasFuncService(label.SuffixFrontendHeadersIsDevelopment), - "getIsDevelopmentHeaders": getFuncBoolService(label.SuffixFrontendHeadersIsDevelopment, false), + "getServiceNames": getServiceNames, + "getServiceNameSuffix": getServiceNameSuffix, + "getPassHostHeader": getFuncBoolService(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getPassTLSCert": getFuncBoolService(label.SuffixFrontendPassTLSCert, label.DefaultPassTLSCert), + "getPriority": getFuncIntService(label.SuffixFrontendPriority, label.DefaultFrontendPriorityInt), + "getEntryPoints": getFuncSliceStringService(label.SuffixFrontendEntryPoints), + "getFrontendRule": p.getFrontendRule, + "getFrontendName": p.getFrontendName, + "getBasicAuth": getFuncSliceStringService(label.SuffixFrontendAuthBasic), + "getWhitelistSourceRange": getFuncSliceStringService(label.SuffixFrontendWhitelistSourceRange), + "getRedirect": getRedirect, + "getErrorPages": getErrorPages, + "getRateLimit": getRateLimit, + "getHeaders": getHeaders, } v := url.Values{} @@ -224,7 +199,7 @@ func (p *Provider) getBackend(application marathon.Application, serviceName stri lblBackend := getLabelName(serviceName, label.SuffixBackend) value := label.GetStringValue(labels, lblBackend, "") if len(value) > 0 { - return "backend" + value + return provider.Normalize("backend" + value) } return provider.Normalize("backend" + application.ID + getServiceNameSuffix(serviceName)) } @@ -295,7 +270,7 @@ func getServiceNames(application marathon.Application) []string { // An empty name "" will be added if no service specific properties exist, // as an indication that there are no sub-services, but only main application if len(names) == 0 { - names = append(names, "") + names = append(names, defaultService) } return names } @@ -328,6 +303,7 @@ func logIllegalServices(task marathon.Task, application marathon.Application) { } } +// Deprecated func hasLoadBalancerLabels(application marathon.Application) bool { method := label.HasP(application.Labels, label.TraefikBackendLoadBalancerMethod) sticky := label.HasP(application.Labels, label.TraefikBackendLoadBalancerSticky) @@ -335,6 +311,7 @@ func hasLoadBalancerLabels(application marathon.Application) bool { return method || sticky || stickiness } +// Deprecated func hasMaxConnLabels(application marathon.Application) bool { mca := label.HasP(application.Labels, label.TraefikBackendMaxConnAmount) mcef := label.HasP(application.Labels, label.TraefikBackendMaxConnExtractorFunc) @@ -344,11 +321,11 @@ func hasMaxConnLabels(application marathon.Application) bool { // TODO: Deprecated // replaced by Stickiness // Deprecated -func getSticky(application marathon.Application) string { +func getSticky(application marathon.Application) bool { if label.HasP(application.Labels, label.TraefikBackendLoadBalancerSticky) { log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) } - return label.GetStringValueP(application.Labels, label.TraefikBackendLoadBalancerSticky, "false") + return label.GetBoolValueP(application.Labels, label.TraefikBackendLoadBalancerSticky, false) } func getPort(task marathon.Task, application marathon.Application, serviceName string) string { @@ -420,14 +397,109 @@ func retrieveAvailablePorts(application marathon.Application, task marathon.Task return []int{} } -func hasRedirect(application marathon.Application, serviceName string) bool { +func getCircuitBreaker(application marathon.Application) *types.CircuitBreaker { + circuitBreaker := label.GetStringValueP(application.Labels, label.TraefikBackendCircuitBreakerExpression, "") + if len(circuitBreaker) == 0 { + return nil + } + return &types.CircuitBreaker{Expression: circuitBreaker} +} + +func getLoadBalancer(application marathon.Application) *types.LoadBalancer { + if !label.HasPrefixP(application.Labels, label.TraefikBackendLoadBalancer) { + return nil + } + + method := label.GetStringValueP(application.Labels, label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod) + + lb := &types.LoadBalancer{ + Method: method, + Sticky: getSticky(application), + } + + if label.GetBoolValueP(application.Labels, label.TraefikBackendLoadBalancerStickiness, false) { + cookieName := label.GetStringValueP(application.Labels, label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName) + lb.Stickiness = &types.Stickiness{CookieName: cookieName} + } + + return lb +} + +func getMaxConn(application marathon.Application) *types.MaxConn { + amount := label.GetInt64ValueP(application.Labels, label.TraefikBackendMaxConnAmount, math.MinInt64) + extractorFunc := label.GetStringValueP(application.Labels, label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc) + + if amount == math.MinInt64 || len(extractorFunc) == 0 { + return nil + } + + return &types.MaxConn{ + Amount: amount, + ExtractorFunc: extractorFunc, + } +} + +func getHealthCheck(application marathon.Application) *types.HealthCheck { + path := label.GetStringValueP(application.Labels, label.TraefikBackendHealthCheckPath, "") + if len(path) == 0 { + return nil + } + + port := label.GetIntValueP(application.Labels, label.TraefikBackendHealthCheckPort, label.DefaultBackendHealthCheckPort) + interval := label.GetStringValueP(application.Labels, label.TraefikBackendHealthCheckInterval, "") + + return &types.HealthCheck{ + Path: path, + Port: port, + Interval: interval, + } +} + +func (p *Provider) getServers(application marathon.Application, serviceName string) map[string]types.Server { + var servers map[string]types.Server + + for _, task := range application.Tasks { + host := p.getBackendServer(*task, application) + if len(host) == 0 { + continue + } + + if servers == nil { + servers = make(map[string]types.Server) + } + + labels := getLabels(application, serviceName) + + port := getPort(*task, application, serviceName) + protocol := label.GetStringValue(labels, getLabelName(serviceName, label.SuffixProtocol), label.DefaultProtocol) + + serverName := provider.Normalize("server-" + task.ID + getServiceNameSuffix(serviceName)) + servers[serverName] = types.Server{ + URL: fmt.Sprintf("%s://%s:%v", protocol, host, port), + Weight: label.GetIntValue(labels, getLabelName(serviceName, label.SuffixWeight), label.DefaultWeightInt), + } + } + + return servers +} + +func getRedirect(application marathon.Application, serviceName string) *types.Redirect { labels := getLabels(application, serviceName) - frep := label.Has(labels, getLabelName(serviceName, label.SuffixFrontendRedirectEntryPoint)) - frrg := label.Has(labels, getLabelName(serviceName, label.SuffixFrontendRedirectRegex)) - frrp := label.Has(labels, getLabelName(serviceName, label.SuffixFrontendRedirectReplacement)) + if label.Has(labels, getLabelName(serviceName, label.SuffixFrontendRedirectEntryPoint)) { + return &types.Redirect{ + EntryPoint: label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendRedirectEntryPoint), ""), + } + } + if label.Has(labels, getLabelName(serviceName, label.SuffixFrontendRedirectRegex)) && + label.Has(labels, getLabelName(serviceName, label.SuffixFrontendRedirectReplacement)) { + return &types.Redirect{ + Regex: label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendRedirectRegex), ""), + Replacement: label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendRedirectReplacement), ""), + } + } - return frep || frrg && frrp + return nil } func getErrorPages(application marathon.Application, serviceName string) map[string]*types.ErrorPage { @@ -440,14 +512,65 @@ func getErrorPages(application marathon.Application, serviceName string) map[str return label.ParseErrorPages(labels, prefix, label.RegexpFrontendErrorPage) } -func getRateLimits(application marathon.Application, serviceName string) map[string]*types.Rate { +func getRateLimit(application marathon.Application, serviceName string) *types.RateLimit { labels := getLabels(application, serviceName) - prefix := getLabelName(serviceName, label.BaseFrontendRateLimit) + + extractorFunc := label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendRateLimitExtractorFunc), "") + if len(extractorFunc) == 0 { + return nil + } + + limits := getRateSet(labels, serviceName) + if len(limits) == 0 { + return nil + } + + return &types.RateLimit{ + ExtractorFunc: extractorFunc, + RateSet: limits, + } +} + +func getRateSet(labels map[string]string, serviceName string) map[string]*types.Rate { + rateSetPrefix := getLabelName(serviceName, label.BaseFrontendRateLimit) if len(serviceName) > 0 { - return label.ParseRateSets(labels, prefix, label.RegexpBaseFrontendRateLimit) + return label.ParseRateSets(labels, rateSetPrefix, label.RegexpBaseFrontendRateLimit) } - return label.ParseRateSets(labels, prefix, label.RegexpFrontendRateLimit) + return label.ParseRateSets(labels, rateSetPrefix, label.RegexpFrontendRateLimit) +} + +func getHeaders(application marathon.Application, serviceName string) *types.Headers { + labels := getLabels(application, serviceName) + + headers := &types.Headers{ + CustomRequestHeaders: label.GetMapValue(labels, getLabelName(serviceName, label.SuffixFrontendRequestHeaders)), + CustomResponseHeaders: label.GetMapValue(labels, getLabelName(serviceName, label.SuffixFrontendResponseHeaders)), + SSLProxyHeaders: label.GetMapValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersSSLProxyHeaders)), + AllowedHosts: label.GetSliceStringValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersAllowedHosts)), + HostsProxyHeaders: label.GetSliceStringValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersHostsProxyHeaders)), + STSSeconds: label.GetInt64Value(labels, getLabelName(serviceName, label.SuffixFrontendHeadersSTSSeconds), 0), + SSLRedirect: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersSSLRedirect), false), + SSLTemporaryRedirect: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersSSLTemporaryRedirect), false), + STSIncludeSubdomains: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersSTSIncludeSubdomains), false), + STSPreload: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersSTSPreload), false), + ForceSTSHeader: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersForceSTSHeader), false), + FrameDeny: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersFrameDeny), false), + ContentTypeNosniff: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersContentTypeNosniff), false), + BrowserXSSFilter: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersBrowserXSSFilter), false), + IsDevelopment: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersIsDevelopment), false), + SSLHost: label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersSSLHost), ""), + CustomFrameOptionsValue: label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersCustomFrameOptionsValue), ""), + ContentSecurityPolicy: label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersContentSecurityPolicy), ""), + PublicKey: label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersPublicKey), ""), + ReferrerPolicy: label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersReferrerPolicy), ""), + } + + if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() { + return nil + } + + return headers } // Label functions @@ -477,25 +600,6 @@ func hasFunc(labelName string) func(application marathon.Application) bool { } } -func hasFuncService(labelName string) func(application marathon.Application, serviceName string) bool { - return func(application marathon.Application, serviceName string) bool { - labels := getLabels(application, serviceName) - lbName := getLabelName(serviceName, labelName) - - value, ok := labels[lbName] - return ok && len(value) > 0 - } -} - -func hasPrefixFuncService(prefix string) func(application marathon.Application, serviceName string) bool { - return func(application marathon.Application, serviceName string) bool { - labels := getLabels(application, serviceName) - lbName := getLabelName(serviceName, prefix) - - return label.HasPrefix(labels, lbName) - } -} - func getFuncStringService(labelName string, defaultValue string) func(application marathon.Application, serviceName string) string { return func(application marathon.Application, serviceName string) string { labels := getLabels(application, serviceName) @@ -512,11 +616,11 @@ func getFuncBoolService(labelName string, defaultValue bool) func(application ma } } -func getFuncInt64Service(labelName string, defaultValue int64) func(application marathon.Application, serviceName string) int64 { - return func(application marathon.Application, serviceName string) int64 { +func getFuncIntService(labelName string, defaultValue int) func(application marathon.Application, serviceName string) int { + return func(application marathon.Application, serviceName string) int { labels := getLabels(application, serviceName) lbName := getLabelName(serviceName, labelName) - return label.GetInt64Value(labels, lbName, defaultValue) + return label.GetIntValue(labels, lbName, defaultValue) } } @@ -527,13 +631,6 @@ func getFuncSliceStringService(labelName string) func(application marathon.Appli } } -func getFuncMapService(labelName string) func(application marathon.Application, serviceName string) map[string]string { - return func(application marathon.Application, serviceName string) map[string]string { - labels := getLabels(application, serviceName) - return label.GetMapValue(labels, getLabelName(serviceName, labelName)) - } -} - func getFuncString(labelName string, defaultValue string) func(application marathon.Application) string { return func(application marathon.Application) string { return label.GetStringValueP(application.Labels, labelName, defaultValue) @@ -545,9 +642,3 @@ func getFuncInt64(labelName string, defaultValue int64) func(application maratho return label.GetInt64ValueP(application.Labels, labelName, defaultValue) } } - -func getFuncInt(labelName string, defaultValue int) func(application marathon.Application) int { - return func(application marathon.Application) int { - return label.GetIntValueP(application.Labels, labelName, defaultValue) - } -} diff --git a/provider/marathon/config_test.go b/provider/marathon/config_test.go index 7347c8ea8..c08f2ea89 100644 --- a/provider/marathon/config_test.go +++ b/provider/marathon/config_test.go @@ -30,20 +30,23 @@ func newFakeClient(applicationsError bool, applications marathon.Applications) * return fakeClient } -func TestLoadConfigAPIErrors(t *testing.T) { +func TestBuildConfigurationAPIErrors(t *testing.T) { fakeClient := newFakeClient(true, marathon.Applications{}) - provider := &Provider{ + + p := &Provider{ marathonClient: fakeClient, } - actualConfig := provider.buildConfiguration() + + actualConfig := p.buildConfiguration() fakeClient.AssertExpectations(t) + if actualConfig != nil { t.Errorf("configuration should have been nil, got %v", actualConfig) } } -func TestLoadConfigNonAPIErrors(t *testing.T) { - cases := []struct { +func TestBuildConfigurationNonAPIErrors(t *testing.T) { + testCases := []struct { desc string application marathon.Application task marathon.Task @@ -62,6 +65,9 @@ func TestLoadConfigNonAPIErrors(t *testing.T) { Rule: "Host:app.docker.localhost", }, }, + PassHostHeader: true, + BasicAuth: []string{}, + EntryPoints: []string{}, }, }, expectedBackends: map[string]*types.Backend{ @@ -91,109 +97,15 @@ func TestLoadConfigNonAPIErrors(t *testing.T) { Rule: "Host:app.docker.localhost", }, }, + PassHostHeader: true, + BasicAuth: []string{}, + EntryPoints: []string{}, }, }, expectedBackends: map[string]*types.Backend{ "backend-app": {}, }, }, - { - desc: "load balancer / circuit breaker labels", - application: application( - appPorts(80), - withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), - withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), - ), - task: localhostTask(taskPorts(80)), - expectedFrontends: map[string]*types.Frontend{ - "frontend-app": { - Backend: "backend-app", - Routes: map[string]types.Route{ - "route-host-app": { - Rule: "Host:app.docker.localhost", - }, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-app": { - Servers: map[string]types.Server{ - "server-task": { - URL: "http://localhost:80", - Weight: 0, - }, - }, - CircuitBreaker: &types.CircuitBreaker{ - Expression: "NetworkErrorRatio() > 0.5", - }, - LoadBalancer: &types.LoadBalancer{ - Method: "drr", - }, - }, - }, - }, - { - desc: "general max connection labels", - application: application( - appPorts(80), - withLabel(label.TraefikBackendMaxConnAmount, "1000"), - withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), - ), - task: localhostTask(taskPorts(80)), - expectedFrontends: map[string]*types.Frontend{ - "frontend-app": { - Backend: "backend-app", - Routes: map[string]types.Route{ - "route-host-app": { - Rule: "Host:app.docker.localhost", - }, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-app": { - Servers: map[string]types.Server{ - "server-task": { - URL: "http://localhost:80", - Weight: 0, - }, - }, - MaxConn: &types.MaxConn{ - Amount: 1000, - ExtractorFunc: "client.ip", - }, - }, - }, - }, - { - desc: "max connection amount label only", - application: application( - appPorts(80), - withLabel(label.TraefikBackendMaxConnAmount, "1000"), - ), - task: localhostTask(taskPorts(80)), - expectedFrontends: map[string]*types.Frontend{ - "frontend-app": { - Backend: "backend-app", - Routes: map[string]types.Route{ - "route-host-app": { - Rule: "Host:app.docker.localhost", - }, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-app": { - Servers: map[string]types.Server{ - "server-task": { - URL: "http://localhost:80", - Weight: 0, - }, - }, - MaxConn: nil, - }, - }, - }, { desc: "max connection extractor function label only", application: application( @@ -209,6 +121,9 @@ func TestLoadConfigNonAPIErrors(t *testing.T) { Rule: "Host:app.docker.localhost", }, }, + PassHostHeader: true, + BasicAuth: []string{}, + EntryPoints: []string{}, }, }, expectedBackends: map[string]*types.Backend{ @@ -223,42 +138,6 @@ func TestLoadConfigNonAPIErrors(t *testing.T) { }, }, }, - { - desc: "health check labels", - application: application( - appPorts(80), - withLabel(label.TraefikBackendHealthCheckPath, "/path"), - withLabel(label.TraefikBackendHealthCheckInterval, "5m"), - ), - task: task( - host("127.0.0.1"), - taskPorts(80), - ), - expectedFrontends: map[string]*types.Frontend{ - "frontend-app": { - Backend: "backend-app", - Routes: map[string]types.Route{ - "route-host-app": { - Rule: "Host:app.docker.localhost", - }, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-app": { - Servers: map[string]types.Server{ - "server-task": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - }, - HealthCheck: &types.HealthCheck{ - Path: "/path", - Interval: "5m", - }, - }, - }, - }, { desc: "multiple ports", application: application( @@ -275,6 +154,9 @@ func TestLoadConfigNonAPIErrors(t *testing.T) { Rule: "Host:app.docker.localhost", }, }, + PassHostHeader: true, + BasicAuth: []string{}, + EntryPoints: []string{}, }, }, expectedBackends: map[string]*types.Backend{ @@ -288,17 +170,258 @@ func TestLoadConfigNonAPIErrors(t *testing.T) { }, }, }, + { + desc: "with all labels", + application: application( + appPorts(80), + withLabel(label.TraefikPort, "666"), + withLabel(label.TraefikProtocol, "https"), + withLabel(label.TraefikWeight, "12"), + + withLabel(label.TraefikBackend, "foobar"), + + withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), + withLabel(label.TraefikBackendHealthCheckPath, "/health"), + withLabel(label.TraefikBackendHealthCheckPort, "880"), + withLabel(label.TraefikBackendHealthCheckInterval, "6"), + withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), + withLabel(label.TraefikBackendLoadBalancerSticky, "true"), + withLabel(label.TraefikBackendLoadBalancerStickiness, "true"), + withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "chocolate"), + withLabel(label.TraefikBackendMaxConnAmount, "666"), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + + withLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + withLabel(label.TraefikFrontendEntryPoints, "http,https"), + withLabel(label.TraefikFrontendPassHostHeader, "true"), + withLabel(label.TraefikFrontendPassTLSCert, "true"), + withLabel(label.TraefikFrontendPriority, "666"), + withLabel(label.TraefikFrontendRedirectEntryPoint, "https"), + withLabel(label.TraefikFrontendRedirectRegex, "nope"), + withLabel(label.TraefikFrontendRedirectReplacement, "nope"), + withLabel(label.TraefikFrontendRule, "Host:traefik.io"), + withLabel(label.TraefikFrontendWhitelistSourceRange, "10.10.10.10"), + + withLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + withLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + withLabel(label.TraefikFrontendSSLProxyHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + withLabel(label.TraefikFrontendAllowedHosts, "foo,bar,bor"), + withLabel(label.TraefikFrontendHostsProxyHeaders, "foo,bar,bor"), + withLabel(label.TraefikFrontendSSLHost, "foo"), + withLabel(label.TraefikFrontendCustomFrameOptionsValue, "foo"), + withLabel(label.TraefikFrontendContentSecurityPolicy, "foo"), + withLabel(label.TraefikFrontendPublicKey, "foo"), + withLabel(label.TraefikFrontendReferrerPolicy, "foo"), + withLabel(label.TraefikFrontendSTSSeconds, "666"), + withLabel(label.TraefikFrontendSSLRedirect, "true"), + withLabel(label.TraefikFrontendSSLTemporaryRedirect, "true"), + withLabel(label.TraefikFrontendSTSIncludeSubdomains, "true"), + withLabel(label.TraefikFrontendSTSPreload, "true"), + withLabel(label.TraefikFrontendForceSTSHeader, "true"), + withLabel(label.TraefikFrontendFrameDeny, "true"), + withLabel(label.TraefikFrontendContentTypeNosniff, "true"), + withLabel(label.TraefikFrontendBrowserXSSFilter, "true"), + withLabel(label.TraefikFrontendIsDevelopment, "true"), + + withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageStatus, "404"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageBackend, "foobar"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageQuery, "foo_query"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageStatus, "500,600"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageBackend, "foobar"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageQuery, "bar_query"), + + withLabel(label.TraefikFrontendRateLimitExtractorFunc, "client.ip"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), + ), + task: task( + host("127.0.0.1"), + taskPorts(80), + ), + expectedFrontends: map[string]*types.Frontend{ + "frontend-app": { + EntryPoints: []string{ + "http", + "https", + }, + Backend: "backendfoobar", + Routes: map[string]types.Route{ + "route-host-app": { + 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{ + "bar": { + Status: []string{ + "500", + "600", + }, + Backend: "foobar", + Query: "bar_query", + }, + "foo": { + Status: []string{ + "404", + }, + Backend: "foobar", + Query: "foo_query", + }, + }, + RateLimit: &types.RateLimit{ + RateSet: map[string]*types.Rate{ + "bar": { + Period: flaeg.Duration(3 * time.Second), + Average: 6, + Burst: 9, + }, + "foo": { + Period: flaeg.Duration(6 * time.Second), + Average: 12, + Burst: 18, + }, + }, + ExtractorFunc: "client.ip", + }, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backendfoobar": { + Servers: map[string]types.Server{ + "server-task": { + 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 _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + test.application.ID = "/app" + test.task.ID = "task" + if test.task.State == "" { + test.task.State = "TASK_RUNNING" + } + test.application.Tasks = []*marathon.Task{&test.task} + + fakeClient := newFakeClient(false, + marathon.Applications{Apps: []marathon.Application{test.application}}) + + p := &Provider{ + Domain: "docker.localhost", + ExposedByDefault: true, + marathonClient: fakeClient, + } + + actualConfig := p.buildConfiguration() + fakeClient.AssertExpectations(t) + + assert.NotNil(t, actualConfig) + assert.Equal(t, test.expectedBackends, actualConfig.Backends) + assert.Equal(t, test.expectedFrontends, actualConfig.Frontends) + }) + } +} + +func TestBuildConfigurationServicesNonAPIErrors(t *testing.T) { + testCases := []struct { + desc string + application marathon.Application + task marathon.Task + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{ { desc: "multiple ports with services", application: application( appPorts(80, 81), withLabel(label.TraefikBackendMaxConnAmount, "1000"), withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), - withLabel("traefik.web.port", "80"), - withLabel("traefik.admin.port", "81"), + withServiceLabel(label.TraefikPort, "80", "web"), + withServiceLabel(label.TraefikPort, "81", "admin"), withLabel("traefik..port", "82"), // This should be ignored, as it fails to match the servicesPropertiesRegexp regex. - withLabel("traefik.web.frontend.rule", "Host:web.app.docker.localhost"), - withLabel("traefik.admin.frontend.rule", "Host:admin.app.docker.localhost"), + withServiceLabel(label.TraefikFrontendRule, "Host:web.app.docker.localhost", "web"), + withServiceLabel(label.TraefikFrontendRule, "Host:admin.app.docker.localhost", "admin"), ), task: localhostTask( taskPorts(80, 81), @@ -311,6 +434,9 @@ func TestLoadConfigNonAPIErrors(t *testing.T) { Rule: "Host:web.app.docker.localhost", }, }, + PassHostHeader: true, + BasicAuth: []string{}, + EntryPoints: []string{}, }, "frontend-app-service-admin": { Backend: "backend-app-service-admin", @@ -319,6 +445,9 @@ func TestLoadConfigNonAPIErrors(t *testing.T) { Rule: "Host:admin.app.docker.localhost", }, }, + PassHostHeader: true, + BasicAuth: []string{}, + EntryPoints: []string{}, }, }, expectedBackends: map[string]*types.Backend{ @@ -348,47 +477,241 @@ func TestLoadConfigNonAPIErrors(t *testing.T) { }, }, }, + { + desc: "when all labels are set", + application: application( + appPorts(80, 81), + + //withLabel(label.TraefikBackend, "foobar"), + + withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), + withLabel(label.TraefikBackendHealthCheckPath, "/health"), + withLabel(label.TraefikBackendHealthCheckPort, "880"), + withLabel(label.TraefikBackendHealthCheckInterval, "6"), + withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), + withLabel(label.TraefikBackendLoadBalancerSticky, "true"), + withLabel(label.TraefikBackendLoadBalancerStickiness, "true"), + withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "chocolate"), + withLabel(label.TraefikBackendMaxConnAmount, "666"), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + + withServiceLabel(label.TraefikPort, "80", "containous"), + withServiceLabel(label.TraefikProtocol, "https", "containous"), + withServiceLabel(label.TraefikWeight, "12", "containous"), + + withServiceLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "containous"), + withServiceLabel(label.TraefikFrontendEntryPoints, "http,https", "containous"), + withServiceLabel(label.TraefikFrontendPassHostHeader, "true", "containous"), + withServiceLabel(label.TraefikFrontendPassTLSCert, "true", "containous"), + withServiceLabel(label.TraefikFrontendPriority, "666", "containous"), + withServiceLabel(label.TraefikFrontendRedirectEntryPoint, "https", "containous"), + withServiceLabel(label.TraefikFrontendRedirectRegex, "nope", "containous"), + withServiceLabel(label.TraefikFrontendRedirectReplacement, "nope", "containous"), + withServiceLabel(label.TraefikFrontendRule, "Host:traefik.io", "containous"), + withServiceLabel(label.TraefikFrontendWhitelistSourceRange, "10.10.10.10", "containous"), + + withServiceLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", "containous"), + withServiceLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", "containous"), + withServiceLabel(label.TraefikFrontendSSLProxyHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", "containous"), + withServiceLabel(label.TraefikFrontendAllowedHosts, "foo,bar,bor", "containous"), + withServiceLabel(label.TraefikFrontendHostsProxyHeaders, "foo,bar,bor", "containous"), + withServiceLabel(label.TraefikFrontendSSLHost, "foo", "containous"), + withServiceLabel(label.TraefikFrontendCustomFrameOptionsValue, "foo", "containous"), + withServiceLabel(label.TraefikFrontendContentSecurityPolicy, "foo", "containous"), + withServiceLabel(label.TraefikFrontendPublicKey, "foo", "containous"), + withServiceLabel(label.TraefikFrontendReferrerPolicy, "foo", "containous"), + withServiceLabel(label.TraefikFrontendSTSSeconds, "666", "containous"), + withServiceLabel(label.TraefikFrontendSSLRedirect, "true", "containous"), + withServiceLabel(label.TraefikFrontendSSLTemporaryRedirect, "true", "containous"), + withServiceLabel(label.TraefikFrontendSTSIncludeSubdomains, "true", "containous"), + withServiceLabel(label.TraefikFrontendSTSPreload, "true", "containous"), + withServiceLabel(label.TraefikFrontendForceSTSHeader, "true", "containous"), + withServiceLabel(label.TraefikFrontendFrameDeny, "true", "containous"), + withServiceLabel(label.TraefikFrontendContentTypeNosniff, "true", "containous"), + withServiceLabel(label.TraefikFrontendBrowserXSSFilter, "true", "containous"), + withServiceLabel(label.TraefikFrontendIsDevelopment, "true", "containous"), + + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageStatus, "404"), + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageBackend, "foobar"), + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageQuery, "foo_query"), + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageStatus, "500,600"), + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageBackend, "foobar"), + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageQuery, "bar_query"), + + withServiceLabel(label.TraefikFrontendRateLimitExtractorFunc, "client.ip", "containous"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), + ), + task: localhostTask( + taskPorts(80, 81), + ), + expectedFrontends: map[string]*types.Frontend{ + "frontend-app-service-containous": { + EntryPoints: []string{ + "http", + "https", + }, + Backend: "backend-app-service-containous", + Routes: map[string]types.Route{ + "route-host-app-service-containous": { + 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{ + "bar": { + Status: []string{ + "500", + "600", + }, + Backend: "foobar", + Query: "bar_query", + }, + "foo": { + Status: []string{ + "404", + }, + Backend: "foobar", + Query: "foo_query", + }, + }, + RateLimit: &types.RateLimit{ + RateSet: map[string]*types.Rate{ + "bar": { + Period: flaeg.Duration(3 * time.Second), + Average: 6, + Burst: 9, + }, + "foo": { + Period: flaeg.Duration(6 * time.Second), + Average: 12, + Burst: 18, + }, + }, + ExtractorFunc: "client.ip", + }, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-app-service-containous": { + Servers: map[string]types.Server{ + "server-task-service-containous": { + URL: "https://localhost:80", + 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 _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { t.Parallel() - c.application.ID = "/app" - c.task.ID = "task" - if c.task.State == "" { - c.task.State = "TASK_RUNNING" - } - c.application.Tasks = []*marathon.Task{&c.task} - - for _, frontend := range c.expectedFrontends { - frontend.PassHostHeader = true - frontend.BasicAuth = []string{} - frontend.EntryPoints = []string{} + test.application.ID = "/app" + test.task.ID = "task" + if test.task.State == "" { + test.task.State = "TASK_RUNNING" } + test.application.Tasks = []*marathon.Task{&test.task} fakeClient := newFakeClient(false, - marathon.Applications{Apps: []marathon.Application{c.application}}) - provider := &Provider{ + marathon.Applications{Apps: []marathon.Application{test.application}}) + + p := &Provider{ Domain: "docker.localhost", ExposedByDefault: true, marathonClient: fakeClient, } - actualConfig := provider.buildConfiguration() + + actualConfig := p.buildConfiguration() fakeClient.AssertExpectations(t) - expectedConfig := &types.Configuration{ - Backends: c.expectedBackends, - Frontends: c.expectedFrontends, - } - assert.Equal(t, expectedConfig, actualConfig) + assert.NotNil(t, actualConfig) + assert.Equal(t, test.expectedBackends, actualConfig.Backends) + assert.Equal(t, test.expectedFrontends, actualConfig.Frontends) }) } } func TestApplicationFilterConstraints(t *testing.T) { - cases := []struct { + testCases := []struct { desc string application marathon.Application marathonLBCompatibility bool @@ -439,30 +762,34 @@ func TestApplicationFilterConstraints(t *testing.T) { }, } - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { t.Parallel() - provider := &Provider{ + + p := &Provider{ ExposedByDefault: true, - MarathonLBCompatibility: c.marathonLBCompatibility, - FilterMarathonConstraints: c.filterMarathonConstraints, + MarathonLBCompatibility: test.marathonLBCompatibility, + FilterMarathonConstraints: test.filterMarathonConstraints, } + constraint, err := types.NewConstraint("tag==valid") if err != nil { - panic(fmt.Sprintf("failed to create constraint 'tag==valid': %s", err)) + t.Fatalf("failed to create constraint 'tag==valid': %v", err) } - provider.Constraints = types.Constraints{constraint} - actual := provider.applicationFilter(c.application) - if actual != c.expected { - t.Errorf("got %v, expected %v", actual, c.expected) + p.Constraints = types.Constraints{constraint} + + actual := p.applicationFilter(test.application) + + if actual != test.expected { + t.Errorf("got %v, expected %v", actual, test.expected) } }) } } func TestApplicationFilterEnabled(t *testing.T) { - cases := []struct { + testCases := []struct { desc string exposedByDefault bool enabledLabel string @@ -506,21 +833,24 @@ func TestApplicationFilterEnabled(t *testing.T) { }, } - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { t.Parallel() - provider := &Provider{ExposedByDefault: c.exposedByDefault} - app := application(withLabel(label.TraefikEnable, c.enabledLabel)) - if provider.applicationFilter(app) != c.expected { - t.Errorf("got unexpected filtering = %t", !c.expected) + + provider := &Provider{ExposedByDefault: test.exposedByDefault} + + app := application(withLabel(label.TraefikEnable, test.enabledLabel)) + + if provider.applicationFilter(app) != test.expected { + t.Errorf("got unexpected filtering = %t", !test.expected) } }) } } func TestTaskFilter(t *testing.T) { - cases := []struct { + testCases := []struct { desc string task marathon.Task application marathon.Application @@ -563,8 +893,8 @@ func TestTaskFilter(t *testing.T) { task: task(taskPorts(80, 81)), application: application( appPorts(80, 81), - labelWithService(label.TraefikPort, "80", "web"), - labelWithService(label.TraefikPort, "illegal", "admin"), + withServiceLabel(label.TraefikPort, "80", "web"), + withServiceLabel(label.TraefikPort, "illegal", "admin"), ), expected: true, }, @@ -573,7 +903,7 @@ func TestTaskFilter(t *testing.T) { task: task(taskPorts(80, 81)), application: application( appPorts(80, 81), - labelWithService(label.TraefikPort, "81", "admin"), + withServiceLabel(label.TraefikPort, "81", "admin"), ), expected: true, }, @@ -636,21 +966,54 @@ func TestTaskFilter(t *testing.T) { }, } - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { t.Parallel() - provider := &Provider{readyChecker: c.readyChecker} - actual := provider.taskFilter(c.task, c.application) - if actual != c.expected { - t.Errorf("actual %v, expected %v", actual, c.expected) - } + + p := &Provider{readyChecker: test.readyChecker} + + actual := p.taskFilter(test.task, test.application) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetSubDomain(t *testing.T) { + testCases := []struct { + path string + expected string + groupAsSubDomain bool + }{ + {"/test", "test", false}, + {"/test", "test", true}, + {"/a/b/c/d", "d.c.b.a", true}, + {"/b/a/d/c", "c.d.a.b", true}, + {"/d/c/b/a", "a.b.c.d", true}, + {"/c/d/a/b", "b.a.d.c", true}, + {"/a/b/c/d", "a-b-c-d", false}, + {"/b/a/d/c", "b-a-d-c", false}, + {"/d/c/b/a", "d-c-b-a", false}, + {"/c/d/a/b", "c-d-a-b", false}, + } + + for _, test := range testCases { + test := test + t.Run(fmt.Sprintf("path=%s,group=%t", test.path, test.groupAsSubDomain), func(t *testing.T) { + t.Parallel() + + p := &Provider{GroupsAsSubDomains: test.groupAsSubDomain} + + actual := p.getSubDomain(test.path) + + assert.Equal(t, test.expected, actual) }) } } func TestGetPort(t *testing.T) { - cases := []struct { + testCases := []struct { desc string application marathon.Application task marathon.Task @@ -757,50 +1120,20 @@ func TestGetPort(t *testing.T) { }, } - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { - t.Parallel() - actual := getPort(c.task, c.application, c.serviceName) - if actual != c.expected { - t.Errorf("actual %q, expected %q", actual, c.expected) - } - }) - } -} - -func TestGetSticky(t *testing.T) { - testCases := []struct { - desc string - application marathon.Application - expected string - }{ - { - desc: "label missing", - application: application(), - expected: "false", - }, - { - desc: "label existing", - application: application(withLabel(label.TraefikBackendLoadBalancerSticky, "true")), - expected: "true", - }, - } - for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := getSticky(test.application) - if actual != test.expected { - t.Errorf("actual %q, expected %q", actual, test.expected) - } + + actual := getPort(test.task, test.application, test.serviceName) + + assert.Equal(t, test.expected, actual) }) } } func TestGetFrontendRule(t *testing.T) { - cases := []struct { + testCases := []struct { desc string application marathon.Application serviceName string @@ -840,31 +1173,31 @@ func TestGetFrontendRule(t *testing.T) { }, { desc: "service label existing", - application: application(labelWithService(label.TraefikFrontendRule, "Host:foo.bar", "app")), + application: application(withServiceLabel(label.TraefikFrontendRule, "Host:foo.bar", "app")), serviceName: "app", marathonLBCompatibility: true, expected: "Host:foo.bar", }, } - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { t.Parallel() - prov := &Provider{ + p := &Provider{ Domain: "docker.localhost", - MarathonLBCompatibility: c.marathonLBCompatibility, - } - actual := prov.getFrontendRule(c.application, c.serviceName) - if actual != c.expected { - t.Errorf("actual %q, expected %q", actual, c.expected) + MarathonLBCompatibility: test.marathonLBCompatibility, } + + actual := p.getFrontendRule(test.application, test.serviceName) + + assert.Equal(t, test.expected, actual) }) } } func TestGetBackend(t *testing.T) { - cases := []struct { + testCases := []struct { desc string application marathon.Application serviceName string @@ -882,59 +1215,29 @@ func TestGetBackend(t *testing.T) { }, { desc: "service label existing", - application: application(labelWithService(label.TraefikBackend, "bar", "app")), + application: application(withServiceLabel(label.TraefikBackend, "bar", "app")), serviceName: "app", expected: "backendbar", }, } - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { t.Parallel() - prov := &Provider{} - actual := prov.getBackend(c.application, c.serviceName) - if actual != c.expected { - t.Errorf("actual %q, expected %q", actual, c.expected) - } - }) - } -} -func TestGetSubDomain(t *testing.T) { - cases := []struct { - path string - expected string - groupAsSubDomain bool - }{ - {"/test", "test", false}, - {"/test", "test", true}, - {"/a/b/c/d", "d.c.b.a", true}, - {"/b/a/d/c", "c.d.a.b", true}, - {"/d/c/b/a", "a.b.c.d", true}, - {"/c/d/a/b", "b.a.d.c", true}, - {"/a/b/c/d", "a-b-c-d", false}, - {"/b/a/d/c", "b-a-d-c", false}, - {"/d/c/b/a", "d-c-b-a", false}, - {"/c/d/a/b", "c-d-a-b", false}, - } + p := &Provider{} - for _, c := range cases { - c := c - t.Run(fmt.Sprintf("path=%s,group=%t", c.path, c.groupAsSubDomain), func(t *testing.T) { - t.Parallel() - prov := &Provider{GroupsAsSubDomains: c.groupAsSubDomain} - actual := prov.getSubDomain(c.path) - if actual != c.expected { - t.Errorf("actual %q, expected %q", actual, c.expected) - } + actual := p.getBackend(test.application, test.serviceName) + + assert.Equal(t, test.expected, actual) }) } } func TestGetBackendServer(t *testing.T) { host := "host" - cases := []struct { + testCases := []struct { desc string application marathon.Application task marathon.Task @@ -990,80 +1293,71 @@ func TestGetBackendServer(t *testing.T) { }, } - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { t.Parallel() - prov := &Provider{ForceTaskHostname: c.forceTaskHostname} - c.task.Host = host - actualServer := prov.getBackendServer(c.task, c.application) - if actualServer != c.expectedServer { - t.Errorf("actual %q, expected %q", actualServer, c.expectedServer) + + p := &Provider{ForceTaskHostname: test.forceTaskHostname} + test.task.Host = host + + actualServer := p.getBackendServer(test.task, test.application) + + assert.Equal(t, test.expectedServer, actualServer) + }) + } +} + +func TestGetSticky(t *testing.T) { + testCases := []struct { + desc string + application marathon.Application + expected bool + }{ + { + desc: "label missing", + application: application(), + expected: false, + }, + { + desc: "label existing", + application: application(withLabel(label.TraefikBackendLoadBalancerSticky, "true")), + expected: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + actual := getSticky(test.application) + if actual != test.expected { + t.Errorf("actual %q, expected %q", actual, test.expected) } }) } } -func TestHasRedirect(t *testing.T) { +func TestGetCircuitBreaker(t *testing.T) { testCases := []struct { desc string application marathon.Application - serviceName string - expected bool + expected *types.CircuitBreaker }{ { - desc: "without redirect labels", - application: application(), - expected: false, + desc: "should return nil when no CB label", + application: application(appPorts(80)), + expected: nil, }, { - desc: "with entry point redirect label", + desc: "should return a struct when CB label is set", application: application( - withLabel(label.TraefikFrontendRedirectEntryPoint, "bar"), + appPorts(80), + withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), ), - expected: true, - }, - { - desc: "with regex redirect labels", - application: application( - withLabel(label.TraefikFrontendRedirectRegex, "bar"), - withLabel(label.TraefikFrontendRedirectReplacement, "bar"), - ), - expected: true, - }, - { - desc: "with entry point redirect label on service", - application: application( - withLabel(label.Prefix+"foo."+label.SuffixFrontendRedirectEntryPoint, "bar"), - ), - serviceName: "foo", - expected: true, - }, - { - desc: "with entry point redirect label on service but not the same service", - application: application( - withLabel(label.Prefix+"foo."+label.SuffixFrontendRedirectEntryPoint, "bar"), - ), - serviceName: "foofoo", - expected: false, - }, - { - desc: "with regex redirect label on service", - application: application( - withLabel(label.Prefix+"foo."+label.SuffixFrontendRedirectRegex, "bar"), - withLabel(label.Prefix+"foo."+label.SuffixFrontendRedirectReplacement, "bar"), - ), - serviceName: "foo", - expected: true, - }, - { - desc: "with regex redirect label on service but not the same service", - application: application( - withLabel(label.Prefix+"foo."+label.SuffixFrontendRedirectRegex, "bar"), - withLabel(label.Prefix+"foo."+label.SuffixFrontendRedirectReplacement, "bar"), - ), - serviceName: "foofoo", - expected: false, + expected: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", + }, }, } @@ -1072,42 +1366,54 @@ func TestHasRedirect(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := hasRedirect(test.application, test.serviceName) + actual := getCircuitBreaker(test.application) assert.Equal(t, test.expected, actual) }) } } -func TestHasPrefixFuncService(t *testing.T) { +func TestGetLoadBalancer(t *testing.T) { testCases := []struct { desc string application marathon.Application - serviceName string - expected bool + expected *types.LoadBalancer }{ { - desc: "with one label", - application: application( - withLabel(label.Prefix+label.BaseFrontendErrorPage+"goo"+label.SuffixErrorPageBackend, "bar"), - ), - expected: true, + desc: "should return nil when no LB labels", + application: application(appPorts(80)), + expected: nil, }, { - desc: "with one label on service", + desc: "should return a struct when labels are set", application: application( - withLabel(label.Prefix+"foo."+label.BaseFrontendErrorPage+"goo"+label.SuffixErrorPageBackend, "bar"), + appPorts(80), + withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), + withLabel(label.TraefikBackendLoadBalancerSticky, "true"), + withLabel(label.TraefikBackendLoadBalancerStickiness, "true"), + withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "foo"), ), - serviceName: "foo", - expected: true, + expected: &types.LoadBalancer{ + Method: "drr", + Sticky: true, + Stickiness: &types.Stickiness{ + CookieName: "foo", + }, + }, }, { - desc: "with one label on service but not the same service", + desc: "should return a nil Stickiness when Stickiness is not set", application: application( - withLabel(label.Prefix+"foo."+label.BaseFrontendErrorPage+"goo"+label.SuffixErrorPageBackend, "bar"), + appPorts(80), + withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), + withLabel(label.TraefikBackendLoadBalancerSticky, "true"), + withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "foo"), ), - serviceName: "foofoo", - expected: false, + expected: &types.LoadBalancer{ + Method: "drr", + Sticky: true, + Stickiness: nil, + }, }, } @@ -1116,7 +1422,273 @@ func TestHasPrefixFuncService(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := hasPrefixFuncService(label.BaseFrontendErrorPage)(test.application, test.serviceName) + actual := getLoadBalancer(test.application) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetMaxConn(t *testing.T) { + testCases := []struct { + desc string + application marathon.Application + expected *types.MaxConn + }{ + { + desc: "should return nil when no max conn labels", + application: application(appPorts(80)), + expected: nil, + }, + { + desc: "should return nil when no amount label", + application: application( + appPorts(80), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + ), + expected: nil, + }, + { + desc: "should return default when no empty extractorFunc label", + application: application( + appPorts(80), + withLabel(label.TraefikBackendMaxConnExtractorFunc, ""), + withLabel(label.TraefikBackendMaxConnAmount, "666"), + ), + expected: &types.MaxConn{ + ExtractorFunc: "request.host", + Amount: 666, + }, + }, + { + desc: "should return a struct when max conn labels are set", + application: application( + appPorts(80), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + withLabel(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() + + actual := getMaxConn(test.application) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetHealthCheck(t *testing.T) { + testCases := []struct { + desc string + application marathon.Application + expected *types.HealthCheck + }{ + { + desc: "should return nil when no health check labels", + application: application(appPorts(80)), + expected: nil, + }, + { + desc: "should return nil when no health check Path label", + application: application( + appPorts(80), + withLabel(label.TraefikBackendHealthCheckPort, "80"), + withLabel(label.TraefikBackendHealthCheckInterval, "6"), + ), + expected: nil, + }, + { + desc: "should return a struct when health check labels are set", + + application: application( + appPorts(80), + withLabel(label.TraefikBackendHealthCheckPath, "/health"), + withLabel(label.TraefikBackendHealthCheckPort, "80"), + withLabel(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() + + actual := getHealthCheck(test.application) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetServers(t *testing.T) { + testCases := []struct { + desc string + application marathon.Application + serviceName string + expected map[string]types.Server + }{ + { + desc: "should return nil when no task", + application: application(ipAddrPerTask(80)), + expected: nil, + }, + { + desc: "should return nil when all hosts are empty", + application: application( + withTasks( + task(ipAddresses("1.1.1.1"), withTaskID("A"), taskPorts(80)), + task(ipAddresses("1.1.1.2"), withTaskID("B"), taskPorts(80)), + task(ipAddresses("1.1.1.3"), withTaskID("C"), taskPorts(80))), + ), + expected: nil, + }, + { + desc: "with 3 tasks", + application: application( + ipAddrPerTask(80), + withTasks( + task(ipAddresses("1.1.1.1"), withTaskID("A"), taskPorts(80)), + task(ipAddresses("1.1.1.2"), withTaskID("B"), taskPorts(80)), + task(ipAddresses("1.1.1.3"), withTaskID("C"), taskPorts(80))), + ), + expected: map[string]types.Server{ + "server-A": { + URL: "http://1.1.1.1:80", + Weight: 0, + }, + "server-B": { + URL: "http://1.1.1.2:80", + Weight: 0, + }, + "server-C": { + URL: "http://1.1.1.3:80", + Weight: 0, + }, + }, + }, + } + + p := &Provider{} + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := p.getServers(test.application, test.serviceName) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetRedirect(t *testing.T) { + testCases := []struct { + desc string + application marathon.Application + serviceName string + expected *types.Redirect + }{ + { + desc: "should return nil when no redirect labels", + application: application(appPorts(80)), + expected: nil, + }, + { + desc: "should use only entry point tag when mix regex redirect and entry point redirect", + application: application( + appPorts(80), + withLabel(label.TraefikFrontendRedirectEntryPoint, "https"), + withLabel(label.TraefikFrontendRedirectRegex, "(.*)"), + withLabel(label.TraefikFrontendRedirectReplacement, "$1"), + ), + expected: &types.Redirect{ + EntryPoint: "https", + }, + }, + { + desc: "should return a struct when entry point redirect label", + application: application( + appPorts(80), + withLabel(label.TraefikFrontendRedirectEntryPoint, "https"), + ), + expected: &types.Redirect{ + EntryPoint: "https", + }, + }, + { + desc: "should return a struct when regex redirect labels", + application: application( + appPorts(80), + withLabel(label.TraefikFrontendRedirectRegex, "(.*)"), + withLabel(label.TraefikFrontendRedirectReplacement, "$1"), + ), + expected: &types.Redirect{ + Regex: "(.*)", + Replacement: "$1", + }, + }, + // Service + { + desc: "should use only entry point tag when mix regex redirect and entry point redirect on service", + application: application( + appPorts(80), + withLabel(label.Prefix+"containous."+label.SuffixFrontendRedirectEntryPoint, "https"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendRedirectRegex, "(.*)"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendRedirectReplacement, "$1"), + ), + serviceName: "containous", + expected: &types.Redirect{ + EntryPoint: "https", + }, + }, + { + desc: "should return a struct when entry point redirect label on service", + application: application( + appPorts(80), + withLabel(label.Prefix+"containous."+label.SuffixFrontendRedirectEntryPoint, "https"), + ), + serviceName: "containous", + expected: &types.Redirect{ + EntryPoint: "https", + }, + }, + { + desc: "should return a struct when regex redirect labels on service", + application: application( + appPorts(80), + withLabel(label.Prefix+"containous."+label.SuffixFrontendRedirectRegex, "(.*)"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendRedirectReplacement, "$1"), + ), + serviceName: "containous", + expected: &types.Redirect{ + Regex: "(.*)", + Replacement: "$1", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getRedirect(test.application, test.serviceName) assert.Equal(t, test.expected, actual) }) @@ -1201,68 +1773,101 @@ func TestGetErrorPages(t *testing.T) { } } -func TestGetRateLimits(t *testing.T) { +func TestGetRateLimit(t *testing.T) { testCases := []struct { desc string application marathon.Application serviceName string - expected map[string]*types.Rate + expected *types.RateLimit }{ { - desc: "with 2 rate limits", + desc: "should return nil when no rate limit labels", + application: application(appPorts(80)), + expected: nil, + }, + { + desc: "should return a struct when rate limit labels are defined", application: application( - withLabel(label.Prefix+label.BaseFrontendRateLimit+"goo."+label.SuffixRateLimitAverage, "1"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"goo."+label.SuffixRateLimitPeriod, "2"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"goo."+label.SuffixRateLimitBurst, "3"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"hoo."+label.SuffixRateLimitAverage, "4"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"hoo."+label.SuffixRateLimitPeriod, "5"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"hoo."+label.SuffixRateLimitBurst, "6"), + appPorts(80), + withLabel(label.TraefikFrontendRateLimitExtractorFunc, "client.ip"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), ), - expected: map[string]*types.Rate{ - "goo": { - Average: 1, - Period: flaeg.Duration(2 * time.Second), - Burst: 3, - }, - "hoo": { - Average: 4, - Period: flaeg.Duration(5 * time.Second), - Burst: 6, + 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: "with 2 rate limits on service", + desc: "should return nil when ExtractorFunc is missing", application: application( - withLabel(label.Prefix+"foo."+label.BaseFrontendRateLimit+"goo."+label.SuffixRateLimitAverage, "1"), - withLabel(label.Prefix+"foo."+label.BaseFrontendRateLimit+"goo."+label.SuffixRateLimitPeriod, "2"), - withLabel(label.Prefix+"foo."+label.BaseFrontendRateLimit+"goo."+label.SuffixRateLimitBurst, "3"), - withLabel(label.Prefix+"foo."+label.BaseFrontendRateLimit+"hoo."+label.SuffixRateLimitAverage, "4"), - withLabel(label.Prefix+"foo."+label.BaseFrontendRateLimit+"hoo."+label.SuffixRateLimitPeriod, "5"), - withLabel(label.Prefix+"foo."+label.BaseFrontendRateLimit+"hoo."+label.SuffixRateLimitBurst, "6"), + appPorts(80), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), ), - serviceName: "foo", - expected: map[string]*types.Rate{ - "goo": { - Average: 1, - Period: flaeg.Duration(2 * time.Second), - Burst: 3, - }, - "hoo": { - Average: 4, - Period: flaeg.Duration(5 * time.Second), - Burst: 6, + expected: nil, + }, + // Service + { + desc: "should return a struct when rate limit labels are defined on service", + application: application( + appPorts(80), + withLabel(label.Prefix+"containous."+label.SuffixFrontendRateLimitExtractorFunc, "client.ip"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), + ), + serviceName: "containous", + 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: "with 1 rate limit on service but not the same service", + desc: "should return nil when ExtractorFunc is missing on service", application: application( - withLabel(label.Prefix+"foo."+label.BaseFrontendRateLimit+"goo."+label.SuffixRateLimitAverage, "1"), - withLabel(label.Prefix+"foo."+label.BaseFrontendRateLimit+"goo."+label.SuffixRateLimitPeriod, "2"), - withLabel(label.Prefix+"foo."+label.BaseFrontendRateLimit+"goo."+label.SuffixRateLimitBurst, "3"), + appPorts(80), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), ), - serviceName: "foofoo", + serviceName: "containous", expected: nil, }, } @@ -1272,9 +1877,151 @@ func TestGetRateLimits(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - limits := getRateLimits(test.application, test.serviceName) + actual := getRateLimit(test.application, test.serviceName) - assert.EqualValues(t, test.expected, limits) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetHeaders(t *testing.T) { + testCases := []struct { + desc string + application marathon.Application + serviceName string + expected *types.Headers + }{ + { + desc: "should return nil when no custom headers options are set", + application: application(appPorts(80)), + expected: nil, + }, + { + desc: "should return a struct when all custom headers options are set", + application: application( + appPorts(80), + withLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + withLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + withLabel(label.TraefikFrontendSSLProxyHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + withLabel(label.TraefikFrontendAllowedHosts, "foo,bar,bor"), + withLabel(label.TraefikFrontendHostsProxyHeaders, "foo,bar,bor"), + withLabel(label.TraefikFrontendSSLHost, "foo"), + withLabel(label.TraefikFrontendCustomFrameOptionsValue, "foo"), + withLabel(label.TraefikFrontendContentSecurityPolicy, "foo"), + withLabel(label.TraefikFrontendPublicKey, "foo"), + withLabel(label.TraefikFrontendReferrerPolicy, "foo"), + withLabel(label.TraefikFrontendSTSSeconds, "666"), + withLabel(label.TraefikFrontendSSLRedirect, "true"), + withLabel(label.TraefikFrontendSSLTemporaryRedirect, "true"), + withLabel(label.TraefikFrontendSTSIncludeSubdomains, "true"), + withLabel(label.TraefikFrontendSTSPreload, "true"), + withLabel(label.TraefikFrontendForceSTSHeader, "true"), + withLabel(label.TraefikFrontendFrameDeny, "true"), + withLabel(label.TraefikFrontendContentTypeNosniff, "true"), + withLabel(label.TraefikFrontendBrowserXSSFilter, "true"), + withLabel(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, + }, + }, + // Service + { + desc: "should return a struct when all custom headers options are set on service", + application: application( + appPorts(80), + withLabel(label.Prefix+"containous."+label.SuffixFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersSSLProxyHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersAllowedHosts, "foo,bar,bor"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersHostsProxyHeaders, "foo,bar,bor"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersSSLHost, "foo"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersCustomFrameOptionsValue, "foo"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersContentSecurityPolicy, "foo"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersPublicKey, "foo"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersReferrerPolicy, "foo"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersSTSSeconds, "666"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersSSLRedirect, "true"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersSSLTemporaryRedirect, "true"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersSTSIncludeSubdomains, "true"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersSTSPreload, "true"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersForceSTSHeader, "true"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersFrameDeny, "true"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersContentTypeNosniff, "true"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersBrowserXSSFilter, "true"), + withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersIsDevelopment, "true"), + ), + serviceName: "containous", + expected: &types.Headers{ + CustomRequestHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + CustomResponseHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + SSLProxyHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + AllowedHosts: []string{"foo", "bar", "bor"}, + HostsProxyHeaders: []string{"foo", "bar", "bor"}, + SSLHost: "foo", + CustomFrameOptionsValue: "foo", + ContentSecurityPolicy: "foo", + PublicKey: "foo", + ReferrerPolicy: "foo", + STSSeconds: 666, + SSLRedirect: true, + SSLTemporaryRedirect: true, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + ContentTypeNosniff: true, + BrowserXSSFilter: true, + IsDevelopment: true, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getHeaders(test.application, test.serviceName) + + assert.Equal(t, test.expected, actual) }) } } diff --git a/templates/marathon.tmpl b/templates/marathon.tmpl index 40f11225a..9612e8017 100644 --- a/templates/marathon.tmpl +++ b/templates/marathon.tmpl @@ -1,192 +1,168 @@ -{{$apps := .Applications}} +{{ $apps := .Applications }} [backends] -{{range $app := $apps}} -{{range $serviceIndex, $serviceName := getServiceNames $app}} +{{range $app := $apps }} +{{range $serviceIndex, $serviceName := getServiceNames $app }} + {{ $backendName := getBackend $app $serviceName}} - [backends."{{getBackend $app $serviceName }}"] + [backends."{{ $backendName }}"] - {{ if hasCircuitBreakerLabels $app }} - [backends."{{getBackend $app $serviceName }}".circuitBreaker] - expression = "{{getCircuitBreakerExpression $app }}" + {{ $circuitBreaker := getCircuitBreaker $app }} + {{if $circuitBreaker }} + [backends."{{ $backendName }}".circuitBreaker] + expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{ if hasLoadBalancerLabels $app }} - [backends."{{getBackend $app $serviceName }}".loadBalancer] - method = "{{getLoadBalancerMethod $app }}" - sticky = {{getSticky $app}} - {{if hasStickinessLabel $app}} - [backends."{{getBackend $app $serviceName }}".loadBalancer.stickiness] - cookieName = "{{getStickinessCookieName $app}}" + {{ $loadBalancer := getLoadBalancer $app }} + {{if $loadBalancer }} + [backends."{{ $backendName }}".loadBalancer] + method = "{{ $loadBalancer.Method }}" + sticky = {{ $loadBalancer.Sticky }} + {{if $loadBalancer.Stickiness }} + [backends."{{ $backendName }}".loadBalancer.stickiness] + cookieName = "{{ $loadBalancer.Stickiness.CookieName }}" {{end}} {{end}} - {{ if hasMaxConnLabels $app }} - [backends."{{getBackend $app $serviceName }}".maxConn] - amount = {{getMaxConnAmount $app }} - extractorFunc = "{{getMaxConnExtractorFunc $app }}" + {{ $maxConn := getMaxConn $app }} + {{if $maxConn }} + [backends."{{ $backendName }}".maxConn] + extractorFunc = "{{ $maxConn.ExtractorFunc }}" + amount = {{ $maxConn.Amount }} {{end}} - {{ if hasHealthCheckLabels $app }} - [backends."{{getBackend $app $serviceName }}".healthCheck] - path = "{{getHealthCheckPath $app }}" - port = {{getHealthCheckPort $app}} - interval = "{{getHealthCheckInterval $app }}" + {{ $healthCheck := getHealthCheck $app }} + {{if $healthCheck }} + [backends."{{ $backendName }}".healthCheck] + path = "{{ $healthCheck.Path }}" + port = {{ $healthCheck.Port }} + interval = "{{ $healthCheck.Interval }}" + {{end}} + + {{range $serverName, $server := getServers $app $serviceName }} + [backends."{{ $backendName }}".servers."{{ $serverName }}"] + url = "{{ $server.URL }}" + weight = {{ $server.Weight }} {{end}} {{end}} - - {{range $task := $app.Tasks}} - {{range $serviceIndex, $serviceName := getServiceNames $app}} - - [backends."{{getBackend $app $serviceName}}".servers."server-{{$task.ID | replace "." "-"}}{{getServiceNameSuffix $serviceName }}"] - url = "{{getProtocol $app $serviceName}}://{{getBackendServer $task $app}}:{{getPort $task $app $serviceName}}" - weight = {{getWeight $app $serviceName}} - - {{end}} - {{end}} - {{end}} [frontends] -{{range $app := $apps}} -{{range $serviceIndex, $serviceName := getServiceNames .}} +{{range $app := $apps }} +{{range $serviceIndex, $serviceName := getServiceNames $app }} + {{ $frontendName := getFrontendName $app $serviceName }} - [frontends."{{ getFrontendName $app $serviceName }}"] - backend = "{{getBackend $app $serviceName}}" - priority = {{getPriority $app $serviceName}} - passHostHeader = {{getPassHostHeader $app $serviceName}} - passTLSCert = {{getPassTLSCert $app $serviceName}} + [frontends."{{ $frontendName }}"] + backend = "{{ getBackend $app $serviceName }}" + priority = {{ getPriority $app $serviceName }} + passHostHeader = {{ getPassHostHeader $app $serviceName }} + passTLSCert = {{ getPassTLSCert $app $serviceName }} - entryPoints = [{{range getEntryPoints $app $serviceName}} + entryPoints = [{{range getEntryPoints $app $serviceName }} "{{.}}", {{end}}] - {{if getWhitelistSourceRange $app $serviceName}} - whitelistSourceRange = [{{range getWhitelistSourceRange $app $serviceName}} + {{ $whitelistSourceRange := getWhitelistSourceRange $app $serviceName }} + {{if $whitelistSourceRange }} + whitelistSourceRange = [{{range $whitelistSourceRange }} "{{.}}", {{end}}] {{end}} - basicAuth = [{{range getBasicAuth $app $serviceName}} + basicAuth = [{{range getBasicAuth $app $serviceName }} "{{.}}", {{end}}] - {{if hasRedirect $app $serviceName}} - [frontends."{{ getFrontendName $app $serviceName }}".redirect] - entryPoint = "{{getRedirectEntryPoint $app $serviceName}}" - regex = "{{getRedirectRegex $app $serviceName}}" - replacement = "{{getRedirectReplacement $app $serviceName}}" + {{ $redirect := getRedirect $app $serviceName }} + {{if $redirect }} + [frontends."{{ $frontendName }}".redirect] + entryPoint = "{{ $redirect.EntryPoint }}" + regex = "{{ $redirect.Regex }}" + replacement = "{{ $redirect.Replacement }}" {{end}} - {{ if hasErrorPages $app $serviceName }} - [frontends."{{ getFrontendName $app $serviceName }}".errors] - {{ range $pageName, $page := getErrorPages $app $serviceName }} - [frontends."{{ getFrontendName $app $serviceName }}".errors.{{ $pageName }}] - status = [{{range $page.Status}} - "{{.}}", - {{end}}] - backend = "{{$page.Backend}}" - query = "{{$page.Query}}" + {{ $errorPages := getErrorPages $app $serviceName }} + {{if $errorPages }} + [frontends."{{ $frontendName }}".errors] + {{range $pageName, $page := $errorPages }} + [frontends."{{ $frontendName }}".errors.{{ $pageName }}] + status = [{{range $page.Status }} + "{{.}}", + {{end}}] + backend = "{{ $page.Backend }}" + query = "{{ $page.Query }}" {{end}} {{end}} - {{ if hasRateLimits $app $serviceName }} - [frontends."{{ getFrontendName $app $serviceName }}".rateLimit] - extractorFunc = "{{ getRateLimitsExtractorFunc $app $serviceName }}" - [frontends."{{ getFrontendName $app $serviceName }}".rateLimit.rateSet] - {{ range $limitName, $rateLimit := getRateLimits $app $serviceName }} - [frontends."{{ getFrontendName $app $serviceName }}".rateLimit.rateSet.{{ $limitName }}] - period = "{{ $rateLimit.Period }}" - average = {{ $rateLimit.Average }} - burst = {{ $rateLimit.Burst }} + {{ $rateLimit := getRateLimit $app $serviceName }} + {{if $rateLimit }} + [frontends."{{ $frontendName }}".rateLimit] + extractorFunc = "{{ $rateLimit.ExtractorFunc }}" + [frontends."{{ $frontendName }}".rateLimit.rateSet] + {{ range $limitName, $limit := $rateLimit.RateSet }} + [frontends."{{ $frontendName }}".rateLimit.rateSet.{{ $limitName }}] + period = "{{ $limit.Period }}" + average = {{ $limit.Average }} + burst = {{ $limit.Burst }} {{end}} {{end}} - {{if hasHeaders $app $serviceName }} - [frontends."{{ getFrontendName $app $serviceName }}".headers] - {{if hasSSLRedirectHeaders $app $serviceName}} - SSLRedirect = {{getSSLRedirectHeaders $app $serviceName}} - {{end}} - {{if hasSSLTemporaryRedirectHeaders $app $serviceName}} - SSLTemporaryRedirect = {{getSSLTemporaryRedirectHeaders $app $serviceName}} - {{end}} - {{if hasSSLHostHeaders $app $serviceName}} - SSLHost = "{{getSSLHostHeaders $app $serviceName}}" - {{end}} - {{if hasSTSSecondsHeaders $app $serviceName}} - STSSeconds = {{getSTSSecondsHeaders $app $serviceName}} - {{end}} - {{if hasSTSIncludeSubdomainsHeaders $app $serviceName}} - STSIncludeSubdomains = {{getSTSIncludeSubdomainsHeaders $app $serviceName}} - {{end}} - {{if hasSTSPreloadHeaders $app $serviceName}} - STSPreload = {{getSTSPreloadHeaders $app $serviceName}} - {{end}} - {{if hasForceSTSHeaderHeaders $app $serviceName}} - ForceSTSHeader = {{getForceSTSHeaderHeaders $app $serviceName}} - {{end}} - {{if hasFrameDenyHeaders $app $serviceName}} - FrameDeny = {{getFrameDenyHeaders $app $serviceName}} - {{end}} - {{if hasCustomFrameOptionsValueHeaders $app $serviceName}} - CustomFrameOptionsValue = "{{getCustomFrameOptionsValueHeaders $app $serviceName}}" - {{end}} - {{if hasContentTypeNosniffHeaders $app $serviceName}} - ContentTypeNosniff = {{getContentTypeNosniffHeaders $app $serviceName}} - {{end}} - {{if hasBrowserXSSFilterHeaders $app $serviceName}} - BrowserXSSFilter = {{getBrowserXSSFilterHeaders $app $serviceName}} - {{end}} - {{if hasContentSecurityPolicyHeaders $app $serviceName}} - ContentSecurityPolicy = "{{getContentSecurityPolicyHeaders $app $serviceName}}" - {{end}} - {{if hasPublicKeyHeaders $app $serviceName}} - PublicKey = "{{getPublicKeyHeaders $app $serviceName}}" - {{end}} - {{if hasReferrerPolicyHeaders $app $serviceName}} - ReferrerPolicy = "{{getReferrerPolicyHeaders $app $serviceName}}" - {{end}} - {{if hasIsDevelopmentHeaders $app $serviceName}} - IsDevelopment = {{getIsDevelopmentHeaders $app $serviceName}} - {{end}} + {{ $headers := getHeaders $app $serviceName }} + {{if $headers }} + [frontends."{{ $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 $app $serviceName}} - AllowedHosts = [{{range getAllowedHostsHeaders $app $serviceName}} - "{{.}}", - {{end}}] - {{end}} + {{if $headers.AllowedHosts }} + AllowedHosts = [{{range $headers.AllowedHosts }} + "{{.}}", + {{end}}] + {{end}} - {{if hasHostsProxyHeaders $app $serviceName}} - HostsProxyHeaders = [{{range getHostsProxyHeaders $app $serviceName}} - "{{.}}", - {{end}}] - {{end}} + {{if $headers.HostsProxyHeaders }} + HostsProxyHeaders = [{{range $headers.HostsProxyHeaders }} + "{{.}}", + {{end}}] + {{end}} - {{if hasRequestHeaders $app $serviceName}} - [frontends."{{ getFrontendName $app $serviceName }}".headers.customRequestHeaders] - {{range $k, $v := getRequestHeaders $app $serviceName}} - {{$k}} = "{{$v}}" + {{if $headers.CustomRequestHeaders }} + [frontends."{{ $frontendName }}".headers.customRequestHeaders] + {{range $k, $v := $headers.CustomRequestHeaders }} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + + {{if $headers.CustomResponseHeaders }} + [frontends."{{ $frontendName }}".headers.customResponseHeaders] + {{range $k, $v := $headers.CustomResponseHeaders }} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + + {{if $headers.SSLProxyHeaders }} + [frontends."{{ $frontendName }}".headers.SSLProxyHeaders] + {{range $k, $v := $headers.SSLProxyHeaders }} + {{$k}} = "{{$v}}" + {{end}} {{end}} {{end}} - {{if hasResponseHeaders $app $serviceName}} - [frontends."{{ getFrontendName $app $serviceName }}".headers.customResponseHeaders] - {{range $k, $v := getResponseHeaders $app $serviceName}} - {{$k}} = "{{$v}}" - {{end}} - {{end}} - - {{if hasSSLProxyHeaders $app $serviceName}} - [frontends."{{ getFrontendName $app $serviceName }}".headers.SSLProxyHeaders] - {{range $k, $v := getSSLProxyHeaders $app $serviceName}} - {{$k}} = "{{$v}}" - {{end}} - {{end}} - {{end}} - - [frontends."{{ getFrontendName $app $serviceName }}".routes."route-host{{$app.ID | replace "/" "-"}}{{getServiceNameSuffix $serviceName }}"] - rule = "{{getFrontendRule $app $serviceName}}" + [frontends."{{ $frontendName }}".routes."route-host{{ $app.ID | replace "/" "-" }}{{ getServiceNameSuffix $serviceName }}"] + rule = "{{ getFrontendRule $app $serviceName }}" {{end}} {{end}}