Factorize labels managements.

This commit is contained in:
Ludovic Fernandez 2018-04-11 12:26:03 +02:00 committed by Traefiker Bot
parent 21f6f81914
commit f804053736
6 changed files with 192 additions and 137 deletions

View file

@ -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)

View file

@ -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)
}

View file

@ -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
}

View file

@ -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

View file

@ -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)
})

View file

@ -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