From 0ea007b26f394013d18d6a3b39305743a7f73be4 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 26 Mar 2018 15:32:04 +0200 Subject: [PATCH] Segments Labels: Rancher & Marathon --- autogen/gentemplates/gen.go | 242 +++- configuration/configuration.go | 17 + docs/configuration/backends/docker.md | 6 +- docs/configuration/backends/marathon.md | 13 +- docs/configuration/backends/rancher.md | 93 +- provider/docker/config.go | 239 +--- .../docker/config_container_docker_test.go | 180 --- provider/label/label.go | 192 +-- provider/label/label_test.go | 697 ++--------- provider/label/partial.go | 301 +++++ provider/label/partial_test.go | 711 +++++++++++ provider/label/segment_test.go | 221 ++++ provider/marathon/builder_test.go | 4 + provider/marathon/config.go | 622 +++------- provider/marathon/config_root.go | 13 + provider/marathon/config_test.go | 1035 +---------------- provider/marathon/convert_types.go | 8 + provider/marathon/deprecated_config.go | 425 +++++++ provider/marathon/deprecated_config_test.go | 762 ++++++++++++ provider/marathon/fake_client_test.go | 24 + provider/marathon/marathon.go | 26 +- provider/rancher/config.go | 354 ++---- provider/rancher/config_root.go | 12 + provider/rancher/config_test.go | 804 +++---------- provider/rancher/deprecated_config.go | 226 ++++ provider/rancher/deprecated_config_test.go | 503 ++++++++ provider/rancher/rancher.go | 12 +- templates/marathon-v1.tmpl | 68 ++ templates/marathon.tmpl | 46 +- templates/rancher-v1.tmpl | 58 + templates/rancher.tmpl | 30 +- 31 files changed, 4288 insertions(+), 3656 deletions(-) create mode 100644 provider/label/partial.go create mode 100644 provider/label/partial_test.go create mode 100644 provider/label/segment_test.go create mode 100644 provider/marathon/config_root.go create mode 100644 provider/marathon/convert_types.go create mode 100644 provider/marathon/deprecated_config.go create mode 100644 provider/marathon/deprecated_config_test.go create mode 100644 provider/marathon/fake_client_test.go create mode 100644 provider/rancher/config_root.go create mode 100644 provider/rancher/deprecated_config.go create mode 100644 provider/rancher/deprecated_config_test.go create mode 100644 templates/marathon-v1.tmpl create mode 100644 templates/rancher-v1.tmpl diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index 935ed5c2f..8b5745066 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -7,9 +7,11 @@ // templates/eureka.tmpl // templates/kubernetes.tmpl // templates/kv.tmpl +// templates/marathon-v1.tmpl // templates/marathon.tmpl // templates/mesos.tmpl // templates/notFound.tmpl +// templates/rancher-v1.tmpl // templates/rancher.tmpl // DO NOT EDIT! @@ -1265,22 +1267,106 @@ func templatesKvTmpl() (*asset, error) { return a, nil } +var _templatesMarathonV1Tmpl = []byte(`{{$apps := .Applications}} + +{{range $app := $apps }} +{{range $task := $app.Tasks }} +{{range $serviceIndex, $serviceName := getServiceNames $app }} + [backends."{{ getBackend $app $serviceName }}".servers."server-{{ $task.ID | replace "." "-"}}{{getServiceNameSuffix $serviceName }}"] + url = "{{ getProtocol $app $serviceName }}://{{ getBackendServer $task $app }}:{{ getPort $task $app $serviceName }}" + weight = {{ getWeight $app $serviceName }} +{{end}} +{{end}} +{{end}} + +{{range $app := $apps }} +{{range $serviceIndex, $serviceName := getServiceNames $app }} + +[backends."{{ getBackend $app $serviceName }}"] + {{if hasMaxConnLabels $app }} + [backends."{{ getBackend $app $serviceName }}".maxConn] + amount = {{ getMaxConnAmount $app }} + extractorFunc = "{{ getMaxConnExtractorFunc $app }}" + {{end}} + + {{if hasLoadBalancerLabels $app }} + [backends."{{ getBackend $app $serviceName }}".loadBalancer] + method = "{{ getLoadBalancerMethod $app }}" + sticky = {{ getSticky $app }} + {{if hasStickinessLabel $app }} + [backends."{{ getBackend $app $serviceName }}".loadBalancer.stickiness] + cookieName = "{{ getStickinessCookieName $app }}" + {{end}} + {{end}} + + {{if hasCircuitBreakerLabels $app }} + [backends."{{ getBackend $app $serviceName }}".circuitBreaker] + expression = "{{ getCircuitBreakerExpression $app }}" + {{end}} + + {{if hasHealthCheckLabels $app }} + [backends."{{ getBackend $app $serviceName }}".healthCheck] + path = "{{ getHealthCheckPath $app }}" + interval = "{{ getHealthCheckInterval $app }}" + {{end}} + +{{end}} +{{end}} + +[frontends] +{{range $app := $apps }} +{{range $serviceIndex, $serviceName := getServiceNames . }} + + [frontends."{{ getFrontendName $app $serviceName | normalize }}"] + backend = "{{ getBackend $app $serviceName }}" + passHostHeader = {{ getPassHostHeader $app $serviceName }} + priority = {{ getPriority $app $serviceName }} + + entryPoints = [{{range getEntryPoints $app $serviceName }} + "{{.}}", + {{end}}] + + basicAuth = [{{range getBasicAuth $app $serviceName }} + "{{.}}", + {{end}}] + + [frontends."{{ getFrontendName $app $serviceName | normalize }}".routes."route-host{{ $app.ID | replace "/" "-" }}{{ getServiceNameSuffix $serviceName }}"] + rule = "{{ getFrontendRule $app $serviceName }}" + +{{end}} +{{end}} +`) + +func templatesMarathonV1TmplBytes() ([]byte, error) { + return _templatesMarathonV1Tmpl, nil +} + +func templatesMarathonV1Tmpl() (*asset, error) { + bytes, err := templatesMarathonV1TmplBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "templates/marathon-v1.tmpl", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _templatesMarathonTmpl = []byte(`{{ $apps := .Applications }} [backends] {{range $app := $apps }} -{{range $serviceIndex, $serviceName := getServiceNames $app }} - {{ $backendName := getBackend $app $serviceName}} + {{ $backendName := getBackendName $app }} [backends."{{ $backendName }}"] - {{ $circuitBreaker := getCircuitBreaker $app }} + {{ $circuitBreaker := getCircuitBreaker $app.SegmentLabels }} {{if $circuitBreaker }} [backends."{{ $backendName }}".circuitBreaker] expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{ $loadBalancer := getLoadBalancer $app }} + {{ $loadBalancer := getLoadBalancer $app.SegmentLabels }} {{if $loadBalancer }} [backends."{{ $backendName }}".loadBalancer] method = "{{ $loadBalancer.Method }}" @@ -1291,14 +1377,14 @@ var _templatesMarathonTmpl = []byte(`{{ $apps := .Applications }} {{end}} {{end}} - {{ $maxConn := getMaxConn $app }} + {{ $maxConn := getMaxConn $app.SegmentLabels }} {{if $maxConn }} [backends."{{ $backendName }}".maxConn] extractorFunc = "{{ $maxConn.ExtractorFunc }}" amount = {{ $maxConn.Amount }} {{end}} - {{ $healthCheck := getHealthCheck $app }} + {{ $healthCheck := getHealthCheck $app.SegmentLabels }} {{if $healthCheck }} [backends."{{ $backendName }}".healthCheck] path = "{{ $healthCheck.Path }}" @@ -1306,7 +1392,7 @@ var _templatesMarathonTmpl = []byte(`{{ $apps := .Applications }} interval = "{{ $healthCheck.Interval }}" {{end}} - {{ $buffering := getBuffering $app }} + {{ $buffering := getBuffering $app.SegmentLabels }} {{if $buffering }} [backends."{{ $backendName }}".buffering] maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} @@ -1316,35 +1402,33 @@ var _templatesMarathonTmpl = []byte(`{{ $apps := .Applications }} retryExpression = "{{ $buffering.RetryExpression }}" {{end}} - {{range $serverName, $server := getServers $app $serviceName }} + {{range $serverName, $server := getServers $app }} [backends."{{ $backendName }}".servers."{{ $serverName }}"] url = "{{ $server.URL }}" weight = {{ $server.Weight }} {{end}} {{end}} -{{end}} [frontends] {{range $app := $apps }} -{{range $serviceIndex, $serviceName := getServiceNames $app }} - {{ $frontendName := getFrontendName $app $serviceName }} + {{ $frontendName := getFrontendName $app }} [frontends."{{ $frontendName }}"] - backend = "{{ getBackend $app $serviceName }}" - priority = {{ getPriority $app $serviceName }} - passHostHeader = {{ getPassHostHeader $app $serviceName }} - passTLSCert = {{ getPassTLSCert $app $serviceName }} + backend = "{{ getBackendName $app }}" + priority = {{ getPriority $app.SegmentLabels }} + passHostHeader = {{ getPassHostHeader $app.SegmentLabels }} + passTLSCert = {{ getPassTLSCert $app.SegmentLabels }} - entryPoints = [{{range getEntryPoints $app $serviceName }} + entryPoints = [{{range getEntryPoints $app.SegmentLabels }} "{{.}}", {{end}}] - basicAuth = [{{range getBasicAuth $app $serviceName }} + basicAuth = [{{range getBasicAuth $app.SegmentLabels }} "{{.}}", {{end}}] - {{ $whitelist := getWhiteList $app $serviceName }} + {{ $whitelist := getWhiteList $app.SegmentLabels }} {{if $whitelist }} [frontends."{{ $frontendName }}".whiteList] sourceRange = [{{range $whitelist.SourceRange }} @@ -1353,7 +1437,7 @@ var _templatesMarathonTmpl = []byte(`{{ $apps := .Applications }} useXForwardedFor = {{ $whitelist.UseXForwardedFor }} {{end}} - {{ $redirect := getRedirect $app $serviceName }} + {{ $redirect := getRedirect $app.SegmentLabels }} {{if $redirect }} [frontends."{{ $frontendName }}".redirect] entryPoint = "{{ $redirect.EntryPoint }}" @@ -1362,7 +1446,7 @@ var _templatesMarathonTmpl = []byte(`{{ $apps := .Applications }} permanent = {{ $redirect.Permanent }} {{end}} - {{ $errorPages := getErrorPages $app $serviceName }} + {{ $errorPages := getErrorPages $app.SegmentLabels }} {{if $errorPages }} [frontends."{{ $frontendName }}".errors] {{range $pageName, $page := $errorPages }} @@ -1375,7 +1459,7 @@ var _templatesMarathonTmpl = []byte(`{{ $apps := .Applications }} {{end}} {{end}} - {{ $rateLimit := getRateLimit $app $serviceName }} + {{ $rateLimit := getRateLimit $app.SegmentLabels }} {{if $rateLimit }} [frontends."{{ $frontendName }}".rateLimit] extractorFunc = "{{ $rateLimit.ExtractorFunc }}" @@ -1388,7 +1472,7 @@ var _templatesMarathonTmpl = []byte(`{{ $apps := .Applications }} {{end}} {{end}} - {{ $headers := getHeaders $app $serviceName }} + {{ $headers := getHeaders $app.SegmentLabels }} {{if $headers }} [frontends."{{ $frontendName }}".headers] SSLRedirect = {{ $headers.SSLRedirect }} @@ -1442,10 +1526,9 @@ var _templatesMarathonTmpl = []byte(`{{ $apps := .Applications }} {{end}} {{end}} - [frontends."{{ $frontendName }}".routes."route-host{{ $app.ID | replace "/" "-" }}{{ getServiceNameSuffix $serviceName }}"] - rule = "{{ getFrontendRule $app $serviceName }}" + [frontends."{{ $frontendName }}".routes."route-host{{ $app.ID | replace "/" "-" }}{{ getSegmentNameSuffix $app.SegmentName }}"] + rule = "{{ getFrontendRule $app }}" -{{end}} {{end}} `) @@ -1682,19 +1765,94 @@ func templatesNotfoundTmpl() (*asset, error) { return a, nil } +var _templatesRancherV1Tmpl = []byte(`{{$backendServers := .Backends}} + +[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}} + + {{range $index, $ip := $backend.Containers }} + [backends."backend-{{ $backendName }}".servers."server-{{ $index }}"] + url = "{{ getProtocol $backend }}://{{ $ip }}:{{ getPort $backend }}" + weight = {{ getWeight $backend }} + {{end}} + +{{end}} + +[frontends] +{{range $frontendName, $service := .Frontends }} + [frontends."frontend-{{ $frontendName }}"] + backend = "backend-{{ getBackend $service }}" + passHostHeader = {{ getPassHostHeader $service }} + priority = {{ getPriority $service }} + + entryPoints = [{{range getEntryPoints $service }} + "{{.}}", + {{end}}] + + basicAuth = [{{range getBasicAuth $service }} + "{{.}}", + {{end}}] + + {{if hasRedirect $service }} + [frontends."frontend-{{ $frontendName }}".redirect] + entryPoint = "{{ getRedirectEntryPoint $service }}" + regex = "{{ getRedirectRegex $service }}" + replacement = "{{ getRedirectReplacement $service }}" + {{end}} + + [frontends."frontend-{{ $frontendName }}".routes."route-frontend-{{ $frontendName }}"] + rule = "{{ getFrontendRule $service }}" +{{end}} +`) + +func templatesRancherV1TmplBytes() ([]byte, error) { + return _templatesRancherV1Tmpl, nil +} + +func templatesRancherV1Tmpl() (*asset, error) { + bytes, err := templatesRancherV1TmplBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "templates/rancher-v1.tmpl", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _templatesRancherTmpl = []byte(`{{ $backendServers := .Backends }} [backends] {{range $backendName, $backend := .Backends }} [backends."backend-{{ $backendName }}"] - {{ $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 }}" @@ -1705,14 +1863,14 @@ var _templatesRancherTmpl = []byte(`{{ $backendServers := .Backends }} {{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 }}" @@ -1720,7 +1878,7 @@ var _templatesRancherTmpl = []byte(`{{ $backendServers := .Backends }} interval = "{{ $healthCheck.Interval }}" {{end}} - {{ $buffering := getBuffering $backend }} + {{ $buffering := getBuffering $backend.SegmentLabels }} {{if $buffering }} [backends."backend-{{ $backendName }}".buffering] maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} @@ -1743,19 +1901,19 @@ var _templatesRancherTmpl = []byte(`{{ $backendServers := .Backends }} [frontends."frontend-{{ $frontendName }}"] backend = "backend-{{ getBackendName $service }}" - priority = {{ getPriority $service }} - passHostHeader = {{ getPassHostHeader $service }} - passTLSCert = {{ getPassTLSCert $service }} + priority = {{ getPriority $service.SegmentLabels }} + passHostHeader = {{ getPassHostHeader $service.SegmentLabels }} + passTLSCert = {{ getPassTLSCert $service.SegmentLabels }} - entryPoints = [{{range getEntryPoints $service }} + entryPoints = [{{range getEntryPoints $service.SegmentLabels }} "{{.}}", {{end}}] - basicAuth = [{{range getBasicAuth $service }} + basicAuth = [{{range getBasicAuth $service.SegmentLabels }} "{{.}}", {{end}}] - {{ $whitelist := getWhiteList $service }} + {{ $whitelist := getWhiteList $service.SegmentLabels }} {{if $whitelist }} [frontends."frontend-{{ $frontendName }}".whiteList] sourceRange = [{{range $whitelist.SourceRange }} @@ -1764,7 +1922,7 @@ var _templatesRancherTmpl = []byte(`{{ $backendServers := .Backends }} useXForwardedFor = {{ $whitelist.UseXForwardedFor }} {{end}} - {{ $redirect := getRedirect $service }} + {{ $redirect := getRedirect $service.SegmentLabels }} {{if $redirect }} [frontends."frontend-{{ $frontendName }}".redirect] entryPoint = "{{ $redirect.EntryPoint }}" @@ -1773,7 +1931,7 @@ var _templatesRancherTmpl = []byte(`{{ $backendServers := .Backends }} permanent = {{ $redirect.Permanent }} {{end}} - {{ $errorPages := getErrorPages $service }} + {{ $errorPages := getErrorPages $service.SegmentLabels }} {{if $errorPages }} [frontends."frontend-{{ $frontendName }}".errors] {{range $pageName, $page := $errorPages }} @@ -1786,7 +1944,7 @@ var _templatesRancherTmpl = []byte(`{{ $backendServers := .Backends }} {{end}} {{end}} - {{ $rateLimit := getRateLimit $service }} + {{ $rateLimit := getRateLimit $service.SegmentLabels }} {{if $rateLimit }} [frontends."frontend-{{ $frontendName }}".rateLimit] extractorFunc = "{{ $rateLimit.ExtractorFunc }}" @@ -1799,7 +1957,7 @@ var _templatesRancherTmpl = []byte(`{{ $backendServers := .Backends }} {{end}} {{end}} - {{ $headers := getHeaders $service }} + {{ $headers := getHeaders $service.SegmentLabels }} {{if $headers }} [frontends."frontend-{{ $frontendName }}".headers] SSLRedirect = {{ $headers.SSLRedirect }} @@ -1933,9 +2091,11 @@ var _bindata = map[string]func() (*asset, error){ "templates/eureka.tmpl": templatesEurekaTmpl, "templates/kubernetes.tmpl": templatesKubernetesTmpl, "templates/kv.tmpl": templatesKvTmpl, + "templates/marathon-v1.tmpl": templatesMarathonV1Tmpl, "templates/marathon.tmpl": templatesMarathonTmpl, "templates/mesos.tmpl": templatesMesosTmpl, "templates/notFound.tmpl": templatesNotfoundTmpl, + "templates/rancher-v1.tmpl": templatesRancherV1Tmpl, "templates/rancher.tmpl": templatesRancherTmpl, } @@ -1988,9 +2148,11 @@ var _bintree = &bintree{nil, map[string]*bintree{ "eureka.tmpl": {templatesEurekaTmpl, map[string]*bintree{}}, "kubernetes.tmpl": {templatesKubernetesTmpl, map[string]*bintree{}}, "kv.tmpl": {templatesKvTmpl, map[string]*bintree{}}, + "marathon-v1.tmpl": {templatesMarathonV1Tmpl, map[string]*bintree{}}, "marathon.tmpl": {templatesMarathonTmpl, map[string]*bintree{}}, "mesos.tmpl": {templatesMesosTmpl, map[string]*bintree{}}, "notFound.tmpl": {templatesNotfoundTmpl, map[string]*bintree{}}, + "rancher-v1.tmpl": {templatesRancherV1Tmpl, map[string]*bintree{}}, "rancher.tmpl": {templatesRancherTmpl, map[string]*bintree{}}, }}, }} diff --git a/configuration/configuration.go b/configuration/configuration.go index 083085f86..7417da93c 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -214,12 +214,22 @@ func (gc *GlobalConfiguration) SetEffectiveConfiguration(configFile string) { if gc.Docker != nil { if len(gc.Docker.Filename) != 0 && gc.Docker.TemplateVersion != 2 { + log.Warn("Template version 1 is deprecated, please use version 2, see TemplateVersion.") gc.Docker.TemplateVersion = 1 } else { gc.Docker.TemplateVersion = 2 } } + if gc.Marathon != nil { + if len(gc.Marathon.Filename) != 0 && gc.Marathon.TemplateVersion != 2 { + log.Warn("Template version 1 is deprecated, please use version 2, see TemplateVersion.") + gc.Marathon.TemplateVersion = 1 + } else { + gc.Marathon.TemplateVersion = 2 + } + } + if gc.Eureka != nil { if gc.Eureka.Delay != 0 { log.Warn("Delay has been deprecated -- please use RefreshSeconds") @@ -228,6 +238,13 @@ func (gc *GlobalConfiguration) SetEffectiveConfiguration(configFile string) { } if gc.Rancher != nil { + if len(gc.Rancher.Filename) != 0 && gc.Rancher.TemplateVersion != 2 { + log.Warn("Template version 1 is deprecated, please use version 2, see TemplateVersion.") + gc.Rancher.TemplateVersion = 1 + } else { + gc.Rancher.TemplateVersion = 2 + } + // Ensure backwards compatibility for now if len(gc.Rancher.AccessKey) > 0 || len(gc.Rancher.Endpoint) > 0 || diff --git a/docs/configuration/backends/docker.md b/docs/configuration/backends/docker.md index 25f332396..221152757 100644 --- a/docs/configuration/backends/docker.md +++ b/docs/configuration/backends/docker.md @@ -161,9 +161,9 @@ exposedbydefault = false To enable constraints see [backend-specific constraints section](/configuration/commons/#backend-specific). -## Labels: overriding default behaviour +## Labels: overriding default behavior -#### Using Docker with Swarm Mode +### Using Docker with Swarm Mode If you use a compose file with the Swarm mode, labels should be defined in the `deploy` part of your service. This behavior is only enabled for docker-compose version 3+ ([Compose file reference](https://docs.docker.com/compose/compose-file/#labels-1)). @@ -177,7 +177,7 @@ services: traefik.docker.network: traefik ``` -#### Using Docker Compose +### Using Docker Compose If you are intending to use only Docker Compose commands (e.g. `docker-compose up --scale whoami=2 -d`), labels should be under your service, otherwise they will be ignored. diff --git a/docs/configuration/backends/marathon.md b/docs/configuration/backends/marathon.md index e1f9c432c..d394012b9 100644 --- a/docs/configuration/backends/marathon.md +++ b/docs/configuration/backends/marathon.md @@ -45,6 +45,15 @@ domain = "marathon.localhost" # # filename = "marathon.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 Marathon apps by default in Traefik. # # Optional @@ -241,6 +250,8 @@ Segment labels are used to define routes to an application exposing multiple por 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. +Segment labels override the default behavior. + | Label | Description | |---------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------| | `traefik..portIndex=1` | Create a service binding with frontend/backend using this port index. Overrides `traefik.portIndex`. | @@ -266,7 +277,7 @@ You can define as many segments as ports exposed in an application. | `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..frontend.whiteList.sourceRange=RANGE` | Overrides `traefik.frontend.whiteList.sourceRange`. | +| `traefik..frontend.whiteList.sourceRange=RANGE` | Overrides `traefik.frontend.whiteList.sourceRange`. | | `traefik..frontend.whiteList.useXForwardedFor=true` | Use `X-Forwarded-For` header as valid source of IP for the white list. | #### Custom Headers diff --git a/docs/configuration/backends/rancher.md b/docs/configuration/backends/rancher.md index 4743e7180..6a2aefff9 100644 --- a/docs/configuration/backends/rancher.md +++ b/docs/configuration/backends/rancher.md @@ -46,6 +46,22 @@ exposedByDefault = false # Default: false # enableServiceHealthFilter = true + +# Override default configuration template. +# For advanced users :) +# +# Optional +# +# filename = "rancher.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" ``` To enable constraints see [backend-specific constraints section](/configuration/commons/#backend-specific). @@ -116,9 +132,11 @@ secretKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" io.rancher.container.create_agent: true ``` -## Labels: overriding default behaviour +## Labels: overriding default behavior -Labels can be used on task containers to override default behaviour: +### On Containers + +Labels can be used on task containers to override default behavior: | Label | Description | |------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -162,19 +180,19 @@ Labels can be used on task containers to override default behaviour: | `traefik.frontend.whiteList.sourceRange=RANGE` | List of IP-Ranges which are allowed to access.
An unset or empty list allows all Source-IPs to access.
If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. | | `traefik.frontend.whiteList.useXForwardedFor=true` | Use `X-Forwarded-For` header as valid source of IP for the white list. | -### Custom Headers +#### 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 | -### Security Headers +#### 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.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. | @@ -192,3 +210,68 @@ Labels can be used on task containers to override default behaviour: | `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. | + +### On containers with Multiple Ports (segment labels) + +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 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.whiteList.sourceRange=RANGE` | Overrides `traefik.frontend.whiteList.sourceRange`. | +| `traefik..frontend.whiteList.useXForwardedFor=true` | Overrides `traefik.frontend.whiteList.useXForwardedFor`. | + +#### Custom Headers + +| Label | Description | +|----------------------------------------------------------------------|-----------------------------------------------------------------| +| `traefik..frontend.headers.customRequestHeaders=EXPR ` | overrides `traefik.frontend.headers.customRequestHeaders=EXPR ` | +| `traefik..frontend.headers.customResponseHeaders=EXPR` | overrides `traefik.frontend.headers.customResponseHeaders=EXPR` | + +#### Security Headers + +| Label | Description | +|-------------------------------------------------------------------------|--------------------------------------------------------------------| +| `traefik..frontend.headers.allowedHosts=EXPR` | overrides `traefik.frontend.headers.allowedHosts=EXPR` | +| `traefik..frontend.headers.hostsProxyHeaders=EXPR` | overrides `traefik.frontend.headers.hostsProxyHeaders=EXPR` | +| `traefik..frontend.headers.SSLRedirect=true` | overrides `traefik.frontend.headers.SSLRedirect=true` | +| `traefik..frontend.headers.SSLTemporaryRedirect=true` | overrides `traefik.frontend.headers.SSLTemporaryRedirect=true` | +| `traefik..frontend.headers.SSLHost=HOST` | overrides `traefik.frontend.headers.SSLHost=HOST` | +| `traefik..frontend.headers.SSLProxyHeaders=EXPR` | overrides `traefik.frontend.headers.SSLProxyHeaders=EXPR` | +| `traefik..frontend.headers.STSSeconds=315360000` | overrides `traefik.frontend.headers.STSSeconds=315360000` | +| `traefik..frontend.headers.STSIncludeSubdomains=true` | overrides `traefik.frontend.headers.STSIncludeSubdomains=true` | +| `traefik..frontend.headers.STSPreload=true` | overrides `traefik.frontend.headers.STSPreload=true` | +| `traefik..frontend.headers.forceSTSHeader=false` | overrides `traefik.frontend.headers.forceSTSHeader=false` | +| `traefik..frontend.headers.frameDeny=false` | overrides `traefik.frontend.headers.frameDeny=false` | +| `traefik..frontend.headers.customFrameOptionsValue=VALUE` | overrides `traefik.frontend.headers.customFrameOptionsValue=VALUE` | +| `traefik..frontend.headers.contentTypeNosniff=true` | overrides `traefik.frontend.headers.contentTypeNosniff=true` | +| `traefik..frontend.headers.browserXSSFilter=true` | overrides `traefik.frontend.headers.browserXSSFilter=true` | +| `traefik..frontend.headers.customBrowserXSSValue=VALUE` | overrides `traefik.frontend.headers.customBrowserXSSValue=VALUE` | +| `traefik..frontend.headers.contentSecurityPolicy=VALUE` | overrides `traefik.frontend.headers.contentSecurityPolicy=VALUE` | +| `traefik..frontend.headers.publicKey=VALUE` | overrides `traefik.frontend.headers.publicKey=VALUE` | +| `traefik..frontend.headers.referrerPolicy=VALUE` | overrides `traefik.frontend.headers.referrerPolicy=VALUE` | +| `traefik..frontend.headers.isDevelopment=false` | overrides `traefik.frontend.headers.isDevelopment=false` | diff --git a/provider/docker/config.go b/provider/docker/config.go index 9c2ea6e38..6d747bb7d 100644 --- a/provider/docker/config.go +++ b/provider/docker/config.go @@ -3,7 +3,6 @@ package docker import ( "context" "fmt" - "math" "strconv" "strings" "text/template" @@ -28,30 +27,30 @@ func (p *Provider) buildConfigurationV2(containersInspected []dockerData) *types "getLabelValue": label.GetStringValue, "getSubDomain": getSubDomain, "isBackendLBSwarm": isBackendLBSwarm, - "getDomain": getFuncStringLabel(label.TraefikDomain, p.Domain), + "getDomain": label.GetFuncString(label.TraefikDomain, p.Domain), // Backend functions "getIPAddress": p.getIPAddress, "getServers": p.getServers, - "getMaxConn": getMaxConn, - "getHealthCheck": getHealthCheck, - "getBuffering": getBuffering, - "getCircuitBreaker": getCircuitBreaker, - "getLoadBalancer": getLoadBalancer, + "getMaxConn": label.GetMaxConn, + "getHealthCheck": label.GetHealthCheck, + "getBuffering": label.GetBuffering, + "getCircuitBreaker": label.GetCircuitBreaker, + "getLoadBalancer": label.GetLoadBalancer, // Frontend functions "getBackendName": getBackendName, - "getPriority": getFuncIntLabel(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), - "getPassHostHeader": getFuncBoolLabel(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), - "getPassTLSCert": getFuncBoolLabel(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), - "getEntryPoints": getFuncSliceStringLabel(label.TraefikFrontendEntryPoints), - "getBasicAuth": getFuncSliceStringLabel(label.TraefikFrontendAuthBasic), + "getPriority": label.GetFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), + "getPassHostHeader": label.GetFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getPassTLSCert": label.GetFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), + "getEntryPoints": label.GetFuncSliceString(label.TraefikFrontendEntryPoints), + "getBasicAuth": label.GetFuncSliceString(label.TraefikFrontendAuthBasic), "getFrontendRule": p.getFrontendRule, - "getRedirect": getRedirect, - "getErrorPages": getErrorPages, - "getRateLimit": getRateLimit, - "getHeaders": getHeaders, - "getWhiteList": getWhiteList, + "getRedirect": label.GetRedirect, + "getErrorPages": label.GetErrorPages, + "getRateLimit": label.GetRateLimit, + "getHeaders": label.GetHeaders, + "getWhiteList": label.GetWhiteList, } // filter containers @@ -276,105 +275,6 @@ func getBackendName(container dockerData) string { return getDefaultBackendName(container) } -func getWhiteList(labels map[string]string) *types.WhiteList { - if label.Has(labels, label.TraefikFrontendWhitelistSourceRange) { - log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikFrontendWhitelistSourceRange, label.TraefikFrontendWhiteListSourceRange) - } - - ranges := label.GetSliceStringValue(labels, label.TraefikFrontendWhiteListSourceRange) - if len(ranges) > 0 { - return &types.WhiteList{ - SourceRange: ranges, - UseXForwardedFor: label.GetBoolValue(labels, label.TraefikFrontendWhiteListUseXForwardedFor, false), - } - } - - // TODO: Deprecated - values := label.GetSliceStringValue(labels, label.TraefikFrontendWhitelistSourceRange) - if len(values) > 0 { - return &types.WhiteList{ - SourceRange: values, - UseXForwardedFor: false, - } - } - - return nil -} - -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 @@ -399,89 +299,6 @@ func getPort(container dockerData) string { 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 @@ -507,27 +324,3 @@ func (p *Provider) getServers(containers []dockerData) map[string]types.Server { 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_docker_test.go b/provider/docker/config_container_docker_test.go index c088f653b..7c78a84d6 100644 --- a/provider/docker/config_container_docker_test.go +++ b/provider/docker/config_container_docker_test.go @@ -645,104 +645,6 @@ func TestDockerTraefikFilter(t *testing.T) { } } -func TestDockerGetFuncStringLabel(t *testing.T) { - testCases := []struct { - labels map[string]string - labelName string - defaultValue string - expected string - }{ - { - labels: nil, - labelName: label.TraefikWeight, - defaultValue: label.DefaultWeight, - expected: "0", - }, - { - 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() - - actual := getFuncStringLabel(test.labelName, test.defaultValue)(test.labels) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestDockerGetSliceStringLabel(t *testing.T) { - testCases := []struct { - desc string - labels map[string]string - labelName string - expected []string - }{ - { - desc: "no whitelist-label", - labels: nil, - expected: nil, - }, - { - desc: "whitelist-label with empty string", - labels: map[string]string{ - label.TraefikFrontendWhiteListSourceRange: "", - }, - labelName: label.TraefikFrontendWhiteListSourceRange, - expected: nil, - }, - { - desc: "whitelist-label with IPv4 mask", - 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", - labels: map[string]string{ - label.TraefikFrontendWhiteListSourceRange: "fe80::/16", - }, - labelName: label.TraefikFrontendWhiteListSourceRange, - expected: []string{ - "fe80::/16", - }, - }, - { - desc: "whitelist-label with multiple masks", - 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() - - actual := getFuncSliceStringLabel(test.labelName)(test.labels) - assert.EqualValues(t, test.expected, actual) - }) - } -} - func TestDockerGetFrontendName(t *testing.T) { testCases := []struct { container docker.ContainerJSON @@ -1027,85 +929,3 @@ func TestDockerGetPort(t *testing.T) { }) } } - -func TestWhiteList(t *testing.T) { - testCases := []struct { - desc string - labels map[string]string - expected *types.WhiteList - }{ - { - desc: "should return nil when no white list labels", - labels: map[string]string{}, - expected: nil, - }, - { - desc: "should return a struct when deprecated label", - labels: map[string]string{ - label.TraefikFrontendWhitelistSourceRange: "10.10.10.10", - }, - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: false, - }, - }, - { - desc: "should return a struct when only range", - labels: map[string]string{ - label.TraefikFrontendWhiteListSourceRange: "10.10.10.10", - }, - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: false, - }, - }, - { - desc: "should return a struct when range and UseXForwardedFor", - labels: map[string]string{ - label.TraefikFrontendWhiteListSourceRange: "10.10.10.10", - label.TraefikFrontendWhiteListUseXForwardedFor: "true", - }, - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: true, - }, - }, - { - desc: "should return a struct when mix deprecated label and new labels", - labels: map[string]string{ - label.TraefikFrontendWhitelistSourceRange: "20.20.20.20", - label.TraefikFrontendWhiteListSourceRange: "10.10.10.10", - label.TraefikFrontendWhiteListUseXForwardedFor: "true", - }, - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: true, - }, - }, - { - desc: "should return nil when only UseXForwardedFor", - labels: map[string]string{ - label.TraefikFrontendWhiteListUseXForwardedFor: "true", - }, - expected: nil, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getWhiteList(test.labels) - assert.Equal(t, test.expected, actual) - }) - } -} diff --git a/provider/label/label.go b/provider/label/label.go index c0a143c34..ff1fe368e 100644 --- a/provider/label/label.go +++ b/provider/label/label.go @@ -7,9 +7,7 @@ import ( "strconv" "strings" - "github.com/containous/flaeg" "github.com/containous/traefik/log" - "github.com/containous/traefik/types" ) const ( @@ -28,7 +26,6 @@ const ( DefaultFrontendPriority = "0" // TODO [breaking] int value DefaultFrontendPriorityInt = 0 // TODO rename to DefaultFrontendPriority DefaultCircuitBreakerExpression = "NetworkErrorRatio() > 1" - DefaultFrontendRedirectEntryPoint = "" DefaultBackendLoadBalancerMethod = "wrr" DefaultBackendMaxconnExtractorFunc = "request.host" DefaultBackendLoadbalancerStickinessCookieName = "" @@ -57,14 +54,6 @@ func GetStringValue(labels map[string]string, labelName string, defaultValue str return defaultValue } -// GetStringValueP get string value associated to a label -func GetStringValueP(labels *map[string]string, labelName string, defaultValue string) string { - if labels == nil { - return defaultValue - } - return GetStringValue(*labels, labelName, defaultValue) -} - // GetBoolValue get bool value associated to a label func GetBoolValue(labels map[string]string, labelName string, defaultValue bool) bool { rawValue, ok := labels[labelName] @@ -77,14 +66,6 @@ func GetBoolValue(labels map[string]string, labelName string, defaultValue bool) return defaultValue } -// GetBoolValueP get bool value associated to a label -func GetBoolValueP(labels *map[string]string, labelName string, defaultValue bool) bool { - if labels == nil { - return defaultValue - } - return GetBoolValue(*labels, labelName, defaultValue) -} - // GetIntValue get int value associated to a label func GetIntValue(labels map[string]string, labelName string, defaultValue int) int { if rawValue, ok := labels[labelName]; ok { @@ -97,14 +78,6 @@ func GetIntValue(labels map[string]string, labelName string, defaultValue int) i return defaultValue } -// GetIntValueP get int value associated to a label -func GetIntValueP(labels *map[string]string, labelName string, defaultValue int) int { - if labels == nil { - return defaultValue - } - return GetIntValue(*labels, labelName, defaultValue) -} - // GetInt64Value get int64 value associated to a label func GetInt64Value(labels map[string]string, labelName string, defaultValue int64) int64 { if rawValue, ok := labels[labelName]; ok { @@ -117,14 +90,6 @@ func GetInt64Value(labels map[string]string, labelName string, defaultValue int6 return defaultValue } -// GetInt64ValueP get int64 value associated to a label -func GetInt64ValueP(labels *map[string]string, labelName string, defaultValue int64) int64 { - if labels == nil { - return defaultValue - } - return GetInt64Value(*labels, labelName, defaultValue) -} - // GetSliceStringValue get a slice of string associated to a label func GetSliceStringValue(labels map[string]string, labelName string) []string { var value []string @@ -139,14 +104,6 @@ func GetSliceStringValue(labels map[string]string, labelName string) []string { return value } -// GetSliceStringValueP get a slice of string value associated to a label -func GetSliceStringValueP(labels *map[string]string, labelName string) []string { - if labels == nil { - return nil - } - return GetSliceStringValue(*labels, labelName) -} - // ParseMapValue get Map value for a label value func ParseMapValue(labelName, values string) map[string]string { mapValue := make(map[string]string) @@ -203,14 +160,6 @@ func Has(labels map[string]string, labelName string) bool { return ok && len(value) > 0 } -// HasP Check if a value is associated to a label -func HasP(labels *map[string]string, labelName string) bool { - if labels == nil { - return false - } - return Has(*labels, labelName) -} - // HasPrefix Check if a value is associated to a less one label with a prefix func HasPrefix(labels map[string]string, prefix string) bool { for name, value := range labels { @@ -221,124 +170,11 @@ func HasPrefix(labels map[string]string, prefix string) bool { return false } -// HasPrefixP Check if a value is associated to a less one label with a prefix -func HasPrefixP(labels *map[string]string, prefix string) bool { - if labels == nil { - return false - } - return HasPrefix(*labels, prefix) -} - -// 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 - - for lblName, value := range labels { - if strings.HasPrefix(lblName, labelPrefix) { - submatch := labelRegex.FindStringSubmatch(lblName) - if len(submatch) != 3 { - log.Errorf("Invalid page error label: %s, sub-match: %v", lblName, submatch) - continue - } - - if errorPages == nil { - errorPages = make(map[string]*types.ErrorPage) - } - - pageName := submatch[1] - - ep, ok := errorPages[pageName] - if !ok { - ep = &types.ErrorPage{} - errorPages[pageName] = ep - } - - switch submatch[2] { - case SuffixErrorPageStatus: - ep.Status = SplitAndTrimString(value, ",") - case SuffixErrorPageQuery: - ep.Query = value - case SuffixErrorPageBackend: - ep.Backend = value - default: - log.Errorf("Invalid page error label: %s", lblName) - continue - } - } - } - - return errorPages -} - -// ParseRateSets parse rate limits to create Rate struct -func ParseRateSets(labels map[string]string, labelPrefix string, labelRegex *regexp.Regexp) map[string]*types.Rate { - var rateSets map[string]*types.Rate - - for lblName, rawValue := range labels { - if strings.HasPrefix(lblName, labelPrefix) && len(rawValue) > 0 { - submatch := labelRegex.FindStringSubmatch(lblName) - if len(submatch) != 3 { - log.Errorf("Invalid rate limit label: %s, sub-match: %v", lblName, submatch) - continue - } - - if rateSets == nil { - rateSets = make(map[string]*types.Rate) - } - - limitName := submatch[1] - - ep, ok := rateSets[limitName] - if !ok { - ep = &types.Rate{} - rateSets[limitName] = ep - } - - switch submatch[2] { - case "period": - var d flaeg.Duration - err := d.Set(rawValue) - if err != nil { - log.Errorf("Unable to parse %q: %q. %v", lblName, rawValue, err) - continue - } - ep.Period = d - case "average": - value, err := strconv.ParseInt(rawValue, 10, 64) - if err != nil { - log.Errorf("Unable to parse %q: %q. %v", lblName, rawValue, err) - continue - } - ep.Average = value - case "burst": - value, err := strconv.ParseInt(rawValue, 10, 64) - if err != nil { - log.Errorf("Unable to parse %q: %q. %v", lblName, rawValue, err) - continue - } - ep.Burst = value - default: - log.Errorf("Invalid rate limit label: %s", lblName) - continue - } - } - } - return rateSets -} - // IsEnabled Check if a container is enabled in Træfik func IsEnabled(labels map[string]string, exposedByDefault bool) bool { return GetBoolValue(labels, TraefikEnable, exposedByDefault) } -// IsEnabledP Check if a container is enabled in Træfik -func IsEnabledP(labels *map[string]string, exposedByDefault bool) bool { - if labels == nil { - return exposedByDefault - } - return IsEnabled(*labels, exposedByDefault) -} - // SplitAndTrimString splits separatedString at the separator character and trims each // piece, filtering out empty pieces. Returns the list of pieces or nil if the input // did not contain a non-empty piece. @@ -354,3 +190,31 @@ func SplitAndTrimString(base string, sep string) []string { return trimmedStrings } + +// GetFuncString a func related to GetStringValue +func GetFuncString(labelName string, defaultValue string) func(map[string]string) string { + return func(labels map[string]string) string { + return GetStringValue(labels, labelName, defaultValue) + } +} + +// GetFuncInt a func related to GetIntValue +func GetFuncInt(labelName string, defaultValue int) func(map[string]string) int { + return func(labels map[string]string) int { + return GetIntValue(labels, labelName, defaultValue) + } +} + +// GetFuncBool a func related to GetBoolValue +func GetFuncBool(labelName string, defaultValue bool) func(map[string]string) bool { + return func(labels map[string]string) bool { + return GetBoolValue(labels, labelName, defaultValue) + } +} + +// GetFuncSliceString a func related to GetSliceStringValue +func GetFuncSliceString(labelName string) func(map[string]string) []string { + return func(labels map[string]string) []string { + return GetSliceStringValue(labels, labelName) + } +} diff --git a/provider/label/label_test.go b/provider/label/label_test.go index 237883bce..6c6670f53 100644 --- a/provider/label/label_test.go +++ b/provider/label/label_test.go @@ -1,11 +1,9 @@ package label import ( + "strconv" "testing" - "time" - "github.com/containous/flaeg" - "github.com/containous/traefik/types" "github.com/stretchr/testify/assert" ) @@ -108,51 +106,6 @@ func TestGetStringValue(t *testing.T) { } } -func TestGetStringValueP(t *testing.T) { - testCases := []struct { - desc string - labels *map[string]string - labelName string - defaultValue string - expected string - }{ - { - desc: "nil labels map", - labels: nil, - labelName: "foo", - defaultValue: "default", - expected: "default", - }, - { - desc: "existing label", - labels: &map[string]string{ - "foo": "bar", - }, - labelName: "foo", - defaultValue: "default", - expected: "bar", - }, - { - desc: "non existing label", - labels: &map[string]string{ - "foo": "bar", - }, - labelName: "fii", - defaultValue: "default", - expected: "default", - }, - } - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - got := GetStringValueP(test.labels, test.labelName, test.defaultValue) - assert.Equal(t, test.expected, got) - }) - } -} - func TestGetBoolValue(t *testing.T) { testCases := []struct { desc string @@ -255,60 +208,6 @@ func TestGetIntValue(t *testing.T) { } } -func TestGetIntValueP(t *testing.T) { - testCases := []struct { - desc string - labels *map[string]string - labelName string - defaultValue int - expected int - }{ - { - desc: "nil map", - labels: nil, - labelName: "foo", - defaultValue: 666, - expected: 666, - }, - { - desc: "invalid int value", - labelName: "foo", - labels: &map[string]string{ - "foo": "bar", - }, - defaultValue: 666, - expected: 666, - }, - { - desc: "negative int value", - labelName: "foo", - labels: &map[string]string{ - "foo": "-1", - }, - defaultValue: 666, - expected: -1, - }, - { - desc: "positive int value", - labelName: "foo", - labels: &map[string]string{ - "foo": "1", - }, - defaultValue: 666, - expected: 1, - }, - } - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - got := GetIntValueP(test.labels, test.labelName, test.defaultValue) - assert.Equal(t, test.expected, got) - }) - } -} - func TestGetInt64Value(t *testing.T) { testCases := []struct { desc string @@ -360,60 +259,6 @@ func TestGetInt64Value(t *testing.T) { } } -func TestGetInt64ValueP(t *testing.T) { - testCases := []struct { - desc string - labels *map[string]string - labelName string - defaultValue int64 - expected int64 - }{ - { - desc: "nil map", - labels: nil, - labelName: "foo", - defaultValue: 666, - expected: 666, - }, - { - desc: "invalid int value", - labelName: "foo", - labels: &map[string]string{ - "foo": "bar", - }, - defaultValue: 666, - expected: 666, - }, - { - desc: "negative int value", - labelName: "foo", - labels: &map[string]string{ - "foo": "-1", - }, - defaultValue: 666, - expected: -1, - }, - { - desc: "positive int value", - labelName: "foo", - labels: &map[string]string{ - "foo": "1", - }, - defaultValue: 666, - expected: 1, - }, - } - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - got := GetInt64ValueP(test.labels, test.labelName, test.defaultValue) - assert.Equal(t, test.expected, got) - }) - } -} - func TestGetSliceStringValue(t *testing.T) { testCases := []struct { desc string @@ -461,47 +306,6 @@ func TestGetSliceStringValue(t *testing.T) { } } -func TestGetSliceStringValueP(t *testing.T) { - testCases := []struct { - desc string - labels *map[string]string - labelName string - expected []string - }{ - { - desc: "nil map", - labels: nil, - labelName: "foo", - expected: nil, - }, - { - desc: "one value, not split", - labels: &map[string]string{ - "foo": "bar", - }, - labelName: "foo", - expected: []string{"bar"}, - }, - { - desc: "several values", - labels: &map[string]string{ - "foo": "bar,bir ,bur", - }, - labelName: "foo", - expected: []string{"bar", "bir", "bur"}, - }, - } - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - got := GetSliceStringValueP(test.labels, test.labelName) - assert.EqualValues(t, test.expected, got) - }) - } -} - func TestGetMapValue(t *testing.T) { testCases := []struct { desc string @@ -680,149 +484,6 @@ func TestHas(t *testing.T) { } } -func TestHasP(t *testing.T) { - testCases := []struct { - desc string - labels *map[string]string - labelName string - expected bool - }{ - { - desc: "nil labels map", - labelName: "foo", - }, - { - desc: "nonexistent label", - labels: &map[string]string{ - "foo": "bar", - }, - labelName: "fii", - expected: false, - }, - { - desc: "existent label", - labels: &map[string]string{ - "foo": "bar", - }, - labelName: "foo", - expected: true, - }, - { - desc: "existent label with empty value", - labels: &map[string]string{ - "foo": "", - }, - labelName: "foo", - expected: false, - }, - } - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - got := HasP(test.labels, test.labelName) - assert.Equal(t, test.expected, got) - }) - } -} - -func TestExtractServiceProperties(t *testing.T) { - testCases := []struct { - desc string - labels map[string]string - expected SegmentProperties - }{ - { - desc: "empty labels map", - expected: SegmentProperties{}, - }, - { - desc: "valid label names", - labels: map[string]string{ - "traefik.foo.port": "bar", - "traefik.foo.frontend.bar": "1bar", - "traefik.foo.backend": "3bar", - }, - expected: SegmentProperties{ - "foo": SegmentPropertyValues{ - "port": "bar", - "frontend.bar": "1bar", - "backend": "3bar", - }, - }, - }, - { - desc: "invalid label names", - labels: map[string]string{ - "foo.frontend.bar": "1bar", - "traefik.foo.frontend.": "2bar", - "traefik.foo.port.bar": "barbar", - "traefik.foo.frontend": "0bar", - "traefik.frontend.foo.backend": "0bar", - }, - expected: SegmentProperties{}, - }, - } - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - got := ExtractServiceProperties(test.labels) - assert.EqualValues(t, test.expected, got) - }) - } -} - -func TestExtractServicePropertiesP(t *testing.T) { - testCases := []struct { - desc string - labels *map[string]string - expected SegmentProperties - }{ - { - desc: "nil labels map", - expected: SegmentProperties{}, - }, - { - desc: "valid label names", - labels: &map[string]string{ - "traefik.foo.port": "bar", - "traefik.foo.frontend.bar": "1bar", - "traefik.foo.backend": "3bar", - }, - expected: SegmentProperties{ - "foo": SegmentPropertyValues{ - "port": "bar", - "frontend.bar": "1bar", - "backend": "3bar", - }, - }, - }, - { - desc: "invalid label names", - labels: &map[string]string{ - "foo.frontend.bar": "1bar", - "traefik.foo.frontend.": "2bar", - "traefik.foo.port.bar": "barbar", - "traefik.foo.frontend": "0bar", - "traefik.frontend.foo.backend": "0bar", - }, - expected: SegmentProperties{}, - }, - } - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - got := ExtractServicePropertiesP(test.labels) - assert.EqualValues(t, test.expected, got) - }) - } -} - func TestIsEnabled(t *testing.T) { testCases := []struct { desc string @@ -884,97 +545,6 @@ func TestIsEnabled(t *testing.T) { } } -func TestIsEnabledP(t *testing.T) { - testCases := []struct { - desc string - labels *map[string]string - exposedByDefault bool - expected bool - }{ - { - desc: "nil labels map & exposedByDefault true", - exposedByDefault: true, - expected: true, - }, - { - desc: "nil labels map & exposedByDefault false", - exposedByDefault: false, - expected: false, - }, - { - desc: "exposedByDefault false and label enable true", - labels: &map[string]string{ - TraefikEnable: "true", - }, - exposedByDefault: false, - expected: true, - }, - { - desc: "exposedByDefault false and label enable false", - labels: &map[string]string{ - TraefikEnable: "false", - }, - exposedByDefault: false, - expected: false, - }, - { - desc: "exposedByDefault true and label enable false", - labels: &map[string]string{ - TraefikEnable: "false", - }, - exposedByDefault: true, - expected: false, - }, - { - desc: "exposedByDefault true and label enable true", - labels: &map[string]string{ - TraefikEnable: "true", - }, - exposedByDefault: true, - expected: true, - }, - } - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - got := IsEnabledP(test.labels, test.exposedByDefault) - assert.Equal(t, test.expected, got) - }) - } -} - -func TestGetServiceLabel(t *testing.T) { - testCases := []struct { - desc string - labelName string - serviceName string - expected string - }{ - { - desc: "without service name", - labelName: TraefikPort, - expected: TraefikPort, - }, - { - desc: "with service name", - labelName: TraefikPort, - serviceName: "bar", - expected: "traefik.bar.port", - }, - } - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - got := GetServiceLabel(test.labelName, test.serviceName) - assert.Equal(t, test.expected, got) - }) - } -} - func TestHasPrefix(t *testing.T) { testCases := []struct { desc string @@ -1023,205 +593,100 @@ func TestHasPrefix(t *testing.T) { } } -func TestParseErrorPages(t *testing.T) { +func TestGetFuncString(t *testing.T) { testCases := []struct { - desc string - labels map[string]string - expected map[string]*types.ErrorPage + labels map[string]string + labelName string + defaultValue string + expected string }{ { - desc: "2 errors pages", - labels: map[string]string{ - Prefix + BaseFrontendErrorPage + "foo." + SuffixErrorPageStatus: "404", - Prefix + BaseFrontendErrorPage + "foo." + SuffixErrorPageBackend: "foo_backend", - Prefix + BaseFrontendErrorPage + "foo." + SuffixErrorPageQuery: "foo_query", - Prefix + BaseFrontendErrorPage + "bar." + SuffixErrorPageStatus: "500,600", - Prefix + BaseFrontendErrorPage + "bar." + SuffixErrorPageBackend: "bar_backend", - Prefix + BaseFrontendErrorPage + "bar." + 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", - }, - }, + labels: nil, + labelName: TraefikWeight, + defaultValue: DefaultWeight, + expected: "0", }, { - desc: "only status field", labels: map[string]string{ - Prefix + BaseFrontendErrorPage + "foo." + SuffixErrorPageStatus: "404", + TraefikWeight: "10", }, - expected: map[string]*types.ErrorPage{ - "foo": { - Status: []string{"404"}, - }, - }, - }, - { - desc: "invalid field", - labels: map[string]string{ - Prefix + BaseFrontendErrorPage + "foo." + "courgette": "404", - }, - expected: map[string]*types.ErrorPage{"foo": {}}, - }, - { - desc: "no error pages labels", - labels: map[string]string{}, - expected: nil, + labelName: TraefikWeight, + defaultValue: DefaultWeight, + expected: "10", }, } - for _, test := range testCases { + for containerID, test := range testCases { test := test - t.Run(test.desc, func(t *testing.T) { + t.Run(test.labelName+strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() - pages := ParseErrorPages(test.labels, Prefix+BaseFrontendErrorPage, RegexpFrontendErrorPage) - - assert.EqualValues(t, test.expected, pages) - }) - } -} - -func TestParseRateSets(t *testing.T) { - testCases := []struct { - desc string - labels map[string]string - expected map[string]*types.Rate - }{ - { - desc: "2 rate limits", - labels: map[string]string{ - Prefix + BaseFrontendRateLimit + "foo." + SuffixRateLimitPeriod: "6", - Prefix + BaseFrontendRateLimit + "foo." + SuffixRateLimitAverage: "12", - Prefix + BaseFrontendRateLimit + "foo." + SuffixRateLimitBurst: "18", - Prefix + BaseFrontendRateLimit + "bar." + SuffixRateLimitPeriod: "3", - Prefix + BaseFrontendRateLimit + "bar." + SuffixRateLimitAverage: "6", - Prefix + BaseFrontendRateLimit + "bar." + SuffixRateLimitBurst: "9", - }, - expected: 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: "no rate limits labels", - labels: map[string]string{}, - expected: nil, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - rateSets := ParseRateSets(test.labels, Prefix+BaseFrontendRateLimit, RegexpFrontendRateLimit) - - assert.EqualValues(t, test.expected, rateSets) - }) - } -} - -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) + actual := GetFuncString(test.labelName, test.defaultValue)(test.labels) assert.Equal(t, test.expected, actual) }) } } + +func TestGetSliceString(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + labelName string + expected []string + }{ + { + desc: "no whitelist-label", + labels: nil, + expected: nil, + }, + { + desc: "whitelist-label with empty string", + labels: map[string]string{ + TraefikFrontendWhiteListSourceRange: "", + }, + labelName: TraefikFrontendWhiteListSourceRange, + expected: nil, + }, + { + desc: "whitelist-label with IPv4 mask", + labels: map[string]string{ + TraefikFrontendWhiteListSourceRange: "1.2.3.4/16", + }, + labelName: TraefikFrontendWhiteListSourceRange, + expected: []string{ + "1.2.3.4/16", + }, + }, + { + desc: "whitelist-label with IPv6 mask", + labels: map[string]string{ + TraefikFrontendWhiteListSourceRange: "fe80::/16", + }, + labelName: TraefikFrontendWhiteListSourceRange, + expected: []string{ + "fe80::/16", + }, + }, + { + desc: "whitelist-label with multiple masks", + labels: map[string]string{ + TraefikFrontendWhiteListSourceRange: "1.1.1.1/24, 1234:abcd::42/32", + }, + labelName: 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() + + actual := GetFuncSliceString(test.labelName)(test.labels) + assert.EqualValues(t, test.expected, actual) + }) + } +} diff --git a/provider/label/partial.go b/provider/label/partial.go new file mode 100644 index 000000000..5ccd9ba3c --- /dev/null +++ b/provider/label/partial.go @@ -0,0 +1,301 @@ +package label + +import ( + "math" + "regexp" + "strconv" + "strings" + + "github.com/containous/flaeg" + "github.com/containous/traefik/log" + "github.com/containous/traefik/types" +) + +// GetWhiteList Create white list from labels +func GetWhiteList(labels map[string]string) *types.WhiteList { + if Has(labels, TraefikFrontendWhitelistSourceRange) { + log.Warnf("Deprecated configuration found: %s. Please use %s.", TraefikFrontendWhitelistSourceRange, TraefikFrontendWhiteListSourceRange) + } + + ranges := GetSliceStringValue(labels, TraefikFrontendWhiteListSourceRange) + if len(ranges) > 0 { + return &types.WhiteList{ + SourceRange: ranges, + UseXForwardedFor: GetBoolValue(labels, TraefikFrontendWhiteListUseXForwardedFor, false), + } + } + + // TODO: Deprecated + values := GetSliceStringValue(labels, TraefikFrontendWhitelistSourceRange) + if len(values) > 0 { + return &types.WhiteList{ + SourceRange: values, + UseXForwardedFor: false, + } + } + + return nil +} + +// GetRedirect Create redirect from labels +func GetRedirect(labels map[string]string) *types.Redirect { + permanent := GetBoolValue(labels, TraefikFrontendRedirectPermanent, false) + + if Has(labels, TraefikFrontendRedirectEntryPoint) { + return &types.Redirect{ + EntryPoint: GetStringValue(labels, TraefikFrontendRedirectEntryPoint, ""), + Permanent: permanent, + } + } + + if Has(labels, TraefikFrontendRedirectRegex) && + Has(labels, TraefikFrontendRedirectReplacement) { + return &types.Redirect{ + Regex: GetStringValue(labels, TraefikFrontendRedirectRegex, ""), + Replacement: GetStringValue(labels, TraefikFrontendRedirectReplacement, ""), + Permanent: permanent, + } + } + + return nil +} + +// GetErrorPages Create error pages from labels +func GetErrorPages(labels map[string]string) map[string]*types.ErrorPage { + prefix := Prefix + BaseFrontendErrorPage + return ParseErrorPages(labels, prefix, RegexpFrontendErrorPage) +} + +// 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 + + for lblName, value := range labels { + if strings.HasPrefix(lblName, labelPrefix) { + submatch := labelRegex.FindStringSubmatch(lblName) + if len(submatch) != 3 { + log.Errorf("Invalid page error label: %s, sub-match: %v", lblName, submatch) + continue + } + + if errorPages == nil { + errorPages = make(map[string]*types.ErrorPage) + } + + pageName := submatch[1] + + ep, ok := errorPages[pageName] + if !ok { + ep = &types.ErrorPage{} + errorPages[pageName] = ep + } + + switch submatch[2] { + case SuffixErrorPageStatus: + ep.Status = SplitAndTrimString(value, ",") + case SuffixErrorPageQuery: + ep.Query = value + case SuffixErrorPageBackend: + ep.Backend = value + default: + log.Errorf("Invalid page error label: %s", lblName) + continue + } + } + } + + return errorPages +} + +// GetRateLimit Create rate limits from labels +func GetRateLimit(labels map[string]string) *types.RateLimit { + extractorFunc := GetStringValue(labels, TraefikFrontendRateLimitExtractorFunc, "") + if len(extractorFunc) == 0 { + return nil + } + + prefix := Prefix + BaseFrontendRateLimit + limits := ParseRateSets(labels, prefix, RegexpFrontendRateLimit) + + return &types.RateLimit{ + ExtractorFunc: extractorFunc, + RateSet: limits, + } +} + +// ParseRateSets parse rate limits to create Rate struct +func ParseRateSets(labels map[string]string, labelPrefix string, labelRegex *regexp.Regexp) map[string]*types.Rate { + var rateSets map[string]*types.Rate + + for lblName, rawValue := range labels { + if strings.HasPrefix(lblName, labelPrefix) && len(rawValue) > 0 { + submatch := labelRegex.FindStringSubmatch(lblName) + if len(submatch) != 3 { + log.Errorf("Invalid rate limit label: %s, sub-match: %v", lblName, submatch) + continue + } + + if rateSets == nil { + rateSets = make(map[string]*types.Rate) + } + + limitName := submatch[1] + + ep, ok := rateSets[limitName] + if !ok { + ep = &types.Rate{} + rateSets[limitName] = ep + } + + switch submatch[2] { + case "period": + var d flaeg.Duration + err := d.Set(rawValue) + if err != nil { + log.Errorf("Unable to parse %q: %q. %v", lblName, rawValue, err) + continue + } + ep.Period = d + case "average": + value, err := strconv.ParseInt(rawValue, 10, 64) + if err != nil { + log.Errorf("Unable to parse %q: %q. %v", lblName, rawValue, err) + continue + } + ep.Average = value + case "burst": + value, err := strconv.ParseInt(rawValue, 10, 64) + if err != nil { + log.Errorf("Unable to parse %q: %q. %v", lblName, rawValue, err) + continue + } + ep.Burst = value + default: + log.Errorf("Invalid rate limit label: %s", lblName) + continue + } + } + } + return rateSets +} + +// GetHeaders Create headers from labels +func GetHeaders(labels map[string]string) *types.Headers { + headers := &types.Headers{ + CustomRequestHeaders: GetMapValue(labels, TraefikFrontendRequestHeaders), + CustomResponseHeaders: GetMapValue(labels, TraefikFrontendResponseHeaders), + SSLProxyHeaders: GetMapValue(labels, TraefikFrontendSSLProxyHeaders), + AllowedHosts: GetSliceStringValue(labels, TraefikFrontendAllowedHosts), + HostsProxyHeaders: GetSliceStringValue(labels, TraefikFrontendHostsProxyHeaders), + STSSeconds: GetInt64Value(labels, TraefikFrontendSTSSeconds, 0), + SSLRedirect: GetBoolValue(labels, TraefikFrontendSSLRedirect, false), + SSLTemporaryRedirect: GetBoolValue(labels, TraefikFrontendSSLTemporaryRedirect, false), + STSIncludeSubdomains: GetBoolValue(labels, TraefikFrontendSTSIncludeSubdomains, false), + STSPreload: GetBoolValue(labels, TraefikFrontendSTSPreload, false), + ForceSTSHeader: GetBoolValue(labels, TraefikFrontendForceSTSHeader, false), + FrameDeny: GetBoolValue(labels, TraefikFrontendFrameDeny, false), + ContentTypeNosniff: GetBoolValue(labels, TraefikFrontendContentTypeNosniff, false), + BrowserXSSFilter: GetBoolValue(labels, TraefikFrontendBrowserXSSFilter, false), + IsDevelopment: GetBoolValue(labels, TraefikFrontendIsDevelopment, false), + SSLHost: GetStringValue(labels, TraefikFrontendSSLHost, ""), + CustomFrameOptionsValue: GetStringValue(labels, TraefikFrontendCustomFrameOptionsValue, ""), + ContentSecurityPolicy: GetStringValue(labels, TraefikFrontendContentSecurityPolicy, ""), + PublicKey: GetStringValue(labels, TraefikFrontendPublicKey, ""), + ReferrerPolicy: GetStringValue(labels, TraefikFrontendReferrerPolicy, ""), + CustomBrowserXSSValue: GetStringValue(labels, TraefikFrontendCustomBrowserXSSValue, ""), + } + + if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() { + return nil + } + + return headers +} + +// GetMaxConn Create max connection from labels +func GetMaxConn(labels map[string]string) *types.MaxConn { + amount := GetInt64Value(labels, TraefikBackendMaxConnAmount, math.MinInt64) + extractorFunc := GetStringValue(labels, TraefikBackendMaxConnExtractorFunc, DefaultBackendMaxconnExtractorFunc) + + if amount == math.MinInt64 || len(extractorFunc) == 0 { + return nil + } + + return &types.MaxConn{ + Amount: amount, + ExtractorFunc: extractorFunc, + } +} + +// GetHealthCheck Create health check from labels +func GetHealthCheck(labels map[string]string) *types.HealthCheck { + path := GetStringValue(labels, TraefikBackendHealthCheckPath, "") + if len(path) == 0 { + return nil + } + + port := GetIntValue(labels, TraefikBackendHealthCheckPort, DefaultBackendHealthCheckPort) + interval := GetStringValue(labels, TraefikBackendHealthCheckInterval, "") + + return &types.HealthCheck{ + Path: path, + Port: port, + Interval: interval, + } +} + +// GetBuffering Create buffering from labels +func GetBuffering(labels map[string]string) *types.Buffering { + if !HasPrefix(labels, TraefikBackendBuffering) { + return nil + } + + return &types.Buffering{ + MaxRequestBodyBytes: GetInt64Value(labels, TraefikBackendBufferingMaxRequestBodyBytes, 0), + MaxResponseBodyBytes: GetInt64Value(labels, TraefikBackendBufferingMaxResponseBodyBytes, 0), + MemRequestBodyBytes: GetInt64Value(labels, TraefikBackendBufferingMemRequestBodyBytes, 0), + MemResponseBodyBytes: GetInt64Value(labels, TraefikBackendBufferingMemResponseBodyBytes, 0), + RetryExpression: GetStringValue(labels, TraefikBackendBufferingRetryExpression, ""), + } +} + +// GetCircuitBreaker Create circuit breaker from labels +func GetCircuitBreaker(labels map[string]string) *types.CircuitBreaker { + circuitBreaker := GetStringValue(labels, TraefikBackendCircuitBreakerExpression, "") + if len(circuitBreaker) == 0 { + return nil + } + return &types.CircuitBreaker{Expression: circuitBreaker} +} + +// GetLoadBalancer Create load balancer from labels +func GetLoadBalancer(labels map[string]string) *types.LoadBalancer { + if !HasPrefix(labels, TraefikBackendLoadBalancer) { + return nil + } + + method := GetStringValue(labels, TraefikBackendLoadBalancerMethod, DefaultBackendLoadBalancerMethod) + + lb := &types.LoadBalancer{ + Method: method, + Sticky: getSticky(labels), + } + + if GetBoolValue(labels, TraefikBackendLoadBalancerStickiness, false) { + cookieName := GetStringValue(labels, TraefikBackendLoadBalancerStickinessCookieName, DefaultBackendLoadbalancerStickinessCookieName) + lb.Stickiness = &types.Stickiness{CookieName: cookieName} + } + + return lb +} + +// TODO: Deprecated +// replaced by Stickiness +// Deprecated +func getSticky(labels map[string]string) bool { + if Has(labels, TraefikBackendLoadBalancerSticky) { + log.Warnf("Deprecated configuration found: %s. Please use %s.", TraefikBackendLoadBalancerSticky, TraefikBackendLoadBalancerStickiness) + } + + return GetBoolValue(labels, TraefikBackendLoadBalancerSticky, false) +} diff --git a/provider/label/partial_test.go b/provider/label/partial_test.go new file mode 100644 index 000000000..33ad0d2b7 --- /dev/null +++ b/provider/label/partial_test.go @@ -0,0 +1,711 @@ +package label + +import ( + "testing" + "time" + + "github.com/containous/flaeg" + "github.com/containous/traefik/types" + "github.com/stretchr/testify/assert" +) + +func TestParseErrorPages(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + expected map[string]*types.ErrorPage + }{ + { + desc: "2 errors pages", + labels: map[string]string{ + Prefix + BaseFrontendErrorPage + "foo." + SuffixErrorPageStatus: "404", + Prefix + BaseFrontendErrorPage + "foo." + SuffixErrorPageBackend: "foo_backend", + Prefix + BaseFrontendErrorPage + "foo." + SuffixErrorPageQuery: "foo_query", + Prefix + BaseFrontendErrorPage + "bar." + SuffixErrorPageStatus: "500,600", + Prefix + BaseFrontendErrorPage + "bar." + SuffixErrorPageBackend: "bar_backend", + Prefix + BaseFrontendErrorPage + "bar." + 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", + labels: map[string]string{ + Prefix + BaseFrontendErrorPage + "foo." + SuffixErrorPageStatus: "404", + }, + expected: map[string]*types.ErrorPage{ + "foo": { + Status: []string{"404"}, + }, + }, + }, + { + desc: "invalid field", + labels: map[string]string{ + Prefix + BaseFrontendErrorPage + "foo." + "courgette": "404", + }, + expected: map[string]*types.ErrorPage{"foo": {}}, + }, + { + desc: "no error pages labels", + labels: map[string]string{}, + expected: nil, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + pages := ParseErrorPages(test.labels, Prefix+BaseFrontendErrorPage, RegexpFrontendErrorPage) + + assert.EqualValues(t, test.expected, pages) + }) + } +} + +func TestParseRateSets(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + expected map[string]*types.Rate + }{ + { + desc: "2 rate limits", + labels: map[string]string{ + Prefix + BaseFrontendRateLimit + "foo." + SuffixRateLimitPeriod: "6", + Prefix + BaseFrontendRateLimit + "foo." + SuffixRateLimitAverage: "12", + Prefix + BaseFrontendRateLimit + "foo." + SuffixRateLimitBurst: "18", + Prefix + BaseFrontendRateLimit + "bar." + SuffixRateLimitPeriod: "3", + Prefix + BaseFrontendRateLimit + "bar." + SuffixRateLimitAverage: "6", + Prefix + BaseFrontendRateLimit + "bar." + SuffixRateLimitBurst: "9", + }, + expected: 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: "no rate limits labels", + labels: map[string]string{}, + expected: nil, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + rateSets := ParseRateSets(test.labels, Prefix+BaseFrontendRateLimit, RegexpFrontendRateLimit) + + assert.EqualValues(t, test.expected, rateSets) + }) + } +} + +func TestWhiteList(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + expected *types.WhiteList + }{ + { + desc: "should return nil when no white list labels", + labels: map[string]string{}, + expected: nil, + }, + { + desc: "should return a struct when deprecated label", + labels: map[string]string{ + TraefikFrontendWhitelistSourceRange: "10.10.10.10", + }, + expected: &types.WhiteList{ + SourceRange: []string{ + "10.10.10.10", + }, + UseXForwardedFor: false, + }, + }, + { + desc: "should return a struct when only range", + labels: map[string]string{ + TraefikFrontendWhiteListSourceRange: "10.10.10.10", + }, + expected: &types.WhiteList{ + SourceRange: []string{ + "10.10.10.10", + }, + UseXForwardedFor: false, + }, + }, + { + desc: "should return a struct when range and UseXForwardedFor", + labels: map[string]string{ + TraefikFrontendWhiteListSourceRange: "10.10.10.10", + TraefikFrontendWhiteListUseXForwardedFor: "true", + }, + expected: &types.WhiteList{ + SourceRange: []string{ + "10.10.10.10", + }, + UseXForwardedFor: true, + }, + }, + { + desc: "should return a struct when mix deprecated label and new labels", + labels: map[string]string{ + TraefikFrontendWhitelistSourceRange: "20.20.20.20", + TraefikFrontendWhiteListSourceRange: "10.10.10.10", + TraefikFrontendWhiteListUseXForwardedFor: "true", + }, + expected: &types.WhiteList{ + SourceRange: []string{ + "10.10.10.10", + }, + UseXForwardedFor: true, + }, + }, + { + desc: "should return nil when only UseXForwardedFor", + labels: map[string]string{ + TraefikFrontendWhiteListUseXForwardedFor: "true", + }, + expected: nil, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := GetWhiteList(test.labels) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetCircuitBreaker(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + expected *types.CircuitBreaker + }{ + { + desc: "should return nil when no CB label", + labels: map[string]string{}, + expected: nil, + }, + { + desc: "should return a struct when CB label is set", + labels: map[string]string{ + 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() + + actual := GetCircuitBreaker(test.labels) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetLoadBalancer(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + expected *types.LoadBalancer + }{ + { + desc: "should return nil when no LB labels", + labels: map[string]string{}, + expected: nil, + }, + { + desc: "should return a struct when labels are set", + labels: map[string]string{ + TraefikBackendLoadBalancerMethod: "drr", + TraefikBackendLoadBalancerSticky: "true", + TraefikBackendLoadBalancerStickiness: "true", + 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", + labels: map[string]string{ + TraefikBackendLoadBalancerMethod: "drr", + TraefikBackendLoadBalancerSticky: "true", + 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() + + actual := GetLoadBalancer(test.labels) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetMaxConn(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + expected *types.MaxConn + }{ + { + desc: "should return nil when no max conn labels", + labels: map[string]string{}, + expected: nil, + }, + { + desc: "should return nil when no amount label", + labels: map[string]string{ + TraefikBackendMaxConnExtractorFunc: "client.ip", + }, + expected: nil, + }, + { + desc: "should return default when no empty extractorFunc label", + labels: map[string]string{ + TraefikBackendMaxConnExtractorFunc: "", + TraefikBackendMaxConnAmount: "666", + }, + expected: &types.MaxConn{ + ExtractorFunc: "request.host", + Amount: 666, + }, + }, + { + desc: "should return a struct when max conn labels are set", + labels: map[string]string{ + TraefikBackendMaxConnExtractorFunc: "client.ip", + TraefikBackendMaxConnAmount: "666", + }, + expected: &types.MaxConn{ + ExtractorFunc: "client.ip", + Amount: 666, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := GetMaxConn(test.labels) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetHealthCheck(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + expected *types.HealthCheck + }{ + { + desc: "should return nil when no health check labels", + labels: map[string]string{}, + expected: nil, + }, + { + desc: "should return nil when no health check Path label", + labels: map[string]string{ + TraefikBackendHealthCheckPort: "80", + TraefikBackendHealthCheckInterval: "6", + }, + expected: nil, + }, + { + desc: "should return a struct when health check labels are set", + labels: map[string]string{ + TraefikBackendHealthCheckPath: "/health", + TraefikBackendHealthCheckPort: "80", + TraefikBackendHealthCheckInterval: "6", + }, + expected: &types.HealthCheck{ + Path: "/health", + Port: 80, + Interval: "6", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := GetHealthCheck(test.labels) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetBuffering(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + expected *types.Buffering + }{ + { + desc: "should return nil when no buffering labels", + labels: map[string]string{}, + expected: nil, + }, + { + desc: "should return a struct when buffering labels are set", + labels: map[string]string{ + TraefikBackendBufferingMaxResponseBodyBytes: "10485760", + TraefikBackendBufferingMemResponseBodyBytes: "2097152", + TraefikBackendBufferingMaxRequestBodyBytes: "10485760", + TraefikBackendBufferingMemRequestBodyBytes: "2097152", + 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() + + actual := GetBuffering(test.labels) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetRedirect(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + expected *types.Redirect + }{ + + { + desc: "should return nil when no redirect labels", + labels: map[string]string{}, + expected: nil, + }, + { + desc: "should use only entry point tag when mix regex redirect and entry point redirect", + labels: map[string]string{ + TraefikFrontendRedirectEntryPoint: "https", + TraefikFrontendRedirectRegex: "(.*)", + TraefikFrontendRedirectReplacement: "$1", + }, + expected: &types.Redirect{ + EntryPoint: "https", + }, + }, + { + desc: "should return a struct when entry point redirect label", + labels: map[string]string{ + TraefikFrontendRedirectEntryPoint: "https", + }, + expected: &types.Redirect{ + EntryPoint: "https", + }, + }, + { + desc: "should return a struct when entry point redirect label (permanent)", + labels: map[string]string{ + TraefikFrontendRedirectEntryPoint: "https", + TraefikFrontendRedirectPermanent: "true", + }, + expected: &types.Redirect{ + EntryPoint: "https", + Permanent: true, + }, + }, + { + desc: "should return a struct when regex redirect labels", + labels: map[string]string{ + TraefikFrontendRedirectRegex: "(.*)", + TraefikFrontendRedirectReplacement: "$1", + }, + expected: &types.Redirect{ + Regex: "(.*)", + Replacement: "$1", + }, + }, + { + desc: "should return a struct when regex redirect labels (permanent)", + labels: map[string]string{ + TraefikFrontendRedirectRegex: "(.*)", + TraefikFrontendRedirectReplacement: "$1", + 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() + + actual := GetRedirect(test.labels) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetRateLimit(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + expected *types.RateLimit + }{ + { + desc: "should return nil when no rate limit labels", + labels: map[string]string{}, + expected: nil, + }, + { + desc: "should return a struct when rate limit labels are defined", + labels: map[string]string{ + TraefikFrontendRateLimitExtractorFunc: "client.ip", + Prefix + BaseFrontendRateLimit + "foo." + SuffixRateLimitPeriod: "6", + Prefix + BaseFrontendRateLimit + "foo." + SuffixRateLimitAverage: "12", + Prefix + BaseFrontendRateLimit + "foo." + SuffixRateLimitBurst: "18", + Prefix + BaseFrontendRateLimit + "bar." + SuffixRateLimitPeriod: "3", + Prefix + BaseFrontendRateLimit + "bar." + SuffixRateLimitAverage: "6", + Prefix + BaseFrontendRateLimit + "bar." + 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", + labels: map[string]string{ + Prefix + BaseFrontendRateLimit + "foo." + SuffixRateLimitPeriod: "6", + Prefix + BaseFrontendRateLimit + "foo." + SuffixRateLimitAverage: "12", + Prefix + BaseFrontendRateLimit + "foo." + SuffixRateLimitBurst: "18", + Prefix + BaseFrontendRateLimit + "bar." + SuffixRateLimitPeriod: "3", + Prefix + BaseFrontendRateLimit + "bar." + SuffixRateLimitAverage: "6", + Prefix + BaseFrontendRateLimit + "bar." + SuffixRateLimitBurst: "9", + }, + expected: nil, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := GetRateLimit(test.labels) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetHeaders(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + expected *types.Headers + }{ + { + desc: "should return nil when no custom headers options are set", + labels: map[string]string{}, + expected: nil, + }, + { + desc: "should return a struct when all custom headers options are set", + labels: map[string]string{ + TraefikFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + TraefikFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + TraefikFrontendSSLProxyHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + TraefikFrontendAllowedHosts: "foo,bar,bor", + TraefikFrontendHostsProxyHeaders: "foo,bar,bor", + TraefikFrontendSSLHost: "foo", + TraefikFrontendCustomFrameOptionsValue: "foo", + TraefikFrontendContentSecurityPolicy: "foo", + TraefikFrontendPublicKey: "foo", + TraefikFrontendReferrerPolicy: "foo", + TraefikFrontendCustomBrowserXSSValue: "foo", + TraefikFrontendSTSSeconds: "666", + TraefikFrontendSSLRedirect: "true", + TraefikFrontendSSLTemporaryRedirect: "true", + TraefikFrontendSTSIncludeSubdomains: "true", + TraefikFrontendSTSPreload: "true", + TraefikFrontendForceSTSHeader: "true", + TraefikFrontendFrameDeny: "true", + TraefikFrontendContentTypeNosniff: "true", + TraefikFrontendBrowserXSSFilter: "true", + 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() + + actual := GetHeaders(test.labels) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestProviderGetErrorPages(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + expected map[string]*types.ErrorPage + }{ + { + desc: "should return nil when no tags", + labels: map[string]string{}, + expected: nil, + }, + { + desc: "should return a map when tags are present", + labels: map[string]string{ + Prefix + BaseFrontendErrorPage + "foo." + SuffixErrorPageStatus: "404", + Prefix + BaseFrontendErrorPage + "foo." + SuffixErrorPageBackend: "foo_backend", + Prefix + BaseFrontendErrorPage + "foo." + SuffixErrorPageQuery: "foo_query", + Prefix + BaseFrontendErrorPage + "bar." + SuffixErrorPageStatus: "500,600", + Prefix + BaseFrontendErrorPage + "bar." + SuffixErrorPageBackend: "bar_backend", + Prefix + BaseFrontendErrorPage + "bar." + 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", + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + result := GetErrorPages(test.labels) + + assert.Equal(t, test.expected, result) + }) + } +} diff --git a/provider/label/segment_test.go b/provider/label/segment_test.go new file mode 100644 index 000000000..663737f12 --- /dev/null +++ b/provider/label/segment_test.go @@ -0,0 +1,221 @@ +package label + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +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) + }) + } +} + +func TestExtractServiceProperties(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + expected SegmentProperties + }{ + { + desc: "empty labels map", + expected: SegmentProperties{}, + }, + { + desc: "valid label names", + labels: map[string]string{ + "traefik.foo.port": "bar", + "traefik.foo.frontend.bar": "1bar", + "traefik.foo.backend": "3bar", + }, + expected: SegmentProperties{ + "foo": SegmentPropertyValues{ + "port": "bar", + "frontend.bar": "1bar", + "backend": "3bar", + }, + }, + }, + { + desc: "invalid label names", + labels: map[string]string{ + "foo.frontend.bar": "1bar", + "traefik.foo.frontend.": "2bar", + "traefik.foo.port.bar": "barbar", + "traefik.foo.frontend": "0bar", + "traefik.frontend.foo.backend": "0bar", + }, + expected: SegmentProperties{}, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := ExtractServiceProperties(test.labels) + assert.EqualValues(t, test.expected, got) + }) + } +} + +func TestExtractServicePropertiesP(t *testing.T) { + testCases := []struct { + desc string + labels *map[string]string + expected SegmentProperties + }{ + { + desc: "nil labels map", + expected: SegmentProperties{}, + }, + { + desc: "valid label names", + labels: &map[string]string{ + "traefik.foo.port": "bar", + "traefik.foo.frontend.bar": "1bar", + "traefik.foo.backend": "3bar", + }, + expected: SegmentProperties{ + "foo": SegmentPropertyValues{ + "port": "bar", + "frontend.bar": "1bar", + "backend": "3bar", + }, + }, + }, + { + desc: "invalid label names", + labels: &map[string]string{ + "foo.frontend.bar": "1bar", + "traefik.foo.frontend.": "2bar", + "traefik.foo.port.bar": "barbar", + "traefik.foo.frontend": "0bar", + "traefik.frontend.foo.backend": "0bar", + }, + expected: SegmentProperties{}, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := ExtractServicePropertiesP(test.labels) + assert.EqualValues(t, test.expected, got) + }) + } +} + +func TestGetServiceLabel(t *testing.T) { + testCases := []struct { + desc string + labelName string + serviceName string + expected string + }{ + { + desc: "without service name", + labelName: TraefikPort, + expected: TraefikPort, + }, + { + desc: "with service name", + labelName: TraefikPort, + serviceName: "bar", + expected: "traefik.bar.port", + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := GetServiceLabel(test.labelName, test.serviceName) + assert.Equal(t, test.expected, got) + }) + } +} diff --git a/provider/marathon/builder_test.go b/provider/marathon/builder_test.go index 9f98e5676..0644c6848 100644 --- a/provider/marathon/builder_test.go +++ b/provider/marathon/builder_test.go @@ -12,6 +12,10 @@ const testTaskName = "taskID" // Functions related to building applications. +func withApplications(apps ...marathon.Application) *marathon.Applications { + return &marathon.Applications{Apps: apps} +} + func application(ops ...func(*marathon.Application)) marathon.Application { app := marathon.Application{} app.EmptyLabels() diff --git a/provider/marathon/config.go b/provider/marathon/config.go index a19128700..4842d1e5d 100644 --- a/provider/marathon/config.go +++ b/provider/marathon/config.go @@ -4,12 +4,10 @@ import ( "errors" "fmt" "math" - "net/url" "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" @@ -19,99 +17,79 @@ import ( const defaultService = "" -func (p *Provider) buildConfiguration() *types.Configuration { +type appData struct { + marathon.Application + SegmentLabels map[string]string + SegmentName string +} + +func (p *Provider) buildConfigurationV2(applications *marathon.Applications) *types.Configuration { var MarathonFuncMap = template.FuncMap{ - "getBackend": p.getBackend, - "getDomain": getFuncStringService(label.SuffixDomain, p.Domain), // see https://github.com/containous/traefik/pull/1693 - "getSubDomain": p.getSubDomain, // see https://github.com/containous/traefik/pull/1693 + "getDomain": label.GetFuncString(label.TraefikDomain, p.Domain), // see https://github.com/containous/traefik/pull/1693 + "getSubDomain": p.getSubDomain, // see https://github.com/containous/traefik/pull/1693 + "getBackendName": p.getBackendName, // Backend functions - "getBackendServer": p.getBackendServer, "getPort": getPort, - "getCircuitBreaker": getCircuitBreaker, - "getLoadBalancer": getLoadBalancer, - "getMaxConn": getMaxConn, - "getHealthCheck": getHealthCheck, - "getBuffering": getBuffering, + "getCircuitBreaker": label.GetCircuitBreaker, + "getLoadBalancer": label.GetLoadBalancer, + "getMaxConn": label.GetMaxConn, + "getHealthCheck": label.GetHealthCheck, + "getBuffering": label.GetBuffering, "getServers": p.getServers, - // TODO Deprecated [breaking] - "getWeight": getFuncIntService(label.SuffixWeight, label.DefaultWeightInt), - // TODO Deprecated [breaking] - "getProtocol": getFuncStringService(label.SuffixProtocol, label.DefaultProtocol), - // TODO Deprecated [breaking] - "hasCircuitBreakerLabels": hasFunc(label.TraefikBackendCircuitBreakerExpression), - // TODO Deprecated [breaking] - "getCircuitBreakerExpression": getFuncString(label.TraefikBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression), - // TODO Deprecated [breaking] - "hasLoadBalancerLabels": hasLoadBalancerLabels, - // TODO Deprecated [breaking] - "getLoadBalancerMethod": getFuncString(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), - // TODO Deprecated [breaking] - "getSticky": getSticky, - // TODO Deprecated [breaking] - "hasStickinessLabel": hasFunc(label.TraefikBackendLoadBalancerStickiness), - // TODO Deprecated [breaking] - "getStickinessCookieName": getFuncString(label.TraefikBackendLoadBalancerStickinessCookieName, ""), - // TODO Deprecated [breaking] - "hasMaxConnLabels": hasMaxConnLabels, - // TODO Deprecated [breaking] - "getMaxConnExtractorFunc": getFuncString(label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc), - // TODO Deprecated [breaking] - "getMaxConnAmount": getFuncInt64(label.TraefikBackendMaxConnAmount, math.MaxInt64), - // TODO Deprecated [breaking] - "hasHealthCheckLabels": hasFunc(label.TraefikBackendHealthCheckPath), - // TODO Deprecated [breaking] - "getHealthCheckPath": getFuncString(label.TraefikBackendHealthCheckPath, ""), - // TODO Deprecated [breaking] - "getHealthCheckInterval": getFuncString(label.TraefikBackendHealthCheckInterval, ""), - // Frontend functions - "getServiceNames": getServiceNames, - "getServiceNameSuffix": getServiceNameSuffix, - "getPassHostHeader": getFuncBoolService(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeaderBool), - "getPassTLSCert": getFuncBoolService(label.SuffixFrontendPassTLSCert, label.DefaultPassTLSCert), - "getPriority": getFuncIntService(label.SuffixFrontendPriority, label.DefaultFrontendPriorityInt), - "getEntryPoints": getFuncSliceStringService(label.SuffixFrontendEntryPoints), + "getSegmentNameSuffix": getSegmentNameSuffix, "getFrontendRule": p.getFrontendRule, "getFrontendName": p.getFrontendName, - "getBasicAuth": getFuncSliceStringService(label.SuffixFrontendAuthBasic), - "getRedirect": getRedirect, - "getErrorPages": getErrorPages, - "getRateLimit": getRateLimit, - "getHeaders": getHeaders, - "getWhiteList": getWhiteList, - - // TODO Deprecated [breaking] - "getWhitelistSourceRange": getFuncSliceStringService(label.SuffixFrontendWhitelistSourceRange), + "getPassHostHeader": label.GetFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getPassTLSCert": label.GetFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), + "getPriority": label.GetFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), + "getEntryPoints": label.GetFuncSliceString(label.TraefikFrontendEntryPoints), + "getBasicAuth": label.GetFuncSliceString(label.TraefikFrontendAuthBasic), + "getRedirect": label.GetRedirect, + "getErrorPages": label.GetErrorPages, + "getRateLimit": label.GetRateLimit, + "getHeaders": label.GetHeaders, + "getWhiteList": label.GetWhiteList, } - v := url.Values{} - v.Add("embed", "apps.tasks") - v.Add("embed", "apps.deployments") - v.Add("embed", "apps.readiness") - applications, err := p.marathonClient.Applications(v) - if err != nil { - log.Errorf("Failed to retrieve Marathon applications: %v", err) - return nil - } - - filteredApps := fun.Filter(p.applicationFilter, applications.Apps).([]marathon.Application) - for i, app := range filteredApps { - filteredApps[i].Tasks = fun.Filter(func(task *marathon.Task) bool { - filtered := p.taskFilter(*task, app) - if filtered { - logIllegalServices(*task, app) + var apps []*appData + for _, app := range applications.Apps { + if p.applicationFilter(app) { + // Tasks + var filteredTasks []*marathon.Task + for _, task := range app.Tasks { + if p.taskFilter(*task, app) { + filteredTasks = append(filteredTasks, task) + logIllegalServices(*task, app) + } } - return filtered - }, app.Tasks).([]*marathon.Task) + + if len(filteredTasks) == 0 { + log.Warnf("No valid tasks for application %s", app.ID) + continue + } + app.Tasks = filteredTasks + + // segments + segmentProperties := label.ExtractTraefikLabels(stringValueMap(app.Labels)) + for segmentName, labels := range segmentProperties { + data := &appData{ + Application: app, + SegmentLabels: labels, + SegmentName: segmentName, + } + apps = append(apps, data) + } + } } templateObjects := struct { - Applications []marathon.Application + Applications []*appData Domain string }{ - Applications: filteredApps, + Applications: apps, Domain: p.Domain, } @@ -124,15 +102,15 @@ func (p *Provider) buildConfiguration() *types.Configuration { func (p *Provider) applicationFilter(app marathon.Application) bool { // Filter disabled application. - if !label.IsEnabledP(app.Labels, p.ExposedByDefault) { + if !label.IsEnabled(stringValueMap(app.Labels), p.ExposedByDefault) { log.Debugf("Filtering disabled Marathon application %s", app.ID) return false } // Filter by constraints. - constraintTags := label.GetSliceStringValueP(app.Labels, label.TraefikTags) + constraintTags := label.GetSliceStringValue(stringValueMap(app.Labels), label.TraefikTags) if p.MarathonLBCompatibility { - if haGroup := label.GetStringValueP(app.Labels, labelLbCompatibilityGroup, ""); len(haGroup) > 0 { + if haGroup := label.GetStringValue(stringValueMap(app.Labels), labelLbCompatibilityGroup, ""); len(haGroup) > 0 { constraintTags = append(constraintTags, haGroup) } } @@ -164,40 +142,32 @@ func (p *Provider) taskFilter(task marathon.Task, application marathon.Applicati return true } -// getFrontendRule returns the frontend rule for the specified application, using -// its label. If service is provided, it will look for serviceName label before generic one. -// It returns a default one (Host) if the label is not present. -func (p *Provider) getFrontendRule(application marathon.Application, serviceName string) string { - labels := getLabels(application, serviceName) - lblFrontendRule := getLabelName(serviceName, label.SuffixFrontendRule) - if value := label.GetStringValue(labels, lblFrontendRule, ""); len(value) > 0 { - return value - } +// logIllegalServices logs illegal service configurations. +// While we cannot filter on the service level, they will eventually get +// rejected once the server configuration is rendered. +func logIllegalServices(task marathon.Task, app marathon.Application) { + segmentProperties := label.ExtractTraefikLabels(stringValueMap(app.Labels)) + for segmentName, labels := range segmentProperties { + // Check for illegal/missing ports. + if _, err := processPorts(app, task, labels); err != nil { + log.Warnf("%s has an illegal configuration: no proper port available", identifier(app, task, segmentName)) + continue + } - if p.MarathonLBCompatibility { - if value := label.GetStringValueP(application.Labels, labelLbCompatibility, ""); len(value) > 0 { - return "Host:" + value + // Check for illegal port label combinations. + hasPortLabel := label.Has(labels, label.TraefikPort) + hasPortIndexLabel := label.Has(labels, label.TraefikPortIndex) + if hasPortLabel && hasPortIndexLabel { + log.Warnf("%s has both port and port index specified; port will take precedence", identifier(app, task, segmentName)) } } +} +func getSegmentNameSuffix(serviceName string) string { if len(serviceName) > 0 { - return "Host:" + strings.ToLower(provider.Normalize(serviceName)) + "." + p.getSubDomain(application.ID) + "." + p.Domain + return "-service-" + provider.Normalize(serviceName) } - return "Host:" + p.getSubDomain(application.ID) + "." + p.Domain -} - -func (p *Provider) getBackend(application marathon.Application, serviceName string) string { - labels := getLabels(application, serviceName) - lblBackend := getLabelName(serviceName, label.SuffixBackend) - value := label.GetStringValue(labels, lblBackend, "") - if len(value) > 0 { - return provider.Normalize("backend" + value) - } - return provider.Normalize("backend" + application.ID + getServiceNameSuffix(serviceName)) -} - -func (p *Provider) getFrontendName(application marathon.Application, serviceName string) string { - return provider.Normalize("frontend" + application.ID + getServiceNameSuffix(serviceName)) + return "" } func (p *Provider) getSubDomain(name string) string { @@ -210,120 +180,43 @@ func (p *Provider) getSubDomain(name string) string { return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1) } -func (p *Provider) getBackendServer(task marathon.Task, application marathon.Application) string { - if application.IPAddressPerTask == nil || p.ForceTaskHostname { - return task.Host - } - - numTaskIPAddresses := len(task.IPAddresses) - switch numTaskIPAddresses { - case 0: - log.Errorf("Missing IP address for Marathon application %s on task %s", application.ID, task.ID) - return "" - case 1: - return task.IPAddresses[0].IPAddress - default: - ipAddressIdx := label.GetIntValueP(application.Labels, labelIPAddressIdx, math.MinInt32) - - if ipAddressIdx == math.MinInt32 { - log.Errorf("Found %d task IP addresses but missing IP address index for Marathon application %s on task %s", - numTaskIPAddresses, application.ID, task.ID) - return "" - } - if ipAddressIdx < 0 || ipAddressIdx > numTaskIPAddresses { - log.Errorf("Cannot use IP address index to select from %d task IP addresses for Marathon application %s on task %s", - numTaskIPAddresses, application.ID, task.ID) - return "" - } - - return task.IPAddresses[ipAddressIdx].IPAddress +func (p *Provider) getBackendName(app appData) string { + + value := label.GetStringValue(app.SegmentLabels, label.TraefikBackend, "") + if len(value) > 0 { + return provider.Normalize("backend" + value) } + return provider.Normalize("backend" + app.ID + getSegmentNameSuffix(app.SegmentName)) } -func identifier(app marathon.Application, task marathon.Task, serviceName string) string { - id := fmt.Sprintf("Marathon task %s from application %s", task.ID, app.ID) - if serviceName != "" { - id += fmt.Sprintf(" (service: %s)", serviceName) - } - return id +func (p *Provider) getFrontendName(app appData) string { + return provider.Normalize("frontend" + app.ID + getSegmentNameSuffix(app.SegmentName)) } -// getServiceNames returns a list of service names for a given application -// An empty name "" will be added if no service specific properties exist, -// as an indication that there are no sub-services, but only main application -func getServiceNames(application marathon.Application) []string { - labelServiceProperties := label.ExtractServicePropertiesP(application.Labels) - - var names []string - for k := range labelServiceProperties { - names = append(names, k) +// getFrontendRule returns the frontend rule for the specified application, using +// its label. If service is provided, it will look for serviceName label before generic one. +// It returns a default one (Host) if the label is not present. +func (p *Provider) getFrontendRule(app appData) string { + if value := label.GetStringValue(app.SegmentLabels, label.TraefikFrontendRule, ""); len(value) > 0 { + return value } - // An empty name "" will be added if no service specific properties exist, - // as an indication that there are no sub-services, but only main application - if len(names) == 0 { - names = append(names, defaultService) - } - return names -} - -func getServiceNameSuffix(serviceName string) string { - if len(serviceName) > 0 { - return "-service-" + provider.Normalize(serviceName) - } - return "" -} - -// logIllegalServices logs illegal service configurations. -// While we cannot filter on the service level, they will eventually get -// rejected once the server configuration is rendered. -func logIllegalServices(task marathon.Task, application marathon.Application) { - for _, serviceName := range getServiceNames(application) { - // Check for illegal/missing ports. - if _, err := processPorts(application, task, serviceName); err != nil { - log.Warnf("%s has an illegal configuration: no proper port available", identifier(application, task, serviceName)) - continue - } - - // Check for illegal port label combinations. - labels := getLabels(application, serviceName) - hasPortLabel := label.Has(labels, getLabelName(serviceName, label.SuffixPort)) - hasPortIndexLabel := label.Has(labels, getLabelName(serviceName, label.SuffixPortIndex)) - if hasPortLabel && hasPortIndexLabel { - log.Warnf("%s has both port and port index specified; port will take precedence", identifier(application, task, serviceName)) + if p.MarathonLBCompatibility { + if value := label.GetStringValue(stringValueMap(app.Labels), labelLbCompatibility, ""); len(value) > 0 { + return "Host:" + value } } -} -// Deprecated -func hasLoadBalancerLabels(application marathon.Application) bool { - method := label.HasP(application.Labels, label.TraefikBackendLoadBalancerMethod) - sticky := label.HasP(application.Labels, label.TraefikBackendLoadBalancerSticky) - stickiness := label.HasP(application.Labels, label.TraefikBackendLoadBalancerStickiness) - return method || sticky || stickiness -} - -// Deprecated -func hasMaxConnLabels(application marathon.Application) bool { - mca := label.HasP(application.Labels, label.TraefikBackendMaxConnAmount) - mcef := label.HasP(application.Labels, label.TraefikBackendMaxConnExtractorFunc) - return mca && mcef -} - -// TODO: Deprecated -// replaced by Stickiness -// Deprecated -func getSticky(application marathon.Application) bool { - if label.HasP(application.Labels, label.TraefikBackendLoadBalancerSticky) { - log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) + if len(app.SegmentName) > 0 { + return "Host:" + strings.ToLower(provider.Normalize(app.SegmentName)) + "." + p.getSubDomain(app.ID) + "." + p.Domain } - return label.GetBoolValueP(application.Labels, label.TraefikBackendLoadBalancerSticky, false) + return "Host:" + p.getSubDomain(app.ID) + "." + p.Domain } -func getPort(task marathon.Task, application marathon.Application, serviceName string) string { - port, err := processPorts(application, task, serviceName) +func getPort(task marathon.Task, app appData) string { + port, err := processPorts(app.Application, task, app.SegmentLabels) if err != nil { - log.Errorf("Unable to process ports for %s: %s", identifier(application, task, serviceName), err) + log.Errorf("Unable to process ports for %s: %s", identifier(app.Application, task, app.SegmentName), err) return "" } @@ -334,12 +227,9 @@ func getPort(task marathon.Task, application marathon.Application, serviceName s // An explicitly specified port is preferred. If none is specified, it selects // one of the available port. The first such found port is returned unless an // optional index is provided. -func processPorts(application marathon.Application, task marathon.Task, serviceName string) (int, error) { - labels := getLabels(application, serviceName) - lblPort := getLabelName(serviceName, label.SuffixPort) - - if label.Has(labels, lblPort) { - port := label.GetIntValue(labels, lblPort, 0) +func processPorts(app marathon.Application, task marathon.Task, labels map[string]string) (int, error) { + if label.Has(labels, label.TraefikPort) { + port := label.GetIntValue(labels, label.TraefikPort, 0) if port <= 0 { return 0, fmt.Errorf("explicitly specified port %d must be larger than zero", port) @@ -348,39 +238,39 @@ func processPorts(application marathon.Application, task marathon.Task, serviceN } } - ports := retrieveAvailablePorts(application, task) + ports := retrieveAvailablePorts(app, task) if len(ports) == 0 { return 0, errors.New("no port found") } - lblPortIndex := getLabelName(serviceName, label.SuffixPortIndex) - portIndex := label.GetIntValue(labels, lblPortIndex, 0) + portIndex := label.GetIntValue(labels, label.TraefikPortIndex, 0) if portIndex < 0 || portIndex > len(ports)-1 { return 0, fmt.Errorf("index %d must be within range (0, %d)", portIndex, len(ports)-1) } return ports[portIndex], nil } -func retrieveAvailablePorts(application marathon.Application, task marathon.Task) []int { +func retrieveAvailablePorts(app marathon.Application, task marathon.Task) []int { // Using default port configuration if len(task.Ports) > 0 { return task.Ports } // Using port definition if available - if application.PortDefinitions != nil && len(*application.PortDefinitions) > 0 { + if app.PortDefinitions != nil && len(*app.PortDefinitions) > 0 { var ports []int - for _, def := range *application.PortDefinitions { + for _, def := range *app.PortDefinitions { if def.Port != nil { ports = append(ports, *def.Port) } } return ports } + // If using IP-per-task using this port definition - if application.IPAddressPerTask != nil && len(*(application.IPAddressPerTask.Discovery).Ports) > 0 { + if app.IPAddressPerTask != nil && app.IPAddressPerTask.Discovery != nil && len(*(app.IPAddressPerTask.Discovery.Ports)) > 0 { var ports []int - for _, def := range *(application.IPAddressPerTask.Discovery).Ports { + for _, def := range *(app.IPAddressPerTask.Discovery.Ports) { ports = append(ports, def.Number) } return ports @@ -389,83 +279,19 @@ func retrieveAvailablePorts(application marathon.Application, task marathon.Task return []int{} } -func getCircuitBreaker(application marathon.Application) *types.CircuitBreaker { - circuitBreaker := label.GetStringValueP(application.Labels, label.TraefikBackendCircuitBreakerExpression, "") - if len(circuitBreaker) == 0 { - return nil +func identifier(app marathon.Application, task marathon.Task, segmentName string) string { + id := fmt.Sprintf("Marathon task %s from application %s", task.ID, app.ID) + if segmentName != "" { + id += fmt.Sprintf(" (segment: %s)", segmentName) } - return &types.CircuitBreaker{Expression: circuitBreaker} + return id } -func getLoadBalancer(application marathon.Application) *types.LoadBalancer { - if !label.HasPrefixP(application.Labels, label.TraefikBackendLoadBalancer) { - return nil - } - - method := label.GetStringValueP(application.Labels, label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod) - - lb := &types.LoadBalancer{ - Method: method, - Sticky: getSticky(application), - } - - if label.GetBoolValueP(application.Labels, label.TraefikBackendLoadBalancerStickiness, false) { - cookieName := label.GetStringValueP(application.Labels, label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName) - lb.Stickiness = &types.Stickiness{CookieName: cookieName} - } - - return lb -} - -func getMaxConn(application marathon.Application) *types.MaxConn { - amount := label.GetInt64ValueP(application.Labels, label.TraefikBackendMaxConnAmount, math.MinInt64) - extractorFunc := label.GetStringValueP(application.Labels, label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc) - - if amount == math.MinInt64 || len(extractorFunc) == 0 { - return nil - } - - return &types.MaxConn{ - Amount: amount, - ExtractorFunc: extractorFunc, - } -} - -func getHealthCheck(application marathon.Application) *types.HealthCheck { - path := label.GetStringValueP(application.Labels, label.TraefikBackendHealthCheckPath, "") - if len(path) == 0 { - return nil - } - - port := label.GetIntValueP(application.Labels, label.TraefikBackendHealthCheckPort, label.DefaultBackendHealthCheckPort) - interval := label.GetStringValueP(application.Labels, label.TraefikBackendHealthCheckInterval, "") - - return &types.HealthCheck{ - Path: path, - Port: port, - Interval: interval, - } -} - -func getBuffering(application marathon.Application) *types.Buffering { - if !label.HasPrefixP(application.Labels, label.TraefikBackendBuffering) { - return nil - } - - return &types.Buffering{ - MaxRequestBodyBytes: label.GetInt64ValueP(application.Labels, label.TraefikBackendBufferingMaxRequestBodyBytes, 0), - MaxResponseBodyBytes: label.GetInt64ValueP(application.Labels, label.TraefikBackendBufferingMaxResponseBodyBytes, 0), - MemRequestBodyBytes: label.GetInt64ValueP(application.Labels, label.TraefikBackendBufferingMemRequestBodyBytes, 0), - MemResponseBodyBytes: label.GetInt64ValueP(application.Labels, label.TraefikBackendBufferingMemResponseBodyBytes, 0), - RetryExpression: label.GetStringValueP(application.Labels, label.TraefikBackendBufferingRetryExpression, ""), - } -} - -func (p *Provider) getServers(application marathon.Application, serviceName string) map[string]types.Server { +func (p *Provider) getServers(app appData) map[string]types.Server { var servers map[string]types.Server - for _, task := range application.Tasks { - host := p.getBackendServer(*task, application) + for _, task := range app.Tasks { + host := p.getBackendServer(*task, app) if len(host) == 0 { continue } @@ -474,197 +300,45 @@ func (p *Provider) getServers(application marathon.Application, serviceName stri servers = make(map[string]types.Server) } - labels := getLabels(application, serviceName) + port := getPort(*task, app) + protocol := label.GetStringValue(app.SegmentLabels, label.TraefikProtocol, label.DefaultProtocol) - port := getPort(*task, application, serviceName) - protocol := label.GetStringValue(labels, getLabelName(serviceName, label.SuffixProtocol), label.DefaultProtocol) - - serverName := provider.Normalize("server-" + task.ID + getServiceNameSuffix(serviceName)) + serverName := provider.Normalize("server-" + task.ID + getSegmentNameSuffix(app.SegmentName)) servers[serverName] = types.Server{ URL: fmt.Sprintf("%s://%s:%v", protocol, host, port), - Weight: label.GetIntValue(labels, getLabelName(serviceName, label.SuffixWeight), label.DefaultWeightInt), + Weight: label.GetIntValue(app.SegmentLabels, label.TraefikWeight, label.DefaultWeightInt), } } return servers } -func getWhiteList(application marathon.Application, serviceName string) *types.WhiteList { - labels := getLabels(application, serviceName) +func (p *Provider) getBackendServer(task marathon.Task, app appData) string { + if app.IPAddressPerTask == nil || p.ForceTaskHostname { + return task.Host + } - ranges := label.GetSliceStringValue(labels, getLabelName(serviceName, label.SuffixFrontendWhiteListSourceRange)) - if len(ranges) > 0 { - return &types.WhiteList{ - SourceRange: ranges, - UseXForwardedFor: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendWhiteListUseXForwardedFor), false), + numTaskIPAddresses := len(task.IPAddresses) + switch numTaskIPAddresses { + case 0: + log.Errorf("Missing IP address for Marathon application %s on task %s", app.ID, task.ID) + return "" + case 1: + return task.IPAddresses[0].IPAddress + default: + ipAddressIdx := label.GetIntValue(stringValueMap(app.Labels), labelIPAddressIdx, math.MinInt32) + + if ipAddressIdx == math.MinInt32 { + log.Errorf("Found %d task IP addresses but missing IP address index for Marathon application %s on task %s", + numTaskIPAddresses, app.ID, task.ID) + return "" } - } - - return nil -} - -func getRedirect(application marathon.Application, serviceName string) *types.Redirect { - labels := getLabels(application, serviceName) - - permanent := label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendRedirectPermanent), false) - - if label.Has(labels, getLabelName(serviceName, label.SuffixFrontendRedirectEntryPoint)) { - return &types.Redirect{ - EntryPoint: label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendRedirectEntryPoint), ""), - Permanent: permanent, + if ipAddressIdx < 0 || ipAddressIdx > numTaskIPAddresses { + log.Errorf("Cannot use IP address index to select from %d task IP addresses for Marathon application %s on task %s", + numTaskIPAddresses, app.ID, task.ID) + return "" } - } - if label.Has(labels, getLabelName(serviceName, label.SuffixFrontendRedirectRegex)) && - label.Has(labels, getLabelName(serviceName, label.SuffixFrontendRedirectReplacement)) { - return &types.Redirect{ - Regex: label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendRedirectRegex), ""), - Replacement: label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendRedirectReplacement), ""), - Permanent: permanent, - } - } - - return nil -} - -func getErrorPages(application marathon.Application, serviceName string) map[string]*types.ErrorPage { - labels := getLabels(application, serviceName) - prefix := getLabelName(serviceName, label.BaseFrontendErrorPage) - - if len(serviceName) > 0 { - return label.ParseErrorPages(labels, prefix, label.RegexpBaseFrontendErrorPage) - } - return label.ParseErrorPages(labels, prefix, label.RegexpFrontendErrorPage) -} - -func getRateLimit(application marathon.Application, serviceName string) *types.RateLimit { - labels := getLabels(application, serviceName) - - extractorFunc := label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendRateLimitExtractorFunc), "") - if len(extractorFunc) == 0 { - return nil - } - - limits := getRateSet(labels, serviceName) - if len(limits) == 0 { - return nil - } - - return &types.RateLimit{ - ExtractorFunc: extractorFunc, - RateSet: limits, - } -} - -func getRateSet(labels map[string]string, serviceName string) map[string]*types.Rate { - rateSetPrefix := getLabelName(serviceName, label.BaseFrontendRateLimit) - - if len(serviceName) > 0 { - return label.ParseRateSets(labels, rateSetPrefix, label.RegexpBaseFrontendRateLimit) - } - return label.ParseRateSets(labels, rateSetPrefix, label.RegexpFrontendRateLimit) -} - -func getHeaders(application marathon.Application, serviceName string) *types.Headers { - labels := getLabels(application, serviceName) - - headers := &types.Headers{ - CustomRequestHeaders: label.GetMapValue(labels, getLabelName(serviceName, label.SuffixFrontendRequestHeaders)), - CustomResponseHeaders: label.GetMapValue(labels, getLabelName(serviceName, label.SuffixFrontendResponseHeaders)), - SSLProxyHeaders: label.GetMapValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersSSLProxyHeaders)), - AllowedHosts: label.GetSliceStringValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersAllowedHosts)), - HostsProxyHeaders: label.GetSliceStringValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersHostsProxyHeaders)), - STSSeconds: label.GetInt64Value(labels, getLabelName(serviceName, label.SuffixFrontendHeadersSTSSeconds), 0), - SSLRedirect: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersSSLRedirect), false), - SSLTemporaryRedirect: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersSSLTemporaryRedirect), false), - STSIncludeSubdomains: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersSTSIncludeSubdomains), false), - STSPreload: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersSTSPreload), false), - ForceSTSHeader: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersForceSTSHeader), false), - FrameDeny: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersFrameDeny), false), - ContentTypeNosniff: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersContentTypeNosniff), false), - BrowserXSSFilter: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersBrowserXSSFilter), false), - IsDevelopment: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersIsDevelopment), false), - SSLHost: label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersSSLHost), ""), - CustomFrameOptionsValue: label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersCustomFrameOptionsValue), ""), - ContentSecurityPolicy: label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersContentSecurityPolicy), ""), - PublicKey: label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersPublicKey), ""), - ReferrerPolicy: label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersReferrerPolicy), ""), - CustomBrowserXSSValue: label.GetStringValue(labels, getLabelName(serviceName, label.SuffixFrontendHeadersCustomBrowserXSSValue), ""), - } - - if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() { - return nil - } - - return headers -} - -// Label functions - -func getLabels(application marathon.Application, serviceName string) map[string]string { - if len(serviceName) > 0 { - return label.ExtractServicePropertiesP(application.Labels)[serviceName] - } - - if application.Labels != nil { - return *application.Labels - } - - return make(map[string]string) -} - -func getLabelName(serviceName string, suffix string) string { - if len(serviceName) != 0 { - return suffix - } - return label.Prefix + suffix -} - -func hasFunc(labelName string) func(application marathon.Application) bool { - return func(application marathon.Application) bool { - return label.HasP(application.Labels, labelName) - } -} - -func getFuncStringService(labelName string, defaultValue string) func(application marathon.Application, serviceName string) string { - return func(application marathon.Application, serviceName string) string { - labels := getLabels(application, serviceName) - lbName := getLabelName(serviceName, labelName) - return label.GetStringValue(labels, lbName, defaultValue) - } -} - -func getFuncBoolService(labelName string, defaultValue bool) func(application marathon.Application, serviceName string) bool { - return func(application marathon.Application, serviceName string) bool { - labels := getLabels(application, serviceName) - lbName := getLabelName(serviceName, labelName) - return label.GetBoolValue(labels, lbName, defaultValue) - } -} - -func getFuncIntService(labelName string, defaultValue int) func(application marathon.Application, serviceName string) int { - return func(application marathon.Application, serviceName string) int { - labels := getLabels(application, serviceName) - lbName := getLabelName(serviceName, labelName) - return label.GetIntValue(labels, lbName, defaultValue) - } -} - -func getFuncSliceStringService(labelName string) func(application marathon.Application, serviceName string) []string { - return func(application marathon.Application, serviceName string) []string { - labels := getLabels(application, serviceName) - return label.GetSliceStringValue(labels, getLabelName(serviceName, labelName)) - } -} - -func getFuncString(labelName string, defaultValue string) func(application marathon.Application) string { - return func(application marathon.Application) string { - return label.GetStringValueP(application.Labels, labelName, defaultValue) - } -} - -func getFuncInt64(labelName string, defaultValue int64) func(application marathon.Application) int64 { - return func(application marathon.Application) int64 { - return label.GetInt64ValueP(application.Labels, labelName, defaultValue) + return task.IPAddresses[ipAddressIdx].IPAddress } } diff --git a/provider/marathon/config_root.go b/provider/marathon/config_root.go new file mode 100644 index 000000000..7a5b66e89 --- /dev/null +++ b/provider/marathon/config_root.go @@ -0,0 +1,13 @@ +package marathon + +import ( + "github.com/containous/traefik/types" + "github.com/gambol99/go-marathon" +) + +func (p *Provider) buildConfiguration(applications *marathon.Applications) *types.Configuration { + if p.TemplateVersion == 1 { + return p.buildConfigurationV1(applications) + } + return p.buildConfigurationV2(applications) +} diff --git a/provider/marathon/config_test.go b/provider/marathon/config_test.go index 328f7ce02..ef92573d9 100644 --- a/provider/marathon/config_test.go +++ b/provider/marathon/config_test.go @@ -1,43 +1,25 @@ package marathon import ( - "errors" "fmt" "testing" "time" "github.com/containous/flaeg" "github.com/containous/traefik/provider/label" - "github.com/containous/traefik/provider/marathon/mocks" "github.com/containous/traefik/types" "github.com/gambol99/go-marathon" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" ) -type fakeClient struct { - mocks.Marathon -} - -func newFakeClient(applicationsError bool, applications marathon.Applications) *fakeClient { - // create an instance of our test object - fakeClient := new(fakeClient) - if applicationsError { - fakeClient.On("Applications", mock.Anything).Return(nil, errors.New("fake Marathon server error")) - } else { - fakeClient.On("Applications", mock.Anything).Return(&applications, nil) - } - return fakeClient -} - -func TestBuildConfigurationAPIErrors(t *testing.T) { +func TestGetConfigurationAPIErrors(t *testing.T) { fakeClient := newFakeClient(true, marathon.Applications{}) p := &Provider{ marathonClient: fakeClient, } - actualConfig := p.buildConfiguration() + actualConfig := p.getConfiguration() fakeClient.AssertExpectations(t) if actualConfig != nil { @@ -45,18 +27,19 @@ func TestBuildConfigurationAPIErrors(t *testing.T) { } } -func TestBuildConfigurationNonAPIErrors(t *testing.T) { +func TestBuildConfiguration(t *testing.T) { testCases := []struct { desc string application marathon.Application - task marathon.Task expectedFrontends map[string]*types.Frontend expectedBackends map[string]*types.Backend }{ { - desc: "simple application", - application: application(appPorts(80)), - task: localhostTask(taskPorts(80)), + desc: "simple application", + application: application( + appPorts(80), + withTasks(localhostTask(taskPorts(80))), + ), expectedFrontends: map[string]*types.Frontend{ "frontend-app": { Backend: "backend-app", @@ -83,36 +66,22 @@ func TestBuildConfigurationNonAPIErrors(t *testing.T) { }, }, { - desc: "filtered task", - application: application(appPorts(80)), - task: localhostTask( - taskPorts(80), - state(taskStateStaging), + desc: "filtered task", + application: application( + appPorts(80), + withTasks(localhostTask(taskPorts(80), state(taskStateStaging))), ), - expectedFrontends: map[string]*types.Frontend{ - "frontend-app": { - Backend: "backend-app", - Routes: map[string]types.Route{ - "route-host-app": { - Rule: "Host:app.docker.localhost", - }, - }, - PassHostHeader: true, - BasicAuth: []string{}, - EntryPoints: []string{}, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-app": {}, - }, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, }, { desc: "max connection extractor function label only", application: application( appPorts(80), + withTasks(localhostTask(taskPorts(80))), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), ), - task: localhostTask(taskPorts(80)), expectedFrontends: map[string]*types.Frontend{ "frontend-app": { Backend: "backend-app", @@ -142,9 +111,7 @@ func TestBuildConfigurationNonAPIErrors(t *testing.T) { desc: "multiple ports", application: application( appPorts(80, 81), - ), - task: localhostTask( - taskPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), ), expectedFrontends: map[string]*types.Frontend{ "frontend-app": { @@ -174,6 +141,8 @@ func TestBuildConfigurationNonAPIErrors(t *testing.T) { desc: "with all labels", application: application( appPorts(80), + withTasks(task(host("127.0.0.1"), taskPorts(80))), + withLabel(label.TraefikPort, "666"), withLabel(label.TraefikProtocol, "https"), withLabel(label.TraefikWeight, "12"), @@ -246,10 +215,6 @@ func TestBuildConfigurationNonAPIErrors(t *testing.T) { withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), ), - task: task( - host("127.0.0.1"), - taskPorts(80), - ), expectedFrontends: map[string]*types.Frontend{ "frontend-app": { EntryPoints: []string{ @@ -396,23 +361,20 @@ func TestBuildConfigurationNonAPIErrors(t *testing.T) { t.Parallel() test.application.ID = "/app" - test.task.ID = "task" - if test.task.State == "" { - test.task.State = "TASK_RUNNING" - } - test.application.Tasks = []*marathon.Task{&test.task} - fakeClient := newFakeClient(false, - marathon.Applications{Apps: []marathon.Application{test.application}}) + for _, task := range test.application.Tasks { + task.ID = "task" + if task.State == "" { + task.State = "TASK_RUNNING" + } + } p := &Provider{ Domain: "docker.localhost", ExposedByDefault: true, - marathonClient: fakeClient, } - actualConfig := p.buildConfiguration() - fakeClient.AssertExpectations(t) + actualConfig := p.buildConfigurationV2(withApplications(test.application)) assert.NotNil(t, actualConfig) assert.Equal(t, test.expectedBackends, actualConfig.Backends) @@ -421,11 +383,10 @@ func TestBuildConfigurationNonAPIErrors(t *testing.T) { } } -func TestBuildConfigurationServicesNonAPIErrors(t *testing.T) { +func TestBuildConfigurationServices(t *testing.T) { testCases := []struct { desc string application marathon.Application - task marathon.Task expectedFrontends map[string]*types.Frontend expectedBackends map[string]*types.Backend }{ @@ -433,17 +394,16 @@ func TestBuildConfigurationServicesNonAPIErrors(t *testing.T) { desc: "multiple ports with services", application: application( appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel(label.TraefikBackendMaxConnAmount, "1000"), withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), withServiceLabel(label.TraefikPort, "80", "web"), withServiceLabel(label.TraefikPort, "81", "admin"), - withLabel("traefik..port", "82"), // This should be ignored, as it fails to match the servicesPropertiesRegexp regex. + withLabel("traefik..port", "82"), // This should be ignored, as it fails to match the segmentPropertiesRegexp regex. withServiceLabel(label.TraefikFrontendRule, "Host:web.app.docker.localhost", "web"), withServiceLabel(label.TraefikFrontendRule, "Host:admin.app.docker.localhost", "admin"), ), - task: localhostTask( - taskPorts(80, 81), - ), expectedFrontends: map[string]*types.Frontend{ "frontend-app-service-web": { Backend: "backend-app-service-web", @@ -499,6 +459,7 @@ func TestBuildConfigurationServicesNonAPIErrors(t *testing.T) { desc: "when all labels are set", application: application( appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), // withLabel(label.TraefikBackend, "foobar"), @@ -572,9 +533,6 @@ func TestBuildConfigurationServicesNonAPIErrors(t *testing.T) { withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), ), - task: localhostTask( - taskPorts(80, 81), - ), expectedFrontends: map[string]*types.Frontend{ "frontend-app-service-containous": { EntryPoints: []string{ @@ -721,23 +679,20 @@ func TestBuildConfigurationServicesNonAPIErrors(t *testing.T) { t.Parallel() test.application.ID = "/app" - test.task.ID = "task" - if test.task.State == "" { - test.task.State = "TASK_RUNNING" - } - test.application.Tasks = []*marathon.Task{&test.task} - fakeClient := newFakeClient(false, - marathon.Applications{Apps: []marathon.Application{test.application}}) + for _, task := range test.application.Tasks { + task.ID = "task" + if task.State == "" { + task.State = "TASK_RUNNING" + } + } p := &Provider{ Domain: "docker.localhost", ExposedByDefault: true, - marathonClient: fakeClient, } - actualConfig := p.buildConfiguration() - fakeClient.AssertExpectations(t) + actualConfig := p.buildConfigurationV2(withApplications(test.application)) assert.NotNil(t, actualConfig) assert.Equal(t, test.expectedBackends, actualConfig.Backends) @@ -1116,7 +1071,7 @@ func TestGetPort(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := getPort(test.task, test.application, test.serviceName) + actual := getPortV1(test.task, test.application, test.serviceName) assert.Equal(t, test.expected, actual) }) @@ -1180,14 +1135,14 @@ func TestGetFrontendRule(t *testing.T) { MarathonLBCompatibility: test.marathonLBCompatibility, } - actual := p.getFrontendRule(test.application, test.serviceName) + actual := p.getFrontendRuleV1(test.application, test.serviceName) assert.Equal(t, test.expected, actual) }) } } -func TestGetBackend(t *testing.T) { +func TestGetBackendName(t *testing.T) { testCases := []struct { desc string application marathon.Application @@ -1219,350 +1174,7 @@ func TestGetBackend(t *testing.T) { p := &Provider{} - actual := p.getBackend(test.application, test.serviceName) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetBackendServer(t *testing.T) { - host := "host" - testCases := []struct { - desc string - application marathon.Application - task marathon.Task - forceTaskHostname bool - expectedServer string - }{ - { - desc: "application without IP-per-task", - application: application(), - expectedServer: host, - }, - { - desc: "task hostname override", - application: application(ipAddrPerTask(8000)), - forceTaskHostname: true, - expectedServer: host, - }, - { - desc: "task IP address missing", - application: application(ipAddrPerTask(8000)), - task: task(), - expectedServer: "", - }, - { - desc: "single task IP address", - application: application(ipAddrPerTask(8000)), - task: task(ipAddresses("1.1.1.1")), - expectedServer: "1.1.1.1", - }, - { - desc: "multiple task IP addresses without index label", - application: application(ipAddrPerTask(8000)), - task: task(ipAddresses("1.1.1.1", "2.2.2.2")), - expectedServer: "", - }, - { - desc: "multiple task IP addresses with invalid index label", - application: application( - withLabel("traefik.ipAddressIdx", "invalid"), - ipAddrPerTask(8000), - ), - task: task(ipAddresses("1.1.1.1", "2.2.2.2")), - expectedServer: "", - }, - { - desc: "multiple task IP addresses with valid index label", - application: application( - withLabel("traefik.ipAddressIdx", "1"), - ipAddrPerTask(8000), - ), - task: task(ipAddresses("1.1.1.1", "2.2.2.2")), - expectedServer: "2.2.2.2", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - p := &Provider{ForceTaskHostname: test.forceTaskHostname} - test.task.Host = host - - actualServer := p.getBackendServer(test.task, test.application) - - assert.Equal(t, test.expectedServer, actualServer) - }) - } -} - -func TestGetSticky(t *testing.T) { - testCases := []struct { - desc string - application marathon.Application - expected bool - }{ - { - desc: "label missing", - application: application(), - expected: false, - }, - { - desc: "label existing", - application: application(withLabel(label.TraefikBackendLoadBalancerSticky, "true")), - expected: true, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - actual := getSticky(test.application) - if actual != test.expected { - t.Errorf("actual %v, expected %v", actual, test.expected) - } - }) - } -} - -func TestGetCircuitBreaker(t *testing.T) { - testCases := []struct { - desc string - application marathon.Application - expected *types.CircuitBreaker - }{ - { - desc: "should return nil when no CB label", - application: application(appPorts(80)), - expected: nil, - }, - { - desc: "should return a struct when CB label is set", - application: application( - appPorts(80), - withLabel(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() - - actual := getCircuitBreaker(test.application) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetLoadBalancer(t *testing.T) { - testCases := []struct { - desc string - application marathon.Application - expected *types.LoadBalancer - }{ - { - desc: "should return nil when no LB labels", - application: application(appPorts(80)), - expected: nil, - }, - { - desc: "should return a struct when labels are set", - application: application( - appPorts(80), - withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), - withLabel(label.TraefikBackendLoadBalancerSticky, "true"), - withLabel(label.TraefikBackendLoadBalancerStickiness, "true"), - withLabel(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", - application: application( - appPorts(80), - withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), - withLabel(label.TraefikBackendLoadBalancerSticky, "true"), - withLabel(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() - - actual := getLoadBalancer(test.application) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetMaxConn(t *testing.T) { - testCases := []struct { - desc string - application marathon.Application - expected *types.MaxConn - }{ - { - desc: "should return nil when no max conn labels", - application: application(appPorts(80)), - expected: nil, - }, - { - desc: "should return nil when no amount label", - application: application( - appPorts(80), - withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), - ), - expected: nil, - }, - { - desc: "should return default when no empty extractorFunc label", - application: application( - appPorts(80), - withLabel(label.TraefikBackendMaxConnExtractorFunc, ""), - withLabel(label.TraefikBackendMaxConnAmount, "666"), - ), - expected: &types.MaxConn{ - ExtractorFunc: "request.host", - Amount: 666, - }, - }, - { - desc: "should return a struct when max conn labels are set", - application: application( - appPorts(80), - withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), - withLabel(label.TraefikBackendMaxConnAmount, "666"), - ), - expected: &types.MaxConn{ - ExtractorFunc: "client.ip", - Amount: 666, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getMaxConn(test.application) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetHealthCheck(t *testing.T) { - testCases := []struct { - desc string - application marathon.Application - expected *types.HealthCheck - }{ - { - desc: "should return nil when no health check labels", - application: application(appPorts(80)), - expected: nil, - }, - { - desc: "should return nil when no health check Path label", - application: application( - appPorts(80), - withLabel(label.TraefikBackendHealthCheckPort, "80"), - withLabel(label.TraefikBackendHealthCheckInterval, "6"), - ), - expected: nil, - }, - { - desc: "should return a struct when health check labels are set", - - application: application( - appPorts(80), - withLabel(label.TraefikBackendHealthCheckPath, "/health"), - withLabel(label.TraefikBackendHealthCheckPort, "80"), - withLabel(label.TraefikBackendHealthCheckInterval, "6"), - ), - expected: &types.HealthCheck{ - Path: "/health", - Port: 80, - Interval: "6", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getHealthCheck(test.application) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetBuffering(t *testing.T) { - testCases := []struct { - desc string - application marathon.Application - expected *types.Buffering - }{ - { - desc: "should return nil when no buffering labels", - application: application(appPorts(80)), - expected: nil, - }, - { - desc: "should return a struct when buffering labels are set", - - application: application( - withLabel(label.TraefikBackendBufferingMaxResponseBodyBytes, "10485760"), - withLabel(label.TraefikBackendBufferingMemResponseBodyBytes, "2097152"), - withLabel(label.TraefikBackendBufferingMaxRequestBodyBytes, "10485760"), - withLabel(label.TraefikBackendBufferingMemRequestBodyBytes, "2097152"), - withLabel(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() - - actual := getBuffering(test.application) + actual := p.getBackendNameV1(test.application, test.serviceName) assert.Equal(t, test.expected, actual) }) @@ -1624,568 +1236,7 @@ func TestGetServers(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := p.getServers(test.application, test.serviceName) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestWhiteList(t *testing.T) { - testCases := []struct { - desc string - application marathon.Application - serviceName string - expected *types.WhiteList - }{ - { - desc: "should return nil when no white list labels", - application: application( - appPorts(80), - ), - expected: nil, - }, - { - desc: "should return a struct when only range", - application: application( - appPorts(80), - withLabel(label.TraefikFrontendWhiteListSourceRange, "10.10.10.10"), - ), - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: false, - }, - }, - { - desc: "should return a struct when range and UseXForwardedFor", - application: application( - appPorts(80), - withLabel(label.TraefikFrontendWhiteListSourceRange, "10.10.10.10"), - withLabel(label.TraefikFrontendWhiteListUseXForwardedFor, "true"), - ), - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: true, - }, - }, - { - desc: "should return nil when only UseXForwardedFor", - application: application( - appPorts(80), - withLabel(label.TraefikFrontendWhiteListUseXForwardedFor, "true"), - ), - expected: nil, - }, - // Service - { - desc: "should return a struct when only range on service", - application: application( - appPorts(80), - withLabel(label.Prefix+"containous."+label.SuffixFrontendWhiteListSourceRange, "10.10.10.10"), - ), - serviceName: "containous", - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: false, - }, - }, - { - desc: "should return a struct when range and UseXForwardedFor on service", - application: application( - appPorts(80), - withLabel(label.Prefix+"containous."+label.SuffixFrontendWhiteListSourceRange, "10.10.10.10"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendWhiteListUseXForwardedFor, "true"), - ), - serviceName: "containous", - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: true, - }, - }, - { - desc: "should return nil when only UseXForwardedFor on service", - application: application( - appPorts(80), - withLabel(label.Prefix+"containous."+label.SuffixFrontendWhiteListUseXForwardedFor, "true"), - ), - serviceName: "containous", - expected: nil, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getWhiteList(test.application, test.serviceName) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetRedirect(t *testing.T) { - testCases := []struct { - desc string - application marathon.Application - serviceName string - expected *types.Redirect - }{ - { - desc: "should return nil when no redirect labels", - application: application(appPorts(80)), - expected: nil, - }, - { - desc: "should use only entry point tag when mix regex redirect and entry point redirect", - application: application( - appPorts(80), - withLabel(label.TraefikFrontendRedirectEntryPoint, "https"), - withLabel(label.TraefikFrontendRedirectRegex, "(.*)"), - withLabel(label.TraefikFrontendRedirectReplacement, "$1"), - ), - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when entry point redirect label", - application: application( - appPorts(80), - withLabel(label.TraefikFrontendRedirectEntryPoint, "https"), - ), - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when entry point redirect label (permanent)", - application: application( - appPorts(80), - withLabel(label.TraefikFrontendRedirectEntryPoint, "https"), - withLabel(label.TraefikFrontendRedirectPermanent, "true"), - ), - expected: &types.Redirect{ - EntryPoint: "https", - Permanent: true, - }, - }, - { - desc: "should return a struct when regex redirect labels", - application: application( - appPorts(80), - withLabel(label.TraefikFrontendRedirectRegex, "(.*)"), - withLabel(label.TraefikFrontendRedirectReplacement, "$1"), - ), - expected: &types.Redirect{ - Regex: "(.*)", - Replacement: "$1", - }, - }, - // Service - { - desc: "should use only entry point tag when mix regex redirect and entry point redirect on service", - application: application( - appPorts(80), - withLabel(label.Prefix+"containous."+label.SuffixFrontendRedirectEntryPoint, "https"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendRedirectRegex, "(.*)"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendRedirectReplacement, "$1"), - ), - serviceName: "containous", - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when entry point redirect label on service", - application: application( - appPorts(80), - withLabel(label.Prefix+"containous."+label.SuffixFrontendRedirectEntryPoint, "https"), - ), - serviceName: "containous", - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when regex redirect labels on service", - application: application( - appPorts(80), - withLabel(label.Prefix+"containous."+label.SuffixFrontendRedirectRegex, "(.*)"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendRedirectReplacement, "$1"), - ), - serviceName: "containous", - expected: &types.Redirect{ - Regex: "(.*)", - Replacement: "$1", - }, - }, - { - desc: "should return a struct when regex redirect labels on service (permanent)", - application: application( - appPorts(80), - withLabel(label.Prefix+"containous."+label.SuffixFrontendRedirectRegex, "(.*)"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendRedirectReplacement, "$1"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendRedirectPermanent, "true"), - ), - serviceName: "containous", - expected: &types.Redirect{ - Regex: "(.*)", - Replacement: "$1", - Permanent: true, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getRedirect(test.application, test.serviceName) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetErrorPages(t *testing.T) { - testCases := []struct { - desc string - application marathon.Application - serviceName string - expected map[string]*types.ErrorPage - }{ - { - desc: "with 2 error pages", - application: application( - withLabel(label.Prefix+label.BaseFrontendErrorPage+"goo."+label.SuffixErrorPageBackend, "bar1"), - withLabel(label.Prefix+label.BaseFrontendErrorPage+"goo."+label.SuffixErrorPageStatus, "bar2"), - withLabel(label.Prefix+label.BaseFrontendErrorPage+"goo."+label.SuffixErrorPageQuery, "bar3"), - withLabel(label.Prefix+label.BaseFrontendErrorPage+"hoo."+label.SuffixErrorPageBackend, "bar4"), - withLabel(label.Prefix+label.BaseFrontendErrorPage+"hoo."+label.SuffixErrorPageStatus, "bar5"), - withLabel(label.Prefix+label.BaseFrontendErrorPage+"hoo."+label.SuffixErrorPageQuery, "bar6"), - ), - expected: map[string]*types.ErrorPage{ - "goo": { - Backend: "bar1", - Query: "bar3", - Status: []string{"bar2"}, - }, - "hoo": { - Backend: "bar4", - Query: "bar6", - Status: []string{"bar5"}, - }, - }, - }, - { - desc: "with 2 error pages on service", - application: application( - withLabel(label.Prefix+"foo."+label.BaseFrontendErrorPage+"goo."+label.SuffixErrorPageBackend, "bar1"), - withLabel(label.Prefix+"foo."+label.BaseFrontendErrorPage+"goo."+label.SuffixErrorPageStatus, "bar2"), - withLabel(label.Prefix+"foo."+label.BaseFrontendErrorPage+"goo."+label.SuffixErrorPageQuery, "bar3"), - withLabel(label.Prefix+"foo."+label.BaseFrontendErrorPage+"hoo."+label.SuffixErrorPageBackend, "bar4"), - withLabel(label.Prefix+"foo."+label.BaseFrontendErrorPage+"hoo."+label.SuffixErrorPageStatus, "bar5"), - withLabel(label.Prefix+"foo."+label.BaseFrontendErrorPage+"hoo."+label.SuffixErrorPageQuery, "bar6"), - ), - serviceName: "foo", - expected: map[string]*types.ErrorPage{ - "goo": { - Backend: "bar1", - Query: "bar3", - Status: []string{"bar2"}, - }, - "hoo": { - Backend: "bar4", - Query: "bar6", - Status: []string{"bar5"}, - }, - }, - }, - { - desc: "with 1 error page on service but not the same service", - application: application( - withLabel(label.Prefix+"foo."+label.BaseFrontendErrorPage+"goo."+label.SuffixErrorPageBackend, "bar1"), - withLabel(label.Prefix+"foo."+label.BaseFrontendErrorPage+"goo."+label.SuffixErrorPageStatus, "bar2"), - withLabel(label.Prefix+"foo."+label.BaseFrontendErrorPage+"goo."+label.SuffixErrorPageQuery, "bar3"), - ), - serviceName: "foofoo", - expected: nil, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - pages := getErrorPages(test.application, test.serviceName) - - assert.EqualValues(t, test.expected, pages) - }) - } -} - -func TestGetRateLimit(t *testing.T) { - testCases := []struct { - desc string - application marathon.Application - serviceName string - expected *types.RateLimit - }{ - { - desc: "should return nil when no rate limit labels", - application: application(appPorts(80)), - expected: nil, - }, - { - desc: "should return a struct when rate limit labels are defined", - application: application( - appPorts(80), - withLabel(label.TraefikFrontendRateLimitExtractorFunc, "client.ip"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), - ), - expected: &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", - application: application( - appPorts(80), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), - ), - expected: nil, - }, - // Service - { - desc: "should return a struct when rate limit labels are defined on service", - application: application( - appPorts(80), - withLabel(label.Prefix+"containous."+label.SuffixFrontendRateLimitExtractorFunc, "client.ip"), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), - ), - serviceName: "containous", - expected: &types.RateLimit{ - ExtractorFunc: "client.ip", - RateSet: map[string]*types.Rate{ - "foo": { - Period: flaeg.Duration(6 * time.Second), - Average: 12, - Burst: 18, - }, - "bar": { - Period: flaeg.Duration(3 * time.Second), - Average: 6, - Burst: 9, - }, - }, - }, - }, - { - desc: "should return nil when ExtractorFunc is missing on service", - application: application( - appPorts(80), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), - ), - serviceName: "containous", - expected: nil, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getRateLimit(test.application, test.serviceName) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetHeaders(t *testing.T) { - testCases := []struct { - desc string - application marathon.Application - serviceName string - expected *types.Headers - }{ - { - desc: "should return nil when no custom headers options are set", - application: application(appPorts(80)), - expected: nil, - }, - { - desc: "should return a struct when all custom headers options are set", - application: application( - appPorts(80), - withLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - withLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - withLabel(label.TraefikFrontendSSLProxyHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - withLabel(label.TraefikFrontendAllowedHosts, "foo,bar,bor"), - withLabel(label.TraefikFrontendHostsProxyHeaders, "foo,bar,bor"), - withLabel(label.TraefikFrontendSSLHost, "foo"), - withLabel(label.TraefikFrontendCustomFrameOptionsValue, "foo"), - withLabel(label.TraefikFrontendContentSecurityPolicy, "foo"), - withLabel(label.TraefikFrontendPublicKey, "foo"), - withLabel(label.TraefikFrontendReferrerPolicy, "foo"), - withLabel(label.TraefikFrontendCustomBrowserXSSValue, "foo"), - withLabel(label.TraefikFrontendSTSSeconds, "666"), - withLabel(label.TraefikFrontendSSLRedirect, "true"), - withLabel(label.TraefikFrontendSSLTemporaryRedirect, "true"), - withLabel(label.TraefikFrontendSTSIncludeSubdomains, "true"), - withLabel(label.TraefikFrontendSTSPreload, "true"), - withLabel(label.TraefikFrontendForceSTSHeader, "true"), - withLabel(label.TraefikFrontendFrameDeny, "true"), - withLabel(label.TraefikFrontendContentTypeNosniff, "true"), - withLabel(label.TraefikFrontendBrowserXSSFilter, "true"), - withLabel(label.TraefikFrontendIsDevelopment, "true"), - ), - expected: &types.Headers{ - CustomRequestHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - CustomResponseHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - SSLProxyHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - AllowedHosts: []string{"foo", "bar", "bor"}, - HostsProxyHeaders: []string{"foo", "bar", "bor"}, - SSLHost: "foo", - CustomFrameOptionsValue: "foo", - ContentSecurityPolicy: "foo", - PublicKey: "foo", - ReferrerPolicy: "foo", - CustomBrowserXSSValue: "foo", - STSSeconds: 666, - SSLRedirect: true, - SSLTemporaryRedirect: true, - STSIncludeSubdomains: true, - STSPreload: true, - ForceSTSHeader: true, - FrameDeny: true, - ContentTypeNosniff: true, - BrowserXSSFilter: true, - IsDevelopment: true, - }, - }, - // Service - { - desc: "should return a struct when all custom headers options are set on service", - application: application( - appPorts(80), - withLabel(label.Prefix+"containous."+label.SuffixFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersSSLProxyHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersAllowedHosts, "foo,bar,bor"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersHostsProxyHeaders, "foo,bar,bor"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersSSLHost, "foo"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersCustomFrameOptionsValue, "foo"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersContentSecurityPolicy, "foo"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersPublicKey, "foo"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersReferrerPolicy, "foo"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersCustomBrowserXSSValue, "foo"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersSTSSeconds, "666"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersSSLRedirect, "true"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersSSLTemporaryRedirect, "true"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersSTSIncludeSubdomains, "true"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersSTSPreload, "true"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersForceSTSHeader, "true"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersFrameDeny, "true"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersContentTypeNosniff, "true"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersBrowserXSSFilter, "true"), - withLabel(label.Prefix+"containous."+label.SuffixFrontendHeadersIsDevelopment, "true"), - ), - serviceName: "containous", - expected: &types.Headers{ - CustomRequestHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - CustomResponseHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - SSLProxyHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - AllowedHosts: []string{"foo", "bar", "bor"}, - HostsProxyHeaders: []string{"foo", "bar", "bor"}, - SSLHost: "foo", - CustomFrameOptionsValue: "foo", - ContentSecurityPolicy: "foo", - PublicKey: "foo", - ReferrerPolicy: "foo", - 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() - - actual := getHeaders(test.application, test.serviceName) + actual := p.getServersV1(test.application, test.serviceName) assert.Equal(t, test.expected, actual) }) diff --git a/provider/marathon/convert_types.go b/provider/marathon/convert_types.go new file mode 100644 index 000000000..fb2895b19 --- /dev/null +++ b/provider/marathon/convert_types.go @@ -0,0 +1,8 @@ +package marathon + +func stringValueMap(mp *map[string]string) map[string]string { + if mp != nil { + return *mp + } + return make(map[string]string) +} diff --git a/provider/marathon/deprecated_config.go b/provider/marathon/deprecated_config.go new file mode 100644 index 000000000..c63962eb6 --- /dev/null +++ b/provider/marathon/deprecated_config.go @@ -0,0 +1,425 @@ +package marathon + +import ( + "errors" + "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/gambol99/go-marathon" +) + +func (p *Provider) buildConfigurationV1(applications *marathon.Applications) *types.Configuration { + var MarathonFuncMap = template.FuncMap{ + "getBackend": p.getBackendNameV1, + "getDomain": getFuncStringServiceV1(label.SuffixDomain, p.Domain), // see https://github.com/containous/traefik/pull/1693 + "getSubDomain": p.getSubDomain, // see https://github.com/containous/traefik/pull/1693 + + // Backend functions + "getBackendServer": p.getBackendServerV1, + "getPort": getPortV1, + "getServers": p.getServersV1, + + "getWeight": getFuncIntServiceV1(label.SuffixWeight, label.DefaultWeightInt), + "getProtocol": getFuncStringServiceV1(label.SuffixProtocol, label.DefaultProtocol), + "hasCircuitBreakerLabels": hasFuncV1(label.TraefikBackendCircuitBreakerExpression), + "getCircuitBreakerExpression": getFuncStringV1(label.TraefikBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression), + "hasLoadBalancerLabels": hasLoadBalancerLabelsV1, + "getLoadBalancerMethod": getFuncStringV1(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), + "getSticky": getStickyV1, + "hasStickinessLabel": hasFuncV1(label.TraefikBackendLoadBalancerStickiness), + "getStickinessCookieName": getFuncStringV1(label.TraefikBackendLoadBalancerStickinessCookieName, ""), + "hasMaxConnLabels": hasMaxConnLabelsV1, + "getMaxConnExtractorFunc": getFuncStringV1(label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc), + "getMaxConnAmount": getFuncInt64V1(label.TraefikBackendMaxConnAmount, math.MaxInt64), + "hasHealthCheckLabels": hasFuncV1(label.TraefikBackendHealthCheckPath), + "getHealthCheckPath": getFuncStringV1(label.TraefikBackendHealthCheckPath, ""), + "getHealthCheckInterval": getFuncStringV1(label.TraefikBackendHealthCheckInterval, ""), + + // Frontend functions + "getServiceNames": getServiceNamesV1, + "getServiceNameSuffix": getSegmentNameSuffix, + "getPassHostHeader": getFuncBoolServiceV1(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getPassTLSCert": getFuncBoolServiceV1(label.SuffixFrontendPassTLSCert, label.DefaultPassTLSCert), + "getPriority": getFuncIntServiceV1(label.SuffixFrontendPriority, label.DefaultFrontendPriorityInt), + "getEntryPoints": getFuncSliceStringServiceV1(label.SuffixFrontendEntryPoints), + "getFrontendRule": p.getFrontendRuleV1, + "getFrontendName": p.getFrontendNameV1, + "getBasicAuth": getFuncSliceStringServiceV1(label.SuffixFrontendAuthBasic), + "getWhitelistSourceRange": getFuncSliceStringServiceV1(label.SuffixFrontendWhitelistSourceRange), + "getWhiteList": getWhiteListV1, + } + + filteredApps := fun.Filter(p.applicationFilter, applications.Apps).([]marathon.Application) + for i, app := range filteredApps { + filteredApps[i].Tasks = fun.Filter(func(task *marathon.Task) bool { + filtered := p.taskFilter(*task, app) + if filtered { + logIllegalServicesV1(*task, app) + } + return filtered + }, app.Tasks).([]*marathon.Task) + } + + templateObjects := struct { + Applications []marathon.Application + Domain string + }{ + Applications: filteredApps, + Domain: p.Domain, + } + + configuration, err := p.GetConfiguration("templates/marathon-v1.tmpl", MarathonFuncMap, templateObjects) + if err != nil { + log.Errorf("Failed to render Marathon configuration template: %v", err) + } + return configuration +} + +// logIllegalServicesV1 logs illegal service configurations. +// While we cannot filter on the service level, they will eventually get +// rejected once the server configuration is rendered. +// Deprecated +func logIllegalServicesV1(task marathon.Task, app marathon.Application) { + for _, serviceName := range getServiceNamesV1(app) { + // Check for illegal/missing ports. + if _, err := processPortsV1(app, task, serviceName); err != nil { + log.Warnf("%s has an illegal configuration: no proper port available", identifierV1(app, task, serviceName)) + continue + } + + // Check for illegal port label combinations. + labels := getLabelsV1(app, serviceName) + hasPortLabel := label.Has(labels, getLabelNameV1(serviceName, label.SuffixPort)) + hasPortIndexLabel := label.Has(labels, getLabelNameV1(serviceName, label.SuffixPortIndex)) + if hasPortLabel && hasPortIndexLabel { + log.Warnf("%s has both port and port index specified; port will take precedence", identifierV1(app, task, serviceName)) + } + } +} + +// Deprecated +func (p *Provider) getBackendNameV1(application marathon.Application, serviceName string) string { + labels := getLabelsV1(application, serviceName) + lblBackend := getLabelNameV1(serviceName, label.SuffixBackend) + value := label.GetStringValue(labels, lblBackend, "") + if len(value) > 0 { + return provider.Normalize("backend" + value) + } + return provider.Normalize("backend" + application.ID + getSegmentNameSuffix(serviceName)) +} + +// Deprecated +func (p *Provider) getFrontendNameV1(application marathon.Application, serviceName string) string { + return provider.Normalize("frontend" + application.ID + getSegmentNameSuffix(serviceName)) +} + +// getFrontendRuleV1 returns the frontend rule for the specified application, using +// its label. If service is provided, it will look for serviceName label before generic one. +// It returns a default one (Host) if the label is not present. +// Deprecated +func (p *Provider) getFrontendRuleV1(application marathon.Application, serviceName string) string { + labels := getLabelsV1(application, serviceName) + lblFrontendRule := getLabelNameV1(serviceName, label.SuffixFrontendRule) + if value := label.GetStringValue(labels, lblFrontendRule, ""); len(value) > 0 { + return value + } + + if p.MarathonLBCompatibility { + if value := label.GetStringValue(stringValueMap(application.Labels), labelLbCompatibility, ""); len(value) > 0 { + return "Host:" + value + } + } + + if len(serviceName) > 0 { + return "Host:" + strings.ToLower(provider.Normalize(serviceName)) + "." + p.getSubDomain(application.ID) + "." + p.Domain + } + return "Host:" + p.getSubDomain(application.ID) + "." + p.Domain +} + +// Deprecated +func (p *Provider) getBackendServerV1(task marathon.Task, application marathon.Application) string { + if application.IPAddressPerTask == nil || p.ForceTaskHostname { + return task.Host + } + + numTaskIPAddresses := len(task.IPAddresses) + switch numTaskIPAddresses { + case 0: + log.Errorf("Missing IP address for Marathon application %s on task %s", application.ID, task.ID) + return "" + case 1: + return task.IPAddresses[0].IPAddress + default: + ipAddressIdx := label.GetIntValue(stringValueMap(application.Labels), labelIPAddressIdx, math.MinInt32) + + if ipAddressIdx == math.MinInt32 { + log.Errorf("Found %d task IP addresses but missing IP address index for Marathon application %s on task %s", + numTaskIPAddresses, application.ID, task.ID) + return "" + } + if ipAddressIdx < 0 || ipAddressIdx > numTaskIPAddresses { + log.Errorf("Cannot use IP address index to select from %d task IP addresses for Marathon application %s on task %s", + numTaskIPAddresses, application.ID, task.ID) + return "" + } + + return task.IPAddresses[ipAddressIdx].IPAddress + } +} + +// getServiceNamesV1 returns a list of service names for a given application +// An empty name "" will be added if no service specific properties exist, +// as an indication that there are no sub-services, but only main application +// Deprecated +func getServiceNamesV1(application marathon.Application) []string { + labelServiceProperties := label.ExtractServicePropertiesP(application.Labels) + + var names []string + for k := range labelServiceProperties { + names = append(names, k) + } + + // An empty name "" will be added if no service specific properties exist, + // as an indication that there are no sub-services, but only main application + if len(names) == 0 { + names = append(names, defaultService) + } + return names +} + +// Deprecated +func identifierV1(app marathon.Application, task marathon.Task, serviceName string) string { + id := fmt.Sprintf("Marathon task %s from application %s", task.ID, app.ID) + if serviceName != "" { + id += fmt.Sprintf(" (service: %s)", serviceName) + } + return id +} + +// Deprecated +func hasLoadBalancerLabelsV1(application marathon.Application) bool { + method := label.Has(stringValueMap(application.Labels), label.TraefikBackendLoadBalancerMethod) + sticky := label.Has(stringValueMap(application.Labels), label.TraefikBackendLoadBalancerSticky) + stickiness := label.Has(stringValueMap(application.Labels), label.TraefikBackendLoadBalancerStickiness) + return method || sticky || stickiness +} + +// Deprecated +func hasMaxConnLabelsV1(application marathon.Application) bool { + mca := label.Has(stringValueMap(application.Labels), label.TraefikBackendMaxConnAmount) + mcef := label.Has(stringValueMap(application.Labels), label.TraefikBackendMaxConnExtractorFunc) + return mca && mcef +} + +// TODO: Deprecated +// replaced by Stickiness +// Deprecated +func getStickyV1(application marathon.Application) bool { + if label.Has(stringValueMap(application.Labels), label.TraefikBackendLoadBalancerSticky) { + log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) + } + return label.GetBoolValue(stringValueMap(application.Labels), label.TraefikBackendLoadBalancerSticky, false) +} + +// Deprecated +func getPortV1(task marathon.Task, application marathon.Application, serviceName string) string { + port, err := processPortsV1(application, task, serviceName) + if err != nil { + log.Errorf("Unable to process ports for %s: %s", identifierV1(application, task, serviceName), err) + return "" + } + + return strconv.Itoa(port) +} + +// processPortsV1 returns the configured port. +// An explicitly specified port is preferred. If none is specified, it selects +// one of the available port. The first such found port is returned unless an +// optional index is provided. +// Deprecated +func processPortsV1(application marathon.Application, task marathon.Task, serviceName string) (int, error) { + labels := getLabelsV1(application, serviceName) + lblPort := getLabelNameV1(serviceName, label.SuffixPort) + + if label.Has(labels, lblPort) { + port := label.GetIntValue(labels, lblPort, 0) + + if port <= 0 { + return 0, fmt.Errorf("explicitly specified port %d must be larger than zero", port) + } else if port > 0 { + return port, nil + } + } + + ports := retrieveAvailablePortsV1(application, task) + if len(ports) == 0 { + return 0, errors.New("no port found") + } + + lblPortIndex := getLabelNameV1(serviceName, label.SuffixPortIndex) + portIndex := label.GetIntValue(labels, lblPortIndex, 0) + if portIndex < 0 || portIndex > len(ports)-1 { + return 0, fmt.Errorf("index %d must be within range (0, %d)", portIndex, len(ports)-1) + } + return ports[portIndex], nil +} + +// Deprecated +func retrieveAvailablePortsV1(application marathon.Application, task marathon.Task) []int { + // Using default port configuration + if len(task.Ports) > 0 { + return task.Ports + } + + // Using port definition if available + if application.PortDefinitions != nil && len(*application.PortDefinitions) > 0 { + var ports []int + for _, def := range *application.PortDefinitions { + if def.Port != nil { + ports = append(ports, *def.Port) + } + } + return ports + } + // If using IP-per-task using this port definition + if application.IPAddressPerTask != nil && len(*(application.IPAddressPerTask.Discovery).Ports) > 0 { + var ports []int + for _, def := range *(application.IPAddressPerTask.Discovery).Ports { + ports = append(ports, def.Number) + } + return ports + } + + return []int{} +} + +// Deprecated +func (p *Provider) getServersV1(application marathon.Application, serviceName string) map[string]types.Server { + var servers map[string]types.Server + + for _, task := range application.Tasks { + host := p.getBackendServerV1(*task, application) + if len(host) == 0 { + continue + } + + if servers == nil { + servers = make(map[string]types.Server) + } + + labels := getLabelsV1(application, serviceName) + + port := getPortV1(*task, application, serviceName) + protocol := label.GetStringValue(labels, getLabelNameV1(serviceName, label.SuffixProtocol), label.DefaultProtocol) + + serverName := provider.Normalize("server-" + task.ID + getSegmentNameSuffix(serviceName)) + servers[serverName] = types.Server{ + URL: fmt.Sprintf("%s://%s:%v", protocol, host, port), + Weight: label.GetIntValue(labels, getLabelNameV1(serviceName, label.SuffixWeight), label.DefaultWeightInt), + } + } + + return servers +} + +// Deprecated +func getWhiteListV1(application marathon.Application, serviceName string) *types.WhiteList { + labels := getLabelsV1(application, serviceName) + + ranges := label.GetSliceStringValue(labels, getLabelNameV1(serviceName, label.SuffixFrontendWhiteListSourceRange)) + if len(ranges) > 0 { + return &types.WhiteList{ + SourceRange: ranges, + UseXForwardedFor: label.GetBoolValue(labels, getLabelNameV1(serviceName, label.SuffixFrontendWhiteListUseXForwardedFor), false), + } + } + + return nil +} + +// Label functions + +// Deprecated +func getLabelsV1(application marathon.Application, serviceName string) map[string]string { + if len(serviceName) > 0 { + return label.ExtractServicePropertiesP(application.Labels)[serviceName] + } + + if application.Labels != nil { + return *application.Labels + } + + return make(map[string]string) +} + +// Deprecated +func getLabelNameV1(serviceName string, suffix string) string { + if len(serviceName) != 0 { + return suffix + } + return label.Prefix + suffix +} + +// Deprecated +func hasFuncV1(labelName string) func(application marathon.Application) bool { + return func(application marathon.Application) bool { + return label.Has(stringValueMap(application.Labels), labelName) + } +} + +// Deprecated +func getFuncStringServiceV1(labelName string, defaultValue string) func(application marathon.Application, serviceName string) string { + return func(application marathon.Application, serviceName string) string { + labels := getLabelsV1(application, serviceName) + lbName := getLabelNameV1(serviceName, labelName) + return label.GetStringValue(labels, lbName, defaultValue) + } +} + +// Deprecated +func getFuncBoolServiceV1(labelName string, defaultValue bool) func(application marathon.Application, serviceName string) bool { + return func(application marathon.Application, serviceName string) bool { + labels := getLabelsV1(application, serviceName) + lbName := getLabelNameV1(serviceName, labelName) + return label.GetBoolValue(labels, lbName, defaultValue) + } +} + +// Deprecated +func getFuncIntServiceV1(labelName string, defaultValue int) func(application marathon.Application, serviceName string) int { + return func(application marathon.Application, serviceName string) int { + labels := getLabelsV1(application, serviceName) + lbName := getLabelNameV1(serviceName, labelName) + return label.GetIntValue(labels, lbName, defaultValue) + } +} + +// Deprecated +func getFuncSliceStringServiceV1(labelName string) func(application marathon.Application, serviceName string) []string { + return func(application marathon.Application, serviceName string) []string { + labels := getLabelsV1(application, serviceName) + return label.GetSliceStringValue(labels, getLabelNameV1(serviceName, labelName)) + } +} + +// Deprecated +func getFuncStringV1(labelName string, defaultValue string) func(application marathon.Application) string { + return func(application marathon.Application) string { + return label.GetStringValue(stringValueMap(application.Labels), labelName, defaultValue) + } +} + +// Deprecated +func getFuncInt64V1(labelName string, defaultValue int64) func(application marathon.Application) int64 { + return func(application marathon.Application) int64 { + return label.GetInt64Value(stringValueMap(application.Labels), labelName, defaultValue) + } +} diff --git a/provider/marathon/deprecated_config_test.go b/provider/marathon/deprecated_config_test.go new file mode 100644 index 000000000..575b9ba9f --- /dev/null +++ b/provider/marathon/deprecated_config_test.go @@ -0,0 +1,762 @@ +package marathon + +import ( + "testing" + + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/gambol99/go-marathon" + "github.com/stretchr/testify/assert" +) + +func TestGetConfigurationAPIErrorsV1(t *testing.T) { + fakeClient := newFakeClient(true, marathon.Applications{}) + + p := &Provider{ + marathonClient: fakeClient, + } + p.TemplateVersion = 1 + + actualConfig := p.getConfiguration() + fakeClient.AssertExpectations(t) + + if actualConfig != nil { + t.Errorf("configuration should have been nil, got %v", actualConfig) + } +} + +func TestBuildConfigurationV1(t *testing.T) { + testCases := []struct { + desc string + application marathon.Application + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{ + { + desc: "simple application", + application: application( + appPorts(80), + withTasks(localhostTask(taskPorts(80))), + ), + expectedFrontends: map[string]*types.Frontend{ + "frontend-app": { + Backend: "backend-app", + Routes: map[string]types.Route{ + "route-host-app": { + Rule: "Host:app.marathon.localhost", + }, + }, + PassHostHeader: true, + BasicAuth: []string{}, + EntryPoints: []string{}, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-app": { + Servers: map[string]types.Server{ + "server-task": { + URL: "http://localhost:80", + Weight: 0, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "filtered task", + application: application( + appPorts(80), + withTasks(localhostTask(taskPorts(80), state(taskStateStaging))), + ), + expectedFrontends: map[string]*types.Frontend{ + "frontend-app": { + Backend: "backend-app", + Routes: map[string]types.Route{ + "route-host-app": { + Rule: "Host:app.marathon.localhost", + }, + }, + PassHostHeader: true, + BasicAuth: []string{}, + EntryPoints: []string{}, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-app": {}, + }, + }, + { + desc: "max connection extractor function label only", + application: application( + appPorts(80), + withTasks(localhostTask(taskPorts(80))), + + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + ), + expectedFrontends: map[string]*types.Frontend{ + "frontend-app": { + Backend: "backend-app", + Routes: map[string]types.Route{ + "route-host-app": { + Rule: "Host:app.marathon.localhost", + }, + }, + PassHostHeader: true, + BasicAuth: []string{}, + EntryPoints: []string{}, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-app": { + Servers: map[string]types.Server{ + "server-task": { + URL: "http://localhost:80", + Weight: 0, + }, + }, + MaxConn: nil, + }, + }, + }, + { + desc: "multiple ports", + application: application( + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + ), + expectedFrontends: map[string]*types.Frontend{ + "frontend-app": { + Backend: "backend-app", + Routes: map[string]types.Route{ + "route-host-app": { + Rule: "Host:app.marathon.localhost", + }, + }, + PassHostHeader: true, + BasicAuth: []string{}, + EntryPoints: []string{}, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-app": { + Servers: map[string]types.Server{ + "server-task": { + URL: "http://localhost:80", + Weight: 0, + }, + }, + }, + }, + }, + { + desc: "with all labels", + application: application( + appPorts(80), + withTasks(task(host("127.0.0.1"), taskPorts(80))), + + withLabel(label.TraefikPort, "666"), + withLabel(label.TraefikProtocol, "https"), + withLabel(label.TraefikWeight, "12"), + + withLabel(label.TraefikBackend, "foobar"), + + withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), + withLabel(label.TraefikBackendHealthCheckPath, "/health"), + withLabel(label.TraefikBackendHealthCheckInterval, "6"), + withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), + withLabel(label.TraefikBackendLoadBalancerSticky, "true"), + withLabel(label.TraefikBackendLoadBalancerStickiness, "true"), + withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "chocolate"), + withLabel(label.TraefikBackendMaxConnAmount, "666"), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + + withLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + withLabel(label.TraefikFrontendEntryPoints, "http,https"), + withLabel(label.TraefikFrontendPassHostHeader, "true"), + withLabel(label.TraefikFrontendPriority, "666"), + withLabel(label.TraefikFrontendRule, "Host:traefik.io"), + ), + expectedFrontends: map[string]*types.Frontend{ + "frontend-app": { + EntryPoints: []string{ + "http", + "https", + }, + Backend: "backendfoobar", + Routes: map[string]types.Route{ + "route-host-app": { + Rule: "Host:traefik.io", + }, + }, + PassHostHeader: true, + Priority: 666, + BasicAuth: []string{ + "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backendfoobar": { + Servers: map[string]types.Server{ + "server-task": { + URL: "https://127.0.0.1:666", + Weight: 12, + }, + }, + CircuitBreaker: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", + }, + LoadBalancer: &types.LoadBalancer{ + Method: "drr", + Sticky: true, + Stickiness: &types.Stickiness{ + CookieName: "chocolate", + }, + }, + MaxConn: &types.MaxConn{ + Amount: 666, + ExtractorFunc: "client.ip", + }, + HealthCheck: &types.HealthCheck{ + Path: "/health", + Interval: "6", + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + test.application.ID = "/app" + + for _, task := range test.application.Tasks { + task.ID = "task" + if task.State == "" { + task.State = "TASK_RUNNING" + } + } + + p := &Provider{ + Domain: "marathon.localhost", + ExposedByDefault: true, + } + + actualConfig := p.buildConfigurationV1(withApplications(test.application)) + + assert.NotNil(t, actualConfig) + assert.Equal(t, test.expectedBackends, actualConfig.Backends) + assert.Equal(t, test.expectedFrontends, actualConfig.Frontends) + }) + } +} + +func TestBuildConfigurationServicesV1(t *testing.T) { + testCases := []struct { + desc string + application marathon.Application + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{ + { + desc: "multiple ports with services", + application: application( + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + + withLabel(label.TraefikBackendMaxConnAmount, "1000"), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + withServiceLabel(label.TraefikPort, "80", "web"), + withServiceLabel(label.TraefikPort, "81", "admin"), + withLabel("traefik..port", "82"), // This should be ignored, as it fails to match the servicesPropertiesRegexp regex. + withServiceLabel(label.TraefikFrontendRule, "Host:web.app.marathon.localhost", "web"), + withServiceLabel(label.TraefikFrontendRule, "Host:admin.app.marathon.localhost", "admin"), + ), + expectedFrontends: map[string]*types.Frontend{ + "frontend-app-service-web": { + Backend: "backend-app-service-web", + Routes: map[string]types.Route{ + `route-host-app-service-web`: { + Rule: "Host:web.app.marathon.localhost", + }, + }, + PassHostHeader: true, + BasicAuth: []string{}, + EntryPoints: []string{}, + }, + "frontend-app-service-admin": { + Backend: "backend-app-service-admin", + Routes: map[string]types.Route{ + `route-host-app-service-admin`: { + Rule: "Host:admin.app.marathon.localhost", + }, + }, + PassHostHeader: true, + BasicAuth: []string{}, + EntryPoints: []string{}, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-app-service-web": { + Servers: map[string]types.Server{ + "server-task-service-web": { + URL: "http://localhost:80", + Weight: 0, + }, + }, + MaxConn: &types.MaxConn{ + Amount: 1000, + ExtractorFunc: "client.ip", + }, + }, + "backend-app-service-admin": { + Servers: map[string]types.Server{ + "server-task-service-admin": { + URL: "http://localhost:81", + Weight: 0, + }, + }, + MaxConn: &types.MaxConn{ + Amount: 1000, + ExtractorFunc: "client.ip", + }, + }, + }, + }, + { + desc: "when all labels are set", + application: application( + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + + withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), + withLabel(label.TraefikBackendHealthCheckPath, "/health"), + withLabel(label.TraefikBackendHealthCheckInterval, "6"), + withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), + withLabel(label.TraefikBackendLoadBalancerSticky, "true"), + withLabel(label.TraefikBackendLoadBalancerStickiness, "true"), + withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "chocolate"), + withLabel(label.TraefikBackendMaxConnAmount, "666"), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + + withServiceLabel(label.TraefikPort, "80", "containous"), + withServiceLabel(label.TraefikProtocol, "https", "containous"), + withServiceLabel(label.TraefikWeight, "12", "containous"), + + withServiceLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "containous"), + withServiceLabel(label.TraefikFrontendEntryPoints, "http,https", "containous"), + withServiceLabel(label.TraefikFrontendPassHostHeader, "true", "containous"), + withServiceLabel(label.TraefikFrontendPriority, "666", "containous"), + withServiceLabel(label.TraefikFrontendRule, "Host:traefik.io", "containous"), + ), + expectedFrontends: map[string]*types.Frontend{ + "frontend-app-service-containous": { + EntryPoints: []string{ + "http", + "https", + }, + Backend: "backend-app-service-containous", + Routes: map[string]types.Route{ + "route-host-app-service-containous": { + Rule: "Host:traefik.io", + }, + }, + PassHostHeader: true, + Priority: 666, + BasicAuth: []string{ + "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-app-service-containous": { + Servers: map[string]types.Server{ + "server-task-service-containous": { + URL: "https://localhost:80", + Weight: 12, + }, + }, + CircuitBreaker: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", + }, + LoadBalancer: &types.LoadBalancer{ + Method: "drr", + Sticky: true, + Stickiness: &types.Stickiness{ + CookieName: "chocolate", + }, + }, + MaxConn: &types.MaxConn{ + Amount: 666, + ExtractorFunc: "client.ip", + }, + HealthCheck: &types.HealthCheck{ + Path: "/health", + Interval: "6", + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + test.application.ID = "/app" + + for _, task := range test.application.Tasks { + task.ID = "task" + if task.State == "" { + task.State = "TASK_RUNNING" + } + } + + p := &Provider{ + Domain: "marathon.localhost", + ExposedByDefault: true, + } + + actualConfig := p.buildConfigurationV1(withApplications(test.application)) + + assert.NotNil(t, actualConfig) + assert.Equal(t, test.expectedBackends, actualConfig.Backends) + assert.Equal(t, test.expectedFrontends, actualConfig.Frontends) + }) + } +} + +func TestGetPortV1(t *testing.T) { + testCases := []struct { + desc string + application marathon.Application + task marathon.Task + serviceName string + expected string + }{ + { + desc: "port missing", + application: application(), + task: task(), + expected: "", + }, + { + desc: "numeric port", + application: application(withLabel(label.TraefikPort, "80")), + task: task(), + expected: "80", + }, + { + desc: "string port", + application: application(withLabel(label.TraefikPort, "foobar")), + task: task(taskPorts(80)), + expected: "", + }, + { + desc: "negative port", + application: application(withLabel(label.TraefikPort, "-1")), + task: task(taskPorts(80)), + expected: "", + }, + { + desc: "task port available", + application: application(), + task: task(taskPorts(80)), + expected: "80", + }, + { + desc: "port definition available", + application: application( + portDefinition(443), + ), + task: task(), + expected: "443", + }, + { + desc: "IP-per-task port available", + application: application(ipAddrPerTask(8000)), + task: task(), + expected: "8000", + }, + { + desc: "multiple task ports available", + application: application(), + task: task(taskPorts(80, 443)), + expected: "80", + }, + { + desc: "numeric port index specified", + application: application(withLabel(label.TraefikPortIndex, "1")), + task: task(taskPorts(80, 443)), + expected: "443", + }, + { + desc: "string port index specified", + application: application(withLabel(label.TraefikPortIndex, "foobar")), + task: task(taskPorts(80)), + expected: "80", + }, + { + desc: "port and port index specified", + application: application( + withLabel(label.TraefikPort, "80"), + withLabel(label.TraefikPortIndex, "1"), + ), + task: task(taskPorts(80, 443)), + expected: "80", + }, + { + desc: "task and application ports specified", + application: application(appPorts(9999)), + task: task(taskPorts(7777)), + expected: "7777", + }, + { + desc: "multiple task ports with service index available", + application: application(withLabel(label.Prefix+"http.portIndex", "0")), + task: task(taskPorts(80, 443)), + serviceName: "http", + expected: "80", + }, + { + desc: "multiple task ports with service port available", + application: application(withLabel(label.Prefix+"https.port", "443")), + task: task(taskPorts(80, 443)), + serviceName: "https", + expected: "443", + }, + { + desc: "multiple task ports with services but default port available", + application: application(withLabel(label.Prefix+"http.weight", "100")), + task: task(taskPorts(80, 443)), + serviceName: "http", + expected: "80", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getPortV1(test.task, test.application, test.serviceName) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetFrontendRuleV1(t *testing.T) { + testCases := []struct { + desc string + application marathon.Application + serviceName string + expected string + marathonLBCompatibility bool + }{ + { + desc: "label missing", + application: application(appID("test")), + marathonLBCompatibility: true, + expected: "Host:test.marathon.localhost", + }, + { + desc: "HAProxy vhost available and LB compat disabled", + application: application( + appID("test"), + withLabel("HAPROXY_0_VHOST", "foo.bar"), + ), + marathonLBCompatibility: false, + expected: "Host:test.marathon.localhost", + }, + { + desc: "HAProxy vhost available and LB compat enabled", + application: application(withLabel("HAPROXY_0_VHOST", "foo.bar")), + marathonLBCompatibility: true, + expected: "Host:foo.bar", + }, + { + desc: "frontend rule available", + + application: application( + withLabel(label.TraefikFrontendRule, "Host:foo.bar"), + withLabel("HAPROXY_0_VHOST", "unused"), + ), + marathonLBCompatibility: true, + expected: "Host:foo.bar", + }, + { + desc: "service label existing", + application: application(withServiceLabel(label.TraefikFrontendRule, "Host:foo.bar", "app")), + serviceName: "app", + marathonLBCompatibility: true, + expected: "Host:foo.bar", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := &Provider{ + Domain: "marathon.localhost", + MarathonLBCompatibility: test.marathonLBCompatibility, + } + + actual := p.getFrontendRuleV1(test.application, test.serviceName) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetBackendV1(t *testing.T) { + testCases := []struct { + desc string + application marathon.Application + serviceName string + expected string + }{ + { + desc: "label missing", + application: application(appID("/group/app")), + expected: "backend-group-app", + }, + { + desc: "label existing", + application: application(withLabel(label.TraefikBackend, "bar")), + expected: "backendbar", + }, + { + desc: "service label existing", + application: application(withServiceLabel(label.TraefikBackend, "bar", "app")), + serviceName: "app", + expected: "backendbar", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := &Provider{} + + actual := p.getBackendNameV1(test.application, test.serviceName) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetBackendServerV1(t *testing.T) { + host := "host" + testCases := []struct { + desc string + application marathon.Application + task marathon.Task + forceTaskHostname bool + expectedServer string + }{ + { + desc: "application without IP-per-task", + application: application(), + expectedServer: host, + }, + { + desc: "task hostname override", + application: application(ipAddrPerTask(8000)), + forceTaskHostname: true, + expectedServer: host, + }, + { + desc: "task IP address missing", + application: application(ipAddrPerTask(8000)), + task: task(), + expectedServer: "", + }, + { + desc: "single task IP address", + application: application(ipAddrPerTask(8000)), + task: task(ipAddresses("1.1.1.1")), + expectedServer: "1.1.1.1", + }, + { + desc: "multiple task IP addresses without index label", + application: application(ipAddrPerTask(8000)), + task: task(ipAddresses("1.1.1.1", "2.2.2.2")), + expectedServer: "", + }, + { + desc: "multiple task IP addresses with invalid index label", + application: application( + withLabel("traefik.ipAddressIdx", "invalid"), + ipAddrPerTask(8000), + ), + task: task(ipAddresses("1.1.1.1", "2.2.2.2")), + expectedServer: "", + }, + { + desc: "multiple task IP addresses with valid index label", + application: application( + withLabel("traefik.ipAddressIdx", "1"), + ipAddrPerTask(8000), + ), + task: task(ipAddresses("1.1.1.1", "2.2.2.2")), + expectedServer: "2.2.2.2", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := &Provider{ForceTaskHostname: test.forceTaskHostname} + test.task.Host = host + + actualServer := p.getBackendServerV1(test.task, test.application) + + assert.Equal(t, test.expectedServer, actualServer) + }) + } +} + +func TestGetStickyV1(t *testing.T) { + testCases := []struct { + desc string + application marathon.Application + expected bool + }{ + { + desc: "label missing", + application: application(), + expected: false, + }, + { + desc: "label existing", + application: application(withLabel(label.TraefikBackendLoadBalancerSticky, "true")), + expected: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getStickyV1(test.application) + if actual != test.expected { + t.Errorf("actual %v, expected %v", actual, test.expected) + } + }) + } +} diff --git a/provider/marathon/fake_client_test.go b/provider/marathon/fake_client_test.go new file mode 100644 index 000000000..42f5e0235 --- /dev/null +++ b/provider/marathon/fake_client_test.go @@ -0,0 +1,24 @@ +package marathon + +import ( + "errors" + + "github.com/containous/traefik/provider/marathon/mocks" + "github.com/gambol99/go-marathon" + "github.com/stretchr/testify/mock" +) + +type fakeClient struct { + mocks.Marathon +} + +func newFakeClient(applicationsError bool, applications marathon.Applications) *fakeClient { + // create an instance of our test object + fakeClient := new(fakeClient) + if applicationsError { + fakeClient.On("Applications", mock.Anything).Return(nil, errors.New("fake Marathon server error")) + } else { + fakeClient.On("Applications", mock.Anything).Return(&applications, nil) + } + return fakeClient +} diff --git a/provider/marathon/marathon.go b/provider/marathon/marathon.go index d0945e54b..bdf13510a 100644 --- a/provider/marathon/marathon.go +++ b/provider/marathon/marathon.go @@ -3,6 +3,7 @@ package marathon import ( "net" "net/http" + "net/url" "time" "github.com/cenk/backoff" @@ -128,7 +129,8 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s return case event := <-update: log.Debugf("Received provider event %s", event) - configuration := p.buildConfiguration() + + configuration := p.getConfiguration() if configuration != nil { configurationChan <- types.ConfigMessage{ ProviderName: "marathon", @@ -139,7 +141,8 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s } }) } - configuration := p.buildConfiguration() + + configuration := p.getConfiguration() configurationChan <- types.ConfigMessage{ ProviderName: "marathon", Configuration: configuration, @@ -156,3 +159,22 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s } return nil } + +func (p *Provider) getConfiguration() *types.Configuration { + applications, err := p.getApplications() + if err != nil { + log.Errorf("Failed to retrieve Marathon applications: %v", err) + return nil + } + + return p.buildConfiguration(applications) +} + +func (p *Provider) getApplications() (*marathon.Applications, error) { + v := url.Values{} + v.Add("embed", "apps.tasks") + v.Add("embed", "apps.deployments") + v.Add("embed", "apps.readiness") + + return p.marathonClient.Applications(v) +} diff --git a/provider/rancher/config.go b/provider/rancher/config.go index af1a1c06a..29fb6d84e 100644 --- a/provider/rancher/config.go +++ b/provider/rancher/config.go @@ -2,7 +2,6 @@ package rancher import ( "fmt" - "math" "strconv" "strings" "text/template" @@ -14,63 +13,32 @@ import ( "github.com/containous/traefik/types" ) -func (p *Provider) buildConfiguration(services []rancherData) *types.Configuration { - +func (p *Provider) buildConfigurationV2(services []rancherData) *types.Configuration { var RancherFuncMap = template.FuncMap{ - "getDomain": getFuncString(label.TraefikDomain, p.Domain), + "getLabelValue": label.GetStringValue, + "getDomain": label.GetFuncString(label.TraefikDomain, p.Domain), // Backend functions - "getCircuitBreaker": getCircuitBreaker, - "getLoadBalancer": getLoadBalancer, - "getMaxConn": getMaxConn, - "getHealthCheck": getHealthCheck, - "getBuffering": getBuffering, + "getCircuitBreaker": label.GetCircuitBreaker, + "getLoadBalancer": label.GetLoadBalancer, + "getMaxConn": label.GetMaxConn, + "getHealthCheck": label.GetHealthCheck, + "getBuffering": label.GetBuffering, "getServers": getServers, - // TODO Deprecated [breaking] - "getPort": getFuncString(label.TraefikPort, ""), - // TODO Deprecated [breaking] - "getProtocol": getFuncString(label.TraefikProtocol, label.DefaultProtocol), - // TODO Deprecated [breaking] - "getWeight": getFuncInt(label.TraefikWeight, label.DefaultWeightInt), - // TODO Deprecated [breaking] - "hasCircuitBreakerLabel": hasFunc(label.TraefikBackendCircuitBreakerExpression), - // TODO Deprecated [breaking] - "getCircuitBreakerExpression": getFuncString(label.TraefikBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression), - // TODO Deprecated [breaking] - "hasLoadBalancerLabel": hasLoadBalancerLabel, - // TODO Deprecated [breaking] - "getLoadBalancerMethod": getFuncString(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), - // TODO Deprecated [breaking] - "hasMaxConnLabels": hasMaxConnLabels, - // TODO Deprecated [breaking] - "getMaxConnAmount": getFuncInt64(label.TraefikBackendMaxConnAmount, 0), - // TODO Deprecated [breaking] - "getMaxConnExtractorFunc": getFuncString(label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc), - // TODO Deprecated [breaking] - "getSticky": getSticky, - // TODO Deprecated [breaking] - "hasStickinessLabel": hasFunc(label.TraefikBackendLoadBalancerStickiness), - // TODO Deprecated [breaking] - "getStickinessCookieName": getFuncString(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName), - // Frontend functions - "getBackend": getBackendName, // TODO Deprecated [breaking] replaced by getBackendName "getBackendName": getBackendName, "getFrontendRule": p.getFrontendRule, - "getPriority": getFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), - "getPassHostHeader": getFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), - "getPassTLSCert": getFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), - "getEntryPoints": getFuncSliceString(label.TraefikFrontendEntryPoints), - "getBasicAuth": getFuncSliceString(label.TraefikFrontendAuthBasic), - "getErrorPages": getErrorPages, - "getRateLimit": getRateLimit, - "getRedirect": getRedirect, - "getHeaders": getHeaders, - "getWhiteList": getWhiteList, - - // TODO Deprecated [breaking] - "getWhitelistSourceRange": getFuncSliceString(label.TraefikFrontendWhitelistSourceRange), + "getPriority": label.GetFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), + "getPassHostHeader": label.GetFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getPassTLSCert": label.GetFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), + "getEntryPoints": label.GetFuncSliceString(label.TraefikFrontendEntryPoints), + "getBasicAuth": label.GetFuncSliceString(label.TraefikFrontendAuthBasic), + "getErrorPages": label.GetErrorPages, + "getRateLimit": label.GetRateLimit, + "getRedirect": label.GetRedirect, + "getHeaders": label.GetHeaders, + "getWhiteList": label.GetWhiteList, } // filter services @@ -80,10 +48,16 @@ func (p *Provider) buildConfiguration(services []rancherData) *types.Configurati backends := map[string]rancherData{} for _, service := range filteredServices { - frontendName := p.getFrontendName(service) - frontends[frontendName] = service - backendName := getBackendName(service) - backends[backendName] = service + segmentProperties := label.ExtractTraefikLabels(service.Labels) + for segmentName, labels := range segmentProperties { + service.SegmentLabels = labels + service.SegmentName = segmentName + + frontendName := p.getFrontendName(service) + frontends[frontendName] = service + backendName := getBackendName(service) + backends[backendName] = service + } } templateObjects := struct { @@ -105,9 +79,19 @@ func (p *Provider) buildConfiguration(services []rancherData) *types.Configurati } func (p *Provider) serviceFilter(service rancherData) bool { - if service.Labels[label.TraefikPort] == "" { - log.Debugf("Filtering service %s without traefik.port label", service.Name) - return false + segmentProperties := label.ExtractTraefikLabels(service.Labels) + + for segmentName, labels := range segmentProperties { + _, err := checkSegmentPort(labels, segmentName) + if err != nil { + log.Debugf("Filtering service %s %s without traefik.port label", service.Name, segmentName) + return false + } + + if len(p.getFrontendRule(service)) == 0 { + log.Debugf("Filtering container with empty frontend rule %s %s", service.Name, segmentName) + return false + } } if !label.IsEnabled(service.Labels, p.ExposedByDefault) { @@ -145,112 +129,37 @@ func (p *Provider) getFrontendRule(service rancherData) string { } func (p *Provider) getFrontendName(service rancherData) string { - return provider.Normalize(p.getFrontendRule(service)) -} - -// TODO: Deprecated -// replaced by Stickiness -// Deprecated -func getSticky(service rancherData) bool { - if label.Has(service.Labels, label.TraefikBackendLoadBalancerSticky) { - log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) + var name string + if len(service.SegmentName) > 0 { + name = getBackendName(service) + } else { + name = p.getFrontendRule(service) } - return label.GetBoolValue(service.Labels, label.TraefikBackendLoadBalancerSticky, false) -} -// Deprecated -func hasLoadBalancerLabel(service rancherData) bool { - method := label.Has(service.Labels, label.TraefikBackendLoadBalancerMethod) - sticky := label.Has(service.Labels, label.TraefikBackendLoadBalancerSticky) - stickiness := label.Has(service.Labels, label.TraefikBackendLoadBalancerStickiness) - cookieName := label.Has(service.Labels, label.TraefikBackendLoadBalancerStickinessCookieName) - return method || sticky || stickiness || cookieName -} - -// Deprecated -func hasMaxConnLabels(service rancherData) bool { - mca := label.Has(service.Labels, label.TraefikBackendMaxConnAmount) - mcef := label.Has(service.Labels, label.TraefikBackendMaxConnExtractorFunc) - return mca && mcef + return provider.Normalize(name) } func getBackendName(service rancherData) string { - backend := label.GetStringValue(service.Labels, label.TraefikBackend, service.Name) + if len(service.SegmentName) > 0 { + return getSegmentBackendName(service) + } + + return getDefaultBackendName(service) +} + +func getSegmentBackendName(service rancherData) string { + if value := label.GetStringValue(service.SegmentLabels, label.TraefikFrontendBackend, ""); len(value) > 0 { + return provider.Normalize(service.Name + "-" + value) + } + + return provider.Normalize(service.Name + "-" + getDefaultBackendName(service) + "-" + service.SegmentName) +} + +func getDefaultBackendName(service rancherData) string { + backend := label.GetStringValue(service.SegmentLabels, label.TraefikBackend, service.Name) return provider.Normalize(backend) } -func getCircuitBreaker(service rancherData) *types.CircuitBreaker { - circuitBreaker := label.GetStringValue(service.Labels, label.TraefikBackendCircuitBreakerExpression, "") - if len(circuitBreaker) == 0 { - return nil - } - return &types.CircuitBreaker{Expression: circuitBreaker} -} - -func getLoadBalancer(service rancherData) *types.LoadBalancer { - if !label.HasPrefix(service.Labels, label.TraefikBackendLoadBalancer) { - return nil - } - - method := label.GetStringValue(service.Labels, label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod) - - lb := &types.LoadBalancer{ - Method: method, - Sticky: getSticky(service), - } - - if label.GetBoolValue(service.Labels, label.TraefikBackendLoadBalancerStickiness, false) { - cookieName := label.GetStringValue(service.Labels, label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName) - lb.Stickiness = &types.Stickiness{CookieName: cookieName} - } - - return lb -} - -func getMaxConn(service rancherData) *types.MaxConn { - amount := label.GetInt64Value(service.Labels, label.TraefikBackendMaxConnAmount, math.MinInt64) - extractorFunc := label.GetStringValue(service.Labels, label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc) - - if amount == math.MinInt64 || len(extractorFunc) == 0 { - return nil - } - - return &types.MaxConn{ - Amount: amount, - ExtractorFunc: extractorFunc, - } -} - -func getHealthCheck(service rancherData) *types.HealthCheck { - path := label.GetStringValue(service.Labels, label.TraefikBackendHealthCheckPath, "") - if len(path) == 0 { - return nil - } - - port := label.GetIntValue(service.Labels, label.TraefikBackendHealthCheckPort, label.DefaultBackendHealthCheckPort) - interval := label.GetStringValue(service.Labels, label.TraefikBackendHealthCheckInterval, "") - - return &types.HealthCheck{ - Path: path, - Port: port, - Interval: interval, - } -} - -func getBuffering(service rancherData) *types.Buffering { - if !label.HasPrefix(service.Labels, label.TraefikBackendBuffering) { - return nil - } - - return &types.Buffering{ - MaxRequestBodyBytes: label.GetInt64Value(service.Labels, label.TraefikBackendBufferingMaxRequestBodyBytes, 0), - MaxResponseBodyBytes: label.GetInt64Value(service.Labels, label.TraefikBackendBufferingMaxResponseBodyBytes, 0), - MemRequestBodyBytes: label.GetInt64Value(service.Labels, label.TraefikBackendBufferingMemRequestBodyBytes, 0), - MemResponseBodyBytes: label.GetInt64Value(service.Labels, label.TraefikBackendBufferingMemResponseBodyBytes, 0), - RetryExpression: label.GetStringValue(service.Labels, label.TraefikBackendBufferingRetryExpression, ""), - } -} - func getServers(service rancherData) map[string]types.Server { var servers map[string]types.Server @@ -259,9 +168,9 @@ func getServers(service rancherData) map[string]types.Server { servers = make(map[string]types.Server) } - protocol := label.GetStringValue(service.Labels, label.TraefikProtocol, label.DefaultProtocol) - port := label.GetStringValue(service.Labels, label.TraefikPort, "") - weight := label.GetIntValue(service.Labels, label.TraefikWeight, label.DefaultWeightInt) + protocol := label.GetStringValue(service.SegmentLabels, label.TraefikProtocol, label.DefaultProtocol) + port := label.GetStringValue(service.SegmentLabels, label.TraefikPort, "") + weight := label.GetIntValue(service.SegmentLabels, label.TraefikWeight, label.DefaultWeightInt) serverName := "server-" + strconv.Itoa(index) servers[serverName] = types.Server{ @@ -273,127 +182,14 @@ func getServers(service rancherData) map[string]types.Server { return servers } -func getWhiteList(service rancherData) *types.WhiteList { - ranges := label.GetSliceStringValue(service.Labels, label.TraefikFrontendWhiteListSourceRange) - - if len(ranges) > 0 { - return &types.WhiteList{ - SourceRange: ranges, - UseXForwardedFor: label.GetBoolValue(service.Labels, label.TraefikFrontendWhiteListUseXForwardedFor, false), +func checkSegmentPort(labels map[string]string, segmentName string) (int, error) { + if rawPort, ok := labels[label.TraefikPort]; ok { + port, err := strconv.Atoi(rawPort) + if err != nil { + return port, fmt.Errorf("invalid port value %q for the segment %q: %v", rawPort, segmentName, err) } + } else { + return 0, 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 getRedirect(service rancherData) *types.Redirect { - permanent := label.GetBoolValue(service.Labels, label.TraefikFrontendRedirectPermanent, false) - - if label.Has(service.Labels, label.TraefikFrontendRedirectEntryPoint) { - return &types.Redirect{ - EntryPoint: label.GetStringValue(service.Labels, label.TraefikFrontendRedirectEntryPoint, ""), - Permanent: permanent, - } - } - - if label.Has(service.Labels, label.TraefikFrontendRedirectRegex) && - label.Has(service.Labels, label.TraefikFrontendRedirectReplacement) { - return &types.Redirect{ - Regex: label.GetStringValue(service.Labels, label.TraefikFrontendRedirectRegex, ""), - Replacement: label.GetStringValue(service.Labels, label.TraefikFrontendRedirectReplacement, ""), - Permanent: permanent, - } - } - - return nil -} - -func getErrorPages(service rancherData) map[string]*types.ErrorPage { - prefix := label.Prefix + label.BaseFrontendErrorPage - return label.ParseErrorPages(service.Labels, prefix, label.RegexpFrontendErrorPage) -} - -func getRateLimit(service rancherData) *types.RateLimit { - extractorFunc := label.GetStringValue(service.Labels, label.TraefikFrontendRateLimitExtractorFunc, "") - if len(extractorFunc) == 0 { - return nil - } - - prefix := label.Prefix + label.BaseFrontendRateLimit - limits := label.ParseRateSets(service.Labels, prefix, label.RegexpFrontendRateLimit) - - return &types.RateLimit{ - ExtractorFunc: extractorFunc, - RateSet: limits, - } -} - -func getHeaders(service rancherData) *types.Headers { - headers := &types.Headers{ - CustomRequestHeaders: label.GetMapValue(service.Labels, label.TraefikFrontendRequestHeaders), - CustomResponseHeaders: label.GetMapValue(service.Labels, label.TraefikFrontendResponseHeaders), - SSLProxyHeaders: label.GetMapValue(service.Labels, label.TraefikFrontendSSLProxyHeaders), - AllowedHosts: label.GetSliceStringValue(service.Labels, label.TraefikFrontendAllowedHosts), - HostsProxyHeaders: label.GetSliceStringValue(service.Labels, label.TraefikFrontendHostsProxyHeaders), - STSSeconds: label.GetInt64Value(service.Labels, label.TraefikFrontendSTSSeconds, 0), - SSLRedirect: label.GetBoolValue(service.Labels, label.TraefikFrontendSSLRedirect, false), - SSLTemporaryRedirect: label.GetBoolValue(service.Labels, label.TraefikFrontendSSLTemporaryRedirect, false), - STSIncludeSubdomains: label.GetBoolValue(service.Labels, label.TraefikFrontendSTSIncludeSubdomains, false), - STSPreload: label.GetBoolValue(service.Labels, label.TraefikFrontendSTSPreload, false), - ForceSTSHeader: label.GetBoolValue(service.Labels, label.TraefikFrontendForceSTSHeader, false), - FrameDeny: label.GetBoolValue(service.Labels, label.TraefikFrontendFrameDeny, false), - ContentTypeNosniff: label.GetBoolValue(service.Labels, label.TraefikFrontendContentTypeNosniff, false), - BrowserXSSFilter: label.GetBoolValue(service.Labels, label.TraefikFrontendBrowserXSSFilter, false), - IsDevelopment: label.GetBoolValue(service.Labels, label.TraefikFrontendIsDevelopment, false), - SSLHost: label.GetStringValue(service.Labels, label.TraefikFrontendSSLHost, ""), - CustomFrameOptionsValue: label.GetStringValue(service.Labels, label.TraefikFrontendCustomFrameOptionsValue, ""), - ContentSecurityPolicy: label.GetStringValue(service.Labels, label.TraefikFrontendContentSecurityPolicy, ""), - PublicKey: label.GetStringValue(service.Labels, label.TraefikFrontendPublicKey, ""), - ReferrerPolicy: label.GetStringValue(service.Labels, label.TraefikFrontendReferrerPolicy, ""), - CustomBrowserXSSValue: label.GetStringValue(service.Labels, label.TraefikFrontendCustomBrowserXSSValue, ""), - } - - if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() { - return nil - } - - return headers -} - -// Label functions - -func getFuncString(labelName string, defaultValue string) func(service rancherData) string { - return func(service rancherData) string { - return label.GetStringValue(service.Labels, labelName, defaultValue) - } -} - -func getFuncBool(labelName string, defaultValue bool) func(service rancherData) bool { - return func(service rancherData) bool { - return label.GetBoolValue(service.Labels, labelName, defaultValue) - } -} - -func getFuncInt(labelName string, defaultValue int) func(service rancherData) int { - return func(service rancherData) int { - return label.GetIntValue(service.Labels, labelName, defaultValue) - } -} - -func getFuncInt64(labelName string, defaultValue int64) func(service rancherData) int64 { - return func(service rancherData) int64 { - return label.GetInt64Value(service.Labels, labelName, defaultValue) - } -} - -func getFuncSliceString(labelName string) func(service rancherData) []string { - return func(service rancherData) []string { - return label.GetSliceStringValue(service.Labels, labelName) - } -} - -func hasFunc(labelName string) func(service rancherData) bool { - return func(service rancherData) bool { - return label.Has(service.Labels, labelName) - } + return 0, nil } diff --git a/provider/rancher/config_root.go b/provider/rancher/config_root.go new file mode 100644 index 000000000..203015470 --- /dev/null +++ b/provider/rancher/config_root.go @@ -0,0 +1,12 @@ +package rancher + +import ( + "github.com/containous/traefik/types" +) + +func (p *Provider) buildConfiguration(containersInspected []rancherData) *types.Configuration { + if p.TemplateVersion == 1 { + return p.buildConfigurationV1(containersInspected) + } + return p.buildConfigurationV2(containersInspected) +} diff --git a/provider/rancher/config_test.go b/provider/rancher/config_test.go index 0a1d746f0..19da23e6e 100644 --- a/provider/rancher/config_test.go +++ b/provider/rancher/config_test.go @@ -251,6 +251,172 @@ func TestProviderBuildConfiguration(t *testing.T) { }, }, }, + { + desc: "when all segment labels are set", + services: []rancherData{ + { + 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.SuffixFrontendWhiteListUseXForwardedFor: "true", + + 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", + }, + Health: "healthy", + Containers: []string{"10.0.0.1", "10.0.0.2"}, + }, + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes": { + EntryPoints: []string{"http", "https"}, + Backend: "backend-sauternes", + Routes: map[string]types.Route{ + "route-frontend-sauternes": { + Rule: "Host:.rancher.localhost", + }, + }, + PassHostHeader: true, + PassTLSCert: true, + Priority: 666, + BasicAuth: []string{ + "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }, + WhiteList: &types.WhiteList{ + SourceRange: []string{ + "10.10.10.10", + }, + UseXForwardedFor: true, + }, + 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{ + "bar": { + Status: []string{"500", "600"}, + Backend: "foobar", + Query: "bar_query", + }, + "foo": { + Status: []string{"404"}, + Backend: "foobar", + Query: "foo_query", + }, + }, + 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, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-sauternes": { + Servers: map[string]types.Server{ + "server-0": { + URL: "https://10.0.0.1:666", + Weight: 12, + }, + "server-1": { + URL: "https://10.0.0.2:666", + Weight: 12, + }, + }, + }, + }, + }, { desc: "with services", services: []rancherData{ @@ -298,7 +464,6 @@ func TestProviderBuildConfiguration(t *testing.T) { for _, test := range testCases { test := test - t.Run(test.desc, func(t *testing.T) { t.Parallel() @@ -634,298 +799,15 @@ func TestGetBackendName(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() + segmentProperties := label.ExtractTraefikLabels(test.service.Labels) + test.service.SegmentLabels = segmentProperties[""] + actual := getBackendName(test.service) assert.Equal(t, test.expected, actual) }) } } -func TestGetCircuitBreaker(t *testing.T) { - testCases := []struct { - desc string - service rancherData - expected *types.CircuitBreaker - }{ - { - desc: "should return nil when no CB label", - service: rancherData{ - Labels: map[string]string{}, - Health: "healthy", - State: "active", - }, - expected: nil, - }, - { - desc: "should return a struct when CB label is set", - service: rancherData{ - Labels: map[string]string{ - label.TraefikBackendCircuitBreakerExpression: "NetworkErrorRatio() > 0.5", - }, - Health: "healthy", - State: "active", - }, - expected: &types.CircuitBreaker{ - Expression: "NetworkErrorRatio() > 0.5", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getCircuitBreaker(test.service) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetLoadBalancer(t *testing.T) { - testCases := []struct { - desc string - service rancherData - expected *types.LoadBalancer - }{ - { - desc: "should return nil when no LB labels", - service: rancherData{ - Labels: map[string]string{}, - Health: "healthy", - State: "active", - }, - expected: nil, - }, - { - desc: "should return a struct when labels are set", - service: rancherData{ - Labels: map[string]string{ - label.TraefikBackendLoadBalancerMethod: "drr", - label.TraefikBackendLoadBalancerSticky: "true", - label.TraefikBackendLoadBalancerStickiness: "true", - label.TraefikBackendLoadBalancerStickinessCookieName: "foo", - }, - Health: "healthy", - State: "active", - }, - expected: &types.LoadBalancer{ - Method: "drr", - Sticky: true, - Stickiness: &types.Stickiness{ - CookieName: "foo", - }, - }, - }, - { - desc: "should return a nil Stickiness when Stickiness is not set", - service: rancherData{ - Labels: map[string]string{ - label.TraefikBackendLoadBalancerMethod: "drr", - label.TraefikBackendLoadBalancerSticky: "true", - label.TraefikBackendLoadBalancerStickinessCookieName: "foo", - }, - Health: "healthy", - State: "active", - }, - expected: &types.LoadBalancer{ - Method: "drr", - Sticky: true, - Stickiness: nil, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getLoadBalancer(test.service) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetMaxConn(t *testing.T) { - testCases := []struct { - desc string - service rancherData - expected *types.MaxConn - }{ - { - desc: "should return nil when no max conn labels", - service: rancherData{ - Labels: map[string]string{}, - Health: "healthy", - State: "active", - }, - expected: nil, - }, - { - desc: "should return nil when no amount label", - service: rancherData{ - Labels: map[string]string{ - label.TraefikBackendMaxConnExtractorFunc: "client.ip", - }, - Health: "healthy", - State: "active", - }, - expected: nil, - }, - { - desc: "should return default when no empty extractorFunc label", - service: rancherData{ - Labels: map[string]string{ - label.TraefikBackendMaxConnExtractorFunc: "", - label.TraefikBackendMaxConnAmount: "666", - }, - Health: "healthy", - State: "active", - }, - expected: &types.MaxConn{ - ExtractorFunc: "request.host", - Amount: 666, - }, - }, - { - desc: "should return a struct when max conn labels are set", - service: rancherData{ - Labels: map[string]string{ - label.TraefikBackendMaxConnExtractorFunc: "client.ip", - label.TraefikBackendMaxConnAmount: "666", - }, - Health: "healthy", - State: "active", - }, - expected: &types.MaxConn{ - ExtractorFunc: "client.ip", - Amount: 666, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getMaxConn(test.service) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetHealthCheck(t *testing.T) { - testCases := []struct { - desc string - service rancherData - expected *types.HealthCheck - }{ - { - desc: "should return nil when no health check labels", - service: rancherData{ - Labels: map[string]string{}, - Health: "healthy", - State: "active", - }, - expected: nil, - }, - { - desc: "should return nil when no health check Path label", - service: rancherData{ - Labels: map[string]string{ - label.TraefikBackendHealthCheckPort: "80", - label.TraefikBackendHealthCheckInterval: "6", - }, - Health: "healthy", - State: "active", - }, - expected: nil, - }, - { - desc: "should return a struct when health check labels are set", - service: rancherData{ - Labels: map[string]string{ - label.TraefikBackendHealthCheckPath: "/health", - label.TraefikBackendHealthCheckPort: "80", - label.TraefikBackendHealthCheckInterval: "6", - }, - Health: "healthy", - State: "active", - }, - expected: &types.HealthCheck{ - Path: "/health", - Port: 80, - Interval: "6", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getHealthCheck(test.service) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetBuffering(t *testing.T) { - testCases := []struct { - desc string - service rancherData - expected *types.Buffering - }{ - { - desc: "should return nil when no buffering labels", - service: rancherData{ - Labels: map[string]string{}, - Health: "healthy", - State: "active", - }, - expected: nil, - }, - { - desc: "should return a struct when buffering labels are set", - service: rancherData{ - Labels: map[string]string{ - label.TraefikBackendBufferingMaxResponseBodyBytes: "10485760", - label.TraefikBackendBufferingMemResponseBodyBytes: "2097152", - label.TraefikBackendBufferingMaxRequestBodyBytes: "10485760", - label.TraefikBackendBufferingMemRequestBodyBytes: "2097152", - label.TraefikBackendBufferingRetryExpression: "IsNetworkError() && Attempts() <= 2", - }, - Health: "healthy", - State: "active", - }, - 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() - - actual := getBuffering(test.service) - - assert.Equal(t, test.expected, actual) - }) - } -} - func TestGetServers(t *testing.T) { testCases := []struct { desc string @@ -998,352 +880,10 @@ func TestGetServers(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() + segmentProperties := label.ExtractTraefikLabels(test.service.Labels) + test.service.SegmentLabels = segmentProperties[""] + actual := getServers(test.service) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestWhiteList(t *testing.T) { - testCases := []struct { - desc string - service rancherData - expected *types.WhiteList - }{ - { - desc: "should return nil when no white list labels", - service: rancherData{ - Labels: map[string]string{}, - Health: "healthy", - State: "active", - }, - expected: nil, - }, - { - desc: "should return a struct when only range", - service: rancherData{ - Labels: map[string]string{ - label.TraefikFrontendWhiteListSourceRange: "10.10.10.10", - }, - Health: "healthy", - State: "active", - }, - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: false, - }, - }, - { - desc: "should return a struct when range and UseXForwardedFor", - service: rancherData{ - Labels: map[string]string{ - label.TraefikFrontendWhiteListSourceRange: "10.10.10.10", - label.TraefikFrontendWhiteListUseXForwardedFor: "true", - }, - Health: "healthy", - State: "active", - }, - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: true, - }, - }, - { - desc: "should return nil when only UseXForwardedFor", - service: rancherData{ - Labels: map[string]string{ - label.TraefikFrontendWhiteListUseXForwardedFor: "true", - }, - Health: "healthy", - State: "active", - }, - expected: nil, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getWhiteList(test.service) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetRedirect(t *testing.T) { - testCases := []struct { - desc string - service rancherData - expected *types.Redirect - }{ - - { - desc: "should return nil when no redirect labels", - service: rancherData{ - Labels: map[string]string{}, - Health: "healthy", - State: "active", - }, - expected: nil, - }, - { - desc: "should use only entry point tag when mix regex redirect and entry point redirect", - service: rancherData{ - Labels: map[string]string{ - label.TraefikFrontendRedirectEntryPoint: "https", - label.TraefikFrontendRedirectRegex: "(.*)", - label.TraefikFrontendRedirectReplacement: "$1", - }, - Health: "healthy", - State: "active", - }, - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when entry point redirect label", - service: rancherData{ - Labels: map[string]string{ - label.TraefikFrontendRedirectEntryPoint: "https", - }, - Health: "healthy", - State: "active", - }, - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when entry point redirect label (permanent)", - service: rancherData{ - Labels: map[string]string{ - label.TraefikFrontendRedirectEntryPoint: "https", - label.TraefikFrontendRedirectPermanent: "true", - }, - Health: "healthy", - State: "active", - }, - expected: &types.Redirect{ - EntryPoint: "https", - Permanent: true, - }, - }, - { - desc: "should return a struct when regex redirect labels", - service: rancherData{ - Labels: map[string]string{ - label.TraefikFrontendRedirectRegex: "(.*)", - label.TraefikFrontendRedirectReplacement: "$1", - }, - Health: "healthy", - State: "active", - }, - expected: &types.Redirect{ - Regex: "(.*)", - Replacement: "$1", - }, - }, - { - desc: "should return a struct when regex redirect labels (permanent)", - service: rancherData{ - Labels: map[string]string{ - label.TraefikFrontendRedirectRegex: "(.*)", - label.TraefikFrontendRedirectReplacement: "$1", - label.TraefikFrontendRedirectPermanent: "true", - }, - Health: "healthy", - State: "active", - }, - expected: &types.Redirect{ - Regex: "(.*)", - Replacement: "$1", - Permanent: true, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getRedirect(test.service) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetRateLimit(t *testing.T) { - testCases := []struct { - desc string - service rancherData - expected *types.RateLimit - }{ - { - desc: "should return nil when no rate limit labels", - service: rancherData{ - Labels: map[string]string{}, - Health: "healthy", - State: "active", - }, - expected: nil, - }, - { - desc: "should return a struct when rate limit labels are defined", - service: rancherData{ - Labels: map[string]string{ - label.TraefikFrontendRateLimitExtractorFunc: "client.ip", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", - }, - Health: "healthy", - State: "active", - }, - expected: &types.RateLimit{ - ExtractorFunc: "client.ip", - RateSet: map[string]*types.Rate{ - "foo": { - Period: flaeg.Duration(6 * time.Second), - Average: 12, - Burst: 18, - }, - "bar": { - Period: flaeg.Duration(3 * time.Second), - Average: 6, - Burst: 9, - }, - }, - }, - }, - { - desc: "should return nil when ExtractorFunc is missing", - service: rancherData{ - Labels: map[string]string{ - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", - }, - Health: "healthy", - State: "active", - }, - expected: nil, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getRateLimit(test.service) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetHeaders(t *testing.T) { - testCases := []struct { - desc string - service rancherData - expected *types.Headers - }{ - { - desc: "should return nil when no custom headers options are set", - service: rancherData{ - Labels: map[string]string{}, - Health: "healthy", - State: "active", - }, - expected: nil, - }, - { - desc: "should return a struct when all custom headers options are set", - service: rancherData{ - Labels: map[string]string{ - label.TraefikFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.TraefikFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.TraefikFrontendSSLProxyHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.TraefikFrontendAllowedHosts: "foo,bar,bor", - label.TraefikFrontendHostsProxyHeaders: "foo,bar,bor", - label.TraefikFrontendSSLHost: "foo", - label.TraefikFrontendCustomFrameOptionsValue: "foo", - label.TraefikFrontendContentSecurityPolicy: "foo", - label.TraefikFrontendPublicKey: "foo", - label.TraefikFrontendReferrerPolicy: "foo", - label.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", - }, - Health: "healthy", - State: "active", - }, - expected: &types.Headers{ - CustomRequestHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - CustomResponseHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - SSLProxyHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - AllowedHosts: []string{"foo", "bar", "bor"}, - HostsProxyHeaders: []string{"foo", "bar", "bor"}, - SSLHost: "foo", - CustomFrameOptionsValue: "foo", - ContentSecurityPolicy: "foo", - PublicKey: "foo", - ReferrerPolicy: "foo", - 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() - - actual := getHeaders(test.service) - assert.Equal(t, test.expected, actual) }) } diff --git a/provider/rancher/deprecated_config.go b/provider/rancher/deprecated_config.go new file mode 100644 index 000000000..003eb4a33 --- /dev/null +++ b/provider/rancher/deprecated_config.go @@ -0,0 +1,226 @@ +package rancher + +import ( + "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" +) + +func (p *Provider) buildConfigurationV1(services []rancherData) *types.Configuration { + var RancherFuncMap = template.FuncMap{ + "getDomain": getFuncStringV1(label.TraefikDomain, p.Domain), + + // Backend functions + "getPort": getFuncStringV1(label.TraefikPort, ""), + "getProtocol": getFuncStringV1(label.TraefikProtocol, label.DefaultProtocol), + "getWeight": getFuncIntV1(label.TraefikWeight, label.DefaultWeightInt), + "hasCircuitBreakerLabel": hasFuncV1(label.TraefikBackendCircuitBreakerExpression), + "getCircuitBreakerExpression": getFuncStringV1(label.TraefikBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression), + "hasLoadBalancerLabel": hasLoadBalancerLabel, + "getLoadBalancerMethod": getFuncStringV1(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), + "hasMaxConnLabels": hasMaxConnLabels, + "getMaxConnAmount": getFuncInt64V1(label.TraefikBackendMaxConnAmount, 0), + "getMaxConnExtractorFunc": getFuncStringV1(label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc), + "getSticky": getStickyV1, + "hasStickinessLabel": hasFuncV1(label.TraefikBackendLoadBalancerStickiness), + "getStickinessCookieName": getFuncStringV1(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName), + + // Frontend functions + "getBackend": getBackendNameV1, + "getFrontendRule": p.getFrontendRule, + "getPriority": getFuncIntV1(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), + "getPassHostHeader": getFuncBoolV1(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getEntryPoints": getFuncSliceStringV1(label.TraefikFrontendEntryPoints), + "getBasicAuth": getFuncSliceStringV1(label.TraefikFrontendAuthBasic), + "hasRedirect": hasRedirect, + "getRedirectEntryPoint": getRedirectEntryPoint, + "getRedirectRegex": getRedirectRegex, + "getRedirectReplacement": getRedirectReplacement, + } + + // filter services + filteredServices := fun.Filter(p.serviceFilterV1, services).([]rancherData) + + frontends := map[string]rancherData{} + backends := map[string]rancherData{} + + for _, service := range filteredServices { + frontendName := p.getFrontendNameV1(service) + frontends[frontendName] = service + backendName := getBackendNameV1(service) + backends[backendName] = service + } + + templateObjects := struct { + Frontends map[string]rancherData + Backends map[string]rancherData + Domain string + }{ + Frontends: frontends, + Backends: backends, + Domain: p.Domain, + } + + configuration, err := p.GetConfiguration("templates/rancher-v1.tmpl", RancherFuncMap, templateObjects) + if err != nil { + log.Error(err) + } + + return configuration +} + +// Deprecated +func (p *Provider) serviceFilterV1(service rancherData) bool { + if service.Labels[label.TraefikPort] == "" { + log.Debugf("Filtering service %s without traefik.port label", service.Name) + return false + } + + if !label.IsEnabled(service.Labels, p.ExposedByDefault) { + log.Debugf("Filtering disabled service %s", service.Name) + return false + } + + constraintTags := label.GetSliceStringValue(service.Labels, label.TraefikTags) + if ok, failingConstraint := p.MatchConstraints(constraintTags); !ok { + if failingConstraint != nil { + log.Debugf("Filtering service %s with constraint %s", service.Name, failingConstraint.String()) + } + return false + } + + // Only filter services by Health (HealthState) and State if EnableServiceHealthFilter is true + if p.EnableServiceHealthFilter { + + if service.Health != "" && service.Health != healthy && service.Health != updatingHealthy { + log.Debugf("Filtering service %s with healthState of %s", service.Name, service.Health) + return false + } + if service.State != "" && service.State != active && service.State != updatingActive && service.State != upgraded && service.State != upgrading { + log.Debugf("Filtering service %s with state of %s", service.Name, service.State) + return false + } + } + + return true +} + +// Deprecated +func (p *Provider) getFrontendNameV1(service rancherData) string { + return provider.Normalize(p.getFrontendRule(service)) +} + +// Deprecated +func getBackendNameV1(service rancherData) string { + backend := label.GetStringValue(service.Labels, label.TraefikBackend, service.Name) + return provider.Normalize(backend) +} + +// TODO: Deprecated +// replaced by Stickiness +// Deprecated +func getStickyV1(service rancherData) bool { + if label.Has(service.Labels, label.TraefikBackendLoadBalancerSticky) { + log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) + } + return label.GetBoolValue(service.Labels, label.TraefikBackendLoadBalancerSticky, false) +} + +// Deprecated +func hasLoadBalancerLabel(service rancherData) bool { + method := label.Has(service.Labels, label.TraefikBackendLoadBalancerMethod) + sticky := label.Has(service.Labels, label.TraefikBackendLoadBalancerSticky) + stickiness := label.Has(service.Labels, label.TraefikBackendLoadBalancerStickiness) + cookieName := label.Has(service.Labels, label.TraefikBackendLoadBalancerStickinessCookieName) + return method || sticky || stickiness || cookieName +} + +// Deprecated +func hasMaxConnLabels(service rancherData) bool { + mca := label.Has(service.Labels, label.TraefikBackendMaxConnAmount) + mcef := label.Has(service.Labels, label.TraefikBackendMaxConnExtractorFunc) + return mca && mcef +} + +func hasRedirect(service rancherData) bool { + value := label.GetStringValue(service.Labels, label.TraefikFrontendRedirectEntryPoint, "") + frep := len(value) > 0 + value = label.GetStringValue(service.Labels, label.TraefikFrontendRedirectRegex, "") + frrg := len(value) > 0 + value = label.GetStringValue(service.Labels, label.TraefikFrontendRedirectReplacement, "") + frrp := len(value) > 0 + + return frep || frrg && frrp +} + +func getRedirectEntryPoint(service rancherData) string { + value := label.GetStringValue(service.Labels, label.TraefikFrontendRedirectEntryPoint, "") + if len(value) == 0 { + return "" + } + return value +} + +func getRedirectRegex(service rancherData) string { + value := label.GetStringValue(service.Labels, label.TraefikFrontendRedirectRegex, "") + if len(value) == 0 { + return "" + } + return value +} + +func getRedirectReplacement(service rancherData) string { + value := label.GetStringValue(service.Labels, label.TraefikFrontendRedirectReplacement, "") + if len(value) == 0 { + return "" + } + return value +} + +// Label functions + +// Deprecated +func getFuncStringV1(labelName string, defaultValue string) func(service rancherData) string { + return func(service rancherData) string { + return label.GetStringValue(service.Labels, labelName, defaultValue) + } +} + +// Deprecated +func getFuncBoolV1(labelName string, defaultValue bool) func(service rancherData) bool { + return func(service rancherData) bool { + return label.GetBoolValue(service.Labels, labelName, defaultValue) + } +} + +// Deprecated +func getFuncIntV1(labelName string, defaultValue int) func(service rancherData) int { + return func(service rancherData) int { + return label.GetIntValue(service.Labels, labelName, defaultValue) + } +} + +// Deprecated +func getFuncInt64V1(labelName string, defaultValue int64) func(service rancherData) int64 { + return func(service rancherData) int64 { + return label.GetInt64Value(service.Labels, labelName, defaultValue) + } +} + +// Deprecated +func getFuncSliceStringV1(labelName string) func(service rancherData) []string { + return func(service rancherData) []string { + return label.GetSliceStringValue(service.Labels, labelName) + } +} + +// Deprecated +func hasFuncV1(labelName string) func(service rancherData) bool { + return func(service rancherData) bool { + return label.Has(service.Labels, labelName) + } +} diff --git a/provider/rancher/deprecated_config_test.go b/provider/rancher/deprecated_config_test.go new file mode 100644 index 000000000..c0d6d9836 --- /dev/null +++ b/provider/rancher/deprecated_config_test.go @@ -0,0 +1,503 @@ +package rancher + +import ( + "testing" + + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProviderBuildConfigurationV1(t *testing.T) { + provider := &Provider{ + Domain: "rancher.localhost", + ExposedByDefault: true, + } + + testCases := []struct { + desc string + services []rancherData + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{ + { + desc: "without services", + services: []rancherData{}, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + desc: "when all labels are set", + services: []rancherData{ + { + Labels: map[string]string{ + label.TraefikPort: "666", + label.TraefikProtocol: "https", + label.TraefikWeight: "12", + + label.TraefikBackend: "foobar", + + label.TraefikBackendCircuitBreakerExpression: "NetworkErrorRatio() > 0.5", + label.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.TraefikFrontendPriority: "666", + label.TraefikFrontendRedirectEntryPoint: "https", + label.TraefikFrontendRedirectRegex: "nope", + label.TraefikFrontendRedirectReplacement: "nope", + label.TraefikFrontendRule: "Host:traefik.io", + }, + Health: "healthy", + Containers: []string{"10.0.0.1", "10.0.0.2"}, + }, + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-traefik-io": { + EntryPoints: []string{ + "http", + "https", + }, + Backend: "backend-foobar", + Routes: map[string]types.Route{ + "route-frontend-Host-traefik-io": { + Rule: "Host:traefik.io", + }, + }, + PassHostHeader: true, + Priority: 666, + BasicAuth: []string{ + "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }, + Redirect: &types.Redirect{ + EntryPoint: "https", + Regex: "nope", + Replacement: "nope", + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foobar": { + Servers: map[string]types.Server{ + "server-0": { + URL: "https://10.0.0.1:666", + Weight: 12, + }, + "server-1": { + URL: "https://10.0.0.2:666", + Weight: 12, + }, + }, + CircuitBreaker: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", + }, + LoadBalancer: &types.LoadBalancer{ + Method: "drr", + Sticky: true, + Stickiness: &types.Stickiness{ + CookieName: "chocolate", + }, + }, + MaxConn: &types.MaxConn{ + Amount: 666, + ExtractorFunc: "client.ip", + }, + }, + }, + }, + { + desc: "with services", + services: []rancherData{ + { + Name: "test/service", + Labels: map[string]string{ + label.TraefikPort: "80", + label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.TraefikFrontendRedirectEntryPoint: "https", + }, + Health: "healthy", + Containers: []string{"127.0.0.1"}, + }, + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-test-service-rancher-localhost": { + Backend: "backend-test-service", + PassHostHeader: true, + EntryPoints: []string{}, + BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + Priority: 0, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + Routes: map[string]types.Route{ + "route-frontend-Host-test-service-rancher-localhost": { + Rule: "Host:test.service.rancher.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test-service": { + Servers: map[string]types.Server{ + "server-0": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actualConfig := provider.buildConfigurationV1(test.services) + require.NotNil(t, actualConfig) + + assert.EqualValues(t, test.expectedBackends, actualConfig.Backends) + assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends) + }) + } +} + +func TestProviderServiceFilterV1(t *testing.T) { + provider := &Provider{ + Domain: "rancher.localhost", + EnableServiceHealthFilter: true, + } + + constraint, _ := types.NewConstraint("tag==ch*se") + provider.Constraints = types.Constraints{constraint} + + testCases := []struct { + desc string + service rancherData + expected bool + }{ + { + desc: "missing Port labels, don't respect constraint", + service: rancherData{ + Labels: map[string]string{ + label.TraefikEnable: "true", + }, + Health: "healthy", + State: "active", + }, + expected: false, + }, + { + desc: "don't respect constraint", + service: rancherData{ + Labels: map[string]string{ + label.TraefikPort: "80", + label.TraefikEnable: "false", + }, + Health: "healthy", + State: "active", + }, + expected: false, + }, + { + desc: "unhealthy", + service: rancherData{ + Labels: map[string]string{ + label.TraefikTags: "cheese", + label.TraefikPort: "80", + label.TraefikEnable: "true", + }, + Health: "unhealthy", + State: "active", + }, + expected: false, + }, + { + desc: "inactive", + service: rancherData{ + Labels: map[string]string{ + label.TraefikTags: "not-cheesy", + label.TraefikPort: "80", + label.TraefikEnable: "true", + }, + Health: "healthy", + State: "inactive", + }, + expected: false, + }, + { + desc: "healthy & active, tag: cheese", + service: rancherData{ + Labels: map[string]string{ + label.TraefikTags: "cheese", + label.TraefikPort: "80", + label.TraefikEnable: "true", + }, + Health: "healthy", + State: "active", + }, + expected: true, + }, + { + desc: "healthy & active, tag: chose", + service: rancherData{ + Labels: map[string]string{ + label.TraefikTags: "chose", + label.TraefikPort: "80", + label.TraefikEnable: "true", + }, + Health: "healthy", + State: "active", + }, + expected: true, + }, + { + desc: "healthy & upgraded", + service: rancherData{ + Labels: map[string]string{ + label.TraefikTags: "cheeeeese", + label.TraefikPort: "80", + label.TraefikEnable: "true", + }, + Health: "healthy", + State: "upgraded", + }, + expected: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := provider.serviceFilterV1(test.service) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestContainerFilterV1(t *testing.T) { + testCases := []struct { + name string + healthState string + state string + expected bool + }{ + { + healthState: "unhealthy", + state: "running", + expected: false, + }, + { + healthState: "healthy", + state: "stopped", + expected: false, + }, + { + state: "stopped", + expected: false, + }, + { + healthState: "healthy", + state: "running", + expected: true, + }, + { + healthState: "updating-healthy", + state: "updating-running", + expected: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.healthState+" "+test.state, func(t *testing.T) { + t.Parallel() + + actual := containerFilter(test.name, test.healthState, test.state) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestProviderGetFrontendNameV1(t *testing.T) { + provider := &Provider{Domain: "rancher.localhost"} + + testCases := []struct { + desc string + service rancherData + expected string + }{ + { + desc: "default", + service: rancherData{ + Name: "foo", + }, + expected: "Host-foo-rancher-localhost", + }, + { + desc: "with Headers label", + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + label.TraefikFrontendRule: "Headers:User-Agent,bat/0.1.0", + }, + }, + expected: "Headers-User-Agent-bat-0-1-0", + }, + { + desc: "with Host label", + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + label.TraefikFrontendRule: "Host:foo.bar", + }, + }, + expected: "Host-foo-bar", + }, + { + desc: "with Path label", + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + label.TraefikFrontendRule: "Path:/test", + }, + }, + expected: "Path-test", + }, + { + desc: "with PathPrefix label", + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + label.TraefikFrontendRule: "PathPrefix:/test2", + }, + }, + expected: "PathPrefix-test2", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := provider.getFrontendNameV1(test.service) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestProviderGetFrontendRuleV1(t *testing.T) { + provider := &Provider{Domain: "rancher.localhost"} + + testCases := []struct { + desc string + service rancherData + expected string + }{ + { + desc: "host", + service: rancherData{ + Name: "foo", + }, + expected: "Host:foo.rancher.localhost", + }, + { + desc: "host with /", + service: rancherData{ + Name: "foo/bar", + }, + expected: "Host:foo.bar.rancher.localhost", + }, + { + desc: "with Host label", + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + label.TraefikFrontendRule: "Host:foo.bar.com", + }, + }, + expected: "Host:foo.bar.com", + }, + { + desc: "with Path label", + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + label.TraefikFrontendRule: "Path:/test", + }, + }, + expected: "Path:/test", + }, + { + desc: "with PathPrefix label", + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + label.TraefikFrontendRule: "PathPrefix:/test2", + }, + }, + expected: "PathPrefix:/test2", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := provider.getFrontendRule(test.service) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetBackendNameV1(t *testing.T) { + testCases := []struct { + desc string + service rancherData + expected string + }{ + { + desc: "without label", + service: rancherData{ + Name: "test-service", + }, + expected: "test-service", + }, + { + desc: "with label", + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + label.TraefikBackend: "foobar", + }, + }, + + expected: "foobar", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getBackendNameV1(test.service) + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/provider/rancher/rancher.go b/provider/rancher/rancher.go index 983f4c2e0..7952fc55e 100644 --- a/provider/rancher/rancher.go +++ b/provider/rancher/rancher.go @@ -38,11 +38,13 @@ type Provider struct { } type rancherData struct { - Name string - Labels map[string]string // List of labels set to container or service - Containers []string - Health string - State string + Name string + Labels map[string]string // List of labels set to container or service + Containers []string + Health string + State string + SegmentLabels map[string]string + SegmentName string } func (r rancherData) String() string { diff --git a/templates/marathon-v1.tmpl b/templates/marathon-v1.tmpl new file mode 100644 index 000000000..2abd83de5 --- /dev/null +++ b/templates/marathon-v1.tmpl @@ -0,0 +1,68 @@ +{{$apps := .Applications}} + +{{range $app := $apps }} +{{range $task := $app.Tasks }} +{{range $serviceIndex, $serviceName := getServiceNames $app }} + [backends."{{ getBackend $app $serviceName }}".servers."server-{{ $task.ID | replace "." "-"}}{{getServiceNameSuffix $serviceName }}"] + url = "{{ getProtocol $app $serviceName }}://{{ getBackendServer $task $app }}:{{ getPort $task $app $serviceName }}" + weight = {{ getWeight $app $serviceName }} +{{end}} +{{end}} +{{end}} + +{{range $app := $apps }} +{{range $serviceIndex, $serviceName := getServiceNames $app }} + +[backends."{{ getBackend $app $serviceName }}"] + {{if hasMaxConnLabels $app }} + [backends."{{ getBackend $app $serviceName }}".maxConn] + amount = {{ getMaxConnAmount $app }} + extractorFunc = "{{ getMaxConnExtractorFunc $app }}" + {{end}} + + {{if hasLoadBalancerLabels $app }} + [backends."{{ getBackend $app $serviceName }}".loadBalancer] + method = "{{ getLoadBalancerMethod $app }}" + sticky = {{ getSticky $app }} + {{if hasStickinessLabel $app }} + [backends."{{ getBackend $app $serviceName }}".loadBalancer.stickiness] + cookieName = "{{ getStickinessCookieName $app }}" + {{end}} + {{end}} + + {{if hasCircuitBreakerLabels $app }} + [backends."{{ getBackend $app $serviceName }}".circuitBreaker] + expression = "{{ getCircuitBreakerExpression $app }}" + {{end}} + + {{if hasHealthCheckLabels $app }} + [backends."{{ getBackend $app $serviceName }}".healthCheck] + path = "{{ getHealthCheckPath $app }}" + interval = "{{ getHealthCheckInterval $app }}" + {{end}} + +{{end}} +{{end}} + +[frontends] +{{range $app := $apps }} +{{range $serviceIndex, $serviceName := getServiceNames . }} + + [frontends."{{ getFrontendName $app $serviceName | normalize }}"] + backend = "{{ getBackend $app $serviceName }}" + passHostHeader = {{ getPassHostHeader $app $serviceName }} + priority = {{ getPriority $app $serviceName }} + + entryPoints = [{{range getEntryPoints $app $serviceName }} + "{{.}}", + {{end}}] + + basicAuth = [{{range getBasicAuth $app $serviceName }} + "{{.}}", + {{end}}] + + [frontends."{{ getFrontendName $app $serviceName | normalize }}".routes."route-host{{ $app.ID | replace "/" "-" }}{{ getServiceNameSuffix $serviceName }}"] + rule = "{{ getFrontendRule $app $serviceName }}" + +{{end}} +{{end}} diff --git a/templates/marathon.tmpl b/templates/marathon.tmpl index d8673515b..2bc3dfb58 100644 --- a/templates/marathon.tmpl +++ b/templates/marathon.tmpl @@ -2,18 +2,17 @@ [backends] {{range $app := $apps }} -{{range $serviceIndex, $serviceName := getServiceNames $app }} - {{ $backendName := getBackend $app $serviceName}} + {{ $backendName := getBackendName $app }} [backends."{{ $backendName }}"] - {{ $circuitBreaker := getCircuitBreaker $app }} + {{ $circuitBreaker := getCircuitBreaker $app.SegmentLabels }} {{if $circuitBreaker }} [backends."{{ $backendName }}".circuitBreaker] expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{ $loadBalancer := getLoadBalancer $app }} + {{ $loadBalancer := getLoadBalancer $app.SegmentLabels }} {{if $loadBalancer }} [backends."{{ $backendName }}".loadBalancer] method = "{{ $loadBalancer.Method }}" @@ -24,14 +23,14 @@ {{end}} {{end}} - {{ $maxConn := getMaxConn $app }} + {{ $maxConn := getMaxConn $app.SegmentLabels }} {{if $maxConn }} [backends."{{ $backendName }}".maxConn] extractorFunc = "{{ $maxConn.ExtractorFunc }}" amount = {{ $maxConn.Amount }} {{end}} - {{ $healthCheck := getHealthCheck $app }} + {{ $healthCheck := getHealthCheck $app.SegmentLabels }} {{if $healthCheck }} [backends."{{ $backendName }}".healthCheck] path = "{{ $healthCheck.Path }}" @@ -39,7 +38,7 @@ interval = "{{ $healthCheck.Interval }}" {{end}} - {{ $buffering := getBuffering $app }} + {{ $buffering := getBuffering $app.SegmentLabels }} {{if $buffering }} [backends."{{ $backendName }}".buffering] maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} @@ -49,35 +48,33 @@ retryExpression = "{{ $buffering.RetryExpression }}" {{end}} - {{range $serverName, $server := getServers $app $serviceName }} + {{range $serverName, $server := getServers $app }} [backends."{{ $backendName }}".servers."{{ $serverName }}"] url = "{{ $server.URL }}" weight = {{ $server.Weight }} {{end}} {{end}} -{{end}} [frontends] {{range $app := $apps }} -{{range $serviceIndex, $serviceName := getServiceNames $app }} - {{ $frontendName := getFrontendName $app $serviceName }} + {{ $frontendName := getFrontendName $app }} [frontends."{{ $frontendName }}"] - backend = "{{ getBackend $app $serviceName }}" - priority = {{ getPriority $app $serviceName }} - passHostHeader = {{ getPassHostHeader $app $serviceName }} - passTLSCert = {{ getPassTLSCert $app $serviceName }} + backend = "{{ getBackendName $app }}" + priority = {{ getPriority $app.SegmentLabels }} + passHostHeader = {{ getPassHostHeader $app.SegmentLabels }} + passTLSCert = {{ getPassTLSCert $app.SegmentLabels }} - entryPoints = [{{range getEntryPoints $app $serviceName }} + entryPoints = [{{range getEntryPoints $app.SegmentLabels }} "{{.}}", {{end}}] - basicAuth = [{{range getBasicAuth $app $serviceName }} + basicAuth = [{{range getBasicAuth $app.SegmentLabels }} "{{.}}", {{end}}] - {{ $whitelist := getWhiteList $app $serviceName }} + {{ $whitelist := getWhiteList $app.SegmentLabels }} {{if $whitelist }} [frontends."{{ $frontendName }}".whiteList] sourceRange = [{{range $whitelist.SourceRange }} @@ -86,7 +83,7 @@ useXForwardedFor = {{ $whitelist.UseXForwardedFor }} {{end}} - {{ $redirect := getRedirect $app $serviceName }} + {{ $redirect := getRedirect $app.SegmentLabels }} {{if $redirect }} [frontends."{{ $frontendName }}".redirect] entryPoint = "{{ $redirect.EntryPoint }}" @@ -95,7 +92,7 @@ permanent = {{ $redirect.Permanent }} {{end}} - {{ $errorPages := getErrorPages $app $serviceName }} + {{ $errorPages := getErrorPages $app.SegmentLabels }} {{if $errorPages }} [frontends."{{ $frontendName }}".errors] {{range $pageName, $page := $errorPages }} @@ -108,7 +105,7 @@ {{end}} {{end}} - {{ $rateLimit := getRateLimit $app $serviceName }} + {{ $rateLimit := getRateLimit $app.SegmentLabels }} {{if $rateLimit }} [frontends."{{ $frontendName }}".rateLimit] extractorFunc = "{{ $rateLimit.ExtractorFunc }}" @@ -121,7 +118,7 @@ {{end}} {{end}} - {{ $headers := getHeaders $app $serviceName }} + {{ $headers := getHeaders $app.SegmentLabels }} {{if $headers }} [frontends."{{ $frontendName }}".headers] SSLRedirect = {{ $headers.SSLRedirect }} @@ -175,8 +172,7 @@ {{end}} {{end}} - [frontends."{{ $frontendName }}".routes."route-host{{ $app.ID | replace "/" "-" }}{{ getServiceNameSuffix $serviceName }}"] - rule = "{{ getFrontendRule $app $serviceName }}" + [frontends."{{ $frontendName }}".routes."route-host{{ $app.ID | replace "/" "-" }}{{ getSegmentNameSuffix $app.SegmentName }}"] + rule = "{{ getFrontendRule $app }}" {{end}} -{{end}} diff --git a/templates/rancher-v1.tmpl b/templates/rancher-v1.tmpl new file mode 100644 index 000000000..b1da1d472 --- /dev/null +++ b/templates/rancher-v1.tmpl @@ -0,0 +1,58 @@ +{{$backendServers := .Backends}} + +[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}} + + {{range $index, $ip := $backend.Containers }} + [backends."backend-{{ $backendName }}".servers."server-{{ $index }}"] + url = "{{ getProtocol $backend }}://{{ $ip }}:{{ getPort $backend }}" + weight = {{ getWeight $backend }} + {{end}} + +{{end}} + +[frontends] +{{range $frontendName, $service := .Frontends }} + [frontends."frontend-{{ $frontendName }}"] + backend = "backend-{{ getBackend $service }}" + passHostHeader = {{ getPassHostHeader $service }} + priority = {{ getPriority $service }} + + entryPoints = [{{range getEntryPoints $service }} + "{{.}}", + {{end}}] + + basicAuth = [{{range getBasicAuth $service }} + "{{.}}", + {{end}}] + + {{if hasRedirect $service }} + [frontends."frontend-{{ $frontendName }}".redirect] + entryPoint = "{{ getRedirectEntryPoint $service }}" + regex = "{{ getRedirectRegex $service }}" + replacement = "{{ getRedirectReplacement $service }}" + {{end}} + + [frontends."frontend-{{ $frontendName }}".routes."route-frontend-{{ $frontendName }}"] + rule = "{{ getFrontendRule $service }}" +{{end}} diff --git a/templates/rancher.tmpl b/templates/rancher.tmpl index 77384c7d4..32c30edbf 100644 --- a/templates/rancher.tmpl +++ b/templates/rancher.tmpl @@ -4,13 +4,13 @@ [backends."backend-{{ $backendName }}"] - {{ $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 }}" @@ -21,14 +21,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 }}" @@ -36,7 +36,7 @@ interval = "{{ $healthCheck.Interval }}" {{end}} - {{ $buffering := getBuffering $backend }} + {{ $buffering := getBuffering $backend.SegmentLabels }} {{if $buffering }} [backends."backend-{{ $backendName }}".buffering] maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} @@ -59,19 +59,19 @@ [frontends."frontend-{{ $frontendName }}"] backend = "backend-{{ getBackendName $service }}" - priority = {{ getPriority $service }} - passHostHeader = {{ getPassHostHeader $service }} - passTLSCert = {{ getPassTLSCert $service }} + priority = {{ getPriority $service.SegmentLabels }} + passHostHeader = {{ getPassHostHeader $service.SegmentLabels }} + passTLSCert = {{ getPassTLSCert $service.SegmentLabels }} - entryPoints = [{{range getEntryPoints $service }} + entryPoints = [{{range getEntryPoints $service.SegmentLabels }} "{{.}}", {{end}}] - basicAuth = [{{range getBasicAuth $service }} + basicAuth = [{{range getBasicAuth $service.SegmentLabels }} "{{.}}", {{end}}] - {{ $whitelist := getWhiteList $service }} + {{ $whitelist := getWhiteList $service.SegmentLabels }} {{if $whitelist }} [frontends."frontend-{{ $frontendName }}".whiteList] sourceRange = [{{range $whitelist.SourceRange }} @@ -80,7 +80,7 @@ useXForwardedFor = {{ $whitelist.UseXForwardedFor }} {{end}} - {{ $redirect := getRedirect $service }} + {{ $redirect := getRedirect $service.SegmentLabels }} {{if $redirect }} [frontends."frontend-{{ $frontendName }}".redirect] entryPoint = "{{ $redirect.EntryPoint }}" @@ -89,7 +89,7 @@ permanent = {{ $redirect.Permanent }} {{end}} - {{ $errorPages := getErrorPages $service }} + {{ $errorPages := getErrorPages $service.SegmentLabels }} {{if $errorPages }} [frontends."frontend-{{ $frontendName }}".errors] {{range $pageName, $page := $errorPages }} @@ -102,7 +102,7 @@ {{end}} {{end}} - {{ $rateLimit := getRateLimit $service }} + {{ $rateLimit := getRateLimit $service.SegmentLabels }} {{if $rateLimit }} [frontends."frontend-{{ $frontendName }}".rateLimit] extractorFunc = "{{ $rateLimit.ExtractorFunc }}" @@ -115,7 +115,7 @@ {{end}} {{end}} - {{ $headers := getHeaders $service }} + {{ $headers := getHeaders $service.SegmentLabels }} {{if $headers }} [frontends."frontend-{{ $frontendName }}".headers] SSLRedirect = {{ $headers.SSLRedirect }}