From 5b37fb83fd6b692662e8ed22a1ee45d896b0cf8b Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Wed, 10 Jan 2018 21:54:46 +0100 Subject: [PATCH] feat(mesos): add all labels. --- provider/mesos/config.go | 314 ++++++++++++-- provider/mesos/config_test.go | 749 +++++++++++++++++++++++++++++++++- templates/mesos.tmpl | 158 ++++++- 3 files changed, 1176 insertions(+), 45 deletions(-) diff --git a/provider/mesos/config.go b/provider/mesos/config.go index 1d6069840..e980dda5b 100644 --- a/provider/mesos/config.go +++ b/provider/mesos/config.go @@ -21,19 +21,40 @@ func (p *Provider) buildConfiguration(tasks []state.Task) *types.Configuration { "getID": getID, // Backend functions + "getBackendName": getBackendName, + "getCircuitBreaker": getCircuitBreaker, + "getLoadBalancer": getLoadBalancer, + "getMaxConn": getMaxConn, + "getHealthCheck": getHealthCheck, + "getServers": p.getServers, + "getHost": p.getHost, + "getServerPort": p.getServerPort, + + // TODO Deprecated [breaking] "getProtocol": getFuncApplicationStringValue(label.TraefikProtocol, label.DefaultProtocol), - "getPort": p.getPort, - "getHost": p.getHost, - "getWeight": getFuncApplicationStringValue(label.TraefikWeight, label.DefaultWeight), - "getBackend": getBackend, + // TODO Deprecated [breaking] + "getWeight": getFuncApplicationStringValue(label.TraefikWeight, label.DefaultWeight), + // TODO Deprecated [breaking] replaced by getBackendName + "getBackend": getBackend, + // TODO Deprecated [breaking] + "getPort": p.getPort, // Frontend functions - "getPassHostHeader": getFuncStringValue(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), - "getPriority": getFuncStringValue(label.TraefikFrontendPriority, label.DefaultFrontendPriority), - "getEntryPoints": getFuncSliceStringValue(label.TraefikFrontendEntryPoints), - "getFrontendRule": p.getFrontendRule, - "getFrontendBackend": getFrontendBackend, - "getFrontEndName": getFrontEndName, + "getFrontEndName": getFrontendName, + "getEntryPoints": getFuncSliceStringValue(label.TraefikFrontendEntryPoints), + "getBasicAuth": getFuncSliceStringValue(label.TraefikFrontendAuthBasic), + "getWhitelistSourceRange": getFuncSliceStringValue(label.TraefikFrontendWhitelistSourceRange), + "getPriority": getFuncStringValue(label.TraefikFrontendPriority, label.DefaultFrontendPriority), + "getPassHostHeader": getFuncBoolValue(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getPassTLSCert": getFuncBoolValue(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), + "getFrontendRule": p.getFrontendRule, + "getRedirect": getRedirect, + "getErrorPages": getErrorPages, + "getRateLimit": getRateLimit, + "getHeaders": getHeaders, + + // TODO Deprecated [breaking] + "getFrontendBackend": getBackendName, } // filter tasks @@ -41,6 +62,7 @@ func (p *Provider) buildConfiguration(tasks []state.Task) *types.Configuration { return taskFilter(task, p.ExposedByDefault) }, tasks).([]state.Task) + // Deprecated var filteredApps []state.Task uniqueApps := make(map[string]struct{}) for _, task := range filteredTasks { @@ -50,14 +72,25 @@ func (p *Provider) buildConfiguration(tasks []state.Task) *types.Configuration { } } + appsTasks := make(map[string][]state.Task) + for _, task := range filteredTasks { + if _, ok := appsTasks[task.DiscoveryInfo.Name]; !ok { + appsTasks[task.DiscoveryInfo.Name] = []state.Task{task} + } else { + appsTasks[task.DiscoveryInfo.Name] = append(appsTasks[task.DiscoveryInfo.Name], task) + } + } + templateObjects := struct { - Applications []state.Task - Tasks []state.Task - Domain string + ApplicationsTasks map[string][]state.Task + Applications []state.Task // Deprecated + Tasks []state.Task // Deprecated + Domain string }{ - Applications: filteredApps, - Tasks: filteredTasks, - Domain: p.Domain, + ApplicationsTasks: appsTasks, + Applications: filteredApps, // Deprecated + Tasks: filteredTasks, // Deprecated + Domain: p.Domain, } configuration, err := p.GetConfiguration("templates/mesos.tmpl", mesosFuncMap, templateObjects) @@ -126,39 +159,58 @@ func getID(task state.Task) string { return provider.Normalize(task.ID) } +// Deprecated func getBackend(task state.Task, apps []state.Task) string { - application, err := getApplication(task, apps) + _, err := getApplication(task, apps) if err != nil { log.Error(err) return "" } - return getFrontendBackend(application) + return getBackendName(task) } -func getFrontendBackend(task state.Task) string { +func getBackendName(task state.Task) string { if value := getStringValue(task, label.TraefikBackend, ""); len(value) > 0 { return value } return provider.Normalize(task.DiscoveryInfo.Name) } -func getFrontEndName(task state.Task) string { +func getFrontendName(task state.Task) string { + // TODO task.ID -> task.Name + task.ID return provider.Normalize(task.ID) } +func (p *Provider) getServerPort(task state.Task) string { + plv := getIntValue(task, label.TraefikPortIndex, math.MinInt32, len(task.DiscoveryInfo.Ports.DiscoveryPorts)-1) + if plv >= 0 { + return strconv.Itoa(task.DiscoveryInfo.Ports.DiscoveryPorts[plv].Number) + } + + if pv := getStringValue(task, label.TraefikPort, ""); len(pv) > 0 { + return pv + } + + for _, port := range task.DiscoveryInfo.Ports.DiscoveryPorts { + return strconv.Itoa(port.Number) + } + return "" +} + +// Deprecated func (p *Provider) getPort(task state.Task, applications []state.Task) string { - application, err := getApplication(task, applications) + _, err := getApplication(task, applications) if err != nil { log.Error(err) return "" } - plv := getIntValue(application, label.TraefikPortIndex, math.MinInt32, len(task.DiscoveryInfo.Ports.DiscoveryPorts)-1) + plv := getIntValue(task, label.TraefikPortIndex, math.MinInt32, len(task.DiscoveryInfo.Ports.DiscoveryPorts)-1) if plv >= 0 { return strconv.Itoa(task.DiscoveryInfo.Ports.DiscoveryPorts[plv].Number) } - if pv := getStringValue(application, label.TraefikPort, ""); len(pv) > 0 { + if pv := getStringValue(task, label.TraefikPort, ""); len(pv) > 0 { return pv } @@ -191,16 +243,174 @@ func (p *Provider) getSubDomain(name string) string { return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1) } +func getCircuitBreaker(task state.Task) *types.CircuitBreaker { + circuitBreaker := getStringValue(task, label.TraefikBackendCircuitBreakerExpression, "") + if len(circuitBreaker) == 0 { + return nil + } + return &types.CircuitBreaker{Expression: circuitBreaker} +} + +func getLoadBalancer(task state.Task) *types.LoadBalancer { + if !hasPrefix(task, label.TraefikBackendLoadBalancer) { + return nil + } + + method := getStringValue(task, label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod) + + lb := &types.LoadBalancer{ + Method: method, + } + + if getBoolValue(task, label.TraefikBackendLoadBalancerStickiness, false) { + cookieName := getStringValue(task, label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName) + lb.Stickiness = &types.Stickiness{CookieName: cookieName} + } + + return lb +} + +func getMaxConn(task state.Task) *types.MaxConn { + amount := getInt64Value(task, label.TraefikBackendMaxConnAmount, math.MinInt64) + extractorFunc := getStringValue(task, label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc) + + if amount == math.MinInt64 || len(extractorFunc) == 0 { + return nil + } + + return &types.MaxConn{ + Amount: amount, + ExtractorFunc: extractorFunc, + } +} + +func getHealthCheck(task state.Task) *types.HealthCheck { + path := getStringValue(task, label.TraefikBackendHealthCheckPath, "") + if len(path) == 0 { + return nil + } + + port := getIntValue(task, label.TraefikBackendHealthCheckPort, label.DefaultBackendHealthCheckPort, math.MaxInt32) + interval := getStringValue(task, label.TraefikBackendHealthCheckInterval, "") + + return &types.HealthCheck{ + Path: path, + Port: port, + Interval: interval, + } +} + +func (p *Provider) getServers(tasks []state.Task) map[string]types.Server { + var servers map[string]types.Server + + for _, task := range tasks { + if servers == nil { + servers = make(map[string]types.Server) + } + + protocol := getStringValue(task, label.TraefikProtocol, label.DefaultProtocol) + host := p.getHost(task) + port := p.getServerPort(task) + + serverName := "server-" + getID(task) + servers[serverName] = types.Server{ + URL: fmt.Sprintf("%s://%s:%s", protocol, host, port), + Weight: getIntValue(task, label.TraefikWeight, label.DefaultWeightInt, math.MaxInt32), + } + } + + return servers +} + +func getRedirect(task state.Task) *types.Redirect { + if hasLabel(task, label.TraefikFrontendRedirectEntryPoint) { + return &types.Redirect{ + EntryPoint: getStringValue(task, label.TraefikFrontendRedirectEntryPoint, ""), + } + } + + if hasLabel(task, label.TraefikFrontendRedirectRegex) && + hasLabel(task, label.TraefikFrontendRedirectReplacement) { + return &types.Redirect{ + Regex: getStringValue(task, label.TraefikFrontendRedirectRegex, ""), + Replacement: getStringValue(task, label.TraefikFrontendRedirectReplacement, ""), + } + } + + return nil +} + +func getErrorPages(task state.Task) map[string]*types.ErrorPage { + prefix := label.Prefix + label.BaseFrontendErrorPage + labels := taskLabelsToMap(task) + return label.ParseErrorPages(labels, prefix, label.RegexpFrontendErrorPage) +} + +func getRateLimit(task state.Task) *types.RateLimit { + extractorFunc := getStringValue(task, label.TraefikFrontendRateLimitExtractorFunc, "") + if len(extractorFunc) == 0 { + return nil + } + + labels := taskLabelsToMap(task) + prefix := label.Prefix + label.BaseFrontendRateLimit + limits := label.ParseRateSets(labels, prefix, label.RegexpFrontendRateLimit) + + return &types.RateLimit{ + ExtractorFunc: extractorFunc, + RateSet: limits, + } +} + +func getHeaders(task state.Task) *types.Headers { + labels := taskLabelsToMap(task) + + 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, ""), + } + + if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() { + return nil + } + + return headers +} + +func isEnabled(task state.Task, exposedByDefault bool) bool { + return getBoolValue(task, label.TraefikEnable, exposedByDefault) +} + // Label functions +// Deprecated func getFuncApplicationStringValue(labelName string, defaultValue string) func(task state.Task, applications []state.Task) string { return func(task state.Task, applications []state.Task) string { - app, err := getApplication(task, applications) - if err == nil { - return getStringValue(app, labelName, defaultValue) + _, err := getApplication(task, applications) + if err != nil { + log.Error(err) + return defaultValue } - log.Error(err) - return defaultValue + + return getStringValue(task, labelName, defaultValue) } } @@ -210,6 +420,12 @@ func getFuncStringValue(labelName string, defaultValue string) func(task state.T } } +func getFuncBoolValue(labelName string, defaultValue bool) func(task state.Task) bool { + return func(task state.Task) bool { + return getBoolValue(task, labelName, defaultValue) + } +} + func getFuncSliceStringValue(labelName string) func(task state.Task) []string { return func(task state.Task) []string { return getSliceStringValue(task, labelName) @@ -218,7 +434,7 @@ func getFuncSliceStringValue(labelName string) func(task state.Task) []string { func getStringValue(task state.Task, labelName string, defaultValue string) string { for _, lbl := range task.Labels { - if lbl.Key == labelName { + if lbl.Key == labelName && len(lbl.Value) > 0 { return lbl.Value } } @@ -263,6 +479,7 @@ func getSliceStringValue(task state.Task, labelName string) []string { return nil } +// Deprecated func getApplication(task state.Task, apps []state.Task) (state.Task, error) { for _, app := range apps { if app.DiscoveryInfo.Name == task.DiscoveryInfo.Name { @@ -272,6 +489,41 @@ func getApplication(task state.Task, apps []state.Task) (state.Task, error) { return state.Task{}, fmt.Errorf("unable to get Mesos application from task %s", task.DiscoveryInfo.Name) } -func isEnabled(task state.Task, exposedByDefault bool) bool { - return getBoolValue(task, label.TraefikEnable, exposedByDefault) +func hasPrefix(task state.Task, prefix string) bool { + for _, lbl := range task.Labels { + if strings.HasPrefix(lbl.Key, prefix) { + return true + } + } + return false +} + +func getInt64Value(task state.Task, labelName string, defaultValue int64) int64 { + for _, lbl := range task.Labels { + if lbl.Key == labelName { + value, err := strconv.ParseInt(lbl.Value, 10, 64) + if err != nil { + log.Warnf("Unable to parse %q: %q, falling back to %v. %v", labelName, lbl.Value, defaultValue, err) + } + return value + } + } + return defaultValue +} + +func hasLabel(task state.Task, label string) bool { + for _, lbl := range task.Labels { + if lbl.Key == label { + return true + } + } + return false +} + +func taskLabelsToMap(task state.Task) map[string]string { + labels := make(map[string]string) + for _, lbl := range task.Labels { + labels[lbl.Key] = lbl.Value + } + return labels } diff --git a/provider/mesos/config_test.go b/provider/mesos/config_test.go index 0a03099de..0ef46fa67 100644 --- a/provider/mesos/config_test.go +++ b/provider/mesos/config_test.go @@ -2,7 +2,9 @@ package mesos import ( "testing" + "time" + "github.com/containous/flaeg" "github.com/containous/traefik/provider/label" "github.com/containous/traefik/types" "github.com/mesos/mesos-go/upid" @@ -11,7 +13,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestBuildConfiguration(t *testing.T) { +func TestBuildConfigurationNew(t *testing.T) { p := &Provider{ Domain: "docker.localhost", ExposedByDefault: true, @@ -25,7 +27,7 @@ func TestBuildConfiguration(t *testing.T) { expectedBackends map[string]*types.Backend }{ { - desc: "should return an empty configuration when no task", + desc: "when no tasks", tasks: []state.Task{}, expectedFrontends: map[string]*types.Frontend{}, expectedBackends: map[string]*types.Backend{}, @@ -64,6 +66,7 @@ func TestBuildConfiguration(t *testing.T) { "frontend-ID1": { Backend: "backend-name1", EntryPoints: []string{}, + BasicAuth: []string{}, PassHostHeader: true, Routes: map[string]types.Route{ "route-host-ID1": { @@ -74,6 +77,7 @@ func TestBuildConfiguration(t *testing.T) { "frontend-ID3": { Backend: "backend-name2", EntryPoints: []string{}, + BasicAuth: []string{}, PassHostHeader: true, Routes: map[string]types.Route{ "route-host-ID3": { @@ -119,12 +123,62 @@ func TestBuildConfiguration(t *testing.T) { withLabel(label.TraefikBackend, "foobar"), + withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), + withLabel(label.TraefikBackendHealthCheckPath, "/health"), + withLabel(label.TraefikBackendHealthCheckPort, "880"), + withLabel(label.TraefikBackendHealthCheckInterval, "6"), + withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), + withLabel(label.TraefikBackendLoadBalancerStickiness, "true"), + withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "chocolate"), + withLabel(label.TraefikBackendMaxConnAmount, "666"), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + + withLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), withLabel(label.TraefikFrontendEntryPoints, "http,https"), withLabel(label.TraefikFrontendPassHostHeader, "true"), withLabel(label.TraefikFrontendPassTLSCert, "true"), withLabel(label.TraefikFrontendPriority, "666"), + withLabel(label.TraefikFrontendRedirectEntryPoint, "https"), + withLabel(label.TraefikFrontendRedirectRegex, "nope"), + withLabel(label.TraefikFrontendRedirectReplacement, "nope"), withLabel(label.TraefikFrontendRule, "Host:traefik.io"), + withLabel(label.TraefikFrontendWhitelistSourceRange, "10.10.10.10"), + withLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type:application/json; charset=utf-8"), + withLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type:application/json; charset=utf-8"), + withLabel(label.TraefikFrontendSSLProxyHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type:application/json; charset=utf-8"), + withLabel(label.TraefikFrontendAllowedHosts, "foo,bar,bor"), + withLabel(label.TraefikFrontendHostsProxyHeaders, "foo,bar,bor"), + withLabel(label.TraefikFrontendSSLHost, "foo"), + withLabel(label.TraefikFrontendCustomFrameOptionsValue, "foo"), + withLabel(label.TraefikFrontendContentSecurityPolicy, "foo"), + withLabel(label.TraefikFrontendPublicKey, "foo"), + withLabel(label.TraefikFrontendReferrerPolicy, "foo"), + withLabel(label.TraefikFrontendSTSSeconds, "666"), + withLabel(label.TraefikFrontendSSLRedirect, "true"), + withLabel(label.TraefikFrontendSSLTemporaryRedirect, "true"), + withLabel(label.TraefikFrontendSTSIncludeSubdomains, "true"), + withLabel(label.TraefikFrontendSTSPreload, "true"), + withLabel(label.TraefikFrontendForceSTSHeader, "true"), + withLabel(label.TraefikFrontendFrameDeny, "true"), + withLabel(label.TraefikFrontendContentTypeNosniff, "true"), + withLabel(label.TraefikFrontendBrowserXSSFilter, "true"), + withLabel(label.TraefikFrontendIsDevelopment, "true"), + + withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageStatus, "404"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageBackend, "foobar"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageQuery, "foo_query"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageStatus, "500,600"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageBackend, "foobar"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageQuery, "bar_query"), + + withLabel(label.TraefikFrontendRateLimitExtractorFunc, "client.ip"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), withIP("10.10.10.10"), withInfo("name1", withPorts( withPortTCP(80, "n"), @@ -145,7 +199,86 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: true, + PassTLSCert: true, Priority: 666, + BasicAuth: []string{ + "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }, + WhitelistSourceRange: []string{ + "10.10.10.10", + }, + Headers: &types.Headers{ + CustomRequestHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + CustomResponseHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + AllowedHosts: []string{ + "foo", + "bar", + "bor", + }, + HostsProxyHeaders: []string{ + "foo", + "bar", + "bor", + }, + SSLRedirect: true, + SSLTemporaryRedirect: true, + SSLHost: "foo", + SSLProxyHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + STSSeconds: 666, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + CustomFrameOptionsValue: "foo", + ContentTypeNosniff: true, + BrowserXSSFilter: true, + ContentSecurityPolicy: "foo", + PublicKey: "foo", + ReferrerPolicy: "foo", + IsDevelopment: true, + }, + Errors: map[string]*types.ErrorPage{ + "foo": { + Status: []string{"404"}, + Query: "foo_query", + Backend: "foobar", + }, + "bar": { + Status: []string{"500", "600"}, + Query: "bar_query", + Backend: "foobar", + }, + }, + RateLimit: &types.RateLimit{ + ExtractorFunc: "client.ip", + RateSet: map[string]*types.Rate{ + "foo": { + Period: flaeg.Duration(6 * time.Second), + Average: 12, + Burst: 18, + }, + "bar": { + Period: flaeg.Duration(3 * time.Second), + Average: 6, + Burst: 9, + }, + }, + }, + Redirect: &types.Redirect{ + EntryPoint: "https", + Regex: "", + Replacement: "", + }, }, }, expectedBackends: map[string]*types.Backend{ @@ -156,6 +289,24 @@ func TestBuildConfiguration(t *testing.T) { Weight: 12, }, }, + CircuitBreaker: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", + }, + LoadBalancer: &types.LoadBalancer{ + Method: "drr", + Stickiness: &types.Stickiness{ + CookieName: "chocolate", + }, + }, + MaxConn: &types.MaxConn{ + Amount: 666, + ExtractorFunc: "client.ip", + }, + HealthCheck: &types.HealthCheck{ + Path: "/health", + Port: 880, + Interval: "6", + }, }, }, }, @@ -434,3 +585,597 @@ func TestGetSubDomain(t *testing.T) { }) } } + +func TestGetCircuitBreaker(t *testing.T) { + testCases := []struct { + desc string + task state.Task + expected *types.CircuitBreaker + }{ + { + desc: "should return nil when no CB labels", + task: aTask("ID1", + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + expected: nil, + }, + { + desc: "should return a struct CB when CB labels are set", + task: aTask("ID1", + withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + 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.task) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetLoadBalancer(t *testing.T) { + testCases := []struct { + desc string + task state.Task + expected *types.LoadBalancer + }{ + { + desc: "should return nil when no LB labels", + task: aTask("ID1", + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + expected: nil, + }, + { + desc: "should return a struct when labels are set", + task: aTask("ID1", + withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), + withLabel(label.TraefikBackendLoadBalancerStickiness, "true"), + withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "foo"), + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + expected: &types.LoadBalancer{ + Method: "drr", + Stickiness: &types.Stickiness{ + CookieName: "foo", + }, + }, + }, + { + desc: "should return a nil Stickiness when Stickiness is not set", + task: aTask("ID1", + withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), + withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "foo"), + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + expected: &types.LoadBalancer{ + Method: "drr", + Stickiness: nil, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getLoadBalancer(test.task) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetMaxConn(t *testing.T) { + testCases := []struct { + desc string + task state.Task + expected *types.MaxConn + }{ + { + desc: "should return nil when no max conn labels", + task: aTask("ID1", + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + expected: nil, + }, + { + desc: "should return nil when no amount label", + task: aTask("ID1", + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + expected: nil, + }, + { + desc: "should return default when empty extractorFunc label", + task: aTask("ID1", + withLabel(label.TraefikBackendMaxConnExtractorFunc, ""), + withLabel(label.TraefikBackendMaxConnAmount, "666"), + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + expected: &types.MaxConn{ + ExtractorFunc: "request.host", + Amount: 666, + }, + }, + { + desc: "should return a struct when max conn labels are set", + task: aTask("ID1", + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + withLabel(label.TraefikBackendMaxConnAmount, "666"), + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + 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.task) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetHealthCheck(t *testing.T) { + testCases := []struct { + desc string + task state.Task + expected *types.HealthCheck + }{ + { + desc: "should return nil when no health check labels", + task: aTask("ID1", + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + expected: nil, + }, + { + desc: "should return nil when no health check Path label", + task: aTask("ID1", + withLabel(label.TraefikBackendHealthCheckPort, "80"), + withLabel(label.TraefikBackendHealthCheckInterval, "6"), + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + expected: nil, + }, + { + desc: "should return a struct when health check labels are set", + task: aTask("ID1", + withLabel(label.TraefikBackendHealthCheckPath, "/health"), + withLabel(label.TraefikBackendHealthCheckPort, "80"), + withLabel(label.TraefikBackendHealthCheckInterval, "6"), + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + 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.task) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetServers(t *testing.T) { + testCases := []struct { + desc string + tasks []state.Task + expected map[string]types.Server + }{ + { + desc: "", + tasks: []state.Task{ + // App 1 + aTask("ID1", + withIP("10.10.10.10"), + withInfo("name1", + withPorts(withPort("TCP", 80, "WEB"))), + withStatus(withHealthy(true), withState("TASK_RUNNING")), + ), + aTask("ID2", + withIP("10.10.10.11"), + withLabel(label.TraefikWeight, "18"), + withInfo("name1", + withPorts(withPort("TCP", 81, "WEB"))), + withStatus(withHealthy(true), withState("TASK_RUNNING")), + ), + // App 2 + aTask("ID3", + withLabel(label.TraefikWeight, "12"), + withIP("20.10.10.10"), + withInfo("name2", + withPorts(withPort("TCP", 80, "WEB"))), + withStatus(withHealthy(true), withState("TASK_RUNNING")), + ), + aTask("ID4", + withLabel(label.TraefikWeight, "6"), + withIP("20.10.10.11"), + withInfo("name2", + withPorts(withPort("TCP", 81, "WEB"))), + withStatus(withHealthy(true), withState("TASK_RUNNING")), + ), + }, + expected: map[string]types.Server{ + "server-ID1": { + URL: "http://10.10.10.10:80", + Weight: 0, + }, + "server-ID2": { + URL: "http://10.10.10.11:81", + Weight: 18, + }, + "server-ID3": { + URL: "http://20.10.10.10:80", + Weight: 12, + }, + "server-ID4": { + URL: "http://20.10.10.11:81", + Weight: 6, + }, + }, + }, + } + + p := &Provider{ + Domain: "docker.localhost", + ExposedByDefault: true, + IPSources: "host", + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := p.getServers(test.tasks) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetRedirect(t *testing.T) { + testCases := []struct { + desc string + task state.Task + expected *types.Redirect + }{ + + { + desc: "should return nil when no redirect labels", + task: aTask("ID1", + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + expected: nil, + }, + { + desc: "should use only entry point tag when mix regex redirect and entry point redirect", + task: aTask("ID1", + withLabel(label.TraefikFrontendRedirectEntryPoint, "https"), + withLabel(label.TraefikFrontendRedirectRegex, "(.*)"), + withLabel(label.TraefikFrontendRedirectReplacement, "$1"), + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + expected: &types.Redirect{ + EntryPoint: "https", + }, + }, + { + desc: "should return a struct when entry point redirect label", + task: aTask("ID1", + withLabel(label.TraefikFrontendRedirectEntryPoint, "https"), + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + expected: &types.Redirect{ + EntryPoint: "https", + }, + }, + { + desc: "should return a struct when regex redirect labels", + task: aTask("ID1", + withLabel(label.TraefikFrontendRedirectRegex, "(.*)"), + withLabel(label.TraefikFrontendRedirectReplacement, "$1"), + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + expected: &types.Redirect{ + Regex: "(.*)", + Replacement: "$1", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getRedirect(test.task) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetErrorPages(t *testing.T) { + testCases := []struct { + desc string + task state.Task + expected map[string]*types.ErrorPage + }{ + { + desc: "2 errors pages", + task: aTask("ID1", + withIP("10.10.10.10"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageStatus, "404"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageBackend, "foo_backend"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageQuery, "foo_query"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageStatus, "500,600"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageBackend, "bar_backend"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageQuery, "bar_query"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + 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", + task: aTask("ID1", + withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageStatus, "404"), + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + 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() + + actual := getErrorPages(test.task) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetRateLimit(t *testing.T) { + testCases := []struct { + desc string + task state.Task + expected *types.RateLimit + }{ + { + desc: "should return nil when no rate limit labels", + task: aTask("ID1", + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + expected: nil, + }, + { + desc: "should return a struct when rate limit labels are defined", + task: aTask("ID1", + 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"), + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + 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", + task: aTask("ID1", + 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"), + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + expected: nil, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getRateLimit(test.task) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetHeaders(t *testing.T) { + testCases := []struct { + desc string + task state.Task + expected *types.Headers + }{ + { + desc: "should return nil when no custom headers options are set", + task: aTask("ID1", + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + expected: nil, + }, + { + desc: "should return a struct when all custom headers options are set", + task: aTask("ID1", + withLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + withLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + withLabel(label.TraefikFrontendSSLProxyHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + withLabel(label.TraefikFrontendAllowedHosts, "foo,bar,bor"), + withLabel(label.TraefikFrontendHostsProxyHeaders, "foo,bar,bor"), + withLabel(label.TraefikFrontendSSLHost, "foo"), + withLabel(label.TraefikFrontendCustomFrameOptionsValue, "foo"), + withLabel(label.TraefikFrontendContentSecurityPolicy, "foo"), + withLabel(label.TraefikFrontendPublicKey, "foo"), + withLabel(label.TraefikFrontendReferrerPolicy, "foo"), + withLabel(label.TraefikFrontendSTSSeconds, "666"), + withLabel(label.TraefikFrontendSSLRedirect, "true"), + withLabel(label.TraefikFrontendSSLTemporaryRedirect, "true"), + withLabel(label.TraefikFrontendSTSIncludeSubdomains, "true"), + withLabel(label.TraefikFrontendSTSPreload, "true"), + withLabel(label.TraefikFrontendForceSTSHeader, "true"), + withLabel(label.TraefikFrontendFrameDeny, "true"), + withLabel(label.TraefikFrontendContentTypeNosniff, "true"), + withLabel(label.TraefikFrontendBrowserXSSFilter, "true"), + withLabel(label.TraefikFrontendIsDevelopment, "true"), + withIP("10.10.10.10"), + withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), + withDefaultStatus(), + ), + expected: &types.Headers{ + CustomRequestHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + CustomResponseHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + SSLProxyHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + AllowedHosts: []string{"foo", "bar", "bor"}, + HostsProxyHeaders: []string{"foo", "bar", "bor"}, + SSLHost: "foo", + CustomFrameOptionsValue: "foo", + ContentSecurityPolicy: "foo", + PublicKey: "foo", + ReferrerPolicy: "foo", + STSSeconds: 666, + SSLRedirect: true, + SSLTemporaryRedirect: true, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + ContentTypeNosniff: true, + BrowserXSSFilter: true, + IsDevelopment: true, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getHeaders(test.task) + + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/templates/mesos.tmpl b/templates/mesos.tmpl index 867a5339d..2273c80ea 100644 --- a/templates/mesos.tmpl +++ b/templates/mesos.tmpl @@ -1,29 +1,163 @@ -{{ $apps := .Applications }} - [backends] -{{range $task := .Tasks }} - {{ $backendName := getBackend $task $apps }} +{{range $applicationName, $tasks := .ApplicationsTasks }} + {{ $app := index $tasks 0 }} + {{ $backendName := getBackendName $app }} - [backends.backend-{{ $backendName }}.servers.server-{{ getID $task }}] - url = "{{ getProtocol $task $apps }}://{{ getHost $task }}:{{ getPort $task $apps }}" - weight = {{ getWeight $task $apps }} + [backends.backend-{{ $backendName }}] + {{ $circuitBreaker := getCircuitBreaker $app }} + {{if $circuitBreaker }} + [backends."backend-{{ $backendName }}".circuitBreaker] + expression = "{{ $circuitBreaker.Expression }}" + {{end}} + + {{ $loadBalancer := getLoadBalancer $app }} + {{if $loadBalancer }} + [backends."backend-{{ $backendName }}".loadBalancer] + method = "{{ $loadBalancer.Method }}" + sticky = {{ $loadBalancer.Sticky }} + {{if $loadBalancer.Stickiness }} + [backends."backend-{{ $backendName }}".loadBalancer.stickiness] + cookieName = "{{ $loadBalancer.Stickiness.CookieName }}" + {{end}} + {{end}} + + {{ $maxConn := getMaxConn $app }} + {{if $maxConn }} + [backends."backend-{{ $backendName }}".maxConn] + extractorFunc = "{{ $maxConn.ExtractorFunc }}" + amount = {{ $maxConn.Amount }} + {{end}} + + {{ $healthCheck := getHealthCheck $app }} + {{if $healthCheck }} + [backends.backend-{{ $backendName }}.healthCheck] + path = "{{ $healthCheck.Path }}" + port = {{ $healthCheck.Port }} + interval = "{{ $healthCheck.Interval }}" + {{end}} + + {{range $serverName, $server := getServers $tasks }} + [backends.backend-{{ $backendName }}.servers.{{ $serverName }}] + url = "{{ $server.URL }}" + weight = {{ $server.Weight }} + {{end}} {{end}} [frontends] -{{range $app := $apps }} +{{range $applicationName, $tasks := .ApplicationsTasks }} + {{ $app := index $tasks 0 }} {{ $frontendName := getFrontEndName $app }} [frontends.frontend-{{ $frontendName }}] - backend = "backend-{{ getFrontendBackend $app }}" + backend = "backend-{{ getBackendName $app }}" priority = {{ getPriority $app }} passHostHeader = {{ getPassHostHeader $app }} + passTLSCert = {{ getPassTLSCert $app }} entryPoints = [{{range getEntryPoints $app }} "{{.}}", {{end}}] - [frontends.frontend-{{ $frontendName }}.routes.route-host-{{ $frontendName }}] - rule = "{{ getFrontendRule $app }}" + {{ $whitelistSourceRange := getWhitelistSourceRange $app }} + {{if $whitelistSourceRange }} + whitelistSourceRange = [{{range $whitelistSourceRange }} + "{{.}}", + {{end}}] + {{end}} -{{end}} + basicAuth = [{{range getBasicAuth $app }} + "{{.}}", + {{end}}] + + {{ $redirect := getRedirect $app }} + {{if $redirect }} + [frontends."frontend-{{ $frontendName }}".redirect] + entryPoint = "{{ $redirect.EntryPoint }}" + regex = "{{ $redirect.Regex }}" + replacement = "{{ $redirect.Replacement }}" + {{end}} + + {{ $errorPages := getErrorPages $app }} + {{if $errorPages }} + [frontends."frontend-{{ $frontendName }}".errors] + {{range $pageName, $page := $errorPages }} + [frontends."frontend-{{ $frontendName }}".errors.{{ $pageName }}] + status = [{{range $page.Status }} + "{{.}}", + {{end}}] + backend = "{{ $page.Backend }}" + query = "{{ $page.Query }}" + {{end}} + {{end}} + + {{ $rateLimit := getRateLimit $app }} + {{if $rateLimit }} + [frontends."frontend-{{ $frontendName }}".rateLimit] + extractorFunc = "{{ $rateLimit.ExtractorFunc }}" + [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet] + {{ range $limitName, $limit := $rateLimit.RateSet }} + [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet.{{ $limitName }}] + period = "{{ $limit.Period }}" + average = {{ $limit.Average }} + burst = {{ $limit.Burst }} + {{end}} + {{end}} + + {{ $headers := getHeaders $app }} + {{if $headers }} + [frontends."frontend-{{ $frontendName }}".headers] + SSLRedirect = {{ $headers.SSLRedirect }} + SSLTemporaryRedirect = {{ $headers.SSLTemporaryRedirect }} + SSLHost = "{{ $headers.SSLHost }}" + STSSeconds = {{ $headers.STSSeconds }} + STSIncludeSubdomains = {{ $headers.STSIncludeSubdomains }} + STSPreload = {{ $headers.STSPreload }} + ForceSTSHeader = {{ $headers.ForceSTSHeader }} + FrameDeny = {{ $headers.FrameDeny }} + CustomFrameOptionsValue = "{{ $headers.CustomFrameOptionsValue }}" + ContentTypeNosniff = {{ $headers.ContentTypeNosniff }} + BrowserXSSFilter = {{ $headers.BrowserXSSFilter }} + ContentSecurityPolicy = "{{ $headers.ContentSecurityPolicy }}" + PublicKey = "{{ $headers.PublicKey }}" + ReferrerPolicy = "{{ $headers.ReferrerPolicy }}" + IsDevelopment = {{ $headers.IsDevelopment }} + + {{if $headers.AllowedHosts }} + AllowedHosts = [{{range $headers.AllowedHosts }} + "{{.}}", + {{end}}] + {{end}} + + {{if $headers.HostsProxyHeaders }} + HostsProxyHeaders = [{{range $headers.HostsProxyHeaders }} + "{{.}}", + {{end}}] + {{end}} + + {{if $headers.CustomRequestHeaders }} + [frontends."frontend-{{ $frontendName }}".headers.customRequestHeaders] + {{range $k, $v := $headers.CustomRequestHeaders }} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + + {{if $headers.CustomResponseHeaders }} + [frontends."frontend-{{ $frontendName }}".headers.customResponseHeaders] + {{range $k, $v := $headers.CustomResponseHeaders }} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + + {{if $headers.SSLProxyHeaders }} + [frontends."frontend-{{ $frontendName }}".headers.SSLProxyHeaders] + {{range $k, $v := $headers.SSLProxyHeaders }} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + {{end}} + + [frontends.frontend-{{ $frontendName }}.routes.route-host-{{ $frontendName }}] + rule = "{{ getFrontendRule $app }}" + +{{end}} \ No newline at end of file