diff --git a/cmd/configuration.go b/cmd/configuration.go index 702600659..1d758c7ae 100644 --- a/cmd/configuration.go +++ b/cmd/configuration.go @@ -155,6 +155,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { defaultDocker.Endpoint = "unix:///var/run/docker.sock" defaultDocker.SwarmMode = false defaultDocker.SwarmModeRefreshSeconds = 15 + defaultDocker.DefaultRule = docker.DefaultTemplateRule // default Rest var defaultRest rest.Provider diff --git a/integration/docker_compose_test.go b/integration/docker_compose_test.go index 459ed2a17..e6dfd3e90 100644 --- a/integration/docker_compose_test.go +++ b/integration/docker_compose_test.go @@ -40,7 +40,14 @@ func (s *DockerComposeSuite) TestComposeScale(c *check.C) { 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) cmd, display := s.traefikCmd(withConfigFile(file)) diff --git a/integration/docker_test.go b/integration/docker_test.go index 6bd2e1520..ac421eb82 100644 --- a/integration/docker_test.go +++ b/integration/docker_test.go @@ -79,11 +79,19 @@ func (s *DockerSuite) SetUpSuite(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) { - 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) cmd, display := s.traefikCmd(withConfigFile(file)) @@ -99,7 +107,15 @@ func (s *DockerSuite) TestSimpleConfiguration(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) 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) { - 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) // Start a container with some labels @@ -177,7 +201,15 @@ func (s *DockerSuite) TestDockerContainersWithLabels(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) // 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 ?) // 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)) c.Assert(err, checker.IsNil) } 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) // Start a container with some labels diff --git a/integration/error_pages_test.go b/integration/error_pages_test.go index b25c81ad9..b321b909f 100644 --- a/integration/error_pages_test.go +++ b/integration/error_pages_test.go @@ -26,7 +26,6 @@ func (s *ErrorPagesSuite) SetUpSuite(c *check.C) { } func (s *ErrorPagesSuite) TestSimpleConfiguration(c *check.C) { - file := s.adaptFile(c, "fixtures/error_pages/simple.toml", struct { Server1 string Server2 string diff --git a/integration/fixtures/access_log_config.toml b/integration/fixtures/access_log_config.toml index 4d5d94c66..868097f3e 100644 --- a/integration/fixtures/access_log_config.toml +++ b/integration/fixtures/access_log_config.toml @@ -25,5 +25,5 @@ checkNewVersion = false [providers] [providers.docker] exposedByDefault = false - domain = "docker.local" + defaultRule = "{{ normalize .Name }}.docker.local" watch = true diff --git a/integration/fixtures/docker/minimal.toml b/integration/fixtures/docker/minimal.toml index 1131541fc..18c411d91 100644 --- a/integration/fixtures/docker/minimal.toml +++ b/integration/fixtures/docker/minimal.toml @@ -10,6 +10,6 @@ logLevel = "DEBUG" [providers] [providers.docker] - endpoint = "{{.DockerHost}}" - domain = "docker.localhost" + endpoint = "{{ .DockerHost }}" + defaultRule = "{{ .DefaultRule }}" exposedByDefault = false diff --git a/integration/fixtures/docker/simple.toml b/integration/fixtures/docker/simple.toml index ab3177f4f..c880f6daa 100644 --- a/integration/fixtures/docker/simple.toml +++ b/integration/fixtures/docker/simple.toml @@ -9,6 +9,6 @@ logLevel = "DEBUG" [providers] [providers.docker] - endpoint = "{{.DockerHost}}" - domain = "docker.localhost" + endpoint = "{{ .DockerHost }}" + defaultRule = "{{ .DefaultRule }}" exposedByDefault = true diff --git a/integration/fixtures/error_pages/error.toml b/integration/fixtures/error_pages/error.toml index 6989048c4..d8dd776d8 100644 --- a/integration/fixtures/error_pages/error.toml +++ b/integration/fixtures/error_pages/error.toml @@ -10,11 +10,9 @@ logLevel = "DEBUG" [routers] [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.error.errors] diff --git a/integration/fixtures/error_pages/simple.toml b/integration/fixtures/error_pages/simple.toml index fbabe4273..ae3747ac5 100644 --- a/integration/fixtures/error_pages/simple.toml +++ b/integration/fixtures/error_pages/simple.toml @@ -10,11 +10,9 @@ logLevel = "DEBUG" [routers] [routers.router1] - middlewares = ["error"] - service = "service1" - - [routers.router1.routes.test_1] rule = "Host:test.local" + service = "service1" + middlewares = ["error"] [middlewares] [middlewares.error.errors] diff --git a/integration/fixtures/simple_hostresolver.toml b/integration/fixtures/simple_hostresolver.toml index 57d029a84..5af67b98d 100644 --- a/integration/fixtures/simple_hostresolver.toml +++ b/integration/fixtures/simple_hostresolver.toml @@ -10,7 +10,7 @@ logLevel = "DEBUG" [providers] [providers.docker] exposedByDefault = false - domain = "docker.local" + defaultRule = "{{ normalize .Name }}.docker.local" watch = true [hostResolver] diff --git a/integration/fixtures/traefik_log_config.toml b/integration/fixtures/traefik_log_config.toml index 764687b44..4cf560609 100644 --- a/integration/fixtures/traefik_log_config.toml +++ b/integration/fixtures/traefik_log_config.toml @@ -21,5 +21,5 @@ checkNewVersion = false [providers] [providers.docker] exposedByDefault = false - domain = "docker.local" + defaultRule = "{{ normalize .Name }}.docker.local" watch = true diff --git a/integration/integration_test.go b/integration/integration_test.go index f6a1bb93f..ae09fb392 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -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") if dockerHost == "" { // Default docker socket dockerHost = "unix:///var/run/docker.sock" } - tempObjects := struct{ DockerHost string }{dockerHost} - return s.adaptFile(c, path, tempObjects) + return dockerHost } func (s *BaseSuite) adaptFile(c *check.C, path string, tempObjects interface{}) string { diff --git a/provider/base_provider.go b/provider/base_provider.go index 30359ecaf..e143a9b52 100644 --- a/provider/base_provider.go +++ b/provider/base_provider.go @@ -5,7 +5,6 @@ import ( "io/ioutil" "strings" "text/template" - "unicode" "github.com/BurntSushi/toml" "github.com/Masterminds/sprig" @@ -48,15 +47,6 @@ func (p *BaseProvider) MatchConstraints(tags []string) (bool, *types.Constraint) 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. func (p *BaseProvider) CreateConfiguration(tmplContent string, funcMap template.FuncMap, templateObjects interface{}) (*config.Configuration, error) { var defaultFuncMap = sprig.TxtFuncMap() @@ -121,20 +111,3 @@ func (p *BaseProvider) getTemplateContent(defaultTemplateFile string) (string, e func split(sep, s string) []string { 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] - } -} diff --git a/provider/configuration.go b/provider/configuration.go index 499df13bf..f41bb37a4 100644 --- a/provider/configuration.go +++ b/provider/configuration.go @@ -1,10 +1,15 @@ package provider import ( + "bytes" "context" "reflect" "sort" + "strings" + "text/template" + "unicode" + "github.com/Masterminds/sprig" "github.com/containous/traefik/config" "github.com/containous/traefik/log" ) @@ -113,3 +118,70 @@ func AddMiddleware(configuration *config.Configuration, middlewareName string, m 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), "-") +} diff --git a/provider/docker/config.go b/provider/docker/config.go index 54279cdb3..95c12f3c0 100644 --- a/provider/docker/config.go +++ b/provider/docker/config.go @@ -39,7 +39,17 @@ func (p *Provider) buildConfiguration(ctx context.Context, containersInspected [ 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 } @@ -69,39 +79,6 @@ func (p *Provider) buildServiceConfiguration(ctx context.Context, container dock 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 { logger := log.FromContext(ctx) diff --git a/provider/docker/config_test.go b/provider/docker/config_test.go index d730f29be..9d1602aaa 100644 --- a/provider/docker/config_test.go +++ b/provider/docker/config_test.go @@ -14,6 +14,304 @@ import ( "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) { testCases := []struct { 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", containers: []dockerData{ @@ -1683,11 +1935,14 @@ func Test_buildConfiguration(t *testing.T) { t.Parallel() p := Provider{ - Domain: "traefik.wtf", ExposedByDefault: true, + DefaultRule: "Host:{{ normalize .Name }}.traefik.wtf", } 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]) diff --git a/provider/docker/docker.go b/provider/docker/docker.go index 164f2f22c..f3a2dc204 100644 --- a/provider/docker/docker.go +++ b/provider/docker/docker.go @@ -2,11 +2,13 @@ package docker import ( "context" + "fmt" "io" "net" "net/http" "strconv" "strings" + "text/template" "time" "github.com/cenk/backoff" @@ -29,8 +31,10 @@ import ( ) 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" + // DefaultTemplateRule The default template for the default rule. + DefaultTemplateRule = "Host:{{ normalize .Name }}" ) var _ provider.Provider = (*Provider)(nil) @@ -39,21 +43,28 @@ var _ provider.Provider = (*Provider)(nil) type Provider struct { provider.BaseProvider `mapstructure:",squash" export:"true"` 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"` 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"` SwarmMode bool `description:"Use Docker on Swarm Mode" export:"true"` Network string `description:"Default Docker network used" 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 { + 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() } -// dockerData holds the need data to the Provider p +// dockerData holds the need data to the provider. type dockerData struct { ID string ServiceName string @@ -65,14 +76,14 @@ type dockerData struct { ExtraConf configuration } -// NetworkSettings holds the networks data to the Provider p +// NetworkSettings holds the networks data to the provider. type networkSettings struct { NetworkMode dockercontainertypes.NetworkMode Ports nat.PortMap Networks map[string]*networkData } -// Network holds the network data to the Provider p +// Network holds the network data to the provider. type networkData struct { Name string Addr string @@ -121,8 +132,7 @@ func (p *Provider) createClient() (client.APIClient, error) { return client.NewClient(p.Endpoint, apiVersion, httpClient, httpHeaders) } -// Provide allows the docker provider to provide configurations to traefik -// using the given configuration channel. +// Provide allows the docker 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, "docker")) diff --git a/provider/docker/label.go b/provider/docker/label.go index b36925125..b0e898702 100644 --- a/provider/docker/label.go +++ b/provider/docker/label.go @@ -15,7 +15,6 @@ const ( type configuration struct { Enable bool Tags []string - Domain string Docker specificConfiguration } @@ -27,13 +26,12 @@ type specificConfiguration struct { func (p *Provider) getConfiguration(container dockerData) (configuration, error) { conf := configuration{ Enable: p.ExposedByDefault, - Domain: p.Domain, Docker: specificConfiguration{ 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 { return configuration{}, err } diff --git a/server/router/rules.go b/server/router/rules.go index fdda85103..9cd127276 100644 --- a/server/router/rules.go +++ b/server/router/rules.go @@ -17,6 +17,10 @@ func addRoute(ctx context.Context, router *mux.Router, rule string, priority int return err } + if len(matchers) == 0 { + return fmt.Errorf("invalid rule: %s", rule) + } + if priority == 0 { priority = len(rule) } diff --git a/server/router/rules_test.go b/server/router/rules_test.go index 49541d53a..0ce9df220 100644 --- a/server/router/rules_test.go +++ b/server/router/rules_test.go @@ -15,16 +15,20 @@ import ( func Test_addRoute(t *testing.T) { testCases := []struct { - desc string - rule string - headers map[string]string - expected map[string]int + desc string + rule string + headers map[string]string + expected map[string]int + expectedError bool }{ { - desc: "no rule", - expected: map[string]int{ - "http://localhost/foo": http.StatusOK, - }, + desc: "no rule", + expectedError: true, + }, + { + desc: "Rule with no matcher", + rule: "rulewithnotmatcher", + expectedError: true, }, { desc: "PathPrefix", @@ -217,24 +221,27 @@ func Test_addRoute(t *testing.T) { router.SkipClean(true) err := addRoute(context.Background(), router, test.rule, 0, handler) - require.NoError(t, err) + if test.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) - // RequestDecorator is necessary for the host rule - reqHost := requestdecorator.New(nil) + // RequestDecorator is necessary for the host rule + reqHost := requestdecorator.New(nil) - results := make(map[string]int) - for calledURL := range test.expected { - w := httptest.NewRecorder() + results := make(map[string]int) + for calledURL := range test.expected { + w := httptest.NewRecorder() - req := testhelpers.MustNewRequest(http.MethodGet, calledURL, nil) - for key, value := range test.headers { - req.Header.Set(key, value) + req := testhelpers.MustNewRequest(http.MethodGet, calledURL, nil) + for key, value := range test.headers { + req.Header.Set(key, value) + } + reqHost.ServeHTTP(w, req, router.ServeHTTP) + results[calledURL] = w.Code } - reqHost.ServeHTTP(w, req, router.ServeHTTP) - results[calledURL] = w.Code + assert.Equal(t, test.expected, results) } - assert.Equal(t, test.expected, results) - }) } }