Adds default rule system on Docker provider.
Co-authored-by: Julien Salleyron <julien@containo.us>
This commit is contained in:
parent
b54c956c5e
commit
04958c6951
20 changed files with 506 additions and 168 deletions
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -25,5 +25,5 @@ checkNewVersion = false
|
|||
[providers]
|
||||
[providers.docker]
|
||||
exposedByDefault = false
|
||||
domain = "docker.local"
|
||||
defaultRule = "{{ normalize .Name }}.docker.local"
|
||||
watch = true
|
||||
|
|
|
@ -10,6 +10,6 @@ logLevel = "DEBUG"
|
|||
|
||||
[providers]
|
||||
[providers.docker]
|
||||
endpoint = "{{.DockerHost}}"
|
||||
domain = "docker.localhost"
|
||||
endpoint = "{{ .DockerHost }}"
|
||||
defaultRule = "{{ .DefaultRule }}"
|
||||
exposedByDefault = false
|
||||
|
|
|
@ -9,6 +9,6 @@ logLevel = "DEBUG"
|
|||
|
||||
[providers]
|
||||
[providers.docker]
|
||||
endpoint = "{{.DockerHost}}"
|
||||
domain = "docker.localhost"
|
||||
endpoint = "{{ .DockerHost }}"
|
||||
defaultRule = "{{ .DefaultRule }}"
|
||||
exposedByDefault = true
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -10,7 +10,7 @@ logLevel = "DEBUG"
|
|||
[providers]
|
||||
[providers.docker]
|
||||
exposedByDefault = false
|
||||
domain = "docker.local"
|
||||
defaultRule = "{{ normalize .Name }}.docker.local"
|
||||
watch = true
|
||||
|
||||
[hostResolver]
|
||||
|
|
|
@ -21,5 +21,5 @@ checkNewVersion = false
|
|||
[providers]
|
||||
[providers.docker]
|
||||
exposedByDefault = false
|
||||
domain = "docker.local"
|
||||
defaultRule = "{{ normalize .Name }}.docker.local"
|
||||
watch = true
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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), "-")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -19,12 +19,16 @@ func Test_addRoute(t *testing.T) {
|
|||
rule string
|
||||
headers map[string]string
|
||||
expected map[string]int
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
desc: "no rule",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Rule with no matcher",
|
||||
rule: "rulewithnotmatcher",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "PathPrefix",
|
||||
|
@ -217,6 +221,9 @@ func Test_addRoute(t *testing.T) {
|
|||
router.SkipClean(true)
|
||||
|
||||
err := addRoute(context.Background(), router, test.rule, 0, handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
|
||||
// RequestDecorator is necessary for the host rule
|
||||
|
@ -234,7 +241,7 @@ func Test_addRoute(t *testing.T) {
|
|||
results[calledURL] = w.Code
|
||||
}
|
||||
assert.Equal(t, test.expected, results)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue