traefik/provider/marathon/marathon_test.go
Timo Reimann 779eeba650 [marathon] Use single API call to fetch Marathon resources.
Change Marathon provider to make just one API call instead of two per
configuration update by means of specifying embedded resources, which
enable retrieving multiple response types from the API at once. Apart
from the obvious savings in API calls, we primarily gain a consistent
view on both applications and tasks that allows us to drop a lot of
correlation logic.  Additionally, it will serve as the basis for the
introduction of readiness checks which require application/task
consistency for correct leverage on the proxy end.

Additional changes:

marathon.go:
- Filter on tasks now embedded inside the applications.
- Reduce/simplify signature on multiple template functions as we do not
  need to check for proper application/task correlation anymore.
- Remove getFrontendBackend in favor of just getBackend.
- Move filtering on enabled/exposed applications from `taskFilter` to
  `applicationFilter`. (The task filter just reached out to the
  applications anyway, so it never made sense to locate it with the
  tasks where the filter was called once for every task even though the
  result would never change.)
- Remove duplicate constraints filter in tasks, where it neither made
  sense to keep as it operates on the application level only.
- Add context to rendering error.

marathon_test.go:
- Simplify and reduce numerous tests.
- Convert tests with high number of cases into parallelized sub-tests.
- Improve readability/structure for several tests.
- Add missing test for enabled/exposed applications.
- Simplify the mocked Marathon server.

marathon.tmpl:
- Update application/task iteration.
- Replace `getFrontendBackend` by `getBackend`.
2017-07-11 14:35:01 +02:00

1463 lines
31 KiB
Go

package marathon
import (
"errors"
"fmt"
"reflect"
"testing"
"github.com/containous/traefik/provider/marathon/mocks"
"github.com/containous/traefik/testhelpers"
"github.com/containous/traefik/types"
"github.com/gambol99/go-marathon"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type fakeClient struct {
mocks.Marathon
}
func newFakeClient(applicationsError bool, applications marathon.Applications) *fakeClient {
// create an instance of our test object
fakeClient := new(fakeClient)
if applicationsError {
fakeClient.On("Applications", mock.Anything).Return(nil, errors.New("fake Marathon server error"))
} else {
fakeClient.On("Applications", mock.Anything).Return(&applications, nil)
}
return fakeClient
}
func TestMarathonLoadConfigAPIErrors(t *testing.T) {
fakeClient := newFakeClient(true, marathon.Applications{})
provider := &Provider{
marathonClient: fakeClient,
}
actualConfig := provider.loadMarathonConfig()
fakeClient.AssertExpectations(t)
if actualConfig != nil {
t.Errorf("configuration should have been nil, got %v", actualConfig)
}
}
func TestMarathonLoadConfigNonAPIErrors(t *testing.T) {
cases := []struct {
desc string
application marathon.Application
task marathon.Task
expectedFrontends map[string]*types.Frontend
expectedBackends map[string]*types.Backend
}{
{
desc: "simple application",
application: marathon.Application{
Ports: []int{80},
Labels: &map[string]string{},
},
task: marathon.Task{
Host: "localhost",
Ports: []int{80},
IPAddresses: []*marathon.IPAddress{
{
IPAddress: "127.0.0.1",
Protocol: "tcp",
},
},
},
expectedFrontends: map[string]*types.Frontend{
"frontend-app": {
Backend: "backend-app",
PassHostHeader: true,
BasicAuth: []string{},
EntryPoints: []string{},
Routes: map[string]types.Route{
"route-host-app": {
Rule: "Host:app.docker.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-app": {
Servers: map[string]types.Server{
"server-task": {
URL: "http://localhost:80",
Weight: 0,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "load balancer / circuit breaker labels",
application: marathon.Application{
Ports: []int{80},
Labels: &map[string]string{
types.LabelBackendLoadbalancerMethod: "drr",
types.LabelBackendCircuitbreakerExpression: "NetworkErrorRatio() > 0.5",
},
},
task: marathon.Task{
Host: "localhost",
Ports: []int{80},
IPAddresses: []*marathon.IPAddress{
{
IPAddress: "127.0.0.1",
Protocol: "tcp",
},
},
},
expectedFrontends: map[string]*types.Frontend{
"frontend-app": {
Backend: "backend-app",
PassHostHeader: true,
BasicAuth: []string{},
EntryPoints: []string{},
Routes: map[string]types.Route{
"route-host-app": {
Rule: "Host:app.docker.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-app": {
Servers: map[string]types.Server{
"server-task": {
URL: "http://localhost:80",
Weight: 0,
},
},
CircuitBreaker: &types.CircuitBreaker{
Expression: "NetworkErrorRatio() > 0.5",
},
LoadBalancer: &types.LoadBalancer{
Method: "drr",
},
},
},
},
{
desc: "general max connection labels",
application: marathon.Application{
Ports: []int{80},
Labels: &map[string]string{
types.LabelBackendMaxconnAmount: "1000",
types.LabelBackendMaxconnExtractorfunc: "client.ip",
},
},
task: marathon.Task{
Host: "localhost",
Ports: []int{80},
IPAddresses: []*marathon.IPAddress{
{
IPAddress: "127.0.0.1",
Protocol: "tcp",
},
},
},
expectedFrontends: map[string]*types.Frontend{
"frontend-app": {
Backend: "backend-app",
PassHostHeader: true,
BasicAuth: []string{},
EntryPoints: []string{},
Routes: map[string]types.Route{
"route-host-app": {
Rule: "Host:app.docker.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-app": {
Servers: map[string]types.Server{
"server-task": {
URL: "http://localhost:80",
Weight: 0,
},
},
MaxConn: &types.MaxConn{
Amount: 1000,
ExtractorFunc: "client.ip",
},
},
},
},
{
desc: "max connection amount label",
application: marathon.Application{
Ports: []int{80},
Labels: &map[string]string{
types.LabelBackendMaxconnAmount: "1000",
},
},
task: marathon.Task{
Host: "localhost",
Ports: []int{80},
IPAddresses: []*marathon.IPAddress{
{
IPAddress: "127.0.0.1",
Protocol: "tcp",
},
},
},
expectedFrontends: map[string]*types.Frontend{
"frontend-app": {
Backend: "backend-app",
PassHostHeader: true,
BasicAuth: []string{},
EntryPoints: []string{},
Routes: map[string]types.Route{
"route-host-app": {
Rule: "Host:app.docker.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-app": {
Servers: map[string]types.Server{
"server-task": {
URL: "http://localhost:80",
Weight: 0,
},
},
MaxConn: nil,
},
},
},
{
desc: "max connection extractor function label",
application: marathon.Application{
Ports: []int{80},
Labels: &map[string]string{
types.LabelBackendMaxconnExtractorfunc: "client.ip",
},
},
task: marathon.Task{
Host: "localhost",
Ports: []int{80},
IPAddresses: []*marathon.IPAddress{
{
IPAddress: "127.0.0.1",
Protocol: "tcp",
},
},
},
expectedFrontends: map[string]*types.Frontend{
"frontend-app": {
Backend: "backend-app",
PassHostHeader: true,
BasicAuth: []string{},
EntryPoints: []string{},
Routes: map[string]types.Route{
"route-host-app": {
Rule: "Host:app.docker.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-app": {
Servers: map[string]types.Server{
"server-task": {
URL: "http://localhost:80",
Weight: 0,
},
},
MaxConn: nil,
},
},
},
{
desc: "health check labels",
application: marathon.Application{
Ports: []int{80},
Labels: &map[string]string{
types.LabelBackendHealthcheckPath: "/path",
types.LabelBackendHealthcheckInterval: "5m",
},
},
task: marathon.Task{
Host: "127.0.0.1",
Ports: []int{80},
IPAddresses: []*marathon.IPAddress{
{
IPAddress: "127.0.0.1",
Protocol: "tcp",
},
},
},
expectedFrontends: map[string]*types.Frontend{
"frontend-app": {
Backend: "backend-app",
PassHostHeader: true,
BasicAuth: []string{},
EntryPoints: []string{},
Routes: map[string]types.Route{
"route-host-app": {
Rule: "Host:app.docker.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-app": {
Servers: map[string]types.Server{
"server-task": {
URL: "http://127.0.0.1:80",
Weight: 0,
},
},
HealthCheck: &types.HealthCheck{
Path: "/path",
Interval: "5m",
},
},
},
},
}
for _, c := range cases {
c := c
t.Run(c.desc, func(t *testing.T) {
t.Parallel()
c.application.ID = "/app"
c.task.ID = "task"
c.application.Tasks = []*marathon.Task{&c.task}
fakeClient := newFakeClient(false,
marathon.Applications{Apps: []marathon.Application{c.application}})
provider := &Provider{
Domain: "docker.localhost",
ExposedByDefault: true,
marathonClient: fakeClient,
}
actualConfig := provider.loadMarathonConfig()
fakeClient.AssertExpectations(t)
expectedConfig := &types.Configuration{
Backends: c.expectedBackends,
Frontends: c.expectedFrontends,
}
assert.Equal(t, expectedConfig, actualConfig)
})
}
}
func TestMarathonTaskFilter(t *testing.T) {
cases := []struct {
task marathon.Task
application marathon.Application
expected bool
exposedByDefault bool
}{
{
task: marathon.Task{
AppID: "missing-port",
Ports: []int{},
},
application: marathon.Application{
ID: "missing-port",
Labels: &map[string]string{},
},
expected: false,
exposedByDefault: true,
},
{
task: marathon.Task{
AppID: "task-not-running",
Ports: []int{80},
State: "TASK_STAGING",
},
application: marathon.Application{
ID: "task-not-running",
Ports: []int{80},
Labels: &map[string]string{},
},
expected: false,
exposedByDefault: true,
},
{
task: marathon.Task{
AppID: "existing-port",
Ports: []int{80},
},
application: marathon.Application{
ID: "existing-port",
Ports: []int{80},
Labels: &map[string]string{},
},
expected: true,
exposedByDefault: true,
},
{
task: marathon.Task{
AppID: "specify-both-port-index-and-number",
Ports: []int{80, 443},
},
application: marathon.Application{
ID: "specify-both-port-index-and-number",
Ports: []int{80, 443},
Labels: &map[string]string{
types.LabelPort: "443",
types.LabelPortIndex: "1",
},
},
expected: false,
exposedByDefault: true,
},
{
task: marathon.Task{
AppID: "healthcheck-available",
Ports: []int{80},
},
application: 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,
},
},
},
application: 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,
},
},
},
application: 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,
},
},
},
application: marathon.Application{
ID: "healthcheck-alive",
Ports: []int{80},
Labels: &map[string]string{},
HealthChecks: &[]marathon.HealthCheck{
*marathon.NewDefaultHealthCheck(),
},
},
expected: true,
exposedByDefault: true,
},
}
provider := &Provider{}
for i, c := range cases {
if c.task.State == "" {
c.task.State = taskStateRunning
}
actual := provider.taskFilter(c.task, c.application)
if actual != c.expected {
t.Fatalf("App %s (#%d): got %v, expected %v", c.task.AppID, i, actual, c.expected)
}
}
}
func TestMarathonApplicationFilterConstraints(t *testing.T) {
cases := []struct {
desc string
application marathon.Application
marathonLBCompatibility bool
expected bool
}{
{
desc: "tags missing",
application: marathon.Application{
ID: "app",
Labels: &map[string]string{},
},
marathonLBCompatibility: false,
expected: false,
},
{
desc: "tag matching",
application: marathon.Application{
ID: "app",
Labels: &map[string]string{
types.LabelTags: "valid",
},
},
marathonLBCompatibility: false,
expected: true,
},
{
desc: "LB compatibility tag matching",
application: marathon.Application{
ID: "app",
Labels: &map[string]string{
"HAPROXY_GROUP": "valid",
types.LabelTags: "notvalid",
},
},
marathonLBCompatibility: true,
expected: true,
},
}
for _, c := range cases {
c := c
t.Run(c.desc, func(t *testing.T) {
t.Parallel()
provider := &Provider{
ExposedByDefault: true,
MarathonLBCompatibility: c.marathonLBCompatibility,
}
constraint, err := types.NewConstraint("tag==valid")
if err != nil {
panic(fmt.Sprintf("failed to create constraint 'tag==valid': %s", err))
}
provider.Constraints = types.Constraints{constraint}
actual := provider.applicationFilter(c.application)
if actual != c.expected {
t.Fatalf("got %v, expected %v", actual, c.expected)
}
})
}
}
func TestMarathonApplicationFilterEnabled(t *testing.T) {
cases := []struct {
desc string
exposed bool
enabledLabel string
expected bool
}{
{
desc: "exposed",
exposed: true,
enabledLabel: "",
expected: true,
},
{
desc: "exposed and tolerated by valid label value",
exposed: true,
enabledLabel: "true",
expected: true,
},
{
desc: "exposed and tolerated by invalid label value",
exposed: true,
enabledLabel: "invalid",
expected: true,
},
{
desc: "exposed but overridden by label",
exposed: true,
enabledLabel: "false",
expected: false,
},
{
desc: "non-exposed",
exposed: false,
enabledLabel: "",
expected: false,
},
{
desc: "non-exposed but overridden by label",
exposed: false,
enabledLabel: "true",
expected: true,
},
}
for _, c := range cases {
c := c
t.Run(c.desc, func(t *testing.T) {
t.Parallel()
provider := &Provider{ExposedByDefault: c.exposed}
app := marathon.Application{
ID: "app",
Labels: &map[string]string{
types.LabelEnable: c.enabledLabel,
},
}
if provider.applicationFilter(app) != c.expected {
t.Errorf("got unexpected filtering = %t", !c.expected)
}
})
}
}
func TestMarathonGetPort(t *testing.T) {
provider := &Provider{}
cases := []struct {
desc string
application marathon.Application
task marathon.Task
expected string
}{
{
desc: "port missing",
application: marathon.Application{
ID: "app",
Labels: &map[string]string{},
},
task: marathon.Task{
AppID: "app",
Ports: []int{},
},
expected: "",
},
{
desc: "explicit port taken",
application: marathon.Application{
ID: "app",
Labels: &map[string]string{
types.LabelPort: "80",
},
},
task: marathon.Task{
AppID: "app",
Ports: []int{},
},
expected: "80",
},
{
desc: "illegal explicit port specified",
application: marathon.Application{
ID: "app",
Labels: &map[string]string{
types.LabelPort: "foobar",
},
},
task: marathon.Task{
AppID: "app",
Ports: []int{80},
},
expected: "",
},
{
desc: "illegal explicit port integer specified",
application: marathon.Application{
ID: "app",
Labels: &map[string]string{
types.LabelPort: "-1",
},
},
task: marathon.Task{
AppID: "app",
Ports: []int{80},
},
expected: "",
},
{
desc: "task port available",
application: 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",
application: 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",
application: 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",
application: marathon.Application{
ID: "app",
Labels: &map[string]string{},
},
task: marathon.Task{
AppID: "app",
Ports: []int{80, 443},
},
expected: "80",
},
{
desc: "indexed port taken",
application: marathon.Application{
ID: "app",
Labels: &map[string]string{
types.LabelPortIndex: "1",
},
},
task: marathon.Task{
AppID: "app",
Ports: []int{80, 443},
},
expected: "443",
},
{
desc: "illegal port index specified",
application: marathon.Application{
ID: "app",
Labels: &map[string]string{
types.LabelPortIndex: "foobar",
},
},
task: marathon.Task{
AppID: "app",
Ports: []int{80},
},
expected: "",
},
{
desc: "task port preferred over application port",
application: 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.application)
if actual != c.expected {
t.Errorf("got %q, want %q", c.expected, actual)
}
})
}
}
func TestMarathonGetWeight(t *testing.T) {
provider := &Provider{}
cases := []struct {
desc string
application marathon.Application
expected string
}{
{
desc: "weight label missing",
application: marathon.Application{
Labels: &map[string]string{},
},
expected: "0",
},
{
desc: "weight label existing",
application: marathon.Application{
Labels: &map[string]string{
types.LabelWeight: "10",
},
},
expected: "10",
},
}
for _, c := range cases {
c := c
t.Run(c.desc, func(t *testing.T) {
t.Parallel()
actual := provider.getWeight(c.application)
if actual != c.expected {
t.Fatalf("actual %s, expected %s", actual, c.expected)
}
})
}
}
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{
types.LabelDomain: "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{}
cases := []struct {
desc string
application marathon.Application
expected string
}{
{
desc: "protocol label missing",
application: marathon.Application{
Labels: &map[string]string{},
},
expected: "http",
},
{
desc: "protocol label existing",
application: marathon.Application{
Labels: &map[string]string{
types.LabelProtocol: "https",
},
},
expected: "https",
},
}
for _, c := range cases {
c := c
t.Run(c.desc, func(t *testing.T) {
t.Parallel()
actual := provider.getProtocol(c.application)
if actual != c.expected {
t.Errorf("got protocol '%s', want '%s'", actual, c.expected)
}
})
}
}
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{
types.LabelFrontendPassHostHeader: "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{
types.LabelFrontendEntryPoints: "http,https",
},
},
expected: []string{"http", "https"},
},
}
for _, a := range applications {
actual := provider.getEntryPoints(a.application)
if !reflect.DeepEqual(a.expected, actual) {
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{
types.LabelFrontendRule: "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{
types.LabelBackend: "bar",
},
},
expected: "bar",
},
}
for _, a := range applications {
actual := provider.getBackend(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(types.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(types.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(types.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 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",
},
},
},
}
}
// Populate task.
test.task.AppID = appID
test.task.Host = "host"
gotServer := provider.getBackendServer(test.task, test.application)
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)
}
})
}
}
func TestMarathonGetBasicAuth(t *testing.T) {
provider := &Provider{}
cases := []struct {
desc string
application marathon.Application
expected []string
}{
{
desc: "basic auth label is empty",
application: marathon.Application{
Labels: &map[string]string{}},
expected: []string{},
},
{
desc: "basic auth label is set with user:password",
application: marathon.Application{
Labels: &map[string]string{
types.LabelFrontendAuthBasic: "user:password",
},
},
expected: []string{"user:password"},
},
}
for _, c := range cases {
c := c
t.Run(c.desc, func(t *testing.T) {
t.Parallel()
actual := provider.getBasicAuth(c.application)
if !reflect.DeepEqual(c.expected, actual) {
t.Errorf("expected %q, got %q", c.expected, actual)
}
})
}
}