Fix unsound behavior

The IP-Per-Task feature changed the behavior for
clients without this configuration (using the task IP instead
of task hostname). This patch make the new behavior available
just for Mesos installation with IP-Per-Task enabled. It also
make it possible to force the use of task's hostname.
This commit is contained in:
Diego de Oliveira 2017-03-26 16:59:08 -03:00 committed by Timo Reimann
parent 97a3564945
commit 592a12dca2
4 changed files with 137 additions and 26 deletions

View file

@ -969,6 +969,16 @@ domain = "marathon.localhost"
# Default: "10s" # Default: "10s"
# #
# keepAlive = "10s" # keepAlive = "10s"
# By default, a task's IP address (as returned by the Marathon API) is used as
# backend server if an IP-per-task configuration can be found; otherwise, the
# name of the host running the task is used.
# The latter behavior can be enforced by enabling this switch.
#
# Optional
# Default: false
#
# forceTaskHostname: false
``` ```
Labels can be used on containers to override default behaviour: Labels can be used on containers to override default behaviour:

View file

@ -42,6 +42,7 @@ type Provider struct {
TLS *provider.ClientTLS `description:"Enable Docker TLS support"` TLS *provider.ClientTLS `description:"Enable Docker TLS support"`
DialerTimeout flaeg.Duration `description:"Set a non-default connection timeout for Marathon"` DialerTimeout flaeg.Duration `description:"Set a non-default connection timeout for Marathon"`
KeepAlive flaeg.Duration `description:"Set a non-default TCP Keep Alive time in seconds"` KeepAlive flaeg.Duration `description:"Set a non-default TCP Keep Alive time in seconds"`
ForceTaskHostname bool `description:"Force to use the task IP hostname."`
Basic *Basic Basic *Basic
marathonClient marathon.Marathon marathonClient marathon.Marathon
} }
@ -526,11 +527,15 @@ func (p *Provider) getBackendServer(task marathon.Task, applications []marathon.
log.Errorf("Unable to get marathon application from task %s", task.AppID) log.Errorf("Unable to get marathon application from task %s", task.AppID)
return "" return ""
} }
if len(task.IPAddresses) == 0 { switch {
case application.IPAddressPerTask == nil || p.ForceTaskHostname:
return task.Host
case len(task.IPAddresses) == 0:
log.Errorf("Missing marathon IPAddress from task %s", task.AppID)
return "" return ""
} else if len(task.IPAddresses) == 1 { case len(task.IPAddresses) == 1:
return task.IPAddresses[0].IPAddress return task.IPAddresses[0].IPAddress
} else { default:
ipAddressIdxStr, ok := p.getLabel(application, "traefik.ipAddressIdx") ipAddressIdxStr, ok := p.getLabel(application, "traefik.ipAddressIdx")
if !ok { if !ok {
log.Errorf("Unable to get marathon IPAddress from task %s", task.AppID) log.Errorf("Unable to get marathon IPAddress from task %s", task.AppID)

View file

@ -1,10 +1,13 @@
package marathon package marathon
import ( import (
"encoding/json"
"errors" "errors"
"reflect" "reflect"
"testing" "testing"
"fmt"
"github.com/containous/traefik/mocks" "github.com/containous/traefik/mocks"
"github.com/containous/traefik/testhelpers" "github.com/containous/traefik/testhelpers"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
@ -108,7 +111,7 @@ func TestMarathonLoadConfig(t *testing.T) {
"backend-test": { "backend-test": {
Servers: map[string]types.Server{ Servers: map[string]types.Server{
"server-test": { "server-test": {
URL: "http://127.0.0.1:80", URL: "http://localhost:80",
Weight: 0, Weight: 0,
}, },
}, },
@ -161,7 +164,7 @@ func TestMarathonLoadConfig(t *testing.T) {
"backend-testLoadBalancerAndCircuitBreaker.dot": { "backend-testLoadBalancerAndCircuitBreaker.dot": {
Servers: map[string]types.Server{ Servers: map[string]types.Server{
"server-testLoadBalancerAndCircuitBreaker-dot": { "server-testLoadBalancerAndCircuitBreaker-dot": {
URL: "http://127.0.0.1:80", URL: "http://localhost:80",
Weight: 0, Weight: 0,
}, },
}, },
@ -219,7 +222,7 @@ func TestMarathonLoadConfig(t *testing.T) {
"backend-testMaxConn": { "backend-testMaxConn": {
Servers: map[string]types.Server{ Servers: map[string]types.Server{
"server-testMaxConn": { "server-testMaxConn": {
URL: "http://127.0.0.1:80", URL: "http://localhost:80",
Weight: 0, Weight: 0,
}, },
}, },
@ -274,7 +277,7 @@ func TestMarathonLoadConfig(t *testing.T) {
"backend-testMaxConnOnlySpecifyAmount": { "backend-testMaxConnOnlySpecifyAmount": {
Servers: map[string]types.Server{ Servers: map[string]types.Server{
"server-testMaxConnOnlySpecifyAmount": { "server-testMaxConnOnlySpecifyAmount": {
URL: "http://127.0.0.1:80", URL: "http://localhost:80",
Weight: 0, Weight: 0,
}, },
}, },
@ -326,7 +329,7 @@ func TestMarathonLoadConfig(t *testing.T) {
"backend-testMaxConnOnlyExtractorFunc": { "backend-testMaxConnOnlyExtractorFunc": {
Servers: map[string]types.Server{ Servers: map[string]types.Server{
"server-testMaxConnOnlyExtractorFunc": { "server-testMaxConnOnlyExtractorFunc": {
URL: "http://127.0.0.1:80", URL: "http://localhost:80",
Weight: 0, Weight: 0,
}, },
}, },
@ -337,27 +340,37 @@ func TestMarathonLoadConfig(t *testing.T) {
} }
for _, c := range cases { for _, c := range cases {
fakeClient := newFakeClient(c.applicationsError, c.applications, c.tasksError, c.tasks) appID := ""
provider := &Provider{ if len(c.applications.Apps) > 0 {
Domain: "docker.localhost", appID = c.applications.Apps[0].ID
ExposedByDefault: true,
marathonClient: fakeClient,
} }
actualConfig := provider.loadMarathonConfig() t.Run(fmt.Sprintf("Running case: %s", appID), func(t *testing.T) {
fakeClient.AssertExpectations(t) fakeClient := newFakeClient(c.applicationsError, c.applications, c.tasksError, c.tasks)
if c.expectedNil { provider := &Provider{
if actualConfig != nil { Domain: "docker.localhost",
t.Fatalf("Should have been nil, got %v", actualConfig) ExposedByDefault: true,
marathonClient: fakeClient,
} }
} else { actualConfig := provider.loadMarathonConfig()
// Compare backends fakeClient.AssertExpectations(t)
if !reflect.DeepEqual(actualConfig.Backends, c.expectedBackends) { if c.expectedNil {
t.Fatalf("expected %#v, got %#v", c.expectedBackends, actualConfig.Backends) if actualConfig != nil {
t.Fatalf("Should have been nil, got %v", actualConfig)
}
} else {
// Compare backends
if !reflect.DeepEqual(actualConfig.Backends, c.expectedBackends) {
expected, _ := json.Marshal(c.expectedBackends)
actual, _ := json.Marshal(actualConfig.Backends)
t.Fatalf("expected\t %s\n, \tgot %s\n", expected, actual)
}
if !reflect.DeepEqual(actualConfig.Frontends, c.expectedFrontends) {
expected, _ := json.Marshal(c.expectedFrontends)
actual, _ := json.Marshal(actualConfig.Frontends)
t.Fatalf("expected\t %s\n, got\t %s\n", expected, actual)
}
} }
if !reflect.DeepEqual(actualConfig.Frontends, c.expectedFrontends) { })
t.Fatalf("expected %#v, got %#v", c.expectedFrontends, actualConfig.Frontends)
}
}
} }
} }
@ -1451,3 +1464,76 @@ func TestMarathonGetSubDomain(t *testing.T) {
} }
} }
} }
func TestGetBackendServer(t *testing.T) {
applications := []struct {
forceTaskHostname bool
application marathon.Application
expected string
}{
{
application: marathon.Application{
ID: "app-without-IP-per-task",
},
expected: "sample.com",
},
{
application: marathon.Application{
ID: "app-with-IP-per-task",
IPAddressPerTask: &marathon.IPAddressPerTask{
Discovery: &marathon.Discovery{
Ports: &[]marathon.Port{
{
Number: 8880,
Name: "p1",
},
},
},
},
},
expected: "192.168.0.1",
}, {
forceTaskHostname: true,
application: marathon.Application{
ID: "app-with-hostname-enforced",
IPAddressPerTask: &marathon.IPAddressPerTask{
Discovery: &marathon.Discovery{
Ports: &[]marathon.Port{
{
Number: 8880,
Name: "p1",
},
},
},
},
},
expected: "sample.com",
},
}
for _, app := range applications {
t.Run(fmt.Sprintf("running %s", app.application.ID), func(t *testing.T) {
provider := &Provider{}
provider.ForceTaskHostname = app.forceTaskHostname
applications := []marathon.Application{app.application}
task := marathon.Task{
AppID: app.application.ID,
Host: "sample.com",
IPAddresses: []*marathon.IPAddress{
{
IPAddress: "192.168.0.1",
},
},
}
actual := provider.getBackendServer(task, applications)
if actual != app.expected {
t.Errorf("App %s, expected %q, got %q", task.AppID, app.expected, actual)
}
})
}
}

View file

@ -599,6 +599,16 @@
# #
# keepAlive = "10s" # keepAlive = "10s"
# By default, a task's IP address (as returned by the Marathon API) is used as
# backend server if an IP-per-task configuration can be found; otherwise, the
# name of the host running the task is used.
# The latter behavior can be enforced by enabling this switch.
#
# Optional
# Default: false
#
# forceTaskHostname: false
################################################################ ################################################################
# Mesos configuration backend # Mesos configuration backend
################################################################ ################################################################