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.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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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), "-")
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue