traefik/provider/marathon/marathon_test.go
Timo Reimann d57f83c31c Make Traefik health checks label-configurable with Marathon.
For the two existing health check parameters (path and interval), we add
support for Marathon labels.

Changes in detail:

- Extend the Marathon provider and template.
- Refactor Server.loadConfig to reduce duplication.
- Refactor the healthcheck package slightly to accommodate the changes
  and allow extending by future parameters.
- Update documentation.
2017-04-28 18:17:25 +02:00

1747 lines
37 KiB
Go

package marathon
import (
"errors"
"reflect"
"testing"
"fmt"
"github.com/containous/traefik/mocks"
"github.com/containous/traefik/testhelpers"
"github.com/containous/traefik/types"
"github.com/davecgh/go-spew/spew"
"github.com/gambol99/go-marathon"
"github.com/stretchr/testify/mock"
)
type fakeClient struct {
mocks.Marathon
}
func newFakeClient(applicationsError bool, applications *marathon.Applications, tasksError bool, tasks *marathon.Tasks) *fakeClient {
// create an instance of our test object
fakeClient := new(fakeClient)
if applicationsError {
fakeClient.On("Applications", mock.Anything).Return(nil, errors.New("error"))
} else {
fakeClient.On("Applications", mock.Anything).Return(applications, nil)
}
if !applicationsError {
if tasksError {
fakeClient.On("AllTasks", mock.Anything).Return(nil, errors.New("error"))
} else {
fakeClient.On("AllTasks", mock.Anything).Return(tasks, nil)
}
}
return fakeClient
}
func TestMarathonLoadConfig(t *testing.T) {
cases := []struct {
applicationsError bool
applications *marathon.Applications
tasksError bool
tasks *marathon.Tasks
expectedNil bool
expectedFrontends map[string]*types.Frontend
expectedBackends map[string]*types.Backend
}{
{
applications: &marathon.Applications{},
tasks: &marathon.Tasks{},
expectedFrontends: map[string]*types.Frontend{},
expectedBackends: map[string]*types.Backend{},
},
{
applicationsError: true,
applications: &marathon.Applications{},
tasks: &marathon.Tasks{},
expectedNil: true,
expectedFrontends: map[string]*types.Frontend{},
expectedBackends: map[string]*types.Backend{},
},
{
applications: &marathon.Applications{},
tasksError: true,
tasks: &marathon.Tasks{},
expectedNil: true,
expectedFrontends: map[string]*types.Frontend{},
expectedBackends: map[string]*types.Backend{},
},
{
applications: &marathon.Applications{
Apps: []marathon.Application{
{
ID: "/test",
Ports: []int{80},
Labels: &map[string]string{},
},
},
},
tasks: &marathon.Tasks{
Tasks: []marathon.Task{
{
ID: "test",
AppID: "/test",
Host: "localhost",
Ports: []int{80},
IPAddresses: []*marathon.IPAddress{
{
IPAddress: "127.0.0.1",
Protocol: "tcp",
},
},
},
},
},
expectedFrontends: map[string]*types.Frontend{
`frontend-test`: {
Backend: "backend-test",
PassHostHeader: true,
EntryPoints: []string{},
Routes: map[string]types.Route{
`route-host-test`: {
Rule: "Host:test.docker.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test": {
Servers: map[string]types.Server{
"server-test": {
URL: "http://localhost:80",
Weight: 0,
},
},
CircuitBreaker: nil,
},
},
},
{
applications: &marathon.Applications{
Apps: []marathon.Application{
{
ID: "/testLoadBalancerAndCircuitBreaker.dot",
Ports: []int{80},
Labels: &map[string]string{
"traefik.backend.loadbalancer.method": "drr",
"traefik.backend.circuitbreaker.expression": "NetworkErrorRatio() > 0.5",
},
},
},
},
tasks: &marathon.Tasks{
Tasks: []marathon.Task{
{
ID: "testLoadBalancerAndCircuitBreaker.dot",
AppID: "/testLoadBalancerAndCircuitBreaker.dot",
Host: "localhost",
Ports: []int{80},
IPAddresses: []*marathon.IPAddress{
{
IPAddress: "127.0.0.1",
Protocol: "tcp",
},
},
},
},
},
expectedFrontends: map[string]*types.Frontend{
`frontend-testLoadBalancerAndCircuitBreaker.dot`: {
Backend: "backend-testLoadBalancerAndCircuitBreaker.dot",
PassHostHeader: true,
EntryPoints: []string{},
Routes: map[string]types.Route{
`route-host-testLoadBalancerAndCircuitBreaker.dot`: {
Rule: "Host:testLoadBalancerAndCircuitBreaker.dot.docker.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-testLoadBalancerAndCircuitBreaker.dot": {
Servers: map[string]types.Server{
"server-testLoadBalancerAndCircuitBreaker-dot": {
URL: "http://localhost:80",
Weight: 0,
},
},
CircuitBreaker: &types.CircuitBreaker{
Expression: "NetworkErrorRatio() > 0.5",
},
LoadBalancer: &types.LoadBalancer{
Method: "drr",
},
},
},
},
{
applications: &marathon.Applications{
Apps: []marathon.Application{
{
ID: "/testMaxConn",
Ports: []int{80},
Labels: &map[string]string{
"traefik.backend.maxconn.amount": "1000",
"traefik.backend.maxconn.extractorfunc": "client.ip",
},
},
},
},
tasks: &marathon.Tasks{
Tasks: []marathon.Task{
{
ID: "testMaxConn",
AppID: "/testMaxConn",
Host: "localhost",
Ports: []int{80},
IPAddresses: []*marathon.IPAddress{
{
IPAddress: "127.0.0.1",
Protocol: "tcp",
},
},
},
},
},
expectedFrontends: map[string]*types.Frontend{
`frontend-testMaxConn`: {
Backend: "backend-testMaxConn",
PassHostHeader: true,
EntryPoints: []string{},
Routes: map[string]types.Route{
`route-host-testMaxConn`: {
Rule: "Host:testMaxConn.docker.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-testMaxConn": {
Servers: map[string]types.Server{
"server-testMaxConn": {
URL: "http://localhost:80",
Weight: 0,
},
},
MaxConn: &types.MaxConn{
Amount: 1000,
ExtractorFunc: "client.ip",
},
},
},
},
{
applications: &marathon.Applications{
Apps: []marathon.Application{
{
ID: "/testMaxConnOnlySpecifyAmount",
Ports: []int{80},
Labels: &map[string]string{
"traefik.backend.maxconn.amount": "1000",
},
},
},
},
tasks: &marathon.Tasks{
Tasks: []marathon.Task{
{
ID: "testMaxConnOnlySpecifyAmount",
AppID: "/testMaxConnOnlySpecifyAmount",
Host: "localhost",
Ports: []int{80},
IPAddresses: []*marathon.IPAddress{
{
IPAddress: "127.0.0.1",
Protocol: "tcp",
},
},
},
},
},
expectedFrontends: map[string]*types.Frontend{
`frontend-testMaxConnOnlySpecifyAmount`: {
Backend: "backend-testMaxConnOnlySpecifyAmount",
PassHostHeader: true,
EntryPoints: []string{},
Routes: map[string]types.Route{
`route-host-testMaxConnOnlySpecifyAmount`: {
Rule: "Host:testMaxConnOnlySpecifyAmount.docker.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-testMaxConnOnlySpecifyAmount": {
Servers: map[string]types.Server{
"server-testMaxConnOnlySpecifyAmount": {
URL: "http://localhost:80",
Weight: 0,
},
},
MaxConn: nil,
},
},
},
{
applications: &marathon.Applications{
Apps: []marathon.Application{
{
ID: "/testMaxConnOnlyExtractorFunc",
Ports: []int{80},
Labels: &map[string]string{
"traefik.backend.maxconn.extractorfunc": "client.ip",
},
},
},
},
tasks: &marathon.Tasks{
Tasks: []marathon.Task{
{
ID: "testMaxConnOnlyExtractorFunc",
AppID: "/testMaxConnOnlyExtractorFunc",
Host: "localhost",
Ports: []int{80},
IPAddresses: []*marathon.IPAddress{
{
IPAddress: "127.0.0.1",
Protocol: "tcp",
},
},
},
},
},
expectedFrontends: map[string]*types.Frontend{
`frontend-testMaxConnOnlyExtractorFunc`: {
Backend: "backend-testMaxConnOnlyExtractorFunc",
PassHostHeader: true,
EntryPoints: []string{},
Routes: map[string]types.Route{
`route-host-testMaxConnOnlyExtractorFunc`: {
Rule: "Host:testMaxConnOnlyExtractorFunc.docker.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-testMaxConnOnlyExtractorFunc": {
Servers: map[string]types.Server{
"server-testMaxConnOnlyExtractorFunc": {
URL: "http://localhost:80",
Weight: 0,
},
},
MaxConn: nil,
},
},
},
}
for _, c := range cases {
appID := ""
if len(c.applications.Apps) > 0 {
appID = c.applications.Apps[0].ID
}
t.Run(fmt.Sprintf("app ID: %s", appID), func(t *testing.T) {
t.Parallel()
fakeClient := newFakeClient(c.applicationsError, c.applications, c.tasksError, c.tasks)
provider := &Provider{
Domain: "docker.localhost",
ExposedByDefault: true,
marathonClient: fakeClient,
}
actualConfig := provider.loadMarathonConfig()
fakeClient.AssertExpectations(t)
if c.expectedNil {
if actualConfig != nil {
t.Fatalf("configuration should have been nil, got %v", actualConfig)
}
} else {
// Compare backends
if !reflect.DeepEqual(actualConfig.Backends, c.expectedBackends) {
t.Errorf("got backend\n%v\nwant\n\n%v", spew.Sdump(actualConfig.Backends), spew.Sdump(c.expectedBackends))
}
if !reflect.DeepEqual(actualConfig.Frontends, c.expectedFrontends) {
t.Errorf("got frontend\n%v\nwant\n\n%v", spew.Sdump(actualConfig.Frontends), spew.Sdump(c.expectedFrontends))
}
}
})
}
}
func TestMarathonTaskFilter(t *testing.T) {
cases := []struct {
task marathon.Task
applications *marathon.Applications
expected bool
exposedByDefault bool
}{
{
task: marathon.Task{},
applications: &marathon.Applications{},
expected: false,
exposedByDefault: true,
},
{
task: marathon.Task{
AppID: "test",
Ports: []int{80},
},
applications: &marathon.Applications{},
expected: false,
exposedByDefault: true,
},
{
task: marathon.Task{
AppID: "test",
Ports: []int{80},
},
applications: &marathon.Applications{
Apps: []marathon.Application{
{
ID: "foo",
Labels: &map[string]string{},
},
},
},
expected: false,
exposedByDefault: true,
},
{
task: marathon.Task{
AppID: "missing-port",
Ports: []int{},
},
applications: &marathon.Applications{
Apps: []marathon.Application{
{
ID: "missing-port",
Labels: &map[string]string{},
},
},
},
expected: false,
exposedByDefault: true,
},
{
task: marathon.Task{
AppID: "existing-port",
Ports: []int{80},
},
applications: &marathon.Applications{
Apps: []marathon.Application{
{
ID: "existing-port",
Ports: []int{80},
Labels: &map[string]string{},
},
},
},
expected: true,
exposedByDefault: true,
},
{
task: marathon.Task{
Ports: []int{80},
},
applications: &marathon.Applications{
Apps: []marathon.Application{
{
ID: "disable",
Ports: []int{80},
Labels: &map[string]string{
"traefik.enable": "false",
},
},
},
},
expected: false,
exposedByDefault: true,
},
{
task: marathon.Task{
AppID: "specify-both-port-index-and-number",
Ports: []int{80, 443},
},
applications: &marathon.Applications{
Apps: []marathon.Application{
{
ID: "specify-both-port-index-and-number",
Ports: []int{80, 443},
Labels: &map[string]string{
"traefik.port": "443",
"traefik.portIndex": "1",
},
},
},
},
expected: false,
exposedByDefault: true,
},
{
task: marathon.Task{
AppID: "healthcheck-available",
Ports: []int{80},
},
applications: &marathon.Applications{
Apps: []marathon.Application{
{
ID: "healthcheck-available",
Ports: []int{80},
Labels: &map[string]string{},
HealthChecks: &[]marathon.HealthCheck{
*marathon.NewDefaultHealthCheck(),
},
},
},
},
expected: true,
exposedByDefault: true,
},
{
task: marathon.Task{
AppID: "healthcheck-false",
Ports: []int{80},
HealthCheckResults: []*marathon.HealthCheckResult{
{
Alive: false,
},
},
},
applications: &marathon.Applications{
Apps: []marathon.Application{
{
ID: "healthcheck-false",
Ports: []int{80},
Labels: &map[string]string{},
HealthChecks: &[]marathon.HealthCheck{
*marathon.NewDefaultHealthCheck(),
},
},
},
},
expected: false,
exposedByDefault: true,
},
{
task: marathon.Task{
AppID: "healthcheck-mixed-results",
Ports: []int{80},
HealthCheckResults: []*marathon.HealthCheckResult{
{
Alive: true,
},
{
Alive: false,
},
},
},
applications: &marathon.Applications{
Apps: []marathon.Application{
{
ID: "healthcheck-mixed-results",
Ports: []int{80},
Labels: &map[string]string{},
HealthChecks: &[]marathon.HealthCheck{
*marathon.NewDefaultHealthCheck(),
},
},
},
},
expected: false,
exposedByDefault: true,
},
{
task: marathon.Task{
AppID: "healthcheck-alive",
Ports: []int{80},
HealthCheckResults: []*marathon.HealthCheckResult{
{
Alive: true,
},
},
},
applications: &marathon.Applications{
Apps: []marathon.Application{
{
ID: "healthcheck-alive",
Ports: []int{80},
Labels: &map[string]string{},
HealthChecks: &[]marathon.HealthCheck{
*marathon.NewDefaultHealthCheck(),
},
},
},
},
expected: true,
exposedByDefault: true,
},
{
task: marathon.Task{
AppID: "disable-default-expose",
Ports: []int{80},
},
applications: &marathon.Applications{
Apps: []marathon.Application{
{
ID: "disable-default-expose",
Ports: []int{80},
Labels: &map[string]string{},
},
},
},
expected: false,
exposedByDefault: false,
},
{
task: marathon.Task{
AppID: "disable-default-expose-disable-in-label",
Ports: []int{80},
},
applications: &marathon.Applications{
Apps: []marathon.Application{
{
ID: "disable-default-expose-disable-in-label",
Ports: []int{80},
Labels: &map[string]string{
"traefik.enable": "false",
},
},
},
},
expected: false,
exposedByDefault: false,
},
{
task: marathon.Task{
AppID: "disable-default-expose-enable-in-label",
Ports: []int{80},
},
applications: &marathon.Applications{
Apps: []marathon.Application{
{
ID: "disable-default-expose-enable-in-label",
Ports: []int{80},
Labels: &map[string]string{
"traefik.enable": "true",
},
},
},
},
expected: true,
exposedByDefault: false,
},
}
provider := &Provider{}
for _, c := range cases {
actual := provider.taskFilter(c.task, c.applications, c.exposedByDefault)
if actual != c.expected {
t.Fatalf("App %s: expected %v, got %v", c.task.AppID, c.expected, actual)
}
}
}
func TestMarathonAppConstraints(t *testing.T) {
cases := []struct {
application marathon.Application
filteredTasks []marathon.Task
expected bool
marathonLBCompatibility bool
}{
{
application: marathon.Application{
ID: "foo1",
Labels: &map[string]string{},
},
filteredTasks: []marathon.Task{
{
AppID: "foo1",
},
},
marathonLBCompatibility: false,
expected: false,
},
{
application: marathon.Application{
ID: "foo2",
Labels: &map[string]string{
"traefik.tags": "valid",
},
},
filteredTasks: []marathon.Task{
{
AppID: "foo2",
},
},
marathonLBCompatibility: false,
expected: true,
},
{
application: marathon.Application{
ID: "foo3",
Labels: &map[string]string{
"HAPROXY_GROUP": "valid",
"traefik.tags": "notvalid",
},
},
filteredTasks: []marathon.Task{
{
AppID: "foo3",
},
},
marathonLBCompatibility: true,
expected: true,
},
}
for _, c := range cases {
provider := &Provider{
MarathonLBCompatibility: c.marathonLBCompatibility,
}
constraint, _ := types.NewConstraint("tag==valid")
provider.Constraints = types.Constraints{constraint}
actual := provider.applicationFilter(c.application, c.filteredTasks)
if actual != c.expected {
t.Fatalf("expected %v, got %v: %v", c.expected, actual, c.application)
}
}
}
func TestMarathonTaskConstraints(t *testing.T) {
cases := []struct {
applications []marathon.Application
filteredTask marathon.Task
expected bool
marathonLBCompatibility bool
}{
{
applications: []marathon.Application{
{
ID: "bar1",
Labels: &map[string]string{},
}, {
ID: "foo1",
Labels: &map[string]string{
"traefik.tags": "other",
},
},
},
filteredTask: marathon.Task{
AppID: "foo1",
Ports: []int{80},
},
marathonLBCompatibility: false,
expected: false,
},
{
applications: []marathon.Application{
{
ID: "foo2",
Labels: &map[string]string{
"traefik.tags": "valid",
},
},
},
filteredTask: marathon.Task{
AppID: "foo2",
Ports: []int{80},
},
marathonLBCompatibility: false,
expected: true,
},
{
applications: []marathon.Application{
{
ID: "foo3",
Labels: &map[string]string{
"HAPROXY_GROUP": "valid",
"traefik.tags": "notvalid",
},
}, {
ID: "foo4",
Labels: &map[string]string{
"HAPROXY_GROUP": "notvalid",
"traefik.tags": "valid",
},
},
},
filteredTask: marathon.Task{
AppID: "foo3",
Ports: []int{80},
},
marathonLBCompatibility: true,
expected: true,
},
}
for _, c := range cases {
provider := &Provider{
MarathonLBCompatibility: c.marathonLBCompatibility,
}
constraint, _ := types.NewConstraint("tag==valid")
provider.Constraints = types.Constraints{constraint}
apps := new(marathon.Applications)
apps.Apps = c.applications
actual := provider.taskFilter(c.filteredTask, apps, true)
if actual != c.expected {
t.Fatalf("expected %v, got %v: %v", c.expected, actual, c.filteredTask)
}
}
}
func TestMarathonApplicationFilter(t *testing.T) {
cases := []struct {
application marathon.Application
filteredTasks []marathon.Task
expected bool
}{
{
application: marathon.Application{
Labels: &map[string]string{},
},
filteredTasks: []marathon.Task{},
expected: false,
},
{
application: marathon.Application{
ID: "test",
Labels: &map[string]string{},
},
filteredTasks: []marathon.Task{},
expected: false,
},
{
application: marathon.Application{
ID: "foo",
Labels: &map[string]string{},
},
filteredTasks: []marathon.Task{
{
AppID: "bar",
},
},
expected: false,
},
{
application: marathon.Application{
ID: "foo",
Labels: &map[string]string{},
},
filteredTasks: []marathon.Task{
{
AppID: "foo",
},
},
expected: true,
},
}
provider := &Provider{}
for _, c := range cases {
actual := provider.applicationFilter(c.application, c.filteredTasks)
if actual != c.expected {
t.Fatalf("expected %v, got %v", c.expected, actual)
}
}
}
func TestMarathonGetPort(t *testing.T) {
provider := &Provider{}
cases := []struct {
desc string
applications []marathon.Application
task marathon.Task
expected string
}{
{
desc: "no applications",
applications: []marathon.Application{},
task: marathon.Task{
AppID: "app",
Ports: []int{80},
},
expected: "",
},
{
desc: "application mismatch",
applications: []marathon.Application{
{
ID: "test1",
Labels: &map[string]string{},
},
},
task: marathon.Task{
AppID: "test2",
Ports: []int{80},
},
expected: "",
},
{
desc: "port missing",
applications: []marathon.Application{
{
ID: "app",
Labels: &map[string]string{},
},
},
task: marathon.Task{
AppID: "app",
Ports: []int{},
},
expected: "",
},
{
desc: "explicit port taken",
applications: []marathon.Application{
{
ID: "app",
Labels: &map[string]string{
"traefik.port": "80",
},
},
},
task: marathon.Task{
AppID: "app",
Ports: []int{},
},
expected: "80",
},
{
desc: "illegal explicit port specified",
applications: []marathon.Application{
{
ID: "app",
Labels: &map[string]string{
"traefik.port": "foobar",
},
},
},
task: marathon.Task{
AppID: "app",
Ports: []int{80},
},
expected: "",
},
{
desc: "illegal explicit port integer specified",
applications: []marathon.Application{
{
ID: "app",
Labels: &map[string]string{
"traefik.port": "-1",
},
},
},
task: marathon.Task{
AppID: "app",
Ports: []int{80},
},
expected: "",
},
{
desc: "task port available",
applications: []marathon.Application{
{
ID: "app",
Labels: &map[string]string{},
PortDefinitions: &[]marathon.PortDefinition{
{
Port: testhelpers.Intp(443),
},
},
IPAddressPerTask: &marathon.IPAddressPerTask{
Discovery: &marathon.Discovery{
Ports: &[]marathon.Port{
{
Number: 8000,
},
},
},
},
},
},
task: marathon.Task{
AppID: "app",
Ports: []int{80},
},
expected: "80",
},
{
desc: "port mapping port available",
applications: []marathon.Application{
{
ID: "app",
Labels: &map[string]string{},
PortDefinitions: &[]marathon.PortDefinition{
{
Port: testhelpers.Intp(443),
},
},
IPAddressPerTask: &marathon.IPAddressPerTask{
Discovery: &marathon.Discovery{
Ports: &[]marathon.Port{
{
Number: 8000,
},
},
},
},
},
},
task: marathon.Task{
AppID: "app",
Ports: []int{},
},
expected: "443",
},
{
desc: "IP-per-task port available",
applications: []marathon.Application{
{
ID: "app",
Labels: &map[string]string{},
IPAddressPerTask: &marathon.IPAddressPerTask{
Discovery: &marathon.Discovery{
Ports: &[]marathon.Port{
{
Number: 8000,
},
},
},
},
},
},
task: marathon.Task{
AppID: "app",
Ports: []int{},
},
expected: "8000",
},
{
desc: "first port taken from multiple ports",
applications: []marathon.Application{
{
ID: "app",
Labels: &map[string]string{},
},
},
task: marathon.Task{
AppID: "app",
Ports: []int{80, 443},
},
expected: "80",
},
{
desc: "indexed port taken",
applications: []marathon.Application{
{
ID: "app",
Labels: &map[string]string{
"traefik.portIndex": "1",
},
},
},
task: marathon.Task{
AppID: "app",
Ports: []int{80, 443},
},
expected: "443",
},
{
desc: "illegal port index specified",
applications: []marathon.Application{
{
ID: "app",
Labels: &map[string]string{
"traefik.portIndex": "foobar",
},
},
},
task: marathon.Task{
AppID: "app",
Ports: []int{80},
},
expected: "",
},
{
desc: "task port preferred over application port",
applications: []marathon.Application{
{
ID: "app",
Ports: []int{9999},
Labels: &map[string]string{},
},
},
task: marathon.Task{
AppID: "app",
Ports: []int{7777},
},
expected: "7777",
},
}
for _, c := range cases {
c := c
t.Run(c.desc, func(t *testing.T) {
t.Parallel()
actual := provider.getPort(c.task, c.applications)
if actual != c.expected {
t.Errorf("got %q, want %q", c.expected, actual)
}
})
}
}
func TestMarathonGetWeight(t *testing.T) {
provider := &Provider{}
applications := []struct {
applications []marathon.Application
task marathon.Task
expected string
}{
{
applications: []marathon.Application{},
task: marathon.Task{},
expected: "0",
},
{
applications: []marathon.Application{
{
ID: "test1",
Labels: &map[string]string{
"traefik.weight": "10",
},
},
},
task: marathon.Task{
AppID: "test2",
},
expected: "0",
},
{
applications: []marathon.Application{
{
ID: "test",
Labels: &map[string]string{
"traefik.test": "10",
},
},
},
task: marathon.Task{
AppID: "test",
},
expected: "0",
},
{
applications: []marathon.Application{
{
ID: "test",
Labels: &map[string]string{
"traefik.weight": "10",
},
},
},
task: marathon.Task{
AppID: "test",
},
expected: "10",
},
}
for _, a := range applications {
actual := provider.getWeight(a.task, a.applications)
if actual != a.expected {
t.Fatalf("expected %q, got %q", a.expected, actual)
}
}
}
func TestMarathonGetDomain(t *testing.T) {
provider := &Provider{
Domain: "docker.localhost",
}
applications := []struct {
application marathon.Application
expected string
}{
{
application: marathon.Application{
Labels: &map[string]string{}},
expected: "docker.localhost",
},
{
application: marathon.Application{
Labels: &map[string]string{
"traefik.domain": "foo.bar",
},
},
expected: "foo.bar",
},
}
for _, a := range applications {
actual := provider.getDomain(a.application)
if actual != a.expected {
t.Fatalf("expected %q, got %q", a.expected, actual)
}
}
}
func TestMarathonGetProtocol(t *testing.T) {
provider := &Provider{}
applications := []struct {
applications []marathon.Application
task marathon.Task
expected string
}{
{
applications: []marathon.Application{},
task: marathon.Task{},
expected: "http",
},
{
applications: []marathon.Application{
{
ID: "test1",
Labels: &map[string]string{
"traefik.protocol": "https",
},
},
},
task: marathon.Task{
AppID: "test2",
},
expected: "http",
},
{
applications: []marathon.Application{
{
ID: "test",
Labels: &map[string]string{
"traefik.foo": "bar",
},
},
},
task: marathon.Task{
AppID: "test",
},
expected: "http",
},
{
applications: []marathon.Application{
{
ID: "test",
Labels: &map[string]string{
"traefik.protocol": "https",
},
},
},
task: marathon.Task{
AppID: "test",
},
expected: "https",
},
}
for _, a := range applications {
actual := provider.getProtocol(a.task, a.applications)
if actual != a.expected {
t.Fatalf("expected %q, got %q", a.expected, actual)
}
}
}
func TestMarathonGetPassHostHeader(t *testing.T) {
provider := &Provider{}
applications := []struct {
application marathon.Application
expected string
}{
{
application: marathon.Application{
Labels: &map[string]string{}},
expected: "true",
},
{
application: marathon.Application{
Labels: &map[string]string{
"traefik.frontend.passHostHeader": "false",
},
},
expected: "false",
},
}
for _, a := range applications {
actual := provider.getPassHostHeader(a.application)
if actual != a.expected {
t.Fatalf("expected %q, got %q", a.expected, actual)
}
}
}
func TestMarathonGetEntryPoints(t *testing.T) {
provider := &Provider{}
applications := []struct {
application marathon.Application
expected []string
}{
{
application: marathon.Application{
Labels: &map[string]string{}},
expected: []string{},
},
{
application: marathon.Application{
Labels: &map[string]string{
"traefik.frontend.entryPoints": "http,https",
},
},
expected: []string{"http", "https"},
},
}
for _, a := range applications {
actual := provider.getEntryPoints(a.application)
if !reflect.DeepEqual(actual, a.expected) {
t.Fatalf("expected %#v, got %#v", a.expected, actual)
}
}
}
func TestMarathonGetFrontendRule(t *testing.T) {
applications := []struct {
application marathon.Application
expected string
marathonLBCompatibility bool
}{
{
application: marathon.Application{
Labels: &map[string]string{}},
marathonLBCompatibility: true,
expected: "Host:.docker.localhost",
},
{
application: marathon.Application{
ID: "test",
Labels: &map[string]string{
"HAPROXY_0_VHOST": "foo.bar",
},
},
marathonLBCompatibility: false,
expected: "Host:test.docker.localhost",
},
{
application: marathon.Application{
Labels: &map[string]string{
"traefik.frontend.rule": "Host:foo.bar",
"HAPROXY_0_VHOST": "notvalid",
},
},
marathonLBCompatibility: true,
expected: "Host:foo.bar",
},
{
application: marathon.Application{
Labels: &map[string]string{
"HAPROXY_0_VHOST": "foo.bar",
},
},
marathonLBCompatibility: true,
expected: "Host:foo.bar",
},
}
for _, a := range applications {
provider := &Provider{
Domain: "docker.localhost",
MarathonLBCompatibility: a.marathonLBCompatibility,
}
actual := provider.getFrontendRule(a.application)
if actual != a.expected {
t.Fatalf("expected %q, got %q", a.expected, actual)
}
}
}
func TestMarathonGetBackend(t *testing.T) {
provider := &Provider{}
applications := []struct {
application marathon.Application
expected string
}{
{
application: marathon.Application{
ID: "foo",
Labels: &map[string]string{
"traefik.backend": "bar",
},
},
expected: "bar",
},
}
for _, a := range applications {
actual := provider.getFrontendBackend(a.application)
if actual != a.expected {
t.Fatalf("expected %q, got %q", a.expected, actual)
}
}
}
func TestMarathonGetSubDomain(t *testing.T) {
providerGroups := &Provider{GroupsAsSubDomains: true}
providerNoGroups := &Provider{GroupsAsSubDomains: false}
apps := []struct {
path string
expected string
provider *Provider
}{
{"/test", "test", providerNoGroups},
{"/test", "test", providerGroups},
{"/a/b/c/d", "d.c.b.a", providerGroups},
{"/b/a/d/c", "c.d.a.b", providerGroups},
{"/d/c/b/a", "a.b.c.d", providerGroups},
{"/c/d/a/b", "b.a.d.c", providerGroups},
{"/a/b/c/d", "a-b-c-d", providerNoGroups},
{"/b/a/d/c", "b-a-d-c", providerNoGroups},
{"/d/c/b/a", "d-c-b-a", providerNoGroups},
{"/c/d/a/b", "c-d-a-b", providerNoGroups},
}
for _, a := range apps {
actual := a.provider.getSubDomain(a.path)
if actual != a.expected {
t.Errorf("expected %q, got %q", a.expected, actual)
}
}
}
func TestMarathonHasHealthCheckLabels(t *testing.T) {
tests := []struct {
desc string
value *string
want bool
}{
{
desc: "label missing",
value: nil,
want: false,
},
{
desc: "empty path",
value: stringp(""),
want: false,
},
{
desc: "non-empty path",
value: stringp("/path"),
want: true,
},
}
for _, test := range tests {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
app := marathon.Application{
Labels: &map[string]string{},
}
if test.value != nil {
app.AddLabel(labelBackendHealthCheckPath, *test.value)
}
prov := &Provider{}
got := prov.hasHealthCheckLabels(app)
if got != test.want {
t.Errorf("got %t, want %t", got, test.want)
}
})
}
}
func TestMarathonGetHealthCheckPath(t *testing.T) {
tests := []struct {
desc string
value *string
want string
}{
{
desc: "label missing",
value: nil,
want: "",
},
{
desc: "path existing",
value: stringp("/path"),
want: "/path",
},
}
for _, test := range tests {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
app := marathon.Application{}
app.EmptyLabels()
if test.value != nil {
app.AddLabel(labelBackendHealthCheckPath, *test.value)
}
prov := &Provider{}
got := prov.getHealthCheckPath(app)
if got != test.want {
t.Errorf("got %s, want %s", got, test.want)
}
})
}
}
func TestMarathonGetHealthCheckInterval(t *testing.T) {
tests := []struct {
desc string
value *string
want string
}{
{
desc: "label missing",
value: nil,
want: "",
},
{
desc: "interval existing",
value: stringp("5m"),
want: "5m",
},
}
for _, test := range tests {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
app := marathon.Application{
Labels: &map[string]string{},
}
if test.value != nil {
app.AddLabel(labelBackendHealthCheckInterval, *test.value)
}
prov := &Provider{}
got := prov.getHealthCheckInterval(app)
if got != test.want {
t.Errorf("got %s, want %s", got, test.want)
}
})
}
}
func stringp(s string) *string {
return &s
}
func TestGetBackendServer(t *testing.T) {
appID := "appId"
host := "host"
tests := []struct {
desc string
application marathon.Application
addIPAddrPerTask bool
task marathon.Task
forceTaskHostname bool
wantServer string
}{
{
desc: "application missing",
application: marathon.Application{ID: "other"},
wantServer: "",
},
{
desc: "application without IP-per-task",
wantServer: host,
},
{
desc: "task hostname override",
addIPAddrPerTask: true,
forceTaskHostname: true,
wantServer: host,
},
{
desc: "task IP address missing",
task: marathon.Task{
IPAddresses: []*marathon.IPAddress{},
},
addIPAddrPerTask: true,
wantServer: "",
},
{
desc: "single task IP address",
task: marathon.Task{
IPAddresses: []*marathon.IPAddress{
{
IPAddress: "1.1.1.1",
},
},
},
addIPAddrPerTask: true,
wantServer: "1.1.1.1",
},
{
desc: "multiple task IP addresses without index label",
task: marathon.Task{
IPAddresses: []*marathon.IPAddress{
{
IPAddress: "1.1.1.1",
},
{
IPAddress: "2.2.2.2",
},
},
},
addIPAddrPerTask: true,
wantServer: "",
},
{
desc: "multiple task IP addresses with invalid index label",
application: marathon.Application{
Labels: &map[string]string{"traefik.ipAddressIdx": "invalid"},
},
task: marathon.Task{
IPAddresses: []*marathon.IPAddress{
{
IPAddress: "1.1.1.1",
},
{
IPAddress: "2.2.2.2",
},
},
},
addIPAddrPerTask: true,
wantServer: "",
},
{
desc: "multiple task IP addresses with valid index label",
application: marathon.Application{
Labels: &map[string]string{"traefik.ipAddressIdx": "1"},
},
task: marathon.Task{
IPAddresses: []*marathon.IPAddress{
{
IPAddress: "1.1.1.1",
},
{
IPAddress: "2.2.2.2",
},
},
},
addIPAddrPerTask: true,
wantServer: "2.2.2.2",
},
}
for _, test := range tests {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
provider := &Provider{ForceTaskHostname: test.forceTaskHostname}
// Populate application.
if test.application.ID == "" {
test.application.ID = appID
}
if test.application.Labels == nil {
test.application.Labels = &map[string]string{}
}
if test.addIPAddrPerTask {
test.application.IPAddressPerTask = &marathon.IPAddressPerTask{
Discovery: &marathon.Discovery{
Ports: &[]marathon.Port{
{
Number: 8000,
Name: "port",
},
},
},
}
}
applications := []marathon.Application{test.application}
// Populate task.
test.task.AppID = appID
test.task.Host = "host"
gotServer := provider.getBackendServer(test.task, applications)
if gotServer != test.wantServer {
t.Errorf("got server '%s', want '%s'", gotServer, test.wantServer)
}
})
}
}
func TestParseIndex(t *testing.T) {
tests := []struct {
idxStr string
length int
shouldSucceed bool
parsed int
}{
{
idxStr: "illegal",
length: 42,
shouldSucceed: false,
},
{
idxStr: "-1",
length: 42,
shouldSucceed: false,
},
{
idxStr: "10",
length: 1,
shouldSucceed: false,
},
{
idxStr: "10",
length: 10,
shouldSucceed: false,
},
{
idxStr: "0",
length: 1,
shouldSucceed: true,
parsed: 0,
},
{
idxStr: "10",
length: 11,
shouldSucceed: true,
parsed: 10,
},
}
for _, test := range tests {
test := test
t.Run(fmt.Sprintf("parseIndex(%s, %d)", test.idxStr, test.length), func(t *testing.T) {
t.Parallel()
parsed, err := parseIndex(test.idxStr, test.length)
if test.shouldSucceed != (err == nil) {
t.Fatalf("got error '%s', want error: %t", err, !test.shouldSucceed)
}
if test.shouldSucceed && parsed != test.parsed {
t.Errorf("got parsed index %d, want %d", parsed, test.parsed)
}
})
}
}