diff --git a/provider/docker/config.go b/provider/docker/config.go index 1b783f5b8..51f7dfc1b 100644 --- a/provider/docker/config.go +++ b/provider/docker/config.go @@ -49,6 +49,8 @@ func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.C "getRedirectEntryPoint": getFuncStringLabel(label.TraefikFrontendRedirectEntryPoint, label.DefaultFrontendRedirectEntryPoint), "getRedirectRegex": getFuncStringLabel(label.TraefikFrontendRedirectRegex, ""), "getRedirectReplacement": getFuncStringLabel(label.TraefikFrontendRedirectReplacement, ""), + "hasErrorPages": hasErrorPages, + "getErrorPages": getErrorPages, // Headers "hasRequestHeaders": hasFunc(label.TraefikFrontendRequestHeaders), "getRequestHeaders": getFuncMapLabel(label.TraefikFrontendRequestHeaders), @@ -114,6 +116,8 @@ func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.C "getServiceRequestHeaders": getFuncServiceMapLabel(label.SuffixFrontendRequestHeaders), "hasServiceResponseHeaders": hasFuncServiceLabel(label.SuffixFrontendResponseHeaders), "getServiceResponseHeaders": getFuncServiceMapLabel(label.SuffixFrontendResponseHeaders), + "hasServiceErrorPages": hasServiceErrorPages, + "getServiceErrorPages": getServiceErrorPages, } // filter containers filteredContainers := fun.Filter(func(container dockerData) bool { diff --git a/provider/docker/config_container.go b/provider/docker/config_container.go index b6511ff4d..8123ddba5 100644 --- a/provider/docker/config_container.go +++ b/provider/docker/config_container.go @@ -8,6 +8,7 @@ import ( "github.com/containous/traefik/log" "github.com/containous/traefik/provider" "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" "github.com/docker/go-connections/nat" ) @@ -172,6 +173,15 @@ func hasRedirect(container dockerData) bool { label.Has(container.Labels, label.TraefikFrontendRedirectReplacement) && label.Has(container.Labels, label.TraefikFrontendRedirectRegex) } +func hasErrorPages(container dockerData) bool { + return label.HasPrefix(container.Labels, label.Prefix+label.BaseFrontendErrorPage) +} + +func getErrorPages(container dockerData) map[string]*types.ErrorPage { + prefix := label.Prefix + label.BaseFrontendErrorPage + return label.ParseErrorPages(container.Labels, prefix, label.RegexpFrontendErrorPage) +} + // Label functions func getFuncInt64Label(labelName string, defaultValue int64) func(container dockerData) int64 { diff --git a/provider/docker/config_container_docker_test.go b/provider/docker/config_container_docker_test.go index 1a246a185..9bf0d2c22 100644 --- a/provider/docker/config_container_docker_test.go +++ b/provider/docker/config_container_docker_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestDockerLoadDockerConfig(t *testing.T) { +func TestDockerBuildConfiguration(t *testing.T) { testCases := []struct { containers []docker.ContainerJSON expectedFrontends map[string]*types.Frontend @@ -181,6 +181,61 @@ func TestDockerLoadDockerConfig(t *testing.T) { }, }, }, + { + containers: []docker.ContainerJSON{ + containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikBackend: "foobar", + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: "404", + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: "foobar", + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: "foo_query", + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: "500,600", + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: "foobar", + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: "bar_query", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-test1-docker-localhost-0": { + EntryPoints: []string{}, + BasicAuth: []string{}, + PassHostHeader: true, + Backend: "backend-foobar", + Routes: map[string]types.Route{ + "route-frontend-Host-test1-docker-localhost-0": { + Rule: "Host:test1.docker.localhost", + }, + }, + Errors: map[string]types.ErrorPage{ + "foo": { + Status: []string{"404"}, + Query: "foo_query", + Backend: "foobar", + }, + "bar": { + Status: []string{"500", "600"}, + Query: "bar_query", + Backend: "foobar", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foobar": { + Servers: map[string]types.Server{ + "server-test1": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + }, + }, + }, } for caseID, test := range testCases { @@ -897,3 +952,59 @@ func TestDockerGetPort(t *testing.T) { }) } } + +func TestGetErrorPages(t *testing.T) { + testCases := []struct { + desc string + data dockerData + expected map[string]*types.ErrorPage + }{ + { + desc: "2 errors pages", + data: parseContainer(containerJSON( + labels(map[string]string{ + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: "404", + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: "foo_backend", + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: "foo_query", + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: "500,600", + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: "bar_backend", + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: "bar_query", + }))), + expected: map[string]*types.ErrorPage{ + "foo": { + Status: []string{"404"}, + Query: "foo_query", + Backend: "foo_backend", + }, + "bar": { + Status: []string{"500", "600"}, + Query: "bar_query", + Backend: "bar_backend", + }, + }, + }, + { + desc: "only status field", + data: parseContainer(containerJSON( + labels(map[string]string{ + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: "404", + }))), + expected: map[string]*types.ErrorPage{ + "foo": { + Status: []string{"404"}, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + pages := getErrorPages(test.data) + + assert.EqualValues(t, test.expected, pages) + }) + } +} diff --git a/provider/docker/config_service.go b/provider/docker/config_service.go index a173494d9..0de24f173 100644 --- a/provider/docker/config_service.go +++ b/provider/docker/config_service.go @@ -7,6 +7,7 @@ import ( "github.com/containous/traefik/provider" "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" ) // Specific functions @@ -50,7 +51,8 @@ func checkServiceLabelPort(container dockerData) error { } // Get only one instance of all service names from service labels servicesLabelNames := label.ServicesPropertiesRegexp.FindStringSubmatch(lbl) - if len(servicesLabelNames) > 0 { + + if len(servicesLabelNames) > 0 && !strings.HasPrefix(lbl, label.TraefikFrontend) { serviceLabels[strings.Split(servicesLabelNames[0], ".")[1]] = struct{}{} } } @@ -96,6 +98,16 @@ func hasServiceRedirect(container dockerData, serviceName string) bool { label.Has(serviceLabels, label.SuffixFrontendRedirectRegex) && label.Has(serviceLabels, label.SuffixFrontendRedirectReplacement) } +func hasServiceErrorPages(container dockerData, serviceName string) bool { + serviceLabels := getServiceLabels(container, serviceName) + return label.HasPrefix(serviceLabels, label.BaseFrontendErrorPage) +} + +func getServiceErrorPages(container dockerData, serviceName string) map[string]*types.ErrorPage { + serviceLabels := getServiceLabels(container, serviceName) + return label.ParseErrorPages(serviceLabels, label.BaseFrontendErrorPage, label.RegexpBaseFrontendErrorPage) +} + // Service label functions func getFuncServiceMapLabel(labelSuffix string) func(container dockerData, serviceName string) map[string]string { diff --git a/provider/docker/config_service_test.go b/provider/docker/config_service_test.go index 48b89bfa0..e93ba4cae 100644 --- a/provider/docker/config_service_test.go +++ b/provider/docker/config_service_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" docker "github.com/docker/docker/api/types" "github.com/stretchr/testify/assert" ) @@ -219,3 +220,60 @@ func TestDockerCheckPortLabels(t *testing.T) { }) } } + +func TestGetServiceErrorPages(t *testing.T) { + service := "courgette" + testCases := []struct { + desc string + data dockerData + expected map[string]*types.ErrorPage + }{ + { + desc: "2 errors pages", + data: parseContainer(containerJSON( + labels(map[string]string{ + label.Prefix + service + "." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: "404", + label.Prefix + service + "." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: "foo_backend", + label.Prefix + service + "." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: "foo_query", + label.Prefix + service + "." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: "500,600", + label.Prefix + service + "." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: "bar_backend", + label.Prefix + service + "." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: "bar_query", + }))), + expected: map[string]*types.ErrorPage{ + "foo": { + Status: []string{"404"}, + Query: "foo_query", + Backend: "foo_backend", + }, + "bar": { + Status: []string{"500", "600"}, + Query: "bar_query", + Backend: "bar_backend", + }, + }, + }, + { + desc: "only status field", + data: parseContainer(containerJSON( + labels(map[string]string{ + label.Prefix + service + ".frontend.errors.foo.status": "404", + }))), + expected: map[string]*types.ErrorPage{ + "foo": { + Status: []string{"404"}, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + pages := getServiceErrorPages(test.data, service) + + assert.EqualValues(t, test.expected, pages) + }) + } +} diff --git a/provider/label/label.go b/provider/label/label.go index 94755d839..6eece3fa0 100644 --- a/provider/label/label.go +++ b/provider/label/label.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/containous/traefik/log" + "github.com/containous/traefik/types" ) const ( @@ -30,12 +31,20 @@ const ( DefaultBackendHealthCheckPort = 0 ) -// ServicesPropertiesRegexp used to extract the name of the service and the name of the property for this service -// All properties are under the format traefik..frontend.*= except the port/portIndex/weight/protocol/backend directly after traefik.. -var ServicesPropertiesRegexp = regexp.MustCompile(`^traefik\.(?P.+?)\.(?Pport|portIndex|weight|protocol|backend|frontend\.(.+))$`) +var ( + // ServicesPropertiesRegexp used to extract the name of the service and the name of the property for this service + // All properties are under the format traefik..frontend.*= except the port/portIndex/weight/protocol/backend directly after traefik.. + ServicesPropertiesRegexp = regexp.MustCompile(`^traefik\.(?P.+?)\.(?Pport|portIndex|weight|protocol|backend|frontend\.(.+))$`) -// PortRegexp used to extract the port label of the service -var PortRegexp = regexp.MustCompile(`^traefik\.(?P.+?)\.port$`) + // PortRegexp used to extract the port label of the service + PortRegexp = regexp.MustCompile(`^traefik\.(?P.+?)\.port$`) + + // RegexpBaseFrontendErrorPage used to extract error pages from service's label + RegexpBaseFrontendErrorPage = regexp.MustCompile(`^frontend\.errors\.(?P[^ .]+)\.(?P[^ .]+)$`) + + // RegexpFrontendErrorPage used to extract error pages from label + RegexpFrontendErrorPage = regexp.MustCompile(`^traefik\.frontend\.errors\.(?P[^ .]+)\.(?P[^ .]+)$`) +) // ServicePropertyValues is a map of services properties // an example value is: weight=42 @@ -200,13 +209,23 @@ func HasP(labels *map[string]string, labelName string) bool { 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 { + if strings.HasPrefix(name, prefix) && len(value) > 0 { + return true + } + } + return false +} + // ExtractServiceProperties Extract services labels func ExtractServiceProperties(labels map[string]string) ServiceProperties { v := make(ServiceProperties) for name, value := range labels { matches := ServicesPropertiesRegexp.FindStringSubmatch(name) - if matches == nil { + if matches == nil || strings.HasPrefix(name, TraefikFrontend) { continue } @@ -239,6 +258,43 @@ func ExtractServicePropertiesP(labels *map[string]string) ServiceProperties { return ExtractServiceProperties(*labels) } +// ParseErrorPages parse error pages to create ErrorPage struct +func ParseErrorPages(labels map[string]string, labelPrefix string, labelRegex *regexp.Regexp) map[string]*types.ErrorPage { + errorPages := make(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 + } + + 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 +} + // IsEnabled Check if a container is enabled in Træfik func IsEnabled(labels map[string]string, exposedByDefault bool) bool { return GetBoolValue(labels, TraefikEnable, exposedByDefault) diff --git a/provider/label/label_test.go b/provider/label/label_test.go index 3347ddb11..53e002e76 100644 --- a/provider/label/label_test.go +++ b/provider/label/label_test.go @@ -3,6 +3,7 @@ package label import ( "testing" + "github.com/containous/traefik/types" "github.com/stretchr/testify/assert" ) @@ -739,22 +740,24 @@ func TestExtractServiceProperties(t *testing.T) { labels: map[string]string{ "traefik.foo.port": "bar", "traefik.foo.frontend.bar": "1bar", - "traefik.foo.frontend.": "2bar", + "traefik.foo.backend": "3bar", }, expected: ServiceProperties{ "foo": ServicePropertyValues{ "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", + "foo.frontend.bar": "1bar", + "traefik.foo.frontend.": "2bar", + "traefik.foo.port.bar": "barbar", + "traefik.foo.frontend": "0bar", + "traefik.frontend.foo.backend": "0bar", }, expected: ServiceProperties{}, }, @@ -785,22 +788,24 @@ func TestExtractServicePropertiesP(t *testing.T) { labels: &map[string]string{ "traefik.foo.port": "bar", "traefik.foo.frontend.bar": "1bar", - "traefik.foo.frontend.": "2bar", + "traefik.foo.backend": "3bar", }, expected: ServiceProperties{ "foo": ServicePropertyValues{ "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", + "foo.frontend.bar": "1bar", + "traefik.foo.frontend.": "2bar", + "traefik.foo.port.bar": "barbar", + "traefik.foo.frontend": "0bar", + "traefik.frontend.foo.backend": "0bar", }, expected: ServiceProperties{}, }, @@ -967,3 +972,112 @@ func TestGetServiceLabel(t *testing.T) { }) } } + +func TestHasPrefix(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + prefix string + expected bool + }{ + { + desc: "nil labels map", + prefix: "foo", + expected: false, + }, + { + desc: "nonexistent prefix", + labels: map[string]string{ + "foo.carotte": "bar", + }, + prefix: "fii", + expected: false, + }, + { + desc: "existent prefix", + labels: map[string]string{ + "foo.carotte": "bar", + }, + prefix: "foo", + expected: true, + }, + { + desc: "existent prefix with empty value", + labels: map[string]string{ + "foo.carotte": "", + }, + prefix: "foo", + expected: false, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := HasPrefix(test.labels, test.prefix) + assert.Equal(t, test.expected, got) + }) + } +} + +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": {}}, + }, + } + + 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) + }) + } +} diff --git a/provider/label/names.go b/provider/label/names.go index 9bacf67be..44aaf20f1 100644 --- a/provider/label/names.go +++ b/provider/label/names.go @@ -23,6 +23,7 @@ const ( SuffixBackendLoadBalancerStickinessCookieName = "backend.loadbalancer.stickiness.cookieName" SuffixBackendMaxConnAmount = "backend.maxconn.amount" SuffixBackendMaxConnExtractorFunc = "backend.maxconn.extractorfunc" + SuffixFrontend = "frontend" SuffixFrontendAuthBasic = "frontend.auth.basic" SuffixFrontendBackend = "frontend.backend" SuffixFrontendEntryPoints = "frontend.entryPoints" @@ -76,6 +77,7 @@ const ( TraefikBackendLoadBalancerStickinessCookieName = Prefix + SuffixBackendLoadBalancerStickinessCookieName TraefikBackendMaxConnAmount = Prefix + SuffixBackendMaxConnAmount TraefikBackendMaxConnExtractorFunc = Prefix + SuffixBackendMaxConnExtractorFunc + TraefikFrontend = Prefix + SuffixFrontend TraefikFrontendAuthBasic = Prefix + SuffixFrontendAuthBasic TraefikFrontendEntryPoints = Prefix + SuffixFrontendEntryPoints TraefikFrontendPassHostHeader = Prefix + SuffixFrontendPassHostHeader @@ -108,4 +110,8 @@ const ( TraefikFrontendPublicKey = Prefix + SuffixFrontendHeadersPublicKey TraefikFrontendReferrerPolicy = Prefix + SuffixFrontendHeadersReferrerPolicy TraefikFrontendIsDevelopment = Prefix + SuffixFrontendHeadersIsDevelopment + BaseFrontendErrorPage = "frontend.errors." + SuffixErrorPageBackend = "backend" + SuffixErrorPageQuery = "query" + SuffixErrorPageStatus = "status" ) diff --git a/templates/docker.tmpl b/templates/docker.tmpl index 46d5af491..57f866163 100644 --- a/templates/docker.tmpl +++ b/templates/docker.tmpl @@ -83,6 +83,18 @@ replacement = "{{getServiceRedirectReplacement $container $serviceName}}" {{end}} + {{ if hasServiceErrorPages $container $serviceName }} + [frontends."frontend-{{getServiceBackend $container $serviceName}}".errors] + {{ range $pageName, $page := getServiceErrorPages $container $serviceName }} + [frontends."frontend-{{getServiceBackend $container $serviceName}}".errors.{{$pageName}}] + status = [{{range $page.Status}} + "{{.}}", + {{end}}] + backend = "{{$page.Backend}}" + query = "{{$page.Query}}" + {{end}} + {{end}} + [frontends."frontend-{{getServiceBackend $container $serviceName}}".routes."service-{{$serviceName | replace "/" "" | replace "." "-"}}"] rule = "{{getServiceFrontendRule $container $serviceName}}" @@ -131,6 +143,18 @@ replacement = "{{getRedirectReplacement $container}}" {{end}} + {{ if hasErrorPages $container }} + [frontends."frontend-{{$frontend}}".errors] + {{ range $pageName, $page := getErrorPages $container }} + [frontends."frontend-{{$frontend}}".errors.{{ $pageName }}] + status = [{{range $page.Status}} + "{{.}}", + {{end}}] + backend = "{{$page.Backend}}" + query = "{{$page.Query}}" + {{end}} + {{end}} + [frontends."frontend-{{$frontend}}".headers] {{if hasSSLRedirectHeaders $container}} SSLRedirect = {{getSSLRedirectHeaders $container}}