diff --git a/docs/configuration/backends/docker.md b/docs/configuration/backends/docker.md index c44bc60e2..be37274f5 100644 --- a/docs/configuration/backends/docker.md +++ b/docs/configuration/backends/docker.md @@ -169,9 +169,10 @@ Labels can be used on containers to override default behaviour. | `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints` | | `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` | | `traefik.frontend.whitelistSourceRange: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.headers.customrequestheaders=EXPR ` | Provides the container with custom request headers that will be appended to each request forwarded to the container. Format: `HEADER:value,HEADER2:value2` | -| `traefik.frontend.headers.customresponseheaders=EXPR` | Appends the headers to each response returned by the container, before forwarding the response to the client. Format: `HEADER:value,HEADER2:value2` | +| `traefik.frontend.headers.customrequestheaders=EXPR ` | Provides the container with custom request headers that will be appended to each request forwarded to the container. Format: `HEADER:value,HEADER2:value2` | +| `traefik.frontend.headers.customresponseheaders=EXPR` | Appends the headers to each response returned by the container, before forwarding the response to the client. Format: `HEADER:value,HEADER2:value2` | | `traefik.docker.network` | Set the docker network to use for connections to this container. If a container is linked to several networks, be sure to set the proper network name (you can check with `docker inspect `) otherwise it will randomly pick one (depending on how docker is returning them). For instance when deploying docker `stack` from compose files, the compose defined networks will be prefixed with the `stack` name. | +| `traefik.frontend.redirect=https` | Enables Redirect to another entryPoint for that frontend (e.g. HTTPS) | ### On Service @@ -188,6 +189,7 @@ Services labels can be used for overriding default behaviour | `traefik..frontend.passHostHeader` | Overrides `traefik.frontend.passHostHeader`. | | `traefik..frontend.priority` | Overrides `traefik.frontend.priority`. | | `traefik..frontend.rule` | Overrides `traefik.frontend.rule`. | +| `traefik..frontend.redirect` | Overrides `traefik.frontend.redirect`. | !!! note diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index d7369ae8f..41971496d 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -88,6 +88,8 @@ Annotations can be used on containers to override default behaviour for the whol Override the default frontend rule type. Default: `PathPrefix`. - `traefik.frontend.priority: "3"` Override the default frontend rule priority. +- `traefik.frontend.redirect: https`: + Enables Redirect to another entryPoint for that frontend (e.g. HTTPS). Annotations can be used on the Kubernetes service to override default behaviour: diff --git a/docs/configuration/backends/rancher.md b/docs/configuration/backends/rancher.md index e1667dc74..1acd56800 100644 --- a/docs/configuration/backends/rancher.md +++ b/docs/configuration/backends/rancher.md @@ -130,8 +130,9 @@ Labels can be used on task containers to override default behaviour: | `traefik.frontend.priority=10` | Override default frontend priority | | `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. | | `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash`. | +| `traefik.frontend.redirect=https` | Enables Redirect to another entryPoint for that frontend (e.g. HTTPS) | | `traefik.backend.circuitbreaker.expression=NetworkErrorRatio() > 0.5` | Create a [circuit breaker](/basics/#backends) to be used against the backend | | `traefik.backend.loadbalancer.method=drr` | Override the default `wrr` load balancer algorithm | | `traefik.backend.loadbalancer.stickiness=true` | Enable backend sticky sessions | | `traefik.backend.loadbalancer.stickiness.cookieName=NAME` | Manually set the cookie name for sticky sessions | -| `traefik.backend.loadbalancer.sticky=true` | Enable backend sticky sessions (DEPRECATED) | +| `traefik.backend.loadbalancer.sticky=true` | Enable backend sticky sessions (DEPRECATED) | \ No newline at end of file diff --git a/provider/docker/docker.go b/provider/docker/docker.go index d5a701bc0..4bcfe5474 100644 --- a/provider/docker/docker.go +++ b/provider/docker/docker.go @@ -271,6 +271,7 @@ func (p *Provider) loadDockerConfig(containersInspected []dockerData) *types.Con "getEntryPoints": p.getEntryPoints, "getBasicAuth": p.getBasicAuth, "getFrontendRule": p.getFrontendRule, + "getRedirect": p.getRedirect, "hasCircuitBreakerLabel": p.hasCircuitBreakerLabel, "getCircuitBreakerExpression": p.getCircuitBreakerExpression, "hasLoadBalancerLabel": p.hasLoadBalancerLabel, @@ -293,6 +294,7 @@ func (p *Provider) loadDockerConfig(containersInspected []dockerData) *types.Con "getServicePassHostHeader": p.getServicePassHostHeader, "getServicePriority": p.getServicePriority, "getServiceBackend": p.getServiceBackend, + "getServiceRedirect": p.getServiceRedirect, "getWhitelistSourceRange": p.getWhitelistSourceRange, "getRequestHeaders": p.getRequestHeaders, "getResponseHeaders": p.getResponseHeaders, @@ -333,6 +335,7 @@ func (p *Provider) loadDockerConfig(containersInspected []dockerData) *types.Con if err != nil { log.Error(err) } + return configuration } @@ -470,6 +473,14 @@ func (p *Provider) getServiceProtocol(container dockerData, serviceName string) return p.getProtocol(container) } +// Extract protocol from labels for a given service and a given docker container +func (p *Provider) getServiceRedirect(container dockerData, serviceName string) string { + if value, ok := getContainerServiceLabel(container, serviceName, "frontend.redirect"); ok { + return value + } + return p.getRedirect(container) +} + func (p *Provider) hasLoadBalancerLabel(container dockerData) bool { _, errMethod := getLabel(container, types.LabelBackendLoadbalancerMethod) _, errSticky := getLabel(container, types.LabelBackendLoadbalancerSticky) @@ -831,6 +842,14 @@ func parseCustomHeaders(container dockerData, containerType string) map[string]s } return customHeaders } + +func (p *Provider) getRedirect(container dockerData) string { + if entryPointredirect, err := getLabel(container, types.LabelFrontendRedirect); err == nil { + return entryPointredirect + } + return "" +} + func isContainerEnabled(container dockerData, exposedByDefault bool) bool { return exposedByDefault && container.Labels[types.LabelEnable] != "false" || container.Labels[types.LabelEnable] == "true" } diff --git a/provider/docker/docker_test.go b/provider/docker/docker_test.go index 6955d01dc..1d24e4974 100644 --- a/provider/docker/docker_test.go +++ b/provider/docker/docker_test.go @@ -902,6 +902,7 @@ func TestDockerLoadDockerConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{}, BasicAuth: []string{}, + Redirect: "", Routes: map[string]types.Route{ "route-frontend-Host-test-docker-localhost-0": { Rule: "Host:test.docker.localhost", @@ -929,6 +930,7 @@ func TestDockerLoadDockerConfig(t *testing.T) { types.LabelBackend: "foobar", types.LabelFrontendEntryPoints: "http,https", types.LabelFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + types.LabelFrontendRedirect: "https", }), ports(nat.PortMap{ "80/tcp": {}, @@ -952,6 +954,7 @@ func TestDockerLoadDockerConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{"http", "https"}, BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + Redirect: "https", Routes: map[string]types.Route{ "route-frontend-Host-test1-docker-localhost-0": { Rule: "Host:test1.docker.localhost", @@ -963,6 +966,7 @@ func TestDockerLoadDockerConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{}, BasicAuth: []string{}, + Redirect: "", Routes: map[string]types.Route{ "route-frontend-Host-test2-docker-localhost-1": { Rule: "Host:test2.docker.localhost", @@ -1010,6 +1014,7 @@ func TestDockerLoadDockerConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{"http", "https"}, BasicAuth: []string{}, + Redirect: "", Routes: map[string]types.Route{ "route-frontend-Host-test1-docker-localhost-0": { Rule: "Host:test1.docker.localhost", diff --git a/provider/docker/service_test.go b/provider/docker/service_test.go index 6198921f3..36fef70e2 100644 --- a/provider/docker/service_test.go +++ b/provider/docker/service_test.go @@ -333,6 +333,7 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) { "traefik.service.port": "2503", "traefik.service.frontend.entryPoints": "http,https", "traefik.service.frontend.auth.basic": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + "traefik.service.frontend.redirect": "https", }), ports(nat.PortMap{ "80/tcp": {}, @@ -346,6 +347,7 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{"http", "https"}, BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + Redirect: "https", Routes: map[string]types.Route{ "service-service": { Rule: "Host:foo.docker.localhost", @@ -379,6 +381,7 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) { "traefik.service.frontend.priority": "5000", "traefik.service.frontend.entryPoints": "http,https,ws", "traefik.service.frontend.auth.basic": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + "traefik.service.frontend.redirect": "https", }), ports(nat.PortMap{ "80/tcp": {}, @@ -405,6 +408,7 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) { Priority: 5000, EntryPoints: []string{"http", "https", "ws"}, BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + Redirect: "https", Routes: map[string]types.Route{ "service-service": { Rule: "Path:/mypath", @@ -416,6 +420,7 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{}, BasicAuth: []string{}, + Redirect: "", Routes: map[string]types.Route{ "service-anotherservice": { Rule: "Path:/anotherpath", diff --git a/provider/docker/swarm_test.go b/provider/docker/swarm_test.go index 1ece57a21..13db8e802 100644 --- a/provider/docker/swarm_test.go +++ b/provider/docker/swarm_test.go @@ -665,6 +665,7 @@ func TestSwarmLoadDockerConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{}, BasicAuth: []string{}, + Redirect: "", Routes: map[string]types.Route{ "route-frontend-Host-test-docker-localhost-0": { Rule: "Host:test.docker.localhost", @@ -699,6 +700,7 @@ func TestSwarmLoadDockerConfig(t *testing.T) { types.LabelBackend: "foobar", types.LabelFrontendEntryPoints: "http,https", types.LabelFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + types.LabelFrontendRedirect: "https", }), withEndpointSpec(modeVIP), withEndpoint(virtualIP("1", "127.0.0.1/24")), @@ -719,6 +721,7 @@ func TestSwarmLoadDockerConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{"http", "https"}, BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + Redirect: "https", Routes: map[string]types.Route{ "route-frontend-Host-test1-docker-localhost-0": { Rule: "Host:test1.docker.localhost", @@ -730,6 +733,7 @@ func TestSwarmLoadDockerConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{}, BasicAuth: []string{}, + Redirect: "", Routes: map[string]types.Route{ "route-frontend-Host-test2-docker-localhost-1": { Rule: "Host:test2.docker.localhost", diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index 602796422..d22795741 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -187,6 +187,8 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) whitelistSourceRangeAnnotation := i.Annotations[annotationKubernetesWhitelistSourceRange] whitelistSourceRange := provider.SplitAndTrimString(whitelistSourceRangeAnnotation) + entryPointRedirect, _ := i.Annotations[types.LabelFrontendRedirect] + if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists { basicAuthCreds, err := handleBasicAuthConfig(i, k8sClient) if err != nil { @@ -203,6 +205,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) Priority: priority, BasicAuth: basicAuthCreds, WhitelistSourceRange: whitelistSourceRange, + Redirect: entryPointRedirect, } } if len(r.Host) > 0 { diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index 32750f4ce..c3107c86a 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -1105,6 +1105,36 @@ func TestIngressAnnotations(t *testing.T) { Paths: []v1beta1.HTTPIngressPath{ { Path: "/auth-realm-customized", + + Backend: v1beta1.IngressBackend{ + ServiceName: "service1", + ServicePort: intstr.FromInt(80), + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + ObjectMeta: v1.ObjectMeta{ + Namespace: "testing", + Annotations: map[string]string{ + "kubernetes.io/ingress.class": "traefik", + types.LabelFrontendRedirect: "https", + }, + }, + Spec: v1beta1.IngressSpec{ + Rules: []v1beta1.IngressRule{ + { + Host: "redirect", + IngressRuleValue: v1beta1.IngressRuleValue{ + HTTP: &v1beta1.HTTPIngressRuleValue{ + Paths: []v1beta1.HTTPIngressPath{ + { + Path: "/https", Backend: v1beta1.IngressBackend{ ServiceName: "service1", ServicePort: intstr.FromInt(80), @@ -1204,6 +1234,19 @@ func TestIngressAnnotations(t *testing.T) { Method: "wrr", }, }, + "redirect/https": { + Servers: map[string]types.Server{ + "http://example.com": { + URL: "http://example.com", + Weight: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: &types.LoadBalancer{ + Sticky: false, + Method: "wrr", + }, + }, "test/whitelist-source-range": { Servers: map[string]types.Server{ "http://example.com": { @@ -1241,6 +1284,7 @@ func TestIngressAnnotations(t *testing.T) { Rule: "Host:foo", }, }, + Redirect: "", }, "other/stuff": { Backend: "other/stuff", @@ -1253,6 +1297,7 @@ func TestIngressAnnotations(t *testing.T) { Rule: "Host:other", }, }, + Redirect: "", }, "basic/auth": { Backend: "basic/auth", @@ -1266,7 +1311,22 @@ func TestIngressAnnotations(t *testing.T) { }, }, BasicAuth: []string{"myUser:myEncodedPW"}, + Redirect: "", }, + "redirect/https": { + Backend: "redirect/https", + PassHostHeader: true, + Routes: map[string]types.Route{ + "/https": { + Rule: "PathPrefix:/https", + }, + "redirect": { + Rule: "Host:redirect", + }, + }, + Redirect: "https", + }, + "test/whitelist-source-range": { Backend: "test/whitelist-source-range", PassHostHeader: true, @@ -1282,6 +1342,7 @@ func TestIngressAnnotations(t *testing.T) { Rule: "Host:test", }, }, + Redirect: "", }, "rewrite/api": { Backend: "rewrite/api", @@ -1294,6 +1355,7 @@ func TestIngressAnnotations(t *testing.T) { Rule: "Host:rewrite", }, }, + Redirect: "", }, }, } diff --git a/provider/rancher/rancher.go b/provider/rancher/rancher.go index 341a285cc..324fb49a3 100644 --- a/provider/rancher/rancher.go +++ b/provider/rancher/rancher.go @@ -76,6 +76,13 @@ func (p *Provider) getBasicAuth(service rancherData) []string { return []string{} } +func (p *Provider) getRedirect(service rancherData) string { + if redirect, err := getServiceLabel(service, types.LabelFrontendRedirect); err == nil { + return redirect + } + return "" +} + func (p *Provider) getFrontendName(service rancherData) string { // Replace '.' with '-' in quoted keys because of this issue https://github.com/BurntSushi/toml/issues/78 return provider.Normalize(p.getFrontendRule(service)) @@ -239,6 +246,7 @@ func (p *Provider) loadRancherConfig(services []rancherData) *types.Configuratio "getSticky": p.getSticky, "hasStickinessLabel": p.hasStickinessLabel, "getStickinessCookieName": p.getStickinessCookieName, + "getRedirect": p.getRedirect, } // filter services @@ -270,6 +278,7 @@ func (p *Provider) loadRancherConfig(services []rancherData) *types.Configuratio if err != nil { log.Error(err) } + return configuration } diff --git a/provider/rancher/rancher_test.go b/provider/rancher/rancher_test.go index 92426eef4..70b005073 100644 --- a/provider/rancher/rancher_test.go +++ b/provider/rancher/rancher_test.go @@ -489,6 +489,35 @@ func TestRancherGetPassHostHeader(t *testing.T) { } } +func TestRancherGetRedirect(t *testing.T) { + provider := &Provider{ + Domain: "rancher.localhost", + } + + testCases := []struct { + service rancherData + expected string + }{ + { + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + types.LabelFrontendRedirect: "https", + }, + }, + + expected: "https", + }, + } + + for _, test := range testCases { + actual := provider.getRedirect(test.service) + if actual != test.expected { + t.Fatalf("got %q, expected %q", actual, test.expected) + } + } +} + func TestRancherGetLabel(t *testing.T) { services := []struct { service rancherData @@ -544,6 +573,7 @@ func TestRancherLoadRancherConfig(t *testing.T) { Labels: map[string]string{ types.LabelPort: "80", types.LabelFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + types.LabelFrontendRedirect: "https", }, Health: "healthy", Containers: []string{"127.0.0.1"}, @@ -556,6 +586,7 @@ func TestRancherLoadRancherConfig(t *testing.T) { EntryPoints: []string{}, BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, Priority: 0, + Redirect: "https", Routes: map[string]types.Route{ "route-frontend-Host-test-service-rancher-localhost": { diff --git a/server/server.go b/server/server.go index 3d0403538..a3281753e 100644 --- a/server/server.go +++ b/server/server.go @@ -1103,6 +1103,21 @@ func (server *Server) loadConfig(configurations types.Configurations, globalConf log.Infof("Configured IP Whitelists: %s", frontend.WhitelistSourceRange) } + if len(frontend.Redirect) > 0 { + proto := "http" + if server.globalConfiguration.EntryPoints[frontend.Redirect].TLS != nil { + proto = "https" + } + + regex, replacement, err := server.buildRedirect(proto, entryPoint) + rewrite, err := middlewares.NewRewrite(regex, replacement, true) + if err != nil { + log.Fatalf("Error creating Frontend Redirect: %v", err) + } + n.Use(rewrite) + log.Debugf("Creating frontend %s redirect to %s", frontendName, proto) + } + if len(frontend.BasicAuth) > 0 { users := types.Users{} for _, user := range frontend.BasicAuth { @@ -1254,21 +1269,13 @@ func (server *Server) wireFrontendBackend(serverRoute *serverRoute, handler http func (server *Server) loadEntryPointConfig(entryPointName string, entryPoint *configuration.EntryPoint) (negroni.Handler, error) { regex := entryPoint.Redirect.Regex replacement := entryPoint.Redirect.Replacement + var err error if len(entryPoint.Redirect.EntryPoint) > 0 { - regex = `^(?:https?:\/\/)?([\w\._-]+)(?::\d+)?(.*)$` - if server.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint] == nil { - return nil, errors.New("Unknown entrypoint " + entryPoint.Redirect.EntryPoint) - } - protocol := "http" + var protocol = "http" if server.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint].TLS != nil { protocol = "https" } - r, _ := regexp.Compile(`(:\d+)`) - match := r.FindStringSubmatch(server.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint].Address) - if len(match) == 0 { - return nil, errors.New("Bad Address format: " + server.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint].Address) - } - replacement = protocol + "://$1" + match[0] + "$2" + regex, replacement, err = server.buildRedirect(protocol, entryPoint) } rewrite, err := middlewares.NewRewrite(regex, replacement, true) if err != nil { @@ -1279,6 +1286,20 @@ func (server *Server) loadEntryPointConfig(entryPointName string, entryPoint *co return rewrite, nil } +func (server *Server) buildRedirect(protocol string, entryPoint *configuration.EntryPoint) (string, string, error) { + regex := `^(?:https?:\/\/)?([\w\._-]+)(?::\d+)?(.*)$` + if server.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint] == nil { + return "", "", fmt.Errorf("unknown target entrypoint %q", entryPoint.Redirect.EntryPoint) + } + r, _ := regexp.Compile(`(:\d+)`) + match := r.FindStringSubmatch(server.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint].Address) + if len(match) == 0 { + return "", "", fmt.Errorf("bad Address format %q", server.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint].Address) + } + replacement := protocol + "://$1" + match[0] + "$2" + return regex, replacement, nil +} + func (server *Server) buildDefaultHTTPRouter() *mux.Router { router := mux.NewRouter() router.NotFoundHandler = http.HandlerFunc(notFoundHandler) diff --git a/server/server_test.go b/server/server_test.go index 128961b3c..c93587370 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -903,6 +903,72 @@ func TestServerResponseEmptyBackend(t *testing.T) { } } +func TestServerLoadConfigBuildRedirect(t *testing.T) { + testCases := []struct { + desc string + replacementProtocol string + globalConfiguration configuration.GlobalConfiguration + originEntryPointName string + expectedReplacement string + }{ + { + desc: "Redirect endpoint http to https with HTTPS protocol", + replacementProtocol: "https", + originEntryPointName: "http", + globalConfiguration: configuration.GlobalConfiguration{ + EntryPoints: configuration.EntryPoints{ + "http": &configuration.EntryPoint{ + Address: ":80", + Redirect: &configuration.Redirect{ + EntryPoint: "https", + }, + }, + "https": &configuration.EntryPoint{ + Address: ":443", + TLS: &tls.TLS{}, + }, + }, + }, + + expectedReplacement: "https://$1:443$2", + }, + { + desc: "Redirect endpoint http to http02 with HTTP protocol", + replacementProtocol: "http", + originEntryPointName: "http", + globalConfiguration: configuration.GlobalConfiguration{ + EntryPoints: configuration.EntryPoints{ + "http": &configuration.EntryPoint{ + Address: ":80", + Redirect: &configuration.Redirect{ + EntryPoint: "http02", + }, + }, + "http02": &configuration.EntryPoint{ + Address: ":88", + }, + }, + }, + + expectedReplacement: "http://$1:88$2", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + srv := Server{globalConfiguration: test.globalConfiguration} + + _, replacement, err := srv.buildRedirect(test.replacementProtocol, srv.globalConfiguration.EntryPoints[test.originEntryPointName]) + + require.NoError(t, err, "build redirect sent an unexpected error") + assert.Equal(t, test.expectedReplacement, replacement, "build redirect does not return the right replacement pattern") + }) + } +} + func buildDynamicConfig(dynamicConfigBuilders ...func(*types.Configuration)) *types.Configuration { config := &types.Configuration{ Frontends: make(map[string]*types.Frontend), diff --git a/templates/docker.tmpl b/templates/docker.tmpl index 8c4cd23a4..54e83672b 100644 --- a/templates/docker.tmpl +++ b/templates/docker.tmpl @@ -47,6 +47,7 @@ [frontends."frontend-{{getServiceBackend $container $serviceName}}"] backend = "backend-{{getServiceBackend $container $serviceName}}" passHostHeader = {{getServicePassHostHeader $container $serviceName}} + redirect = "{{getServiceRedirect $container $serviceName}}" {{if getWhitelistSourceRange $container}} whitelistSourceRange = [{{range getWhitelistSourceRange $container}} "{{.}}", @@ -66,6 +67,7 @@ [frontends."frontend-{{$frontend}}"] backend = "backend-{{getBackend $container}}" passHostHeader = {{getPassHostHeader $container}} + redirect = "{{getRedirect $container}}" {{if getWhitelistSourceRange $container}} whitelistSourceRange = [{{range getWhitelistSourceRange $container}} "{{.}}", diff --git a/templates/kubernetes.tmpl b/templates/kubernetes.tmpl index 8f31c3e79..fb5a1a334 100644 --- a/templates/kubernetes.tmpl +++ b/templates/kubernetes.tmpl @@ -25,6 +25,7 @@ backend = "{{$frontend.Backend}}" priority = {{$frontend.Priority}} passHostHeader = {{$frontend.PassHostHeader}} + redirect = "{{$frontend.Redirect}}" basicAuth = [{{range $frontend.BasicAuth}} "{{.}}", {{end}}] diff --git a/templates/rancher.tmpl b/templates/rancher.tmpl index a5355c252..034168e76 100644 --- a/templates/rancher.tmpl +++ b/templates/rancher.tmpl @@ -34,6 +34,7 @@ backend = "backend-{{getBackend $service}}" passHostHeader = {{getPassHostHeader $service}} priority = {{getPriority $service}} + redirect = "{{getRedirect $service}}" entryPoints = [{{range getEntryPoints $service}} "{{.}}", {{end}}] diff --git a/types/common_label.go b/types/common_label.go index 5fa3c1a55..794420c73 100644 --- a/types/common_label.go +++ b/types/common_label.go @@ -20,6 +20,7 @@ const ( LabelFrontendPriority = LabelPrefix + "frontend.priority" LabelFrontendRule = LabelPrefix + "frontend.rule" LabelFrontendRuleType = LabelPrefix + "frontend.rule.type" + LabelFrontendRedirect = LabelPrefix + "frontend.redirect" LabelTraefikFrontendValue = LabelPrefix + "frontend.value" LabelTraefikFrontendWhitelistSourceRange = LabelPrefix + "frontend.whitelistSourceRange" LabelBackend = LabelPrefix + "backend" diff --git a/types/types.go b/types/types.go index 3cabd236c..c92318d3a 100644 --- a/types/types.go +++ b/types/types.go @@ -153,6 +153,7 @@ type Frontend struct { Headers Headers `json:"headers,omitempty"` Errors map[string]ErrorPage `json:"errors,omitempty"` RateLimit *RateLimit `json:"ratelimit,omitempty"` + Redirect string `json:"redirect,omitempty"` } // LoadBalancerMethod holds the method of load balancing to use. diff --git a/webui/src/app/sections/providers/frontend-monitor/frontend-monitor.html b/webui/src/app/sections/providers/frontend-monitor/frontend-monitor.html index f46943dcf..b8748df7c 100644 --- a/webui/src/app/sections/providers/frontend-monitor/frontend-monitor.html +++ b/webui/src/app/sections/providers/frontend-monitor/frontend-monitor.html @@ -18,6 +18,7 @@ {{entryPoint}}  + Redirect to {{frontendCtrl.frontend.redirect}} PassHostHeader