Adds default rule system on Docker provider.

Co-authored-by: Julien Salleyron <julien@containo.us>
This commit is contained in:
Ludovic Fernandez 2019-01-21 19:06:02 +01:00 committed by Traefiker Bot
parent b54c956c5e
commit 04958c6951
20 changed files with 506 additions and 168 deletions

View file

@ -155,6 +155,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
defaultDocker.Endpoint = "unix:///var/run/docker.sock" defaultDocker.Endpoint = "unix:///var/run/docker.sock"
defaultDocker.SwarmMode = false defaultDocker.SwarmMode = false
defaultDocker.SwarmModeRefreshSeconds = 15 defaultDocker.SwarmModeRefreshSeconds = 15
defaultDocker.DefaultRule = docker.DefaultTemplateRule
// default Rest // default Rest
var defaultRest rest.Provider var defaultRest rest.Provider

View file

@ -40,7 +40,14 @@ func (s *DockerComposeSuite) TestComposeScale(c *check.C) {
s.composeProject.Scale(c, composeService, serviceCount) s.composeProject.Scale(c, composeService, serviceCount)
file := s.adaptFileForHost(c, "fixtures/docker/minimal.toml") tempObjects := struct {
DockerHost string
DefaultRule string
}{
DockerHost: s.getDockerHost(),
DefaultRule: "Host:{{ normalize .Name }}.docker.localhost",
}
file := s.adaptFile(c, "fixtures/docker/minimal.toml", tempObjects)
defer os.Remove(file) defer os.Remove(file)
cmd, display := s.traefikCmd(withConfigFile(file)) cmd, display := s.traefikCmd(withConfigFile(file))

View file

@ -79,11 +79,19 @@ func (s *DockerSuite) SetUpSuite(c *check.C) {
} }
func (s *DockerSuite) TearDownTest(c *check.C) { func (s *DockerSuite) TearDownTest(c *check.C) {
s.project.Clean(c, os.Getenv("CIRCLECI") != "") s.project.Clean(c, os.Getenv("CIRCLECI") != "") // FIXME
} }
func (s *DockerSuite) TestSimpleConfiguration(c *check.C) { func (s *DockerSuite) TestSimpleConfiguration(c *check.C) {
file := s.adaptFileForHost(c, "fixtures/docker/simple.toml") tempObjects := struct {
DockerHost string
DefaultRule string
}{
DockerHost: s.getDockerHost(),
DefaultRule: "Host:{{ normalize .Name }}.docker.localhost",
}
file := s.adaptFile(c, "fixtures/docker/simple.toml", tempObjects)
defer os.Remove(file) defer os.Remove(file)
cmd, display := s.traefikCmd(withConfigFile(file)) cmd, display := s.traefikCmd(withConfigFile(file))
@ -99,7 +107,15 @@ func (s *DockerSuite) TestSimpleConfiguration(c *check.C) {
} }
func (s *DockerSuite) TestDefaultDockerContainers(c *check.C) { func (s *DockerSuite) TestDefaultDockerContainers(c *check.C) {
file := s.adaptFileForHost(c, "fixtures/docker/simple.toml") tempObjects := struct {
DockerHost string
DefaultRule string
}{
DockerHost: s.getDockerHost(),
DefaultRule: "Host:{{ normalize .Name }}.docker.localhost",
}
file := s.adaptFile(c, "fixtures/docker/simple.toml", tempObjects)
defer os.Remove(file) defer os.Remove(file)
name := s.startContainer(c, "swarm:1.0.0", "manage", "token://blablabla") name := s.startContainer(c, "swarm:1.0.0", "manage", "token://blablabla")
@ -129,7 +145,15 @@ func (s *DockerSuite) TestDefaultDockerContainers(c *check.C) {
} }
func (s *DockerSuite) TestDockerContainersWithLabels(c *check.C) { func (s *DockerSuite) TestDockerContainersWithLabels(c *check.C) {
file := s.adaptFileForHost(c, "fixtures/docker/simple.toml") tempObjects := struct {
DockerHost string
DefaultRule string
}{
DockerHost: s.getDockerHost(),
DefaultRule: "Host:{{ normalize .Name }}.docker.localhost",
}
file := s.adaptFile(c, "fixtures/docker/simple.toml", tempObjects)
defer os.Remove(file) defer os.Remove(file)
// Start a container with some labels // Start a container with some labels
@ -177,7 +201,15 @@ func (s *DockerSuite) TestDockerContainersWithLabels(c *check.C) {
} }
func (s *DockerSuite) TestDockerContainersWithOneMissingLabels(c *check.C) { func (s *DockerSuite) TestDockerContainersWithOneMissingLabels(c *check.C) {
file := s.adaptFileForHost(c, "fixtures/docker/simple.toml") tempObjects := struct {
DockerHost string
DefaultRule string
}{
DockerHost: s.getDockerHost(),
DefaultRule: "Host:{{ normalize .Name }}.docker.localhost",
}
file := s.adaptFile(c, "fixtures/docker/simple.toml", tempObjects)
defer os.Remove(file) defer os.Remove(file)
// Start a container with some labels // Start a container with some labels
@ -199,13 +231,21 @@ func (s *DockerSuite) TestDockerContainersWithOneMissingLabels(c *check.C) {
// FIXME Need to wait than 500 milliseconds more (for swarm or traefik to boot up ?) // FIXME Need to wait than 500 milliseconds more (for swarm or traefik to boot up ?)
// TODO validate : run on 80 // TODO validate : run on 80
// Expected a 404 as we did not comfigure anything // Expected a 404 as we did not configure anything
err = try.Request(req, 1500*time.Millisecond, try.StatusCodeIs(http.StatusNotFound)) err = try.Request(req, 1500*time.Millisecond, try.StatusCodeIs(http.StatusNotFound))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
} }
func (s *DockerSuite) TestRestartDockerContainers(c *check.C) { func (s *DockerSuite) TestRestartDockerContainers(c *check.C) {
file := s.adaptFileForHost(c, "fixtures/docker/simple.toml") tempObjects := struct {
DockerHost string
DefaultRule string
}{
DockerHost: s.getDockerHost(),
DefaultRule: "Host:{{ normalize .Name }}.docker.localhost",
}
file := s.adaptFile(c, "fixtures/docker/simple.toml", tempObjects)
defer os.Remove(file) defer os.Remove(file)
// Start a container with some labels // Start a container with some labels

View file

@ -26,7 +26,6 @@ func (s *ErrorPagesSuite) SetUpSuite(c *check.C) {
} }
func (s *ErrorPagesSuite) TestSimpleConfiguration(c *check.C) { func (s *ErrorPagesSuite) TestSimpleConfiguration(c *check.C) {
file := s.adaptFile(c, "fixtures/error_pages/simple.toml", struct { file := s.adaptFile(c, "fixtures/error_pages/simple.toml", struct {
Server1 string Server1 string
Server2 string Server2 string

View file

@ -25,5 +25,5 @@ checkNewVersion = false
[providers] [providers]
[providers.docker] [providers.docker]
exposedByDefault = false exposedByDefault = false
domain = "docker.local" defaultRule = "{{ normalize .Name }}.docker.local"
watch = true watch = true

View file

@ -10,6 +10,6 @@ logLevel = "DEBUG"
[providers] [providers]
[providers.docker] [providers.docker]
endpoint = "{{.DockerHost}}" endpoint = "{{ .DockerHost }}"
domain = "docker.localhost" defaultRule = "{{ .DefaultRule }}"
exposedByDefault = false exposedByDefault = false

View file

@ -9,6 +9,6 @@ logLevel = "DEBUG"
[providers] [providers]
[providers.docker] [providers.docker]
endpoint = "{{.DockerHost}}" endpoint = "{{ .DockerHost }}"
domain = "docker.localhost" defaultRule = "{{ .DefaultRule }}"
exposedByDefault = true exposedByDefault = true

View file

@ -10,11 +10,9 @@ logLevel = "DEBUG"
[routers] [routers]
[routers.router1] [routers.router1]
middlewares = ["error"]
service = "service1"
[routers.router1.routes.test_1]
rule = "Host:test.local" rule = "Host:test.local"
service = "service1"
middlewares = ["error"]
[middlewares] [middlewares]
[middlewares.error.errors] [middlewares.error.errors]

View file

@ -10,11 +10,9 @@ logLevel = "DEBUG"
[routers] [routers]
[routers.router1] [routers.router1]
middlewares = ["error"]
service = "service1"
[routers.router1.routes.test_1]
rule = "Host:test.local" rule = "Host:test.local"
service = "service1"
middlewares = ["error"]
[middlewares] [middlewares]
[middlewares.error.errors] [middlewares.error.errors]

View file

@ -10,7 +10,7 @@ logLevel = "DEBUG"
[providers] [providers]
[providers.docker] [providers.docker]
exposedByDefault = false exposedByDefault = false
domain = "docker.local" defaultRule = "{{ normalize .Name }}.docker.local"
watch = true watch = true
[hostResolver] [hostResolver]

View file

@ -21,5 +21,5 @@ checkNewVersion = false
[providers] [providers]
[providers.docker] [providers.docker]
exposedByDefault = false exposedByDefault = false
domain = "docker.local" defaultRule = "{{ normalize .Name }}.docker.local"
watch = true watch = true

View file

@ -142,14 +142,13 @@ func (s *BaseSuite) displayTraefikLog(c *check.C, output *bytes.Buffer) {
} }
} }
func (s *BaseSuite) adaptFileForHost(c *check.C, path string) string { func (s *BaseSuite) getDockerHost() string {
dockerHost := os.Getenv("DOCKER_HOST") dockerHost := os.Getenv("DOCKER_HOST")
if dockerHost == "" { if dockerHost == "" {
// Default docker socket // Default docker socket
dockerHost = "unix:///var/run/docker.sock" dockerHost = "unix:///var/run/docker.sock"
} }
tempObjects := struct{ DockerHost string }{dockerHost} return dockerHost
return s.adaptFile(c, path, tempObjects)
} }
func (s *BaseSuite) adaptFile(c *check.C, path string, tempObjects interface{}) string { func (s *BaseSuite) adaptFile(c *check.C, path string, tempObjects interface{}) string {

View file

@ -5,7 +5,6 @@ import (
"io/ioutil" "io/ioutil"
"strings" "strings"
"text/template" "text/template"
"unicode"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/Masterminds/sprig" "github.com/Masterminds/sprig"
@ -48,15 +47,6 @@ func (p *BaseProvider) MatchConstraints(tags []string) (bool, *types.Constraint)
return true, nil return true, nil
} }
// GetConfiguration returns the provider configuration from default template (file or content) or overrode template file.
func (p *BaseProvider) GetConfiguration(defaultTemplate string, funcMap template.FuncMap, templateObjects interface{}) (*config.Configuration, error) {
tmplContent, err := p.getTemplateContent(defaultTemplate)
if err != nil {
return nil, err
}
return p.CreateConfiguration(tmplContent, funcMap, templateObjects)
}
// CreateConfiguration creates a provider configuration from content using templating. // CreateConfiguration creates a provider configuration from content using templating.
func (p *BaseProvider) CreateConfiguration(tmplContent string, funcMap template.FuncMap, templateObjects interface{}) (*config.Configuration, error) { func (p *BaseProvider) CreateConfiguration(tmplContent string, funcMap template.FuncMap, templateObjects interface{}) (*config.Configuration, error) {
var defaultFuncMap = sprig.TxtFuncMap() var defaultFuncMap = sprig.TxtFuncMap()
@ -121,20 +111,3 @@ func (p *BaseProvider) getTemplateContent(defaultTemplateFile string) (string, e
func split(sep, s string) []string { func split(sep, s string) []string {
return strings.Split(s, sep) return strings.Split(s, sep)
} }
// Normalize transforms a string that work with the rest of traefik.
// Replace '.' with '-' in quoted keys because of this issue https://github.com/BurntSushi/toml/issues/78
func Normalize(name string) string {
fargs := func(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
}
// get function
return strings.Join(strings.FieldsFunc(name, fargs), "-")
}
// ReverseStringSlice inverts the order of the given slice of string.
func ReverseStringSlice(slice *[]string) {
for i, j := 0, len(*slice)-1; i < j; i, j = i+1, j-1 {
(*slice)[i], (*slice)[j] = (*slice)[j], (*slice)[i]
}
}

View file

@ -1,10 +1,15 @@
package provider package provider
import ( import (
"bytes"
"context" "context"
"reflect" "reflect"
"sort" "sort"
"strings"
"text/template"
"unicode"
"github.com/Masterminds/sprig"
"github.com/containous/traefik/config" "github.com/containous/traefik/config"
"github.com/containous/traefik/log" "github.com/containous/traefik/log"
) )
@ -113,3 +118,70 @@ func AddMiddleware(configuration *config.Configuration, middlewareName string, m
return reflect.DeepEqual(configuration.Middlewares[middlewareName], middleware) return reflect.DeepEqual(configuration.Middlewares[middlewareName], middleware)
} }
// MakeDefaultRuleTemplate Creates the default rule template.
func MakeDefaultRuleTemplate(defaultRule string, funcMap template.FuncMap) (*template.Template, error) {
defaultFuncMap := sprig.TxtFuncMap()
defaultFuncMap["normalize"] = Normalize
for k, fn := range funcMap {
defaultFuncMap[k] = fn
}
return template.New("defaultRule").Funcs(defaultFuncMap).Parse(defaultRule)
}
// BuildRouterConfiguration Builds a router configuration.
func BuildRouterConfiguration(ctx context.Context, configuration *config.Configuration, defaultRouterName string, defaultRuleTpl *template.Template, model interface{}) {
logger := log.FromContext(ctx)
if len(configuration.Routers) == 0 {
if len(configuration.Services) > 1 {
log.FromContext(ctx).Info("Could not create a router for the container: too many services")
} else {
configuration.Routers = make(map[string]*config.Router)
configuration.Routers[defaultRouterName] = &config.Router{}
}
}
for routerName, router := range configuration.Routers {
loggerRouter := logger.WithField(log.RouterName, routerName)
if len(router.Rule) == 0 {
writer := &bytes.Buffer{}
if err := defaultRuleTpl.Execute(writer, model); err != nil {
loggerRouter.Errorf("Error while parsing default rule: %v", err)
delete(configuration.Routers, routerName)
continue
}
router.Rule = writer.String()
if len(router.Rule) == 0 {
loggerRouter.Error("Undefined rule")
delete(configuration.Routers, routerName)
continue
}
}
if len(router.Service) == 0 {
if len(configuration.Services) > 1 {
delete(configuration.Routers, routerName)
loggerRouter.
Error("Could not define the service name for the router: too many services")
continue
}
for serviceName := range configuration.Services {
router.Service = serviceName
}
}
}
}
// Normalize Replace all special chars with `-`.
func Normalize(name string) string {
fargs := func(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
}
// get function
return strings.Join(strings.FieldsFunc(name, fargs), "-")
}

View file

@ -39,7 +39,17 @@ func (p *Provider) buildConfiguration(ctx context.Context, containersInspected [
continue continue
} }
p.buildRouterConfiguration(ctxContainer, container, confFromLabel) serviceName := getServiceName(container)
model := struct {
Name string
Labels map[string]string
}{
Name: serviceName,
Labels: container.Labels,
}
provider.BuildRouterConfiguration(ctx, confFromLabel, serviceName, p.defaultRuleTpl, model)
configurations[containerName] = confFromLabel configurations[containerName] = confFromLabel
} }
@ -69,39 +79,6 @@ func (p *Provider) buildServiceConfiguration(ctx context.Context, container dock
return nil return nil
} }
func (p *Provider) buildRouterConfiguration(ctx context.Context, container dockerData, configuration *config.Configuration) {
logger := log.FromContext(ctx)
serviceName := getServiceName(container)
if len(configuration.Routers) == 0 {
if len(configuration.Services) > 1 {
logger.Info("could not create a router for the container: too many services")
} else {
configuration.Routers = make(map[string]*config.Router)
configuration.Routers[serviceName] = &config.Router{}
}
}
for routerName, router := range configuration.Routers {
if router.Rule == "" {
router.Rule = "Host:" + getSubDomain(serviceName) + "." + container.ExtraConf.Domain
}
if router.Service == "" {
if len(configuration.Services) > 1 {
delete(configuration.Routers, routerName)
logger.WithField(log.RouterName, routerName).
Error("Could not define the service name for the router: too many services")
continue
}
for serviceName := range configuration.Services {
router.Service = serviceName
}
}
}
}
func (p *Provider) keepContainer(ctx context.Context, container dockerData) bool { func (p *Provider) keepContainer(ctx context.Context, container dockerData) bool {
logger := log.FromContext(ctx) logger := log.FromContext(ctx)

View file

@ -14,6 +14,304 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestDefaultRule(t *testing.T) {
testCases := []struct {
desc string
containers []dockerData
defaultRule string
expected *config.Configuration
}{
{
desc: "default rule with no variable",
containers: []dockerData{
{
ServiceName: "Test",
Name: "Test",
Labels: map[string]string{},
NetworkSettings: networkSettings{
Ports: nat.PortMap{
nat.Port("80/tcp"): []nat.PortBinding{},
},
Networks: map[string]*networkData{
"bridge": {
Name: "bridge",
Addr: "127.0.0.1",
},
},
},
},
},
defaultRule: "Host:foo.bar",
expected: &config.Configuration{
Routers: map[string]*config.Router{
"Test": {
Service: "Test",
Rule: "Host:foo.bar",
},
},
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: "default rule with service name",
containers: []dockerData{
{
ServiceName: "Test",
Name: "Test",
Labels: map[string]string{},
NetworkSettings: networkSettings{
Ports: nat.PortMap{
nat.Port("80/tcp"): []nat.PortBinding{},
},
Networks: map[string]*networkData{
"bridge": {
Name: "bridge",
Addr: "127.0.0.1",
},
},
},
},
},
defaultRule: "Host:{{ .Name }}.foo.bar",
expected: &config.Configuration{
Routers: map[string]*config.Router{
"Test": {
Service: "Test",
Rule: "Host:Test.foo.bar",
},
},
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: "default rule with label",
containers: []dockerData{
{
ServiceName: "Test",
Name: "Test",
Labels: map[string]string{
"traefik.domain": "foo.bar",
},
NetworkSettings: networkSettings{
Ports: nat.PortMap{
nat.Port("80/tcp"): []nat.PortBinding{},
},
Networks: map[string]*networkData{
"bridge": {
Name: "bridge",
Addr: "127.0.0.1",
},
},
},
},
},
defaultRule: `Host:{{ .Name }}.{{ index .Labels "traefik.domain" }}`,
expected: &config.Configuration{
Routers: map[string]*config.Router{
"Test": {
Service: "Test",
Rule: "Host:Test.foo.bar",
},
},
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: "invalid rule",
containers: []dockerData{
{
ServiceName: "Test",
Name: "Test",
Labels: map[string]string{},
NetworkSettings: networkSettings{
Ports: nat.PortMap{
nat.Port("80/tcp"): []nat.PortBinding{},
},
Networks: map[string]*networkData{
"bridge": {
Name: "bridge",
Addr: "127.0.0.1",
},
},
},
},
},
defaultRule: `Host:{{ .Toto }}`,
expected: &config.Configuration{
Routers: map[string]*config.Router{},
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: "undefined rule",
containers: []dockerData{
{
ServiceName: "Test",
Name: "Test",
Labels: map[string]string{},
NetworkSettings: networkSettings{
Ports: nat.PortMap{
nat.Port("80/tcp"): []nat.PortBinding{},
},
Networks: map[string]*networkData{
"bridge": {
Name: "bridge",
Addr: "127.0.0.1",
},
},
},
},
},
defaultRule: ``,
expected: &config.Configuration{
Routers: map[string]*config.Router{},
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: "default template rule",
containers: []dockerData{
{
ServiceName: "Test",
Name: "Test",
Labels: map[string]string{},
NetworkSettings: networkSettings{
Ports: nat.PortMap{
nat.Port("80/tcp"): []nat.PortBinding{},
},
Networks: map[string]*networkData{
"bridge": {
Name: "bridge",
Addr: "127.0.0.1",
},
},
},
},
},
defaultRule: DefaultTemplateRule,
expected: &config.Configuration{
Routers: map[string]*config.Router{
"Test": {
Service: "Test",
Rule: "Host:Test",
},
},
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,
},
},
},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
p := Provider{
ExposedByDefault: true,
DefaultRule: test.defaultRule,
}
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)
})
}
}
func Test_buildConfiguration(t *testing.T) { func Test_buildConfiguration(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
@ -1571,52 +1869,6 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
}, },
{
desc: "one container with domain label",
containers: []dockerData{
{
ServiceName: "Test",
Name: "Test",
Labels: map[string]string{
"traefik.domain": "traefik.io",
},
NetworkSettings: networkSettings{
Ports: nat.PortMap{
nat.Port("80/tcp"): []nat.PortBinding{},
},
Networks: map[string]*networkData{
"bridge": {
Name: "bridge",
Addr: "127.0.0.1",
},
},
},
},
},
expected: &config.Configuration{
Routers: map[string]*config.Router{
"Test": {
Service: "Test",
Rule: "Host:Test.traefik.io",
},
},
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", desc: "Middlewares used in router",
containers: []dockerData{ containers: []dockerData{
@ -1683,11 +1935,14 @@ func Test_buildConfiguration(t *testing.T) {
t.Parallel() t.Parallel()
p := Provider{ p := Provider{
Domain: "traefik.wtf",
ExposedByDefault: true, ExposedByDefault: true,
DefaultRule: "Host:{{ normalize .Name }}.traefik.wtf",
} }
p.Constraints = test.constraints p.Constraints = test.constraints
err := p.Init()
require.NoError(t, err)
for i := 0; i < len(test.containers); i++ { for i := 0; i < len(test.containers); i++ {
var err error var err error
test.containers[i].ExtraConf, err = p.getConfiguration(test.containers[i]) test.containers[i].ExtraConf, err = p.getConfiguration(test.containers[i])

View file

@ -2,11 +2,13 @@ package docker
import ( import (
"context" "context"
"fmt"
"io" "io"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"text/template"
"time" "time"
"github.com/cenk/backoff" "github.com/cenk/backoff"
@ -29,8 +31,10 @@ import (
) )
const ( const (
// SwarmAPIVersion is a constant holding the version of the Provider API traefik will use // SwarmAPIVersion is a constant holding the version of the Provider API traefik will use.
SwarmAPIVersion = "1.24" SwarmAPIVersion = "1.24"
// DefaultTemplateRule The default template for the default rule.
DefaultTemplateRule = "Host:{{ normalize .Name }}"
) )
var _ provider.Provider = (*Provider)(nil) var _ provider.Provider = (*Provider)(nil)
@ -39,21 +43,28 @@ var _ provider.Provider = (*Provider)(nil)
type Provider struct { type Provider struct {
provider.BaseProvider `mapstructure:",squash" export:"true"` provider.BaseProvider `mapstructure:",squash" export:"true"`
Endpoint string `description:"Docker server endpoint. Can be a tcp or a unix socket endpoint"` Endpoint string `description:"Docker server endpoint. Can be a tcp or a unix socket endpoint"`
Domain string `description:"Default domain used"` DefaultRule string `description:"Default rule"`
TLS *types.ClientTLS `description:"Enable Docker TLS support" export:"true"` TLS *types.ClientTLS `description:"Enable Docker TLS support" export:"true"`
ExposedByDefault bool `description:"Expose containers by default" export:"true"` ExposedByDefault bool `description:"Expose containers by default" export:"true"`
UseBindPortIP bool `description:"Use the ip address from the bound port, rather than from the inner network" export:"true"` UseBindPortIP bool `description:"Use the ip address from the bound port, rather than from the inner network" export:"true"`
SwarmMode bool `description:"Use Docker on Swarm Mode" export:"true"` SwarmMode bool `description:"Use Docker on Swarm Mode" export:"true"`
Network string `description:"Default Docker network used" export:"true"` Network string `description:"Default Docker network used" export:"true"`
SwarmModeRefreshSeconds int `description:"Polling interval for swarm mode (in seconds)" export:"true"` SwarmModeRefreshSeconds int `description:"Polling interval for swarm mode (in seconds)" export:"true"`
defaultRuleTpl *template.Template
} }
// Init the provider // Init the provider.
func (p *Provider) Init() error { 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 p.BaseProvider.Init() return p.BaseProvider.Init()
} }
// dockerData holds the need data to the Provider p // dockerData holds the need data to the provider.
type dockerData struct { type dockerData struct {
ID string ID string
ServiceName string ServiceName string
@ -65,14 +76,14 @@ type dockerData struct {
ExtraConf configuration ExtraConf configuration
} }
// NetworkSettings holds the networks data to the Provider p // NetworkSettings holds the networks data to the provider.
type networkSettings struct { type networkSettings struct {
NetworkMode dockercontainertypes.NetworkMode NetworkMode dockercontainertypes.NetworkMode
Ports nat.PortMap Ports nat.PortMap
Networks map[string]*networkData Networks map[string]*networkData
} }
// Network holds the network data to the Provider p // Network holds the network data to the provider.
type networkData struct { type networkData struct {
Name string Name string
Addr string Addr string
@ -121,8 +132,7 @@ func (p *Provider) createClient() (client.APIClient, error) {
return client.NewClient(p.Endpoint, apiVersion, httpClient, httpHeaders) return client.NewClient(p.Endpoint, apiVersion, httpClient, httpHeaders)
} }
// Provide allows the docker provider to provide configurations to traefik // Provide allows the docker provider to provide configurations to traefik using the given configuration channel.
// using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- config.Message, pool *safe.Pool) error { func (p *Provider) Provide(configurationChan chan<- config.Message, pool *safe.Pool) error {
pool.GoCtx(func(routineCtx context.Context) { pool.GoCtx(func(routineCtx context.Context) {
ctxLog := log.With(routineCtx, log.Str(log.ProviderName, "docker")) ctxLog := log.With(routineCtx, log.Str(log.ProviderName, "docker"))

View file

@ -15,7 +15,6 @@ const (
type configuration struct { type configuration struct {
Enable bool Enable bool
Tags []string Tags []string
Domain string
Docker specificConfiguration Docker specificConfiguration
} }
@ -27,13 +26,12 @@ type specificConfiguration struct {
func (p *Provider) getConfiguration(container dockerData) (configuration, error) { func (p *Provider) getConfiguration(container dockerData) (configuration, error) {
conf := configuration{ conf := configuration{
Enable: p.ExposedByDefault, Enable: p.ExposedByDefault,
Domain: p.Domain,
Docker: specificConfiguration{ Docker: specificConfiguration{
Network: p.Network, Network: p.Network,
}, },
} }
err := label.Decode(container.Labels, &conf, "traefik.docker.", "traefik.domain", "traefik.enable", "traefik.tags") err := label.Decode(container.Labels, &conf, "traefik.docker.", "traefik.enable", "traefik.tags")
if err != nil { if err != nil {
return configuration{}, err return configuration{}, err
} }

View file

@ -17,6 +17,10 @@ func addRoute(ctx context.Context, router *mux.Router, rule string, priority int
return err return err
} }
if len(matchers) == 0 {
return fmt.Errorf("invalid rule: %s", rule)
}
if priority == 0 { if priority == 0 {
priority = len(rule) priority = len(rule)
} }

View file

@ -19,12 +19,16 @@ func Test_addRoute(t *testing.T) {
rule string rule string
headers map[string]string headers map[string]string
expected map[string]int expected map[string]int
expectedError bool
}{ }{
{ {
desc: "no rule", desc: "no rule",
expected: map[string]int{ expectedError: true,
"http://localhost/foo": http.StatusOK,
}, },
{
desc: "Rule with no matcher",
rule: "rulewithnotmatcher",
expectedError: true,
}, },
{ {
desc: "PathPrefix", desc: "PathPrefix",
@ -217,6 +221,9 @@ func Test_addRoute(t *testing.T) {
router.SkipClean(true) router.SkipClean(true)
err := addRoute(context.Background(), router, test.rule, 0, handler) err := addRoute(context.Background(), router, test.rule, 0, handler)
if test.expectedError {
require.Error(t, err)
} else {
require.NoError(t, err) require.NoError(t, err)
// RequestDecorator is necessary for the host rule // RequestDecorator is necessary for the host rule
@ -234,7 +241,7 @@ func Test_addRoute(t *testing.T) {
results[calledURL] = w.Code results[calledURL] = w.Code
} }
assert.Equal(t, test.expected, results) assert.Equal(t, test.expected, results)
}
}) })
} }
} }