1
0
Fork 0

Dynamic Configuration Refactoring

This commit is contained in:
Ludovic Fernandez 2018-11-14 10:18:03 +01:00 committed by Traefiker Bot
parent d3ae88f108
commit a09dfa3ce1
452 changed files with 21023 additions and 9419 deletions

View file

@ -0,0 +1,80 @@
package ecs
import (
"github.com/aws/aws-sdk-go/service/ecs"
)
func instance(ops ...func(*ecsInstance)) ecsInstance {
e := &ecsInstance{
containerDefinition: &ecs.ContainerDefinition{},
}
for _, op := range ops {
op(e)
}
return *e
}
func name(name string) func(*ecsInstance) {
return func(e *ecsInstance) {
e.Name = name
}
}
func ID(ID string) func(*ecsInstance) {
return func(e *ecsInstance) {
e.ID = ID
}
}
func iMachine(opts ...func(*machine)) func(*ecsInstance) {
return func(e *ecsInstance) {
e.machine = &machine{}
for _, opt := range opts {
opt(e.machine)
}
}
}
func mState(state string) func(*machine) {
return func(m *machine) {
m.state = state
}
}
func mPrivateIP(ip string) func(*machine) {
return func(m *machine) {
m.privateIP = ip
}
}
func mPorts(opts ...func(*portMapping)) func(*machine) {
return func(m *machine) {
for _, opt := range opts {
p := &portMapping{}
opt(p)
m.ports = append(m.ports, *p)
}
}
}
func mPort(containerPort int32, hostPort int32) func(*portMapping) {
return func(pm *portMapping) {
pm.containerPort = int64(containerPort)
pm.hostPort = int64(hostPort)
}
}
func labels(labels map[string]string) func(*ecsInstance) {
return func(c *ecsInstance) {
c.TraefikLabels = labels
}
}
func dockerLabels(labels map[string]*string) func(*ecsInstance) {
return func(c *ecsInstance) {
c.containerDefinition.DockerLabels = labels
}
}

View file

@ -0,0 +1,32 @@
package ecs
import (
"fmt"
"strings"
)
// Clusters holds ecs clusters name
type Clusters []string
// Set adds strings elem into the the parser
// it splits str on , and ;
func (c *Clusters) Set(str string) error {
fargs := func(c rune) bool {
return c == ',' || c == ';'
}
// get function
slice := strings.FieldsFunc(str, fargs)
*c = append(*c, slice...)
return nil
}
// Get Clusters
func (c *Clusters) Get() interface{} { return *c }
// String return slice in a string
func (c *Clusters) String() string { return fmt.Sprintf("%v", *c) }
// SetValue sets Clusters into the parser
func (c *Clusters) SetValue(val interface{}) {
*c = val.(Clusters)
}

View file

@ -0,0 +1,145 @@
package ecs
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestClustersSet(t *testing.T) {
tests := []struct {
desc string
value string
expected Clusters
}{
{
desc: "One value should return Clusters of size 1",
value: "cluster",
expected: Clusters{"cluster"},
},
{
desc: "Two values separated by comma should return Clusters of size 2",
value: "cluster1,cluster2",
expected: Clusters{"cluster1", "cluster2"},
},
{
desc: "Two values separated by semicolon should return Clusters of size 2",
value: "cluster1;cluster2",
expected: Clusters{"cluster1", "cluster2"},
},
{
desc: "Three values separated by comma and semicolon should return Clusters of size 3",
value: "cluster1,cluster2;cluster3",
expected: Clusters{"cluster1", "cluster2", "cluster3"},
},
}
for _, test := range tests {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var clusters Clusters
err := clusters.Set(test.value)
assert.Nil(t, err)
assert.Equal(t, test.expected, clusters)
})
}
}
func TestClustersGet(t *testing.T) {
tests := []struct {
desc string
clusters Clusters
expected Clusters
}{
{
desc: "Should return 1 cluster",
clusters: Clusters{"cluster"},
expected: Clusters{"cluster"},
},
{
desc: "Should return 2 clusters",
clusters: Clusters{"cluster1", "cluster2"},
expected: Clusters{"cluster1", "cluster2"},
},
{
desc: "Should return 3 clusters",
clusters: Clusters{"cluster1", "cluster2", "cluster3"},
expected: Clusters{"cluster1", "cluster2", "cluster3"},
},
}
for _, test := range tests {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := test.clusters.Get()
assert.Equal(t, test.expected, actual)
})
}
}
func TestClustersString(t *testing.T) {
tests := []struct {
desc string
clusters Clusters
expected string
}{
{
desc: "Should return 1 cluster",
clusters: Clusters{"cluster"},
expected: "[cluster]",
},
{
desc: "Should return 2 clusters",
clusters: Clusters{"cluster1", "cluster2"},
expected: "[cluster1 cluster2]",
},
{
desc: "Should return 3 clusters",
clusters: Clusters{"cluster1", "cluster2", "cluster3"},
expected: "[cluster1 cluster2 cluster3]",
},
}
for _, test := range tests {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := test.clusters.String()
assert.Equal(t, test.expected, actual)
})
}
}
func TestClustersSetValue(t *testing.T) {
tests := []struct {
desc string
clusters Clusters
expected Clusters
}{
{
desc: "Should return Clusters of size 1",
clusters: Clusters{"cluster"},
expected: Clusters{"cluster"},
},
{
desc: "Should return Clusters of size 2",
clusters: Clusters{"cluster1", "cluster2"},
expected: Clusters{"cluster1", "cluster2"},
},
{
desc: "Should return Clusters of size 3",
clusters: Clusters{"cluster1", "cluster2", "cluster3"},
expected: Clusters{"cluster1", "cluster2", "cluster3"},
},
}
for _, test := range tests {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var slice Clusters
slice.SetValue(test.clusters)
assert.Equal(t, test.expected, slice)
})
}
}

252
old/provider/ecs/config.go Normal file
View file

@ -0,0 +1,252 @@
package ecs
import (
"crypto/md5"
"encoding/hex"
"fmt"
"net"
"strconv"
"strings"
"text/template"
"github.com/BurntSushi/ty/fun"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/containous/traefik/old/log"
"github.com/containous/traefik/old/provider"
"github.com/containous/traefik/old/provider/label"
"github.com/containous/traefik/old/types"
)
// buildConfiguration fills the config template with the given instances
func (p *Provider) buildConfiguration(instances []ecsInstance) (*types.Configuration, error) {
var ecsFuncMap = template.FuncMap{
// Backend functions
"getHost": getHost,
"getPort": getPort,
"getCircuitBreaker": label.GetCircuitBreaker,
"getLoadBalancer": label.GetLoadBalancer,
"getMaxConn": label.GetMaxConn,
"getHealthCheck": label.GetHealthCheck,
"getBuffering": label.GetBuffering,
"getResponseForwarding": label.GetResponseForwarding,
"getServers": getServers,
// Frontend functions
"filterFrontends": filterFrontends,
"getFrontendRule": p.getFrontendRule,
"getFrontendName": p.getFrontendName,
"getPassHostHeader": label.GetFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader),
"getPassTLSCert": label.GetFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert),
"getPassTLSClientCert": label.GetTLSClientCert,
"getPriority": label.GetFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriority),
"getBasicAuth": label.GetFuncSliceString(label.TraefikFrontendAuthBasic), // Deprecated
"getAuth": label.GetAuth,
"getEntryPoints": label.GetFuncSliceString(label.TraefikFrontendEntryPoints),
"getRedirect": label.GetRedirect,
"getErrorPages": label.GetErrorPages,
"getRateLimit": label.GetRateLimit,
"getHeaders": label.GetHeaders,
"getWhiteList": label.GetWhiteList,
}
services := make(map[string][]ecsInstance)
for _, instance := range instances {
segmentProperties := label.ExtractTraefikLabels(instance.TraefikLabels)
for segmentName, labels := range segmentProperties {
instance.SegmentLabels = labels
instance.SegmentName = segmentName
backendName := getBackendName(instance)
if p.filterInstance(instance) {
if serviceInstances, ok := services[backendName]; ok {
services[backendName] = append(serviceInstances, instance)
} else {
services[backendName] = []ecsInstance{instance}
}
}
}
}
return p.GetConfiguration("templates/ecs.tmpl", ecsFuncMap, struct {
Services map[string][]ecsInstance
}{
Services: services,
})
}
func (p *Provider) filterInstance(i ecsInstance) bool {
if i.machine == nil {
log.Debug("Filtering ecs instance with nil machine")
return false
}
if labelPort := label.GetStringValue(i.TraefikLabels, label.TraefikPort, ""); len(i.machine.ports) == 0 && labelPort == "" {
log.Debugf("Filtering ecs instance without port %s (%s)", i.Name, i.ID)
return false
}
if strings.ToLower(i.machine.state) != ec2.InstanceStateNameRunning {
log.Debugf("Filtering ecs instance with an incorrect state %s (%s) (state = %s)", i.Name, i.ID, i.machine.state)
return false
}
if len(i.machine.privateIP) == 0 {
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
}
constraintTags := label.GetSliceStringValue(i.TraefikLabels, label.TraefikTags)
if ok, failingConstraint := p.MatchConstraints(constraintTags); !ok {
if failingConstraint != nil {
log.Debugf("Filtering ecs instance pruned by constraint %s (%s) (constraint = %q)", i.Name, i.ID, failingConstraint.String())
}
return false
}
return true
}
func getBackendName(i ecsInstance) string {
if len(i.SegmentName) > 0 {
return getSegmentBackendName(i)
}
return getDefaultBackendName(i)
}
func getSegmentBackendName(i ecsInstance) string {
if value := label.GetStringValue(i.SegmentLabels, label.TraefikBackend, ""); len(value) > 0 {
return provider.Normalize(i.Name + "-" + value)
}
return provider.Normalize(i.Name + "-" + i.SegmentName)
}
func getDefaultBackendName(i ecsInstance) string {
if value := label.GetStringValue(i.SegmentLabels, label.TraefikBackend, ""); len(value) != 0 {
return provider.Normalize(value)
}
return provider.Normalize(i.Name)
}
func (p *Provider) getFrontendRule(i ecsInstance) string {
if value := label.GetStringValue(i.SegmentLabels, label.TraefikFrontendRule, ""); len(value) != 0 {
return value
}
domain := label.GetStringValue(i.SegmentLabels, label.TraefikDomain, p.Domain)
if len(domain) > 0 {
domain = "." + domain
}
defaultRule := "Host:" + strings.ToLower(strings.Replace(i.Name, "_", "-", -1)) + domain
return label.GetStringValue(i.TraefikLabels, label.TraefikFrontendRule, defaultRule)
}
func (p *Provider) getFrontendName(instance ecsInstance) string {
name := getBackendName(instance)
if len(instance.SegmentName) > 0 {
name = instance.SegmentName + "-" + name
}
return provider.Normalize(name)
}
func getHost(i ecsInstance) string {
return i.machine.privateIP
}
func getPort(i ecsInstance) string {
value := label.GetStringValue(i.SegmentLabels, label.TraefikPort, "")
if len(value) == 0 {
value = label.GetStringValue(i.TraefikLabels, label.TraefikPort, "")
}
if len(value) > 0 {
port, err := strconv.ParseInt(value, 10, 64)
if err == nil {
for _, mapping := range i.machine.ports {
if port == mapping.hostPort || port == mapping.containerPort {
return strconv.FormatInt(mapping.hostPort, 10)
}
}
return value
}
}
return strconv.FormatInt(i.machine.ports[0].hostPort, 10)
}
func filterFrontends(instances []ecsInstance) []ecsInstance {
byName := make(map[string]struct{})
return fun.Filter(func(i ecsInstance) bool {
backendName := getBackendName(i)
if len(i.SegmentName) > 0 {
backendName = backendName + "-" + i.SegmentName
}
_, found := byName[backendName]
if !found {
byName[backendName] = struct{}{}
}
return !found
}, instances).([]ecsInstance)
}
func getServers(instances []ecsInstance) map[string]types.Server {
var servers map[string]types.Server
for _, instance := range instances {
if servers == nil {
servers = make(map[string]types.Server)
}
protocol := label.GetStringValue(instance.SegmentLabels, label.TraefikProtocol, label.DefaultProtocol)
host := getHost(instance)
port := getPort(instance)
serverURL := fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(host, port))
serverName := getServerName(instance, serverURL)
if _, exist := servers[serverName]; exist {
log.Debugf("Skipping server %q with the same URL.", serverName)
continue
}
servers[serverName] = types.Server{
URL: serverURL,
Weight: label.GetIntValue(instance.SegmentLabels, label.TraefikWeight, label.DefaultWeight),
}
}
return servers
}
func isEnabled(i ecsInstance, exposedByDefault bool) bool {
return label.GetBoolValue(i.TraefikLabels, label.TraefikEnable, exposedByDefault)
}
func getServerName(instance ecsInstance, url string) string {
hash := md5.New()
_, err := hash.Write([]byte(url))
if err != nil {
// Impossible case
log.Errorf("Fail to hash server URL %q", url)
}
if len(instance.SegmentName) > 0 {
return provider.Normalize(fmt.Sprintf("server-%s-%s-%s", instance.Name, instance.ID, hex.EncodeToString(hash.Sum(nil))))
}
return provider.Normalize(fmt.Sprintf("server-%s-%s", instance.Name, instance.ID))
}

View file

@ -0,0 +1,881 @@
package ecs
import (
"testing"
"time"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/containous/flaeg/parse"
"github.com/containous/traefik/old/provider/label"
"github.com/containous/traefik/old/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSegmentBuildConfiguration(t *testing.T) {
testCases := []struct {
desc string
instanceInfo []ecsInstance
expectedFrontends map[string]*types.Frontend
expectedBackends map[string]*types.Backend
}{
{
desc: "when no container",
instanceInfo: []ecsInstance{},
expectedFrontends: map[string]*types.Frontend{},
expectedBackends: map[string]*types.Backend{},
},
{
desc: "simple configuration",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("foo"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.frontend.entryPoints": "http,https",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
PassHostHeader: true,
EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.ecs.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-123456789abc-863563a2e23c95502862016417ee95ea": {
URL: "http://127.0.0.1:2503",
Weight: label.DefaultWeight,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "auth basic",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("foo"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.frontend.entryPoints": "http,https",
label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsersFile: ".htpasswd",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicRemoveHeader: "true",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
PassHostHeader: true,
EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.ecs.localhost",
},
},
Auth: &types.Auth{
HeaderField: "X-WebAuth-User",
Basic: &types.Basic{
RemoveHeader: true,
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
UsersFile: ".htpasswd",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-123456789abc-863563a2e23c95502862016417ee95ea": {
URL: "http://127.0.0.1:2503",
Weight: label.DefaultWeight,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "auth basic backward compatibility",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("foo"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.frontend.entryPoints": "http,https",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
PassHostHeader: true,
EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.ecs.localhost",
},
},
Auth: &types.Auth{
Basic: &types.Basic{
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-123456789abc-863563a2e23c95502862016417ee95ea": {
URL: "http://127.0.0.1:2503",
Weight: label.DefaultWeight,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "auth digest",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("foo"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.frontend.entryPoints": "http,https",
label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsersFile: ".htpasswd",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestRemoveHeader: "true",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
PassHostHeader: true,
EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.ecs.localhost",
},
},
Auth: &types.Auth{
HeaderField: "X-WebAuth-User",
Digest: &types.Digest{
RemoveHeader: true,
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
UsersFile: ".htpasswd",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-123456789abc-863563a2e23c95502862016417ee95ea": {
URL: "http://127.0.0.1:2503",
Weight: label.DefaultWeight,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "auth forward",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("foo"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.frontend.entryPoints": "http,https",
label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardAddress: "auth.server",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTrustForwardHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCa: "ca.crt",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCaOptional: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCert: "server.crt",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSKey: "server.key",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSInsecureSkipVerify: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardAuthResponseHeaders: "X-Auth-User,X-Auth-Token",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
PassHostHeader: true,
EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.ecs.localhost",
},
},
Auth: &types.Auth{
HeaderField: "X-WebAuth-User",
Forward: &types.Forward{
Address: "auth.server",
TLS: &types.ClientTLS{
CA: "ca.crt",
CAOptional: true,
Cert: "server.crt",
Key: "server.key",
InsecureSkipVerify: true,
},
TrustForwardHeader: true,
AuthResponseHeaders: []string{"X-Auth-User", "X-Auth-Token"},
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-123456789abc-863563a2e23c95502862016417ee95ea": {
URL: "http://127.0.0.1:2503",
Weight: label.DefaultWeight,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "when all labels are set",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("foo"),
labels(map[string]string{
label.Prefix + "sauternes." + label.SuffixPort: "666",
label.Prefix + "sauternes." + label.SuffixProtocol: "https",
label.Prefix + "sauternes." + label.SuffixWeight: "12",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicRemoveHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsersFile: ".htpasswd",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestRemoveHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsersFile: ".htpasswd",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardAddress: "auth.server",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTrustForwardHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCa: "ca.crt",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCaOptional: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCert: "server.crt",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSKey: "server.key",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSInsecureSkipVerify: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertPem: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosNotBefore: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosNotAfter: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSans: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectCommonName: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectCountry: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectLocality: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectOrganization: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectProvince: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectSerialNumber: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.Prefix + "sauternes." + label.SuffixFrontendEntryPoints: "http,https",
label.Prefix + "sauternes." + label.SuffixFrontendPassHostHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSCert: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPriority: "666",
label.Prefix + "sauternes." + label.SuffixFrontendRedirectEntryPoint: "https",
label.Prefix + "sauternes." + label.SuffixFrontendRedirectRegex: "nope",
label.Prefix + "sauternes." + label.SuffixFrontendRedirectReplacement: "nope",
label.Prefix + "sauternes." + label.SuffixFrontendRedirectPermanent: "true",
label.Prefix + "sauternes." + label.SuffixFrontendWhiteListSourceRange: "10.10.10.10",
label.Prefix + "sauternes." + label.SuffixFrontendWhiteListIPStrategyExcludedIPS: "10.10.10.10,10.10.10.11",
label.Prefix + "sauternes." + label.SuffixFrontendWhiteListIPStrategyDepth: "5",
label.Prefix + "sauternes." + label.SuffixFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8",
label.Prefix + "sauternes." + label.SuffixFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLProxyHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersAllowedHosts: "foo,bar,bor",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersHostsProxyHeaders: "foo,bar,bor",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLHost: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersCustomFrameOptionsValue: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersContentSecurityPolicy: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersPublicKey: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersReferrerPolicy: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersCustomBrowserXSSValue: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSSeconds: "666",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLForceHost: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLRedirect: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLTemporaryRedirect: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSIncludeSubdomains: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSPreload: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersForceSTSHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersFrameDeny: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersContentTypeNosniff: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersBrowserXSSFilter: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersIsDevelopment: "true",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: "404",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: "foobar",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: "foo_query",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: "500,600",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: "foobar",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: "bar_query",
label.Prefix + "sauternes." + label.SuffixFrontendRateLimitExtractorFunc: "client.ip",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 666),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
EntryPoints: []string{
"http",
"https",
},
PassHostHeader: true,
PassTLSCert: true,
Priority: 666,
PassTLSClientCert: &types.TLSClientHeaders{
PEM: true,
Infos: &types.TLSClientCertificateInfos{
NotBefore: true,
Sans: true,
NotAfter: true,
Subject: &types.TLSCLientCertificateSubjectInfos{
CommonName: true,
Country: true,
Locality: true,
Organization: true,
Province: true,
SerialNumber: true,
},
},
},
Auth: &types.Auth{
HeaderField: "X-WebAuth-User",
Basic: &types.Basic{
RemoveHeader: true,
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
UsersFile: ".htpasswd",
},
},
WhiteList: &types.WhiteList{
SourceRange: []string{"10.10.10.10"},
IPStrategy: &types.IPStrategy{
Depth: 5,
ExcludedIPs: []string{"10.10.10.10", "10.10.10.11"},
},
},
Headers: &types.Headers{
CustomRequestHeaders: map[string]string{
"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
"Content-Type": "application/json; charset=utf-8",
},
CustomResponseHeaders: map[string]string{
"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
"Content-Type": "application/json; charset=utf-8",
},
AllowedHosts: []string{
"foo",
"bar",
"bor",
},
HostsProxyHeaders: []string{
"foo",
"bar",
"bor",
},
SSLRedirect: true,
SSLTemporaryRedirect: true,
SSLForceHost: true,
SSLHost: "foo",
SSLProxyHeaders: map[string]string{
"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
"Content-Type": "application/json; charset=utf-8",
},
STSSeconds: 666,
STSIncludeSubdomains: true,
STSPreload: true,
ForceSTSHeader: true,
FrameDeny: true,
CustomFrameOptionsValue: "foo",
ContentTypeNosniff: true,
BrowserXSSFilter: true,
CustomBrowserXSSValue: "foo",
ContentSecurityPolicy: "foo",
PublicKey: "foo",
ReferrerPolicy: "foo",
IsDevelopment: true,
},
Errors: map[string]*types.ErrorPage{
"foo": {
Status: []string{"404"},
Query: "foo_query",
Backend: "backend-foobar",
},
"bar": {
Status: []string{"500", "600"},
Query: "bar_query",
Backend: "backend-foobar",
},
},
RateLimit: &types.RateLimit{
ExtractorFunc: "client.ip",
RateSet: map[string]*types.Rate{
"foo": {
Period: parse.Duration(6 * time.Second),
Average: 12,
Burst: 18,
},
"bar": {
Period: parse.Duration(3 * time.Second),
Average: 6,
Burst: 9,
},
},
},
Redirect: &types.Redirect{
EntryPoint: "https",
Regex: "",
Replacement: "",
Permanent: true,
},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.ecs.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-123456789abc-7f6444e0dff3330c8b0ad2bbbd383b0f": {
URL: "https://127.0.0.1:666",
Weight: 12,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "several containers",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("test1"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.protocol": "https",
"traefik.sauternes.weight": "80",
"traefik.sauternes.backend": "foobar",
"traefik.sauternes.frontend.passHostHeader": "false",
"traefik.sauternes.frontend.rule": "Path:/mypath",
"traefik.sauternes.frontend.priority": "5000",
"traefik.sauternes.frontend.entryPoints": "http,https,ws",
"traefik.sauternes.frontend.auth.basic": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
"traefik.sauternes.frontend.redirect.entryPoint": "https",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
),
),
),
instance(
ID("abc987654321"),
name("test2"),
labels(map[string]string{
"traefik.anothersauternes.port": "8079",
"traefik.anothersauternes.weight": "33",
"traefik.anothersauternes.frontend.rule": "Path:/anotherpath",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.2"),
mPorts(
mPort(80, 8079),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: false,
Priority: 5000,
EntryPoints: []string{"http", "https", "ws"},
Auth: &types.Auth{
Basic: &types.Basic{
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
},
},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-sauternes-test1-foobar": {
Rule: "Path:/mypath",
},
},
},
"frontend-anothersauternes-test2-anothersauternes": {
Backend: "backend-test2-anothersauternes",
PassHostHeader: true,
EntryPoints: []string{},
Routes: map[string]types.Route{
"route-frontend-anothersauternes-test2-anothersauternes": {
Rule: "Path:/anotherpath",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test1-foobar": {
Servers: map[string]types.Server{
"server-test1-123456789abc-79533a101142718f0fdf84c42593c41e": {
URL: "https://127.0.0.1:2503",
Weight: 80,
},
},
CircuitBreaker: nil,
},
"backend-test2-anothersauternes": {
Servers: map[string]types.Server{
"server-test2-abc987654321-045e3e4aa5a744a325c099b803700a93": {
URL: "http://127.0.0.2:8079",
Weight: 33,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "several segments with the same backend name and same port",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("test1"),
labels(map[string]string{
"traefik.port": "2503",
"traefik.protocol": "https",
"traefik.weight": "80",
"traefik.frontend.entryPoints": "http,https",
"traefik.frontend.redirect.entryPoint": "https",
"traefik.sauternes.backend": "foobar",
"traefik.sauternes.frontend.rule": "Path:/sauternes",
"traefik.sauternes.frontend.priority": "5000",
"traefik.arbois.backend": "foobar",
"traefik.arbois.frontend.rule": "Path:/arbois",
"traefik.arbois.frontend.priority": "3000",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 5000,
EntryPoints: []string{"http", "https"},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-sauternes-test1-foobar": {
Rule: "Path:/sauternes",
},
},
},
"frontend-arbois-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 3000,
EntryPoints: []string{"http", "https"},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-arbois-test1-foobar": {
Rule: "Path:/arbois",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test1-foobar": {
Servers: map[string]types.Server{
"server-test1-123456789abc-79533a101142718f0fdf84c42593c41e": {
URL: "https://127.0.0.1:2503",
Weight: 80,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "several segments with the same backend name and different port (wrong behavior)",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("test1"),
labels(map[string]string{
"traefik.protocol": "https",
"traefik.frontend.entryPoints": "http,https",
"traefik.frontend.redirect.entryPoint": "https",
"traefik.sauternes.port": "2503",
"traefik.sauternes.weight": "80",
"traefik.sauternes.backend": "foobar",
"traefik.sauternes.frontend.rule": "Path:/sauternes",
"traefik.sauternes.frontend.priority": "5000",
"traefik.arbois.port": "2504",
"traefik.arbois.weight": "90",
"traefik.arbois.backend": "foobar",
"traefik.arbois.frontend.rule": "Path:/arbois",
"traefik.arbois.frontend.priority": "3000",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
mPort(80, 2504),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 5000,
EntryPoints: []string{"http", "https"},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-sauternes-test1-foobar": {
Rule: "Path:/sauternes",
},
},
},
"frontend-arbois-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 3000,
EntryPoints: []string{"http", "https"},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-arbois-test1-foobar": {
Rule: "Path:/arbois",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test1-foobar": {
Servers: map[string]types.Server{
"server-test1-123456789abc-79533a101142718f0fdf84c42593c41e": {
URL: "https://127.0.0.1:2503",
Weight: 80,
},
"server-test1-123456789abc-315a41140f1bd825b066e39686c18482": {
URL: "https://127.0.0.1:2504",
Weight: 90,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "several segments with the same backend name and different port binding",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("test1"),
labels(map[string]string{
"traefik.protocol": "https",
"traefik.frontend.entryPoints": "http,https",
"traefik.frontend.redirect.entryPoint": "https",
"traefik.sauternes.port": "2503",
"traefik.sauternes.weight": "80",
"traefik.sauternes.backend": "foobar",
"traefik.sauternes.frontend.rule": "Path:/sauternes",
"traefik.sauternes.frontend.priority": "5000",
"traefik.arbois.port": "8080",
"traefik.arbois.weight": "90",
"traefik.arbois.backend": "foobar",
"traefik.arbois.frontend.rule": "Path:/arbois",
"traefik.arbois.frontend.priority": "3000",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
mPort(8080, 2504),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 5000,
EntryPoints: []string{"http", "https"},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-sauternes-test1-foobar": {
Rule: "Path:/sauternes",
},
},
},
"frontend-arbois-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 3000,
EntryPoints: []string{"http", "https"},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-arbois-test1-foobar": {
Rule: "Path:/arbois",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test1-foobar": {
Servers: map[string]types.Server{
"server-test1-123456789abc-79533a101142718f0fdf84c42593c41e": {
URL: "https://127.0.0.1:2503",
Weight: 80,
},
"server-test1-123456789abc-315a41140f1bd825b066e39686c18482": {
URL: "https://127.0.0.1:2504",
Weight: 90,
},
},
CircuitBreaker: nil,
},
},
},
}
provider := &Provider{
Domain: "ecs.localhost",
ExposedByDefault: true,
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actualConfig, err := provider.buildConfiguration(test.instanceInfo)
assert.NoError(t, err)
require.NotNil(t, actualConfig, "actualConfig")
assert.EqualValues(t, test.expectedBackends, actualConfig.Backends)
assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends)
})
}
}

File diff suppressed because it is too large Load diff

432
old/provider/ecs/ecs.go Normal file
View file

@ -0,0 +1,432 @@
package ecs
import (
"context"
"fmt"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/defaults"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/cenk/backoff"
"github.com/containous/traefik/job"
"github.com/containous/traefik/old/log"
"github.com/containous/traefik/old/provider"
"github.com/containous/traefik/old/types"
"github.com/containous/traefik/safe"
)
var _ provider.Provider = (*Provider)(nil)
// Provider holds configurations of the provider.
type Provider struct {
provider.BaseProvider `mapstructure:",squash" export:"true"`
Domain string `description:"Default domain used"`
ExposedByDefault bool `description:"Expose containers by default" export:"true"`
RefreshSeconds int `description:"Polling interval (in seconds)" export:"true"`
// Provider lookup parameters
Clusters Clusters `description:"ECS Clusters name"`
AutoDiscoverClusters bool `description:"Auto discover cluster" export:"true"`
Region string `description:"The AWS region to use for requests" export:"true"`
AccessKeyID string `description:"The AWS credentials access key to use for making requests"`
SecretAccessKey string `description:"The AWS credentials access key to use for making requests"`
}
type ecsInstance struct {
Name string
ID string
containerDefinition *ecs.ContainerDefinition
machine *machine
TraefikLabels map[string]string
SegmentLabels map[string]string
SegmentName string
}
type portMapping struct {
containerPort int64
hostPort int64
}
type machine struct {
state string
privateIP string
ports []portMapping
}
type awsClient struct {
ecs *ecs.ECS
ec2 *ec2.EC2
}
// Init the provider
func (p *Provider) Init(constraints types.Constraints) error {
return p.BaseProvider.Init(constraints)
}
func (p *Provider) createClient() (*awsClient, error) {
sess, err := session.NewSession()
if err != nil {
return nil, err
}
ec2meta := ec2metadata.New(sess)
if p.Region == "" {
log.Infoln("No EC2 region provided, querying instance metadata endpoint...")
identity, err := ec2meta.GetInstanceIdentityDocument()
if err != nil {
return nil, err
}
p.Region = identity.Region
}
cfg := &aws.Config{
Region: &p.Region,
Credentials: credentials.NewChainCredentials(
[]credentials.Provider{
&credentials.StaticProvider{
Value: credentials.Value{
AccessKeyID: p.AccessKeyID,
SecretAccessKey: p.SecretAccessKey,
},
},
&credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{},
defaults.RemoteCredProvider(*(defaults.Config()), defaults.Handlers()),
}),
}
if p.Trace {
cfg.WithLogger(aws.LoggerFunc(func(args ...interface{}) {
log.Debug(args...)
}))
}
return &awsClient{
ecs: ecs.New(sess, cfg),
ec2: ec2.New(sess, cfg),
}, nil
}
// 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) error {
handleCanceled := func(ctx context.Context, err error) error {
if ctx.Err() == context.Canceled || err == context.Canceled {
return nil
}
return err
}
pool.Go(func(stop chan bool) {
ctx, cancel := context.WithCancel(context.Background())
safe.Go(func() {
<-stop
cancel()
})
operation := func() error {
awsClient, err := p.createClient()
if err != nil {
return err
}
configuration, err := p.loadECSConfig(ctx, awsClient)
if err != nil {
return handleCanceled(ctx, err)
}
configurationChan <- types.ConfigMessage{
ProviderName: "ecs",
Configuration: configuration,
}
if p.Watch {
reload := time.NewTicker(time.Second * time.Duration(p.RefreshSeconds))
defer reload.Stop()
for {
select {
case <-reload.C:
configuration, err := p.loadECSConfig(ctx, awsClient)
if err != nil {
return handleCanceled(ctx, err)
}
configurationChan <- types.ConfigMessage{
ProviderName: "ecs",
Configuration: configuration,
}
case <-ctx.Done():
return handleCanceled(ctx, ctx.Err())
}
}
}
return nil
}
notify := func(err error, time time.Duration) {
log.Errorf("Provider connection error %+v, retrying in %s", err, time)
}
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
if err != nil {
log.Errorf("Cannot connect to Provider api %+v", err)
}
})
return nil
}
// 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) {
var clustersArn []*string
var clusters Clusters
if p.AutoDiscoverClusters {
input := &ecs.ListClustersInput{}
for {
result, err := client.ecs.ListClusters(input)
if err != nil {
return nil, err
}
if result != nil {
clustersArn = append(clustersArn, result.ClusterArns...)
input.NextToken = result.NextToken
if result.NextToken == nil {
break
}
} else {
break
}
}
for _, cArn := range clustersArn {
clusters = append(clusters, *cArn)
}
} else {
clusters = p.Clusters
}
var instances []ecsInstance
log.Debugf("ECS Clusters: %s", clusters)
for _, c := range clusters {
input := &ecs.ListTasksInput{
Cluster: &c,
DesiredStatus: aws.String(ecs.DesiredStatusRunning),
}
tasks := make(map[string]*ecs.Task)
err := client.ecs.ListTasksPagesWithContext(ctx, input, func(page *ecs.ListTasksOutput, lastPage bool) bool {
if len(page.TaskArns) > 0 {
resp, err := client.ecs.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{
Tasks: page.TaskArns,
Cluster: &c,
})
if err != nil {
log.Errorf("Unable to describe tasks for %v", page.TaskArns)
} else {
for _, t := range resp.Tasks {
tasks[aws.StringValue(t.TaskArn)] = t
}
}
}
return !lastPage
})
if err != nil {
log.Error("Unable to list tasks")
return nil, err
}
// Skip to the next cluster if there are no tasks found on
// this cluster.
if len(tasks) == 0 {
continue
}
ec2Instances, err := p.lookupEc2Instances(ctx, client, &c, tasks)
if err != nil {
return nil, err
}
taskDefinitions, err := p.lookupTaskDefinitions(ctx, client, tasks)
if err != nil {
return nil, err
}
for key, task := range tasks {
containerInstance := ec2Instances[aws.StringValue(task.ContainerInstanceArn)]
taskDef := taskDefinitions[key]
for _, container := range task.Containers {
var containerDefinition *ecs.ContainerDefinition
for _, def := range taskDef.ContainerDefinitions {
if aws.StringValue(container.Name) == aws.StringValue(def.Name) {
containerDefinition = def
break
}
}
if containerDefinition == nil {
log.Debugf("Unable to find container definition for %s", aws.StringValue(container.Name))
continue
}
var mach *machine
if aws.StringValue(task.LaunchType) == ecs.LaunchTypeFargate {
var ports []portMapping
for _, mapping := range containerDefinition.PortMappings {
if mapping != nil {
ports = append(ports, portMapping{
hostPort: aws.Int64Value(mapping.HostPort),
containerPort: aws.Int64Value(mapping.ContainerPort),
})
}
}
mach = &machine{
privateIP: aws.StringValue(container.NetworkInterfaces[0].PrivateIpv4Address),
ports: ports,
state: aws.StringValue(task.LastStatus),
}
} else {
var ports []portMapping
for _, mapping := range container.NetworkBindings {
if mapping != nil {
ports = append(ports, portMapping{
hostPort: aws.Int64Value(mapping.HostPort),
containerPort: aws.Int64Value(mapping.ContainerPort),
})
}
}
mach = &machine{
privateIP: aws.StringValue(containerInstance.PrivateIpAddress),
ports: ports,
state: aws.StringValue(containerInstance.State.Name),
}
}
instances = append(instances, ecsInstance{
Name: fmt.Sprintf("%s-%s", strings.Replace(aws.StringValue(task.Group), ":", "-", 1), *container.Name),
ID: key[len(key)-12:],
containerDefinition: containerDefinition,
machine: mach,
TraefikLabels: aws.StringValueMap(containerDefinition.DockerLabels),
})
}
}
}
return instances, nil
}
func (p *Provider) lookupEc2Instances(ctx context.Context, client *awsClient, clusterName *string, ecsDatas map[string]*ecs.Task) (map[string]*ec2.Instance, error) {
instanceIds := make(map[string]string)
ec2Instances := make(map[string]*ec2.Instance)
var containerInstancesArns []*string
var instanceArns []*string
for _, task := range ecsDatas {
if task.ContainerInstanceArn != nil {
containerInstancesArns = append(containerInstancesArns, task.ContainerInstanceArn)
}
}
for _, arns := range p.chunkIDs(containerInstancesArns) {
resp, err := client.ecs.DescribeContainerInstancesWithContext(ctx, &ecs.DescribeContainerInstancesInput{
ContainerInstances: arns,
Cluster: clusterName,
})
if err != nil {
log.Errorf("Unable to describe container instances: %v", err)
return nil, err
}
for _, container := range resp.ContainerInstances {
instanceIds[aws.StringValue(container.Ec2InstanceId)] = aws.StringValue(container.ContainerInstanceArn)
instanceArns = append(instanceArns, container.Ec2InstanceId)
}
}
if len(instanceArns) > 0 {
for _, ids := range p.chunkIDs(instanceArns) {
input := &ec2.DescribeInstancesInput{
InstanceIds: ids,
}
err := client.ec2.DescribeInstancesPagesWithContext(ctx, input, func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
if len(page.Reservations) > 0 {
for _, r := range page.Reservations {
for _, i := range r.Instances {
if i.InstanceId != nil {
ec2Instances[instanceIds[aws.StringValue(i.InstanceId)]] = i
}
}
}
}
return !lastPage
})
if err != nil {
log.Errorf("Unable to describe instances: %v", err)
return nil, err
}
}
}
return ec2Instances, nil
}
func (p *Provider) lookupTaskDefinitions(ctx context.Context, client *awsClient, taskDefArns map[string]*ecs.Task) (map[string]*ecs.TaskDefinition, error) {
taskDef := make(map[string]*ecs.TaskDefinition)
for arn, task := range taskDefArns {
resp, err := client.ecs.DescribeTaskDefinitionWithContext(ctx, &ecs.DescribeTaskDefinitionInput{
TaskDefinition: task.TaskDefinitionArn,
})
if err != nil {
log.Errorf("Unable to describe task definition: %s", err)
return nil, err
}
taskDef[arn] = resp.TaskDefinition
}
return taskDef, nil
}
func (p *Provider) loadECSConfig(ctx context.Context, client *awsClient) (*types.Configuration, error) {
instances, err := p.listInstances(ctx, client)
if err != nil {
return nil, err
}
return p.buildConfiguration(instances)
}
// chunkIDs ECS expects no more than 100 parameters be passed to a API call;
// thus, pack each string into an array capped at 100 elements
func (p *Provider) chunkIDs(ids []*string) [][]*string {
var chuncked [][]*string
for i := 0; i < len(ids); i += 100 {
var sliceEnd int
if i+100 < len(ids) {
sliceEnd = i + 100
} else {
sliceEnd = len(ids)
}
chuncked = append(chuncked, ids[i:sliceEnd])
}
return chuncked
}

View file

@ -0,0 +1,88 @@
package ecs
import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/stretchr/testify/assert"
)
func TestChunkIDs(t *testing.T) {
provider := &Provider{}
testCases := []struct {
desc string
count int
expected []int
}{
{
desc: "0 element",
count: 0,
expected: []int(nil),
},
{
desc: "1 element",
count: 1,
expected: []int{1},
},
{
desc: "99 elements, 1 chunk",
count: 99,
expected: []int{99},
},
{
desc: "100 elements, 1 chunk",
count: 100,
expected: []int{100},
},
{
desc: "101 elements, 2 chunks",
count: 101,
expected: []int{100, 1},
},
{
desc: "199 elements, 2 chunks",
count: 199,
expected: []int{100, 99},
},
{
desc: "200 elements, 2 chunks",
count: 200,
expected: []int{100, 100},
},
{
desc: "201 elements, 3 chunks",
count: 201,
expected: []int{100, 100, 1},
},
{
desc: "555 elements, 5 chunks",
count: 555,
expected: []int{100, 100, 100, 100, 100, 55},
},
{
desc: "1001 elements, 11 chunks",
count: 1001,
expected: []int{100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 1},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var IDs []*string
for v := 0; v < test.count; v++ {
IDs = append(IDs, aws.String("a"))
}
var outCount []int
for _, el := range provider.chunkIDs(IDs) {
outCount = append(outCount, len(el))
}
assert.Equal(t, test.expected, outCount)
})
}
}