diff --git a/docs/configuration/backends/mesos.md b/docs/configuration/backends/mesos.md index 68ee3733c..2a51c0d04 100644 --- a/docs/configuration/backends/mesos.md +++ b/docs/configuration/backends/mesos.md @@ -110,7 +110,8 @@ The following labels can be defined on Mesos tasks. They adjust the behavior for |------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `traefik.domain` | Default domain used for frontend rules. | | `traefik.enable=false` | Disable this container in Træfik | -| `traefik.port=80` | Register this port. Useful when the container exposes multiples ports. | +| `traefik.port=80` | Register this port. Useful when the application exposes multiple ports. | +| `traefik.portName=web` | Register port by name in the application's ports array. Useful when the application exposes multiple ports. | | `traefik.portIndex=1` | Register port by index in the application's ports array. Useful when the application exposes multiple ports. | | `traefik.protocol=https` | Override the default `http` protocol | | `traefik.weight=10` | Assign this weight to the container | @@ -183,3 +184,74 @@ The following labels can be defined on Mesos tasks. They adjust the behavior for | `traefik.frontend.headers.STSSeconds=315360000` | Sets the max-age of the STS header. | | `traefik.frontend.headers.STSIncludeSubdomains=true` | Adds the `IncludeSubdomains` section of the STS header. | | `traefik.frontend.headers.STSPreload=true` | Adds the preload flag to the STS header. | + +### Applications with Multiple Ports (segment labels) + +Segment labels are used to define routes to an application exposing multiple ports. +A segment is a group of labels that apply to a port exposed by an application. +You can define as many segments as ports exposed in an application. + +Additionally, if a segment name matches a named port, that port will be used unless `portIndex`, `portName`, or `port` labels are specified for that segment. + +Segment labels override the default behavior. + +| Label | Description | +|---------------------------------------------------------------------------|-------------------------------------------------------------| +| `traefik..backend=BACKEND` | Same as `traefik.backend` | +| `traefik..domain=DOMAIN` | Same as `traefik.domain` | +| `traefik..portIndex=1` | Same as `traefik.portIndex` | +| `traefik..portName=web` | Same as `traefik.portName` | +| `traefik..port=PORT` | Same as `traefik.port` | +| `traefik..protocol=http` | Same as `traefik.protocol` | +| `traefik..weight=10` | Same as `traefik.weight` | +| `traefik..frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` | +| `traefik..frontend.entryPoints=https` | Same as `traefik.frontend.entryPoints` | +| `traefik..frontend.errors..backend=NAME` | Same as `traefik.frontend.errors..backend` | +| `traefik..frontend.errors..query=PATH` | Same as `traefik.frontend.errors..query` | +| `traefik..frontend.errors..status=RANGE` | Same as `traefik.frontend.errors..status` | +| `traefik..frontend.passHostHeader=true` | Same as `traefik.frontend.passHostHeader` | +| `traefik..frontend.passTLSCert=true` | Same as `traefik.frontend.passTLSCert` | +| `traefik..frontend.priority=10` | Same as `traefik.frontend.priority` | +| `traefik..frontend.rateLimit.extractorFunc=EXP` | Same as `traefik.frontend.rateLimit.extractorFunc` | +| `traefik..frontend.rateLimit.rateSet..period=6` | Same as `traefik.frontend.rateLimit.rateSet..period` | +| `traefik..frontend.rateLimit.rateSet..average=6` | Same as `traefik.frontend.rateLimit.rateSet..average` | +| `traefik..frontend.rateLimit.rateSet..burst=6` | Same as `traefik.frontend.rateLimit.rateSet..burst` | +| `traefik..frontend.redirect.entryPoint=https` | Same as `traefik.frontend.redirect.entryPoint` | +| `traefik..frontend.redirect.regex=^http://localhost/(.*)` | Same as `traefik.frontend.redirect.regex` | +| `traefik..frontend.redirect.replacement=http://mydomain/$1` | Same as `traefik.frontend.redirect.replacement` | +| `traefik..frontend.redirect.permanent=true` | Same as `traefik.frontend.redirect.permanent` | +| `traefik..frontend.rule=EXP` | Same as `traefik.frontend.rule` | +| `traefik..frontend.whiteList.sourceRange=RANGE` | Same as `traefik.frontend.whiteList.sourceRange` | +| `traefik..frontend.whiteList.useXForwardedFor=true` | Same as `traefik.frontend.whiteList.useXForwardedFor` | + +#### Custom Headers + +| Label | Description | +|----------------------------------------------------------------------|----------------------------------------------------------| +| `traefik..frontend.headers.customRequestHeaders=EXPR ` | Same as `traefik.frontend.headers.customRequestHeaders` | +| `traefik..frontend.headers.customResponseHeaders=EXPR` | Same as `traefik.frontend.headers.customResponseHeaders` | + +#### Security Headers + +| Label | Description | +|-------------------------------------------------------------------------|--------------------------------------------------------------| +| `traefik..frontend.headers.allowedHosts=EXPR` | Same as `traefik.frontend.headers.allowedHosts` | +| `traefik..frontend.headers.browserXSSFilter=true` | Same as `traefik.frontend.headers.browserXSSFilter` | +| `traefik..frontend.headers.contentSecurityPolicy=VALUE` | Same as `traefik.frontend.headers.contentSecurityPolicy` | +| `traefik..frontend.headers.contentTypeNosniff=true` | Same as `traefik.frontend.headers.contentTypeNosniff` | +| `traefik..frontend.headers.customBrowserXSSValue=VALUE` | Same as `traefik.frontend.headers.customBrowserXSSValue` | +| `traefik..frontend.headers.customFrameOptionsValue=VALUE` | Same as `traefik.frontend.headers.customFrameOptionsValue` | +| `traefik..frontend.headers.forceSTSHeader=false` | Same as `traefik.frontend.headers.forceSTSHeader` | +| `traefik..frontend.headers.frameDeny=false` | Same as `traefik.frontend.headers.frameDeny` | +| `traefik..frontend.headers.hostsProxyHeaders=EXPR` | Same as `traefik.frontend.headers.hostsProxyHeaders` | +| `traefik..frontend.headers.isDevelopment=false` | Same as `traefik.frontend.headers.isDevelopment` | +| `traefik..frontend.headers.publicKey=VALUE` | Same as `traefik.frontend.headers.publicKey` | +| `traefik..frontend.headers.referrerPolicy=VALUE` | Same as `traefik.frontend.headers.referrerPolicy` | +| `traefik..frontend.headers.SSLRedirect=true` | Same as `traefik.frontend.headers.SSLRedirect` | +| `traefik..frontend.headers.SSLTemporaryRedirect=true` | Same as `traefik.frontend.headers.SSLTemporaryRedirect` | +| `traefik..frontend.headers.SSLHost=HOST` | Same as `traefik.frontend.headers.SSLHost` | +| `traefik..frontend.headers.SSLForceHost=true` | Same as `traefik.frontend.headers.SSLForceHost` | +| `traefik..frontend.headers.SSLProxyHeaders=EXPR` | Same as `traefik.frontend.headers.SSLProxyHeaders=EXPR` | +| `traefik..frontend.headers.STSSeconds=315360000` | Same as `traefik.frontend.headers.STSSeconds=315360000` | +| `traefik..frontend.headers.STSIncludeSubdomains=true` | Same as `traefik.frontend.headers.STSIncludeSubdomains=true` | +| `traefik..frontend.headers.STSPreload=true` | Same as `traefik.frontend.headers.STSPreload=true` | diff --git a/provider/label/names.go b/provider/label/names.go index 3efcc04f2..8cf4ad9fd 100644 --- a/provider/label/names.go +++ b/provider/label/names.go @@ -7,6 +7,7 @@ const ( SuffixDomain = "domain" SuffixEnable = "enable" SuffixPort = "port" + SuffixPortName = "portName" SuffixPortIndex = "portIndex" SuffixProtocol = "protocol" SuffixTags = "tags" @@ -75,6 +76,7 @@ const ( TraefikDomain = Prefix + SuffixDomain TraefikEnable = Prefix + SuffixEnable TraefikPort = Prefix + SuffixPort + TraefikPortName = Prefix + SuffixPortName TraefikPortIndex = Prefix + SuffixPortIndex TraefikProtocol = Prefix + SuffixProtocol TraefikTags = Prefix + SuffixTags diff --git a/provider/label/segment.go b/provider/label/segment.go index 6eee9857c..e190c7d33 100644 --- a/provider/label/segment.go +++ b/provider/label/segment.go @@ -10,7 +10,7 @@ import ( var ( // SegmentPropertiesRegexp used to extract the name of the segment and the name of the property for this segment // All properties are under the format traefik..frontend.*= except the port/portIndex/weight/protocol/backend directly after traefik.. - SegmentPropertiesRegexp = regexp.MustCompile(`^traefik\.(?P.+?)\.(?Pport|portIndex|weight|protocol|backend|frontend\.(.+))$`) + SegmentPropertiesRegexp = regexp.MustCompile(`^traefik\.(?P.+?)\.(?Pport|portIndex|portName|weight|protocol|backend|frontend\.(.+))$`) // PortRegexp used to extract the port label of the segment PortRegexp = regexp.MustCompile(`^traefik\.(?P.+?)\.port$`) diff --git a/provider/mesos/config.go b/provider/mesos/config.go index a5d874c5f..db354d72d 100644 --- a/provider/mesos/config.go +++ b/provider/mesos/config.go @@ -17,12 +17,15 @@ import ( type taskData struct { state.Task TraefikLabels map[string]string + SegmentName string } func (p *Provider) buildConfigurationV2(tasks []state.Task) *types.Configuration { var mesosFuncMap = template.FuncMap{ - "getDomain": label.GetFuncString(label.TraefikDomain, p.Domain), - "getID": getID, + "getDomain": label.GetFuncString(label.TraefikDomain, p.Domain), + "getSubDomain": p.getSubDomain, + "getSegmentSubDomain": p.getSegmentSubDomain, + "getID": getID, // Backend functions "getBackendName": getBackendName, @@ -36,35 +39,22 @@ func (p *Provider) buildConfigurationV2(tasks []state.Task) *types.Configuration "getServerPort": p.getServerPort, // Frontend functions - "getFrontEndName": getFrontendName, - "getEntryPoints": label.GetFuncSliceString(label.TraefikFrontendEntryPoints), - "getBasicAuth": label.GetFuncSliceString(label.TraefikFrontendAuthBasic), - "getPriority": label.GetFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriority), - "getPassHostHeader": label.GetFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), - "getPassTLSCert": label.GetFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), - "getFrontendRule": p.getFrontendRule, - "getRedirect": label.GetRedirect, - "getErrorPages": label.GetErrorPages, - "getRateLimit": label.GetRateLimit, - "getHeaders": label.GetHeaders, - "getWhiteList": label.GetWhiteList, + "getSegmentNameSuffix": getSegmentNameSuffix, + "getFrontEndName": getFrontendName, + "getEntryPoints": label.GetFuncSliceString(label.TraefikFrontendEntryPoints), + "getBasicAuth": label.GetFuncSliceString(label.TraefikFrontendAuthBasic), + "getPriority": label.GetFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriority), + "getPassHostHeader": label.GetFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), + "getPassTLSCert": label.GetFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), + "getFrontendRule": p.getFrontendRule, + "getRedirect": label.GetRedirect, + "getErrorPages": label.GetErrorPages, + "getRateLimit": label.GetRateLimit, + "getHeaders": label.GetHeaders, + "getWhiteList": label.GetWhiteList, } - // filter tasks - appsTasks := make(map[string][]taskData) - for _, task := range tasks { - data := taskData{ - Task: task, - TraefikLabels: extractLabels(task), - } - if taskFilter(data, p.ExposedByDefault) { - if _, ok := appsTasks[task.DiscoveryInfo.Name]; !ok { - appsTasks[task.DiscoveryInfo.Name] = []taskData{data} - } else { - appsTasks[task.DiscoveryInfo.Name] = append(appsTasks[task.DiscoveryInfo.Name], data) - } - } - } + appsTasks := p.filterTasks(tasks) templateObjects := struct { ApplicationsTasks map[string][]taskData @@ -82,19 +72,47 @@ func (p *Provider) buildConfigurationV2(tasks []state.Task) *types.Configuration return configuration } -func taskFilter(task taskData, exposedByDefaultFlag bool) bool { - if len(task.DiscoveryInfo.Ports.DiscoveryPorts) == 0 { - log.Debugf("Filtering Mesos task without port %s", task.Name) - return false +func (p *Provider) filterTasks(tasks []state.Task) map[string][]taskData { + appsTasks := make(map[string][]taskData) + + for _, task := range tasks { + taskLabels := label.ExtractTraefikLabels(extractLabels(task)) + for segmentName, traefikLabels := range taskLabels { + data := taskData{ + Task: task, + TraefikLabels: traefikLabels, + SegmentName: segmentName, + } + + if taskFilter(data, p.ExposedByDefault) { + name := getName(data) + if _, ok := appsTasks[name]; !ok { + appsTasks[name] = []taskData{data} + } else { + appsTasks[name] = append(appsTasks[name], data) + } + } + } } + return appsTasks +} + +func taskFilter(task taskData, exposedByDefaultFlag bool) bool { + name := getName(task) + + if len(task.DiscoveryInfo.Ports.DiscoveryPorts) == 0 { + log.Debugf("Filtering Mesos task without port %s", name) + return false + } if !isEnabled(task, exposedByDefaultFlag) { - log.Debugf("Filtering disabled Mesos task %s", task.DiscoveryInfo.Name) + log.Debugf("Filtering disabled Mesos task %s", name) return false } // filter indeterminable task port portIndexLabel := label.GetStringValue(task.TraefikLabels, label.TraefikPortIndex, "") + portNameLabel := label.GetStringValue(task.TraefikLabels, label.TraefikPortName, "") portValueLabel := label.GetStringValue(task.TraefikLabels, label.TraefikPort, "") if portIndexLabel != "" && portValueLabel != "" { log.Debugf("Filtering Mesos task %s specifying both %q' and %q labels", task.Name, label.TraefikPortIndex, label.TraefikPort) @@ -127,10 +145,24 @@ func taskFilter(task taskData, exposedByDefaultFlag bool) bool { return false } } + if portNameLabel != "" { + var foundPort bool + for _, exposedPort := range task.DiscoveryInfo.Ports.DiscoveryPorts { + if portNameLabel == exposedPort.Name { + foundPort = true + break + } + } + + if !foundPort { + log.Debugf("Filtering Mesos task %s without a matching port for %q label", task.Name, label.TraefikPortName) + return false + } + } // filter healthChecks if task.Statuses != nil && len(task.Statuses) > 0 && task.Statuses[0].Healthy != nil && !*task.Statuses[0].Healthy { - log.Debugf("Filtering Mesos task %s with bad healthCheck", task.DiscoveryInfo.Name) + log.Debugf("Filtering Mesos task %s with bad healthCheck", name) return false } @@ -138,16 +170,27 @@ func taskFilter(task taskData, exposedByDefaultFlag bool) bool { } func getID(task taskData) string { - return provider.Normalize(task.ID) + return provider.Normalize(task.ID + getSegmentNameSuffix(task.SegmentName)) +} + +func getName(task taskData) string { + return provider.Normalize(task.DiscoveryInfo.Name + getSegmentNameSuffix(task.SegmentName)) } func getBackendName(task taskData) string { - return label.GetStringValue(task.TraefikLabels, label.TraefikBackend, provider.Normalize(task.DiscoveryInfo.Name)) + return label.GetStringValue(task.TraefikLabels, label.TraefikBackend, getName(task)) } func getFrontendName(task taskData) string { // TODO task.ID -> task.Name + task.ID - return provider.Normalize(task.ID) + return provider.Normalize(task.ID + getSegmentNameSuffix(task.SegmentName)) +} + +func getSegmentNameSuffix(serviceName string) string { + if len(serviceName) > 0 { + return "-service-" + provider.Normalize(serviceName) + } + return "" } func (p *Provider) getSubDomain(name string) string { @@ -157,7 +200,15 @@ func (p *Provider) getSubDomain(name string) string { reverseName := strings.Join(splitedName, ".") return reverseName } - return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1) + return strings.Replace(strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1), "_", "-", -1) +} + +func (p *Provider) getSegmentSubDomain(task taskData) string { + subDomain := strings.ToLower(p.getSubDomain(task.DiscoveryInfo.Name)) + if len(task.SegmentName) > 0 { + subDomain = strings.ToLower(provider.Normalize(task.SegmentName)) + "." + subDomain + } + return subDomain } // getFrontendRule returns the frontend rule for the specified application, using it's label. @@ -168,7 +219,8 @@ func (p *Provider) getFrontendRule(task taskData) string { } domain := label.GetStringValue(task.TraefikLabels, label.TraefikDomain, p.Domain) - return "Host:" + strings.ToLower(strings.Replace(p.getSubDomain(task.DiscoveryInfo.Name), "_", "-", -1)) + "." + domain + + return "Host:" + p.getSegmentSubDomain(task) + "." + domain } func (p *Provider) getServers(tasks []taskData) map[string]types.Server { @@ -198,13 +250,27 @@ func (p *Provider) getHost(task taskData) string { } func (p *Provider) getServerPort(task taskData) string { + if label.Has(task.TraefikLabels, label.TraefikPort) { + pv := label.GetIntValue(task.TraefikLabels, label.TraefikPort, 0) + if pv <= 0 { + log.Errorf("explicitly specified port %d must be larger than zero", pv) + return "" + } + return strconv.Itoa(pv) + } + plv := getIntValue(task.TraefikLabels, label.TraefikPortIndex, math.MinInt32, len(task.DiscoveryInfo.Ports.DiscoveryPorts)-1) if plv >= 0 { return strconv.Itoa(task.DiscoveryInfo.Ports.DiscoveryPorts[plv].Number) } - if pv := label.GetStringValue(task.TraefikLabels, label.TraefikPort, ""); len(pv) > 0 { - return pv + // Find named port using traefik.portName or the segment name + if pn := label.GetStringValue(task.TraefikLabels, label.TraefikPortName, task.SegmentName); len(pn) > 0 { + for _, port := range task.DiscoveryInfo.Ports.DiscoveryPorts { + if pn == port.Name { + return strconv.Itoa(port.Number) + } + } } for _, port := range task.DiscoveryInfo.Ports.DiscoveryPorts { @@ -224,7 +290,7 @@ func getIntValue(labels map[string]string, labelName string, defaultValue int, m if value <= maxValue { return value } - log.Warnf("The value %q for %q exceed the max authorized value %q, falling back to %v.", value, labelName, maxValue, defaultValue) + log.Warnf("The value %d for %s exceed the max authorized value %d, falling back to %d.", value, labelName, maxValue, defaultValue) return defaultValue } diff --git a/provider/mesos/config_test.go b/provider/mesos/config_test.go index d294c23ce..904c446fd 100644 --- a/provider/mesos/config_test.go +++ b/provider/mesos/config_test.go @@ -355,6 +355,341 @@ func TestBuildConfiguration(t *testing.T) { } } +func TestBuildConfigurationSegments(t *testing.T) { + p := &Provider{ + Domain: "mesos.localhost", + ExposedByDefault: true, + IPSources: "host", + } + + testCases := []struct { + desc string + tasks []state.Task + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{ + { + desc: "multiple ports with segments", + tasks: []state.Task{ + aTask("app-taskID", + withIP("127.0.0.1"), + withInfo("/app", + withPorts( + withPort("TCP", 80, "web"), + withPort("TCP", 81, "admin"), + ), + ), + withStatus(withHealthy(true), withState("TASK_RUNNING")), + withLabel(label.TraefikBackendMaxConnAmount, "1000"), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + withSegmentLabel(label.TraefikPort, "80", "web"), + withSegmentLabel(label.TraefikPort, "81", "admin"), + withLabel("traefik..port", "82"), // This should be ignored, as it fails to match the segmentPropertiesRegexp regex. + withSegmentLabel(label.TraefikFrontendRule, "Host:web.app.mesos.localhost", "web"), + withSegmentLabel(label.TraefikFrontendRule, "Host:admin.app.mesos.localhost", "admin"), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-app-taskID-service-web": { + Backend: "backend-app-service-web", + Routes: map[string]types.Route{ + `route-host-app-taskID-service-web`: { + Rule: "Host:web.app.mesos.localhost", + }, + }, + PassHostHeader: true, + BasicAuth: []string{}, + EntryPoints: []string{}, + }, + "frontend-app-taskID-service-admin": { + Backend: "backend-app-service-admin", + Routes: map[string]types.Route{ + `route-host-app-taskID-service-admin`: { + Rule: "Host:admin.app.mesos.localhost", + }, + }, + PassHostHeader: true, + BasicAuth: []string{}, + EntryPoints: []string{}, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-app-service-web": { + Servers: map[string]types.Server{ + "server-app-taskID-service-web": { + URL: "http://127.0.0.1:80", + Weight: label.DefaultWeight, + }, + }, + MaxConn: &types.MaxConn{ + Amount: 1000, + ExtractorFunc: "client.ip", + }, + }, + "backend-app-service-admin": { + Servers: map[string]types.Server{ + "server-app-taskID-service-admin": { + URL: "http://127.0.0.1:81", + Weight: label.DefaultWeight, + }, + }, + MaxConn: &types.MaxConn{ + Amount: 1000, + ExtractorFunc: "client.ip", + }, + }, + }, + }, + { + desc: "when all labels are set", + tasks: []state.Task{ + aTask("app-taskID", + withIP("127.0.0.1"), + withInfo("/app", + withPorts( + withPort("TCP", 80, "web"), + withPort("TCP", 81, "admin"), + ), + ), + withStatus(withHealthy(true), withState("TASK_RUNNING")), + + withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), + withLabel(label.TraefikBackendHealthCheckScheme, "http"), + withLabel(label.TraefikBackendHealthCheckPath, "/health"), + withLabel(label.TraefikBackendHealthCheckPort, "880"), + withLabel(label.TraefikBackendHealthCheckInterval, "6"), + withLabel(label.TraefikBackendHealthCheckHostname, "foo.com"), + withLabel(label.TraefikBackendHealthCheckHeaders, "Foo:bar || Bar:foo"), + withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), + withLabel(label.TraefikBackendLoadBalancerSticky, "true"), + withLabel(label.TraefikBackendLoadBalancerStickiness, "true"), + withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "chocolate"), + withLabel(label.TraefikBackendMaxConnAmount, "666"), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + withLabel(label.TraefikBackendBufferingMaxResponseBodyBytes, "10485760"), + withLabel(label.TraefikBackendBufferingMemResponseBodyBytes, "2097152"), + withLabel(label.TraefikBackendBufferingMaxRequestBodyBytes, "10485760"), + withLabel(label.TraefikBackendBufferingMemRequestBodyBytes, "2097152"), + withLabel(label.TraefikBackendBufferingRetryExpression, "IsNetworkError() && Attempts() <= 2"), + + withSegmentLabel(label.TraefikPort, "80", "containous"), + withSegmentLabel(label.TraefikPortName, "web", "containous"), + withSegmentLabel(label.TraefikProtocol, "https", "containous"), + withSegmentLabel(label.TraefikWeight, "12", "containous"), + + withSegmentLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "containous"), + withSegmentLabel(label.TraefikFrontendEntryPoints, "http,https", "containous"), + withSegmentLabel(label.TraefikFrontendPassHostHeader, "true", "containous"), + withSegmentLabel(label.TraefikFrontendPassTLSCert, "true", "containous"), + withSegmentLabel(label.TraefikFrontendPriority, "666", "containous"), + withSegmentLabel(label.TraefikFrontendRedirectEntryPoint, "https", "containous"), + withSegmentLabel(label.TraefikFrontendRedirectRegex, "nope", "containous"), + withSegmentLabel(label.TraefikFrontendRedirectReplacement, "nope", "containous"), + withSegmentLabel(label.TraefikFrontendRedirectPermanent, "true", "containous"), + withSegmentLabel(label.TraefikFrontendRule, "Host:traefik.io", "containous"), + withSegmentLabel(label.TraefikFrontendWhiteListSourceRange, "10.10.10.10", "containous"), + withSegmentLabel(label.TraefikFrontendWhiteListUseXForwardedFor, "true", "containous"), + + withSegmentLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", "containous"), + withSegmentLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", "containous"), + withSegmentLabel(label.TraefikFrontendSSLProxyHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", "containous"), + withSegmentLabel(label.TraefikFrontendAllowedHosts, "foo,bar,bor", "containous"), + withSegmentLabel(label.TraefikFrontendHostsProxyHeaders, "foo,bar,bor", "containous"), + withSegmentLabel(label.TraefikFrontendSSLForceHost, "true", "containous"), + withSegmentLabel(label.TraefikFrontendSSLHost, "foo", "containous"), + withSegmentLabel(label.TraefikFrontendCustomFrameOptionsValue, "foo", "containous"), + withSegmentLabel(label.TraefikFrontendContentSecurityPolicy, "foo", "containous"), + withSegmentLabel(label.TraefikFrontendPublicKey, "foo", "containous"), + withSegmentLabel(label.TraefikFrontendReferrerPolicy, "foo", "containous"), + withSegmentLabel(label.TraefikFrontendCustomBrowserXSSValue, "foo", "containous"), + withSegmentLabel(label.TraefikFrontendSTSSeconds, "666", "containous"), + withSegmentLabel(label.TraefikFrontendSSLRedirect, "true", "containous"), + withSegmentLabel(label.TraefikFrontendSSLTemporaryRedirect, "true", "containous"), + withSegmentLabel(label.TraefikFrontendSTSIncludeSubdomains, "true", "containous"), + withSegmentLabel(label.TraefikFrontendSTSPreload, "true", "containous"), + withSegmentLabel(label.TraefikFrontendForceSTSHeader, "true", "containous"), + withSegmentLabel(label.TraefikFrontendFrameDeny, "true", "containous"), + withSegmentLabel(label.TraefikFrontendContentTypeNosniff, "true", "containous"), + withSegmentLabel(label.TraefikFrontendBrowserXSSFilter, "true", "containous"), + withSegmentLabel(label.TraefikFrontendIsDevelopment, "true", "containous"), + + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageStatus, "404"), + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageBackend, "foobar"), + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageQuery, "foo_query"), + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageStatus, "500,600"), + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageBackend, "foobar"), + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageQuery, "bar_query"), + + withSegmentLabel(label.TraefikFrontendRateLimitExtractorFunc, "client.ip", "containous"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-app-taskID-service-containous": { + EntryPoints: []string{ + "http", + "https", + }, + Backend: "backend-app-service-containous", + Routes: map[string]types.Route{ + "route-host-app-taskID-service-containous": { + Rule: "Host:traefik.io", + }, + }, + PassHostHeader: true, + PassTLSCert: true, + Priority: 666, + BasicAuth: []string{ + "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }, + WhiteList: &types.WhiteList{ + SourceRange: []string{"10.10.10.10"}, + UseXForwardedFor: true, + }, + Headers: &types.Headers{ + CustomRequestHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + CustomResponseHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + AllowedHosts: []string{ + "foo", + "bar", + "bor", + }, + HostsProxyHeaders: []string{ + "foo", + "bar", + "bor", + }, + SSLRedirect: true, + SSLTemporaryRedirect: true, + SSLForceHost: true, + SSLHost: "foo", + SSLProxyHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + STSSeconds: 666, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + CustomFrameOptionsValue: "foo", + ContentTypeNosniff: true, + BrowserXSSFilter: true, + CustomBrowserXSSValue: "foo", + ContentSecurityPolicy: "foo", + PublicKey: "foo", + ReferrerPolicy: "foo", + IsDevelopment: true, + }, + Errors: map[string]*types.ErrorPage{ + "bar": { + Status: []string{ + "500", + "600", + }, + Backend: "backend-foobar", + Query: "bar_query", + }, + "foo": { + Status: []string{ + "404", + }, + Backend: "backend-foobar", + Query: "foo_query", + }, + }, + RateLimit: &types.RateLimit{ + RateSet: map[string]*types.Rate{ + "bar": { + Period: flaeg.Duration(3 * time.Second), + Average: 6, + Burst: 9, + }, + "foo": { + Period: flaeg.Duration(6 * time.Second), + Average: 12, + Burst: 18, + }, + }, + ExtractorFunc: "client.ip", + }, + Redirect: &types.Redirect{ + EntryPoint: "https", + Permanent: true, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-app-service-containous": { + Servers: map[string]types.Server{ + "server-app-taskID-service-containous": { + URL: "https://127.0.0.1:80", + Weight: 12, + }, + }, + CircuitBreaker: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", + }, + LoadBalancer: &types.LoadBalancer{ + Method: "drr", + Sticky: true, + Stickiness: &types.Stickiness{ + CookieName: "chocolate", + }, + }, + MaxConn: &types.MaxConn{ + Amount: 666, + ExtractorFunc: "client.ip", + }, + HealthCheck: &types.HealthCheck{ + Scheme: "http", + Path: "/health", + Port: 880, + Interval: "6", + Hostname: "foo.com", + Headers: map[string]string{ + "Bar": "foo", + "Foo": "bar", + }, + }, + Buffering: &types.Buffering{ + MaxResponseBodyBytes: 10485760, + MemResponseBodyBytes: 2097152, + MaxRequestBodyBytes: 10485760, + MemRequestBodyBytes: 2097152, + RetryExpression: "IsNetworkError() && Attempts() <= 2", + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actualConfig := p.buildConfigurationV2(test.tasks) + + require.NotNil(t, actualConfig) + assert.Equal(t, test.expectedBackends, actualConfig.Backends) + assert.Equal(t, test.expectedFrontends, actualConfig.Frontends) + }) + } +} + func TestTaskFilter(t *testing.T) { testCases := []struct { desc string @@ -370,13 +705,13 @@ func TestTaskFilter(t *testing.T) { }, { desc: "task not healthy", - mesosTask: aTaskData("test", withStatus(withState("TASK_RUNNING"))), + mesosTask: aTaskData("test", "", withStatus(withState("TASK_RUNNING"))), exposedByDefault: true, expected: false, }, { desc: "exposedByDefault false and traefik.enable false", - mesosTask: aTaskData("test", + mesosTask: aTaskData("test", "", withDefaultStatus(), withLabel(label.TraefikEnable, "false"), withInfo("test", withPorts(withPortTCP(80, "WEB"))), @@ -386,7 +721,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "traefik.enable = true", - mesosTask: aTaskData("test", + mesosTask: aTaskData("test", "", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withInfo("test", withPorts(withPortTCP(80, "WEB"))), @@ -396,7 +731,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "exposedByDefault true and traefik.enable true", - mesosTask: aTaskData("test", + mesosTask: aTaskData("test", "", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withInfo("test", withPorts(withPortTCP(80, "WEB"))), @@ -406,7 +741,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "exposedByDefault true and traefik.enable false", - mesosTask: aTaskData("test", + mesosTask: aTaskData("test", "", withDefaultStatus(), withLabel(label.TraefikEnable, "false"), withInfo("test", withPorts(withPortTCP(80, "WEB"))), @@ -416,11 +751,11 @@ func TestTaskFilter(t *testing.T) { }, { desc: "traefik.portIndex and traefik.port both set", - mesosTask: aTaskData("test", + mesosTask: aTaskData("test", "", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withLabel(label.TraefikPortIndex, "1"), - withLabel(label.TraefikEnable, "80"), + withLabel(label.TraefikPort, "80"), withInfo("test", withPorts(withPortTCP(80, "WEB"))), ), exposedByDefault: true, @@ -428,7 +763,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "valid traefik.portIndex", - mesosTask: aTaskData("test", + mesosTask: aTaskData("test", "", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withLabel(label.TraefikPortIndex, "1"), @@ -440,9 +775,37 @@ func TestTaskFilter(t *testing.T) { exposedByDefault: true, expected: true, }, + { + desc: "valid traefik.portName", + mesosTask: aTaskData("test", "", + withDefaultStatus(), + withLabel(label.TraefikEnable, "true"), + withLabel(label.TraefikPortName, "https"), + withInfo("test", withPorts( + withPortTCP(80, "http"), + withPortTCP(443, "https"), + )), + ), + exposedByDefault: true, + expected: true, + }, + { + desc: "missing traefik.portName", + mesosTask: aTaskData("test", "", + withDefaultStatus(), + withLabel(label.TraefikEnable, "true"), + withLabel(label.TraefikPortName, "foo"), + withInfo("test", withPorts( + withPortTCP(80, "http"), + withPortTCP(443, "https"), + )), + ), + exposedByDefault: true, + expected: false, + }, { desc: "default to first port index", - mesosTask: aTaskData("test", + mesosTask: aTaskData("test", "", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withInfo("test", withPorts( @@ -455,7 +818,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "traefik.portIndex and discoveryPorts don't correspond", - mesosTask: aTaskData("test", + mesosTask: aTaskData("test", "", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withLabel(label.TraefikPortIndex, "1"), @@ -466,7 +829,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "traefik.portIndex and discoveryPorts correspond", - mesosTask: aTaskData("test", + mesosTask: aTaskData("test", "", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withLabel(label.TraefikPortIndex, "0"), @@ -477,7 +840,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "traefik.port is not an integer", - mesosTask: aTaskData("test", + mesosTask: aTaskData("test", "", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withLabel(label.TraefikPort, "TRAEFIK"), @@ -488,7 +851,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "traefik.port is not the same as discovery.port", - mesosTask: aTaskData("test", + mesosTask: aTaskData("test", "", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withLabel(label.TraefikPort, "443"), @@ -499,7 +862,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "traefik.port is the same as discovery.port", - mesosTask: aTaskData("test", + mesosTask: aTaskData("test", "", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withLabel(label.TraefikPort, "80"), @@ -510,7 +873,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "healthy nil", - mesosTask: aTaskData("test", + mesosTask: aTaskData("test", "", withStatus( withState("TASK_RUNNING"), ), @@ -523,7 +886,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "healthy false", - mesosTask: aTaskData("test", + mesosTask: aTaskData("test", "", withStatus( withState("TASK_RUNNING"), withHealthy(false), @@ -554,6 +917,172 @@ func TestTaskFilter(t *testing.T) { } } +func TestGetServerPort(t *testing.T) { + testCases := []struct { + desc string + task taskData + expected string + }{ + { + desc: "port missing", + task: aTaskData("", ""), + expected: "", + }, + { + desc: "numeric port", + task: aTaskData("", "", withLabel(label.TraefikPort, "80")), + expected: "80", + }, + { + desc: "string port", + task: aTaskData("", "", + withLabel(label.TraefikPort, "foobar"), + withInfo("", withPorts(withPort("TCP", 80, ""))), + ), + expected: "", + }, + { + desc: "negative port", + task: aTaskData("", "", + withLabel(label.TraefikPort, "-1"), + withInfo("", withPorts(withPort("TCP", 80, ""))), + ), + expected: "", + }, + { + desc: "task port available", + task: aTaskData("", "", + withInfo("", withPorts(withPort("TCP", 80, ""))), + ), + expected: "80", + }, + { + desc: "multiple task ports available", + task: aTaskData("", "", + withInfo("", withPorts( + withPort("TCP", 80, ""), + withPort("TCP", 443, ""), + )), + ), + expected: "80", + }, + { + desc: "numeric port index specified", + task: aTaskData("", "", + withLabel(label.TraefikPortIndex, "1"), + withInfo("", withPorts( + withPort("TCP", 80, ""), + withPort("TCP", 443, ""), + )), + ), + expected: "443", + }, + { + desc: "string port name specified", + task: aTaskData("", "", + withLabel(label.TraefikPortName, "https"), + withInfo("", withPorts( + withPort("TCP", 80, "http"), + withPort("TCP", 443, "https"), + )), + ), + expected: "443", + }, + { + desc: "string port index specified", + task: aTaskData("", "", + withLabel(label.TraefikPortIndex, "foobar"), + withInfo("", withPorts( + withPort("TCP", 80, ""), + )), + ), + expected: "80", + }, + { + desc: "port and port index specified", + task: aTaskData("", "", + withLabel(label.TraefikPort, "80"), + withLabel(label.TraefikPortIndex, "1"), + withInfo("", withPorts( + withPort("TCP", 80, ""), + withPort("TCP", 443, ""), + )), + ), + expected: "80", + }, + { + desc: "multiple task ports with service index available", + task: aTaskData("", "http", + withSegmentLabel(label.TraefikPortIndex, "0", "http"), + withInfo("", withPorts( + withPort("TCP", 80, ""), + withPort("TCP", 443, ""), + )), + ), + expected: "80", + }, + { + desc: "multiple task ports with service port available", + task: aTaskData("", "https", + withSegmentLabel(label.TraefikPort, "443", "https"), + withInfo("", withPorts( + withPort("TCP", 80, ""), + withPort("TCP", 443, ""), + )), + ), + expected: "443", + }, + { + desc: "multiple task ports with service port name available", + task: aTaskData("", "https", + withSegmentLabel(label.TraefikPortName, "b", "https"), + withInfo("", withPorts( + withPort("TCP", 80, "a"), + withPort("TCP", 443, "b"), + )), + ), + expected: "443", + }, + { + desc: "multiple task ports with segment matching port name", + task: aTaskData("", "b", + withInfo("", withPorts( + withPort("TCP", 80, "a"), + withPort("TCP", 443, "b"), + )), + ), + expected: "443", + }, + { + desc: "multiple task ports with services but default port available", + task: aTaskData("", "http", + withSegmentLabel(label.TraefikWeight, "100", "http"), + withInfo("", withPorts( + withPort("TCP", 80, ""), + withPort("TCP", 443, ""), + )), + ), + expected: "80", + }, + } + + p := &Provider{ + ExposedByDefault: true, + IPSources: "host", + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := p.getServerPort(test.task) + + assert.Equal(t, test.expected, actual) + }) + } +} + func TestGetSubDomain(t *testing.T) { providerGroups := &Provider{GroupsAsSubDomains: true} providerNoGroups := &Provider{GroupsAsSubDomains: false} @@ -597,13 +1126,13 @@ func TestGetServers(t *testing.T) { desc: "", tasks: []taskData{ // App 1 - aTaskData("ID1", + aTaskData("ID1", "", withIP("10.10.10.10"), withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), withStatus(withHealthy(true), withState("TASK_RUNNING")), ), - aTaskData("ID2", + aTaskData("ID2", "", withIP("10.10.10.11"), withLabel(label.TraefikWeight, "18"), withInfo("name1", @@ -611,14 +1140,14 @@ func TestGetServers(t *testing.T) { withStatus(withHealthy(true), withState("TASK_RUNNING")), ), // App 2 - aTaskData("ID3", + aTaskData("ID3", "", withLabel(label.TraefikWeight, "12"), withIP("20.10.10.10"), withInfo("name2", withPorts(withPort("TCP", 80, "WEB"))), withStatus(withHealthy(true), withState("TASK_RUNNING")), ), - aTaskData("ID4", + aTaskData("ID4", "", withLabel(label.TraefikWeight, "6"), withIP("20.10.10.11"), withInfo("name2", @@ -645,6 +1174,68 @@ func TestGetServers(t *testing.T) { }, }, }, + { + desc: "with segments matching port names", + tasks: segmentedTaskData([]string{"WEB1", "WEB2", "WEB3"}, + aTask("ID1", + withIP("10.10.10.10"), + withInfo("name1", + withPorts( + withPort("TCP", 81, "WEB1"), + withPort("TCP", 82, "WEB2"), + withPort("TCP", 83, "WEB3"), + )), + withStatus(withHealthy(true), withState("TASK_RUNNING")), + ), + ), + expected: map[string]types.Server{ + "server-ID1-service-WEB1": { + URL: "http://10.10.10.10:81", + Weight: label.DefaultWeight, + }, + "server-ID1-service-WEB2": { + URL: "http://10.10.10.10:82", + Weight: label.DefaultWeight, + }, + "server-ID1-service-WEB3": { + URL: "http://10.10.10.10:83", + Weight: label.DefaultWeight, + }, + }, + }, + { + desc: "with segments and portname labels", + tasks: segmentedTaskData([]string{"a", "b", "c"}, + aTask("ID1", + withIP("10.10.10.10"), + withInfo("name1", + withPorts( + withPort("TCP", 81, "WEB1"), + withPort("TCP", 82, "WEB2"), + withPort("TCP", 83, "WEB3"), + )), + withSegmentLabel(label.TraefikPortName, "WEB2", "a"), + withSegmentLabel(label.TraefikPortName, "WEB3", "b"), + withSegmentLabel(label.TraefikPortName, "WEB1", "c"), + withStatus(withHealthy(true), withState("TASK_RUNNING")), + ), + ), + + expected: map[string]types.Server{ + "server-ID1-service-a": { + URL: "http://10.10.10.10:82", + Weight: label.DefaultWeight, + }, + "server-ID1-service-b": { + URL: "http://10.10.10.10:83", + Weight: label.DefaultWeight, + }, + "server-ID1-service-c": { + URL: "http://10.10.10.10:81", + Weight: label.DefaultWeight, + }, + }, + }, } p := &Provider{ @@ -665,6 +1256,49 @@ func TestGetServers(t *testing.T) { } } +func TestGetBackendName(t *testing.T) { + testCases := []struct { + desc string + mesosTask taskData + expected string + }{ + { + desc: "label missing", + mesosTask: aTaskData("group-app-taskID", "", + withInfo("/group/app"), + ), + expected: "group-app", + }, + { + desc: "label existing", + mesosTask: aTaskData("", "", + withInfo(""), + withLabel(label.TraefikBackend, "bar"), + ), + expected: "bar", + }, + { + desc: "segment label existing", + mesosTask: aTaskData("", "app", + withInfo(""), + withSegmentLabel(label.TraefikBackend, "bar", "app"), + ), + expected: "bar", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getBackendName(test.mesosTask) + + assert.Equal(t, test.expected, actual) + }) + } +} + func TestGetFrontendRule(t *testing.T) { p := Provider{ Domain: "mesos.localhost", @@ -677,22 +1311,30 @@ func TestGetFrontendRule(t *testing.T) { }{ { desc: "label missing", - mesosTask: aTaskData("test", + mesosTask: aTaskData("test", "", withInfo("foo"), ), expected: "Host:foo.mesos.localhost", }, { desc: "label domain", - mesosTask: aTaskData("test", + mesosTask: aTaskData("test", "", withInfo("foo"), withLabel(label.TraefikDomain, "traefik.localhost"), ), expected: "Host:foo.traefik.localhost", }, + { + desc: "with segment", + mesosTask: aTaskData("test", "bar", + withInfo("foo"), + withLabel(label.TraefikDomain, "traefik.localhost"), + ), + expected: "Host:bar.foo.traefik.localhost", + }, { desc: "frontend rule available", - mesosTask: aTaskData("test", + mesosTask: aTaskData("test", "", withInfo("foo"), withLabel(label.TraefikFrontendRule, "Host:foo.bar"), ), diff --git a/provider/mesos/mesos_helper_test.go b/provider/mesos/mesos_helper_test.go index 0f30ab073..3dccae789 100644 --- a/provider/mesos/mesos_helper_test.go +++ b/provider/mesos/mesos_helper_test.go @@ -1,8 +1,10 @@ package mesos import ( + "strings" "testing" + "github.com/containous/traefik/provider/label" "github.com/mesosphere/mesos-dns/records/state" "github.com/stretchr/testify/assert" ) @@ -48,12 +50,29 @@ func TestBuilder(t *testing.T) { assert.Equal(t, expected, result) } -func aTaskData(id string, ops ...func(*state.Task)) taskData { +func aTaskData(id, segment string, ops ...func(*state.Task)) taskData { ts := &state.Task{ID: id} for _, op := range ops { op(ts) } - return taskData{Task: *ts, TraefikLabels: extractLabels(*ts)} + lbls := label.ExtractTraefikLabels(extractLabels(*ts)) + if len(lbls[segment]) > 0 { + return taskData{Task: *ts, TraefikLabels: lbls[segment], SegmentName: segment} + } + return taskData{Task: *ts, TraefikLabels: lbls[""], SegmentName: segment} +} + +func segmentedTaskData(segments []string, ts state.Task) []taskData { + td := []taskData{} + lbls := label.ExtractTraefikLabels(extractLabels(ts)) + for _, s := range segments { + if l, ok := lbls[s]; !ok { + td = append(td, taskData{Task: ts, TraefikLabels: lbls[""], SegmentName: s}) + } else { + td = append(td, taskData{Task: ts, TraefikLabels: l, SegmentName: s}) + } + } + return td } func aTask(id string, ops ...func(*state.Task)) state.Task { @@ -148,6 +167,18 @@ func withLabel(key, value string) func(*state.Task) { } } +func withSegmentLabel(key, value, segmentName string) func(*state.Task) { + if len(segmentName) == 0 { + panic("segmentName can not be empty") + } + + property := strings.TrimPrefix(key, label.Prefix) + return func(task *state.Task) { + lbl := state.Label{Key: label.Prefix + segmentName + "." + property, Value: value} + task.Labels = append(task.Labels, lbl) + } +} + func Bool(v bool) *bool { return &v }