diff --git a/provider/ecs/config.go b/provider/ecs/config.go index 6a226dbd2..46c14435a 100644 --- a/provider/ecs/config.go +++ b/provider/ecs/config.go @@ -8,13 +8,26 @@ import ( "github.com/BurntSushi/ty/fun" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/containous/traefik/log" "github.com/containous/traefik/provider" "github.com/containous/traefik/provider/label" "github.com/containous/traefik/types" ) // buildConfiguration fills the config template with the given instances -func (p *Provider) buildConfigurationV2(services map[string][]ecsInstance) (*types.Configuration, error) { +func (p *Provider) buildConfigurationV2(instances []ecsInstance) (*types.Configuration, error) { + services := make(map[string][]ecsInstance) + for _, instance := range instances { + if p.filterInstance(instance) { + if serviceInstances, ok := services[instance.Name]; ok { + services[instance.Name] = append(serviceInstances, instance) + } else { + services[instance.Name] = []ecsInstance{instance} + } + } + } + var ecsFuncMap = template.FuncMap{ // Backend functions "getHost": getHost, @@ -48,6 +61,35 @@ func (p *Provider) buildConfigurationV2(services map[string][]ecsInstance) (*typ }) } +func (p *Provider) filterInstance(i ecsInstance) bool { + if labelPort := label.GetStringValue(i.TraefikLabels, 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 { + log.Debugf("Filtering ecs instance with missing ec2 information %s (%s)", i.Name, i.ID) + return false + } + + if aws.StringValue(i.machine.State.Name) != ec2.InstanceStateNameRunning { + log.Debugf("Filtering ecs instance with an incorrect state %s (%s) (state = %s)", i.Name, i.ID, aws.StringValue(i.machine.State.Name)) + return false + } + + if i.machine.PrivateIpAddress == nil { + log.Debugf("Filtering ecs instance without an ip address %s (%s)", i.Name, i.ID) + return false + } + + if !isEnabled(i, p.ExposedByDefault) { + log.Debugf("Filtering disabled ecs instance %s (%s)", i.Name, i.ID) + return false + } + + return true +} + func (p *Provider) getFrontendRule(i ecsInstance) string { defaultRule := "Host:" + strings.ToLower(strings.Replace(i.Name, "_", "-", -1)) + "." + p.Domain return label.GetStringValue(i.TraefikLabels, label.TraefikFrontendRule, defaultRule) diff --git a/provider/ecs/config_root.go b/provider/ecs/config_root.go index 318feddfd..622edfcfc 100644 --- a/provider/ecs/config_root.go +++ b/provider/ecs/config_root.go @@ -4,9 +4,9 @@ import ( "github.com/containous/traefik/types" ) -func (p *Provider) buildConfiguration(services map[string][]ecsInstance) (*types.Configuration, error) { +func (p *Provider) buildConfiguration(instances []ecsInstance) (*types.Configuration, error) { if p.TemplateVersion == 1 { - return p.buildConfigurationV1(services) + return p.buildConfigurationV1(instances) } - return p.buildConfigurationV2(services) + return p.buildConfigurationV2(instances) } diff --git a/provider/ecs/config_test.go b/provider/ecs/config_test.go index 95c745c82..3aea2ba2e 100644 --- a/provider/ecs/config_test.go +++ b/provider/ecs/config_test.go @@ -15,21 +15,24 @@ import ( func TestBuildConfiguration(t *testing.T) { testCases := []struct { - desc string - services map[string][]ecsInstance - expected *types.Configuration - err error + desc string + instances []ecsInstance + expected *types.Configuration + err error }{ { desc: "config parsed successfully", - services: map[string][]ecsInstance{ - "testing": {{ + instances: []ecsInstance{ + { Name: "instance", ID: "1", containerDefinition: &ecs.ContainerDefinition{ DockerLabels: map[string]*string{}, }, machine: &ec2.Instance{ + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameRunning), + }, PrivateIpAddress: aws.String("10.0.0.1"), }, container: &ecs.Container{ @@ -37,11 +40,11 @@ func TestBuildConfiguration(t *testing.T) { HostPort: aws.Int64(1337), }}, }, - }}, + }, }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ - "backend-testing": { + "backend-instance": { Servers: map[string]types.Server{ "server-instance-1": { URL: "http://10.0.0.1:1337", @@ -49,11 +52,11 @@ func TestBuildConfiguration(t *testing.T) { }, }, Frontends: map[string]*types.Frontend{ - "frontend-testing": { + "frontend-instance": { EntryPoints: []string{}, - Backend: "backend-testing", + Backend: "backend-instance", Routes: map[string]types.Route{ - "route-frontend-testing": { + "route-frontend-instance": { Rule: "Host:instance.", }, }, @@ -65,8 +68,8 @@ func TestBuildConfiguration(t *testing.T) { }, { desc: "config parsed successfully with health check labels", - services: map[string][]ecsInstance{ - "testing": {{ + instances: []ecsInstance{ + { Name: "instance", ID: "1", containerDefinition: &ecs.ContainerDefinition{ @@ -75,6 +78,9 @@ func TestBuildConfiguration(t *testing.T) { label.TraefikBackendHealthCheckInterval: aws.String("1s"), }}, machine: &ec2.Instance{ + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameRunning), + }, PrivateIpAddress: aws.String("10.0.0.1"), }, container: &ecs.Container{ @@ -82,11 +88,11 @@ func TestBuildConfiguration(t *testing.T) { HostPort: aws.Int64(1337), }}, }, - }}, + }, }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ - "backend-testing": { + "backend-instance": { HealthCheck: &types.HealthCheck{ Path: "/health", Interval: "1s", @@ -98,11 +104,11 @@ func TestBuildConfiguration(t *testing.T) { }, }, Frontends: map[string]*types.Frontend{ - "frontend-testing": { + "frontend-instance": { EntryPoints: []string{}, - Backend: "backend-testing", + Backend: "backend-instance", Routes: map[string]types.Route{ - "route-frontend-testing": { + "route-frontend-instance": { Rule: "Host:instance.", }, }, @@ -114,8 +120,8 @@ func TestBuildConfiguration(t *testing.T) { }, { desc: "when all labels are set", - services: map[string][]ecsInstance{ - "testing-instance": {{ + instances: []ecsInstance{ + { Name: "testing-instance", ID: "6", containerDefinition: &ecs.ContainerDefinition{ @@ -193,6 +199,9 @@ func TestBuildConfiguration(t *testing.T) { label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"), }}, machine: &ec2.Instance{ + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameRunning), + }, PrivateIpAddress: aws.String("10.0.0.1"), }, container: &ecs.Container{ @@ -200,7 +209,7 @@ func TestBuildConfiguration(t *testing.T) { HostPort: aws.Int64(1337), }}, }, - }}, + }, }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -351,11 +360,11 @@ func TestBuildConfiguration(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - p := &Provider{} + p := &Provider{ExposedByDefault: true} - services := fakeLoadTraefikLabels(test.services) + instances := fakeLoadTraefikLabels(test.instances) - got, err := p.buildConfiguration(services) + got, err := p.buildConfiguration(instances) assert.Equal(t, test.err, err) // , err.Error() assert.Equal(t, test.expected, got, test.desc) }) @@ -363,27 +372,6 @@ func TestBuildConfiguration(t *testing.T) { } 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"), - }) - testCases := []struct { desc string instanceInfo ecsInstance @@ -419,44 +407,66 @@ func TestFilterInstance(t *testing.T) { expected: true, }, { - desc: "Instance with nil private ip and exposed by default enabled should be filtered", - instanceInfo: nilPrivateIP, + desc: "Instance with nil private ip and exposed by default enabled should be filtered", + instanceInfo: func() ecsInstance { + nilPrivateIP := simpleEcsInstance(map[string]*string{}) + nilPrivateIP.machine.PrivateIpAddress = nil + return nilPrivateIP + }(), exposedByDefault: true, expected: false, }, { - desc: "Instance with nil machine and exposed by default enabled should be filtered", - instanceInfo: nilMachine, + desc: "Instance with nil machine and exposed by default enabled should be filtered", + instanceInfo: func() ecsInstance { + nilMachine := simpleEcsInstance(map[string]*string{}) + nilMachine.machine = nil + return nilMachine + }(), exposedByDefault: true, expected: false, }, { - desc: "Instance with nil machine state and exposed by default enabled should be filtered", - instanceInfo: nilMachineState, + desc: "Instance with nil machine state and exposed by default enabled should be filtered", + instanceInfo: func() ecsInstance { + nilMachineState := simpleEcsInstance(map[string]*string{}) + nilMachineState.machine.State = nil + return nilMachineState + }(), exposedByDefault: true, expected: false, }, { - desc: "Instance with nil machine state name and exposed by default enabled should be filtered", - instanceInfo: nilMachineStateName, + desc: "Instance with nil machine state name and exposed by default enabled should be filtered", + instanceInfo: func() ecsInstance { + nilMachineStateName := simpleEcsInstance(map[string]*string{}) + nilMachineStateName.machine.State.Name = nil + return nilMachineStateName + }(), exposedByDefault: true, expected: false, }, { - desc: "Instance with invalid machine state and exposed by default enabled should be filtered", - instanceInfo: invalidMachineState, + desc: "Instance with invalid machine state and exposed by default enabled should be filtered", + instanceInfo: func() ecsInstance { + invalidMachineState := simpleEcsInstance(map[string]*string{}) + invalidMachineState.machine.State.Name = aws.String(ec2.InstanceStateNameStopped) + return invalidMachineState + }(), exposedByDefault: true, expected: false, }, { desc: "Instance with no port mappings should be filtered", - instanceInfo: noNetwork, + instanceInfo: simpleEcsInstanceNoNetwork(map[string]*string{}), exposedByDefault: true, expected: false, }, { - desc: "Instance with no port mapping and with label should not be filtered", - instanceInfo: noNetworkWithLabel, + desc: "Instance with no port mapping and with label should not be filtered", + instanceInfo: simpleEcsInstanceNoNetwork(map[string]*string{ + label.TraefikPort: aws.String("80"), + }), exposedByDefault: true, expected: true, }, @@ -470,6 +480,7 @@ func TestFilterInstance(t *testing.T) { prov := &Provider{ ExposedByDefault: test.exposedByDefault, } + actual := prov.filterInstance(test.instanceInfo) assert.Equal(t, test.expected, actual) }) @@ -756,15 +767,11 @@ func simpleEcsInstanceNoNetwork(labels map[string]*string) ecsInstance { }) } -func fakeLoadTraefikLabels(services map[string][]ecsInstance) map[string][]ecsInstance { - result := make(map[string][]ecsInstance) - for name, srcInstances := range services { - var instances []ecsInstance - for _, instance := range srcInstances { - instance.TraefikLabels = aws.StringValueMap(instance.containerDefinition.DockerLabels) - instances = append(instances, instance) - } - result[name] = instances +func fakeLoadTraefikLabels(instances []ecsInstance) []ecsInstance { + var result []ecsInstance + for _, instance := range instances { + instance.TraefikLabels = aws.StringValueMap(instance.containerDefinition.DockerLabels) + result = append(result, instance) } return result } diff --git a/provider/ecs/deprecated_config.go b/provider/ecs/deprecated_config.go index 14cc0e0be..809a28951 100644 --- a/provider/ecs/deprecated_config.go +++ b/provider/ecs/deprecated_config.go @@ -5,6 +5,7 @@ import ( "text/template" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/containous/traefik/log" "github.com/containous/traefik/provider/label" "github.com/containous/traefik/types" @@ -12,7 +13,18 @@ import ( // buildConfiguration fills the config template with the given instances // Deprecated -func (p *Provider) buildConfigurationV1(services map[string][]ecsInstance) (*types.Configuration, error) { +func (p *Provider) buildConfigurationV1(instances []ecsInstance) (*types.Configuration, error) { + services := make(map[string][]ecsInstance) + for _, instance := range instances { + if p.filterInstanceV1(instance) { + if serviceInstances, ok := services[instance.Name]; ok { + services[instance.Name] = append(serviceInstances, instance) + } else { + services[instance.Name] = []ecsInstance{instance} + } + } + } + var ecsFuncMap = template.FuncMap{ // Backend functions "getHost": getHost, @@ -45,6 +57,35 @@ func (p *Provider) buildConfigurationV1(services map[string][]ecsInstance) (*typ }) } +func (p *Provider) filterInstanceV1(i ecsInstance) bool { + if labelPort := getStringValueV1(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 { + log.Debugf("Filtering ecs instance in an missing ec2 information %s (%s)", i.Name, i.ID) + return false + } + + if aws.StringValue(i.machine.State.Name) != ec2.InstanceStateNameRunning { + log.Debugf("Filtering ecs instance in an incorrect state %s (%s) (state = %s)", i.Name, i.ID, aws.StringValue(i.machine.State.Name)) + return false + } + + if i.machine.PrivateIpAddress == nil { + log.Debugf("Filtering ecs instance without an ip address %s (%s)", i.Name, i.ID) + return false + } + + if !isEnabled(i, p.ExposedByDefault) { + log.Debugf("Filtering disabled ecs instance %s (%s)", i.Name, i.ID) + return false + } + + return true +} + // TODO: Deprecated // replaced by Stickiness // Deprecated diff --git a/provider/ecs/deprecated_config_test.go b/provider/ecs/deprecated_config_test.go index 0e73bf05b..480c9de24 100644 --- a/provider/ecs/deprecated_config_test.go +++ b/provider/ecs/deprecated_config_test.go @@ -13,21 +13,24 @@ import ( func TestBuildConfigurationV1(t *testing.T) { testCases := []struct { - desc string - services map[string][]ecsInstance - expected *types.Configuration - err error + desc string + instances []ecsInstance + expected *types.Configuration + err error }{ { desc: "config parsed successfully", - services: map[string][]ecsInstance{ - "testing": {{ + instances: []ecsInstance{ + { Name: "testing", ID: "1", containerDefinition: &ecs.ContainerDefinition{ DockerLabels: map[string]*string{}, }, machine: &ec2.Instance{ + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameRunning), + }, PrivateIpAddress: aws.String("10.0.0.1"), }, container: &ecs.Container{ @@ -35,7 +38,7 @@ func TestBuildConfigurationV1(t *testing.T) { HostPort: aws.Int64(1337), }}, }, - }}, + }, }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -66,8 +69,8 @@ func TestBuildConfigurationV1(t *testing.T) { }, { desc: "config parsed successfully with health check labels", - services: map[string][]ecsInstance{ - "testing": {{ + instances: []ecsInstance{ + { Name: "testing", ID: "1", containerDefinition: &ecs.ContainerDefinition{ @@ -76,6 +79,9 @@ func TestBuildConfigurationV1(t *testing.T) { label.TraefikBackendHealthCheckInterval: aws.String("1s"), }}, machine: &ec2.Instance{ + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameRunning), + }, PrivateIpAddress: aws.String("10.0.0.1"), }, container: &ecs.Container{ @@ -83,7 +89,7 @@ func TestBuildConfigurationV1(t *testing.T) { HostPort: aws.Int64(1337), }}, }, - }}, + }, }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -118,8 +124,8 @@ func TestBuildConfigurationV1(t *testing.T) { }, { desc: "when all labels are set", - services: map[string][]ecsInstance{ - "testing-instance": {{ + instances: []ecsInstance{ + { Name: "testing-instance", ID: "6", containerDefinition: &ecs.ContainerDefinition{ @@ -144,6 +150,9 @@ func TestBuildConfigurationV1(t *testing.T) { label.TraefikFrontendRule: aws.String("Host:traefik.io"), }}, machine: &ec2.Instance{ + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameRunning), + }, PrivateIpAddress: aws.String("10.0.0.1"), }, container: &ecs.Container{ @@ -151,7 +160,7 @@ func TestBuildConfigurationV1(t *testing.T) { HostPort: aws.Int64(1337), }}, }, - }}, + }, }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -204,11 +213,11 @@ func TestBuildConfigurationV1(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - provider := &Provider{} + provider := &Provider{ExposedByDefault: true} - services := fakeLoadTraefikLabels(test.services) + instances := fakeLoadTraefikLabels(test.instances) - got, err := provider.buildConfigurationV1(services) + got, err := provider.buildConfigurationV1(instances) assert.Equal(t, test.err, err) // , err.Error() assert.Equal(t, test.expected, got, test.desc) }) diff --git a/provider/ecs/ecs.go b/provider/ecs/ecs.go index 187e3d88d..8be3d1e8f 100644 --- a/provider/ecs/ecs.go +++ b/provider/ecs/ecs.go @@ -6,7 +6,6 @@ import ( "strings" "time" - "github.com/BurntSushi/ty/fun" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/defaults" @@ -19,7 +18,6 @@ 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" ) @@ -105,7 +103,6 @@ func (p *Provider) createClient() (*awsClient, error) { // Provide allows the ecs provider to provide configurations to traefik // using the given configuration channel. func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error { - p.Constraints = append(p.Constraints, constraints...) handleCanceled := func(ctx context.Context, err error) error { @@ -179,26 +176,6 @@ func wrapAws(ctx context.Context, req *request.Request) error { return req.Send() } -func (p *Provider) loadECSConfig(ctx context.Context, client *awsClient) (*types.Configuration, error) { - instances, err := p.listInstances(ctx, client) - if err != nil { - return nil, err - } - - instances = fun.Filter(p.filterInstance, instances).([]ecsInstance) - - services := make(map[string][]ecsInstance) - - for _, instance := range instances { - if serviceInstances, ok := services[instance.Name]; ok { - services[instance.Name] = append(serviceInstances, instance) - } else { - services[instance.Name] = []ecsInstance{instance} - } - } - return p.buildConfiguration(services) -} - // Find all running Provider tasks in a cluster, also collect the task definitions (for docker labels) // and the EC2 instance data func (p *Provider) listInstances(ctx context.Context, client *awsClient) ([]ecsInstance, error) { @@ -401,34 +378,13 @@ func (p *Provider) lookupTaskDefinitions(ctx context.Context, client *awsClient, return taskDefinitions, nil } -func (p *Provider) filterInstance(i ecsInstance) bool { - - if labelPort := getStringValueV1(i, label.TraefikPort, ""); len(i.container.NetworkBindings) == 0 && labelPort == "" { - log.Debugf("Filtering ecs instance without port %s (%s)", i.Name, i.ID) - return false +func (p *Provider) loadECSConfig(ctx context.Context, client *awsClient) (*types.Configuration, error) { + instances, err := p.listInstances(ctx, client) + if err != nil { + return nil, err } - 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 - } - - if aws.StringValue(i.machine.State.Name) != ec2.InstanceStateNameRunning { - log.Debugf("Filtering ecs instance in an incorrect state %s (%s) (state = %s)", i.Name, i.ID, aws.StringValue(i.machine.State.Name)) - return false - } - - if i.machine.PrivateIpAddress == nil { - log.Debugf("Filtering ecs instance without an ip address %s (%s)", i.Name, i.ID) - return false - } - - if !isEnabled(i, p.ExposedByDefault) { - log.Debugf("Filtering disabled ecs instance %s (%s)", i.Name, i.ID) - return false - } - - return true + return p.buildConfiguration(instances) } // Provider expects no more than 100 parameters be passed to a DescribeTask call; thus, pack