1
0
Fork 0

Add Rancher provider again

This commit is contained in:
Manuel Zapf 2019-04-05 12:22:04 +02:00 committed by Traefiker Bot
parent ed12366d52
commit e1d097ea20
31 changed files with 2585 additions and 295 deletions

View file

@ -44,6 +44,9 @@ func NewProviderAggregator(conf static.Providers) ProviderAggregator {
if conf.KubernetesCRD != nil {
p.quietAddProvider(conf.KubernetesCRD)
}
if conf.Rancher != nil {
p.quietAddProvider(conf.Rancher)
}
return p
}

View file

@ -0,0 +1,237 @@
package rancher
import (
"context"
"errors"
"fmt"
"net"
"strings"
"github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/log"
"github.com/containous/traefik/pkg/provider"
"github.com/containous/traefik/pkg/provider/label"
)
func (p *Provider) buildConfiguration(ctx context.Context, services []rancherData) *config.Configuration {
configurations := make(map[string]*config.Configuration)
for _, service := range services {
ctxService := log.With(ctx, log.Str("service", service.Name))
if !p.keepService(ctx, service) {
continue
}
logger := log.FromContext(ctxService)
confFromLabel, err := label.DecodeConfiguration(service.Labels)
if err != nil {
logger.Error(err)
continue
}
if len(confFromLabel.TCP.Routers) > 0 || len(confFromLabel.TCP.Services) > 0 {
err := p.buildTCPServiceConfiguration(ctxService, service, confFromLabel.TCP)
if err != nil {
logger.Error(err)
continue
}
provider.BuildTCPRouterConfiguration(ctxService, confFromLabel.TCP)
if len(confFromLabel.HTTP.Routers) == 0 &&
len(confFromLabel.HTTP.Middlewares) == 0 &&
len(confFromLabel.HTTP.Services) == 0 {
configurations[service.Name] = confFromLabel
continue
}
}
err = p.buildServiceConfiguration(ctx, service, confFromLabel.HTTP)
if err != nil {
logger.Error(err)
continue
}
model := struct {
Name string
Labels map[string]string
}{
Name: service.Name,
Labels: service.Labels,
}
provider.BuildRouterConfiguration(ctx, confFromLabel.HTTP, service.Name, p.defaultRuleTpl, model)
configurations[service.Name] = confFromLabel
}
return provider.Merge(ctx, configurations)
}
func (p *Provider) buildTCPServiceConfiguration(ctx context.Context, service rancherData, configuration *config.TCPConfiguration) error {
serviceName := service.Name
if len(configuration.Services) == 0 {
configuration.Services = make(map[string]*config.TCPService)
lb := &config.TCPLoadBalancerService{}
lb.SetDefaults()
configuration.Services[serviceName] = &config.TCPService{
LoadBalancer: lb,
}
}
for _, confService := range configuration.Services {
err := p.addServerTCP(ctx, service, confService.LoadBalancer)
if err != nil {
return err
}
}
return nil
}
func (p *Provider) buildServiceConfiguration(ctx context.Context, service rancherData, configuration *config.HTTPConfiguration) error {
serviceName := service.Name
if len(configuration.Services) == 0 {
configuration.Services = make(map[string]*config.Service)
lb := &config.LoadBalancerService{}
lb.SetDefaults()
configuration.Services[serviceName] = &config.Service{
LoadBalancer: lb,
}
}
for _, confService := range configuration.Services {
err := p.addServers(ctx, service, confService.LoadBalancer)
if err != nil {
return err
}
}
return nil
}
func (p *Provider) keepService(ctx context.Context, service rancherData) bool {
logger := log.FromContext(ctx)
if !service.ExtraConf.Enable {
logger.Debug("Filtering disabled service.")
return false
}
if ok, failingConstraint := p.MatchConstraints(service.ExtraConf.Tags); !ok {
if failingConstraint != nil {
logger.Debugf("service pruned by %q constraint", failingConstraint.String())
}
return false
}
if p.EnableServiceHealthFilter {
if service.Health != "" && service.Health != healthy && service.Health != updatingHealthy {
logger.Debugf("Filtering service %s with healthState of %s \n", service.Name, service.Health)
return false
}
if service.State != "" && service.State != active && service.State != updatingActive && service.State != upgraded && service.State != upgrading {
logger.Debugf("Filtering service %s with state of %s \n", service.Name, service.State)
return false
}
}
return true
}
func (p *Provider) addServerTCP(ctx context.Context, service rancherData, loadBalancer *config.TCPLoadBalancerService) error {
log.FromContext(ctx).Debugf("Trying to add servers for service %s \n", service.Name)
serverPort := ""
if loadBalancer != nil && len(loadBalancer.Servers) > 0 {
serverPort = loadBalancer.Servers[0].Port
}
port := getServicePort(service)
if len(loadBalancer.Servers) == 0 {
server := config.TCPServer{}
server.SetDefaults()
loadBalancer.Servers = []config.TCPServer{server}
}
if serverPort != "" {
port = serverPort
loadBalancer.Servers[0].Port = ""
}
if port == "" {
return errors.New("port is missing")
}
var servers []config.TCPServer
for _, containerIP := range service.Containers {
servers = append(servers, config.TCPServer{
Address: net.JoinHostPort(containerIP, port),
Weight: 1,
})
}
loadBalancer.Servers = servers
return nil
}
func (p *Provider) addServers(ctx context.Context, service rancherData, loadBalancer *config.LoadBalancerService) error {
log.FromContext(ctx).Debugf("Trying to add servers for service %s \n", service.Name)
serverPort := getLBServerPort(loadBalancer)
port := getServicePort(service)
if len(loadBalancer.Servers) == 0 {
server := config.Server{}
server.SetDefaults()
loadBalancer.Servers = []config.Server{server}
}
if serverPort != "" {
port = serverPort
loadBalancer.Servers[0].Port = ""
}
if port == "" {
return errors.New("port is missing")
}
var servers []config.Server
for _, containerIP := range service.Containers {
servers = append(servers, config.Server{
URL: fmt.Sprintf("%s://%s", loadBalancer.Servers[0].Scheme, net.JoinHostPort(containerIP, port)),
Weight: 1,
})
}
loadBalancer.Servers = servers
return nil
}
func getLBServerPort(loadBalancer *config.LoadBalancerService) string {
if loadBalancer != nil && len(loadBalancer.Servers) > 0 {
return loadBalancer.Servers[0].Port
}
return ""
}
func getServicePort(data rancherData) string {
rawPort := strings.Split(data.Port, "/")[0]
hostPort := strings.Split(rawPort, ":")
if len(hostPort) >= 2 {
return hostPort[1]
}
if len(hostPort) > 0 && hostPort[0] != "" {
return hostPort[0]
}
return rawPort
}

View file

@ -0,0 +1,788 @@
package rancher
import (
"context"
"testing"
"github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_buildConfiguration(t *testing.T) {
testCases := []struct {
desc string
containers []rancherData
constraints types.Constraints
expected *config.Configuration
}{
{
desc: "one service no label",
containers: []rancherData{
{
Name: "Test",
Labels: map[string]string{},
Port: "80/tcp",
Containers: []string{"127.0.0.1"},
Health: "",
State: "",
},
},
expected: &config.Configuration{
TCP: &config.TCPConfiguration{
Routers: map[string]*config.TCPRouter{},
Services: map[string]*config.TCPService{},
},
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{
"Test": {
Service: "Test",
Rule: "Host(`Test.traefik.wtf`)",
},
},
Middlewares: map[string]*config.Middleware{},
Services: map[string]*config.Service{
"Test": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1:80",
Weight: 1,
},
},
Method: "wrr",
PassHostHeader: true,
},
},
},
},
},
},
{
desc: "two services no label",
containers: []rancherData{
{
Name: "Test1",
Labels: map[string]string{},
Port: "80/tcp",
Containers: []string{"127.0.0.1"},
Health: "",
State: "",
},
{
Name: "Test2",
Labels: map[string]string{},
Port: "80/tcp",
Containers: []string{"127.0.0.2"},
Health: "",
State: "",
},
},
expected: &config.Configuration{
TCP: &config.TCPConfiguration{
Routers: map[string]*config.TCPRouter{},
Services: map[string]*config.TCPService{},
},
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{
"Test1": {
Service: "Test1",
Rule: "Host(`Test1.traefik.wtf`)",
},
"Test2": {
Service: "Test2",
Rule: "Host(`Test2.traefik.wtf`)",
},
},
Middlewares: map[string]*config.Middleware{},
Services: map[string]*config.Service{
"Test1": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1:80",
Weight: 1,
},
},
Method: "wrr",
PassHostHeader: true,
},
},
"Test2": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.2:80",
Weight: 1,
},
},
Method: "wrr",
PassHostHeader: true,
},
},
},
},
},
},
{
desc: "two services no label multiple containers",
containers: []rancherData{
{
Name: "Test1",
Labels: map[string]string{},
Port: "80/tcp",
Containers: []string{"127.0.0.1", "127.0.0.2"},
Health: "",
State: "",
},
{
Name: "Test2",
Labels: map[string]string{},
Port: "80/tcp",
Containers: []string{"128.0.0.1"},
Health: "",
State: "",
},
},
expected: &config.Configuration{
TCP: &config.TCPConfiguration{
Routers: map[string]*config.TCPRouter{},
Services: map[string]*config.TCPService{},
},
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{
"Test1": {
Service: "Test1",
Rule: "Host(`Test1.traefik.wtf`)",
},
"Test2": {
Service: "Test2",
Rule: "Host(`Test2.traefik.wtf`)",
},
},
Middlewares: map[string]*config.Middleware{},
Services: map[string]*config.Service{
"Test1": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1:80",
Weight: 1,
},
{
URL: "http://127.0.0.2:80",
Weight: 1,
},
},
Method: "wrr",
PassHostHeader: true,
},
},
"Test2": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://128.0.0.1:80",
Weight: 1,
},
},
Method: "wrr",
PassHostHeader: true,
},
},
},
},
},
},
{
desc: "one service some labels",
containers: []rancherData{
{
Name: "Test",
Labels: map[string]string{
"traefik.http.services.Service1.loadbalancer.method": "wrr",
"traefik.http.routers.Router1.rule": "Host(`foo.com`)",
"traefik.http.routers.Router1.service": "Service1",
},
Port: "80/tcp",
Containers: []string{"127.0.0.1"},
Health: "",
State: "",
},
},
expected: &config.Configuration{
TCP: &config.TCPConfiguration{
Routers: map[string]*config.TCPRouter{},
Services: map[string]*config.TCPService{},
},
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{
"Router1": {
Service: "Service1",
Rule: "Host(`foo.com`)",
},
},
Middlewares: map[string]*config.Middleware{},
Services: map[string]*config.Service{
"Service1": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1:80",
Weight: 1,
},
},
Method: "wrr",
PassHostHeader: true,
},
},
},
},
},
},
{
desc: "one service which is unhealthy",
containers: []rancherData{
{
Name: "Test",
Port: "80/tcp",
Containers: []string{"127.0.0.1"},
Health: "broken",
State: "",
},
},
expected: &config.Configuration{
TCP: &config.TCPConfiguration{
Routers: map[string]*config.TCPRouter{},
Services: map[string]*config.TCPService{},
},
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{},
Middlewares: map[string]*config.Middleware{},
Services: map[string]*config.Service{},
},
},
},
{
desc: "one service which is upgrading",
containers: []rancherData{
{
Name: "Test",
Port: "80/tcp",
Containers: []string{"127.0.0.1"},
Health: "",
State: "upgradefailed",
},
},
expected: &config.Configuration{
TCP: &config.TCPConfiguration{
Routers: map[string]*config.TCPRouter{},
Services: map[string]*config.TCPService{},
},
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{},
Middlewares: map[string]*config.Middleware{},
Services: map[string]*config.Service{},
},
},
},
{
desc: "one service with rule label and has a host exposed port",
containers: []rancherData{
{
Name: "Test",
Labels: map[string]string{
"traefik.http.routers.Router1.rule": "Host(`foo.com`)",
},
Port: "12345:80/tcp",
Containers: []string{"127.0.0.1"},
Health: "",
State: "",
},
},
expected: &config.Configuration{
TCP: &config.TCPConfiguration{
Routers: map[string]*config.TCPRouter{},
Services: map[string]*config.TCPService{},
},
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{
"Router1": {
Service: "Test",
Rule: "Host(`foo.com`)",
},
},
Middlewares: map[string]*config.Middleware{},
Services: map[string]*config.Service{
"Test": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1:80",
Weight: 1,
},
},
Method: "wrr",
PassHostHeader: true,
},
},
},
},
},
},
{
desc: "one service with non matching constraints",
containers: []rancherData{
{
Name: "Test",
Labels: map[string]string{
"traefik.http.routers.Router1.rule": "Host(`foo.com`)",
},
Port: "12345:80/tcp",
Containers: []string{"127.0.0.1"},
Health: "",
State: "",
},
},
constraints: types.Constraints{
&types.Constraint{
Key: "tag",
MustMatch: true,
Regex: "bar",
},
},
expected: &config.Configuration{
TCP: &config.TCPConfiguration{
Routers: map[string]*config.TCPRouter{},
Services: map[string]*config.TCPService{},
},
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{},
Middlewares: map[string]*config.Middleware{},
Services: map[string]*config.Service{},
},
},
},
{
desc: "one service with matching constraints",
containers: []rancherData{
{
Name: "Test",
Labels: map[string]string{
"traefik.tags": "foo",
},
Port: "80/tcp",
Containers: []string{"127.0.0.1"},
Health: "",
State: "",
},
},
constraints: types.Constraints{
&types.Constraint{
Key: "tag",
MustMatch: true,
Regex: "foo",
},
},
expected: &config.Configuration{
TCP: &config.TCPConfiguration{
Routers: map[string]*config.TCPRouter{},
Services: map[string]*config.TCPService{},
},
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{
"Test": {
Service: "Test",
Rule: "Host(`Test.traefik.wtf`)",
},
},
Middlewares: map[string]*config.Middleware{},
Services: map[string]*config.Service{
"Test": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1:80",
Weight: 1,
},
},
Method: "wrr",
PassHostHeader: true,
},
},
},
},
},
},
{
desc: "Middlewares used in router",
containers: []rancherData{
{
Name: "Test",
Labels: map[string]string{
"traefik.http.middlewares.Middleware1.basicauth.users": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
"traefik.http.routers.Test.middlewares": "Middleware1",
},
Port: "80/tcp",
Containers: []string{"127.0.0.1"},
Health: "",
State: "",
},
},
expected: &config.Configuration{
TCP: &config.TCPConfiguration{
Routers: map[string]*config.TCPRouter{},
Services: map[string]*config.TCPService{},
},
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{
"Test": {
Service: "Test",
Rule: "Host(`Test.traefik.wtf`)",
Middlewares: []string{"Middleware1"},
},
},
Middlewares: map[string]*config.Middleware{
"Middleware1": {
BasicAuth: &config.BasicAuth{
Users: []string{
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
},
},
},
},
Services: map[string]*config.Service{
"Test": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1:80",
Weight: 1,
},
},
Method: "wrr",
PassHostHeader: true,
},
},
},
},
},
},
{
desc: "Port in labels",
containers: []rancherData{
{
Name: "Test",
Labels: map[string]string{
"traefik.http.services.Test.loadbalancer.server.port": "80",
},
Port: "",
Containers: []string{"127.0.0.1"},
Health: "",
State: "",
},
},
expected: &config.Configuration{
TCP: &config.TCPConfiguration{
Routers: map[string]*config.TCPRouter{},
Services: map[string]*config.TCPService{},
},
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{
"Test": {
Service: "Test",
Rule: "Host(`Test.traefik.wtf`)",
},
},
Middlewares: map[string]*config.Middleware{},
Services: map[string]*config.Service{
"Test": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1:80",
Weight: 1,
},
},
Method: "wrr",
PassHostHeader: true,
},
},
},
},
},
},
{
desc: "tcp with label",
containers: []rancherData{
{
Name: "Test",
Labels: map[string]string{
"traefik.tcp.routers.foo.rule": "HostSNI(`foo.bar`)",
"traefik.tcp.routers.foo.tls": "true",
},
Port: "80/tcp",
Containers: []string{"127.0.0.1"},
Health: "",
State: "",
},
},
expected: &config.Configuration{
TCP: &config.TCPConfiguration{
Routers: map[string]*config.TCPRouter{
"foo": {
Service: "Test",
Rule: "HostSNI(`foo.bar`)",
TLS: &config.RouterTCPTLSConfig{},
},
},
Services: map[string]*config.TCPService{
"Test": {
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.1:80",
Weight: 1,
},
},
Method: "wrr",
},
},
},
},
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{},
Middlewares: map[string]*config.Middleware{},
Services: map[string]*config.Service{},
},
},
},
{
desc: "tcp with label without rule",
containers: []rancherData{
{
Name: "Test",
Labels: map[string]string{
"traefik.tcp.routers.foo.tls": "true",
},
Port: "80/tcp",
Containers: []string{"127.0.0.1"},
Health: "",
State: "",
},
},
expected: &config.Configuration{
TCP: &config.TCPConfiguration{
Routers: map[string]*config.TCPRouter{},
Services: map[string]*config.TCPService{
"Test": {
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.1:80",
Weight: 1,
},
},
Method: "wrr",
},
},
},
},
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{},
Middlewares: map[string]*config.Middleware{},
Services: map[string]*config.Service{},
},
},
},
{
desc: "tcp with label and port",
containers: []rancherData{
{
Name: "Test",
Labels: map[string]string{
"traefik.tcp.routers.foo.rule": "HostSNI(`foo.bar`)",
"traefik.tcp.routers.foo.tls": "true",
"traefik.tcp.services.foo.loadbalancer.server.port": "8080",
},
Port: "80/tcp",
Containers: []string{"127.0.0.1"},
Health: "",
State: "",
},
},
expected: &config.Configuration{
TCP: &config.TCPConfiguration{
Routers: map[string]*config.TCPRouter{
"foo": {
Service: "foo",
Rule: "HostSNI(`foo.bar`)",
TLS: &config.RouterTCPTLSConfig{},
},
},
Services: map[string]*config.TCPService{
"foo": {
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.1:8080",
Weight: 1,
},
},
Method: "wrr",
},
},
},
},
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{},
Middlewares: map[string]*config.Middleware{},
Services: map[string]*config.Service{},
},
},
},
{
desc: "tcp with label and port and http service",
containers: []rancherData{
{
Name: "Test",
Labels: map[string]string{
"traefik.tcp.routers.foo.rule": "HostSNI(`foo.bar`)",
"traefik.tcp.routers.foo.tls": "true",
"traefik.tcp.services.foo.loadbalancer.server.port": "8080",
"traefik.http.services.Service1.loadbalancer.method": "drr",
},
Port: "80/tcp",
Containers: []string{"127.0.0.1", "127.0.0.2"},
Health: "",
State: "",
},
},
expected: &config.Configuration{
TCP: &config.TCPConfiguration{
Routers: map[string]*config.TCPRouter{
"foo": {
Service: "foo",
Rule: "HostSNI(`foo.bar`)",
TLS: &config.RouterTCPTLSConfig{},
},
},
Services: map[string]*config.TCPService{
"foo": {
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.1:8080",
Weight: 1,
},
{
Address: "127.0.0.2:8080",
Weight: 1,
},
},
Method: "wrr",
},
},
},
},
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{
"Test": {
Service: "Service1",
Rule: "Host(`Test.traefik.wtf`)",
},
},
Middlewares: map[string]*config.Middleware{},
Services: map[string]*config.Service{
"Service1": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1:80",
Weight: 1,
},
{
URL: "http://127.0.0.2:80",
Weight: 1,
},
},
Method: "drr",
PassHostHeader: true,
},
},
},
},
},
},
{
desc: "tcp with label for tcp service",
containers: []rancherData{
{
Name: "Test",
Labels: map[string]string{
"traefik.tcp.services.foo.loadbalancer.server.port": "8080",
},
Port: "80/tcp",
Containers: []string{"127.0.0.1"},
Health: "",
State: "",
},
},
expected: &config.Configuration{
TCP: &config.TCPConfiguration{
Routers: map[string]*config.TCPRouter{},
Services: map[string]*config.TCPService{
"foo": {
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.1:8080",
Weight: 1,
},
},
Method: "wrr",
},
},
},
},
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{},
Middlewares: map[string]*config.Middleware{},
Services: map[string]*config.Service{},
},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
p := Provider{
ExposedByDefault: true,
DefaultRule: "Host(`{{ normalize .Name }}.traefik.wtf`)",
EnableServiceHealthFilter: true,
}
p.Constraints = test.constraints
err := p.Init()
require.NoError(t, err)
for i := 0; i < len(test.containers); i++ {
var err error
test.containers[i].ExtraConf, err = p.getConfiguration(test.containers[i])
require.NoError(t, err)
}
configuration := p.buildConfiguration(context.Background(), test.containers)
assert.Equal(t, test.expected, configuration)
})
}
}

View file

@ -0,0 +1,23 @@
package rancher
import (
"github.com/containous/traefik/pkg/provider/label"
)
type configuration struct {
Enable bool
Tags []string
}
func (p *Provider) getConfiguration(service rancherData) (configuration, error) {
conf := configuration{
Enable: p.ExposedByDefault,
}
err := label.Decode(service.Labels, &conf, "traefik.rancher.", "traefik.enable", "traefik.tags")
if err != nil {
return configuration{}, err
}
return conf, nil
}

View file

@ -0,0 +1,221 @@
package rancher
import (
"context"
"fmt"
"text/template"
"time"
"github.com/cenkalti/backoff"
"github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/job"
"github.com/containous/traefik/pkg/log"
"github.com/containous/traefik/pkg/provider"
"github.com/containous/traefik/pkg/safe"
rancher "github.com/rancher/go-rancher-metadata/metadata"
)
const (
// DefaultTemplateRule The default template for the default rule.
DefaultTemplateRule = "Host(`{{ normalize .Name }}`)"
)
// Health
const (
healthy = "healthy"
updatingHealthy = "updating-healthy"
)
// State
const (
active = "active"
running = "running"
upgraded = "upgraded"
upgrading = "upgrading"
updatingActive = "updating-active"
updatingRunning = "updating-running"
)
var _ provider.Provider = (*Provider)(nil)
// Provider holds configurations of the provider.
type Provider struct {
provider.Constrainer `mapstructure:",squash" export:"true"`
Watch bool `description:"Watch provider" export:"true"`
DefaultRule string `description:"Default rule"`
ExposedByDefault bool `description:"Expose containers by default" export:"true"`
EnableServiceHealthFilter bool
RefreshSeconds int
defaultRuleTpl *template.Template
IntervalPoll bool `description:"Poll the Rancher metadata service every 'rancher.refreshseconds' (less accurate)"`
Prefix string `description:"Prefix used for accessing the Rancher metadata service"`
}
type rancherData struct {
Name string
Labels map[string]string
Containers []string
Health string
State string
Port string
ExtraConf configuration
}
// Init the provider.
func (p *Provider) Init() error {
defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil)
if err != nil {
return fmt.Errorf("error while parsing default rule: %v", err)
}
p.defaultRuleTpl = defaultRuleTpl
return nil
}
func (p *Provider) createClient(ctx context.Context) (rancher.Client, error) {
metadataServiceURL := fmt.Sprintf("http://rancher-metadata.rancher.internal/%s", p.Prefix)
client, err := rancher.NewClientAndWait(metadataServiceURL)
if err != nil {
log.FromContext(ctx).Errorf("Failed to create Rancher metadata service client: %v", err)
return nil, err
}
return client, nil
}
// Provide allows the rancher provider to provide configurations to traefik using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- config.Message, pool *safe.Pool) error {
pool.GoCtx(func(routineCtx context.Context) {
ctxLog := log.With(routineCtx, log.Str(log.ProviderName, "rancher"))
logger := log.FromContext(ctxLog)
operation := func() error {
client, err := p.createClient(ctxLog)
if err != nil {
logger.Errorf("Failed to create the metadata client metadata service: %v", err)
return err
}
updateConfiguration := func(_ string) {
stacks, err := client.GetStacks()
if err != nil {
logger.Errorf("Failed to query Rancher metadata service: %v", err)
return
}
rancherData := p.parseMetadataSourcedRancherData(ctxLog, stacks)
logger.Printf("Received Rancher data %+v", rancherData)
configuration := p.buildConfiguration(ctxLog, rancherData)
configurationChan <- config.Message{
ProviderName: "rancher",
Configuration: configuration,
}
}
updateConfiguration("init")
if p.Watch {
if p.IntervalPoll {
p.intervalPoll(ctxLog, client, updateConfiguration)
} else {
// Long polling should be favored for the most accurate configuration updates.
// Holds the connection until there is either a change in the metadata repository or `p.RefreshSeconds` has elapsed.
client.OnChangeCtx(ctxLog, p.RefreshSeconds, updateConfiguration)
}
}
return nil
}
notify := func(err error, time time.Duration) {
logger.Errorf("Provider connection error %+v, retrying in %s", err, time)
}
err := backoff.RetryNotify(safe.OperationWithRecover(operation), backoff.WithContext(job.NewBackOff(backoff.NewExponentialBackOff()), ctxLog), notify)
if err != nil {
logger.Errorf("Cannot connect to Provider server: %+v", err)
}
})
return nil
}
func (p *Provider) intervalPoll(ctx context.Context, client rancher.Client, updateConfiguration func(string)) {
ticker := time.NewTicker(time.Second * time.Duration(p.RefreshSeconds))
defer ticker.Stop()
var version string
for {
select {
case <-ticker.C:
newVersion, err := client.GetVersion()
if err != nil {
log.FromContext(ctx).Errorf("Failed to create Rancher metadata service client: %v", err)
} else if version != newVersion {
version = newVersion
updateConfiguration(version)
}
case <-ctx.Done():
return
}
}
}
func (p *Provider) parseMetadataSourcedRancherData(ctx context.Context, stacks []rancher.Stack) (rancherDataList []rancherData) {
for _, stack := range stacks {
for _, service := range stack.Services {
ctxSvc := log.With(ctx, log.Str("stack", stack.Name), log.Str("service", service.Name))
logger := log.FromContext(ctxSvc)
servicePort := ""
if len(service.Ports) > 0 {
servicePort = service.Ports[0]
}
for _, port := range service.Ports {
logger.Debugf("Set Port %s", port)
}
var containerIPAddresses []string
for _, container := range service.Containers {
if containerFilter(ctxSvc, container.Name, container.HealthState, container.State) {
containerIPAddresses = append(containerIPAddresses, container.PrimaryIp)
}
}
service := rancherData{
Name: service.Name + "/" + stack.Name,
State: service.State,
Labels: service.Labels,
Port: servicePort,
Containers: containerIPAddresses,
}
extraConf, err := p.getConfiguration(service)
if err != nil {
logger.Errorf("Skip container %s: %v", service.Name, err)
continue
}
service.ExtraConf = extraConf
rancherDataList = append(rancherDataList, service)
}
}
return rancherDataList
}
func containerFilter(ctx context.Context, name, healthState, state string) bool {
logger := log.FromContext(ctx)
if healthState != "" && healthState != healthy && healthState != updatingHealthy {
logger.Debugf("Filtering container %s with healthState of %s", name, healthState)
return false
}
if state != "" && state != running && state != updatingRunning && state != upgraded {
logger.Debugf("Filtering container %s with state of %s", name, state)
return false
}
return true
}