package ecs import ( "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ecs" "github.com/containous/traefik/types" "github.com/stretchr/testify/assert" ) func makeEcsInstance(containerDef *ecs.ContainerDefinition) ecsInstance { container := &ecs.Container{ Name: containerDef.Name, NetworkBindings: make([]*ecs.NetworkBinding, len(containerDef.PortMappings)), } for i, pm := range containerDef.PortMappings { container.NetworkBindings[i] = &ecs.NetworkBinding{ HostPort: pm.HostPort, ContainerPort: pm.ContainerPort, Protocol: pm.Protocol, BindIP: aws.String("0.0.0.0"), } } return ecsInstance{ Name: "foo-http", ID: "123456789abc", task: &ecs.Task{ Containers: []*ecs.Container{container}, }, taskDefinition: &ecs.TaskDefinition{ ContainerDefinitions: []*ecs.ContainerDefinition{containerDef}, }, container: container, containerDefinition: containerDef, machine: &ec2.Instance{ PrivateIpAddress: aws.String("10.0.0.0"), State: &ec2.InstanceState{ Name: aws.String(ec2.InstanceStateNameRunning), }, }, } } func simpleEcsInstance(labels map[string]*string) ecsInstance { return makeEcsInstance(&ecs.ContainerDefinition{ Name: aws.String("http"), PortMappings: []*ecs.PortMapping{{ HostPort: aws.Int64(80), ContainerPort: aws.Int64(80), Protocol: aws.String("tcp"), }}, DockerLabels: labels, }) } func simpleEcsInstanceNoNetwork(labels map[string]*string) ecsInstance { return makeEcsInstance(&ecs.ContainerDefinition{ Name: aws.String("http"), PortMappings: []*ecs.PortMapping{}, DockerLabels: labels, }) } func TestEcsProtocol(t *testing.T) { tests := []struct { desc string expected string instanceInfo ecsInstance provider *Provider }{ { desc: "Protocol label is not set should return a string equals to http", expected: "http", instanceInfo: simpleEcsInstance(map[string]*string{}), provider: &Provider{}, }, { desc: "Protocol label is set to http should return a string equals to http", expected: "http", instanceInfo: simpleEcsInstance(map[string]*string{ types.LabelProtocol: aws.String("http"), }), provider: &Provider{}, }, { desc: "Protocol label is set to https should return a string equals to https", expected: "https", instanceInfo: simpleEcsInstance(map[string]*string{ types.LabelProtocol: aws.String("https"), }), provider: &Provider{}, }, } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() actual := test.provider.getProtocol(test.instanceInfo) assert.Equal(t, test.expected, actual) }) } } func TestEcsHost(t *testing.T) { tests := []struct { desc string expected string instanceInfo ecsInstance provider *Provider }{ { desc: "Default host should be 10.0.0.0", expected: "10.0.0.0", instanceInfo: simpleEcsInstance(map[string]*string{}), provider: &Provider{}, }, } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() actual := test.provider.getHost(test.instanceInfo) assert.Equal(t, test.expected, actual) }) } } func TestEcsPort(t *testing.T) { tests := []struct { desc string expected string instanceInfo ecsInstance provider *Provider }{ { desc: "Default port should be 80", expected: "80", instanceInfo: simpleEcsInstance(map[string]*string{}), provider: &Provider{}, }, } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() actual := test.provider.getPort(test.instanceInfo) assert.Equal(t, test.expected, actual) }) } } func TestEcsWeight(t *testing.T) { tests := []struct { desc string expected string instanceInfo ecsInstance provider *Provider }{ { desc: "Weight label not set should return a string equals to 0", expected: "0", instanceInfo: simpleEcsInstance(map[string]*string{}), provider: &Provider{}, }, { desc: "Weight label set 0 should return a string equals to 0", expected: "0", instanceInfo: simpleEcsInstance(map[string]*string{ types.LabelWeight: aws.String("0"), }), provider: &Provider{}, }, { desc: "Weight label set -1 should return a string equals to -1", expected: "-1", instanceInfo: simpleEcsInstance(map[string]*string{ types.LabelWeight: aws.String("-1"), }), provider: &Provider{}, }, { desc: "Weight label set 10 should return a string equals to 10", expected: "10", instanceInfo: simpleEcsInstance(map[string]*string{ types.LabelWeight: aws.String("10"), }), provider: &Provider{}, }, } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() actual := test.provider.getWeight(test.instanceInfo) assert.Equal(t, test.expected, actual) }) } } func TestEcsPassHostHeader(t *testing.T) { tests := []struct { desc string expected string instanceInfo ecsInstance provider *Provider }{ { desc: "Frontend pass host header label not set should return a string equals to true", expected: "true", instanceInfo: simpleEcsInstance(map[string]*string{}), provider: &Provider{}, }, { desc: "Frontend pass host header label set to false should return a string equals to false", expected: "false", instanceInfo: simpleEcsInstance(map[string]*string{ types.LabelFrontendPassHostHeader: aws.String("false"), }), provider: &Provider{}, }, { desc: "Frontend pass host header label set to true should return a string equals to true", expected: "true", instanceInfo: simpleEcsInstance(map[string]*string{ types.LabelFrontendPassHostHeader: aws.String("true"), }), provider: &Provider{}, }, } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() actual := test.provider.getPassHostHeader(test.instanceInfo) assert.Equal(t, test.expected, actual) }) } } func TestEcsPriority(t *testing.T) { tests := []struct { desc string expected string instanceInfo ecsInstance provider *Provider }{ { desc: "Frontend priority label not set should return a string equals to 0", expected: "0", instanceInfo: simpleEcsInstance(map[string]*string{}), provider: &Provider{}, }, { desc: "Frontend priority label set to 10 should return a string equals to 10", expected: "10", instanceInfo: simpleEcsInstance(map[string]*string{ types.LabelFrontendPriority: aws.String("10"), }), provider: &Provider{}, }, { desc: "Frontend priority label set to -1 should return a string equals to -1", expected: "-1", instanceInfo: simpleEcsInstance(map[string]*string{ types.LabelFrontendPriority: aws.String("-1"), }), provider: &Provider{}, }, } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() actual := test.provider.getPriority(test.instanceInfo) assert.Equal(t, test.expected, actual) }) } } func TestEcsEntryPoints(t *testing.T) { tests := []struct { desc string expected []string instanceInfo ecsInstance provider *Provider }{ { desc: "Frontend entrypoints label not set should return empty array", expected: []string{}, instanceInfo: simpleEcsInstance(map[string]*string{}), provider: &Provider{}, }, { desc: "Frontend entrypoints label set to http should return a string array of 1 element", expected: []string{"http"}, instanceInfo: simpleEcsInstance(map[string]*string{ types.LabelFrontendEntryPoints: aws.String("http"), }), provider: &Provider{}, }, { desc: "Frontend entrypoints label set to http,https should return a string array of 2 elements", expected: []string{"http", "https"}, instanceInfo: simpleEcsInstance(map[string]*string{ types.LabelFrontendEntryPoints: aws.String("http,https"), }), provider: &Provider{}, }, } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() actual := test.provider.getEntryPoints(test.instanceInfo) assert.Equal(t, test.expected, actual) }) } } func TestFilterInstance(t *testing.T) { nilPrivateIP := simpleEcsInstance(map[string]*string{}) nilPrivateIP.machine.PrivateIpAddress = nil nilMachine := simpleEcsInstance(map[string]*string{}) nilMachine.machine = nil nilMachineState := simpleEcsInstance(map[string]*string{}) nilMachineState.machine.State = nil nilMachineStateName := simpleEcsInstance(map[string]*string{}) nilMachineStateName.machine.State.Name = nil invalidMachineState := simpleEcsInstance(map[string]*string{}) invalidMachineState.machine.State.Name = aws.String(ec2.InstanceStateNameStopped) noNetwork := simpleEcsInstanceNoNetwork(map[string]*string{}) noNetworkWithLabel := simpleEcsInstanceNoNetwork(map[string]*string{ types.LabelPort: aws.String("80"), }) tests := []struct { desc string expected bool instanceInfo ecsInstance provider *Provider }{ { desc: "Instance without enable label and exposed by default enabled should be not filtered", expected: true, instanceInfo: simpleEcsInstance(map[string]*string{}), provider: &Provider{ ExposedByDefault: true, }, }, { desc: "Instance without enable label and exposed by default disabled should be filtered", expected: false, instanceInfo: simpleEcsInstance(map[string]*string{}), provider: &Provider{ ExposedByDefault: false, }, }, { desc: "Instance with enable label set to false and exposed by default enabled should be filtered", expected: false, instanceInfo: simpleEcsInstance(map[string]*string{ types.LabelEnable: aws.String("false"), }), provider: &Provider{ ExposedByDefault: true, }, }, { desc: "Instance with enable label set to true and exposed by default disabled should be not filtered", expected: true, instanceInfo: simpleEcsInstance(map[string]*string{ types.LabelEnable: aws.String("true"), }), provider: &Provider{ ExposedByDefault: false, }, }, { desc: "Instance with nil private ip and exposed by default enabled should be filtered", expected: false, instanceInfo: nilPrivateIP, provider: &Provider{ ExposedByDefault: true, }, }, { desc: "Instance with nil machine and exposed by default enabled should be filtered", expected: false, instanceInfo: nilMachine, provider: &Provider{ ExposedByDefault: true, }, }, { desc: "Instance with nil machine state and exposed by default enabled should be filtered", expected: false, instanceInfo: nilMachineState, provider: &Provider{ ExposedByDefault: true, }, }, { desc: "Instance with nil machine state name and exposed by default enabled should be filtered", expected: false, instanceInfo: nilMachineStateName, provider: &Provider{ ExposedByDefault: true, }, }, { desc: "Instance with invalid machine state and exposed by default enabled should be filtered", expected: false, instanceInfo: invalidMachineState, provider: &Provider{ ExposedByDefault: true, }, }, { desc: "Instance with no port mappings should be filtered", expected: false, instanceInfo: noNetwork, provider: &Provider{ ExposedByDefault: true, }, }, { desc: "Instance with no port mapping and with label should not be filtered", expected: true, instanceInfo: noNetworkWithLabel, provider: &Provider{ ExposedByDefault: true, }, }, } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() actual := test.provider.filterInstance(test.instanceInfo) assert.Equal(t, test.expected, actual) }) } } func TestTaskChunking(t *testing.T) { testval := "a" tests := []struct { desc string count int expectedLengths []int provider *Provider }{ { desc: "0 parameter should return nil", count: 0, expectedLengths: []int(nil), provider: &Provider{}, }, { desc: "1 parameter should return 1 array of 1 element", count: 1, expectedLengths: []int{1}, provider: &Provider{}, }, { desc: "99 parameters should return 1 array of 99 elements", count: 99, expectedLengths: []int{99}, provider: &Provider{}, }, { desc: "100 parameters should return 1 array of 100 elements", count: 100, expectedLengths: []int{100}, provider: &Provider{}, }, { desc: "101 parameters should return 1 array of 100 elements and 1 array of 1 element", count: 101, expectedLengths: []int{100, 1}, provider: &Provider{}, }, { desc: "199 parameters should return 1 array of 100 elements and 1 array of 99 elements", count: 199, expectedLengths: []int{100, 99}, provider: &Provider{}, }, { desc: "200 parameters should return 2 arrays of 100 elements each", count: 200, expectedLengths: []int{100, 100}, provider: &Provider{}, }, { desc: "201 parameters should return 2 arrays of 100 elements each and 1 array of 1 element", count: 201, expectedLengths: []int{100, 100, 1}, provider: &Provider{}, }, { desc: "555 parameters should return 5 arrays of 100 elements each and 1 array of 55 elements", count: 555, expectedLengths: []int{100, 100, 100, 100, 100, 55}, provider: &Provider{}, }, { desc: "1001 parameters should return 10 arrays of 100 elements each and 1 array of 1 element", count: 1001, expectedLengths: []int{100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 1}, provider: &Provider{}, }, } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { var tasks []*string for v := 0; v < test.count; v++ { tasks = append(tasks, &testval) } out := test.provider.chunkedTaskArns(tasks) var outCount []int for _, el := range out { outCount = append(outCount, len(el)) } assert.Equal(t, test.expectedLengths, outCount, "Chunking %d elements", test.count) }) } } func TestEcsGetBasicAuth(t *testing.T) { cases := []struct { desc string instance ecsInstance expected []string }{ { desc: "label missing", instance: simpleEcsInstance(map[string]*string{}), expected: []string{}, }, { desc: "label existing", instance: simpleEcsInstance(map[string]*string{ types.LabelFrontendAuthBasic: aws.String("user:password"), }), expected: []string{"user:password"}, }, } for _, test := range cases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() provider := &Provider{} actual := provider.getBasicAuth(test.instance) assert.Equal(t, test.expected, actual) }) } } func TestGenerateECSConfig(t *testing.T) { provider := &Provider{} tests := []struct { desc string services map[string][]ecsInstance exp *types.Configuration err error }{ { desc: "config parsed successfully", services: map[string][]ecsInstance{ "testing": { { Name: "instance-1", containerDefinition: &ecs.ContainerDefinition{ DockerLabels: map[string]*string{}, }, machine: &ec2.Instance{ PrivateIpAddress: func(s string) *string { return &s }("10.0.0.1"), }, container: &ecs.Container{ NetworkBindings: []*ecs.NetworkBinding{ { HostPort: func(i int64) *int64 { return &i }(1337), }, }, }, }, }, }, exp: &types.Configuration{ Backends: map[string]*types.Backend{ "backend-instance-1": { Servers: map[string]types.Server{ "server-instance-1": { URL: "http://10.0.0.1:1337", }, }, }, "backend-testing": { LoadBalancer: &types.LoadBalancer{ Method: "wrr", }, }, }, Frontends: map[string]*types.Frontend{ "frontend-testing": { EntryPoints: []string{}, Backend: "backend-testing", Routes: map[string]types.Route{ "route-frontend-testing": { Rule: "Host:instance-1.", }, }, PassHostHeader: true, BasicAuth: []string{}, }, }, }, }, { desc: "config parsed successfully with health check labels", services: map[string][]ecsInstance{ "testing": { { Name: "instance-1", containerDefinition: &ecs.ContainerDefinition{ DockerLabels: map[string]*string{ types.LabelBackendHealthcheckPath: func(s string) *string { return &s }("/health"), types.LabelBackendHealthcheckInterval: func(s string) *string { return &s }("1s"), }, }, machine: &ec2.Instance{ PrivateIpAddress: func(s string) *string { return &s }("10.0.0.1"), }, container: &ecs.Container{ NetworkBindings: []*ecs.NetworkBinding{ { HostPort: func(i int64) *int64 { return &i }(1337), }, }, }, }, }, }, exp: &types.Configuration{ Backends: map[string]*types.Backend{ "backend-instance-1": { Servers: map[string]types.Server{ "server-instance-1": { URL: "http://10.0.0.1:1337", }, }, }, "backend-testing": { LoadBalancer: &types.LoadBalancer{ Method: "wrr", }, HealthCheck: &types.HealthCheck{ Path: "/health", Interval: "1s", }, }, }, Frontends: map[string]*types.Frontend{ "frontend-testing": { EntryPoints: []string{}, Backend: "backend-testing", Routes: map[string]types.Route{ "route-frontend-testing": { Rule: "Host:instance-1.", }, }, PassHostHeader: true, BasicAuth: []string{}, }, }, }, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { got, err := provider.generateECSConfig(test.services) assert.Equal(t, test.err, err) assert.Equal(t, test.exp, got) }) } } func TestEcsWithoutPort(t *testing.T) { tests := []struct { desc string expected string instanceInfo ecsInstance provider *Provider }{ { desc: "Label should override network port", expected: "4242", instanceInfo: simpleEcsInstance(map[string]*string{ types.LabelPort: aws.String("4242"), }), provider: &Provider{}, }, { desc: "Label should provide exposed port", expected: "80", instanceInfo: simpleEcsInstanceNoNetwork(map[string]*string{ types.LabelPort: aws.String("80"), }), provider: &Provider{}, }, } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() actual := test.provider.getPort(test.instanceInfo) assert.Equal(t, test.expected, actual) }) } }