From c705d6f9b34358ac3734c2ccd67a97ec38745e40 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Sat, 2 Dec 2017 19:30:16 +0100 Subject: [PATCH] refactor(ecs): rewrite configuration system. --- autogen/gentemplates/gen.go | 2 +- provider/ecs/config.go | 170 +++ provider/ecs/{ecs_test.go => config_test.go} | 1143 +++++++----------- provider/ecs/ecs.go | 182 +-- templates/ecs.tmpl | 2 +- 5 files changed, 642 insertions(+), 857 deletions(-) create mode 100644 provider/ecs/config.go rename provider/ecs/{ecs_test.go => config_test.go} (53%) diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index 21ecd6d49..dd7db4c23 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -305,7 +305,7 @@ func templatesDockerTmpl() (*asset, error) { var _templatesEcsTmpl = []byte(`[backends]{{range $serviceName, $instances := .Services}} [backends.backend-{{ $serviceName }}.loadbalancer] method = "{{ getLoadBalancerMethod $instances}}" - sticky = {{ getLoadBalancerSticky $instances}} + sticky = {{ getSticky $instances}} {{if hasStickinessLabel $instances}} [backends.backend-{{ $serviceName }}.loadbalancer.stickiness] cookieName = "{{getStickinessCookieName $instances}}" diff --git a/provider/ecs/config.go b/provider/ecs/config.go new file mode 100644 index 000000000..07118d0a9 --- /dev/null +++ b/provider/ecs/config.go @@ -0,0 +1,170 @@ +package ecs + +import ( + "strconv" + "strings" + "text/template" + + "github.com/BurntSushi/ty/fun" + "github.com/containous/traefik/log" + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" +) + +// buildConfiguration fills the config template with the given instances +func (p *Provider) buildConfiguration(services map[string][]ecsInstance) (*types.Configuration, error) { + var ecsFuncMap = template.FuncMap{ + "filterFrontends": filterFrontends, + "getFrontendRule": p.getFrontendRule, + "getBasicAuth": getFuncSliceString(label.TraefikFrontendAuthBasic), + "getLoadBalancerMethod": getFuncFirstStringValue(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), + "getSticky": getSticky, + "hasStickinessLabel": getFuncFirstBoolValue(label.TraefikBackendLoadBalancerStickiness, false), + "getStickinessCookieName": getFuncFirstStringValue(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName), + "getProtocol": getFuncStringValue(label.TraefikProtocol, label.DefaultProtocol), + "getHost": getHost, + "getPort": getPort, + "getWeight": getFuncStringValue(label.TraefikWeight, label.DefaultWeight), + "getPassHostHeader": getFuncStringValue(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), + "getPriority": getFuncStringValue(label.TraefikFrontendPriority, label.DefaultFrontendPriority), + "getEntryPoints": getFuncSliceString(label.TraefikFrontendEntryPoints), + "hasHealthCheckLabels": hasFuncFirst(label.TraefikBackendHealthCheckPath), + "getHealthCheckPath": getFuncFirstStringValue(label.TraefikBackendHealthCheckPath, ""), + "getHealthCheckInterval": getFuncFirstStringValue(label.TraefikBackendHealthCheckInterval, ""), + } + return p.GetConfiguration("templates/ecs.tmpl", ecsFuncMap, struct { + Services map[string][]ecsInstance + }{ + services, + }) +} + +func (p *Provider) getFrontendRule(i ecsInstance) string { + defaultRule := "Host:" + strings.ToLower(strings.Replace(i.Name, "_", "-", -1)) + "." + p.Domain + return getStringValue(i, label.TraefikFrontendRule, defaultRule) +} + +// TODO: Deprecated +// Deprecated replaced by Stickiness +func getSticky(instances []ecsInstance) string { + if hasFirst(instances, label.TraefikBackendLoadBalancerSticky) { + log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) + } + return getFirstStringValue(instances, label.TraefikBackendLoadBalancerSticky, "false") +} + +func getHost(i ecsInstance) string { + return *i.machine.PrivateIpAddress +} + +func getPort(i ecsInstance) string { + if value := getStringValue(i, label.TraefikPort, ""); len(value) > 0 { + return value + } + return strconv.FormatInt(*i.container.NetworkBindings[0].HostPort, 10) +} + +func filterFrontends(instances []ecsInstance) []ecsInstance { + byName := make(map[string]struct{}) + + return fun.Filter(func(i ecsInstance) bool { + _, found := byName[i.Name] + if !found { + byName[i.Name] = struct{}{} + } + return !found + }, instances).([]ecsInstance) +} + +// Label functions + +func getFuncStringValue(labelName string, defaultValue string) func(i ecsInstance) string { + return func(i ecsInstance) string { + return getStringValue(i, labelName, defaultValue) + } +} + +func getFuncSliceString(labelName string) func(i ecsInstance) []string { + return func(i ecsInstance) []string { + return getSliceString(i, labelName) + } +} + +func hasFuncFirst(labelName string) func(instances []ecsInstance) bool { + return func(instances []ecsInstance) bool { + return hasFirst(instances, labelName) + } +} + +func getFuncFirstStringValue(labelName string, defaultValue string) func(instances []ecsInstance) string { + return func(instances []ecsInstance) string { + return getFirstStringValue(instances, labelName, defaultValue) + } +} + +func getFuncFirstBoolValue(labelName string, defaultValue bool) func(instances []ecsInstance) bool { + return func(instances []ecsInstance) bool { + if len(instances) < 0 { + return defaultValue + } + return getBoolValue(instances[0], labelName, defaultValue) + } +} + +func getStringValue(i ecsInstance, labelName string, defaultValue string) string { + if v, ok := i.containerDefinition.DockerLabels[labelName]; ok { + if v == nil { + return defaultValue + } + if len(*v) == 0 { + return defaultValue + } + return *v + } + return defaultValue +} + +func getBoolValue(i ecsInstance, labelName string, defaultValue bool) bool { + rawValue, ok := i.containerDefinition.DockerLabels[labelName] + if ok { + if rawValue != nil { + v, err := strconv.ParseBool(*rawValue) + if err == nil { + return v + } + } + } + return defaultValue +} + +func getSliceString(i ecsInstance, labelName string) []string { + if value, ok := i.containerDefinition.DockerLabels[labelName]; ok { + if value == nil { + return nil + } + if len(*value) == 0 { + return nil + } + return label.SplitAndTrimString(*value, ",") + } + return nil +} + +func hasFirst(instances []ecsInstance, labelName string) bool { + if len(instances) > 0 { + v, ok := instances[0].containerDefinition.DockerLabels[labelName] + return ok && v != nil && len(*v) != 0 + } + return false +} + +func getFirstStringValue(instances []ecsInstance, labelName string, defaultValue string) string { + if len(instances) == 0 { + return defaultValue + } + return getStringValue(instances[0], labelName, defaultValue) +} + +func isEnabled(i ecsInstance, exposedByDefault bool) bool { + return getBoolValue(i, label.TraefikEnable, exposedByDefault) +} diff --git a/provider/ecs/ecs_test.go b/provider/ecs/config_test.go similarity index 53% rename from provider/ecs/ecs_test.go rename to provider/ecs/config_test.go index 01e3e5619..899ab28e6 100644 --- a/provider/ecs/ecs_test.go +++ b/provider/ecs/config_test.go @@ -6,10 +6,470 @@ import ( "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/provider/label" "github.com/containous/traefik/types" "github.com/stretchr/testify/assert" ) +func TestBuildConfiguration(t *testing.T) { + provider := &Provider{} + tests := []struct { + desc string + services map[string][]ecsInstance + expected *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), + }, + }, + }, + }, + }, + }, + expected: &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{ + label.TraefikBackendHealthCheckPath: func(s string) *string { return &s }("/health"), + label.TraefikBackendHealthCheckInterval: 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), + }, + }, + }, + }, + }, + }, + expected: &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.buildConfiguration(test.services) + assert.Equal(t, test.err, err) + assert.Equal(t, test.expected, got) + }) + } +} + +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{ + label.TraefikPort: aws.String("80"), + }) + + tests := []struct { + desc string + instanceInfo ecsInstance + exposedByDefault bool + expected bool + }{ + { + desc: "Instance without enable label and exposed by default enabled should be not filtered", + instanceInfo: simpleEcsInstance(map[string]*string{}), + exposedByDefault: true, + expected: true, + }, + { + desc: "Instance without enable label and exposed by default disabled should be filtered", + instanceInfo: simpleEcsInstance(map[string]*string{}), + exposedByDefault: false, + expected: false, + }, + { + desc: "Instance with enable label set to false and exposed by default enabled should be filtered", + instanceInfo: simpleEcsInstance(map[string]*string{ + label.TraefikEnable: aws.String("false"), + }), + exposedByDefault: true, + expected: false, + }, + { + desc: "Instance with enable label set to true and exposed by default disabled should be not filtered", + instanceInfo: simpleEcsInstance(map[string]*string{ + label.TraefikEnable: aws.String("true"), + }), + exposedByDefault: false, + expected: true, + }, + { + desc: "Instance with nil private ip and exposed by default enabled should be filtered", + instanceInfo: nilPrivateIP, + exposedByDefault: true, + expected: false, + }, + { + desc: "Instance with nil machine and exposed by default enabled should be filtered", + instanceInfo: nilMachine, + exposedByDefault: true, + expected: false, + }, + { + desc: "Instance with nil machine state and exposed by default enabled should be filtered", + instanceInfo: nilMachineState, + exposedByDefault: true, + expected: false, + }, + { + desc: "Instance with nil machine state name and exposed by default enabled should be filtered", + instanceInfo: nilMachineStateName, + exposedByDefault: true, + expected: false, + }, + { + desc: "Instance with invalid machine state and exposed by default enabled should be filtered", + instanceInfo: invalidMachineState, + exposedByDefault: true, + expected: false, + }, + { + desc: "Instance with no port mappings should be filtered", + instanceInfo: noNetwork, + exposedByDefault: true, + expected: false, + }, + { + desc: "Instance with no port mapping and with label should not be filtered", + instanceInfo: noNetworkWithLabel, + exposedByDefault: true, + expected: true, + }, + } + + for _, test := range tests { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + prov := &Provider{ + ExposedByDefault: test.exposedByDefault, + } + actual := prov.filterInstance(test.instanceInfo) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestChunkedTaskArns(t *testing.T) { + testVal := "a" + tests := []struct { + desc string + count int + expectedLengths []int + }{ + { + desc: "0 parameter should return nil", + count: 0, + expectedLengths: []int(nil), + }, + { + desc: "1 parameter should return 1 array of 1 element", + count: 1, + expectedLengths: []int{1}, + }, + { + desc: "99 parameters should return 1 array of 99 elements", + count: 99, + expectedLengths: []int{99}, + }, + { + desc: "100 parameters should return 1 array of 100 elements", + count: 100, + expectedLengths: []int{100}, + }, + { + desc: "101 parameters should return 1 array of 100 elements and 1 array of 1 element", + count: 101, + expectedLengths: []int{100, 1}, + }, + { + desc: "199 parameters should return 1 array of 100 elements and 1 array of 99 elements", + count: 199, + expectedLengths: []int{100, 99}, + }, + { + desc: "200 parameters should return 2 arrays of 100 elements each", + count: 200, + expectedLengths: []int{100, 100}, + }, + { + desc: "201 parameters should return 2 arrays of 100 elements each and 1 array of 1 element", + count: 201, + expectedLengths: []int{100, 100, 1}, + }, + { + 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}, + }, + { + 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}, + }, + } + + 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 := 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 TestGetHost(t *testing.T) { + tests := []struct { + desc string + expected string + instanceInfo ecsInstance + }{ + { + desc: "Default host should be 10.0.0.0", + expected: "10.0.0.0", + instanceInfo: simpleEcsInstance(map[string]*string{}), + }, + } + + for _, test := range tests { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + actual := getHost(test.instanceInfo) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetPort(t *testing.T) { + tests := []struct { + desc string + expected string + instanceInfo ecsInstance + }{ + { + desc: "Default port should be 80", + expected: "80", + instanceInfo: simpleEcsInstance(map[string]*string{}), + }, + { + desc: "Label should override network port", + expected: "4242", + instanceInfo: simpleEcsInstance(map[string]*string{ + label.TraefikPort: aws.String("4242"), + }), + }, + { + desc: "Label should provide exposed port", + expected: "80", + instanceInfo: simpleEcsInstanceNoNetwork(map[string]*string{ + label.TraefikPort: aws.String("80"), + }), + }, + } + + for _, test := range tests { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + actual := getPort(test.instanceInfo) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetFuncStringValue(t *testing.T) { + tests := []struct { + desc string + expected string + instanceInfo ecsInstance + }{ + { + desc: "Protocol label is not set should return a string equals to http", + expected: "http", + instanceInfo: simpleEcsInstance(map[string]*string{}), + }, + { + desc: "Protocol label is set to http should return a string equals to http", + expected: "http", + instanceInfo: simpleEcsInstance(map[string]*string{ + label.TraefikProtocol: aws.String("http"), + }), + }, + { + desc: "Protocol label is set to https should return a string equals to https", + expected: "https", + instanceInfo: simpleEcsInstance(map[string]*string{ + label.TraefikProtocol: aws.String("https"), + }), + }, + } + + for _, test := range tests { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getFuncStringValue(label.TraefikProtocol, label.DefaultProtocol)(test.instanceInfo) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetFuncSliceString(t *testing.T) { + tests := []struct { + desc string + expected []string + instanceInfo ecsInstance + }{ + { + desc: "Frontend entrypoints label not set should return empty array", + expected: nil, + instanceInfo: simpleEcsInstance(map[string]*string{}), + }, + { + desc: "Frontend entrypoints label set to http should return a string array of 1 element", + expected: []string{"http"}, + instanceInfo: simpleEcsInstance(map[string]*string{ + label.TraefikFrontendEntryPoints: aws.String("http"), + }), + }, + { + 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{ + label.TraefikFrontendEntryPoints: aws.String("http,https"), + }), + }, + } + + for _, test := range tests { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + actual := getFuncSliceString(label.TraefikFrontendEntryPoints)(test.instanceInfo) + assert.Equal(t, test.expected, actual) + }) + } +} + func makeEcsInstance(containerDef *ecs.ContainerDefinition) ecsInstance { container := &ecs.Container{ Name: containerDef.Name, @@ -64,686 +524,3 @@ func simpleEcsInstanceNoNetwork(labels map[string]*string) ecsInstance { 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) - }) - } -} diff --git a/provider/ecs/ecs.go b/provider/ecs/ecs.go index 5f5e47174..21ed99a58 100644 --- a/provider/ecs/ecs.go +++ b/provider/ecs/ecs.go @@ -3,9 +3,7 @@ package ecs import ( "context" "fmt" - "strconv" "strings" - "text/template" "time" "github.com/BurntSushi/ty/fun" @@ -21,6 +19,7 @@ import ( "github.com/containous/traefik/job" "github.com/containous/traefik/log" "github.com/containous/traefik/provider" + "github.com/containous/traefik/provider/label" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" ) @@ -178,34 +177,6 @@ func wrapAws(ctx context.Context, req *request.Request) error { return req.Send() } -// generateECSConfig fills the config template with the given instances -func (p *Provider) generateECSConfig(services map[string][]ecsInstance) (*types.Configuration, error) { - var ecsFuncMap = template.FuncMap{ - "filterFrontends": p.filterFrontends, - "getFrontendRule": p.getFrontendRule, - "getBasicAuth": p.getBasicAuth, - "getLoadBalancerMethod": p.getLoadBalancerMethod, - "getLoadBalancerSticky": p.getLoadBalancerSticky, - "hasStickinessLabel": p.hasStickinessLabel, - "getStickinessCookieName": p.getStickinessCookieName, - "getProtocol": p.getProtocol, - "getHost": p.getHost, - "getPort": p.getPort, - "getWeight": p.getWeight, - "getPassHostHeader": p.getPassHostHeader, - "getPriority": p.getPriority, - "getEntryPoints": p.getEntryPoints, - "hasHealthCheckLabels": p.hasHealthCheckLabels, - "getHealthCheckPath": p.getHealthCheckPath, - "getHealthCheckInterval": p.getHealthCheckInterval, - } - return p.GetConfiguration("templates/ecs.tmpl", ecsFuncMap, struct { - Services map[string][]ecsInstance - }{ - services, - }) -} - func (p *Provider) loadECSConfig(ctx context.Context, client *awsClient) (*types.Configuration, error) { instances, err := p.listInstances(ctx, client) if err != nil { @@ -223,7 +194,7 @@ func (p *Provider) loadECSConfig(ctx context.Context, client *awsClient) (*types services[instance.Name] = []ecsInstance{instance} } } - return p.generateECSConfig(services) + return p.buildConfiguration(services) } // Find all running Provider tasks in a cluster, also collect the task definitions (for docker labels) @@ -285,7 +256,7 @@ func (p *Provider) listInstances(ctx context.Context, client *awsClient) ([]ecsI continue } - chunkedTaskArns := p.chunkedTaskArns(taskArns) + chunkedTaskArns := chunkedTaskArns(taskArns) var tasks []*ecs.Task for _, arns := range chunkedTaskArns { @@ -424,22 +395,14 @@ func (p *Provider) lookupTaskDefinitions(ctx context.Context, client *awsClient, return taskDefinitions, nil } -func (p *Provider) label(i ecsInstance, k string) string { - if v, found := i.containerDefinition.DockerLabels[k]; found { - return *v - } - return "" -} - func (p *Provider) filterInstance(i ecsInstance) bool { - if labelPort := p.label(i, types.LabelPort); len(i.container.NetworkBindings) == 0 && labelPort == "" { + + if labelPort := getStringValue(i, label.TraefikPort, ""); len(i.container.NetworkBindings) == 0 && labelPort == "" { log.Debugf("Filtering ecs instance without port %s (%s)", i.Name, i.ID) return false } - if i.machine == nil || - i.machine.State == nil || - i.machine.State.Name == nil { + if i.machine == nil || i.machine.State == nil || i.machine.State.Name == nil { log.Debugf("Filtering ecs instance in an missing ec2 information %s (%s)", i.Name, i.ID) return false } @@ -454,99 +417,20 @@ func (p *Provider) filterInstance(i ecsInstance) bool { return false } - label := p.label(i, types.LabelEnable) - enabled := p.ExposedByDefault && label != "false" || label == "true" - if !enabled { - log.Debugf("Filtering disabled ecs instance %s (%s) (traefik.enabled = '%s')", i.Name, i.ID, label) + if !isEnabled(i, p.ExposedByDefault) { + log.Debugf("Filtering disabled ecs instance %s (%s)", i.Name, i.ID) return false } return true } -func (p *Provider) filterFrontends(instances []ecsInstance) []ecsInstance { - byName := make(map[string]bool) - - return fun.Filter(func(i ecsInstance) bool { - if _, found := byName[i.Name]; !found { - byName[i.Name] = true - return true - } - - return false - }, instances).([]ecsInstance) -} - -func (p *Provider) getFrontendRule(i ecsInstance) string { - if label := p.label(i, types.LabelFrontendRule); label != "" { - return label - } - return "Host:" + strings.ToLower(strings.Replace(i.Name, "_", "-", -1)) + "." + p.Domain -} - -func (p *Provider) getBasicAuth(i ecsInstance) []string { - label := p.label(i, types.LabelFrontendAuthBasic) - if label != "" { - return strings.Split(label, ",") - } - return []string{} -} - -func (p *Provider) getFirstInstanceLabel(instances []ecsInstance, labelName string) string { - if len(instances) > 0 { - return p.label(instances[0], labelName) - } - return "" -} - -func (p *Provider) getLoadBalancerSticky(instances []ecsInstance) string { - if len(instances) > 0 { - label := p.getFirstInstanceLabel(instances, types.LabelBackendLoadbalancerSticky) - if label != "" { - log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) - return label - } - } - return "false" -} - -func (p *Provider) hasStickinessLabel(instances []ecsInstance) bool { - stickinessLabel := p.getFirstInstanceLabel(instances, types.LabelBackendLoadbalancerStickiness) - return len(stickinessLabel) > 0 && strings.EqualFold(strings.TrimSpace(stickinessLabel), "true") -} - -func (p *Provider) getStickinessCookieName(instances []ecsInstance) string { - return p.getFirstInstanceLabel(instances, types.LabelBackendLoadbalancerStickinessCookieName) -} - -func (p *Provider) getLoadBalancerMethod(instances []ecsInstance) string { - if len(instances) > 0 { - label := p.label(instances[0], types.LabelBackendLoadbalancerMethod) - if label != "" { - return label - } - } - return "wrr" -} - -func (p *Provider) hasHealthCheckLabels(instances []ecsInstance) bool { - return p.getHealthCheckPath(instances) != "" -} - -func (p *Provider) getHealthCheckPath(instances []ecsInstance) string { - return p.getFirstInstanceLabel(instances, types.LabelBackendHealthcheckPath) -} - -func (p *Provider) getHealthCheckInterval(instances []ecsInstance) string { - return p.getFirstInstanceLabel(instances, types.LabelBackendHealthcheckInterval) -} - // Provider expects no more than 100 parameters be passed to a DescribeTask call; thus, pack // each string into an array capped at 100 elements -func (p *Provider) chunkedTaskArns(tasks []*string) [][]*string { +func chunkedTaskArns(tasks []*string) [][]*string { var chunkedTasks [][]*string for i := 0; i < len(tasks); i += 100 { - sliceEnd := -1 + var sliceEnd int if i+100 < len(tasks) { sliceEnd = i + 100 } else { @@ -556,49 +440,3 @@ func (p *Provider) chunkedTaskArns(tasks []*string) [][]*string { } return chunkedTasks } - -func (p *Provider) getProtocol(i ecsInstance) string { - if label := p.label(i, types.LabelProtocol); label != "" { - return label - } - return "http" -} - -func (p *Provider) getHost(i ecsInstance) string { - return *i.machine.PrivateIpAddress -} - -func (p *Provider) getPort(i ecsInstance) string { - if port := p.label(i, types.LabelPort); port != "" { - return port - } - return strconv.FormatInt(*i.container.NetworkBindings[0].HostPort, 10) -} - -func (p *Provider) getWeight(i ecsInstance) string { - if label := p.label(i, types.LabelWeight); label != "" { - return label - } - return "0" -} - -func (p *Provider) getPassHostHeader(i ecsInstance) string { - if label := p.label(i, types.LabelFrontendPassHostHeader); label != "" { - return label - } - return "true" -} - -func (p *Provider) getPriority(i ecsInstance) string { - if label := p.label(i, types.LabelFrontendPriority); label != "" { - return label - } - return "0" -} - -func (p *Provider) getEntryPoints(i ecsInstance) []string { - if label := p.label(i, types.LabelFrontendEntryPoints); label != "" { - return strings.Split(label, ",") - } - return []string{} -} diff --git a/templates/ecs.tmpl b/templates/ecs.tmpl index b469969ae..7e280c17c 100644 --- a/templates/ecs.tmpl +++ b/templates/ecs.tmpl @@ -1,7 +1,7 @@ [backends]{{range $serviceName, $instances := .Services}} [backends.backend-{{ $serviceName }}.loadbalancer] method = "{{ getLoadBalancerMethod $instances}}" - sticky = {{ getLoadBalancerSticky $instances}} + sticky = {{ getSticky $instances}} {{if hasStickinessLabel $instances}} [backends.backend-{{ $serviceName }}.loadbalancer.stickiness] cookieName = "{{getStickinessCookieName $instances}}"