From 48024847293070c5c7baeebd123d5c7e055f884c Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Fri, 23 Mar 2018 13:30:03 +0100 Subject: [PATCH] Segment labels: Docker --- Makefile | 2 +- autogen/gentemplates/gen.go | 395 ++++--- cmd/configuration.go | 14 +- configuration/configuration.go | 8 + docs/configuration/backends/docker.md | 116 ++- docs/configuration/backends/marathon.md | 100 +- provider/docker/config.go | 511 ++++++++-- provider/docker/config_container.go | 358 ------- .../docker/config_container_docker_test.go | 687 +------------ .../docker/config_container_swarm_test.go | 92 +- provider/docker/config_root.go | 12 + provider/docker/config_segment_test.go | 351 +++++++ provider/docker/config_service.go | 277 ----- provider/docker/deprecated_config.go | 201 ++++ provider/docker/deprecated_container.go | 214 ++++ .../deprecated_container_docker_test.go | 965 ++++++++++++++++++ .../docker/deprecated_container_swarm_test.go | 706 +++++++++++++ provider/docker/deprecated_service.go | 231 +++++ ...ice_test.go => deprecated_service_test.go} | 560 +--------- provider/docker/docker.go | 10 +- provider/label/label.go | 77 -- provider/label/label_test.go | 108 +- provider/label/names.go | 4 +- provider/label/segment.go | 167 +++ provider/provider.go | 1 + rules/rules.go | 17 +- templates/docker-v1.tmpl | 192 ++++ templates/docker.tmpl | 183 +--- 28 files changed, 4095 insertions(+), 2464 deletions(-) delete mode 100644 provider/docker/config_container.go create mode 100644 provider/docker/config_root.go create mode 100644 provider/docker/config_segment_test.go delete mode 100644 provider/docker/config_service.go create mode 100644 provider/docker/deprecated_config.go create mode 100644 provider/docker/deprecated_container.go create mode 100644 provider/docker/deprecated_container_docker_test.go create mode 100644 provider/docker/deprecated_container_swarm_test.go create mode 100644 provider/docker/deprecated_service.go rename provider/docker/{config_service_test.go => deprecated_service_test.go} (52%) create mode 100644 provider/label/segment.go create mode 100644 templates/docker-v1.tmpl diff --git a/Makefile b/Makefile index a8b5ea4b6..bba47bc7e 100644 --- a/Makefile +++ b/Makefile @@ -73,7 +73,7 @@ test-integration: build ## run the integration tests $(DOCKER_RUN_TRAEFIK) ./script/make.sh generate binary test-integration TEST_HOST=1 ./script/make.sh test-integration -validate: build ## validate gofmt, golint and go vet +validate: build ## validate code, vendor and autogen $(DOCKER_RUN_TRAEFIK) ./script/make.sh validate-gofmt validate-govet validate-golint validate-misspell validate-vendor validate-autogen build: dist diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index 91228b2ce..e7e392d8f 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -1,6 +1,7 @@ // Code generated by go-bindata. // sources: // templates/consul_catalog.tmpl +// templates/docker-v1.tmpl // templates/docker.tmpl // templates/ecs.tmpl // templates/eureka.tmpl @@ -244,17 +245,227 @@ func templatesConsul_catalogTmpl() (*asset, error) { return a, nil } +var _templatesDockerV1Tmpl = []byte(`{{$backendServers := .Servers}} + +[backends] +{{range $backendName, $backend := .Backends }} + + {{if hasCircuitBreakerLabel $backend }} + [backends."backend-{{ $backendName }}".circuitbreaker] + expression = "{{ getCircuitBreakerExpression $backend }}" + {{end}} + + {{if hasLoadBalancerLabel $backend }} + [backends."backend-{{ $backendName }}".loadbalancer] + method = "{{ getLoadBalancerMethod $backend }}" + sticky = {{ getSticky $backend }} + {{if hasStickinessLabel $backend }} + [backends."backend-{{ $backendName }}".loadbalancer.stickiness] + cookieName = "{{ getStickinessCookieName $backend }}" + {{end}} + {{end}} + + {{if hasMaxConnLabels $backend }} + [backends."backend-{{ $backendName }}".maxconn] + amount = {{ getMaxConnAmount $backend }} + extractorfunc = "{{ getMaxConnExtractorFunc $backend }}" + {{end}} + + {{ $servers := index $backendServers $backendName }} + {{range $serverName, $server := $servers }} + {{if hasServices $server }} + {{$services := getServiceNames $server }} + {{range $serviceIndex, $serviceName := $services }} + [backends."backend-{{ getServiceBackend $server $serviceName }}".servers."service-{{ $serverName }}"] + url = "{{ getServiceProtocol $server $serviceName }}://{{ getIPAddress $server }}:{{ getServicePort $server $serviceName }}" + weight = {{ getServiceWeight $server $serviceName }} + {{end}} + {{else}} + [backends."backend-{{ $backendName }}".servers."server-{{$server.Name | replace "/" "" | replace "." "-"}}"] + url = "{{ getProtocol $server }}://{{ getIPAddress $server }}:{{ getPort $server }}" + weight = {{ getWeight $server }} + {{end}} + {{end}} + +{{end}} + +[frontends] +{{range $frontend, $containers := .Frontends}} + {{$container := index $containers 0}} + + {{if hasServices $container }} + {{ $services := getServiceNames $container }} + {{range $serviceIndex, $serviceName := $services }} + [frontends."frontend-{{ getServiceBackend $container $serviceName }}"] + backend = "backend-{{ getServiceBackend $container $serviceName }}" + passHostHeader = {{ getServicePassHostHeader $container $serviceName }} + passTLSCert = {{ getServicePassTLSCert $container $serviceName }} + + {{if getWhitelistSourceRange $container }} + whitelistSourceRange = [{{range getWhitelistSourceRange $container }} + "{{.}}", + {{end}}] + {{end}} + + priority = {{ getServicePriority $container $serviceName }} + + entryPoints = [{{range getServiceEntryPoints $container $serviceName }} + "{{.}}", + {{end}}] + + basicAuth = [{{range getServiceBasicAuth $container $serviceName }} + "{{.}}", + {{end}}] + + {{if hasServiceRedirect $container $serviceName }} + [frontends."frontend-{{ getServiceBackend $container $serviceName }}".redirect] + entryPoint = "{{ getServiceRedirectEntryPoint $container $serviceName }}" + regex = "{{ getServiceRedirectRegex $container $serviceName }}" + replacement = "{{ getServiceRedirectReplacement $container $serviceName }}" + {{end}} + + [frontends."frontend-{{ getServiceBackend $container $serviceName }}".routes."service-{{ $serviceName | replace "/" "" | replace "." "-" }}"] + rule = "{{ getServiceFrontendRule $container $serviceName }}" + {{end}} + {{else}} + [frontends."frontend-{{ $frontend }}"] + backend = "backend-{{ getBackend $container }}" + passHostHeader = {{ getPassHostHeader $container}} + passTLSCert = {{ getPassTLSCert $container }} + priority = {{ getPriority $container }} + + {{if getWhitelistSourceRange $container}} + whitelistSourceRange = [{{range getWhitelistSourceRange $container}} + "{{.}}", + {{end}}] + {{end}} + + entryPoints = [{{range getEntryPoints $container }} + "{{.}}", + {{end}}] + + basicAuth = [{{range getBasicAuth $container }} + "{{.}}", + {{end}}] + + {{if hasRedirect $container}} + [frontends."frontend-{{$frontend}}".redirect] + entryPoint = "{{getRedirectEntryPoint $container}}" + regex = "{{getRedirectRegex $container}}" + replacement = "{{getRedirectReplacement $container}}" + {{end}} + + {{if hasHeaders $container }} + [frontends."frontend-{{ $frontend }}".headers] + {{if hasSSLRedirectHeaders $container}} + SSLRedirect = {{getSSLRedirectHeaders $container}} + {{end}} + {{if hasSSLTemporaryRedirectHeaders $container}} + SSLTemporaryRedirect = {{getSSLTemporaryRedirectHeaders $container}} + {{end}} + {{if hasSSLHostHeaders $container}} + SSLHost = "{{getSSLHostHeaders $container}}" + {{end}} + {{if hasSTSSecondsHeaders $container}} + STSSeconds = {{getSTSSecondsHeaders $container}} + {{end}} + {{if hasSTSIncludeSubdomainsHeaders $container}} + STSIncludeSubdomains = {{getSTSIncludeSubdomainsHeaders $container}} + {{end}} + {{if hasSTSPreloadHeaders $container}} + STSPreload = {{getSTSPreloadHeaders $container}} + {{end}} + {{if hasForceSTSHeaderHeaders $container}} + ForceSTSHeader = {{getForceSTSHeaderHeaders $container}} + {{end}} + {{if hasFrameDenyHeaders $container}} + FrameDeny = {{getFrameDenyHeaders $container}} + {{end}} + {{if hasCustomFrameOptionsValueHeaders $container}} + CustomFrameOptionsValue = "{{getCustomFrameOptionsValueHeaders $container}}" + {{end}} + {{if hasContentTypeNosniffHeaders $container}} + ContentTypeNosniff = {{getContentTypeNosniffHeaders $container}} + {{end}} + {{if hasBrowserXSSFilterHeaders $container}} + BrowserXSSFilter = {{getBrowserXSSFilterHeaders $container}} + {{end}} + {{if hasContentSecurityPolicyHeaders $container}} + ContentSecurityPolicy = "{{getContentSecurityPolicyHeaders $container}}" + {{end}} + {{if hasPublicKeyHeaders $container}} + PublicKey = "{{getPublicKeyHeaders $container}}" + {{end}} + {{if hasReferrerPolicyHeaders $container}} + ReferrerPolicy = "{{getReferrerPolicyHeaders $container}}" + {{end}} + {{if hasIsDevelopmentHeaders $container}} + IsDevelopment = {{getIsDevelopmentHeaders $container}} + {{end}} + {{if hasAllowedHostsHeaders $container}} + AllowedHosts = [{{range getAllowedHostsHeaders $container}} + "{{.}}", + {{end}}] + {{end}} + {{if hasHostsProxyHeaders $container}} + HostsProxyHeaders = [{{range getHostsProxyHeaders $container}} + "{{.}}", + {{end}}] + {{end}} + {{if hasRequestHeaders $container}} + [frontends."frontend-{{$frontend}}".headers.customrequestheaders] + {{range $k, $v := getRequestHeaders $container}} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + {{if hasResponseHeaders $container}} + [frontends."frontend-{{$frontend}}".headers.customresponseheaders] + {{range $k, $v := getResponseHeaders $container}} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + {{if hasSSLProxyHeaders $container}} + [frontends."frontend-{{$frontend}}".headers.SSLProxyHeaders] + {{range $k, $v := getSSLProxyHeaders $container}} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + {{end}} + + [frontends."frontend-{{$frontend}}".routes."route-frontend-{{$frontend}}"] + rule = "{{getFrontendRule $container}}" + {{end}} + +{{end}} +`) + +func templatesDockerV1TmplBytes() ([]byte, error) { + return _templatesDockerV1Tmpl, nil +} + +func templatesDockerV1Tmpl() (*asset, error) { + bytes, err := templatesDockerV1TmplBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "templates/docker-v1.tmpl", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} [backends] -{{range $backendName, $backend := .Backends}} +{{range $backendName, $servers := .Servers}} +{{ $backend := index $servers 0 }} - {{ $circuitBreaker := getCircuitBreaker $backend }} + {{ $circuitBreaker := getCircuitBreaker $backend.SegmentLabels }} {{if $circuitBreaker }} [backends."backend-{{ $backendName }}".circuitBreaker] expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{ $loadBalancer := getLoadBalancer $backend }} + {{ $loadBalancer := getLoadBalancer $backend.SegmentLabels }} {{if $loadBalancer }} [backends."backend-{{ $backendName }}".loadBalancer] method = "{{ $loadBalancer.Method }}" @@ -265,14 +476,14 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} {{end}} {{end}} - {{ $maxConn := getMaxConn $backend }} + {{ $maxConn := getMaxConn $backend.SegmentLabels }} {{if $maxConn }} [backends."backend-{{ $backendName }}".maxConn] extractorFunc = "{{ $maxConn.ExtractorFunc }}" amount = {{ $maxConn.Amount }} {{end}} - {{ $healthCheck := getHealthCheck $backend }} + {{ $healthCheck := getHealthCheck $backend.SegmentLabels }} {{if $healthCheck }} [backends."backend-{{ $backendName }}".healthCheck] path = "{{ $healthCheck.Path }}" @@ -280,7 +491,7 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} interval = "{{ $healthCheck.Interval }}" {{end}} - {{ $buffering := getBuffering $backend }} + {{ $buffering := getBuffering $backend.SegmentLabels }} {{if $buffering }} [backends."backend-{{ $backendName }}".buffering] maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} @@ -290,173 +501,40 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} retryExpression = "{{ $buffering.RetryExpression }}" {{end}} - {{ $servers := index $backendServers $backendName }} - {{range $serverName, $server := $servers }} - {{if hasServices $server }} - {{ $services := getServiceNames $server }} - {{range $serviceIndex, $serviceName := $services }} - [backends."backend-{{ getServiceBackendName $server $serviceName }}".servers."service-{{ $serverName }}"] - url = "{{ getServiceProtocol $server $serviceName }}://{{ getIPAddress $server }}:{{ getServicePort $server $serviceName }}" - weight = {{ getServiceWeight $server $serviceName }} - {{end}} - {{else}} - [backends."backend-{{ $backendName }}".servers."server-{{ $server.Name | replace "/" "" | replace "." "-" }}"] - url = "{{ getProtocol $server }}://{{ getIPAddress $server }}:{{ getPort $server }}" - weight = {{ getWeight $server }} - {{end}} + {{range $serverName, $server := getServers $servers }} + [backends."backend-{{ $backendName }}".servers."{{ $serverName }}"] + url = "{{ $server.URL }}" + weight = {{ $server.Weight }} {{end}} {{end}} [frontends] {{range $frontendName, $containers := .Frontends }} - {{$container := index $containers 0}} - - {{if hasServices $container }} - {{ $services := getServiceNames $container }} - - {{range $serviceIndex, $serviceName := $services }} - {{ $ServiceFrontendName := getServiceBackendName $container $serviceName }} - - [frontends."frontend-{{ $ServiceFrontendName }}"] - backend = "backend-{{ $ServiceFrontendName }}" - priority = {{ getServicePriority $container $serviceName }} - passHostHeader = {{ getServicePassHostHeader $container $serviceName }} - passTLSCert = {{ getServicePassTLSCert $container $serviceName }} - - entryPoints = [{{range getServiceEntryPoints $container $serviceName }} - "{{.}}", - {{end}}] - - {{ $whitelistSourceRange := getServiceWhitelistSourceRange $container $serviceName }} - {{if $whitelistSourceRange }} - whitelistSourceRange = [{{range $whitelistSourceRange }} - "{{.}}", - {{end}}] - {{end}} - - basicAuth = [{{range getServiceBasicAuth $container $serviceName }} - "{{.}}", - {{end}}] - - {{ $redirect := getServiceRedirect $container $serviceName }} - {{if $redirect }} - [frontends."frontend-{{ $ServiceFrontendName }}".redirect] - entryPoint = "{{ $redirect.EntryPoint }}" - regex = "{{ $redirect.Regex }}" - replacement = "{{ $redirect.Replacement }}" - permanent = {{ $redirect.Permanent }} - {{end}} - - {{ $errorPages := getServiceErrorPages $container $serviceName }} - {{if $errorPages }} - [frontends."frontend-{{ $ServiceFrontendName }}".errors] - {{ range $pageName, $page := $errorPages }} - [frontends."frontend-{{ $ServiceFrontendName }}".errors."{{ $pageName }}"] - status = [{{range $page.Status }} - "{{.}}", - {{end}}] - backend = "{{ $page.Backend }}" - query = "{{ $page.Query }}" - {{end}} - {{end}} - - {{ $rateLimit := getServiceRateLimit $container $serviceName }} - {{if $rateLimit }} - [frontends."frontend-{{ $ServiceFrontendName }}".rateLimit] - extractorFunc = "{{ $rateLimit.ExtractorFunc }}" - [frontends."frontend-{{ $ServiceFrontendName }}".rateLimit.rateSet] - {{range $limitName, $limit := $rateLimit.RateSet }} - [frontends."frontend-{{ $ServiceFrontendName }}".rateLimit.rateSet."{{ $limitName }}"] - period = "{{ $limit.Period }}" - average = {{ $limit.Average }} - burst = {{ $limit.Burst }} - {{end}} - {{end}} - - {{ $headers := getServiceHeaders $container $serviceName }} - {{if $headers }} - [frontends."frontend-{{ $ServiceFrontendName }}".headers] - SSLRedirect = {{ $headers.SSLRedirect }} - SSLTemporaryRedirect = {{ $headers.SSLTemporaryRedirect }} - SSLHost = "{{ $headers.SSLHost }}" - STSSeconds = {{ $headers.STSSeconds }} - STSIncludeSubdomains = {{ $headers.STSIncludeSubdomains }} - STSPreload = {{ $headers.STSPreload }} - ForceSTSHeader = {{ $headers.ForceSTSHeader }} - FrameDeny = {{ $headers.FrameDeny }} - CustomFrameOptionsValue = "{{ $headers.CustomFrameOptionsValue }}" - ContentTypeNosniff = {{ $headers.ContentTypeNosniff }} - BrowserXSSFilter = {{ $headers.BrowserXSSFilter }} - CustomBrowserXSSValue = "{{ $headers.CustomBrowserXSSValue }}" - ContentSecurityPolicy = "{{ $headers.ContentSecurityPolicy }}" - PublicKey = "{{ $headers.PublicKey }}" - ReferrerPolicy = "{{ $headers.ReferrerPolicy }}" - IsDevelopment = {{ $headers.IsDevelopment }} - - {{if $headers.AllowedHosts }} - AllowedHosts = [{{range $headers.AllowedHosts }} - "{{.}}", - {{end}}] - {{end}} - - {{if $headers.HostsProxyHeaders }} - HostsProxyHeaders = [{{range $headers.HostsProxyHeaders }} - "{{.}}", - {{end}}] - {{end}} - - {{if $headers.CustomRequestHeaders }} - [frontends."frontend-{{ $ServiceFrontendName }}".headers.customRequestHeaders] - {{range $k, $v := $headers.CustomRequestHeaders }} - {{$k}} = "{{$v}}" - {{end}} - {{end}} - - {{if $headers.CustomResponseHeaders }} - [frontends."frontend-{{ $ServiceFrontendName }}".headers.customResponseHeaders] - {{range $k, $v := $headers.CustomResponseHeaders }} - {{$k}} = "{{$v}}" - {{end}} - {{end}} - - {{if $headers.SSLProxyHeaders }} - [frontends."frontend-{{ $ServiceFrontendName }}".headers.SSLProxyHeaders] - {{range $k, $v := $headers.SSLProxyHeaders }} - {{$k}} = "{{$v}}" - {{end}} - {{end}} - {{end}} - - [frontends."frontend-{{ $ServiceFrontendName }}".routes."service-{{ $serviceName | replace "/" "" | replace "." "-" }}"] - rule = "{{ getServiceFrontendRule $container $serviceName }}" - - {{end}} ## end range services - - {{else}} + {{ $container := index $containers 0 }} [frontends."frontend-{{ $frontendName }}"] backend = "backend-{{ getBackendName $container }}" - priority = {{ getPriority $container }} - passHostHeader = {{ getPassHostHeader $container }} - passTLSCert = {{ getPassTLSCert $container }} + priority = {{ getPriority $container.SegmentLabels }} + passHostHeader = {{ getPassHostHeader $container.SegmentLabels }} + passTLSCert = {{ getPassTLSCert $container.SegmentLabels }} - entryPoints = [{{range getEntryPoints $container }} + entryPoints = [{{range getEntryPoints $container.SegmentLabels }} "{{.}}", {{end}}] - {{ $whitelistSourceRange := getWhitelistSourceRange $container}} + {{ $whitelistSourceRange := getWhitelistSourceRange $container.SegmentLabels }} {{if $whitelistSourceRange }} whitelistSourceRange = [{{range $whitelistSourceRange }} "{{.}}", {{end}}] {{end}} - basicAuth = [{{range getBasicAuth $container }} + basicAuth = [{{range getBasicAuth $container.SegmentLabels }} "{{.}}", {{end}}] - {{ $redirect := getRedirect $container }} + {{ $redirect := getRedirect $container.SegmentLabels }} {{if $redirect }} [frontends."frontend-{{ $frontendName }}".redirect] entryPoint = "{{ $redirect.EntryPoint }}" @@ -465,7 +543,7 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} permanent = {{ $redirect.Permanent }} {{end}} - {{ $errorPages := getErrorPages $container }} + {{ $errorPages := getErrorPages $container.SegmentLabels }} {{if $errorPages }} [frontends."frontend-{{ $frontendName }}".errors] {{range $pageName, $page := $errorPages }} @@ -478,12 +556,12 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} {{end}} {{end}} - {{ $rateLimit := getRateLimit $container }} + {{ $rateLimit := getRateLimit $container.SegmentLabels }} {{if $rateLimit }} [frontends."frontend-{{ $frontendName }}".rateLimit] extractorFunc = "{{ $rateLimit.ExtractorFunc }}" [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet] - {{range $limitName, $limit := $rateLimit.RateSet }} + {{ range $limitName, $limit := $rateLimit.RateSet }} [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet."{{ $limitName }}"] period = "{{ $limit.Period }}" average = {{ $limit.Average }} @@ -491,7 +569,7 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} {{end}} {{end}} - {{ $headers := getHeaders $container }} + {{ $headers := getHeaders $container.SegmentLabels }} {{if $headers }} [frontends."frontend-{{ $frontendName }}".headers] SSLRedirect = {{ $headers.SSLRedirect }} @@ -505,8 +583,8 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} CustomFrameOptionsValue = "{{ $headers.CustomFrameOptionsValue }}" ContentTypeNosniff = {{ $headers.ContentTypeNosniff }} BrowserXSSFilter = {{ $headers.BrowserXSSFilter }} - CustomBrowserXSSValue = "{{ $headers.CustomBrowserXSSValue }}" ContentSecurityPolicy = "{{ $headers.ContentSecurityPolicy }}" + CustomBrowserXSSValue = "{{ $headers.CustomBrowserXSSValue }}" PublicKey = "{{ $headers.PublicKey }}" ReferrerPolicy = "{{ $headers.ReferrerPolicy }}" IsDevelopment = {{ $headers.IsDevelopment }} @@ -543,13 +621,12 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} {{$k}} = "{{$v}}" {{end}} {{end}} + {{end}} [frontends."frontend-{{ $frontendName }}".routes."route-frontend-{{ $frontendName }}"] rule = "{{ getFrontendRule $container }}" - {{end}} - {{end}} `) @@ -1833,6 +1910,7 @@ func AssetNames() []string { // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "templates/consul_catalog.tmpl": templatesConsul_catalogTmpl, + "templates/docker-v1.tmpl": templatesDockerV1Tmpl, "templates/docker.tmpl": templatesDockerTmpl, "templates/ecs.tmpl": templatesEcsTmpl, "templates/eureka.tmpl": templatesEurekaTmpl, @@ -1887,6 +1965,7 @@ type bintree struct { var _bintree = &bintree{nil, map[string]*bintree{ "templates": {nil, map[string]*bintree{ "consul_catalog.tmpl": {templatesConsul_catalogTmpl, map[string]*bintree{}}, + "docker-v1.tmpl": {templatesDockerV1Tmpl, map[string]*bintree{}}, "docker.tmpl": {templatesDockerTmpl, map[string]*bintree{}}, "ecs.tmpl": {templatesEcsTmpl, map[string]*bintree{}}, "eureka.tmpl": {templatesEurekaTmpl, map[string]*bintree{}}, diff --git a/cmd/configuration.go b/cmd/configuration.go index eeacaf7c2..49c9dd742 100644 --- a/cmd/configuration.go +++ b/cmd/configuration.go @@ -39,7 +39,7 @@ type TraefikConfiguration struct { // NewTraefikDefaultPointersConfiguration creates a TraefikConfiguration with pointers default values func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { - //default Docker + // default Docker var defaultDocker docker.Provider defaultDocker.Watch = true defaultDocker.ExposedByDefault = true @@ -49,7 +49,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { // default File var defaultFile file.Provider defaultFile.Watch = true - defaultFile.Filename = "" //needs equivalent to viper.ConfigFileUsed() + defaultFile.Filename = "" // needs equivalent to viper.ConfigFileUsed() // default Rest var defaultRest rest.Provider @@ -113,21 +113,21 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { defaultEtcd.Prefix = "/traefik" defaultEtcd.Constraints = types.Constraints{} - //default Zookeeper + // default Zookeeper var defaultZookeeper zk.Provider defaultZookeeper.Watch = true defaultZookeeper.Endpoint = "127.0.0.1:2181" defaultZookeeper.Prefix = "traefik" defaultZookeeper.Constraints = types.Constraints{} - //default Boltdb + // default Boltdb var defaultBoltDb boltdb.Provider defaultBoltDb.Watch = true defaultBoltDb.Endpoint = "127.0.0.1:4001" defaultBoltDb.Prefix = "/traefik" defaultBoltDb.Constraints = types.Constraints{} - //default Kubernetes + // default Kubernetes var defaultKubernetes kubernetes.Provider defaultKubernetes.Watch = true defaultKubernetes.Constraints = types.Constraints{} @@ -142,7 +142,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { defaultMesos.ZkDetectionTimeout = 30 defaultMesos.StateTimeoutSecond = 30 - //default ECS + // default ECS var defaultECS ecs.Provider defaultECS.Watch = true defaultECS.ExposedByDefault = true @@ -151,7 +151,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { defaultECS.RefreshSeconds = 15 defaultECS.Constraints = types.Constraints{} - //default Rancher + // default Rancher var defaultRancher rancher.Provider defaultRancher.Watch = true defaultRancher.ExposedByDefault = true diff --git a/configuration/configuration.go b/configuration/configuration.go index 4a85f7544..3f2c9651e 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -201,6 +201,14 @@ func (gc *GlobalConfiguration) SetEffectiveConfiguration(configFile string) { gc.LifeCycle.GraceTimeOut = gc.GraceTimeOut } + if gc.Docker != nil { + if len(gc.Docker.Filename) != 0 && gc.Docker.TemplateVersion != 2 { + gc.Docker.TemplateVersion = 1 + } else { + gc.Docker.TemplateVersion = 2 + } + } + if gc.Eureka != nil { if gc.Eureka.Delay != 0 { log.Warn("Delay has been deprecated -- please use RefreshSeconds") diff --git a/docs/configuration/backends/docker.md b/docs/configuration/backends/docker.md index a56de8341..8a84f1a0a 100644 --- a/docs/configuration/backends/docker.md +++ b/docs/configuration/backends/docker.md @@ -39,6 +39,15 @@ watch = true # # filename = "docker.tmpl" +# Override template version +# For advanced users :) +# +# Optional +# - "1": previous template version (must be used only with older custom templates, see "filename") +# - "2": current template version (must be used to force template version when "filename" is used) +# +# templateVersion = "2" + # Expose containers by default in Traefik. # If set to false, containers that don't have `traefik.enable=true` will be ignored. # @@ -123,6 +132,15 @@ swarmmode = true # # filename = "docker.tmpl" +# Override template version +# For advanced users :) +# +# Optional +# - "1": previous template version (must be used only with older custom templates, see "filename") +# - "2": current template version (must be used to force template version when "filename" is used) +# +# templateVersion = "2" + # Expose services by default in Traefik. # # Optional @@ -254,70 +272,74 @@ Or if your service references external network use it's name instead. | `traefik.frontend.headers.referrerPolicy=VALUE` | Adds referrer policy header. | | `traefik.frontend.headers.isDevelopment=false` | This will cause the `AllowedHosts`, `SSLRedirect`, and `STSSeconds`/`STSIncludeSubdomains` options to be ignored during development.
When deploying to production, be sure to set this to false. | -### On Service +### On containers with Multiple Ports (segment labels) -Services labels can be used for overriding default behaviour +Segment labels are used to define routes to a container exposing multiple ports. +A segment is a group of labels that apply to a port exposed by a container. +You can define as many segments as ports exposed in a container. + +Segment labels override the default behavior. | Label | Description | |---------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------| -| `traefik..port=PORT` | Overrides `traefik.port`. If several ports need to be exposed, the service labels could be used. | -| `traefik..protocol` | Overrides `traefik.protocol`. | -| `traefik..weight` | Assign this service weight. Overrides `traefik.weight`. | -| `traefik..frontend.auth.basic` | Sets a Basic Auth for that frontend | -| `traefik..frontend.backend=BACKEND` | Assign this service frontend to `BACKEND`. Default is to assign to the service backend. | -| `traefik..frontend.entryPoints` | Overrides `traefik.frontend.entrypoints` | -| `traefik..frontend.errors..backend=NAME` | See [custom error pages](/configuration/commons/#custom-error-pages) section. | -| `traefik..frontend.errors..query=PATH` | See [custom error pages](/configuration/commons/#custom-error-pages) section. | -| `traefik..frontend.errors..status=RANGE` | See [custom error pages](/configuration/commons/#custom-error-pages) section. | -| `traefik..frontend.passHostHeader` | Overrides `traefik.frontend.passHostHeader`. | -| `traefik..frontend.passTLSCert` | Overrides `traefik.frontend.passTLSCert`. | -| `traefik..frontend.priority` | Overrides `traefik.frontend.priority`. | -| `traefik..frontend.rateLimit.extractorFunc=EXP` | See [rate limiting](/configuration/commons/#rate-limiting) section. | -| `traefik..frontend.rateLimit.rateSet..period=6` | See [rate limiting](/configuration/commons/#rate-limiting) section. | -| `traefik..frontend.rateLimit.rateSet..average=6` | See [rate limiting](/configuration/commons/#rate-limiting) section. | -| `traefik..frontend.rateLimit.rateSet..burst=6` | See [rate limiting](/configuration/commons/#rate-limiting) section. | -| `traefik..frontend.redirect.entryPoint=https` | Overrides `traefik.frontend.redirect.entryPoint`. | -| `traefik..frontend.redirect.regex=^http://localhost/(.*)` | Overrides `traefik.frontend.redirect.regex`. | -| `traefik..frontend.redirect.replacement=http://mydomain/$1` | Overrides `traefik.frontend.redirect.replacement`. | -| `traefik..frontend.redirect.permanent=true` | Return 301 instead of 302. | -| `traefik..frontend.rule` | Overrides `traefik.frontend.rule`. | -| `traefik..frontend.whitelistSourceRange=RANGE` | Overrides `traefik.frontend.whitelistSourceRange`. | +| `traefik..port=PORT` | Overrides `traefik.port`. If several ports need to be exposed, the segment labels could be used. | +| `traefik..protocol` | Overrides `traefik.protocol`. | +| `traefik..weight` | Assign this segment weight. Overrides `traefik.weight`. | +| `traefik..frontend.auth.basic` | Sets a Basic Auth for that frontend | +| `traefik..frontend.backend=BACKEND` | Assign this segment frontend to `BACKEND`. Default is to assign to the segment backend. | +| `traefik..frontend.entryPoints` | Overrides `traefik.frontend.entrypoints` | +| `traefik..frontend.errors..backend=NAME` | See [custom error pages](/configuration/commons/#custom-error-pages) section. | +| `traefik..frontend.errors..query=PATH` | See [custom error pages](/configuration/commons/#custom-error-pages) section. | +| `traefik..frontend.errors..status=RANGE` | See [custom error pages](/configuration/commons/#custom-error-pages) section. | +| `traefik..frontend.passHostHeader` | Overrides `traefik.frontend.passHostHeader`. | +| `traefik..frontend.passTLSCert` | Overrides `traefik.frontend.passTLSCert`. | +| `traefik..frontend.priority` | Overrides `traefik.frontend.priority`. | +| `traefik..frontend.rateLimit.extractorFunc=EXP` | See [rate limiting](/configuration/commons/#rate-limiting) section. | +| `traefik..frontend.rateLimit.rateSet..period=6` | See [rate limiting](/configuration/commons/#rate-limiting) section. | +| `traefik..frontend.rateLimit.rateSet..average=6` | See [rate limiting](/configuration/commons/#rate-limiting) section. | +| `traefik..frontend.rateLimit.rateSet..burst=6` | See [rate limiting](/configuration/commons/#rate-limiting) section. | +| `traefik..frontend.redirect.entryPoint=https` | Overrides `traefik.frontend.redirect.entryPoint`. | +| `traefik..frontend.redirect.regex=^http://localhost/(.*)` | Overrides `traefik.frontend.redirect.regex`. | +| `traefik..frontend.redirect.replacement=http://mydomain/$1` | Overrides `traefik.frontend.redirect.replacement`. | +| `traefik..frontend.redirect.permanent=true` | Return 301 instead of 302. | +| `traefik..frontend.rule` | Overrides `traefik.frontend.rule`. | +| `traefik..frontend.whitelistSourceRange=RANGE` | Overrides `traefik.frontend.whitelistSourceRange`. | #### Custom Headers | Label | Description | |----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `traefik..frontend.headers.customRequestHeaders=EXPR ` | Provides the container with custom request headers that will be appended to each request forwarded to the container.
Format: HEADER:value||HEADER2:value2 | -| `traefik..frontend.headers.customResponseHeaders=EXPR` | Appends the headers to each response returned by the container, before forwarding the response to the client.
Format: HEADER:value||HEADER2:value2 | +| `traefik..frontend.headers.customRequestHeaders=EXPR ` | Provides the container with custom request headers that will be appended to each request forwarded to the container.
Format: HEADER:value||HEADER2:value2 | +| `traefik..frontend.headers.customResponseHeaders=EXPR` | Appends the headers to each response returned by the container, before forwarding the response to the client.
Format: HEADER:value||HEADER2:value2 | #### Security Headers | Label | Description | |-------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `traefik..frontend.headers.allowedHosts=EXPR` | Provides a list of allowed hosts that requests will be processed.
Format: `Host1,Host2` | -| `traefik..frontend.headers.hostsProxyHeaders=EXPR ` | Provides a list of headers that the proxied hostname may be stored.
Format: `HEADER1,HEADER2` | -| `traefik..frontend.headers.SSLRedirect=true` | Forces the frontend to redirect to SSL if a non-SSL request is sent. | -| `traefik..frontend.headers.SSLTemporaryRedirect=true` | Forces the frontend to redirect to SSL if a non-SSL request is sent, but by sending a 302 instead of a 301. | -| `traefik..frontend.headers.SSLHost=HOST` | This setting configures the hostname that redirects will be based on. Default is "", which is the same host as the request. | -| `traefik..frontend.headers.SSLProxyHeaders=EXPR` | Header combinations that would signify a proper SSL Request (Such as `X-Forwarded-For:https`).
Format: HEADER:value||HEADER2:value2 | -| `traefik..frontend.headers.STSSeconds=315360000` | Sets the max-age of the STS header. | -| `traefik..frontend.headers.STSIncludeSubdomains=true` | Adds the `IncludeSubdomains` section of the STS header. | -| `traefik..frontend.headers.STSPreload=true` | Adds the preload flag to the STS header. | -| `traefik..frontend.headers.forceSTSHeader=false` | Adds the STS header to non-SSL requests. | -| `traefik..frontend.headers.frameDeny=false` | Adds the `X-Frame-Options` header with the value of `DENY`. | -| `traefik..frontend.headers.customFrameOptionsValue=VALUE` | Overrides the `X-Frame-Options` header with the custom value. | -| `traefik..frontend.headers.contentTypeNosniff=true` | Adds the `X-Content-Type-Options` header with the value `nosniff`. | -| `traefik..frontend.headers.browserXSSFilter=true` | Adds the X-XSS-Protection header with the value `1; mode=block`. | -| `traefik..frontend.headers.customBrowserXSSValue=VALUE` | Set custom value for X-XSS-Protection header. This overrides the BrowserXssFilter option. | -| `traefik..frontend.headers.contentSecurityPolicy=VALUE` | Adds CSP Header with the custom value. | -| `traefik..frontend.headers.publicKey=VALUE` | Adds pinned HTST public key header. | -| `traefik..frontend.headers.referrerPolicy=VALUE` | Adds referrer policy header. | -| `traefik..frontend.headers.isDevelopment=false` | This will cause the `AllowedHosts`, `SSLRedirect`, and `STSSeconds`/`STSIncludeSubdomains` options to be ignored during development.
When deploying to production, be sure to set this to false. | +| `traefik..frontend.headers.allowedHosts=EXPR` | Provides a list of allowed hosts that requests will be processed.
Format: `Host1,Host2` | +| `traefik..frontend.headers.hostsProxyHeaders=EXPR ` | Provides a list of headers that the proxied hostname may be stored.
Format: `HEADER1,HEADER2` | +| `traefik..frontend.headers.SSLRedirect=true` | Forces the frontend to redirect to SSL if a non-SSL request is sent. | +| `traefik..frontend.headers.SSLTemporaryRedirect=true` | Forces the frontend to redirect to SSL if a non-SSL request is sent, but by sending a 302 instead of a 301. | +| `traefik..frontend.headers.SSLHost=HOST` | This setting configures the hostname that redirects will be based on. Default is "", which is the same host as the request. | +| `traefik..frontend.headers.SSLProxyHeaders=EXPR` | Header combinations that would signify a proper SSL Request (Such as `X-Forwarded-For:https`).
Format: HEADER:value||HEADER2:value2 | +| `traefik..frontend.headers.STSSeconds=315360000` | Sets the max-age of the STS header. | +| `traefik..frontend.headers.STSIncludeSubdomains=true` | Adds the `IncludeSubdomains` section of the STS header. | +| `traefik..frontend.headers.STSPreload=true` | Adds the preload flag to the STS header. | +| `traefik..frontend.headers.forceSTSHeader=false` | Adds the STS header to non-SSL requests. | +| `traefik..frontend.headers.frameDeny=false` | Adds the `X-Frame-Options` header with the value of `DENY`. | +| `traefik..frontend.headers.customFrameOptionsValue=VALUE` | Overrides the `X-Frame-Options` header with the custom value. | +| `traefik..frontend.headers.contentTypeNosniff=true` | Adds the `X-Content-Type-Options` header with the value `nosniff`. | +| `traefik..frontend.headers.browserXSSFilter=true` | Adds the X-XSS-Protection header with the value `1; mode=block`. | +| `traefik..frontend.headers.customBrowserXSSValue=VALUE` | Set custom value for X-XSS-Protection header. This overrides the BrowserXssFilter option. | +| `traefik..frontend.headers.contentSecurityPolicy=VALUE` | Adds CSP Header with the custom value. | +| `traefik..frontend.headers.publicKey=VALUE` | Adds pinned HTST public key header. | +| `traefik..frontend.headers.referrerPolicy=VALUE` | Adds referrer policy header. | +| `traefik..frontend.headers.isDevelopment=false` | This will cause the `AllowedHosts`, `SSLRedirect`, and `STSSeconds`/`STSIncludeSubdomains` options to be ignored during development.
When deploying to production, be sure to set this to false. | !!! note - If a label is defined both as a `container label` and a `service label` (for example `traefik..port=PORT` and `traefik.port=PORT` ), the `service label` is used to defined the `` property (`port` in the example). + If a label is defined both as a `container label` and a `segment label` (for example `traefik..port=PORT` and `traefik.port=PORT` ), the `segment label` is used to defined the `` property (`port` in the example). - It's possible to mix `container labels` and `service labels`, in this case `container labels` are used as default value for missing `service labels` but no frontends are going to be created with the `container labels`. + It's possible to mix `container labels` and `segment labels`, in this case `container labels` are used as default value for missing `segment labels` but no frontends are going to be created with the `container labels`. More details in this [example](/user-guide/docker-and-lets-encrypt/#labels). diff --git a/docs/configuration/backends/marathon.md b/docs/configuration/backends/marathon.md index 45ab426e6..3100b8926 100644 --- a/docs/configuration/backends/marathon.md +++ b/docs/configuration/backends/marathon.md @@ -150,15 +150,15 @@ domain = "marathon.localhost" To enable constraints see [backend-specific constraints section](/configuration/commons/#backend-specific). -## Labels: overriding default behaviour +## Labels: overriding default behavior -Marathon labels may be used to dynamically change the routing and forwarding behaviour. +Marathon labels may be used to dynamically change the routing and forwarding behavior. They may be specified on one of two levels: Application or service. ### Application Level -The following labels can be defined on Marathon applications. They adjust the behaviour for the entire application. +The following labels can be defined on Marathon applications. They adjust the behavior for the entire application. | Label | Description | |------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -234,63 +234,65 @@ The following labels can be defined on Marathon applications. They adjust the be | `traefik.frontend.headers.referrerPolicy=VALUE` | Adds referrer policy header. | | `traefik.frontend.headers.isDevelopment=false` | This will cause the `AllowedHosts`, `SSLRedirect`, and `STSSeconds`/`STSIncludeSubdomains` options to be ignored during development.
When deploying to production, be sure to set this to false. | -### Service Level +### Applications with Multiple Ports (segment labels) -For applications that expose multiple ports, specific labels can be used to extract one frontend/backend configuration pair per port. Each such pair is called a _service_. The (freely choosable) name of the service is an integral part of the service label name. +Segment labels are used to define routes to an application exposing multiple ports. +A segment is a group of labels that apply to a port exposed by an application. +You can define as many segments as ports exposed in an application. | Label | Description | |---------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------| -| `traefik..portIndex=1` | Create a service binding with frontend/backend using this port index. Overrides `traefik.portIndex`. | -| `traefik..port=PORT` | Overrides `traefik.port`. If several ports need to be exposed, the service labels could be used. | -| `traefik..protocol=http` | Overrides `traefik.protocol`. | -| `traefik..weight=10` | Assign this service weight. Overrides `traefik.weight`. | -| `traefik..frontend.auth.basic=EXPR` | Sets a Basic Auth for that frontend | -| `traefik..frontend.backend=BACKEND` | Assign this service frontend to `BACKEND`. Default is to assign to the service backend. | -| `traefik..frontend.entryPoints=https` | Overrides `traefik.frontend.entrypoints` | -| `traefik..frontend.errors..backend=NAME` | See [custom error pages](/configuration/commons/#custom-error-pages) section. | -| `traefik..frontend.errors..query=PATH` | See [custom error pages](/configuration/commons/#custom-error-pages) section. | -| `traefik..frontend.errors..status=RANGE` | See [custom error pages](/configuration/commons/#custom-error-pages) section. | -| `traefik..frontend.passHostHeader=true` | Overrides `traefik.frontend.passHostHeader`. | -| `traefik..frontend.passTLSCert=true` | Overrides `traefik.frontend.passTLSCert`. | -| `traefik..frontend.priority=10` | Overrides `traefik.frontend.priority`. | -| `traefik..frontend.rateLimit.extractorFunc=EXP` | See [rate limiting](/configuration/commons/#rate-limiting) section. | -| `traefik..frontend.rateLimit.rateSet..period=6` | See [rate limiting](/configuration/commons/#rate-limiting) section. | -| `traefik..frontend.rateLimit.rateSet..average=6` | See [rate limiting](/configuration/commons/#rate-limiting) section. | -| `traefik..frontend.rateLimit.rateSet..burst=6` | See [rate limiting](/configuration/commons/#rate-limiting) section. | -| `traefik..frontend.redirect.entryPoint=https` | Overrides `traefik.frontend.redirect.entryPoint`. | -| `traefik..frontend.redirect.regex=^http://localhost/(.*)` | Overrides `traefik.frontend.redirect.regex`. | -| `traefik..frontend.redirect.replacement=http://mydomain/$1` | Overrides `traefik.frontend.redirect.replacement`. | -| `traefik..frontend.redirect.permanent=true` | Return 301 instead of 302. | -| `traefik..frontend.rule=EXP` | Overrides `traefik.frontend.rule`. Default: `{service_name}.{sub_domain}.{domain}` | -| `traefik..frontend.whitelistSourceRange=RANGE` | Overrides `traefik.frontend.whitelistSourceRange`. | +| `traefik..portIndex=1` | Create a service binding with frontend/backend using this port index. Overrides `traefik.portIndex`. | +| `traefik..port=PORT` | Overrides `traefik.port`. If several ports need to be exposed, the service labels could be used. | +| `traefik..protocol=http` | Overrides `traefik.protocol`. | +| `traefik..weight=10` | Assign this service weight. Overrides `traefik.weight`. | +| `traefik..frontend.auth.basic=EXPR` | Sets a Basic Auth for that frontend | +| `traefik..frontend.backend=BACKEND` | Assign this service frontend to `BACKEND`. Default is to assign to the service backend. | +| `traefik..frontend.entryPoints=https` | Overrides `traefik.frontend.entrypoints` | +| `traefik..frontend.errors..backend=NAME` | See [custom error pages](/configuration/commons/#custom-error-pages) section. | +| `traefik..frontend.errors..query=PATH` | See [custom error pages](/configuration/commons/#custom-error-pages) section. | +| `traefik..frontend.errors..status=RANGE` | See [custom error pages](/configuration/commons/#custom-error-pages) section. | +| `traefik..frontend.passHostHeader=true` | Overrides `traefik.frontend.passHostHeader`. | +| `traefik..frontend.passTLSCert=true` | Overrides `traefik.frontend.passTLSCert`. | +| `traefik..frontend.priority=10` | Overrides `traefik.frontend.priority`. | +| `traefik..frontend.rateLimit.extractorFunc=EXP` | See [rate limiting](/configuration/commons/#rate-limiting) section. | +| `traefik..frontend.rateLimit.rateSet..period=6` | See [rate limiting](/configuration/commons/#rate-limiting) section. | +| `traefik..frontend.rateLimit.rateSet..average=6` | See [rate limiting](/configuration/commons/#rate-limiting) section. | +| `traefik..frontend.rateLimit.rateSet..burst=6` | See [rate limiting](/configuration/commons/#rate-limiting) section. | +| `traefik..frontend.redirect.entryPoint=https` | Overrides `traefik.frontend.redirect.entryPoint`. | +| `traefik..frontend.redirect.regex=^http://localhost/(.*)` | Overrides `traefik.frontend.redirect.regex`. | +| `traefik..frontend.redirect.replacement=http://mydomain/$1` | Overrides `traefik.frontend.redirect.replacement`. | +| `traefik..frontend.redirect.permanent=true` | Return 301 instead of 302. | +| `traefik..frontend.rule=EXP` | Overrides `traefik.frontend.rule`. Default: `{service_name}.{sub_domain}.{domain}` | +| `traefik..frontend.whitelistSourceRange=RANGE` | Overrides `traefik.frontend.whitelistSourceRange`. | #### Custom Headers | Label | Description | |----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `traefik..frontend.headers.customRequestHeaders=EXPR ` | Provides the container with custom request headers that will be appended to each request forwarded to the container.
Format: HEADER:value||HEADER2:value2 | -| `traefik..frontend.headers.customResponseHeaders=EXPR` | Appends the headers to each response returned by the container, before forwarding the response to the client.
Format: HEADER:value||HEADER2:value2 | +| `traefik..frontend.headers.customRequestHeaders=EXPR ` | Provides the container with custom request headers that will be appended to each request forwarded to the container.
Format: HEADER:value||HEADER2:value2 | +| `traefik..frontend.headers.customResponseHeaders=EXPR` | Appends the headers to each response returned by the container, before forwarding the response to the client.
Format: HEADER:value||HEADER2:value2 | #### Security Headers | Label | Description | |-------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `traefik..frontend.headers.allowedHosts=EXPR` | Provides a list of allowed hosts that requests will be processed.
Format: `Host1,Host2` | -| `traefik..frontend.headers.hostsProxyHeaders=EXPR ` | Provides a list of headers that the proxied hostname may be stored.
Format: `HEADER1,HEADER2` | -| `traefik..frontend.headers.SSLRedirect=true` | Forces the frontend to redirect to SSL if a non-SSL request is sent. | -| `traefik..frontend.headers.SSLTemporaryRedirect=true` | Forces the frontend to redirect to SSL if a non-SSL request is sent, but by sending a 302 instead of a 301. | -| `traefik..frontend.headers.SSLHost=HOST` | This setting configures the hostname that redirects will be based on. Default is "", which is the same host as the request. | -| `traefik..frontend.headers.SSLProxyHeaders=EXPR` | Header combinations that would signify a proper SSL Request (Such as `X-Forwarded-For:https`).
Format: HEADER:value||HEADER2:value2 | -| `traefik..frontend.headers.STSSeconds=315360000` | Sets the max-age of the STS header. | -| `traefik..frontend.headers.STSIncludeSubdomains=true` | Adds the `IncludeSubdomains` section of the STS header. | -| `traefik..frontend.headers.STSPreload=true` | Adds the preload flag to the STS header. | -| `traefik..frontend.headers.forceSTSHeader=false` | Adds the STS header to non-SSL requests. | -| `traefik..frontend.headers.frameDeny=false` | Adds the `X-Frame-Options` header with the value of `DENY`. | -| `traefik..frontend.headers.customFrameOptionsValue=VALUE` | Overrides the `X-Frame-Options` header with the custom value. | -| `traefik..frontend.headers.contentTypeNosniff=true` | Adds the `X-Content-Type-Options` header with the value `nosniff`. | -| `traefik..frontend.headers.browserXSSFilter=true` | Adds the X-XSS-Protection header with the value `1; mode=block`. | -| `traefik..frontend.headers.customBrowserXSSValue=VALUE` | Set custom value for X-XSS-Protection header. This overrides the BrowserXssFilter option. | -| `traefik..frontend.headers.contentSecurityPolicy=VALUE` | Adds CSP Header with the custom value. | -| `traefik..frontend.headers.publicKey=VALUE` | Adds pinned HTST public key header. | -| `traefik..frontend.headers.referrerPolicy=VALUE` | Adds referrer policy header. | -| `traefik..frontend.headers.isDevelopment=false` | This will cause the `AllowedHosts`, `SSLRedirect`, and `STSSeconds`/`STSIncludeSubdomains` options to be ignored during development.
When deploying to production, be sure to set this to false. | +| `traefik..frontend.headers.allowedHosts=EXPR` | Provides a list of allowed hosts that requests will be processed.
Format: `Host1,Host2` | +| `traefik..frontend.headers.hostsProxyHeaders=EXPR ` | Provides a list of headers that the proxied hostname may be stored.
Format: `HEADER1,HEADER2` | +| `traefik..frontend.headers.SSLRedirect=true` | Forces the frontend to redirect to SSL if a non-SSL request is sent. | +| `traefik..frontend.headers.SSLTemporaryRedirect=true` | Forces the frontend to redirect to SSL if a non-SSL request is sent, but by sending a 302 instead of a 301. | +| `traefik..frontend.headers.SSLHost=HOST` | This setting configures the hostname that redirects will be based on. Default is "", which is the same host as the request. | +| `traefik..frontend.headers.SSLProxyHeaders=EXPR` | Header combinations that would signify a proper SSL Request (Such as `X-Forwarded-For:https`).
Format: HEADER:value||HEADER2:value2 | +| `traefik..frontend.headers.STSSeconds=315360000` | Sets the max-age of the STS header. | +| `traefik..frontend.headers.STSIncludeSubdomains=true` | Adds the `IncludeSubdomains` section of the STS header. | +| `traefik..frontend.headers.STSPreload=true` | Adds the preload flag to the STS header. | +| `traefik..frontend.headers.forceSTSHeader=false` | Adds the STS header to non-SSL requests. | +| `traefik..frontend.headers.frameDeny=false` | Adds the `X-Frame-Options` header with the value of `DENY`. | +| `traefik..frontend.headers.customFrameOptionsValue=VALUE` | Overrides the `X-Frame-Options` header with the custom value. | +| `traefik..frontend.headers.contentTypeNosniff=true` | Adds the `X-Content-Type-Options` header with the value `nosniff`. | +| `traefik..frontend.headers.browserXSSFilter=true` | Adds the X-XSS-Protection header with the value `1; mode=block`. | +| `traefik..frontend.headers.customBrowserXSSValue=VALUE` | Set custom value for X-XSS-Protection header. This overrides the BrowserXssFilter option. | +| `traefik..frontend.headers.contentSecurityPolicy=VALUE` | Adds CSP Header with the custom value. | +| `traefik..frontend.headers.publicKey=VALUE` | Adds pinned HTST public key header. | +| `traefik..frontend.headers.referrerPolicy=VALUE` | Adds referrer policy header. | +| `traefik..frontend.headers.isDevelopment=false` | This will cause the `AllowedHosts`, `SSLRedirect`, and `STSSeconds`/`STSIncludeSubdomains` options to be ignored during development.
When deploying to production, be sure to set this to false. | diff --git a/provider/docker/config.go b/provider/docker/config.go index 1942ccc69..c584f07bb 100644 --- a/provider/docker/config.go +++ b/provider/docker/config.go @@ -1,56 +1,45 @@ package docker import ( + "context" + "fmt" "math" "strconv" + "strings" "text/template" "github.com/BurntSushi/ty/fun" "github.com/containous/traefik/log" + "github.com/containous/traefik/provider" "github.com/containous/traefik/provider/label" "github.com/containous/traefik/types" + "github.com/docker/go-connections/nat" ) -func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.Configuration { - var DockerFuncMap = template.FuncMap{ - "getDomain": getFuncStringLabel(label.TraefikDomain, p.Domain), +const ( + labelDockerNetwork = "traefik.docker.network" + labelBackendLoadBalancerSwarm = "traefik.backend.loadbalancer.swarm" + labelDockerComposeProject = "com.docker.compose.project" + labelDockerComposeService = "com.docker.compose.service" +) + +func (p *Provider) buildConfigurationV2(containersInspected []dockerData) *types.Configuration { + dockerFuncMap := template.FuncMap{ + "getLabelValue": label.GetStringValue, "getSubDomain": getSubDomain, - "isBackendLBSwarm": isBackendLBSwarm, // FIXME dead ? + "isBackendLBSwarm": isBackendLBSwarm, + "getDomain": getFuncStringLabel(label.TraefikDomain, p.Domain), // Backend functions "getIPAddress": p.getIPAddress, - "getPort": getPort, - "getWeight": getFuncIntLabel(label.TraefikWeight, label.DefaultWeightInt), - "getProtocol": getFuncStringLabel(label.TraefikProtocol, label.DefaultProtocol), + "getServers": p.getServers, "getMaxConn": getMaxConn, "getHealthCheck": getHealthCheck, "getBuffering": getBuffering, "getCircuitBreaker": getCircuitBreaker, "getLoadBalancer": getLoadBalancer, - // TODO Deprecated [breaking] - "hasCircuitBreakerLabel": hasFunc(label.TraefikBackendCircuitBreakerExpression), - // TODO Deprecated [breaking] - "getCircuitBreakerExpression": getFuncStringLabel(label.TraefikBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression), - // TODO Deprecated [breaking] - "hasLoadBalancerLabel": hasLoadBalancerLabel, - // TODO Deprecated [breaking] - "getLoadBalancerMethod": getFuncStringLabel(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), - // TODO Deprecated [breaking] - "hasMaxConnLabels": hasMaxConnLabels, - // TODO Deprecated [breaking] - "getMaxConnAmount": getFuncInt64Label(label.TraefikBackendMaxConnAmount, math.MaxInt64), - // TODO Deprecated [breaking] - "getMaxConnExtractorFunc": getFuncStringLabel(label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc), - // TODO Deprecated [breaking] - "getSticky": getSticky, - // TODO Deprecated [breaking] - "hasStickinessLabel": hasFunc(label.TraefikBackendLoadBalancerStickiness), - // TODO Deprecated [breaking] - "getStickinessCookieName": getFuncStringLabel(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName), - // Frontend functions - "getBackend": getBackendName, // TODO Deprecated [breaking] replaced by getBackendName "getBackendName": getBackendName, "getPriority": getFuncIntLabel(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), "getPassHostHeader": getFuncBoolLabel(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), @@ -59,72 +48,56 @@ func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.C "getBasicAuth": getFuncSliceStringLabel(label.TraefikFrontendAuthBasic), "getWhitelistSourceRange": getFuncSliceStringLabel(label.TraefikFrontendWhitelistSourceRange), "getFrontendRule": p.getFrontendRule, - - "getRedirect": getRedirect, - "getErrorPages": getErrorPages, - "getRateLimit": getRateLimit, - "getHeaders": getHeaders, - - // Services - "hasServices": hasServices, - "getServiceNames": getServiceNames, - "getServiceBackend": getServiceBackendName, // TODO Deprecated [breaking] replaced by getServiceBackendName - "getServiceBackendName": getServiceBackendName, - // Services - Backend server functions - "getServicePort": getServicePort, - "getServiceProtocol": getFuncServiceStringLabel(label.SuffixProtocol, label.DefaultProtocol), - "getServiceWeight": getFuncServiceStringLabel(label.SuffixWeight, label.DefaultWeight), - // Services - Frontend functions - "getServiceEntryPoints": getFuncServiceSliceStringLabel(label.SuffixFrontendEntryPoints), - "getServiceWhitelistSourceRange": getFuncServiceSliceStringLabel(label.SuffixFrontendWhitelistSourceRange), - "getServiceBasicAuth": getFuncServiceSliceStringLabel(label.SuffixFrontendAuthBasic), - "getServiceFrontendRule": p.getServiceFrontendRule, - "getServicePassHostHeader": getFuncServiceBoolLabel(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeaderBool), - "getServicePassTLSCert": getFuncServiceBoolLabel(label.SuffixFrontendPassTLSCert, label.DefaultPassTLSCert), - "getServicePriority": getFuncServiceIntLabel(label.SuffixFrontendPriority, label.DefaultFrontendPriorityInt), - - "getServiceRedirect": getServiceRedirect, - "getServiceErrorPages": getServiceErrorPages, - "getServiceRateLimit": getServiceRateLimit, - "getServiceHeaders": getServiceHeaders, + "getRedirect": getRedirect, + "getErrorPages": getErrorPages, + "getRateLimit": getRateLimit, + "getHeaders": getHeaders, } + // filter containers - filteredContainers := fun.Filter(func(container dockerData) bool { - return p.containerFilter(container) - }, containersInspected).([]dockerData) + filteredContainers := fun.Filter(p.containerFilter, containersInspected).([]dockerData) frontends := map[string][]dockerData{} - backends := map[string]dockerData{} servers := map[string][]dockerData{} + serviceNames := make(map[string]struct{}) + for idx, container := range filteredContainers { - if _, exists := serviceNames[container.ServiceName]; !exists { - frontendName := p.getFrontendName(container, idx) - frontends[frontendName] = append(frontends[frontendName], container) - if len(container.ServiceName) > 0 { - serviceNames[container.ServiceName] = struct{}{} + segmentProperties := label.ExtractTraefikLabels(container.Labels) + for segmentName, labels := range segmentProperties { + container.SegmentLabels = labels + container.SegmentName = segmentName + + // Frontends + if _, exists := serviceNames[container.ServiceName+segmentName]; !exists { + frontendName := p.getFrontendName(container, idx) + frontends[frontendName] = append(frontends[frontendName], container) + if len(container.ServiceName+segmentName) > 0 { + serviceNames[container.ServiceName+segmentName] = struct{}{} + } } + + // Backends + backendName := getBackendName(container) + + // Servers + servers[backendName] = append(servers[backendName], container) } - backendName := getBackendName(container) - backends[backendName] = container - servers[backendName] = append(servers[backendName], container) } templateObjects := struct { Containers []dockerData Frontends map[string][]dockerData - Backends map[string]dockerData Servers map[string][]dockerData Domain string }{ Containers: filteredContainers, Frontends: frontends, - Backends: backends, Servers: servers, Domain: p.Domain, } - configuration, err := p.GetConfiguration("templates/docker.tmpl", DockerFuncMap, templateObjects) + configuration, err := p.GetConfiguration("templates/docker.tmpl", dockerFuncMap, templateObjects) if err != nil { log.Error(err) } @@ -132,29 +105,33 @@ func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.C return configuration } -func (p Provider) containerFilter(container dockerData) bool { +func (p *Provider) containerFilter(container dockerData) bool { if !label.IsEnabled(container.Labels, p.ExposedByDefault) { log.Debugf("Filtering disabled container %s", container.Name) return false } - var err error - portLabel := "traefik.port label" - if hasServices(container) { - portLabel = "traefik..port or " + portLabel + "s" - err = checkServiceLabelPort(container) - } else { - _, err = strconv.Atoi(container.Labels[label.TraefikPort]) + segmentProperties := label.ExtractTraefikLabels(container.Labels) + + var errPort error + for segmentName, labels := range segmentProperties { + errPort = checkSegmentPort(labels, segmentName) + + if len(p.getFrontendRule(container)) == 0 { + log.Debugf("Filtering container with empty frontend rule %s %s", container.Name, segmentName) + return false + } } - if len(container.NetworkSettings.Ports) == 0 && err != nil { - log.Debugf("Filtering container without port and no %s %s : %s", portLabel, container.Name, err.Error()) + + if len(container.NetworkSettings.Ports) == 0 && errPort != nil { + log.Debugf("Filtering container without port, %s: %v", container.Name, errPort) return false } constraintTags := label.SplitAndTrimString(container.Labels[label.TraefikTags], ",") if ok, failingConstraint := p.MatchConstraints(constraintTags); !ok { if failingConstraint != nil { - log.Debugf("Container %v pruned by '%v' constraint", container.Name, failingConstraint.String()) + log.Debugf("Container %s pruned by %q constraint", container.Name, failingConstraint.String()) } return false } @@ -164,10 +141,368 @@ func (p Provider) containerFilter(container dockerData) bool { return false } - if len(p.getFrontendRule(container)) == 0 { - log.Debugf("Filtering container with empty frontend rule %s", container.Name) - return false - } - return true } + +func checkSegmentPort(labels map[string]string, segmentName string) error { + if port, ok := labels[label.TraefikPort]; ok { + _, err := strconv.Atoi(port) + if err != nil { + return fmt.Errorf("invalid port value %q for the segment %q: %v", port, segmentName, err) + } + } else { + return fmt.Errorf("port label is missing, please use %s as default value or define port label for all segments ('traefik..port')", label.TraefikPort) + } + return nil +} + +func (p *Provider) getFrontendName(container dockerData, idx int) string { + var name string + if len(container.SegmentName) > 0 { + name = getBackendName(container) + } else { + name = p.getFrontendRule(container) + "-" + strconv.Itoa(idx) + } + + return provider.Normalize(name) +} + +func (p *Provider) getFrontendRule(container dockerData) string { + if value := label.GetStringValue(container.SegmentLabels, label.TraefikFrontendRule, ""); len(value) != 0 { + return value + } + + if values, err := label.GetStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil { + return "Host:" + getSubDomain(values[labelDockerComposeService]+"."+values[labelDockerComposeProject]) + "." + p.Domain + } + + if len(p.Domain) > 0 { + return "Host:" + getSubDomain(container.ServiceName) + "." + p.Domain + } + + return "" +} + +func (p Provider) getIPAddress(container dockerData) string { + + if value := label.GetStringValue(container.Labels, labelDockerNetwork, ""); value != "" { + networkSettings := container.NetworkSettings + if networkSettings.Networks != nil { + network := networkSettings.Networks[value] + if network != nil { + return network.Addr + } + + log.Warnf("Could not find network named '%s' for container '%s'! Maybe you're missing the project's prefix in the label? Defaulting to first available network.", value, container.Name) + } + } + + if container.NetworkSettings.NetworkMode.IsHost() { + if container.Node != nil { + if container.Node.IPAddress != "" { + return container.Node.IPAddress + } + } + return "127.0.0.1" + } + + if container.NetworkSettings.NetworkMode.IsContainer() { + dockerClient, err := p.createClient() + if err != nil { + log.Warnf("Unable to get IP address for container %s, error: %s", container.Name, err) + return "" + } + + connectedContainer := container.NetworkSettings.NetworkMode.ConnectedContainer() + containerInspected, err := dockerClient.ContainerInspect(context.Background(), connectedContainer) + if err != nil { + log.Warnf("Unable to get IP address for container %s : Failed to inspect container ID %s, error: %s", container.Name, connectedContainer, err) + return "" + } + return p.getIPAddress(parseContainer(containerInspected)) + } + + if p.UseBindPortIP { + port := getPortV1(container) + for netPort, portBindings := range container.NetworkSettings.Ports { + if string(netPort) == port+"/TCP" || string(netPort) == port+"/UDP" { + for _, p := range portBindings { + return p.HostIP + } + } + } + } + + for _, network := range container.NetworkSettings.Networks { + return network.Addr + } + return "" +} + +// Escape beginning slash "/", convert all others to dash "-", and convert underscores "_" to dash "-" +func getSubDomain(name string) string { + return strings.Replace(strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1), "_", "-", -1) +} + +func isBackendLBSwarm(container dockerData) bool { + return label.GetBoolValue(container.Labels, labelBackendLoadBalancerSwarm, false) +} + +func getSegmentBackendName(container dockerData) string { + if value := label.GetStringValue(container.SegmentLabels, label.TraefikFrontendBackend, ""); len(value) > 0 { + return provider.Normalize(container.ServiceName + "-" + value) + } + + return provider.Normalize(container.ServiceName + "-" + getDefaultBackendName(container) + "-" + container.SegmentName) +} + +func getDefaultBackendName(container dockerData) string { + if value := label.GetStringValue(container.SegmentLabels, label.TraefikBackend, ""); len(value) != 0 { + return provider.Normalize(value) + } + + if values, err := label.GetStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil { + return provider.Normalize(values[labelDockerComposeService] + "_" + values[labelDockerComposeProject]) + } + + return provider.Normalize(container.ServiceName) +} + +func getBackendName(container dockerData) string { + if len(container.SegmentName) > 0 { + return getSegmentBackendName(container) + } + + return getDefaultBackendName(container) +} + +func getRedirect(labels map[string]string) *types.Redirect { + permanent := label.GetBoolValue(labels, label.TraefikFrontendRedirectPermanent, false) + + if label.Has(labels, label.TraefikFrontendRedirectEntryPoint) { + return &types.Redirect{ + EntryPoint: label.GetStringValue(labels, label.TraefikFrontendRedirectEntryPoint, ""), + Permanent: permanent, + } + } + + if label.Has(labels, label.TraefikFrontendRedirectRegex) && + label.Has(labels, label.TraefikFrontendRedirectReplacement) { + return &types.Redirect{ + Regex: label.GetStringValue(labels, label.TraefikFrontendRedirectRegex, ""), + Replacement: label.GetStringValue(labels, label.TraefikFrontendRedirectReplacement, ""), + Permanent: permanent, + } + } + + return nil +} + +func getErrorPages(labels map[string]string) map[string]*types.ErrorPage { + prefix := label.Prefix + label.BaseFrontendErrorPage + return label.ParseErrorPages(labels, prefix, label.RegexpFrontendErrorPage) +} + +func getRateLimit(labels map[string]string) *types.RateLimit { + extractorFunc := label.GetStringValue(labels, label.TraefikFrontendRateLimitExtractorFunc, "") + if len(extractorFunc) == 0 { + return nil + } + + prefix := label.Prefix + label.BaseFrontendRateLimit + limits := label.ParseRateSets(labels, prefix, label.RegexpFrontendRateLimit) + + return &types.RateLimit{ + ExtractorFunc: extractorFunc, + RateSet: limits, + } +} + +func getHeaders(labels map[string]string) *types.Headers { + headers := &types.Headers{ + CustomRequestHeaders: label.GetMapValue(labels, label.TraefikFrontendRequestHeaders), + CustomResponseHeaders: label.GetMapValue(labels, label.TraefikFrontendResponseHeaders), + SSLProxyHeaders: label.GetMapValue(labels, label.TraefikFrontendSSLProxyHeaders), + AllowedHosts: label.GetSliceStringValue(labels, label.TraefikFrontendAllowedHosts), + HostsProxyHeaders: label.GetSliceStringValue(labels, label.TraefikFrontendHostsProxyHeaders), + STSSeconds: label.GetInt64Value(labels, label.TraefikFrontendSTSSeconds, 0), + SSLRedirect: label.GetBoolValue(labels, label.TraefikFrontendSSLRedirect, false), + SSLTemporaryRedirect: label.GetBoolValue(labels, label.TraefikFrontendSSLTemporaryRedirect, false), + STSIncludeSubdomains: label.GetBoolValue(labels, label.TraefikFrontendSTSIncludeSubdomains, false), + STSPreload: label.GetBoolValue(labels, label.TraefikFrontendSTSPreload, false), + ForceSTSHeader: label.GetBoolValue(labels, label.TraefikFrontendForceSTSHeader, false), + FrameDeny: label.GetBoolValue(labels, label.TraefikFrontendFrameDeny, false), + ContentTypeNosniff: label.GetBoolValue(labels, label.TraefikFrontendContentTypeNosniff, false), + BrowserXSSFilter: label.GetBoolValue(labels, label.TraefikFrontendBrowserXSSFilter, false), + IsDevelopment: label.GetBoolValue(labels, label.TraefikFrontendIsDevelopment, false), + SSLHost: label.GetStringValue(labels, label.TraefikFrontendSSLHost, ""), + CustomFrameOptionsValue: label.GetStringValue(labels, label.TraefikFrontendCustomFrameOptionsValue, ""), + ContentSecurityPolicy: label.GetStringValue(labels, label.TraefikFrontendContentSecurityPolicy, ""), + PublicKey: label.GetStringValue(labels, label.TraefikFrontendPublicKey, ""), + ReferrerPolicy: label.GetStringValue(labels, label.TraefikFrontendReferrerPolicy, ""), + CustomBrowserXSSValue: label.GetStringValue(labels, label.TraefikFrontendCustomBrowserXSSValue, ""), + } + + if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() { + return nil + } + + return headers +} + +func getPort(container dockerData) string { + if value := label.GetStringValue(container.SegmentLabels, label.TraefikPort, ""); len(value) != 0 { + return value + } + + // See iteration order in https://blog.golang.org/go-maps-in-action + var ports []nat.Port + for port := range container.NetworkSettings.Ports { + ports = append(ports, port) + } + + less := func(i, j nat.Port) bool { + return i.Int() < j.Int() + } + nat.Sort(ports, less) + + if len(ports) > 0 { + min := ports[0] + return min.Port() + } + + return "" +} + +func getMaxConn(labels map[string]string) *types.MaxConn { + amount := label.GetInt64Value(labels, label.TraefikBackendMaxConnAmount, math.MinInt64) + extractorFunc := label.GetStringValue(labels, label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc) + + if amount == math.MinInt64 || len(extractorFunc) == 0 { + return nil + } + + return &types.MaxConn{ + Amount: amount, + ExtractorFunc: extractorFunc, + } +} + +func getHealthCheck(labels map[string]string) *types.HealthCheck { + path := label.GetStringValue(labels, label.TraefikBackendHealthCheckPath, "") + if len(path) == 0 { + return nil + } + + port := label.GetIntValue(labels, label.TraefikBackendHealthCheckPort, label.DefaultBackendHealthCheckPort) + interval := label.GetStringValue(labels, label.TraefikBackendHealthCheckInterval, "") + + return &types.HealthCheck{ + Path: path, + Port: port, + Interval: interval, + } +} + +func getBuffering(labels map[string]string) *types.Buffering { + if !label.HasPrefix(labels, label.TraefikBackendBuffering) { + return nil + } + + return &types.Buffering{ + MaxRequestBodyBytes: label.GetInt64Value(labels, label.TraefikBackendBufferingMaxRequestBodyBytes, 0), + MaxResponseBodyBytes: label.GetInt64Value(labels, label.TraefikBackendBufferingMaxResponseBodyBytes, 0), + MemRequestBodyBytes: label.GetInt64Value(labels, label.TraefikBackendBufferingMemRequestBodyBytes, 0), + MemResponseBodyBytes: label.GetInt64Value(labels, label.TraefikBackendBufferingMemResponseBodyBytes, 0), + RetryExpression: label.GetStringValue(labels, label.TraefikBackendBufferingRetryExpression, ""), + } +} + +func getCircuitBreaker(labels map[string]string) *types.CircuitBreaker { + circuitBreaker := label.GetStringValue(labels, label.TraefikBackendCircuitBreakerExpression, "") + if len(circuitBreaker) == 0 { + return nil + } + return &types.CircuitBreaker{Expression: circuitBreaker} +} + +func getLoadBalancer(labels map[string]string) *types.LoadBalancer { + if !label.HasPrefix(labels, label.TraefikBackendLoadBalancer) { + return nil + } + + method := label.GetStringValue(labels, label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod) + + lb := &types.LoadBalancer{ + Method: method, + Sticky: getSticky(labels), + } + + if label.GetBoolValue(labels, label.TraefikBackendLoadBalancerStickiness, false) { + cookieName := label.GetStringValue(labels, label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName) + lb.Stickiness = &types.Stickiness{CookieName: cookieName} + } + + return lb +} + +// TODO: Deprecated +// replaced by Stickiness +// Deprecated +func getSticky(labels map[string]string) bool { + if label.Has(labels, label.TraefikBackendLoadBalancerSticky) { + log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) + } + + return label.GetBoolValue(labels, label.TraefikBackendLoadBalancerSticky, false) +} + +func (p *Provider) getServers(containers []dockerData) map[string]types.Server { + var servers map[string]types.Server + + for i, container := range containers { + if servers == nil { + servers = make(map[string]types.Server) + } + + protocol := label.GetStringValue(container.SegmentLabels, label.TraefikProtocol, label.DefaultProtocol) + ip := p.getIPAddress(container) + port := getPort(container) + + serverName := "server-" + container.SegmentName + "-" + container.Name + if len(container.SegmentName) > 0 { + serverName += "-" + strconv.Itoa(i) + } + + servers[provider.Normalize(serverName)] = types.Server{ + URL: fmt.Sprintf("%s://%s:%s", protocol, ip, port), + Weight: label.GetIntValue(container.SegmentLabels, label.TraefikWeight, label.DefaultWeightInt), + } + } + + return servers +} + +func getFuncStringLabel(labelName string, defaultValue string) func(map[string]string) string { + return func(labels map[string]string) string { + return label.GetStringValue(labels, labelName, defaultValue) + } +} + +func getFuncIntLabel(labelName string, defaultValue int) func(map[string]string) int { + return func(labels map[string]string) int { + return label.GetIntValue(labels, labelName, defaultValue) + } +} + +func getFuncBoolLabel(labelName string, defaultValue bool) func(map[string]string) bool { + return func(labels map[string]string) bool { + return label.GetBoolValue(labels, labelName, defaultValue) + } +} + +func getFuncSliceStringLabel(labelName string) func(map[string]string) []string { + return func(labels map[string]string) []string { + return label.GetSliceStringValue(labels, labelName) + } +} diff --git a/provider/docker/config_container.go b/provider/docker/config_container.go deleted file mode 100644 index b6fcb28e1..000000000 --- a/provider/docker/config_container.go +++ /dev/null @@ -1,358 +0,0 @@ -package docker - -import ( - "context" - "math" - "strconv" - "strings" - - "github.com/containous/traefik/log" - "github.com/containous/traefik/provider" - "github.com/containous/traefik/provider/label" - "github.com/containous/traefik/types" - "github.com/docker/go-connections/nat" -) - -const ( - labelDockerNetwork = "traefik.docker.network" - labelBackendLoadBalancerSwarm = "traefik.backend.loadbalancer.swarm" - labelDockerComposeProject = "com.docker.compose.project" - labelDockerComposeService = "com.docker.compose.service" -) - -// Specific functions - -func (p Provider) getFrontendName(container dockerData, idx int) string { - return provider.Normalize(p.getFrontendRule(container) + "-" + strconv.Itoa(idx)) -} - -// GetFrontendRule returns the frontend rule for the specified container, using -// it's label. It returns a default one (Host) if the label is not present. -func (p Provider) getFrontendRule(container dockerData) string { - if value := label.GetStringValue(container.Labels, label.TraefikFrontendRule, ""); len(value) != 0 { - return value - } - - if values, err := label.GetStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil { - return "Host:" + getSubDomain(values[labelDockerComposeService]+"."+values[labelDockerComposeProject]) + "." + p.Domain - } - - if len(p.Domain) > 0 { - return "Host:" + getSubDomain(container.ServiceName) + "." + p.Domain - } - - return "" -} - -func (p Provider) getIPAddress(container dockerData) string { - - if value := label.GetStringValue(container.Labels, labelDockerNetwork, ""); value != "" { - networkSettings := container.NetworkSettings - if networkSettings.Networks != nil { - network := networkSettings.Networks[value] - if network != nil { - return network.Addr - } - - log.Warnf("Could not find network named '%s' for container '%s'! Maybe you're missing the project's prefix in the label? Defaulting to first available network.", value, container.Name) - } - } - - if container.NetworkSettings.NetworkMode.IsHost() { - if container.Node != nil { - if container.Node.IPAddress != "" { - return container.Node.IPAddress - } - } - return "127.0.0.1" - } - - if container.NetworkSettings.NetworkMode.IsContainer() { - dockerClient, err := p.createClient() - if err != nil { - log.Warnf("Unable to get IP address for container %s, error: %s", container.Name, err) - return "" - } - - connectedContainer := container.NetworkSettings.NetworkMode.ConnectedContainer() - containerInspected, err := dockerClient.ContainerInspect(context.Background(), connectedContainer) - if err != nil { - log.Warnf("Unable to get IP address for container %s : Failed to inspect container ID %s, error: %s", container.Name, connectedContainer, err) - return "" - } - return p.getIPAddress(parseContainer(containerInspected)) - } - - if p.UseBindPortIP { - port := getPort(container) - for netPort, portBindings := range container.NetworkSettings.Ports { - if string(netPort) == port+"/TCP" || string(netPort) == port+"/UDP" { - for _, p := range portBindings { - return p.HostIP - } - } - } - } - - for _, network := range container.NetworkSettings.Networks { - return network.Addr - } - return "" -} - -func getBackendName(container dockerData) string { - if value := label.GetStringValue(container.Labels, label.TraefikBackend, ""); len(value) != 0 { - return provider.Normalize(value) - } - - if values, err := label.GetStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil { - return provider.Normalize(values[labelDockerComposeService] + "_" + values[labelDockerComposeProject]) - } - - return provider.Normalize(container.ServiceName) -} - -func getPort(container dockerData) string { - if value := label.GetStringValue(container.Labels, label.TraefikPort, ""); len(value) != 0 { - return value - } - - // See iteration order in https://blog.golang.org/go-maps-in-action - var ports []nat.Port - for port := range container.NetworkSettings.Ports { - ports = append(ports, port) - } - - less := func(i, j nat.Port) bool { - return i.Int() < j.Int() - } - nat.Sort(ports, less) - - if len(ports) > 0 { - min := ports[0] - return min.Port() - } - - return "" -} - -// Escape beginning slash "/", convert all others to dash "-", and convert underscores "_" to dash "-" -func getSubDomain(name string) string { - return strings.Replace(strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1), "_", "-", -1) -} - -func isBackendLBSwarm(container dockerData) bool { - return label.GetBoolValue(container.Labels, labelBackendLoadBalancerSwarm, false) -} - -func getMaxConn(container dockerData) *types.MaxConn { - amount := label.GetInt64Value(container.Labels, label.TraefikBackendMaxConnAmount, math.MinInt64) - extractorFunc := label.GetStringValue(container.Labels, label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc) - - if amount == math.MinInt64 || len(extractorFunc) == 0 { - return nil - } - - return &types.MaxConn{ - Amount: amount, - ExtractorFunc: extractorFunc, - } -} - -func getLoadBalancer(container dockerData) *types.LoadBalancer { - if !label.HasPrefix(container.Labels, label.TraefikBackendLoadBalancer) { - return nil - } - - method := label.GetStringValue(container.Labels, label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod) - - lb := &types.LoadBalancer{ - Method: method, - Sticky: getSticky(container), - } - - if label.GetBoolValue(container.Labels, label.TraefikBackendLoadBalancerStickiness, false) { - cookieName := label.GetStringValue(container.Labels, label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName) - lb.Stickiness = &types.Stickiness{CookieName: cookieName} - } - - return lb -} - -// TODO: Deprecated -// replaced by Stickiness -// Deprecated -func getSticky(container dockerData) bool { - if label.Has(container.Labels, label.TraefikBackendLoadBalancerSticky) { - log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) - } - - return label.GetBoolValue(container.Labels, label.TraefikBackendLoadBalancerSticky, false) -} - -func getCircuitBreaker(container dockerData) *types.CircuitBreaker { - circuitBreaker := label.GetStringValue(container.Labels, label.TraefikBackendCircuitBreakerExpression, "") - if len(circuitBreaker) == 0 { - return nil - } - return &types.CircuitBreaker{Expression: circuitBreaker} -} - -func getHealthCheck(container dockerData) *types.HealthCheck { - path := label.GetStringValue(container.Labels, label.TraefikBackendHealthCheckPath, "") - if len(path) == 0 { - return nil - } - - port := label.GetIntValue(container.Labels, label.TraefikBackendHealthCheckPort, label.DefaultBackendHealthCheckPort) - interval := label.GetStringValue(container.Labels, label.TraefikBackendHealthCheckInterval, "") - - return &types.HealthCheck{ - Path: path, - Port: port, - Interval: interval, - } -} - -func getBuffering(container dockerData) *types.Buffering { - if !label.HasPrefix(container.Labels, label.TraefikBackendBuffering) { - return nil - } - - return &types.Buffering{ - MaxRequestBodyBytes: label.GetInt64Value(container.Labels, label.TraefikBackendBufferingMaxRequestBodyBytes, 0), - MaxResponseBodyBytes: label.GetInt64Value(container.Labels, label.TraefikBackendBufferingMaxResponseBodyBytes, 0), - MemRequestBodyBytes: label.GetInt64Value(container.Labels, label.TraefikBackendBufferingMemRequestBodyBytes, 0), - MemResponseBodyBytes: label.GetInt64Value(container.Labels, label.TraefikBackendBufferingMemResponseBodyBytes, 0), - RetryExpression: label.GetStringValue(container.Labels, label.TraefikBackendBufferingRetryExpression, ""), - } -} - -func getRedirect(container dockerData) *types.Redirect { - permanent := label.GetBoolValue(container.Labels, label.TraefikFrontendRedirectPermanent, false) - - if label.Has(container.Labels, label.TraefikFrontendRedirectEntryPoint) { - return &types.Redirect{ - EntryPoint: label.GetStringValue(container.Labels, label.TraefikFrontendRedirectEntryPoint, ""), - Permanent: permanent, - } - } - - if label.Has(container.Labels, label.TraefikFrontendRedirectRegex) && - label.Has(container.Labels, label.TraefikFrontendRedirectReplacement) { - return &types.Redirect{ - Regex: label.GetStringValue(container.Labels, label.TraefikFrontendRedirectRegex, ""), - Replacement: label.GetStringValue(container.Labels, label.TraefikFrontendRedirectReplacement, ""), - Permanent: permanent, - } - } - - return nil -} - -func getErrorPages(container dockerData) map[string]*types.ErrorPage { - prefix := label.Prefix + label.BaseFrontendErrorPage - return label.ParseErrorPages(container.Labels, prefix, label.RegexpFrontendErrorPage) -} - -func getRateLimit(container dockerData) *types.RateLimit { - extractorFunc := label.GetStringValue(container.Labels, label.TraefikFrontendRateLimitExtractorFunc, "") - if len(extractorFunc) == 0 { - return nil - } - - prefix := label.Prefix + label.BaseFrontendRateLimit - limits := label.ParseRateSets(container.Labels, prefix, label.RegexpFrontendRateLimit) - - return &types.RateLimit{ - ExtractorFunc: extractorFunc, - RateSet: limits, - } -} - -func getHeaders(container dockerData) *types.Headers { - headers := &types.Headers{ - CustomRequestHeaders: label.GetMapValue(container.Labels, label.TraefikFrontendRequestHeaders), - CustomResponseHeaders: label.GetMapValue(container.Labels, label.TraefikFrontendResponseHeaders), - SSLProxyHeaders: label.GetMapValue(container.Labels, label.TraefikFrontendSSLProxyHeaders), - AllowedHosts: label.GetSliceStringValue(container.Labels, label.TraefikFrontendAllowedHosts), - HostsProxyHeaders: label.GetSliceStringValue(container.Labels, label.TraefikFrontendHostsProxyHeaders), - STSSeconds: label.GetInt64Value(container.Labels, label.TraefikFrontendSTSSeconds, 0), - SSLRedirect: label.GetBoolValue(container.Labels, label.TraefikFrontendSSLRedirect, false), - SSLTemporaryRedirect: label.GetBoolValue(container.Labels, label.TraefikFrontendSSLTemporaryRedirect, false), - STSIncludeSubdomains: label.GetBoolValue(container.Labels, label.TraefikFrontendSTSIncludeSubdomains, false), - STSPreload: label.GetBoolValue(container.Labels, label.TraefikFrontendSTSPreload, false), - ForceSTSHeader: label.GetBoolValue(container.Labels, label.TraefikFrontendForceSTSHeader, false), - FrameDeny: label.GetBoolValue(container.Labels, label.TraefikFrontendFrameDeny, false), - ContentTypeNosniff: label.GetBoolValue(container.Labels, label.TraefikFrontendContentTypeNosniff, false), - BrowserXSSFilter: label.GetBoolValue(container.Labels, label.TraefikFrontendBrowserXSSFilter, false), - IsDevelopment: label.GetBoolValue(container.Labels, label.TraefikFrontendIsDevelopment, false), - SSLHost: label.GetStringValue(container.Labels, label.TraefikFrontendSSLHost, ""), - CustomFrameOptionsValue: label.GetStringValue(container.Labels, label.TraefikFrontendCustomFrameOptionsValue, ""), - ContentSecurityPolicy: label.GetStringValue(container.Labels, label.TraefikFrontendContentSecurityPolicy, ""), - PublicKey: label.GetStringValue(container.Labels, label.TraefikFrontendPublicKey, ""), - ReferrerPolicy: label.GetStringValue(container.Labels, label.TraefikFrontendReferrerPolicy, ""), - CustomBrowserXSSValue: label.GetStringValue(container.Labels, label.TraefikFrontendCustomBrowserXSSValue, ""), - } - - if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() { - return nil - } - - return headers -} - -// Deprecated -func hasLoadBalancerLabel(container dockerData) bool { - method := label.Has(container.Labels, label.TraefikBackendLoadBalancerMethod) - sticky := label.Has(container.Labels, label.TraefikBackendLoadBalancerSticky) - stickiness := label.Has(container.Labels, label.TraefikBackendLoadBalancerStickiness) - cookieName := label.Has(container.Labels, label.TraefikBackendLoadBalancerStickinessCookieName) - return method || sticky || stickiness || cookieName -} - -// Deprecated -func hasMaxConnLabels(container dockerData) bool { - mca := label.Has(container.Labels, label.TraefikBackendMaxConnAmount) - mcef := label.Has(container.Labels, label.TraefikBackendMaxConnExtractorFunc) - return mca && mcef -} - -// Label functions - -func getFuncStringLabel(labelName string, defaultValue string) func(container dockerData) string { - return func(container dockerData) string { - return label.GetStringValue(container.Labels, labelName, defaultValue) - } -} - -func getFuncBoolLabel(labelName string, defaultValue bool) func(container dockerData) bool { - return func(container dockerData) bool { - return label.GetBoolValue(container.Labels, labelName, defaultValue) - } -} - -func getFuncSliceStringLabel(labelName string) func(container dockerData) []string { - return func(container dockerData) []string { - return label.GetSliceStringValue(container.Labels, labelName) - } -} - -func getFuncIntLabel(labelName string, defaultValue int) func(container dockerData) int { - return func(container dockerData) int { - return label.GetIntValue(container.Labels, labelName, defaultValue) - } -} - -func getFuncInt64Label(labelName string, defaultValue int64) func(container dockerData) int64 { - return func(container dockerData) int64 { - return label.GetInt64Value(container.Labels, labelName, defaultValue) - } -} - -// Deprecated -func hasFunc(labelName string) func(container dockerData) bool { - return func(container dockerData) bool { - return label.Has(container.Labels, labelName) - } -} diff --git a/provider/docker/config_container_docker_test.go b/provider/docker/config_container_docker_test.go index 6feb82da0..ebe35f0b0 100644 --- a/provider/docker/config_container_docker_test.go +++ b/provider/docker/config_container_docker_test.go @@ -1,7 +1,6 @@ package docker import ( - "reflect" "strconv" "testing" "time" @@ -319,7 +318,7 @@ func TestDockerBuildConfiguration(t *testing.T) { Domain: "docker.localhost", ExposedByDefault: true, } - actualConfig := provider.buildConfiguration(dockerDataList) + actualConfig := provider.buildConfigurationV2(dockerDataList) require.NotNil(t, actualConfig, "actualConfig") assert.EqualValues(t, test.expectedBackends, actualConfig.Backends) @@ -631,7 +630,11 @@ func TestDockerTraefikFilter(t *testing.T) { test := test t.Run(strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() + dData := parseContainer(test.container) + segmentProperties := label.ExtractTraefikLabels(dData.Labels) + dData.SegmentLabels = segmentProperties[""] + actual := test.provider.containerFilter(dData) if actual != test.expected { t.Errorf("expected %v for %+v, got %+v", test.expected, test, actual) @@ -642,21 +645,21 @@ func TestDockerTraefikFilter(t *testing.T) { func TestDockerGetFuncStringLabel(t *testing.T) { testCases := []struct { - container docker.ContainerJSON + labels map[string]string labelName string defaultValue string expected string }{ { - container: containerJSON(), + labels: nil, labelName: label.TraefikWeight, defaultValue: label.DefaultWeight, expected: "0", }, { - container: containerJSON(labels(map[string]string{ + labels: map[string]string{ label.TraefikWeight: "10", - })), + }, labelName: label.TraefikWeight, defaultValue: label.DefaultWeight, expected: "10", @@ -668,13 +671,8 @@ func TestDockerGetFuncStringLabel(t *testing.T) { t.Run(test.labelName+strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() - dData := parseContainer(test.container) - - actual := getFuncStringLabel(test.labelName, test.defaultValue)(dData) - - if actual != test.expected { - t.Errorf("got %q, expected %q", actual, test.expected) - } + actual := getFuncStringLabel(test.labelName, test.defaultValue)(test.labels) + assert.Equal(t, test.expected, actual) }) } } @@ -682,28 +680,28 @@ func TestDockerGetFuncStringLabel(t *testing.T) { func TestDockerGetSliceStringLabel(t *testing.T) { testCases := []struct { desc string - container docker.ContainerJSON + labels map[string]string labelName string expected []string }{ { - desc: "no whitelist-label", - container: containerJSON(), - expected: nil, + desc: "no whitelist-label", + labels: nil, + expected: nil, }, { desc: "whitelist-label with empty string", - container: containerJSON(labels(map[string]string{ + labels: map[string]string{ label.TraefikFrontendWhitelistSourceRange: "", - })), + }, labelName: label.TraefikFrontendWhitelistSourceRange, expected: nil, }, { desc: "whitelist-label with IPv4 mask", - container: containerJSON(labels(map[string]string{ + labels: map[string]string{ label.TraefikFrontendWhitelistSourceRange: "1.2.3.4/16", - })), + }, labelName: label.TraefikFrontendWhitelistSourceRange, expected: []string{ "1.2.3.4/16", @@ -711,9 +709,9 @@ func TestDockerGetSliceStringLabel(t *testing.T) { }, { desc: "whitelist-label with IPv6 mask", - container: containerJSON(labels(map[string]string{ + labels: map[string]string{ label.TraefikFrontendWhitelistSourceRange: "fe80::/16", - })), + }, labelName: label.TraefikFrontendWhitelistSourceRange, expected: []string{ "fe80::/16", @@ -721,9 +719,9 @@ func TestDockerGetSliceStringLabel(t *testing.T) { }, { desc: "whitelist-label with multiple masks", - container: containerJSON(labels(map[string]string{ + labels: map[string]string{ label.TraefikFrontendWhitelistSourceRange: "1.1.1.1/24, 1234:abcd::42/32", - })), + }, labelName: label.TraefikFrontendWhitelistSourceRange, expected: []string{ "1.1.1.1/24", @@ -736,13 +734,9 @@ func TestDockerGetSliceStringLabel(t *testing.T) { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() - dData := parseContainer(test.container) - actual := getFuncSliceStringLabel(test.labelName)(dData) - - if !reflect.DeepEqual(actual, test.expected) { - t.Errorf("expected %q, got %q", test.expected, actual) - } + actual := getFuncSliceStringLabel(test.labelName)(test.labels) + assert.EqualValues(t, test.expected, actual) }) } } @@ -793,14 +787,17 @@ func TestDockerGetFrontendName(t *testing.T) { test := test t.Run(strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() + dData := parseContainer(test.container) + segmentProperties := label.ExtractTraefikLabels(dData.Labels) + dData.SegmentLabels = segmentProperties[""] + provider := &Provider{ Domain: "docker.localhost", } + actual := provider.getFrontendName(dData, 0) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } + assert.Equal(t, test.expected, actual) }) } } @@ -842,14 +839,16 @@ func TestDockerGetFrontendRule(t *testing.T) { test := test t.Run(strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() + dData := parseContainer(test.container) + segmentProperties := label.ExtractTraefikLabels(dData.Labels) + dData.SegmentLabels = segmentProperties[""] + provider := &Provider{ Domain: "docker.localhost", } actual := provider.getFrontendRule(dData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } + assert.Equal(t, test.expected, actual) }) } } @@ -886,11 +885,13 @@ func TestDockerGetBackendName(t *testing.T) { test := test t.Run(strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() + dData := parseContainer(test.container) + segmentProperties := label.ExtractTraefikLabels(dData.Labels) + dData.SegmentLabels = segmentProperties[""] + actual := getBackendName(dData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } + assert.Equal(t, test.expected, actual) }) } } @@ -950,12 +951,15 @@ func TestDockerGetIPAddress(t *testing.T) { test := test t.Run(strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() + dData := parseContainer(test.container) + segmentProperties := label.ExtractTraefikLabels(dData.Labels) + dData.SegmentLabels = segmentProperties[""] + provider := &Provider{} + actual := provider.getIPAddress(dData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } + assert.Equal(t, test.expected, actual) }) } } @@ -1007,599 +1011,16 @@ func TestDockerGetPort(t *testing.T) { }, } - for containerID, e := range testCases { - e := e + for containerID, test := range testCases { + test := test t.Run(strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() - dData := parseContainer(e.container) + + dData := parseContainer(test.container) + segmentProperties := label.ExtractTraefikLabels(dData.Labels) + dData.SegmentLabels = segmentProperties[""] + actual := getPort(dData) - if actual != e.expected { - t.Errorf("expected %q, got %q", e.expected, actual) - } - }) - } -} - -func TestDockerGetMaxConn(t *testing.T) { - testCases := []struct { - desc string - container docker.ContainerJSON - expected *types.MaxConn - }{ - { - desc: "should return nil when no max conn labels", - container: containerJSON( - name("test1"), - labels(map[string]string{})), - expected: nil, - }, - { - desc: "should return nil when no amount label", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikBackendMaxConnExtractorFunc: "client.ip", - })), - expected: nil, - }, - { - desc: "should return default when no empty extractorFunc label", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikBackendMaxConnExtractorFunc: "", - label.TraefikBackendMaxConnAmount: "666", - })), - expected: &types.MaxConn{ - ExtractorFunc: "request.host", - Amount: 666, - }, - }, - { - desc: "should return a struct when max conn labels are set", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikBackendMaxConnExtractorFunc: "client.ip", - label.TraefikBackendMaxConnAmount: "666", - })), - expected: &types.MaxConn{ - ExtractorFunc: "client.ip", - Amount: 666, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - dData := parseContainer(test.container) - - actual := getMaxConn(dData) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestDockerGetCircuitBreaker(t *testing.T) { - testCases := []struct { - desc string - container docker.ContainerJSON - expected *types.CircuitBreaker - }{ - { - desc: "should return nil when no CB labels", - container: containerJSON( - name("test1"), - labels(map[string]string{})), - expected: nil, - }, - { - desc: "should return a struct CB when CB labels are set", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikBackendCircuitBreakerExpression: "NetworkErrorRatio() > 0.5", - })), - expected: &types.CircuitBreaker{ - Expression: "NetworkErrorRatio() > 0.5", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - dData := parseContainer(test.container) - - actual := getCircuitBreaker(dData) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestDockerGetLoadBalancer(t *testing.T) { - testCases := []struct { - desc string - container docker.ContainerJSON - expected *types.LoadBalancer - }{ - { - desc: "should return nil when no LB labels", - container: containerJSON( - name("test1"), - labels(map[string]string{})), - expected: nil, - }, - { - desc: "should return a struct when labels are set", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikBackendLoadBalancerMethod: "drr", - label.TraefikBackendLoadBalancerSticky: "true", - label.TraefikBackendLoadBalancerStickiness: "true", - label.TraefikBackendLoadBalancerStickinessCookieName: "foo", - })), - expected: &types.LoadBalancer{ - Method: "drr", - Sticky: true, - Stickiness: &types.Stickiness{ - CookieName: "foo", - }, - }, - }, - { - desc: "should return a nil Stickiness when Stickiness is not set", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikBackendLoadBalancerMethod: "drr", - label.TraefikBackendLoadBalancerSticky: "true", - label.TraefikBackendLoadBalancerStickinessCookieName: "foo", - })), - expected: &types.LoadBalancer{ - Method: "drr", - Sticky: true, - Stickiness: nil, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - dData := parseContainer(test.container) - - actual := getLoadBalancer(dData) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestDockerGetRedirect(t *testing.T) { - testCases := []struct { - desc string - container docker.ContainerJSON - expected *types.Redirect - }{ - { - desc: "should return nil when no redirect labels", - container: containerJSON( - name("test1"), - labels(map[string]string{})), - expected: nil, - }, - { - desc: "should use only entry point tag when mix regex redirect and entry point redirect", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikFrontendRedirectEntryPoint: "https", - label.TraefikFrontendRedirectRegex: "(.*)", - label.TraefikFrontendRedirectReplacement: "$1", - }), - ), - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when entry point redirect label", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikFrontendRedirectEntryPoint: "https", - }), - ), - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when entry point redirect label (permanent)", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikFrontendRedirectEntryPoint: "https", - label.TraefikFrontendRedirectPermanent: "true", - }), - ), - expected: &types.Redirect{ - EntryPoint: "https", - Permanent: true, - }, - }, - { - desc: "should return a struct when regex redirect labels", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikFrontendRedirectRegex: "(.*)", - label.TraefikFrontendRedirectReplacement: "$1", - }), - ), - expected: &types.Redirect{ - Regex: "(.*)", - Replacement: "$1", - }, - }, - { - desc: "should return a struct when regex redirect tags (permanent)", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikFrontendRedirectRegex: "(.*)", - label.TraefikFrontendRedirectReplacement: "$1", - label.TraefikFrontendRedirectPermanent: "true", - }), - ), - expected: &types.Redirect{ - Regex: "(.*)", - Replacement: "$1", - Permanent: true, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - dData := parseContainer(test.container) - - actual := getRedirect(dData) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestDockerGetRateLimit(t *testing.T) { - testCases := []struct { - desc string - container docker.ContainerJSON - expected *types.RateLimit - }{ - { - desc: "should return nil when no rate limit labels", - container: containerJSON( - name("test1"), - labels(map[string]string{})), - expected: nil, - }, - { - desc: "should return a struct when rate limit labels are defined", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikFrontendRateLimitExtractorFunc: "client.ip", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", - })), - expected: &types.RateLimit{ - ExtractorFunc: "client.ip", - RateSet: map[string]*types.Rate{ - "foo": { - Period: flaeg.Duration(6 * time.Second), - Average: 12, - Burst: 18, - }, - "bar": { - Period: flaeg.Duration(3 * time.Second), - Average: 6, - Burst: 9, - }, - }, - }, - }, - { - desc: "should return nil when ExtractorFunc is missing", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", - })), - expected: nil, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - dData := parseContainer(test.container) - - actual := getRateLimit(dData) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetErrorPages(t *testing.T) { - testCases := []struct { - desc string - data dockerData - expected map[string]*types.ErrorPage - }{ - { - desc: "2 errors pages", - data: parseContainer(containerJSON( - labels(map[string]string{ - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: "404", - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: "foo_backend", - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: "foo_query", - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: "500,600", - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: "bar_backend", - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: "bar_query", - }))), - expected: map[string]*types.ErrorPage{ - "foo": { - Status: []string{"404"}, - Query: "foo_query", - Backend: "foo_backend", - }, - "bar": { - Status: []string{"500", "600"}, - Query: "bar_query", - Backend: "bar_backend", - }, - }, - }, - { - desc: "only status field", - data: parseContainer(containerJSON( - labels(map[string]string{ - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: "404", - }))), - expected: map[string]*types.ErrorPage{ - "foo": { - Status: []string{"404"}, - }, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - pages := getErrorPages(test.data) - - assert.EqualValues(t, test.expected, pages) - }) - } -} - -func TestDockerGetHealthCheck(t *testing.T) { - testCases := []struct { - desc string - container docker.ContainerJSON - expected *types.HealthCheck - }{ - { - desc: "should return nil when no health check labels", - container: containerJSON( - name("test1"), - labels(map[string]string{})), - expected: nil, - }, - { - desc: "should return nil when no health check Path label", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikBackendHealthCheckPort: "80", - label.TraefikBackendHealthCheckInterval: "6", - })), - expected: nil, - }, - { - desc: "should return a struct when health check labels are set", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikBackendHealthCheckPath: "/health", - label.TraefikBackendHealthCheckPort: "80", - label.TraefikBackendHealthCheckInterval: "6", - })), - expected: &types.HealthCheck{ - Path: "/health", - Port: 80, - Interval: "6", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - dData := parseContainer(test.container) - - actual := getHealthCheck(dData) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestDockerGetBuffering(t *testing.T) { - testCases := []struct { - desc string - container docker.ContainerJSON - expected *types.Buffering - }{ - { - desc: "should return nil when no health check labels", - container: containerJSON( - name("test1"), - labels(map[string]string{})), - expected: nil, - }, - { - desc: "should return a struct when buffering labels are set", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikBackendBufferingMaxResponseBodyBytes: "10485760", - label.TraefikBackendBufferingMemResponseBodyBytes: "2097152", - label.TraefikBackendBufferingMaxRequestBodyBytes: "10485760", - label.TraefikBackendBufferingMemRequestBodyBytes: "2097152", - label.TraefikBackendBufferingRetryExpression: "IsNetworkError() && Attempts() <= 2", - })), - expected: &types.Buffering{ - MaxResponseBodyBytes: 10485760, - MemResponseBodyBytes: 2097152, - MaxRequestBodyBytes: 10485760, - MemRequestBodyBytes: 2097152, - RetryExpression: "IsNetworkError() && Attempts() <= 2", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - dData := parseContainer(test.container) - - actual := getBuffering(dData) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestDockerGetHeaders(t *testing.T) { - testCases := []struct { - desc string - container docker.ContainerJSON - expected *types.Headers - }{ - { - desc: "should return nil when no custom headers options are set", - container: containerJSON( - name("test1"), - labels(map[string]string{})), - expected: nil, - }, - { - desc: "should return a struct when all custom headers options are set", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.TraefikFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.TraefikFrontendSSLProxyHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.TraefikFrontendAllowedHosts: "foo,bar,bor", - label.TraefikFrontendHostsProxyHeaders: "foo,bar,bor", - label.TraefikFrontendSSLHost: "foo", - label.TraefikFrontendCustomFrameOptionsValue: "foo", - label.TraefikFrontendContentSecurityPolicy: "foo", - label.TraefikFrontendPublicKey: "foo", - label.TraefikFrontendReferrerPolicy: "foo", - label.TraefikFrontendCustomBrowserXSSValue: "foo", - label.TraefikFrontendSTSSeconds: "666", - label.TraefikFrontendSSLRedirect: "true", - label.TraefikFrontendSSLTemporaryRedirect: "true", - label.TraefikFrontendSTSIncludeSubdomains: "true", - label.TraefikFrontendSTSPreload: "true", - label.TraefikFrontendForceSTSHeader: "true", - label.TraefikFrontendFrameDeny: "true", - label.TraefikFrontendContentTypeNosniff: "true", - label.TraefikFrontendBrowserXSSFilter: "true", - label.TraefikFrontendIsDevelopment: "true", - }), - ), - expected: &types.Headers{ - CustomRequestHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - CustomResponseHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - SSLProxyHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - AllowedHosts: []string{"foo", "bar", "bor"}, - HostsProxyHeaders: []string{"foo", "bar", "bor"}, - SSLHost: "foo", - CustomFrameOptionsValue: "foo", - ContentSecurityPolicy: "foo", - PublicKey: "foo", - ReferrerPolicy: "foo", - CustomBrowserXSSValue: "foo", - STSSeconds: 666, - SSLRedirect: true, - SSLTemporaryRedirect: true, - STSIncludeSubdomains: true, - STSPreload: true, - ForceSTSHeader: true, - FrameDeny: true, - ContentTypeNosniff: true, - BrowserXSSFilter: true, - IsDevelopment: true, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - dData := parseContainer(test.container) - - actual := getHeaders(dData) - assert.Equal(t, test.expected, actual) }) } diff --git a/provider/docker/config_container_swarm_test.go b/provider/docker/config_container_swarm_test.go index 08bbfb5fe..cae3468a4 100644 --- a/provider/docker/config_container_swarm_test.go +++ b/provider/docker/config_container_swarm_test.go @@ -331,7 +331,7 @@ func TestSwarmBuildConfiguration(t *testing.T) { SwarmMode: true, } - actualConfig := provider.buildConfiguration(dockerDataList) + actualConfig := provider.buildConfigurationV2(dockerDataList) require.NotNil(t, actualConfig, "actualConfig") assert.EqualValues(t, test.expectedBackends, actualConfig.Backends) @@ -465,7 +465,11 @@ func TestSwarmTraefikFilter(t *testing.T) { test := test t.Run(strconv.Itoa(serviceID), func(t *testing.T) { t.Parallel() + dData := parseService(test.service, test.networks) + segmentProperties := label.ExtractTraefikLabels(dData.Labels) + dData.SegmentLabels = segmentProperties[""] + actual := test.provider.containerFilter(dData) if actual != test.expected { t.Errorf("expected %v for %+v, got %+v", test.expected, test, actual) @@ -474,47 +478,6 @@ func TestSwarmTraefikFilter(t *testing.T) { } } -func TestSwarmGetFuncStringLabel(t *testing.T) { - testCases := []struct { - service swarm.Service - labelName string - defaultValue string - networks map[string]*docker.NetworkResource - expected string - }{ - { - service: swarmService(), - labelName: label.TraefikWeight, - defaultValue: label.DefaultWeight, - networks: map[string]*docker.NetworkResource{}, - expected: "0", - }, - { - service: swarmService(serviceLabels(map[string]string{ - label.TraefikWeight: "10", - })), - labelName: label.TraefikWeight, - defaultValue: label.DefaultWeight, - networks: map[string]*docker.NetworkResource{}, - expected: "10", - }, - } - - for serviceID, test := range testCases { - test := test - t.Run(test.labelName+strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - - dData := parseService(test.service, test.networks) - - actual := getFuncStringLabel(test.labelName, test.defaultValue)(dData) - if actual != test.expected { - t.Errorf("got %q, expected %q", actual, test.expected) - } - }) - } -} - func TestSwarmGetFrontendName(t *testing.T) { testCases := []struct { service swarm.Service @@ -563,15 +526,18 @@ func TestSwarmGetFrontendName(t *testing.T) { test := test t.Run(strconv.Itoa(serviceID), func(t *testing.T) { t.Parallel() + dData := parseService(test.service, test.networks) + segmentProperties := label.ExtractTraefikLabels(dData.Labels) + dData.SegmentLabels = segmentProperties[""] + provider := &Provider{ Domain: "docker.localhost", SwarmMode: true, } + actual := provider.getFrontendName(dData, 0) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } + assert.Equal(t, test.expected, actual) }) } } @@ -612,15 +578,18 @@ func TestSwarmGetFrontendRule(t *testing.T) { test := test t.Run(strconv.Itoa(serviceID), func(t *testing.T) { t.Parallel() + dData := parseService(test.service, test.networks) + segmentProperties := label.ExtractTraefikLabels(dData.Labels) + dData.SegmentLabels = segmentProperties[""] + provider := &Provider{ Domain: "docker.localhost", SwarmMode: true, } + actual := provider.getFrontendRule(dData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } + assert.Equal(t, test.expected, actual) }) } } @@ -654,11 +623,13 @@ func TestSwarmGetBackendName(t *testing.T) { test := test t.Run(strconv.Itoa(serviceID), func(t *testing.T) { t.Parallel() + dData := parseService(test.service, test.networks) + segmentProperties := label.ExtractTraefikLabels(dData.Labels) + dData.SegmentLabels = segmentProperties[""] + actual := getBackendName(dData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } + assert.Equal(t, test.expected, actual) }) } } @@ -713,14 +684,17 @@ func TestSwarmGetIPAddress(t *testing.T) { test := test t.Run(strconv.Itoa(serviceID), func(t *testing.T) { t.Parallel() - dData := parseService(test.service, test.networks) + provider := &Provider{ SwarmMode: true, } + + dData := parseService(test.service, test.networks) + segmentProperties := label.ExtractTraefikLabels(dData.Labels) + dData.SegmentLabels = segmentProperties[""] + actual := provider.getIPAddress(dData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } + assert.Equal(t, test.expected, actual) }) } } @@ -747,11 +721,13 @@ func TestSwarmGetPort(t *testing.T) { test := test t.Run(strconv.Itoa(serviceID), func(t *testing.T) { t.Parallel() + dData := parseService(test.service, test.networks) + segmentProperties := label.ExtractTraefikLabels(dData.Labels) + dData.SegmentLabels = segmentProperties[""] + actual := getPort(dData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } + assert.Equal(t, test.expected, actual) }) } } diff --git a/provider/docker/config_root.go b/provider/docker/config_root.go new file mode 100644 index 000000000..5819e49a4 --- /dev/null +++ b/provider/docker/config_root.go @@ -0,0 +1,12 @@ +package docker + +import ( + "github.com/containous/traefik/types" +) + +func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.Configuration { + if p.TemplateVersion == 1 { + return p.buildConfigurationV1(containersInspected) + } + return p.buildConfigurationV2(containersInspected) +} diff --git a/provider/docker/config_segment_test.go b/provider/docker/config_segment_test.go new file mode 100644 index 000000000..e1c10fe7b --- /dev/null +++ b/provider/docker/config_segment_test.go @@ -0,0 +1,351 @@ +package docker + +import ( + "testing" + "time" + + "github.com/containous/flaeg" + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + docker "github.com/docker/docker/api/types" + "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSegmentBuildConfiguration(t *testing.T) { + testCases := []struct { + desc string + containers []docker.ContainerJSON + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{ + { + desc: "when no container", + containers: []docker.ContainerJSON{}, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + desc: "simple configuration", + containers: []docker.ContainerJSON{ + containerJSON( + name("foo"), + labels(map[string]string{ + "traefik.sauternes.port": "2503", + "traefik.sauternes.frontend.entryPoints": "http,https", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-foo-foo-sauternes": { + Backend: "backend-foo-foo-sauternes", + PassHostHeader: true, + EntryPoints: []string{"http", "https"}, + BasicAuth: []string{}, + Routes: map[string]types.Route{ + "route-frontend-foo-foo-sauternes": { + Rule: "Host:foo.docker.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foo-foo-sauternes": { + Servers: map[string]types.Server{ + "server-sauternes-foo-0": { + URL: "http://127.0.0.1:2503", + Weight: 0, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "when all labels are set", + containers: []docker.ContainerJSON{ + containerJSON( + name("foo"), + labels(map[string]string{ + label.Prefix + "sauternes." + label.SuffixPort: "666", + label.Prefix + "sauternes." + label.SuffixProtocol: "https", + label.Prefix + "sauternes." + label.SuffixWeight: "12", + + label.Prefix + "sauternes." + label.SuffixFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.Prefix + "sauternes." + label.SuffixFrontendEntryPoints: "http,https", + label.Prefix + "sauternes." + label.SuffixFrontendPassHostHeader: "true", + label.Prefix + "sauternes." + label.SuffixFrontendPassTLSCert: "true", + label.Prefix + "sauternes." + label.SuffixFrontendPriority: "666", + label.Prefix + "sauternes." + label.SuffixFrontendRedirectEntryPoint: "https", + label.Prefix + "sauternes." + label.SuffixFrontendRedirectRegex: "nope", + label.Prefix + "sauternes." + label.SuffixFrontendRedirectReplacement: "nope", + label.Prefix + "sauternes." + label.SuffixFrontendRedirectPermanent: "true", + label.Prefix + "sauternes." + label.SuffixFrontendWhitelistSourceRange: "10.10.10.10", + + label.Prefix + "sauternes." + label.SuffixFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.Prefix + "sauternes." + label.SuffixFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLProxyHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersAllowedHosts: "foo,bar,bor", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersHostsProxyHeaders: "foo,bar,bor", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLHost: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersCustomFrameOptionsValue: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersContentSecurityPolicy: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersPublicKey: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersReferrerPolicy: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersCustomBrowserXSSValue: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSSeconds: "666", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLRedirect: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLTemporaryRedirect: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSIncludeSubdomains: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSPreload: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersForceSTSHeader: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersFrameDeny: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersContentTypeNosniff: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersBrowserXSSFilter: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersIsDevelopment: "true", + + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: "404", + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: "foobar", + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: "foo_query", + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: "500,600", + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: "foobar", + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: "bar_query", + + label.Prefix + "sauternes." + label.SuffixFrontendRateLimitExtractorFunc: "client.ip", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-foo-foo-sauternes": { + Backend: "backend-foo-foo-sauternes", + EntryPoints: []string{ + "http", + "https", + }, + PassHostHeader: true, + PassTLSCert: true, + Priority: 666, + BasicAuth: []string{ + "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }, + WhitelistSourceRange: []string{ + "10.10.10.10", + }, + Headers: &types.Headers{ + CustomRequestHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + CustomResponseHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + AllowedHosts: []string{ + "foo", + "bar", + "bor", + }, + HostsProxyHeaders: []string{ + "foo", + "bar", + "bor", + }, + SSLRedirect: true, + SSLTemporaryRedirect: true, + SSLHost: "foo", + SSLProxyHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + STSSeconds: 666, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + CustomFrameOptionsValue: "foo", + ContentTypeNosniff: true, + BrowserXSSFilter: true, + CustomBrowserXSSValue: "foo", + ContentSecurityPolicy: "foo", + PublicKey: "foo", + ReferrerPolicy: "foo", + IsDevelopment: true, + }, + Errors: map[string]*types.ErrorPage{ + "foo": { + Status: []string{"404"}, + Query: "foo_query", + Backend: "foobar", + }, + "bar": { + Status: []string{"500", "600"}, + Query: "bar_query", + Backend: "foobar", + }, + }, + RateLimit: &types.RateLimit{ + ExtractorFunc: "client.ip", + RateSet: map[string]*types.Rate{ + "foo": { + Period: flaeg.Duration(6 * time.Second), + Average: 12, + Burst: 18, + }, + "bar": { + Period: flaeg.Duration(3 * time.Second), + Average: 6, + Burst: 9, + }, + }, + }, + Redirect: &types.Redirect{ + EntryPoint: "https", + Regex: "", + Replacement: "", + Permanent: true, + }, + + Routes: map[string]types.Route{ + "route-frontend-foo-foo-sauternes": { + Rule: "Host:foo.docker.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foo-foo-sauternes": { + Servers: map[string]types.Server{ + "server-sauternes-foo-0": { + URL: "https://127.0.0.1:666", + Weight: 12, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "several containers", + containers: []docker.ContainerJSON{ + containerJSON( + name("test1"), + labels(map[string]string{ + "traefik.sauternes.port": "2503", + "traefik.sauternes.protocol": "https", + "traefik.sauternes.weight": "80", + "traefik.sauternes.frontend.backend": "foobar", + "traefik.sauternes.frontend.passHostHeader": "false", + "traefik.sauternes.frontend.rule": "Path:/mypath", + "traefik.sauternes.frontend.priority": "5000", + "traefik.sauternes.frontend.entryPoints": "http,https,ws", + "traefik.sauternes.frontend.auth.basic": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + "traefik.sauternes.frontend.redirect.entryPoint": "https", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + containerJSON( + name("test2"), + labels(map[string]string{ + "traefik.anothersauternes.port": "8079", + "traefik.anothersauternes.weight": "33", + "traefik.anothersauternes.frontend.rule": "Path:/anotherpath", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-test1-foobar": { + Backend: "backend-test1-foobar", + PassHostHeader: false, + Priority: 5000, + EntryPoints: []string{"http", "https", "ws"}, + BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + Routes: map[string]types.Route{ + "route-frontend-test1-foobar": { + Rule: "Path:/mypath", + }, + }, + }, + "frontend-test2-test2-anothersauternes": { + Backend: "backend-test2-test2-anothersauternes", + PassHostHeader: true, + EntryPoints: []string{}, + BasicAuth: []string{}, + Routes: map[string]types.Route{ + "route-frontend-test2-test2-anothersauternes": { + Rule: "Path:/anotherpath", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test1-foobar": { + Servers: map[string]types.Server{ + "server-sauternes-test1-0": { + URL: "https://127.0.0.1:2503", + Weight: 80, + }, + }, + CircuitBreaker: nil, + }, + "backend-test2-test2-anothersauternes": { + Servers: map[string]types.Server{ + "server-anothersauternes-test2-0": { + URL: "http://127.0.0.1:8079", + Weight: 33, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + } + + provider := &Provider{ + Domain: "docker.localhost", + ExposedByDefault: true, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + var dockerDataList []dockerData + for _, container := range test.containers { + dData := parseContainer(container) + dockerDataList = append(dockerDataList, dData) + } + + actualConfig := provider.buildConfigurationV2(dockerDataList) + require.NotNil(t, actualConfig, "actualConfig") + + assert.EqualValues(t, test.expectedBackends, actualConfig.Backends) + assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends) + }) + } +} diff --git a/provider/docker/config_service.go b/provider/docker/config_service.go deleted file mode 100644 index 439516adb..000000000 --- a/provider/docker/config_service.go +++ /dev/null @@ -1,277 +0,0 @@ -package docker - -import ( - "errors" - "strconv" - "strings" - - "github.com/containous/traefik/provider" - "github.com/containous/traefik/provider/label" - "github.com/containous/traefik/types" -) - -// Specific functions - -// Extract rule from labels for a given service and a given docker container -func (p Provider) getServiceFrontendRule(container dockerData, serviceName string) string { - if value, ok := getServiceLabels(container, serviceName)[label.SuffixFrontendRule]; ok { - return value - } - return p.getFrontendRule(container) -} - -// Check if for the given container, we find labels that are defining services -func hasServices(container dockerData) bool { - return len(label.ExtractServiceProperties(container.Labels)) > 0 -} - -// Gets array of service names for a given container -func getServiceNames(container dockerData) []string { - labelServiceProperties := label.ExtractServiceProperties(container.Labels) - keys := make([]string, 0, len(labelServiceProperties)) - for k := range labelServiceProperties { - keys = append(keys, k) - } - return keys -} - -// checkServiceLabelPort checks if all service names have a port service label -// or if port container label exists for default value -func checkServiceLabelPort(container dockerData) error { - // If port container label is present, there is a default values for all ports, use it for the check - _, err := strconv.Atoi(container.Labels[label.TraefikPort]) - if err != nil { - serviceLabelPorts := make(map[string]struct{}) - serviceLabels := make(map[string]struct{}) - for lbl := range container.Labels { - // Get all port service labels - portLabel := extractServicePort(lbl) - if len(portLabel) > 0 { - serviceLabelPorts[portLabel[0]] = struct{}{} - } - // Get only one instance of all service names from service labels - servicesLabelNames := label.FindServiceSubmatch(lbl) - - if len(servicesLabelNames) > 0 { - serviceLabels[strings.Split(servicesLabelNames[0], ".")[1]] = struct{}{} - } - } - // If the number of service labels is different than the number of port services label - // there is an error - if len(serviceLabels) == len(serviceLabelPorts) { - for labelPort := range serviceLabelPorts { - _, err = strconv.Atoi(container.Labels[labelPort]) - if err != nil { - break - } - } - } else { - err = errors.New("port service labels missing, please use traefik.port as default value or define all port service labels") - } - } - return err -} - -func extractServicePort(labelName string) []string { - if strings.HasPrefix(labelName, label.TraefikFrontend+".") || - strings.HasPrefix(labelName, label.TraefikBackend+".") { - return nil - } - - return label.PortRegexp.FindStringSubmatch(labelName) -} - -// Extract backend from labels for a given service and a given docker container -func getServiceBackendName(container dockerData, serviceName string) string { - if value, ok := getServiceLabels(container, serviceName)[label.SuffixFrontendBackend]; ok { - return provider.Normalize(container.ServiceName + "-" + value) - } - return provider.Normalize(container.ServiceName + "-" + getBackendName(container) + "-" + serviceName) -} - -// Extract port from labels for a given service and a given docker container -func getServicePort(container dockerData, serviceName string) string { - if value, ok := getServiceLabels(container, serviceName)[label.SuffixPort]; ok { - return value - } - return getPort(container) -} - -func getServiceRedirect(container dockerData, serviceName string) *types.Redirect { - serviceLabels := getServiceLabels(container, serviceName) - - permanent := getServiceBoolValue(container, serviceLabels, label.SuffixFrontendRedirectPermanent, false) - - if hasStrictServiceLabel(serviceLabels, label.SuffixFrontendRedirectEntryPoint) { - return &types.Redirect{ - EntryPoint: getStrictServiceStringValue(serviceLabels, label.SuffixFrontendRedirectEntryPoint, label.DefaultFrontendRedirectEntryPoint), - Permanent: permanent, - } - } - - if hasStrictServiceLabel(serviceLabels, label.SuffixFrontendRedirectRegex) && - hasStrictServiceLabel(serviceLabels, label.SuffixFrontendRedirectReplacement) { - return &types.Redirect{ - Regex: getStrictServiceStringValue(serviceLabels, label.SuffixFrontendRedirectRegex, ""), - Replacement: getStrictServiceStringValue(serviceLabels, label.SuffixFrontendRedirectReplacement, ""), - Permanent: permanent, - } - } - - return getRedirect(container) -} - -func getServiceErrorPages(container dockerData, serviceName string) map[string]*types.ErrorPage { - serviceLabels := getServiceLabels(container, serviceName) - - if label.HasPrefix(serviceLabels, label.BaseFrontendErrorPage) { - return label.ParseErrorPages(serviceLabels, label.BaseFrontendErrorPage, label.RegexpBaseFrontendErrorPage) - } - - return getErrorPages(container) -} - -func getServiceRateLimit(container dockerData, serviceName string) *types.RateLimit { - serviceLabels := getServiceLabels(container, serviceName) - - if hasStrictServiceLabel(serviceLabels, label.SuffixFrontendRateLimitExtractorFunc) { - extractorFunc := getStrictServiceStringValue(serviceLabels, label.SuffixFrontendRateLimitExtractorFunc, label.DefaultBackendMaxconnExtractorFunc) - return &types.RateLimit{ - ExtractorFunc: extractorFunc, - RateSet: label.ParseRateSets(serviceLabels, label.BaseFrontendRateLimit, label.RegexpBaseFrontendRateLimit), - } - } - - return getRateLimit(container) -} - -func getServiceHeaders(container dockerData, serviceName string) *types.Headers { - serviceLabels := getServiceLabels(container, serviceName) - - headers := &types.Headers{ - CustomRequestHeaders: getServiceMapValue(container, serviceLabels, serviceName, label.SuffixFrontendRequestHeaders), - CustomResponseHeaders: getServiceMapValue(container, serviceLabels, serviceName, label.SuffixFrontendResponseHeaders), - SSLProxyHeaders: getServiceMapValue(container, serviceLabels, serviceName, label.SuffixFrontendHeadersSSLProxyHeaders), - AllowedHosts: getServiceSliceValue(container, serviceLabels, label.SuffixFrontendHeadersAllowedHosts), - HostsProxyHeaders: getServiceSliceValue(container, serviceLabels, label.SuffixFrontendHeadersHostsProxyHeaders), - STSSeconds: getServiceInt64Value(container, serviceLabels, label.SuffixFrontendHeadersSTSSeconds, 0), - SSLRedirect: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersSSLRedirect, false), - SSLTemporaryRedirect: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersSSLTemporaryRedirect, false), - STSIncludeSubdomains: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersSTSIncludeSubdomains, false), - STSPreload: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersSTSPreload, false), - ForceSTSHeader: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersForceSTSHeader, false), - FrameDeny: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersFrameDeny, false), - ContentTypeNosniff: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersContentTypeNosniff, false), - BrowserXSSFilter: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersBrowserXSSFilter, false), - IsDevelopment: getServiceBoolValue(container, serviceLabels, label.SuffixFrontendHeadersIsDevelopment, false), - SSLHost: getServiceStringValue(container, serviceLabels, label.SuffixFrontendHeadersSSLHost, ""), - CustomFrameOptionsValue: getServiceStringValue(container, serviceLabels, label.SuffixFrontendHeadersCustomFrameOptionsValue, ""), - ContentSecurityPolicy: getServiceStringValue(container, serviceLabels, label.SuffixFrontendHeadersContentSecurityPolicy, ""), - PublicKey: getServiceStringValue(container, serviceLabels, label.SuffixFrontendHeadersPublicKey, ""), - ReferrerPolicy: getServiceStringValue(container, serviceLabels, label.SuffixFrontendHeadersReferrerPolicy, ""), - CustomBrowserXSSValue: getServiceStringValue(container, serviceLabels, label.SuffixFrontendHeadersCustomBrowserXSSValue, ""), - } - - if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() { - return nil - } - - return headers -} - -// Service label functions - -func getFuncServiceSliceStringLabel(labelSuffix string) func(container dockerData, serviceName string) []string { - return func(container dockerData, serviceName string) []string { - serviceLabels := getServiceLabels(container, serviceName) - return getServiceSliceValue(container, serviceLabels, labelSuffix) - } -} - -func getFuncServiceStringLabel(labelSuffix string, defaultValue string) func(container dockerData, serviceName string) string { - return func(container dockerData, serviceName string) string { - serviceLabels := getServiceLabels(container, serviceName) - return getServiceStringValue(container, serviceLabels, labelSuffix, defaultValue) - } -} - -func getFuncServiceBoolLabel(labelSuffix string, defaultValue bool) func(container dockerData, serviceName string) bool { - return func(container dockerData, serviceName string) bool { - serviceLabels := getServiceLabels(container, serviceName) - return getServiceBoolValue(container, serviceLabels, labelSuffix, defaultValue) - } -} - -func getFuncServiceIntLabel(labelSuffix string, defaultValue int) func(container dockerData, serviceName string) int { - return func(container dockerData, serviceName string) int { - return getServiceIntLabel(container, serviceName, labelSuffix, defaultValue) - } -} - -func hasStrictServiceLabel(serviceLabels map[string]string, labelSuffix string) bool { - value, ok := serviceLabels[labelSuffix] - return ok && len(value) > 0 -} - -func getServiceStringValue(container dockerData, serviceLabels map[string]string, labelSuffix string, defaultValue string) string { - if value, ok := serviceLabels[labelSuffix]; ok { - return value - } - return label.GetStringValue(container.Labels, label.Prefix+labelSuffix, defaultValue) -} - -func getStrictServiceStringValue(serviceLabels map[string]string, labelSuffix string, defaultValue string) string { - if value, ok := serviceLabels[labelSuffix]; ok { - return value - } - return defaultValue -} - -func getServiceMapValue(container dockerData, serviceLabels map[string]string, serviceName string, labelSuffix string) map[string]string { - if value, ok := serviceLabels[labelSuffix]; ok { - lblName := label.GetServiceLabel(labelSuffix, serviceName) - return label.ParseMapValue(lblName, value) - } - return label.GetMapValue(container.Labels, label.Prefix+labelSuffix) -} - -func getServiceSliceValue(container dockerData, serviceLabels map[string]string, labelSuffix string) []string { - if value, ok := serviceLabels[labelSuffix]; ok { - return label.SplitAndTrimString(value, ",") - } - return label.GetSliceStringValue(container.Labels, label.Prefix+labelSuffix) -} - -func getServiceBoolValue(container dockerData, serviceLabels map[string]string, labelSuffix string, defaultValue bool) bool { - if rawValue, ok := serviceLabels[labelSuffix]; ok { - value, err := strconv.ParseBool(rawValue) - if err == nil { - return value - } - } - return label.GetBoolValue(container.Labels, label.Prefix+labelSuffix, defaultValue) -} - -func getServiceIntLabel(container dockerData, serviceName string, labelSuffix string, defaultValue int) int { - if rawValue, ok := getServiceLabels(container, serviceName)[labelSuffix]; ok { - value, err := strconv.Atoi(rawValue) - if err == nil { - return value - } - } - return label.GetIntValue(container.Labels, label.Prefix+labelSuffix, defaultValue) -} - -func getServiceInt64Value(container dockerData, serviceLabels map[string]string, labelSuffix string, defaultValue int64) int64 { - if rawValue, ok := serviceLabels[labelSuffix]; ok { - value, err := strconv.ParseInt(rawValue, 10, 64) - if err == nil { - return value - } - } - return label.GetInt64Value(container.Labels, label.Prefix+labelSuffix, defaultValue) -} - -func getServiceLabels(container dockerData, serviceName string) label.ServicePropertyValues { - return label.ExtractServiceProperties(container.Labels)[serviceName] -} diff --git a/provider/docker/deprecated_config.go b/provider/docker/deprecated_config.go new file mode 100644 index 000000000..0b0ca3890 --- /dev/null +++ b/provider/docker/deprecated_config.go @@ -0,0 +1,201 @@ +package docker + +import ( + "math" + "strconv" + "text/template" + + "github.com/BurntSushi/ty/fun" + "github.com/containous/traefik/log" + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" +) + +// Deprecated +func (p *Provider) buildConfigurationV1(containersInspected []dockerData) *types.Configuration { + var DockerFuncMap = template.FuncMap{ + "getDomain": getFuncStringLabelV1(label.TraefikDomain, p.Domain), + "getSubDomain": getSubDomain, + "isBackendLBSwarm": isBackendLBSwarm, + + // Backend functions + "getIPAddress": p.getIPAddress, + "getPort": getPortV1, + "getWeight": getFuncIntLabelV1(label.TraefikWeight, label.DefaultWeightInt), + "getProtocol": getFuncStringLabelV1(label.TraefikProtocol, label.DefaultProtocol), + + "hasCircuitBreakerLabel": hasFuncV1(label.TraefikBackendCircuitBreakerExpression), + "getCircuitBreakerExpression": getFuncStringLabelV1(label.TraefikBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression), + "hasLoadBalancerLabel": hasLoadBalancerLabelV1, + "getLoadBalancerMethod": getFuncStringLabelV1(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), + "hasMaxConnLabels": hasMaxConnLabelsV1, + "getMaxConnAmount": getFuncInt64LabelV1(label.TraefikBackendMaxConnAmount, math.MaxInt64), + "getMaxConnExtractorFunc": getFuncStringLabelV1(label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc), + "getSticky": getStickyV1, + "hasStickinessLabel": hasFuncV1(label.TraefikBackendLoadBalancerStickiness), + "getStickinessCookieName": getFuncStringLabelV1(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName), + + // Frontend functions + "getBackend": getBackendNameV1, + "getBackendName": getBackendNameV1, + "getPriority": getFuncIntLabelV1(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), + "getPassHostHeader": getFuncBoolLabelV1(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getPassTLSCert": getFuncBoolLabelV1(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), + "getEntryPoints": getFuncSliceStringLabelV1(label.TraefikFrontendEntryPoints), + "getBasicAuth": getFuncSliceStringLabelV1(label.TraefikFrontendAuthBasic), + "getWhitelistSourceRange": getFuncSliceStringLabelV1(label.TraefikFrontendWhitelistSourceRange), + "getFrontendRule": p.getFrontendRuleV1, + "hasRedirect": hasRedirectV1, + "getRedirectEntryPoint": getFuncStringLabelV1(label.TraefikFrontendRedirectEntryPoint, ""), + "getRedirectRegex": getFuncStringLabelV1(label.TraefikFrontendRedirectRegex, ""), + "getRedirectReplacement": getFuncStringLabelV1(label.TraefikFrontendRedirectReplacement, ""), + + "hasHeaders": hasHeadersV1, + "hasRequestHeaders": hasLabelV1(label.TraefikFrontendRequestHeaders), + "getRequestHeaders": getFuncMapLabelV1(label.TraefikFrontendRequestHeaders), + "hasResponseHeaders": hasLabelV1(label.TraefikFrontendResponseHeaders), + "getResponseHeaders": getFuncMapLabelV1(label.TraefikFrontendResponseHeaders), + "hasAllowedHostsHeaders": hasLabelV1(label.TraefikFrontendAllowedHosts), + "getAllowedHostsHeaders": getFuncSliceStringLabelV1(label.TraefikFrontendAllowedHosts), + "hasHostsProxyHeaders": hasLabelV1(label.TraefikFrontendHostsProxyHeaders), + "getHostsProxyHeaders": getFuncSliceStringLabelV1(label.TraefikFrontendHostsProxyHeaders), + "hasSSLRedirectHeaders": hasLabelV1(label.TraefikFrontendSSLRedirect), + "getSSLRedirectHeaders": getFuncBoolLabelV1(label.TraefikFrontendSSLRedirect, false), + "hasSSLTemporaryRedirectHeaders": hasLabelV1(label.TraefikFrontendSSLTemporaryRedirect), + "getSSLTemporaryRedirectHeaders": getFuncBoolLabelV1(label.TraefikFrontendSSLTemporaryRedirect, false), + "hasSSLHostHeaders": hasLabelV1(label.TraefikFrontendSSLHost), + "getSSLHostHeaders": getFuncStringLabelV1(label.TraefikFrontendSSLHost, ""), + "hasSSLProxyHeaders": hasLabelV1(label.TraefikFrontendSSLProxyHeaders), + "getSSLProxyHeaders": getFuncMapLabelV1(label.TraefikFrontendSSLProxyHeaders), + "hasSTSSecondsHeaders": hasLabelV1(label.TraefikFrontendSTSSeconds), + "getSTSSecondsHeaders": getFuncInt64LabelV1(label.TraefikFrontendSTSSeconds, 0), + "hasSTSIncludeSubdomainsHeaders": hasLabelV1(label.TraefikFrontendSTSIncludeSubdomains), + "getSTSIncludeSubdomainsHeaders": getFuncBoolLabelV1(label.TraefikFrontendSTSIncludeSubdomains, false), + "hasSTSPreloadHeaders": hasLabelV1(label.TraefikFrontendSTSPreload), + "getSTSPreloadHeaders": getFuncBoolLabelV1(label.TraefikFrontendSTSPreload, false), + "hasForceSTSHeaderHeaders": hasLabelV1(label.TraefikFrontendForceSTSHeader), + "getForceSTSHeaderHeaders": getFuncBoolLabelV1(label.TraefikFrontendForceSTSHeader, false), + "hasFrameDenyHeaders": hasLabelV1(label.TraefikFrontendFrameDeny), + "getFrameDenyHeaders": getFuncBoolLabelV1(label.TraefikFrontendFrameDeny, false), + "hasCustomFrameOptionsValueHeaders": hasLabelV1(label.TraefikFrontendCustomFrameOptionsValue), + "getCustomFrameOptionsValueHeaders": getFuncStringLabelV1(label.TraefikFrontendCustomFrameOptionsValue, ""), + "hasContentTypeNosniffHeaders": hasLabelV1(label.TraefikFrontendContentTypeNosniff), + "getContentTypeNosniffHeaders": getFuncBoolLabelV1(label.TraefikFrontendContentTypeNosniff, false), + "hasBrowserXSSFilterHeaders": hasLabelV1(label.TraefikFrontendBrowserXSSFilter), + "getBrowserXSSFilterHeaders": getFuncBoolLabelV1(label.TraefikFrontendBrowserXSSFilter, false), + "hasContentSecurityPolicyHeaders": hasLabelV1(label.TraefikFrontendContentSecurityPolicy), + "getContentSecurityPolicyHeaders": getFuncStringLabelV1(label.TraefikFrontendContentSecurityPolicy, ""), + "hasPublicKeyHeaders": hasLabelV1(label.TraefikFrontendPublicKey), + "getPublicKeyHeaders": getFuncStringLabelV1(label.TraefikFrontendPublicKey, ""), + "hasReferrerPolicyHeaders": hasLabelV1(label.TraefikFrontendReferrerPolicy), + "getReferrerPolicyHeaders": getFuncStringLabelV1(label.TraefikFrontendReferrerPolicy, ""), + "hasIsDevelopmentHeaders": hasLabelV1(label.TraefikFrontendIsDevelopment), + "getIsDevelopmentHeaders": getFuncBoolLabelV1(label.TraefikFrontendIsDevelopment, false), + + // Services + "hasServices": hasServicesV1, + "getServiceNames": getServiceNamesV1, + "getServiceBackend": getServiceBackendNameV1, + "getServiceBackendName": getServiceBackendNameV1, + // Services - Backend server functions + "getServicePort": getServicePortV1, + "getServiceProtocol": getFuncServiceStringLabelV1(label.SuffixProtocol, label.DefaultProtocol), + "getServiceWeight": getFuncServiceStringLabelV1(label.SuffixWeight, label.DefaultWeight), + // Services - Frontend functions + "getServiceEntryPoints": getFuncServiceSliceStringLabelV1(label.SuffixFrontendEntryPoints), + "getServiceWhitelistSourceRange": getFuncServiceSliceStringLabelV1(label.SuffixFrontendWhitelistSourceRange), + "getServiceBasicAuth": getFuncServiceSliceStringLabelV1(label.SuffixFrontendAuthBasic), + "getServiceFrontendRule": p.getServiceFrontendRuleV1, + "getServicePassHostHeader": getFuncServiceBoolLabelV1(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getServicePassTLSCert": getFuncServiceBoolLabelV1(label.SuffixFrontendPassTLSCert, label.DefaultPassTLSCert), + "getServicePriority": getFuncServiceIntLabelV1(label.SuffixFrontendPriority, label.DefaultFrontendPriorityInt), + "hasServiceRedirect": hasServiceRedirectV1, + "getServiceRedirectEntryPoint": getFuncServiceStringLabelV1(label.SuffixFrontendRedirectEntryPoint, ""), + "getServiceRedirectReplacement": getFuncServiceStringLabelV1(label.SuffixFrontendRedirectReplacement, ""), + "getServiceRedirectRegex": getFuncServiceStringLabelV1(label.SuffixFrontendRedirectRegex, ""), + } + + // filter containers + filteredContainers := fun.Filter(func(container dockerData) bool { + return p.containerFilterV1(container) + }, containersInspected).([]dockerData) + + frontends := map[string][]dockerData{} + backends := map[string]dockerData{} + servers := map[string][]dockerData{} + serviceNames := make(map[string]struct{}) + for idx, container := range filteredContainers { + if _, exists := serviceNames[container.ServiceName]; !exists { + frontendName := p.getFrontendNameV1(container, idx) + frontends[frontendName] = append(frontends[frontendName], container) + if len(container.ServiceName) > 0 { + serviceNames[container.ServiceName] = struct{}{} + } + } + backendName := getBackendNameV1(container) + backends[backendName] = container + servers[backendName] = append(servers[backendName], container) + } + + templateObjects := struct { + Containers []dockerData + Frontends map[string][]dockerData + Backends map[string]dockerData + Servers map[string][]dockerData + Domain string + }{ + Containers: filteredContainers, + Frontends: frontends, + Backends: backends, + Servers: servers, + Domain: p.Domain, + } + + configuration, err := p.GetConfiguration("templates/docker-v1.tmpl", DockerFuncMap, templateObjects) + if err != nil { + log.Error(err) + } + + return configuration +} + +// Deprecated +func (p Provider) containerFilterV1(container dockerData) bool { + if !label.IsEnabled(container.Labels, p.ExposedByDefault) { + log.Debugf("Filtering disabled container %s", container.Name) + return false + } + + var err error + portLabel := "traefik.port label" + if hasServicesV1(container) { + portLabel = "traefik..port or " + portLabel + "s" + err = checkServiceLabelPortV1(container) + } else { + _, err = strconv.Atoi(container.Labels[label.TraefikPort]) + } + if len(container.NetworkSettings.Ports) == 0 && err != nil { + log.Debugf("Filtering container without port and no %s %s : %s", portLabel, container.Name, err.Error()) + return false + } + + constraintTags := label.SplitAndTrimString(container.Labels[label.TraefikTags], ",") + if ok, failingConstraint := p.MatchConstraints(constraintTags); !ok { + if failingConstraint != nil { + log.Debugf("Container %v pruned by '%v' constraint", container.Name, failingConstraint.String()) + } + return false + } + + if container.Health != "" && container.Health != "healthy" { + log.Debugf("Filtering unhealthy or starting container %s", container.Name) + return false + } + + if len(p.getFrontendRuleV1(container)) == 0 { + log.Debugf("Filtering container with empty frontend rule %s", container.Name) + return false + } + + return true +} diff --git a/provider/docker/deprecated_container.go b/provider/docker/deprecated_container.go new file mode 100644 index 000000000..ce05082b2 --- /dev/null +++ b/provider/docker/deprecated_container.go @@ -0,0 +1,214 @@ +package docker + +import ( + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/containous/traefik/log" + "github.com/containous/traefik/provider" + "github.com/containous/traefik/provider/label" + "github.com/docker/go-connections/nat" +) + +// Specific functions + +// Deprecated +func (p Provider) getFrontendNameV1(container dockerData, idx int) string { + return provider.Normalize(p.getFrontendRuleV1(container) + "-" + strconv.Itoa(idx)) +} + +// GetFrontendRule returns the frontend rule for the specified container, using +// it's label. It returns a default one (Host) if the label is not present. +// Deprecated +func (p Provider) getFrontendRuleV1(container dockerData) string { + if value := label.GetStringValue(container.Labels, label.TraefikFrontendRule, ""); len(value) != 0 { + return value + } + + if values, err := label.GetStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil { + return "Host:" + getSubDomain(values[labelDockerComposeService]+"."+values[labelDockerComposeProject]) + "." + p.Domain + } + + if len(p.Domain) > 0 { + return "Host:" + getSubDomain(container.ServiceName) + "." + p.Domain + } + + return "" +} + +// Deprecated +func getBackendNameV1(container dockerData) string { + if value := label.GetStringValue(container.Labels, label.TraefikBackend, ""); len(value) != 0 { + return provider.Normalize(value) + } + + if values, err := label.GetStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil { + return provider.Normalize(values[labelDockerComposeService] + "_" + values[labelDockerComposeProject]) + } + + return provider.Normalize(container.ServiceName) +} + +// Deprecated +func getPortV1(container dockerData) string { + if value := label.GetStringValue(container.Labels, label.TraefikPort, ""); len(value) != 0 { + return value + } + + // See iteration order in https://blog.golang.org/go-maps-in-action + var ports []nat.Port + for port := range container.NetworkSettings.Ports { + ports = append(ports, port) + } + + less := func(i, j nat.Port) bool { + return i.Int() < j.Int() + } + nat.Sort(ports, less) + + if len(ports) > 0 { + min := ports[0] + return min.Port() + } + + return "" +} + +// replaced by Stickiness +// Deprecated +func getStickyV1(container dockerData) bool { + if label.Has(container.Labels, label.TraefikBackendLoadBalancerSticky) { + log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) + } + + return label.GetBoolValue(container.Labels, label.TraefikBackendLoadBalancerSticky, false) +} + +// Deprecated +func hasLoadBalancerLabelV1(container dockerData) bool { + method := label.Has(container.Labels, label.TraefikBackendLoadBalancerMethod) + sticky := label.Has(container.Labels, label.TraefikBackendLoadBalancerSticky) + stickiness := label.Has(container.Labels, label.TraefikBackendLoadBalancerStickiness) + cookieName := label.Has(container.Labels, label.TraefikBackendLoadBalancerStickinessCookieName) + return method || sticky || stickiness || cookieName +} + +// Deprecated +func hasMaxConnLabelsV1(container dockerData) bool { + mca := label.Has(container.Labels, label.TraefikBackendMaxConnAmount) + mcef := label.Has(container.Labels, label.TraefikBackendMaxConnExtractorFunc) + return mca && mcef +} + +// Deprecated +func hasRedirectV1(container dockerData) bool { + return hasLabelV1(label.TraefikFrontendRedirectEntryPoint)(container) || + hasLabelV1(label.TraefikFrontendRedirectReplacement)(container) && hasLabelV1(label.TraefikFrontendRedirectRegex)(container) +} + +// Deprecated +func hasHeadersV1(container dockerData) bool { + for key := range container.Labels { + if strings.HasPrefix(key, label.Prefix+"frontend.headers.") { + return true + } + } + return false +} + +// Label functions + +// Deprecated +func getFuncStringLabelV1(labelName string, defaultValue string) func(container dockerData) string { + return func(container dockerData) string { + return label.GetStringValue(container.Labels, labelName, defaultValue) + } +} + +// Deprecated +func getFuncBoolLabelV1(labelName string, defaultValue bool) func(container dockerData) bool { + return func(container dockerData) bool { + return label.GetBoolValue(container.Labels, labelName, defaultValue) + } +} + +// Deprecated +func getFuncSliceStringLabelV1(labelName string) func(container dockerData) []string { + return func(container dockerData) []string { + return label.GetSliceStringValue(container.Labels, labelName) + } +} + +// Deprecated +func getFuncIntLabelV1(labelName string, defaultValue int) func(container dockerData) int { + return func(container dockerData) int { + return label.GetIntValue(container.Labels, labelName, defaultValue) + } +} + +// Deprecated +func getFuncInt64LabelV1(labelName string, defaultValue int64) func(container dockerData) int64 { + return func(container dockerData) int64 { + return label.GetInt64Value(container.Labels, labelName, defaultValue) + } +} + +// Deprecated +func hasFuncV1(labelName string) func(container dockerData) bool { + return func(container dockerData) bool { + return label.Has(container.Labels, labelName) + } +} + +// Deprecated +func hasLabelV1(label string) func(container dockerData) bool { + return func(container dockerData) bool { + lbl, err := getLabelV1(container, label) + return err == nil && len(lbl) > 0 + } +} + +// Deprecated +func getLabelV1(container dockerData, label string) (string, error) { + if value, ok := container.Labels[label]; ok { + return value, nil + } + return "", fmt.Errorf("label not found: %s", label) +} + +// Deprecated +func getFuncMapLabelV1(labelName string) func(container dockerData) map[string]string { + return func(container dockerData) map[string]string { + return parseMapLabelV1(container, labelName) + } +} + +// Deprecated +func parseMapLabelV1(container dockerData, labelName string) map[string]string { + if parts, err := getLabelV1(container, labelName); err == nil { + if len(parts) == 0 { + log.Errorf("Could not load %q", labelName) + return nil + } + + values := make(map[string]string) + for _, headers := range strings.Split(parts, "||") { + pair := strings.SplitN(headers, ":", 2) + if len(pair) != 2 { + log.Warnf("Could not load %q: %v, skipping...", labelName, pair) + } else { + values[http.CanonicalHeaderKey(strings.TrimSpace(pair[0]))] = strings.TrimSpace(pair[1]) + } + } + + if len(values) == 0 { + log.Errorf("Could not load %q", labelName) + return nil + } + return values + } + + return nil +} diff --git a/provider/docker/deprecated_container_docker_test.go b/provider/docker/deprecated_container_docker_test.go new file mode 100644 index 000000000..6aeef186d --- /dev/null +++ b/provider/docker/deprecated_container_docker_test.go @@ -0,0 +1,965 @@ +package docker + +import ( + "reflect" + "strconv" + "testing" + + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + docker "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDockerBuildConfigurationV1(t *testing.T) { + testCases := []struct { + desc string + containers []docker.ContainerJSON + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{ + { + desc: "when no container", + containers: []docker.ContainerJSON{}, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + desc: "when basic container configuration", + containers: []docker.ContainerJSON{ + containerJSON( + name("test"), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-test-docker-localhost-0": { + Backend: "backend-test", + PassHostHeader: true, + EntryPoints: []string{}, + BasicAuth: []string{}, + Routes: map[string]types.Route{ + "route-frontend-Host-test-docker-localhost-0": { + Rule: "Host:test.docker.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test": { + Servers: map[string]types.Server{ + "server-test": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "when container has label 'enable' to false", + containers: []docker.ContainerJSON{ + containerJSON( + name("test"), + labels(map[string]string{ + label.TraefikEnable: "false", + label.TraefikPort: "666", + label.TraefikProtocol: "https", + label.TraefikWeight: "12", + label.TraefikBackend: "foobar", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + }, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + desc: "when all labels are set", + containers: []docker.ContainerJSON{ + containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikPort: "666", + label.TraefikProtocol: "https", + label.TraefikWeight: "12", + + label.TraefikBackend: "foobar", + + label.TraefikBackendCircuitBreakerExpression: "NetworkErrorRatio() > 0.5", + label.TraefikBackendLoadBalancerMethod: "drr", + label.TraefikBackendLoadBalancerSticky: "true", + label.TraefikBackendLoadBalancerStickiness: "true", + label.TraefikBackendLoadBalancerStickinessCookieName: "chocolate", + label.TraefikBackendMaxConnAmount: "666", + label.TraefikBackendMaxConnExtractorFunc: "client.ip", + + label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.TraefikFrontendEntryPoints: "http,https", + label.TraefikFrontendPassHostHeader: "true", + label.TraefikFrontendPassTLSCert: "true", + label.TraefikFrontendPriority: "666", + label.TraefikFrontendRedirectEntryPoint: "https", + label.TraefikFrontendRedirectRegex: "nope", + label.TraefikFrontendRedirectReplacement: "nope", + label.TraefikFrontendRule: "Host:traefik.io", + label.TraefikFrontendWhitelistSourceRange: "10.10.10.10", + + label.TraefikFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.TraefikFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.TraefikFrontendSSLProxyHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.TraefikFrontendAllowedHosts: "foo,bar,bor", + label.TraefikFrontendHostsProxyHeaders: "foo,bar,bor", + label.TraefikFrontendSSLHost: "foo", + label.TraefikFrontendCustomFrameOptionsValue: "foo", + label.TraefikFrontendContentSecurityPolicy: "foo", + label.TraefikFrontendPublicKey: "foo", + label.TraefikFrontendReferrerPolicy: "foo", + label.TraefikFrontendSTSSeconds: "666", + label.TraefikFrontendSSLRedirect: "true", + label.TraefikFrontendSSLTemporaryRedirect: "true", + label.TraefikFrontendSTSIncludeSubdomains: "true", + label.TraefikFrontendSTSPreload: "true", + label.TraefikFrontendForceSTSHeader: "true", + label.TraefikFrontendFrameDeny: "true", + label.TraefikFrontendContentTypeNosniff: "true", + label.TraefikFrontendBrowserXSSFilter: "true", + label.TraefikFrontendIsDevelopment: "true", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-traefik-io-0": { + EntryPoints: []string{ + "http", + "https", + }, + Backend: "backend-foobar", + Routes: map[string]types.Route{ + "route-frontend-Host-traefik-io-0": { + Rule: "Host:traefik.io", + }, + }, + PassHostHeader: true, + PassTLSCert: true, + Priority: 666, + BasicAuth: []string{ + "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }, + WhitelistSourceRange: []string{ + "10.10.10.10", + }, + Headers: &types.Headers{ + CustomRequestHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + CustomResponseHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + AllowedHosts: []string{ + "foo", + "bar", + "bor", + }, + HostsProxyHeaders: []string{ + "foo", + "bar", + "bor", + }, + SSLRedirect: true, + SSLTemporaryRedirect: true, + SSLHost: "foo", + SSLProxyHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + STSSeconds: 666, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + CustomFrameOptionsValue: "foo", + ContentTypeNosniff: true, + BrowserXSSFilter: true, + ContentSecurityPolicy: "foo", + PublicKey: "foo", + ReferrerPolicy: "foo", + IsDevelopment: true, + }, + Redirect: &types.Redirect{ + EntryPoint: "https", + Regex: "nope", + Replacement: "nope", + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foobar": { + Servers: map[string]types.Server{ + "server-test1": { + URL: "https://127.0.0.1:666", + Weight: 12, + }, + }, + CircuitBreaker: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", + }, + LoadBalancer: &types.LoadBalancer{ + Method: "drr", + Sticky: true, + Stickiness: &types.Stickiness{ + CookieName: "chocolate", + }, + }, + MaxConn: &types.MaxConn{ + Amount: 666, + ExtractorFunc: "client.ip", + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + var dockerDataList []dockerData + for _, cont := range test.containers { + dData := parseContainer(cont) + dockerDataList = append(dockerDataList, dData) + } + + provider := &Provider{ + Domain: "docker.localhost", + ExposedByDefault: true, + } + actualConfig := provider.buildConfigurationV1(dockerDataList) + require.NotNil(t, actualConfig, "actualConfig") + + assert.EqualValues(t, test.expectedBackends, actualConfig.Backends) + assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends) + }) + } +} + +func TestDockerTraefikFilterV1(t *testing.T) { + testCases := []struct { + container docker.ContainerJSON + expected bool + provider *Provider + }{ + { + container: docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "container", + }, + Config: &container.Config{}, + NetworkSettings: &docker.NetworkSettings{}, + }, + expected: false, + provider: &Provider{ + Domain: "test", + ExposedByDefault: true, + }, + }, + { + container: docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "container", + }, + Config: &container.Config{ + Labels: map[string]string{ + label.TraefikEnable: "false", + }, + }, + NetworkSettings: &docker.NetworkSettings{ + NetworkSettingsBase: docker.NetworkSettingsBase{ + Ports: nat.PortMap{ + "80/tcp": {}, + }, + }, + }, + }, + provider: &Provider{ + Domain: "test", + ExposedByDefault: true, + }, + expected: false, + }, + { + container: docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "container", + }, + Config: &container.Config{ + Labels: map[string]string{ + label.TraefikFrontendRule: "Host:foo.bar", + }, + }, + NetworkSettings: &docker.NetworkSettings{ + NetworkSettingsBase: docker.NetworkSettingsBase{ + Ports: nat.PortMap{ + "80/tcp": {}, + }, + }, + }, + }, + provider: &Provider{ + Domain: "test", + ExposedByDefault: true, + }, + expected: true, + }, + { + container: docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "container-multi-ports", + }, + Config: &container.Config{}, + NetworkSettings: &docker.NetworkSettings{ + NetworkSettingsBase: docker.NetworkSettingsBase{ + Ports: nat.PortMap{ + "80/tcp": {}, + "443/tcp": {}, + }, + }, + }, + }, + provider: &Provider{ + Domain: "test", + ExposedByDefault: true, + }, + expected: true, + }, + { + container: docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "container", + }, + Config: &container.Config{}, + NetworkSettings: &docker.NetworkSettings{ + NetworkSettingsBase: docker.NetworkSettingsBase{ + Ports: nat.PortMap{ + "80/tcp": {}, + }, + }, + }, + }, + provider: &Provider{ + Domain: "test", + ExposedByDefault: true, + }, + expected: true, + }, + { + container: docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "container", + }, + Config: &container.Config{ + Labels: map[string]string{ + label.TraefikPort: "80", + }, + }, + NetworkSettings: &docker.NetworkSettings{ + NetworkSettingsBase: docker.NetworkSettingsBase{ + Ports: nat.PortMap{ + "80/tcp": {}, + "443/tcp": {}, + }, + }, + }, + }, + provider: &Provider{ + Domain: "test", + ExposedByDefault: true, + }, + expected: true, + }, + { + container: docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "container", + }, + Config: &container.Config{ + Labels: map[string]string{ + label.TraefikEnable: "true", + }, + }, + NetworkSettings: &docker.NetworkSettings{ + NetworkSettingsBase: docker.NetworkSettingsBase{ + Ports: nat.PortMap{ + "80/tcp": {}, + }, + }, + }, + }, + provider: &Provider{ + Domain: "test", + ExposedByDefault: true, + }, + expected: true, + }, + { + container: docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "container", + }, + Config: &container.Config{ + Labels: map[string]string{ + label.TraefikEnable: "anything", + }, + }, + NetworkSettings: &docker.NetworkSettings{ + NetworkSettingsBase: docker.NetworkSettingsBase{ + Ports: nat.PortMap{ + "80/tcp": {}, + }, + }, + }, + }, + provider: &Provider{ + Domain: "test", + ExposedByDefault: true, + }, + expected: true, + }, + { + container: docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "container", + }, + Config: &container.Config{ + Labels: map[string]string{ + label.TraefikFrontendRule: "Host:foo.bar", + }, + }, + NetworkSettings: &docker.NetworkSettings{ + NetworkSettingsBase: docker.NetworkSettingsBase{ + Ports: nat.PortMap{ + "80/tcp": {}, + }, + }, + }, + }, + provider: &Provider{ + Domain: "test", + ExposedByDefault: true, + }, + expected: true, + }, + { + container: docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "container", + }, + Config: &container.Config{}, + NetworkSettings: &docker.NetworkSettings{ + NetworkSettingsBase: docker.NetworkSettingsBase{ + Ports: nat.PortMap{ + "80/tcp": {}, + }, + }, + }, + }, + provider: &Provider{ + Domain: "test", + ExposedByDefault: false, + }, + expected: false, + }, + { + container: docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "container", + }, + Config: &container.Config{ + Labels: map[string]string{ + label.TraefikEnable: "true", + }, + }, + NetworkSettings: &docker.NetworkSettings{ + NetworkSettingsBase: docker.NetworkSettingsBase{ + Ports: nat.PortMap{ + "80/tcp": {}, + }, + }, + }, + }, + provider: &Provider{ + Domain: "test", + ExposedByDefault: false, + }, + expected: true, + }, + { + container: docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "container", + }, + Config: &container.Config{ + Labels: map[string]string{ + label.TraefikEnable: "true", + }, + }, + NetworkSettings: &docker.NetworkSettings{ + NetworkSettingsBase: docker.NetworkSettingsBase{ + Ports: nat.PortMap{ + "80/tcp": {}, + }, + }, + }, + }, + provider: &Provider{ + ExposedByDefault: false, + }, + expected: false, + }, + { + container: docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "container", + }, + Config: &container.Config{ + Labels: map[string]string{ + label.TraefikEnable: "true", + label.TraefikFrontendRule: "Host:i.love.this.host", + }, + }, + NetworkSettings: &docker.NetworkSettings{ + NetworkSettingsBase: docker.NetworkSettingsBase{ + Ports: nat.PortMap{ + "80/tcp": {}, + }, + }, + }, + }, + provider: &Provider{ + ExposedByDefault: false, + }, + expected: true, + }, + } + + for containerID, test := range testCases { + test := test + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := test.provider.containerFilterV1(dData) + if actual != test.expected { + t.Errorf("expected %v for %+v, got %+v", test.expected, test, actual) + } + }) + } +} + +func TestDockerGetFuncStringLabelV1(t *testing.T) { + testCases := []struct { + container docker.ContainerJSON + labelName string + defaultValue string + expected string + }{ + { + container: containerJSON(), + labelName: label.TraefikWeight, + defaultValue: label.DefaultWeight, + expected: "0", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikWeight: "10", + })), + labelName: label.TraefikWeight, + defaultValue: label.DefaultWeight, + expected: "10", + }, + } + + for containerID, test := range testCases { + test := test + t.Run(test.labelName+strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := getFuncStringLabelV1(test.labelName, test.defaultValue)(dData) + if actual != test.expected { + t.Errorf("got %q, expected %q", actual, test.expected) + } + }) + } +} + +func TestDockerGetSliceStringLabelV1(t *testing.T) { + testCases := []struct { + desc string + container docker.ContainerJSON + labelName string + expected []string + }{ + { + desc: "no whitelist-label", + container: containerJSON(), + expected: nil, + }, + { + desc: "whitelist-label with empty string", + container: containerJSON(labels(map[string]string{ + label.TraefikFrontendWhitelistSourceRange: "", + })), + labelName: label.TraefikFrontendWhitelistSourceRange, + expected: nil, + }, + { + desc: "whitelist-label with IPv4 mask", + container: containerJSON(labels(map[string]string{ + label.TraefikFrontendWhitelistSourceRange: "1.2.3.4/16", + })), + labelName: label.TraefikFrontendWhitelistSourceRange, + expected: []string{ + "1.2.3.4/16", + }, + }, + { + desc: "whitelist-label with IPv6 mask", + container: containerJSON(labels(map[string]string{ + label.TraefikFrontendWhitelistSourceRange: "fe80::/16", + })), + labelName: label.TraefikFrontendWhitelistSourceRange, + expected: []string{ + "fe80::/16", + }, + }, + { + desc: "whitelist-label with multiple masks", + container: containerJSON(labels(map[string]string{ + label.TraefikFrontendWhitelistSourceRange: "1.1.1.1/24, 1234:abcd::42/32", + })), + labelName: label.TraefikFrontendWhitelistSourceRange, + expected: []string{ + "1.1.1.1/24", + "1234:abcd::42/32", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := getFuncSliceStringLabelV1(test.labelName)(dData) + if !reflect.DeepEqual(actual, test.expected) { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestDockerGetFrontendNameV1(t *testing.T) { + testCases := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(name("foo")), + expected: "Host-foo-docker-localhost-0", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikFrontendRule: "Headers:User-Agent,bat/0.1.0", + })), + expected: "Headers-User-Agent-bat-0-1-0-0", + }, + { + container: containerJSON(labels(map[string]string{ + "com.docker.compose.project": "foo", + "com.docker.compose.service": "bar", + })), + expected: "Host-bar-foo-docker-localhost-0", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikFrontendRule: "Host:foo.bar", + })), + expected: "Host-foo-bar-0", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikFrontendRule: "Path:/test", + })), + expected: "Path-test-0", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikFrontendRule: "PathPrefix:/test2", + })), + expected: "PathPrefix-test2-0", + }, + } + + for containerID, test := range testCases { + test := test + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + provider := &Provider{ + Domain: "docker.localhost", + } + + actual := provider.getFrontendNameV1(dData, 0) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestDockerGetFrontendRuleV1(t *testing.T) { + testCases := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(name("foo")), + expected: "Host:foo.docker.localhost", + }, + { + container: containerJSON(name("bar")), + expected: "Host:bar.docker.localhost", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikFrontendRule: "Host:foo.bar", + })), + expected: "Host:foo.bar", + }, { + container: containerJSON(labels(map[string]string{ + "com.docker.compose.project": "foo", + "com.docker.compose.service": "bar", + })), + expected: "Host:bar.foo.docker.localhost", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikFrontendRule: "Path:/test", + })), + expected: "Path:/test", + }, + } + + for containerID, test := range testCases { + test := test + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + provider := &Provider{ + Domain: "docker.localhost", + } + + actual := provider.getFrontendRuleV1(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestDockerGetBackendNameV1(t *testing.T) { + testCases := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(name("foo")), + expected: "foo", + }, + { + container: containerJSON(name("bar")), + expected: "bar", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikBackend: "foobar", + })), + expected: "foobar", + }, + { + container: containerJSON(labels(map[string]string{ + "com.docker.compose.project": "foo", + "com.docker.compose.service": "bar", + })), + expected: "bar-foo", + }, + } + + for containerID, test := range testCases { + test := test + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := getBackendNameV1(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestDockerGetIPAddressV1(t *testing.T) { + testCases := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(withNetwork("testnet", ipv4("10.11.12.13"))), + expected: "10.11.12.13", + }, + { + container: containerJSON( + labels(map[string]string{ + labelDockerNetwork: "testnet", + }), + withNetwork("testnet", ipv4("10.11.12.13")), + ), + expected: "10.11.12.13", + }, + { + container: containerJSON( + labels(map[string]string{ + labelDockerNetwork: "testnet2", + }), + withNetwork("testnet", ipv4("10.11.12.13")), + withNetwork("testnet2", ipv4("10.11.12.14")), + ), + expected: "10.11.12.14", + }, + { + container: containerJSON( + networkMode("host"), + withNetwork("testnet", ipv4("10.11.12.13")), + withNetwork("testnet2", ipv4("10.11.12.14")), + ), + expected: "127.0.0.1", + }, + { + container: containerJSON( + networkMode("host"), + ), + expected: "127.0.0.1", + }, + { + container: containerJSON( + networkMode("host"), + nodeIP("10.0.0.5"), + ), + expected: "10.0.0.5", + }, + } + + for containerID, test := range testCases { + test := test + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dData := parseContainer(test.container) + provider := &Provider{} + actual := provider.getIPAddress(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestDockerGetPortV1(t *testing.T) { + testCases := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(name("foo")), + expected: "", + }, + { + container: containerJSON(ports(nat.PortMap{ + "80/tcp": {}, + })), + expected: "80", + }, + { + container: containerJSON(ports(nat.PortMap{ + "80/tcp": {}, + "443/tcp": {}, + })), + expected: "80", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikPort: "8080", + })), + expected: "8080", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikPort: "8080", + }), ports(nat.PortMap{ + "80/tcp": {}, + })), + expected: "8080", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikPort: "8080", + }), ports(nat.PortMap{ + "8080/tcp": {}, + "80/tcp": {}, + })), + expected: "8080", + }, + } + + for containerID, e := range testCases { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + + dData := parseContainer(e.container) + + actual := getPortV1(dData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} diff --git a/provider/docker/deprecated_container_swarm_test.go b/provider/docker/deprecated_container_swarm_test.go new file mode 100644 index 000000000..387cd803b --- /dev/null +++ b/provider/docker/deprecated_container_swarm_test.go @@ -0,0 +1,706 @@ +package docker + +import ( + "strconv" + "testing" + + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + docker "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSwarmBuildConfigurationV1(t *testing.T) { + testCases := []struct { + desc string + services []swarm.Service + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + networks map[string]*docker.NetworkResource + }{ + { + desc: "when no container", + services: []swarm.Service{}, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + networks: map[string]*docker.NetworkResource{}, + }, + { + desc: "when basic container configuration", + services: []swarm.Service{ + swarmService( + serviceName("test"), + serviceLabels(map[string]string{ + label.TraefikPort: "80", + }), + withEndpointSpec(modeVIP), + withEndpoint(virtualIP("1", "127.0.0.1/24")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-test-docker-localhost-0": { + Backend: "backend-test", + PassHostHeader: true, + EntryPoints: []string{}, + BasicAuth: []string{}, + Routes: map[string]types.Route{ + "route-frontend-Host-test-docker-localhost-0": { + Rule: "Host:test.docker.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test": { + Servers: map[string]types.Server{ + "server-test": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + }, + }, + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foo", + }, + }, + }, + { + desc: "when container has label 'enable' to false", + services: []swarm.Service{ + swarmService( + serviceName("test1"), + serviceLabels(map[string]string{ + label.TraefikEnable: "false", + label.TraefikPort: "666", + label.TraefikProtocol: "https", + label.TraefikWeight: "12", + label.TraefikBackend: "foobar", + }), + withEndpointSpec(modeVIP), + withEndpoint(virtualIP("1", "127.0.0.1/24")), + ), + }, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foo", + }, + }, + }, + { + desc: "when all labels are set", + services: []swarm.Service{ + swarmService( + serviceName("test1"), + serviceLabels(map[string]string{ + label.TraefikPort: "666", + label.TraefikProtocol: "https", + label.TraefikWeight: "12", + + label.TraefikBackend: "foobar", + + label.TraefikBackendCircuitBreakerExpression: "NetworkErrorRatio() > 0.5", + label.TraefikBackendLoadBalancerMethod: "drr", + label.TraefikBackendLoadBalancerSticky: "true", + label.TraefikBackendLoadBalancerStickiness: "true", + label.TraefikBackendLoadBalancerStickinessCookieName: "chocolate", + label.TraefikBackendMaxConnAmount: "666", + label.TraefikBackendMaxConnExtractorFunc: "client.ip", + + label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.TraefikFrontendEntryPoints: "http,https", + label.TraefikFrontendPassHostHeader: "true", + label.TraefikFrontendPassTLSCert: "true", + label.TraefikFrontendPriority: "666", + label.TraefikFrontendRedirectEntryPoint: "https", + label.TraefikFrontendRedirectRegex: "nope", + label.TraefikFrontendRedirectReplacement: "nope", + label.TraefikFrontendRule: "Host:traefik.io", + label.TraefikFrontendWhitelistSourceRange: "10.10.10.10", + + label.TraefikFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.TraefikFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.TraefikFrontendSSLProxyHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.TraefikFrontendAllowedHosts: "foo,bar,bor", + label.TraefikFrontendHostsProxyHeaders: "foo,bar,bor", + label.TraefikFrontendSSLHost: "foo", + label.TraefikFrontendCustomFrameOptionsValue: "foo", + label.TraefikFrontendContentSecurityPolicy: "foo", + label.TraefikFrontendPublicKey: "foo", + label.TraefikFrontendReferrerPolicy: "foo", + label.TraefikFrontendSTSSeconds: "666", + label.TraefikFrontendSSLRedirect: "true", + label.TraefikFrontendSSLTemporaryRedirect: "true", + label.TraefikFrontendSTSIncludeSubdomains: "true", + label.TraefikFrontendSTSPreload: "true", + label.TraefikFrontendForceSTSHeader: "true", + label.TraefikFrontendFrameDeny: "true", + label.TraefikFrontendContentTypeNosniff: "true", + label.TraefikFrontendBrowserXSSFilter: "true", + label.TraefikFrontendIsDevelopment: "true", + }), + withEndpointSpec(modeVIP), + withEndpoint(virtualIP("1", "127.0.0.1/24")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-traefik-io-0": { + EntryPoints: []string{ + "http", + "https", + }, + Backend: "backend-foobar", + Routes: map[string]types.Route{ + "route-frontend-Host-traefik-io-0": { + Rule: "Host:traefik.io", + }, + }, + PassHostHeader: true, + PassTLSCert: true, + Priority: 666, + BasicAuth: []string{ + "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }, + WhitelistSourceRange: []string{ + "10.10.10.10", + }, + Headers: &types.Headers{ + CustomRequestHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + CustomResponseHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + AllowedHosts: []string{ + "foo", + "bar", + "bor", + }, + HostsProxyHeaders: []string{ + "foo", + "bar", + "bor", + }, + SSLRedirect: true, + SSLTemporaryRedirect: true, + SSLHost: "foo", + SSLProxyHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + STSSeconds: 666, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + CustomFrameOptionsValue: "foo", + ContentTypeNosniff: true, + BrowserXSSFilter: true, + ContentSecurityPolicy: "foo", + PublicKey: "foo", + ReferrerPolicy: "foo", + IsDevelopment: true, + }, + Redirect: &types.Redirect{ + EntryPoint: "https", + Regex: "nope", + Replacement: "nope", + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foobar": { + Servers: map[string]types.Server{ + "server-test1": { + URL: "https://127.0.0.1:666", + Weight: 12, + }, + }, + CircuitBreaker: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", + }, + LoadBalancer: &types.LoadBalancer{ + Method: "drr", + Sticky: true, + Stickiness: &types.Stickiness{ + CookieName: "chocolate", + }, + }, + MaxConn: &types.MaxConn{ + Amount: 666, + ExtractorFunc: "client.ip", + }, + }, + }, + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foo", + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + var dockerDataList []dockerData + for _, service := range test.services { + dData := parseService(service, test.networks) + dockerDataList = append(dockerDataList, dData) + } + + provider := &Provider{ + Domain: "docker.localhost", + ExposedByDefault: true, + SwarmMode: true, + } + + actualConfig := provider.buildConfigurationV1(dockerDataList) + require.NotNil(t, actualConfig, "actualConfig") + + assert.EqualValues(t, test.expectedBackends, actualConfig.Backends) + assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends) + }) + } +} + +func TestSwarmTraefikFilterV1(t *testing.T) { + testCases := []struct { + service swarm.Service + expected bool + networks map[string]*docker.NetworkResource + provider *Provider + }{ + { + service: swarmService(), + expected: false, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: true, + }, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikEnable: "false", + label.TraefikPort: "80", + })), + expected: false, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: true, + }, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Host:foo.bar", + label.TraefikPort: "80", + })), + expected: true, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: true, + }, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikPort: "80", + })), + expected: true, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: true, + }, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikEnable: "true", + label.TraefikPort: "80", + })), + expected: true, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: true, + }, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikEnable: "anything", + label.TraefikPort: "80", + })), + expected: true, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: true, + }, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Host:foo.bar", + label.TraefikPort: "80", + })), + expected: true, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: true, + }, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikPort: "80", + })), + expected: false, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: false, + }, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikEnable: "true", + label.TraefikPort: "80", + })), + expected: true, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: false, + }, + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + + dData := parseService(test.service, test.networks) + + actual := test.provider.containerFilterV1(dData) + if actual != test.expected { + t.Errorf("expected %v for %+v, got %+v", test.expected, test, actual) + } + }) + } +} + +func TestSwarmGetFuncStringLabelV1(t *testing.T) { + testCases := []struct { + service swarm.Service + labelName string + defaultValue string + networks map[string]*docker.NetworkResource + expected string + }{ + { + service: swarmService(), + labelName: label.TraefikWeight, + defaultValue: label.DefaultWeight, + networks: map[string]*docker.NetworkResource{}, + expected: "0", + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikWeight: "10", + })), + labelName: label.TraefikWeight, + defaultValue: label.DefaultWeight, + networks: map[string]*docker.NetworkResource{}, + expected: "10", + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(test.labelName+strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + + dData := parseService(test.service, test.networks) + + actual := getFuncStringLabelV1(test.labelName, test.defaultValue)(dData) + if actual != test.expected { + t.Errorf("got %q, expected %q", actual, test.expected) + } + }) + } +} + +func TestSwarmGetFrontendNameV1(t *testing.T) { + testCases := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(serviceName("foo")), + expected: "Host-foo-docker-localhost-0", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Headers:User-Agent,bat/0.1.0", + })), + expected: "Headers-User-Agent-bat-0-1-0-0", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Host:foo.bar", + })), + expected: "Host-foo-bar-0", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Path:/test", + })), + expected: "Path-test-0", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService( + serviceName("test"), + serviceLabels(map[string]string{ + label.TraefikFrontendRule: "PathPrefix:/test2", + }), + ), + expected: "PathPrefix-test2-0", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + + dData := parseService(test.service, test.networks) + + provider := &Provider{ + Domain: "docker.localhost", + SwarmMode: true, + } + + actual := provider.getFrontendNameV1(dData, 0) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestSwarmGetFrontendRuleV1(t *testing.T) { + testCases := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(serviceName("foo")), + expected: "Host:foo.docker.localhost", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceName("bar")), + expected: "Host:bar.docker.localhost", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Host:foo.bar", + })), + expected: "Host:foo.bar", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Path:/test", + })), + expected: "Path:/test", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + + dData := parseService(test.service, test.networks) + + provider := &Provider{ + Domain: "docker.localhost", + SwarmMode: true, + } + + actual := provider.getFrontendRuleV1(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestSwarmGetBackendNameV1(t *testing.T) { + testCases := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(serviceName("foo")), + expected: "foo", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceName("bar")), + expected: "bar", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikBackend: "foobar", + })), + expected: "foobar", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + + dData := parseService(test.service, test.networks) + + actual := getBackendNameV1(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestSwarmGetIPAddressV1(t *testing.T) { + testCases := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(withEndpointSpec(modeDNSSR)), + expected: "", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService( + withEndpointSpec(modeVIP), + withEndpoint(virtualIP("1", "10.11.12.13/24")), + ), + expected: "10.11.12.13", + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foo", + }, + }, + }, + { + service: swarmService( + serviceLabels(map[string]string{ + labelDockerNetwork: "barnet", + }), + withEndpointSpec(modeVIP), + withEndpoint( + virtualIP("1", "10.11.12.13/24"), + virtualIP("2", "10.11.12.99/24"), + ), + ), + expected: "10.11.12.99", + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foonet", + }, + "2": { + Name: "barnet", + }, + }, + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + + dData := parseService(test.service, test.networks) + + provider := &Provider{ + SwarmMode: true, + } + + actual := provider.getIPAddress(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestSwarmGetPortV1(t *testing.T) { + testCases := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService( + serviceLabels(map[string]string{ + label.TraefikPort: "8080", + }), + withEndpointSpec(modeDNSSR), + ), + expected: "8080", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + + dData := parseService(test.service, test.networks) + + actual := getPortV1(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} diff --git a/provider/docker/deprecated_service.go b/provider/docker/deprecated_service.go new file mode 100644 index 000000000..ab8098f7a --- /dev/null +++ b/provider/docker/deprecated_service.go @@ -0,0 +1,231 @@ +package docker + +import ( + "errors" + "strconv" + "strings" + + "github.com/containous/traefik/provider" + "github.com/containous/traefik/provider/label" +) + +// Specific functions + +// Extract rule from labels for a given service and a given docker container +// Deprecated +func (p Provider) getServiceFrontendRuleV1(container dockerData, serviceName string) string { + if value, ok := getServiceLabelsV1(container, serviceName)[label.SuffixFrontendRule]; ok { + return value + } + return p.getFrontendRuleV1(container) +} + +// Check if for the given container, we find labels that are defining services +// Deprecated +func hasServicesV1(container dockerData) bool { + return len(label.ExtractServiceProperties(container.Labels)) > 0 +} + +// Gets array of service names for a given container +// Deprecated +func getServiceNamesV1(container dockerData) []string { + labelServiceProperties := label.ExtractServiceProperties(container.Labels) + keys := make([]string, 0, len(labelServiceProperties)) + for k := range labelServiceProperties { + keys = append(keys, k) + } + return keys +} + +// checkServiceLabelPort checks if all service names have a port service label +// or if port container label exists for default value +// Deprecated +func checkServiceLabelPortV1(container dockerData) error { + // If port container label is present, there is a default values for all ports, use it for the check + _, err := strconv.Atoi(container.Labels[label.TraefikPort]) + if err != nil { + serviceLabelPorts := make(map[string]struct{}) + serviceLabels := make(map[string]struct{}) + for lbl := range container.Labels { + // Get all port service labels + portLabel := extractServicePortV1(lbl) + if len(portLabel) > 0 { + serviceLabelPorts[portLabel[0]] = struct{}{} + } + // Get only one instance of all service names from service labels + servicesLabelNames := label.FindSegmentSubmatch(lbl) + + if len(servicesLabelNames) > 0 { + serviceLabels[strings.Split(servicesLabelNames[0], ".")[1]] = struct{}{} + } + } + // If the number of service labels is different than the number of port services label + // there is an error + if len(serviceLabels) == len(serviceLabelPorts) { + for labelPort := range serviceLabelPorts { + _, err = strconv.Atoi(container.Labels[labelPort]) + if err != nil { + break + } + } + } else { + err = errors.New("port service labels missing, please use traefik.port as default value or define all port service labels") + } + } + return err +} + +// Deprecated +func extractServicePortV1(labelName string) []string { + if strings.HasPrefix(labelName, label.TraefikFrontend+".") || + strings.HasPrefix(labelName, label.TraefikBackend+".") { + return nil + } + + return label.PortRegexp.FindStringSubmatch(labelName) +} + +// Extract backend from labels for a given service and a given docker container +// Deprecated +func getServiceBackendNameV1(container dockerData, serviceName string) string { + if value, ok := getServiceLabelsV1(container, serviceName)[label.SuffixFrontendBackend]; ok { + return provider.Normalize(container.ServiceName + "-" + value) + } + return provider.Normalize(container.ServiceName + "-" + getBackendNameV1(container) + "-" + serviceName) +} + +// Extract port from labels for a given service and a given docker container +// Deprecated +func getServicePortV1(container dockerData, serviceName string) string { + if value, ok := getServiceLabelsV1(container, serviceName)[label.SuffixPort]; ok { + return value + } + return getPortV1(container) +} + +// Service label functions + +// Deprecated +func getFuncServiceSliceStringLabelV1(labelSuffix string) func(container dockerData, serviceName string) []string { + return func(container dockerData, serviceName string) []string { + serviceLabels := getServiceLabelsV1(container, serviceName) + return getServiceSliceValueV1(container, serviceLabels, labelSuffix) + } +} + +// Deprecated +func getFuncServiceStringLabelV1(labelSuffix string, defaultValue string) func(container dockerData, serviceName string) string { + return func(container dockerData, serviceName string) string { + serviceLabels := getServiceLabelsV1(container, serviceName) + return getServiceStringValueV1(container, serviceLabels, labelSuffix, defaultValue) + } +} + +// Deprecated +func getFuncServiceBoolLabelV1(labelSuffix string, defaultValue bool) func(container dockerData, serviceName string) bool { + return func(container dockerData, serviceName string) bool { + serviceLabels := getServiceLabelsV1(container, serviceName) + return getServiceBoolValueV1(container, serviceLabels, labelSuffix, defaultValue) + } +} + +// Deprecated +func getFuncServiceIntLabelV1(labelSuffix string, defaultValue int) func(container dockerData, serviceName string) int { + return func(container dockerData, serviceName string) int { + return getServiceIntLabelV1(container, serviceName, labelSuffix, defaultValue) + } +} + +// Deprecated +func hasStrictServiceLabelV1(serviceLabels map[string]string, labelSuffix string) bool { + value, ok := serviceLabels[labelSuffix] + return ok && len(value) > 0 +} + +// Deprecated +func getServiceStringValueV1(container dockerData, serviceLabels map[string]string, labelSuffix string, defaultValue string) string { + if value, ok := serviceLabels[labelSuffix]; ok { + return value + } + return label.GetStringValue(container.Labels, label.Prefix+labelSuffix, defaultValue) +} + +// Deprecated +func getStrictServiceStringValueV1(serviceLabels map[string]string, labelSuffix string, defaultValue string) string { + if value, ok := serviceLabels[labelSuffix]; ok { + return value + } + return defaultValue +} + +// Deprecated +func getServiceMapValueV1(container dockerData, serviceLabels map[string]string, serviceName string, labelSuffix string) map[string]string { + if value, ok := serviceLabels[labelSuffix]; ok { + lblName := label.GetServiceLabel(labelSuffix, serviceName) + return label.ParseMapValue(lblName, value) + } + return label.GetMapValue(container.Labels, label.Prefix+labelSuffix) +} + +// Deprecated +func getServiceSliceValueV1(container dockerData, serviceLabels map[string]string, labelSuffix string) []string { + if value, ok := serviceLabels[labelSuffix]; ok { + return label.SplitAndTrimString(value, ",") + } + return label.GetSliceStringValue(container.Labels, label.Prefix+labelSuffix) +} + +// Deprecated +func getServiceBoolValueV1(container dockerData, serviceLabels map[string]string, labelSuffix string, defaultValue bool) bool { + if rawValue, ok := serviceLabels[labelSuffix]; ok { + value, err := strconv.ParseBool(rawValue) + if err == nil { + return value + } + } + return label.GetBoolValue(container.Labels, label.Prefix+labelSuffix, defaultValue) +} + +// Deprecated +func getServiceIntLabelV1(container dockerData, serviceName string, labelSuffix string, defaultValue int) int { + if rawValue, ok := getServiceLabelsV1(container, serviceName)[labelSuffix]; ok { + value, err := strconv.Atoi(rawValue) + if err == nil { + return value + } + } + return label.GetIntValue(container.Labels, label.Prefix+labelSuffix, defaultValue) +} + +// Deprecated +func getServiceInt64ValueV1(container dockerData, serviceLabels map[string]string, labelSuffix string, defaultValue int64) int64 { + if rawValue, ok := serviceLabels[labelSuffix]; ok { + value, err := strconv.ParseInt(rawValue, 10, 64) + if err == nil { + return value + } + } + return label.GetInt64Value(container.Labels, label.Prefix+labelSuffix, defaultValue) +} + +// Deprecated +func getServiceLabelsV1(container dockerData, serviceName string) label.SegmentPropertyValues { + return label.ExtractServiceProperties(container.Labels)[serviceName] +} + +// Deprecated +func hasServiceRedirectV1(container dockerData, serviceName string) bool { + serviceLabels, ok := label.ExtractServiceProperties(container.Labels)[serviceName] + if !ok || len(serviceLabels) == 0 { + return false + } + + value, ok := serviceLabels[label.SuffixFrontendRedirectEntryPoint] + frep := ok && len(value) > 0 + value, ok = serviceLabels[label.SuffixFrontendRedirectRegex] + frrg := ok && len(value) > 0 + value, ok = serviceLabels[label.SuffixFrontendRedirectReplacement] + frrp := ok && len(value) > 0 + + return frep || frrg && frrp +} diff --git a/provider/docker/config_service_test.go b/provider/docker/deprecated_service_test.go similarity index 52% rename from provider/docker/config_service_test.go rename to provider/docker/deprecated_service_test.go index 56a1c7715..552c6640b 100644 --- a/provider/docker/config_service_test.go +++ b/provider/docker/deprecated_service_test.go @@ -4,9 +4,7 @@ import ( "reflect" "strconv" "testing" - "time" - "github.com/containous/flaeg" "github.com/containous/traefik/provider/label" "github.com/containous/traefik/types" docker "github.com/docker/docker/api/types" @@ -15,7 +13,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestDockerServiceBuildConfiguration(t *testing.T) { +func TestDockerServiceBuildConfigurationV1(t *testing.T) { testCases := []struct { desc string containers []docker.ContainerJSON @@ -110,21 +108,6 @@ func TestDockerServiceBuildConfiguration(t *testing.T) { label.Prefix + "service." + label.SuffixFrontendHeadersContentTypeNosniff: "true", label.Prefix + "service." + label.SuffixFrontendHeadersBrowserXSSFilter: "true", label.Prefix + "service." + label.SuffixFrontendHeadersIsDevelopment: "true", - - label.Prefix + "service." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: "404", - label.Prefix + "service." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: "foobar", - label.Prefix + "service." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: "foo_query", - label.Prefix + "service." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: "500,600", - label.Prefix + "service." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: "foobar", - label.Prefix + "service." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: "bar_query", - - label.Prefix + "service." + label.SuffixFrontendRateLimitExtractorFunc: "client.ip", - label.Prefix + "service." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", - label.Prefix + "service." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", - label.Prefix + "service." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", - label.Prefix + "service." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", - label.Prefix + "service." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", - label.Prefix + "service." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", }), ports(nat.PortMap{ "80/tcp": {}, @@ -146,84 +129,11 @@ func TestDockerServiceBuildConfiguration(t *testing.T) { "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, - CustomBrowserXSSValue: "foo", - ContentSecurityPolicy: "foo", - PublicKey: "foo", - ReferrerPolicy: "foo", - IsDevelopment: true, - }, - - Errors: map[string]*types.ErrorPage{ - "foo": { - Status: []string{"404"}, - Query: "foo_query", - Backend: "foobar", - }, - "bar": { - Status: []string{"500", "600"}, - Query: "bar_query", - Backend: "foobar", - }, - }, - RateLimit: &types.RateLimit{ - ExtractorFunc: "client.ip", - RateSet: map[string]*types.Rate{ - "foo": { - Period: flaeg.Duration(6 * time.Second), - Average: 12, - Burst: 18, - }, - "bar": { - Period: flaeg.Duration(3 * time.Second), - Average: 6, - Burst: 9, - }, - }, - }, Redirect: &types.Redirect{ EntryPoint: "https", - Regex: "", - Replacement: "", - Permanent: true, + Regex: "nope", + Replacement: "nope", }, - Routes: map[string]types.Route{ "service-service": { Rule: "Host:foo.docker.localhost", @@ -344,7 +254,7 @@ func TestDockerServiceBuildConfiguration(t *testing.T) { dockerDataList = append(dockerDataList, dData) } - actualConfig := provider.buildConfiguration(dockerDataList) + actualConfig := provider.buildConfigurationV1(dockerDataList) require.NotNil(t, actualConfig, "actualConfig") assert.EqualValues(t, test.expectedBackends, actualConfig.Backends) @@ -353,7 +263,7 @@ func TestDockerServiceBuildConfiguration(t *testing.T) { } } -func TestDockerGetFuncServiceStringLabel(t *testing.T) { +func TestDockerGetFuncServiceStringLabelV1(t *testing.T) { testCases := []struct { container docker.ContainerJSON suffixLabel string @@ -391,7 +301,7 @@ func TestDockerGetFuncServiceStringLabel(t *testing.T) { dData := parseContainer(test.container) - actual := getFuncServiceStringLabel(test.suffixLabel, test.defaultValue)(dData, "myservice") + actual := getFuncServiceStringLabelV1(test.suffixLabel, test.defaultValue)(dData, "myservice") if actual != test.expected { t.Errorf("got %q, expected %q", actual, test.expected) } @@ -399,7 +309,7 @@ func TestDockerGetFuncServiceStringLabel(t *testing.T) { } } -func TestDockerGetFuncServiceSliceStringLabel(t *testing.T) { +func TestDockerGetFuncServiceSliceStringLabelV1(t *testing.T) { testCases := []struct { container docker.ContainerJSON suffixLabel string @@ -433,7 +343,7 @@ func TestDockerGetFuncServiceSliceStringLabel(t *testing.T) { dData := parseContainer(test.container) - actual := getFuncServiceSliceStringLabel(test.suffixLabel)(dData, "myservice") + actual := getFuncServiceSliceStringLabelV1(test.suffixLabel)(dData, "myservice") if !reflect.DeepEqual(actual, test.expected) { t.Errorf("for container %q: got %q, expected %q", dData.Name, actual, test.expected) @@ -442,7 +352,7 @@ func TestDockerGetFuncServiceSliceStringLabel(t *testing.T) { } } -func TestDockerGetServiceStringValue(t *testing.T) { +func TestDockerGetServiceStringValueV1(t *testing.T) { testCases := []struct { desc string container docker.ContainerJSON @@ -488,14 +398,14 @@ func TestDockerGetServiceStringValue(t *testing.T) { dData := parseContainer(test.container) - actual := getServiceStringValue(dData, test.serviceLabels, test.labelSuffix, test.defaultValue) + actual := getServiceStringValueV1(dData, test.serviceLabels, test.labelSuffix, test.defaultValue) assert.Equal(t, test.expected, actual) }) } } -func TestDockerHasStrictServiceLabel(t *testing.T) { +func TestDockerHasStrictServiceLabelV1(t *testing.T) { testCases := []struct { desc string serviceLabels map[string]string @@ -523,14 +433,14 @@ func TestDockerHasStrictServiceLabel(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := hasStrictServiceLabel(test.serviceLabels, test.labelSuffix) + actual := hasStrictServiceLabelV1(test.serviceLabels, test.labelSuffix) assert.Equal(t, test.expected, actual) }) } } -func TestDockerGetStrictServiceStringValue(t *testing.T) { +func TestDockerGetStrictServiceStringValueV1(t *testing.T) { testCases := []struct { desc string serviceLabels map[string]string @@ -569,14 +479,14 @@ func TestDockerGetStrictServiceStringValue(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := getStrictServiceStringValue(test.serviceLabels, test.labelSuffix, test.defaultValue) + actual := getStrictServiceStringValueV1(test.serviceLabels, test.labelSuffix, test.defaultValue) assert.Equal(t, test.expected, actual) }) } } -func TestDockerGetServiceMapValue(t *testing.T) { +func TestDockerGetServiceMapValueV1(t *testing.T) { testCases := []struct { desc string container docker.ContainerJSON @@ -636,14 +546,14 @@ func TestDockerGetServiceMapValue(t *testing.T) { dData := parseContainer(test.container) - actual := getServiceMapValue(dData, test.serviceLabels, test.serviceName, test.labelSuffix) + actual := getServiceMapValueV1(dData, test.serviceLabels, test.serviceName, test.labelSuffix) assert.Equal(t, test.expected, actual) }) } } -func TestDockerGetServiceSliceValue(t *testing.T) { +func TestDockerGetServiceSliceValueV1(t *testing.T) { testCases := []struct { desc string container docker.ContainerJSON @@ -694,14 +604,14 @@ func TestDockerGetServiceSliceValue(t *testing.T) { dData := parseContainer(test.container) - actual := getServiceSliceValue(dData, test.serviceLabels, test.labelSuffix) + actual := getServiceSliceValueV1(dData, test.serviceLabels, test.labelSuffix) assert.Equal(t, test.expected, actual) }) } } -func TestDockerGetServiceBoolValue(t *testing.T) { +func TestDockerGetServiceBoolValueV1(t *testing.T) { testCases := []struct { desc string container docker.ContainerJSON @@ -755,14 +665,14 @@ func TestDockerGetServiceBoolValue(t *testing.T) { dData := parseContainer(test.container) - actual := getServiceBoolValue(dData, test.serviceLabels, test.labelSuffix, test.defaultValue) + actual := getServiceBoolValueV1(dData, test.serviceLabels, test.labelSuffix, test.defaultValue) assert.Equal(t, test.expected, actual) }) } } -func TestDockerGetServiceInt64Value(t *testing.T) { +func TestDockerGetServiceInt64ValueV1(t *testing.T) { testCases := []struct { desc string container docker.ContainerJSON @@ -816,14 +726,14 @@ func TestDockerGetServiceInt64Value(t *testing.T) { dData := parseContainer(test.container) - actual := getServiceInt64Value(dData, test.serviceLabels, test.labelSuffix, test.defaultValue) + actual := getServiceInt64ValueV1(dData, test.serviceLabels, test.labelSuffix, test.defaultValue) assert.Equal(t, test.expected, actual) }) } } -func TestDockerCheckPortLabels(t *testing.T) { +func TestDockerCheckPortLabelsV1(t *testing.T) { testCases := []struct { container docker.ContainerJSON expectedError bool @@ -862,7 +772,7 @@ func TestDockerCheckPortLabels(t *testing.T) { t.Parallel() dData := parseContainer(test.container) - err := checkServiceLabelPort(dData) + err := checkServiceLabelPortV1(dData) if test.expectedError && err == nil { t.Error("expected an error but got nil") @@ -873,7 +783,7 @@ func TestDockerCheckPortLabels(t *testing.T) { } } -func TestDockerGetServiceBackendName(t *testing.T) { +func TestDockerGetServiceBackendNameV1(t *testing.T) { testCases := []struct { container docker.ContainerJSON expected string @@ -911,7 +821,7 @@ func TestDockerGetServiceBackendName(t *testing.T) { t.Run(strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() dData := parseContainer(test.container) - actual := getServiceBackendName(dData, "myservice") + actual := getServiceBackendNameV1(dData, "myservice") if actual != test.expected { t.Errorf("expected %q, got %q", test.expected, actual) } @@ -919,7 +829,7 @@ func TestDockerGetServiceBackendName(t *testing.T) { } } -func TestDockerGetServiceFrontendRule(t *testing.T) { +func TestDockerGetServiceFrontendRuleV1(t *testing.T) { provider := &Provider{} testCases := []struct { @@ -949,7 +859,7 @@ func TestDockerGetServiceFrontendRule(t *testing.T) { t.Run(strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() dData := parseContainer(test.container) - actual := provider.getServiceFrontendRule(dData, "myservice") + actual := provider.getServiceFrontendRuleV1(dData, "myservice") if actual != test.expected { t.Errorf("expected %q, got %q", test.expected, actual) } @@ -957,7 +867,7 @@ func TestDockerGetServiceFrontendRule(t *testing.T) { } } -func TestDockerGetServicePort(t *testing.T) { +func TestDockerGetServicePortV1(t *testing.T) { testCases := []struct { container docker.ContainerJSON expected string @@ -985,420 +895,10 @@ func TestDockerGetServicePort(t *testing.T) { t.Run(strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() dData := parseContainer(test.container) - actual := getServicePort(dData, "myservice") + actual := getServicePortV1(dData, "myservice") if actual != test.expected { t.Errorf("expected %q, got %q", test.expected, actual) } }) } } - -func TestDockerGetServiceRedirect(t *testing.T) { - service := "rubiks" - - testCases := []struct { - desc string - container docker.ContainerJSON - expected *types.Redirect - }{ - { - desc: "should return nil when no redirect labels", - container: containerJSON( - name("test1"), - labels(map[string]string{})), - expected: nil, - }, - { - desc: "should use only entry point tag when mix regex redirect and entry point redirect", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.Prefix + service + "." + label.SuffixFrontendRedirectEntryPoint: "https", - label.Prefix + service + "." + label.SuffixFrontendRedirectRegex: "(.*)", - label.Prefix + service + "." + label.SuffixFrontendRedirectReplacement: "$1", - }), - ), - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when entry point redirect label", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.Prefix + service + "." + label.SuffixFrontendRedirectEntryPoint: "https", - }), - ), - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when entry point redirect label (fallback to container labels)", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikFrontendRedirectEntryPoint: "https", - }), - ), - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when regex redirect labels", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.Prefix + service + "." + label.SuffixFrontendRedirectRegex: "(.*)", - label.Prefix + service + "." + label.SuffixFrontendRedirectReplacement: "$1", - }), - ), - expected: &types.Redirect{ - Regex: "(.*)", - Replacement: "$1", - }, - }, - { - desc: "should return a struct when regex redirect labels (fallback to container labels)", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikFrontendRedirectRegex: "(.*)", - label.TraefikFrontendRedirectReplacement: "$1", - }), - ), - expected: &types.Redirect{ - Regex: "(.*)", - Replacement: "$1", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - dData := parseContainer(test.container) - - actual := getServiceRedirect(dData, service) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestDockerGetServiceHeaders(t *testing.T) { - service := "rubiks" - - testCases := []struct { - desc string - container docker.ContainerJSON - expected *types.Headers - }{ - { - desc: "should return nil when no custom headers options are set", - container: containerJSON( - name("test1"), - labels(map[string]string{})), - expected: nil, - }, - { - desc: "should return a struct when all custom headers options are set", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.Prefix + service + "." + label.SuffixFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.Prefix + service + "." + label.SuffixFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.Prefix + service + "." + label.SuffixFrontendHeadersSSLProxyHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.Prefix + service + "." + label.SuffixFrontendHeadersAllowedHosts: "foo,bar,bor", - label.Prefix + service + "." + label.SuffixFrontendHeadersHostsProxyHeaders: "foo,bar,bor", - label.Prefix + service + "." + label.SuffixFrontendHeadersSSLHost: "foo", - label.Prefix + service + "." + label.SuffixFrontendHeadersCustomFrameOptionsValue: "foo", - label.Prefix + service + "." + label.SuffixFrontendHeadersContentSecurityPolicy: "foo", - label.Prefix + service + "." + label.SuffixFrontendHeadersPublicKey: "foo", - label.Prefix + service + "." + label.SuffixFrontendHeadersReferrerPolicy: "foo", - label.Prefix + service + "." + label.SuffixFrontendHeadersCustomBrowserXSSValue: "foo", - label.Prefix + service + "." + label.SuffixFrontendHeadersSTSSeconds: "666", - label.Prefix + service + "." + label.SuffixFrontendHeadersSSLRedirect: "true", - label.Prefix + service + "." + label.SuffixFrontendHeadersSSLTemporaryRedirect: "true", - label.Prefix + service + "." + label.SuffixFrontendHeadersSTSIncludeSubdomains: "true", - label.Prefix + service + "." + label.SuffixFrontendHeadersSTSPreload: "true", - label.Prefix + service + "." + label.SuffixFrontendHeadersForceSTSHeader: "true", - label.Prefix + service + "." + label.SuffixFrontendHeadersFrameDeny: "true", - label.Prefix + service + "." + label.SuffixFrontendHeadersContentTypeNosniff: "true", - label.Prefix + service + "." + label.SuffixFrontendHeadersBrowserXSSFilter: "true", - label.Prefix + service + "." + label.SuffixFrontendHeadersIsDevelopment: "true", - }), - ), - expected: &types.Headers{ - CustomRequestHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - CustomResponseHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - SSLProxyHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - AllowedHosts: []string{"foo", "bar", "bor"}, - HostsProxyHeaders: []string{"foo", "bar", "bor"}, - SSLHost: "foo", - CustomFrameOptionsValue: "foo", - ContentSecurityPolicy: "foo", - PublicKey: "foo", - ReferrerPolicy: "foo", - CustomBrowserXSSValue: "foo", - STSSeconds: 666, - SSLRedirect: true, - SSLTemporaryRedirect: true, - STSIncludeSubdomains: true, - STSPreload: true, - ForceSTSHeader: true, - FrameDeny: true, - ContentTypeNosniff: true, - BrowserXSSFilter: true, - IsDevelopment: true, - }, - }, - { - desc: "should return a struct when all custom headers options are set (fallback to container labels)", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.TraefikFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.TraefikFrontendSSLProxyHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.TraefikFrontendAllowedHosts: "foo,bar,bor", - label.TraefikFrontendHostsProxyHeaders: "foo,bar,bor", - label.TraefikFrontendSSLHost: "foo", - label.TraefikFrontendCustomFrameOptionsValue: "foo", - label.TraefikFrontendContentSecurityPolicy: "foo", - label.TraefikFrontendPublicKey: "foo", - label.TraefikFrontendReferrerPolicy: "foo", - label.TraefikFrontendCustomBrowserXSSValue: "foo", - label.TraefikFrontendSTSSeconds: "666", - label.TraefikFrontendSSLRedirect: "true", - label.TraefikFrontendSSLTemporaryRedirect: "true", - label.TraefikFrontendSTSIncludeSubdomains: "true", - label.TraefikFrontendSTSPreload: "true", - label.TraefikFrontendForceSTSHeader: "true", - label.TraefikFrontendFrameDeny: "true", - label.TraefikFrontendContentTypeNosniff: "true", - label.TraefikFrontendBrowserXSSFilter: "true", - label.TraefikFrontendIsDevelopment: "true", - }), - ), - expected: &types.Headers{ - CustomRequestHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - CustomResponseHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - SSLProxyHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - AllowedHosts: []string{"foo", "bar", "bor"}, - HostsProxyHeaders: []string{"foo", "bar", "bor"}, - SSLHost: "foo", - CustomFrameOptionsValue: "foo", - ContentSecurityPolicy: "foo", - PublicKey: "foo", - ReferrerPolicy: "foo", - CustomBrowserXSSValue: "foo", - STSSeconds: 666, - SSLRedirect: true, - SSLTemporaryRedirect: true, - STSIncludeSubdomains: true, - STSPreload: true, - ForceSTSHeader: true, - FrameDeny: true, - ContentTypeNosniff: true, - BrowserXSSFilter: true, - IsDevelopment: true, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - dData := parseContainer(test.container) - - actual := getServiceHeaders(dData, service) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestDockerGetServiceRateLimit(t *testing.T) { - service := "rubiks" - - testCases := []struct { - desc string - container docker.ContainerJSON - expected *types.RateLimit - }{ - { - desc: "should return nil when no rate limit labels", - container: containerJSON( - name("test1"), - labels(map[string]string{})), - expected: nil, - }, - { - desc: "should return a struct when rate limit labels are defined", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.Prefix + service + "." + label.SuffixFrontendRateLimitExtractorFunc: "client.ip", - label.Prefix + service + "." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", - label.Prefix + service + "." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", - label.Prefix + service + "." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", - label.Prefix + service + "." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", - label.Prefix + service + "." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", - label.Prefix + service + "." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", - })), - expected: &types.RateLimit{ - ExtractorFunc: "client.ip", - RateSet: map[string]*types.Rate{ - "foo": { - Period: flaeg.Duration(6 * time.Second), - Average: 12, - Burst: 18, - }, - "bar": { - Period: flaeg.Duration(3 * time.Second), - Average: 6, - Burst: 9, - }, - }, - }, - }, - { - desc: "should return nil when ExtractorFunc is missing", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", - })), - expected: nil, - }, - { - desc: "should return a struct when rate limit labels are defined (fallback to container labels)", - container: containerJSON( - name("test1"), - labels(map[string]string{ - label.TraefikFrontendRateLimitExtractorFunc: "client.ip", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", - })), - expected: &types.RateLimit{ - ExtractorFunc: "client.ip", - RateSet: map[string]*types.Rate{ - "foo": { - Period: flaeg.Duration(6 * time.Second), - Average: 12, - Burst: 18, - }, - "bar": { - Period: flaeg.Duration(3 * time.Second), - Average: 6, - Burst: 9, - }, - }, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - dData := parseContainer(test.container) - - actual := getServiceRateLimit(dData, service) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestDockerGetServiceErrorPages(t *testing.T) { - service := "courgette" - testCases := []struct { - desc string - data dockerData - expected map[string]*types.ErrorPage - }{ - { - desc: "2 errors pages", - data: parseContainer(containerJSON( - labels(map[string]string{ - label.Prefix + service + "." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: "404", - label.Prefix + service + "." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: "foo_backend", - label.Prefix + service + "." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: "foo_query", - label.Prefix + service + "." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: "500,600", - label.Prefix + service + "." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: "bar_backend", - label.Prefix + service + "." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: "bar_query", - }))), - expected: map[string]*types.ErrorPage{ - "foo": { - Status: []string{"404"}, - Query: "foo_query", - Backend: "foo_backend", - }, - "bar": { - Status: []string{"500", "600"}, - Query: "bar_query", - Backend: "bar_backend", - }, - }, - }, - { - desc: "only status field", - data: parseContainer(containerJSON( - labels(map[string]string{ - label.Prefix + service + ".frontend.errors.foo.status": "404", - }))), - expected: map[string]*types.ErrorPage{ - "foo": { - Status: []string{"404"}, - }, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - pages := getServiceErrorPages(test.data, service) - - assert.EqualValues(t, test.expected, pages) - }) - } -} diff --git a/provider/docker/docker.go b/provider/docker/docker.go index 588a77362..c52ce5801 100644 --- a/provider/docker/docker.go +++ b/provider/docker/docker.go @@ -55,6 +55,8 @@ type dockerData struct { NetworkSettings networkSettings Health string Node *dockertypes.ContainerNode + SegmentLabels map[string]string + SegmentName string } // NetworkSettings holds the networks data to the Provider p @@ -84,12 +86,12 @@ func (p *Provider) createClient() (client.APIClient, error) { tr := &http.Transport{ TLSClientConfig: config, } - proto, addr, _, err := client.ParseHost(p.Endpoint) + + hostURL, err := client.ParseHostURL(p.Endpoint) if err != nil { return nil, err } - - sockets.ConfigureTransport(tr, proto, addr) + sockets.ConfigureTransport(tr, hostURL.Scheme, hostURL.Host) httpClient = &http.Client{ Transport: tr, @@ -290,7 +292,7 @@ func parseContainer(container dockertypes.ContainerJSON) dockerData { if container.ContainerJSONBase != nil { dData.Name = container.ContainerJSONBase.Name - dData.ServiceName = dData.Name //Default ServiceName to be the container's Name. + dData.ServiceName = dData.Name // Default ServiceName to be the container's Name. dData.Node = container.ContainerJSONBase.Node if container.ContainerJSONBase.HostConfig != nil { diff --git a/provider/label/label.go b/provider/label/label.go index 771c50899..c0a143c34 100644 --- a/provider/label/label.go +++ b/provider/label/label.go @@ -36,13 +36,6 @@ const ( ) var ( - // ServicesPropertiesRegexp used to extract the name of the service and the name of the property for this service - // All properties are under the format traefik..frontend.*= except the port/portIndex/weight/protocol/backend directly after traefik.. - ServicesPropertiesRegexp = regexp.MustCompile(`^traefik\.(?P.+?)\.(?Pport|portIndex|weight|protocol|backend|frontend\.(.+))$`) - - // PortRegexp used to extract the port label of the service - PortRegexp = regexp.MustCompile(`^traefik\.(?P.+?)\.port$`) - // RegexpBaseFrontendErrorPage used to extract error pages from service's label RegexpBaseFrontendErrorPage = regexp.MustCompile(`^frontend\.errors\.(?P[^ .]+)\.(?P[^ .]+)$`) @@ -56,15 +49,6 @@ var ( RegexpFrontendRateLimit = regexp.MustCompile(`^traefik\.frontend\.rateLimit\.rateSet\.(?P[^ .]+)\.(?P[^ .]+)$`) ) -// ServicePropertyValues is a map of services properties -// an example value is: weight=42 -type ServicePropertyValues map[string]string - -// ServiceProperties is a map of service properties per service, -// which we can get with label[serviceName][propertyName]. -// It yields a property value. -type ServiceProperties map[string]ServicePropertyValues - // GetStringValue get string value associated to a label func GetStringValue(labels map[string]string, labelName string, defaultValue string) string { if value, ok := labels[labelName]; ok && len(value) > 0 { @@ -245,56 +229,6 @@ func HasPrefixP(labels *map[string]string, prefix string) bool { return HasPrefix(*labels, prefix) } -// FindServiceSubmatch split service label -func FindServiceSubmatch(name string) []string { - matches := ServicesPropertiesRegexp.FindStringSubmatch(name) - if matches == nil || - strings.HasPrefix(name, TraefikFrontend+".") || - strings.HasPrefix(name, TraefikBackend+".") { - return nil - } - return matches -} - -// ExtractServiceProperties Extract services labels -func ExtractServiceProperties(labels map[string]string) ServiceProperties { - v := make(ServiceProperties) - - for name, value := range labels { - matches := FindServiceSubmatch(name) - if matches == nil { - continue - } - - var serviceName string - var propertyName string - for i, name := range ServicesPropertiesRegexp.SubexpNames() { - if i != 0 { - if name == "service_name" { - serviceName = matches[i] - } else if name == "property_name" { - propertyName = matches[i] - } - } - } - - if _, ok := v[serviceName]; !ok { - v[serviceName] = make(ServicePropertyValues) - } - v[serviceName][propertyName] = value - } - - return v -} - -// ExtractServicePropertiesP Extract services labels -func ExtractServicePropertiesP(labels *map[string]string) ServiceProperties { - if labels == nil { - return make(ServiceProperties) - } - return ExtractServiceProperties(*labels) -} - // ParseErrorPages parse error pages to create ErrorPage struct func ParseErrorPages(labels map[string]string, labelPrefix string, labelRegex *regexp.Regexp) map[string]*types.ErrorPage { var errorPages map[string]*types.ErrorPage @@ -420,14 +354,3 @@ func SplitAndTrimString(base string, sep string) []string { return trimmedStrings } - -// GetServiceLabel converts a key value of Label*, given a serviceName, -// into a pattern .. -// i.e. For LabelFrontendRule and serviceName=app it will return "traefik.app.frontend.rule" -func GetServiceLabel(labelName, serviceName string) string { - if len(serviceName) > 0 { - property := strings.TrimPrefix(labelName, Prefix) - return Prefix + serviceName + "." + property - } - return labelName -} diff --git a/provider/label/label_test.go b/provider/label/label_test.go index 045304dd2..237883bce 100644 --- a/provider/label/label_test.go +++ b/provider/label/label_test.go @@ -731,11 +731,11 @@ func TestExtractServiceProperties(t *testing.T) { testCases := []struct { desc string labels map[string]string - expected ServiceProperties + expected SegmentProperties }{ { desc: "empty labels map", - expected: ServiceProperties{}, + expected: SegmentProperties{}, }, { desc: "valid label names", @@ -744,8 +744,8 @@ func TestExtractServiceProperties(t *testing.T) { "traefik.foo.frontend.bar": "1bar", "traefik.foo.backend": "3bar", }, - expected: ServiceProperties{ - "foo": ServicePropertyValues{ + expected: SegmentProperties{ + "foo": SegmentPropertyValues{ "port": "bar", "frontend.bar": "1bar", "backend": "3bar", @@ -761,7 +761,7 @@ func TestExtractServiceProperties(t *testing.T) { "traefik.foo.frontend": "0bar", "traefik.frontend.foo.backend": "0bar", }, - expected: ServiceProperties{}, + expected: SegmentProperties{}, }, } for _, test := range testCases { @@ -779,11 +779,11 @@ func TestExtractServicePropertiesP(t *testing.T) { testCases := []struct { desc string labels *map[string]string - expected ServiceProperties + expected SegmentProperties }{ { desc: "nil labels map", - expected: ServiceProperties{}, + expected: SegmentProperties{}, }, { desc: "valid label names", @@ -792,8 +792,8 @@ func TestExtractServicePropertiesP(t *testing.T) { "traefik.foo.frontend.bar": "1bar", "traefik.foo.backend": "3bar", }, - expected: ServiceProperties{ - "foo": ServicePropertyValues{ + expected: SegmentProperties{ + "foo": SegmentPropertyValues{ "port": "bar", "frontend.bar": "1bar", "backend": "3bar", @@ -809,7 +809,7 @@ func TestExtractServicePropertiesP(t *testing.T) { "traefik.foo.frontend": "0bar", "traefik.frontend.foo.backend": "0bar", }, - expected: ServiceProperties{}, + expected: SegmentProperties{}, }, } for _, test := range testCases { @@ -1137,3 +1137,91 @@ func TestParseRateSets(t *testing.T) { }) } } + +func TestExtractTraefikLabels(t *testing.T) { + testCases := []struct { + desc string + prefix string + originLabels map[string]string + expected SegmentProperties + }{ + { + desc: "nil labels map", + prefix: "traefik", + originLabels: nil, + expected: SegmentProperties{"": {}}, + }, + { + desc: "container labels", + prefix: "traefik", + originLabels: map[string]string{ + "frontend.priority": "foo", // missing prefix: skip + "traefik.port": "bar", + }, + expected: SegmentProperties{ + "": { + "traefik.port": "bar", + }, + }, + }, + { + desc: "segment labels: only segment no default", + prefix: "traefik", + originLabels: map[string]string{ + "traefik.goo.frontend.priority": "A", + "traefik.goo.port": "D", + "traefik.port": "C", + }, + expected: SegmentProperties{ + "goo": { + "traefik.frontend.priority": "A", + "traefik.port": "D", + }, + }, + }, + { + desc: "segment labels: use default", + prefix: "traefik", + originLabels: map[string]string{ + "traefik.guu.frontend.priority": "B", + "traefik.port": "C", + }, + expected: SegmentProperties{ + "guu": { + "traefik.frontend.priority": "B", + "traefik.port": "C", + }, + }, + }, + { + desc: "segment labels: several segments", + prefix: "traefik", + originLabels: map[string]string{ + "traefik.goo.frontend.priority": "A", + "traefik.goo.port": "D", + "traefik.guu.frontend.priority": "B", + "traefik.port": "C", + }, + expected: SegmentProperties{ + "goo": { + "traefik.frontend.priority": "A", + "traefik.port": "D", + }, + "guu": { + "traefik.frontend.priority": "B", + "traefik.port": "C", + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := ExtractTraefikLabels(test.originLabels) + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/provider/label/names.go b/provider/label/names.go index ac9b3faaf..ef13b4538 100644 --- a/provider/label/names.go +++ b/provider/label/names.go @@ -65,7 +65,6 @@ const ( SuffixFrontendRedirectReplacement = "frontend.redirect.replacement" SuffixFrontendRedirectPermanent = "frontend.redirect.permanent" SuffixFrontendRule = "frontend.rule" - SuffixFrontendRuleType = "frontend.rule.type" SuffixFrontendWhitelistSourceRange = "frontend.whitelistSourceRange" TraefikDomain = Prefix + SuffixDomain TraefikEnable = Prefix + SuffixEnable @@ -96,6 +95,7 @@ const ( TraefikBackendBufferingRetryExpression = Prefix + SuffixBackendBufferingRetryExpression TraefikFrontend = Prefix + SuffixFrontend TraefikFrontendAuthBasic = Prefix + SuffixFrontendAuthBasic + TraefikFrontendBackend = Prefix + SuffixFrontendBackend TraefikFrontendEntryPoints = Prefix + SuffixFrontendEntryPoints TraefikFrontendPassHostHeader = Prefix + SuffixFrontendPassHostHeader TraefikFrontendPassTLSCert = Prefix + SuffixFrontendPassTLSCert @@ -106,9 +106,7 @@ const ( TraefikFrontendRedirectReplacement = Prefix + SuffixFrontendRedirectReplacement TraefikFrontendRedirectPermanent = Prefix + SuffixFrontendRedirectPermanent TraefikFrontendRule = Prefix + SuffixFrontendRule - TraefikFrontendRuleType = Prefix + SuffixFrontendRuleType // k8s only TraefikFrontendWhitelistSourceRange = Prefix + SuffixFrontendWhitelistSourceRange - TraefikFrontendHeaders = Prefix + SuffixFrontendHeaders TraefikFrontendRequestHeaders = Prefix + SuffixFrontendRequestHeaders TraefikFrontendResponseHeaders = Prefix + SuffixFrontendResponseHeaders TraefikFrontendAllowedHosts = Prefix + SuffixFrontendHeadersAllowedHosts diff --git a/provider/label/segment.go b/provider/label/segment.go new file mode 100644 index 000000000..8274e4490 --- /dev/null +++ b/provider/label/segment.go @@ -0,0 +1,167 @@ +package label + +import ( + "regexp" + "strings" + + "github.com/containous/traefik/log" +) + +var ( + // SegmentPropertiesRegexp used to extract the name of the segment and the name of the property for this segment + // All properties are under the format traefik..frontend.*= except the port/portIndex/weight/protocol/backend directly after traefik.. + SegmentPropertiesRegexp = regexp.MustCompile(`^traefik\.(?P.+?)\.(?Pport|portIndex|weight|protocol|backend|frontend\.(.+))$`) + + // PortRegexp used to extract the port label of the segment + PortRegexp = regexp.MustCompile(`^traefik\.(?P.+?)\.port$`) +) + +// SegmentPropertyValues is a map of segment properties +// an example value is: weight=42 +type SegmentPropertyValues map[string]string + +// SegmentProperties is a map of segment properties per segment, +// which we can get with label[segmentName][propertyName]. +// It yields a property value. +type SegmentProperties map[string]SegmentPropertyValues + +// FindSegmentSubmatch split segment labels +func FindSegmentSubmatch(name string) []string { + matches := SegmentPropertiesRegexp.FindStringSubmatch(name) + if matches == nil || + strings.HasPrefix(name, TraefikFrontend+".") || + strings.HasPrefix(name, TraefikBackend+".") { + return nil + } + return matches +} + +// ExtractServicePropertiesP Extract services labels +// Deprecated +func ExtractServicePropertiesP(labels *map[string]string) SegmentProperties { + if labels == nil { + return make(SegmentProperties) + } + return ExtractServiceProperties(*labels) +} + +// ExtractServiceProperties Extract services labels +// Deprecated +func ExtractServiceProperties(labels map[string]string) SegmentProperties { + v := make(SegmentProperties) + + for name, value := range labels { + matches := FindSegmentSubmatch(name) + if matches == nil { + continue + } + + var segmentName string + var propertyName string + for i, name := range SegmentPropertiesRegexp.SubexpNames() { + // the group 0 is anonymous because it's always the root expression + if i != 0 { + if name == "segment_name" { + segmentName = matches[i] + } else if name == "property_name" { + propertyName = matches[i] + } + } + } + + if _, ok := v[segmentName]; !ok { + v[segmentName] = make(SegmentPropertyValues) + } + v[segmentName][propertyName] = value + } + + return v +} + +// GetServiceLabel converts a key value of Label*, given a serviceName, +// into a pattern .. +// i.e. For LabelFrontendRule and serviceName=app it will return "traefik.app.frontend.rule" +// Deprecated +func GetServiceLabel(labelName, serviceName string) string { + if len(serviceName) > 0 { + property := strings.TrimPrefix(labelName, Prefix) + return Prefix + serviceName + "." + property + } + return labelName +} + +// ExtractTraefikLabels transform labels to segment labels +func ExtractTraefikLabels(originLabels map[string]string) SegmentProperties { + allLabels := make(SegmentProperties) + + if _, ok := allLabels[""]; !ok { + allLabels[""] = make(SegmentPropertyValues) + } + + for name, value := range originLabels { + if !strings.HasPrefix(name, Prefix) { + continue + } + + matches := FindSegmentSubmatch(name) + if matches == nil { + // Classic labels + allLabels[""][name] = value + } else { + // segments labels + var segmentName string + var propertyName string + for i, name := range SegmentPropertiesRegexp.SubexpNames() { + // the group 0 is anonymous because it's always the root expression + if i != 0 { + if name == "segment_name" { + segmentName = matches[i] + } else if name == "property_name" { + propertyName = matches[i] + } + } + } + + if _, ok := allLabels[segmentName]; !ok { + allLabels[segmentName] = make(SegmentPropertyValues) + } + allLabels[segmentName][Prefix+propertyName] = value + } + } + log.Debug(originLabels, allLabels) + + allLabels.mergeDefault() + + return allLabels +} + +func (s SegmentProperties) mergeDefault() { + // if SegmentProperties contains the default segment, merge each segments with the default segment + if defaultLabels, okDefault := s[""]; okDefault { + + segmentsNames := s.GetSegmentNames() + if len(defaultLabels) > 0 { + for _, name := range segmentsNames { + segmentLabels := s[name] + for key, value := range defaultLabels { + if _, ok := segmentLabels[key]; !ok { + segmentLabels[key] = value + } + } + } + } + + if len(segmentsNames) > 1 { + delete(s, "") + } + } +} + +// GetSegmentNames get all segment names +func (s SegmentProperties) GetSegmentNames() []string { + var names []string + for name := range s { + names = append(names, name) + } + return names +} diff --git a/provider/provider.go b/provider/provider.go index 2ea0e2dc4..578a7c570 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -28,6 +28,7 @@ type BaseProvider struct { Filename string `description:"Override default configuration template. For advanced users :)" export:"true"` Constraints types.Constraints `description:"Filter services by constraint, matching with Traefik tags." export:"true"` Trace bool `description:"Display additional provider logs (if available)." export:"true"` + TemplateVersion int `description:"Template version." export:"true"` DebugLogGeneratedTemplate bool `description:"Enable debug logging of generated configuration template." export:"true"` } diff --git a/rules/rules.go b/rules/rules.go index 301d15ccb..3ea8fef24 100644 --- a/rules/rules.go +++ b/rules/rules.go @@ -188,7 +188,7 @@ func (r *Rules) parseRules(expression string, onRule func(functionName string, f } if len(expression) == 0 { - return errors.New("Empty rule") + return errors.New("empty rule") } f := func(c rune) bool { @@ -208,16 +208,18 @@ func (r *Rules) parseRules(expression string, onRule func(functionName string, f if len(parsedFunctions) == 0 { return fmt.Errorf("error parsing rule: '%s'", rule) } + functionName := strings.TrimSpace(parsedFunctions[0]) parsedFunction, ok := functions[functionName] if !ok { return fmt.Errorf("error parsing rule: '%s'. Unknown function: '%s'", rule, parsedFunctions[0]) } parsedFunctions = append(parsedFunctions[:0], parsedFunctions[1:]...) + + // get function fargs := func(c rune) bool { return c == ',' } - // get function parsedArgs := strings.FieldsFunc(strings.Join(parsedFunctions, ":"), fargs) if len(parsedArgs) == 0 { return fmt.Errorf("error parsing args from rule: '%s'", rule) @@ -229,7 +231,7 @@ func (r *Rules) parseRules(expression string, onRule func(functionName string, f err := onRule(functionName, parsedFunction, parsedArgs) if err != nil { - return fmt.Errorf("Parsing error on rule: %v", err) + return fmt.Errorf("parsing error on rule: %v", err) } } return nil @@ -238,6 +240,7 @@ func (r *Rules) parseRules(expression string, onRule func(functionName string, f // Parse parses rules expressions func (r *Rules) Parse(expression string) (*mux.Route, error) { var resultRoute *mux.Route + err := r.parseRules(expression, func(functionName string, function interface{}, arguments []string) error { inputs := make([]reflect.Value, len(arguments)) for i := range arguments { @@ -252,21 +255,22 @@ func (r *Rules) Parse(expression string) (*mux.Route, error) { if resultRoute.GetError() != nil { return resultRoute.GetError() } - } else { - return fmt.Errorf("Method not found: '%s'", functionName) + return fmt.Errorf("method not found: '%s'", functionName) } return nil }) if err != nil { return nil, fmt.Errorf("error parsing rule: %v", err) } + return resultRoute, nil } // ParseDomains parses rules expressions and returns domains func (r *Rules) ParseDomains(expression string) ([]string, error) { - domains := []string{} + var domains []string + err := r.parseRules(expression, func(functionName string, function interface{}, arguments []string) error { if functionName == "Host" { domains = append(domains, arguments...) @@ -276,5 +280,6 @@ func (r *Rules) ParseDomains(expression string) ([]string, error) { if err != nil { return nil, fmt.Errorf("error parsing domains: %v", err) } + return fun.Map(types.CanonicalDomain, domains).([]string), nil } diff --git a/templates/docker-v1.tmpl b/templates/docker-v1.tmpl new file mode 100644 index 000000000..7163c19f7 --- /dev/null +++ b/templates/docker-v1.tmpl @@ -0,0 +1,192 @@ +{{$backendServers := .Servers}} + +[backends] +{{range $backendName, $backend := .Backends }} + + {{if hasCircuitBreakerLabel $backend }} + [backends."backend-{{ $backendName }}".circuitbreaker] + expression = "{{ getCircuitBreakerExpression $backend }}" + {{end}} + + {{if hasLoadBalancerLabel $backend }} + [backends."backend-{{ $backendName }}".loadbalancer] + method = "{{ getLoadBalancerMethod $backend }}" + sticky = {{ getSticky $backend }} + {{if hasStickinessLabel $backend }} + [backends."backend-{{ $backendName }}".loadbalancer.stickiness] + cookieName = "{{ getStickinessCookieName $backend }}" + {{end}} + {{end}} + + {{if hasMaxConnLabels $backend }} + [backends."backend-{{ $backendName }}".maxconn] + amount = {{ getMaxConnAmount $backend }} + extractorfunc = "{{ getMaxConnExtractorFunc $backend }}" + {{end}} + + {{ $servers := index $backendServers $backendName }} + {{range $serverName, $server := $servers }} + {{if hasServices $server }} + {{$services := getServiceNames $server }} + {{range $serviceIndex, $serviceName := $services }} + [backends."backend-{{ getServiceBackend $server $serviceName }}".servers."service-{{ $serverName }}"] + url = "{{ getServiceProtocol $server $serviceName }}://{{ getIPAddress $server }}:{{ getServicePort $server $serviceName }}" + weight = {{ getServiceWeight $server $serviceName }} + {{end}} + {{else}} + [backends."backend-{{ $backendName }}".servers."server-{{$server.Name | replace "/" "" | replace "." "-"}}"] + url = "{{ getProtocol $server }}://{{ getIPAddress $server }}:{{ getPort $server }}" + weight = {{ getWeight $server }} + {{end}} + {{end}} + +{{end}} + +[frontends] +{{range $frontend, $containers := .Frontends}} + {{$container := index $containers 0}} + + {{if hasServices $container }} + {{ $services := getServiceNames $container }} + {{range $serviceIndex, $serviceName := $services }} + [frontends."frontend-{{ getServiceBackend $container $serviceName }}"] + backend = "backend-{{ getServiceBackend $container $serviceName }}" + passHostHeader = {{ getServicePassHostHeader $container $serviceName }} + passTLSCert = {{ getServicePassTLSCert $container $serviceName }} + + {{if getWhitelistSourceRange $container }} + whitelistSourceRange = [{{range getWhitelistSourceRange $container }} + "{{.}}", + {{end}}] + {{end}} + + priority = {{ getServicePriority $container $serviceName }} + + entryPoints = [{{range getServiceEntryPoints $container $serviceName }} + "{{.}}", + {{end}}] + + basicAuth = [{{range getServiceBasicAuth $container $serviceName }} + "{{.}}", + {{end}}] + + {{if hasServiceRedirect $container $serviceName }} + [frontends."frontend-{{ getServiceBackend $container $serviceName }}".redirect] + entryPoint = "{{ getServiceRedirectEntryPoint $container $serviceName }}" + regex = "{{ getServiceRedirectRegex $container $serviceName }}" + replacement = "{{ getServiceRedirectReplacement $container $serviceName }}" + {{end}} + + [frontends."frontend-{{ getServiceBackend $container $serviceName }}".routes."service-{{ $serviceName | replace "/" "" | replace "." "-" }}"] + rule = "{{ getServiceFrontendRule $container $serviceName }}" + {{end}} + {{else}} + [frontends."frontend-{{ $frontend }}"] + backend = "backend-{{ getBackend $container }}" + passHostHeader = {{ getPassHostHeader $container}} + passTLSCert = {{ getPassTLSCert $container }} + priority = {{ getPriority $container }} + + {{if getWhitelistSourceRange $container}} + whitelistSourceRange = [{{range getWhitelistSourceRange $container}} + "{{.}}", + {{end}}] + {{end}} + + entryPoints = [{{range getEntryPoints $container }} + "{{.}}", + {{end}}] + + basicAuth = [{{range getBasicAuth $container }} + "{{.}}", + {{end}}] + + {{if hasRedirect $container}} + [frontends."frontend-{{$frontend}}".redirect] + entryPoint = "{{getRedirectEntryPoint $container}}" + regex = "{{getRedirectRegex $container}}" + replacement = "{{getRedirectReplacement $container}}" + {{end}} + + {{if hasHeaders $container }} + [frontends."frontend-{{ $frontend }}".headers] + {{if hasSSLRedirectHeaders $container}} + SSLRedirect = {{getSSLRedirectHeaders $container}} + {{end}} + {{if hasSSLTemporaryRedirectHeaders $container}} + SSLTemporaryRedirect = {{getSSLTemporaryRedirectHeaders $container}} + {{end}} + {{if hasSSLHostHeaders $container}} + SSLHost = "{{getSSLHostHeaders $container}}" + {{end}} + {{if hasSTSSecondsHeaders $container}} + STSSeconds = {{getSTSSecondsHeaders $container}} + {{end}} + {{if hasSTSIncludeSubdomainsHeaders $container}} + STSIncludeSubdomains = {{getSTSIncludeSubdomainsHeaders $container}} + {{end}} + {{if hasSTSPreloadHeaders $container}} + STSPreload = {{getSTSPreloadHeaders $container}} + {{end}} + {{if hasForceSTSHeaderHeaders $container}} + ForceSTSHeader = {{getForceSTSHeaderHeaders $container}} + {{end}} + {{if hasFrameDenyHeaders $container}} + FrameDeny = {{getFrameDenyHeaders $container}} + {{end}} + {{if hasCustomFrameOptionsValueHeaders $container}} + CustomFrameOptionsValue = "{{getCustomFrameOptionsValueHeaders $container}}" + {{end}} + {{if hasContentTypeNosniffHeaders $container}} + ContentTypeNosniff = {{getContentTypeNosniffHeaders $container}} + {{end}} + {{if hasBrowserXSSFilterHeaders $container}} + BrowserXSSFilter = {{getBrowserXSSFilterHeaders $container}} + {{end}} + {{if hasContentSecurityPolicyHeaders $container}} + ContentSecurityPolicy = "{{getContentSecurityPolicyHeaders $container}}" + {{end}} + {{if hasPublicKeyHeaders $container}} + PublicKey = "{{getPublicKeyHeaders $container}}" + {{end}} + {{if hasReferrerPolicyHeaders $container}} + ReferrerPolicy = "{{getReferrerPolicyHeaders $container}}" + {{end}} + {{if hasIsDevelopmentHeaders $container}} + IsDevelopment = {{getIsDevelopmentHeaders $container}} + {{end}} + {{if hasAllowedHostsHeaders $container}} + AllowedHosts = [{{range getAllowedHostsHeaders $container}} + "{{.}}", + {{end}}] + {{end}} + {{if hasHostsProxyHeaders $container}} + HostsProxyHeaders = [{{range getHostsProxyHeaders $container}} + "{{.}}", + {{end}}] + {{end}} + {{if hasRequestHeaders $container}} + [frontends."frontend-{{$frontend}}".headers.customrequestheaders] + {{range $k, $v := getRequestHeaders $container}} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + {{if hasResponseHeaders $container}} + [frontends."frontend-{{$frontend}}".headers.customresponseheaders] + {{range $k, $v := getResponseHeaders $container}} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + {{if hasSSLProxyHeaders $container}} + [frontends."frontend-{{$frontend}}".headers.SSLProxyHeaders] + {{range $k, $v := getSSLProxyHeaders $container}} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + {{end}} + + [frontends."frontend-{{$frontend}}".routes."route-frontend-{{$frontend}}"] + rule = "{{getFrontendRule $container}}" + {{end}} + +{{end}} diff --git a/templates/docker.tmpl b/templates/docker.tmpl index 283e5b671..3592e9e94 100644 --- a/templates/docker.tmpl +++ b/templates/docker.tmpl @@ -1,14 +1,15 @@ {{$backendServers := .Servers}} [backends] -{{range $backendName, $backend := .Backends}} +{{range $backendName, $servers := .Servers}} +{{ $backend := index $servers 0 }} - {{ $circuitBreaker := getCircuitBreaker $backend }} + {{ $circuitBreaker := getCircuitBreaker $backend.SegmentLabels }} {{if $circuitBreaker }} [backends."backend-{{ $backendName }}".circuitBreaker] expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{ $loadBalancer := getLoadBalancer $backend }} + {{ $loadBalancer := getLoadBalancer $backend.SegmentLabels }} {{if $loadBalancer }} [backends."backend-{{ $backendName }}".loadBalancer] method = "{{ $loadBalancer.Method }}" @@ -19,14 +20,14 @@ {{end}} {{end}} - {{ $maxConn := getMaxConn $backend }} + {{ $maxConn := getMaxConn $backend.SegmentLabels }} {{if $maxConn }} [backends."backend-{{ $backendName }}".maxConn] extractorFunc = "{{ $maxConn.ExtractorFunc }}" amount = {{ $maxConn.Amount }} {{end}} - {{ $healthCheck := getHealthCheck $backend }} + {{ $healthCheck := getHealthCheck $backend.SegmentLabels }} {{if $healthCheck }} [backends."backend-{{ $backendName }}".healthCheck] path = "{{ $healthCheck.Path }}" @@ -34,7 +35,7 @@ interval = "{{ $healthCheck.Interval }}" {{end}} - {{ $buffering := getBuffering $backend }} + {{ $buffering := getBuffering $backend.SegmentLabels }} {{if $buffering }} [backends."backend-{{ $backendName }}".buffering] maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} @@ -44,173 +45,40 @@ retryExpression = "{{ $buffering.RetryExpression }}" {{end}} - {{ $servers := index $backendServers $backendName }} - {{range $serverName, $server := $servers }} - {{if hasServices $server }} - {{ $services := getServiceNames $server }} - {{range $serviceIndex, $serviceName := $services }} - [backends."backend-{{ getServiceBackendName $server $serviceName }}".servers."service-{{ $serverName }}"] - url = "{{ getServiceProtocol $server $serviceName }}://{{ getIPAddress $server }}:{{ getServicePort $server $serviceName }}" - weight = {{ getServiceWeight $server $serviceName }} - {{end}} - {{else}} - [backends."backend-{{ $backendName }}".servers."server-{{ $server.Name | replace "/" "" | replace "." "-" }}"] - url = "{{ getProtocol $server }}://{{ getIPAddress $server }}:{{ getPort $server }}" - weight = {{ getWeight $server }} - {{end}} + {{range $serverName, $server := getServers $servers }} + [backends."backend-{{ $backendName }}".servers."{{ $serverName }}"] + url = "{{ $server.URL }}" + weight = {{ $server.Weight }} {{end}} {{end}} [frontends] {{range $frontendName, $containers := .Frontends }} - {{$container := index $containers 0}} - - {{if hasServices $container }} - {{ $services := getServiceNames $container }} - - {{range $serviceIndex, $serviceName := $services }} - {{ $ServiceFrontendName := getServiceBackendName $container $serviceName }} - - [frontends."frontend-{{ $ServiceFrontendName }}"] - backend = "backend-{{ $ServiceFrontendName }}" - priority = {{ getServicePriority $container $serviceName }} - passHostHeader = {{ getServicePassHostHeader $container $serviceName }} - passTLSCert = {{ getServicePassTLSCert $container $serviceName }} - - entryPoints = [{{range getServiceEntryPoints $container $serviceName }} - "{{.}}", - {{end}}] - - {{ $whitelistSourceRange := getServiceWhitelistSourceRange $container $serviceName }} - {{if $whitelistSourceRange }} - whitelistSourceRange = [{{range $whitelistSourceRange }} - "{{.}}", - {{end}}] - {{end}} - - basicAuth = [{{range getServiceBasicAuth $container $serviceName }} - "{{.}}", - {{end}}] - - {{ $redirect := getServiceRedirect $container $serviceName }} - {{if $redirect }} - [frontends."frontend-{{ $ServiceFrontendName }}".redirect] - entryPoint = "{{ $redirect.EntryPoint }}" - regex = "{{ $redirect.Regex }}" - replacement = "{{ $redirect.Replacement }}" - permanent = {{ $redirect.Permanent }} - {{end}} - - {{ $errorPages := getServiceErrorPages $container $serviceName }} - {{if $errorPages }} - [frontends."frontend-{{ $ServiceFrontendName }}".errors] - {{ range $pageName, $page := $errorPages }} - [frontends."frontend-{{ $ServiceFrontendName }}".errors."{{ $pageName }}"] - status = [{{range $page.Status }} - "{{.}}", - {{end}}] - backend = "{{ $page.Backend }}" - query = "{{ $page.Query }}" - {{end}} - {{end}} - - {{ $rateLimit := getServiceRateLimit $container $serviceName }} - {{if $rateLimit }} - [frontends."frontend-{{ $ServiceFrontendName }}".rateLimit] - extractorFunc = "{{ $rateLimit.ExtractorFunc }}" - [frontends."frontend-{{ $ServiceFrontendName }}".rateLimit.rateSet] - {{range $limitName, $limit := $rateLimit.RateSet }} - [frontends."frontend-{{ $ServiceFrontendName }}".rateLimit.rateSet."{{ $limitName }}"] - period = "{{ $limit.Period }}" - average = {{ $limit.Average }} - burst = {{ $limit.Burst }} - {{end}} - {{end}} - - {{ $headers := getServiceHeaders $container $serviceName }} - {{if $headers }} - [frontends."frontend-{{ $ServiceFrontendName }}".headers] - SSLRedirect = {{ $headers.SSLRedirect }} - SSLTemporaryRedirect = {{ $headers.SSLTemporaryRedirect }} - SSLHost = "{{ $headers.SSLHost }}" - STSSeconds = {{ $headers.STSSeconds }} - STSIncludeSubdomains = {{ $headers.STSIncludeSubdomains }} - STSPreload = {{ $headers.STSPreload }} - ForceSTSHeader = {{ $headers.ForceSTSHeader }} - FrameDeny = {{ $headers.FrameDeny }} - CustomFrameOptionsValue = "{{ $headers.CustomFrameOptionsValue }}" - ContentTypeNosniff = {{ $headers.ContentTypeNosniff }} - BrowserXSSFilter = {{ $headers.BrowserXSSFilter }} - CustomBrowserXSSValue = "{{ $headers.CustomBrowserXSSValue }}" - ContentSecurityPolicy = "{{ $headers.ContentSecurityPolicy }}" - PublicKey = "{{ $headers.PublicKey }}" - ReferrerPolicy = "{{ $headers.ReferrerPolicy }}" - IsDevelopment = {{ $headers.IsDevelopment }} - - {{if $headers.AllowedHosts }} - AllowedHosts = [{{range $headers.AllowedHosts }} - "{{.}}", - {{end}}] - {{end}} - - {{if $headers.HostsProxyHeaders }} - HostsProxyHeaders = [{{range $headers.HostsProxyHeaders }} - "{{.}}", - {{end}}] - {{end}} - - {{if $headers.CustomRequestHeaders }} - [frontends."frontend-{{ $ServiceFrontendName }}".headers.customRequestHeaders] - {{range $k, $v := $headers.CustomRequestHeaders }} - {{$k}} = "{{$v}}" - {{end}} - {{end}} - - {{if $headers.CustomResponseHeaders }} - [frontends."frontend-{{ $ServiceFrontendName }}".headers.customResponseHeaders] - {{range $k, $v := $headers.CustomResponseHeaders }} - {{$k}} = "{{$v}}" - {{end}} - {{end}} - - {{if $headers.SSLProxyHeaders }} - [frontends."frontend-{{ $ServiceFrontendName }}".headers.SSLProxyHeaders] - {{range $k, $v := $headers.SSLProxyHeaders }} - {{$k}} = "{{$v}}" - {{end}} - {{end}} - {{end}} - - [frontends."frontend-{{ $ServiceFrontendName }}".routes."service-{{ $serviceName | replace "/" "" | replace "." "-" }}"] - rule = "{{ getServiceFrontendRule $container $serviceName }}" - - {{end}} ## end range services - - {{else}} + {{ $container := index $containers 0 }} [frontends."frontend-{{ $frontendName }}"] backend = "backend-{{ getBackendName $container }}" - priority = {{ getPriority $container }} - passHostHeader = {{ getPassHostHeader $container }} - passTLSCert = {{ getPassTLSCert $container }} + priority = {{ getPriority $container.SegmentLabels }} + passHostHeader = {{ getPassHostHeader $container.SegmentLabels }} + passTLSCert = {{ getPassTLSCert $container.SegmentLabels }} - entryPoints = [{{range getEntryPoints $container }} + entryPoints = [{{range getEntryPoints $container.SegmentLabels }} "{{.}}", {{end}}] - {{ $whitelistSourceRange := getWhitelistSourceRange $container}} + {{ $whitelistSourceRange := getWhitelistSourceRange $container.SegmentLabels }} {{if $whitelistSourceRange }} whitelistSourceRange = [{{range $whitelistSourceRange }} "{{.}}", {{end}}] {{end}} - basicAuth = [{{range getBasicAuth $container }} + basicAuth = [{{range getBasicAuth $container.SegmentLabels }} "{{.}}", {{end}}] - {{ $redirect := getRedirect $container }} + {{ $redirect := getRedirect $container.SegmentLabels }} {{if $redirect }} [frontends."frontend-{{ $frontendName }}".redirect] entryPoint = "{{ $redirect.EntryPoint }}" @@ -219,7 +87,7 @@ permanent = {{ $redirect.Permanent }} {{end}} - {{ $errorPages := getErrorPages $container }} + {{ $errorPages := getErrorPages $container.SegmentLabels }} {{if $errorPages }} [frontends."frontend-{{ $frontendName }}".errors] {{range $pageName, $page := $errorPages }} @@ -232,12 +100,12 @@ {{end}} {{end}} - {{ $rateLimit := getRateLimit $container }} + {{ $rateLimit := getRateLimit $container.SegmentLabels }} {{if $rateLimit }} [frontends."frontend-{{ $frontendName }}".rateLimit] extractorFunc = "{{ $rateLimit.ExtractorFunc }}" [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet] - {{range $limitName, $limit := $rateLimit.RateSet }} + {{ range $limitName, $limit := $rateLimit.RateSet }} [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet."{{ $limitName }}"] period = "{{ $limit.Period }}" average = {{ $limit.Average }} @@ -245,7 +113,7 @@ {{end}} {{end}} - {{ $headers := getHeaders $container }} + {{ $headers := getHeaders $container.SegmentLabels }} {{if $headers }} [frontends."frontend-{{ $frontendName }}".headers] SSLRedirect = {{ $headers.SSLRedirect }} @@ -259,8 +127,8 @@ CustomFrameOptionsValue = "{{ $headers.CustomFrameOptionsValue }}" ContentTypeNosniff = {{ $headers.ContentTypeNosniff }} BrowserXSSFilter = {{ $headers.BrowserXSSFilter }} - CustomBrowserXSSValue = "{{ $headers.CustomBrowserXSSValue }}" ContentSecurityPolicy = "{{ $headers.ContentSecurityPolicy }}" + CustomBrowserXSSValue = "{{ $headers.CustomBrowserXSSValue }}" PublicKey = "{{ $headers.PublicKey }}" ReferrerPolicy = "{{ $headers.ReferrerPolicy }}" IsDevelopment = {{ $headers.IsDevelopment }} @@ -297,11 +165,10 @@ {{$k}} = "{{$v}}" {{end}} {{end}} + {{end}} [frontends."frontend-{{ $frontendName }}".routes."route-frontend-{{ $frontendName }}"] rule = "{{ getFrontendRule $container }}" - {{end}} - {{end}}