diff --git a/integration/docker_test.go b/integration/docker_test.go index 4920ceb32..0cd61ecfc 100644 --- a/integration/docker_test.go +++ b/integration/docker_test.go @@ -10,7 +10,7 @@ import ( "time" "github.com/containous/traefik/integration/try" - "github.com/containous/traefik/types" + "github.com/containous/traefik/provider/label" "github.com/docker/docker/pkg/namesgenerator" "github.com/go-check/check" d "github.com/libkermit/docker" @@ -138,13 +138,13 @@ func (s *DockerSuite) TestDockerContainersWithLabels(c *check.C) { defer os.Remove(file) // Start a container with some labels labels := map[string]string{ - types.LabelFrontendRule: "Host:my.super.host", + label.TraefikFrontendRule: "Host:my.super.host", } s.startContainerWithLabels(c, "swarm:1.0.0", labels, "manage", "token://blabla") // Start another container by replacing a '.' by a '-' labels = map[string]string{ - types.LabelFrontendRule: "Host:my-super.host", + label.TraefikFrontendRule: "Host:my-super.host", } s.startContainerWithLabels(c, "swarm:1.0.0", labels, "manage", "token://blablabla") // Start traefik @@ -159,7 +159,7 @@ func (s *DockerSuite) TestDockerContainersWithLabels(c *check.C) { req.Host = "my-super.host" // FIXME Need to wait than 500 milliseconds more (for swarm or traefik to boot up ?) - resp, err := try.ResponseUntilStatusCode(req, 1500*time.Millisecond, http.StatusOK) + _, err = try.ResponseUntilStatusCode(req, 1500*time.Millisecond, http.StatusOK) c.Assert(err, checker.IsNil) req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/version", nil) @@ -167,7 +167,7 @@ func (s *DockerSuite) TestDockerContainersWithLabels(c *check.C) { req.Host = "my.super.host" // FIXME Need to wait than 500 milliseconds more (for swarm or traefik to boot up ?) - resp, err = try.ResponseUntilStatusCode(req, 1500*time.Millisecond, http.StatusOK) + resp, err := try.ResponseUntilStatusCode(req, 1500*time.Millisecond, http.StatusOK) c.Assert(err, checker.IsNil) body, err := ioutil.ReadAll(resp.Body) @@ -184,7 +184,7 @@ func (s *DockerSuite) TestDockerContainersWithOneMissingLabels(c *check.C) { defer os.Remove(file) // Start a container with some labels labels := map[string]string{ - types.LabelTraefikFrontendValue: "my.super.host", + label.TraefikFrontendValue: "my.super.host", } s.startContainerWithLabels(c, "swarm:1.0.0", labels, "manage", "token://blabla") @@ -213,9 +213,9 @@ func (s *DockerSuite) TestDockerContainersWithServiceLabels(c *check.C) { defer os.Remove(file) // Start a container with some labels labels := map[string]string{ - types.LabelPrefix + "servicename.frontend.rule": "Host:my.super.host", - types.LabelFrontendRule: "Host:my.wrong.host", - types.LabelPort: "2375", + label.Prefix + "servicename.frontend.rule": "Host:my.super.host", + label.TraefikFrontendRule: "Host:my.wrong.host", + label.TraefikPort: "2375", } s.startContainerWithLabels(c, "swarm:1.0.0", labels, "manage", "token://blabla") @@ -248,8 +248,8 @@ func (s *DockerSuite) TestRestartDockerContainers(c *check.C) { defer os.Remove(file) // Start a container with some labels labels := map[string]string{ - types.LabelPrefix + "frontend.rule": "Host:my.super.host", - types.LabelPort: "2375", + label.Prefix + "frontend.rule": "Host:my.super.host", + label.TraefikPort: "2375", } s.startContainerWithNameAndLabels(c, "powpow", "swarm:1.0.0", labels, "manage", "token://blabla") diff --git a/provider/docker/config.go b/provider/docker/config.go new file mode 100644 index 000000000..b3559587c --- /dev/null +++ b/provider/docker/config.go @@ -0,0 +1,177 @@ +package docker + +import ( + "math" + "strconv" + "text/template" + + "github.com/BurntSushi/ty/fun" + "github.com/containous/traefik/log" + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" +) + +func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.Configuration { + var DockerFuncMap = template.FuncMap{ + "getBackend": getBackend, + "getIPAddress": p.getIPAddress, + "getPort": getPort, + "getWeight": getFuncStringLabel(label.TraefikWeight, label.DefaultWeight), + "getDomain": getFuncStringLabel(label.TraefikDomain, p.Domain), + "getProtocol": getFuncStringLabel(label.TraefikProtocol, label.DefaultProtocol), + "getPassHostHeader": getFuncStringLabel(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), + "getPriority": getFuncStringLabel(label.TraefikFrontendPriority, label.DefaultFrontendPriority), + "getEntryPoints": getFuncSliceStringLabel(label.TraefikFrontendEntryPoints), + "getBasicAuth": getFuncSliceStringLabel(label.TraefikFrontendAuthBasic), + "getFrontendRule": p.getFrontendRule, + "getRedirect": getFuncStringLabel(label.TraefikFrontendRedirect, label.DefaultFrontendRedirect), + "hasCircuitBreakerLabel": hasFunc(label.TraefikBackendCircuitBreakerExpression), + "getCircuitBreakerExpression": getFuncStringLabel(label.TraefikBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression), + "hasLoadBalancerLabel": hasLoadBalancerLabel, + "getLoadBalancerMethod": getFuncStringLabel(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), + "hasMaxConnLabels": hasMaxConnLabels, + "getMaxConnAmount": getFuncInt64Label(label.TraefikBackendMaxConnAmount, math.MaxInt64), + "getMaxConnExtractorFunc": getFuncStringLabel(label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc), + "getSticky": getSticky, + "hasStickinessLabel": hasFunc(label.TraefikBackendLoadBalancerStickiness), + "getStickinessCookieName": getFuncStringLabel(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName), + "isBackendLBSwarm": isBackendLBSwarm, // FIXME DEAD ? + "getServiceBackend": getServiceBackend, + "getServiceRedirect": getFuncServiceStringLabel(label.SuffixFrontendRedirect, label.DefaultFrontendRedirect), + "getWhitelistSourceRange": getFuncSliceStringLabel(label.TraefikFrontendWhitelistSourceRange), + + "hasRequestHeaders": hasFunc(label.TraefikFrontendRequestHeaders), + "getRequestHeaders": getFuncMapLabel(label.TraefikFrontendRequestHeaders), + "hasResponseHeaders": hasFunc(label.TraefikFrontendResponseHeaders), + "getResponseHeaders": getFuncMapLabel(label.TraefikFrontendResponseHeaders), + "hasAllowedHostsHeaders": hasFunc(label.TraefikFrontendAllowedHosts), + "getAllowedHostsHeaders": getFuncSliceStringLabel(label.TraefikFrontendAllowedHosts), + "hasHostsProxyHeaders": hasFunc(label.TraefikFrontendHostsProxyHeaders), + "getHostsProxyHeaders": getFuncSliceStringLabel(label.TraefikFrontendHostsProxyHeaders), + "hasSSLRedirectHeaders": hasFunc(label.TraefikFrontendSSLRedirect), + "getSSLRedirectHeaders": getFuncBoolLabel(label.TraefikFrontendSSLRedirect, false), + "hasSSLTemporaryRedirectHeaders": hasFunc(label.TraefikFrontendSSLTemporaryRedirect), + "getSSLTemporaryRedirectHeaders": getFuncBoolLabel(label.TraefikFrontendSSLTemporaryRedirect, false), + "hasSSLHostHeaders": hasFunc(label.TraefikFrontendSSLHost), + "getSSLHostHeaders": getFuncStringLabel(label.TraefikFrontendSSLHost, ""), + "hasSSLProxyHeaders": hasFunc(label.TraefikFrontendSSLProxyHeaders), + "getSSLProxyHeaders": getFuncMapLabel(label.TraefikFrontendSSLProxyHeaders), + "hasSTSSecondsHeaders": hasFunc(label.TraefikFrontendSTSSeconds), + "getSTSSecondsHeaders": getFuncInt64Label(label.TraefikFrontendSTSSeconds, 0), + "hasSTSIncludeSubdomainsHeaders": hasFunc(label.TraefikFrontendSTSIncludeSubdomains), + "getSTSIncludeSubdomainsHeaders": getFuncBoolLabel(label.TraefikFrontendSTSIncludeSubdomains, false), + "hasSTSPreloadHeaders": hasFunc(label.TraefikFrontendSTSPreload), + "getSTSPreloadHeaders": getFuncBoolLabel(label.TraefikFrontendSTSPreload, false), + "hasForceSTSHeaderHeaders": hasFunc(label.TraefikFrontendForceSTSHeader), + "getForceSTSHeaderHeaders": getFuncBoolLabel(label.TraefikFrontendForceSTSHeader, false), + "hasFrameDenyHeaders": hasFunc(label.TraefikFrontendFrameDeny), + "getFrameDenyHeaders": getFuncBoolLabel(label.TraefikFrontendFrameDeny, false), + "hasCustomFrameOptionsValueHeaders": hasFunc(label.TraefikFrontendCustomFrameOptionsValue), + "getCustomFrameOptionsValueHeaders": getFuncStringLabel(label.TraefikFrontendCustomFrameOptionsValue, ""), + "hasContentTypeNosniffHeaders": hasFunc(label.TraefikFrontendContentTypeNosniff), + "getContentTypeNosniffHeaders": getFuncBoolLabel(label.TraefikFrontendContentTypeNosniff, false), + "hasBrowserXSSFilterHeaders": hasFunc(label.TraefikFrontendBrowserXSSFilter), + "getBrowserXSSFilterHeaders": getFuncBoolLabel(label.TraefikFrontendBrowserXSSFilter, false), + "hasContentSecurityPolicyHeaders": hasFunc(label.TraefikFrontendContentSecurityPolicy), + "getContentSecurityPolicyHeaders": getFuncStringLabel(label.TraefikFrontendContentSecurityPolicy, ""), + "hasPublicKeyHeaders": hasFunc(label.TraefikFrontendPublicKey), + "getPublicKeyHeaders": getFuncStringLabel(label.TraefikFrontendPublicKey, ""), + "hasReferrerPolicyHeaders": hasFunc(label.TraefikFrontendReferrerPolicy), + "getReferrerPolicyHeaders": getFuncStringLabel(label.TraefikFrontendReferrerPolicy, ""), + "hasIsDevelopmentHeaders": hasFunc(label.TraefikFrontendIsDevelopment), + "getIsDevelopmentHeaders": getFuncBoolLabel(label.TraefikFrontendIsDevelopment, false), + + "hasServices": hasServices, + "getServiceNames": getServiceNames, + "getServicePort": getServicePort, + "getServiceWeight": getFuncServiceStringLabel(label.SuffixWeight, label.DefaultWeight), + "getServiceProtocol": getFuncServiceStringLabel(label.SuffixProtocol, label.DefaultProtocol), + "getServiceEntryPoints": getFuncServiceSliceStringLabel(label.SuffixFrontendEntryPoints), + "getServiceBasicAuth": getFuncServiceSliceStringLabel(label.SuffixFrontendAuthBasic), + "getServiceFrontendRule": p.getServiceFrontendRule, + "getServicePassHostHeader": getFuncServiceStringLabel(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeader), + "getServicePriority": getFuncServiceStringLabel(label.SuffixFrontendPriority, label.DefaultFrontendPriority), + } + // filter containers + filteredContainers := fun.Filter(func(container dockerData) bool { + return p.containerFilter(container) + }, containersInspected).([]dockerData) + + frontends := map[string][]dockerData{} + backends := map[string]dockerData{} + servers := map[string][]dockerData{} + serviceNames := make(map[string]struct{}) + for idx, container := range filteredContainers { + if _, exists := serviceNames[container.ServiceName]; !exists { + frontendName := p.getFrontendName(container, idx) + frontends[frontendName] = append(frontends[frontendName], container) + if len(container.ServiceName) > 0 { + serviceNames[container.ServiceName] = struct{}{} + } + } + backendName := getBackend(container) + backends[backendName] = container + servers[backendName] = append(servers[backendName], container) + } + + templateObjects := struct { + Containers []dockerData + Frontends map[string][]dockerData + Backends map[string]dockerData + Servers map[string][]dockerData + Domain string + }{ + filteredContainers, + frontends, + backends, + servers, + p.Domain, + } + + configuration, err := p.GetConfiguration("templates/docker.tmpl", DockerFuncMap, templateObjects) + if err != nil { + log.Error(err) + } + + return configuration +} + +func (p Provider) containerFilter(container dockerData) bool { + if !label.IsEnabled(container.Labels, p.ExposedByDefault) { + log.Debugf("Filtering disabled container %s", container.Name) + return false + } + + var err error + portLabel := "traefik.port label" + if hasServices(container) { + portLabel = "traefik..port or " + portLabel + "s" + err = checkServiceLabelPort(container) + } else { + _, err = strconv.Atoi(container.Labels[label.TraefikPort]) + } + if len(container.NetworkSettings.Ports) == 0 && err != nil { + log.Debugf("Filtering container without port and no %s %s : %s", portLabel, container.Name, err.Error()) + return false + } + + constraintTags := label.SplitAndTrimString(container.Labels[label.TraefikTags], ",") + if ok, failingConstraint := p.MatchConstraints(constraintTags); !ok { + if failingConstraint != nil { + log.Debugf("Container %v pruned by '%v' constraint", container.Name, failingConstraint.String()) + } + return false + } + + if container.Health != "" && container.Health != "healthy" { + log.Debugf("Filtering unhealthy or starting container %s", container.Name) + return false + } + + if len(p.getFrontendRule(container)) == 0 { + log.Debugf("Filtering container with empty frontend rule %s", container.Name) + return false + } + + return true +} diff --git a/provider/docker/config_container.go b/provider/docker/config_container.go new file mode 100644 index 000000000..12326ca1a --- /dev/null +++ b/provider/docker/config_container.go @@ -0,0 +1,206 @@ +package docker + +import ( + "context" + "strconv" + "strings" + + "github.com/containous/traefik/log" + "github.com/containous/traefik/provider" + "github.com/containous/traefik/provider/label" + "github.com/docker/go-connections/nat" +) + +const ( + labelDockerNetwork = "traefik.docker.network" + labelBackendLoadBalancerSwarm = "traefik.backend.loadbalancer.swarm" + labelDockerComposeProject = "com.docker.compose.project" + labelDockerComposeService = "com.docker.compose.service" +) + +// Specific functions + +func (p Provider) getFrontendName(container dockerData, idx int) string { + return provider.Normalize(p.getFrontendRule(container) + "-" + strconv.Itoa(idx)) +} + +// GetFrontendRule returns the frontend rule for the specified container, using +// it's label. It returns a default one (Host) if the label is not present. +func (p Provider) getFrontendRule(container dockerData) string { + if value := label.GetStringValue(container.Labels, label.TraefikFrontendRule, ""); len(value) != 0 { + return value + } + + if values, err := label.GetStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil { + return "Host:" + getSubDomain(values[labelDockerComposeService]+"."+values[labelDockerComposeProject]) + "." + p.Domain + } + + if len(p.Domain) > 0 { + return "Host:" + getSubDomain(container.ServiceName) + "." + p.Domain + } + + return "" +} + +func (p Provider) getIPAddress(container dockerData) string { + + if value := label.GetStringValue(container.Labels, labelDockerNetwork, ""); value != "" { + networkSettings := container.NetworkSettings + if networkSettings.Networks != nil { + network := networkSettings.Networks[value] + if network != nil { + return network.Addr + } + + log.Warnf("Could not find network named '%s' for container '%s'! Maybe you're missing the project's prefix in the label? Defaulting to first available network.", value, container.Name) + } + } + + if container.NetworkSettings.NetworkMode.IsHost() { + if container.Node != nil { + if container.Node.IPAddress != "" { + return container.Node.IPAddress + } + } + return "127.0.0.1" + } + + if container.NetworkSettings.NetworkMode.IsContainer() { + dockerClient, err := p.createClient() + if err != nil { + log.Warnf("Unable to get IP address for container %s, error: %s", container.Name, err) + return "" + } + + connectedContainer := container.NetworkSettings.NetworkMode.ConnectedContainer() + containerInspected, err := dockerClient.ContainerInspect(context.Background(), connectedContainer) + if err != nil { + log.Warnf("Unable to get IP address for container %s : Failed to inspect container ID %s, error: %s", container.Name, connectedContainer, err) + return "" + } + return p.getIPAddress(parseContainer(containerInspected)) + } + + if p.UseBindPortIP { + port := getPort(container) + for netPort, portBindings := range container.NetworkSettings.Ports { + if string(netPort) == port+"/TCP" || string(netPort) == port+"/UDP" { + for _, p := range portBindings { + return p.HostIP + } + } + } + } + + for _, network := range container.NetworkSettings.Networks { + return network.Addr + } + return "" +} + +func hasLoadBalancerLabel(container dockerData) bool { + method := label.Has(container.Labels, label.TraefikBackendLoadBalancerMethod) + sticky := label.Has(container.Labels, label.TraefikBackendLoadBalancerSticky) + stickiness := label.Has(container.Labels, label.TraefikBackendLoadBalancerStickiness) + cookieName := label.Has(container.Labels, label.TraefikBackendLoadBalancerStickinessCookieName) + return method || sticky || stickiness || cookieName +} + +func hasMaxConnLabels(container dockerData) bool { + mca := label.Has(container.Labels, label.TraefikBackendMaxConnAmount) + mcef := label.Has(container.Labels, label.TraefikBackendMaxConnExtractorFunc) + return mca && mcef +} + +func getBackend(container dockerData) string { + if value := label.GetStringValue(container.Labels, label.TraefikBackend, ""); len(value) != 0 { + return provider.Normalize(value) + } + + if values, err := label.GetStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil { + return provider.Normalize(values[labelDockerComposeService] + "_" + values[labelDockerComposeProject]) + } + + return provider.Normalize(container.ServiceName) +} + +func getPort(container dockerData) string { + if value := label.GetStringValue(container.Labels, label.TraefikPort, ""); len(value) != 0 { + return value + } + + // See iteration order in https://blog.golang.org/go-maps-in-action + var ports []nat.Port + for port := range container.NetworkSettings.Ports { + ports = append(ports, port) + } + + less := func(i, j nat.Port) bool { + return i.Int() < j.Int() + } + nat.Sort(ports, less) + + if len(ports) > 0 { + min := ports[0] + return min.Port() + } + + return "" +} + +// Escape beginning slash "/", convert all others to dash "-", and convert underscores "_" to dash "-" +func getSubDomain(name string) string { + return strings.Replace(strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1), "_", "-", -1) +} + +// TODO: Deprecated +// Deprecated replaced by Stickiness +func getSticky(container dockerData) string { + if label.Has(container.Labels, label.TraefikBackendLoadBalancerSticky) { + log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) + } + + return label.GetStringValue(container.Labels, label.TraefikBackendLoadBalancerSticky, "false") +} + +func isBackendLBSwarm(container dockerData) bool { + return label.GetBoolValue(container.Labels, labelBackendLoadBalancerSwarm, false) +} + +// Label functions + +func getFuncInt64Label(labelName string, defaultValue int64) func(container dockerData) int64 { + return func(container dockerData) int64 { + return label.GetInt64Value(container.Labels, labelName, defaultValue) + } +} + +func getFuncMapLabel(labelName string) func(container dockerData) map[string]string { + return func(container dockerData) map[string]string { + return label.GetMapValue(container.Labels, labelName) + } +} + +func getFuncStringLabel(labelName string, defaultValue string) func(container dockerData) string { + return func(container dockerData) string { + return label.GetStringValue(container.Labels, labelName, defaultValue) + } +} + +func getFuncBoolLabel(labelName string, defaultValue bool) func(container dockerData) bool { + return func(container dockerData) bool { + return label.GetBoolValue(container.Labels, labelName, defaultValue) + } +} + +func getFuncSliceStringLabel(labelName string) func(container dockerData) []string { + return func(container dockerData) []string { + return label.GetSliceStringValue(container.Labels, labelName) + } +} + +func hasFunc(labelName string) func(container dockerData) bool { + return func(container dockerData) bool { + return label.Has(container.Labels, labelName) + } +} diff --git a/provider/docker/docker_test.go b/provider/docker/config_container_docker_test.go similarity index 75% rename from provider/docker/docker_test.go rename to provider/docker/config_container_docker_test.go index 101301db3..b4ba6c283 100644 --- a/provider/docker/docker_test.go +++ b/provider/docker/config_container_docker_test.go @@ -3,371 +3,206 @@ package docker import ( "reflect" "strconv" - "strings" "testing" + "github.com/containous/traefik/provider/label" "github.com/containous/traefik/types" docker "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/go-connections/nat" - "github.com/stretchr/testify/assert" ) -func TestDockerGetFrontendName(t *testing.T) { +func TestDockerLoadDockerConfig(t *testing.T) { testCases := []struct { - container docker.ContainerJSON - expected string + containers []docker.ContainerJSON + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend }{ { - container: containerJSON(name("foo")), - expected: "Host-foo-docker-localhost-0", + containers: []docker.ContainerJSON{}, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, }, { - container: containerJSON(labels(map[string]string{ - types.LabelFrontendRule: "Headers:User-Agent,bat/0.1.0", - })), - expected: "Headers-User-Agent-bat-0-1-0-0", - }, - { - container: containerJSON(labels(map[string]string{ - "com.docker.compose.project": "foo", - "com.docker.compose.service": "bar", - })), - expected: "Host-bar-foo-docker-localhost-0", - }, - { - container: containerJSON(labels(map[string]string{ - types.LabelFrontendRule: "Host:foo.bar", - })), - expected: "Host-foo-bar-0", - }, - { - container: containerJSON(labels(map[string]string{ - types.LabelFrontendRule: "Path:/test", - })), - expected: "Path-test-0", - }, - { - container: containerJSON(labels(map[string]string{ - types.LabelFrontendRule: "PathPrefix:/test2", - })), - expected: "PathPrefix-test2-0", - }, - } - - for containerID, test := range testCases { - test := test - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(test.container) - provider := &Provider{ - Domain: "docker.localhost", - } - actual := provider.getFrontendName(dockerData, 0) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } - }) - } -} - -func TestDockerGetFrontendRule(t *testing.T) { - testCases := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: containerJSON(name("foo")), - expected: "Host:foo.docker.localhost", - }, - { - container: containerJSON(name("bar")), - expected: "Host:bar.docker.localhost", - }, - { - container: containerJSON(labels(map[string]string{ - types.LabelFrontendRule: "Host:foo.bar", - })), - expected: "Host:foo.bar", - }, { - container: containerJSON(labels(map[string]string{ - "com.docker.compose.project": "foo", - "com.docker.compose.service": "bar", - })), - expected: "Host:bar.foo.docker.localhost", - }, - { - container: containerJSON(labels(map[string]string{ - types.LabelFrontendRule: "Path:/test", - })), - expected: "Path:/test", - }, - } - - for containerID, test := range testCases { - test := test - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(test.container) - provider := &Provider{ - Domain: "docker.localhost", - } - actual := provider.getFrontendRule(dockerData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } - }) - } -} - -func TestDockerGetBackend(t *testing.T) { - testCases := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: containerJSON(name("foo")), - expected: "foo", - }, - { - container: containerJSON(name("bar")), - expected: "bar", - }, - { - container: containerJSON(labels(map[string]string{ - types.LabelBackend: "foobar", - })), - expected: "foobar", - }, - { - container: containerJSON(labels(map[string]string{ - "com.docker.compose.project": "foo", - "com.docker.compose.service": "bar", - })), - expected: "bar-foo", - }, - } - - for containerID, test := range testCases { - test := test - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(test.container) - actual := getBackend(dockerData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } - }) - } -} - -func TestDockerGetIPAddress(t *testing.T) { - testCases := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: containerJSON(withNetwork("testnet", ipv4("10.11.12.13"))), - expected: "10.11.12.13", - }, - { - container: containerJSON( - labels(map[string]string{ - labelDockerNetwork: "testnet", - }), - withNetwork("testnet", ipv4("10.11.12.13")), - ), - expected: "10.11.12.13", - }, - { - container: containerJSON( - labels(map[string]string{ - labelDockerNetwork: "testnet2", - }), - withNetwork("testnet", ipv4("10.11.12.13")), - withNetwork("testnet2", ipv4("10.11.12.14")), - ), - expected: "10.11.12.14", - }, - { - container: containerJSON( - networkMode("host"), - withNetwork("testnet", ipv4("10.11.12.13")), - withNetwork("testnet2", ipv4("10.11.12.14")), - ), - expected: "127.0.0.1", - }, - { - container: containerJSON( - networkMode("host"), - ), - expected: "127.0.0.1", - }, - { - container: containerJSON( - networkMode("host"), - nodeIP("10.0.0.5"), - ), - expected: "10.0.0.5", - }, - } - - for containerID, test := range testCases { - test := test - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(test.container) - provider := &Provider{} - actual := provider.getIPAddress(dockerData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } - }) - } -} - -func TestDockerGetPort(t *testing.T) { - testCases := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: containerJSON(name("foo")), - expected: "", - }, - { - container: containerJSON(ports(nat.PortMap{ - "80/tcp": {}, - })), - expected: "80", - }, - { - container: containerJSON(ports(nat.PortMap{ - "80/tcp": {}, - "443/tcp": {}, - })), - expected: "80", - }, - { - container: containerJSON(labels(map[string]string{ - types.LabelPort: "8080", - })), - expected: "8080", - }, - { - container: containerJSON(labels(map[string]string{ - types.LabelPort: "8080", - }), ports(nat.PortMap{ - "80/tcp": {}, - })), - expected: "8080", - }, - { - container: containerJSON(labels(map[string]string{ - types.LabelPort: "8080", - }), ports(nat.PortMap{ - "8080/tcp": {}, - "80/tcp": {}, - })), - expected: "8080", - }, - } - - for containerID, e := range testCases { - e := e - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(e.container) - actual := getPort(dockerData) - if actual != e.expected { - t.Errorf("expected %q, got %q", e.expected, actual) - } - }) - } -} - -func TestDockerGetLabel(t *testing.T) { - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: containerJSON(), - expected: "label not found:", - }, - { - container: containerJSON(labels(map[string]string{ - "foo": "bar", - })), - expected: "", - }, - } - - for containerID, test := range containers { - test := test - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(test.container) - label, err := getLabel(dockerData, "foo") - if test.expected != "" { - if err == nil || !strings.Contains(err.Error(), test.expected) { - t.Errorf("expected an error with %q, got %v", test.expected, err) - } - } else { - if label != "bar" { - t.Errorf("expected label 'bar', got %s", label) - } - } - }) - } -} - -func TestDockerGetLabels(t *testing.T) { - testCases := []struct { - container docker.ContainerJSON - expectedLabels map[string]string - expectedError string - }{ - { - container: containerJSON(), - expectedLabels: map[string]string{}, - expectedError: "label not found:", - }, - { - container: containerJSON(labels(map[string]string{ - "foo": "fooz", - })), - expectedLabels: map[string]string{ - "foo": "fooz", + containers: []docker.ContainerJSON{ + containerJSON( + name("test"), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-test-docker-localhost-0": { + Backend: "backend-test", + PassHostHeader: true, + EntryPoints: []string{}, + BasicAuth: []string{}, + Redirect: "", + Routes: map[string]types.Route{ + "route-frontend-Host-test-docker-localhost-0": { + Rule: "Host:test.docker.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test": { + Servers: map[string]types.Server{ + "server-test": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + CircuitBreaker: nil, + }, }, - expectedError: "label not found: bar", }, { - container: containerJSON(labels(map[string]string{ - "foo": "fooz", - "bar": "barz", - })), - expectedLabels: map[string]string{ - "foo": "fooz", - "bar": "barz", + containers: []docker.ContainerJSON{ + containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikBackend: "foobar", + label.TraefikFrontendEntryPoints: "http,https", + label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.TraefikFrontendRedirect: "https", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + containerJSON( + name("test2"), + labels(map[string]string{ + label.TraefikBackend: "foobar", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-test1-docker-localhost-0": { + Backend: "backend-foobar", + PassHostHeader: true, + EntryPoints: []string{"http", "https"}, + BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + Redirect: "https", + Routes: map[string]types.Route{ + "route-frontend-Host-test1-docker-localhost-0": { + Rule: "Host:test1.docker.localhost", + }, + }, + }, + "frontend-Host-test2-docker-localhost-1": { + Backend: "backend-foobar", + PassHostHeader: true, + EntryPoints: []string{}, + BasicAuth: []string{}, + Redirect: "", + Routes: map[string]types.Route{ + "route-frontend-Host-test2-docker-localhost-1": { + Rule: "Host:test2.docker.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foobar": { + Servers: map[string]types.Server{ + "server-test1": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + "server-test2": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + containers: []docker.ContainerJSON{ + containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikBackend: "foobar", + label.TraefikFrontendEntryPoints: "http,https", + label.TraefikBackendMaxConnAmount: "1000", + label.TraefikBackendMaxConnExtractorFunc: "somethingelse", + label.TraefikBackendLoadBalancerMethod: "drr", + label.TraefikBackendCircuitBreakerExpression: "NetworkErrorRatio() > 0.5", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-test1-docker-localhost-0": { + Backend: "backend-foobar", + PassHostHeader: true, + EntryPoints: []string{"http", "https"}, + BasicAuth: []string{}, + Redirect: "", + Routes: map[string]types.Route{ + "route-frontend-Host-test1-docker-localhost-0": { + Rule: "Host:test1.docker.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foobar": { + Servers: map[string]types.Server{ + "server-test1": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + CircuitBreaker: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", + }, + LoadBalancer: &types.LoadBalancer{ + Method: "drr", + }, + MaxConn: &types.MaxConn{ + Amount: 1000, + ExtractorFunc: "somethingelse", + }, + }, }, - expectedError: "", }, } - for containerID, test := range testCases { + for caseID, test := range testCases { test := test - t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Run(strconv.Itoa(caseID), func(t *testing.T) { t.Parallel() - dockerData := parseContainer(test.container) - labels, err := getLabels(dockerData, []string{"foo", "bar"}) - if !reflect.DeepEqual(labels, test.expectedLabels) { - t.Errorf("expect %v, got %v", test.expectedLabels, labels) + var dockerDataList []dockerData + for _, cont := range test.containers { + dData := parseContainer(cont) + dockerDataList = append(dockerDataList, dData) } - if test.expectedError != "" { - if err == nil || !strings.Contains(err.Error(), test.expectedError) { - t.Errorf("expected an error with %q, got %v", test.expectedError, err) - } + + provider := &Provider{ + Domain: "docker.localhost", + ExposedByDefault: true, + } + actualConfig := provider.buildConfiguration(dockerDataList) + // Compare backends + if !reflect.DeepEqual(actualConfig.Backends, test.expectedBackends) { + t.Errorf("expected %#v, got %#v", test.expectedBackends, actualConfig.Backends) + } + if !reflect.DeepEqual(actualConfig.Frontends, test.expectedFrontends) { + t.Errorf("expected %#v, got %#v", test.expectedFrontends, actualConfig.Frontends) } }) } @@ -400,7 +235,7 @@ func TestDockerTraefikFilter(t *testing.T) { }, Config: &container.Config{ Labels: map[string]string{ - types.LabelEnable: "false", + label.TraefikEnable: "false", }, }, NetworkSettings: &docker.NetworkSettings{ @@ -424,7 +259,7 @@ func TestDockerTraefikFilter(t *testing.T) { }, Config: &container.Config{ Labels: map[string]string{ - types.LabelFrontendRule: "Host:foo.bar", + label.TraefikFrontendRule: "Host:foo.bar", }, }, NetworkSettings: &docker.NetworkSettings{ @@ -489,7 +324,7 @@ func TestDockerTraefikFilter(t *testing.T) { }, Config: &container.Config{ Labels: map[string]string{ - types.LabelPort: "80", + label.TraefikPort: "80", }, }, NetworkSettings: &docker.NetworkSettings{ @@ -514,7 +349,7 @@ func TestDockerTraefikFilter(t *testing.T) { }, Config: &container.Config{ Labels: map[string]string{ - types.LabelEnable: "true", + label.TraefikEnable: "true", }, }, NetworkSettings: &docker.NetworkSettings{ @@ -538,7 +373,7 @@ func TestDockerTraefikFilter(t *testing.T) { }, Config: &container.Config{ Labels: map[string]string{ - types.LabelEnable: "anything", + label.TraefikEnable: "anything", }, }, NetworkSettings: &docker.NetworkSettings{ @@ -562,7 +397,7 @@ func TestDockerTraefikFilter(t *testing.T) { }, Config: &container.Config{ Labels: map[string]string{ - types.LabelFrontendRule: "Host:foo.bar", + label.TraefikFrontendRule: "Host:foo.bar", }, }, NetworkSettings: &docker.NetworkSettings{ @@ -606,7 +441,7 @@ func TestDockerTraefikFilter(t *testing.T) { }, Config: &container.Config{ Labels: map[string]string{ - types.LabelEnable: "true", + label.TraefikEnable: "true", }, }, NetworkSettings: &docker.NetworkSettings{ @@ -630,7 +465,7 @@ func TestDockerTraefikFilter(t *testing.T) { }, Config: &container.Config{ Labels: map[string]string{ - types.LabelEnable: "true", + label.TraefikEnable: "true", }, }, NetworkSettings: &docker.NetworkSettings{ @@ -653,8 +488,8 @@ func TestDockerTraefikFilter(t *testing.T) { }, Config: &container.Config{ Labels: map[string]string{ - types.LabelEnable: "true", - types.LabelFrontendRule: "Host:i.love.this.host", + label.TraefikEnable: "true", + label.TraefikFrontendRule: "Host:i.love.this.host", }, }, NetworkSettings: &docker.NetworkSettings{ @@ -676,8 +511,8 @@ func TestDockerTraefikFilter(t *testing.T) { test := test t.Run(strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() - dockerData := parseContainer(test.container) - actual := test.provider.containerFilter(dockerData) + dData := parseContainer(test.container) + actual := test.provider.containerFilter(dData) if actual != test.expected { t.Errorf("expected %v for %+v, got %+v", test.expected, test, actual) } @@ -685,226 +520,95 @@ func TestDockerTraefikFilter(t *testing.T) { } } -func TestDockerLoadDockerConfig(t *testing.T) { +func TestDockerGetFuncStringLabel(t *testing.T) { testCases := []struct { - containers []docker.ContainerJSON - expectedFrontends map[string]*types.Frontend - expectedBackends map[string]*types.Backend + container docker.ContainerJSON + labelName string + defaultValue string + expected string }{ { - containers: []docker.ContainerJSON{}, - expectedFrontends: map[string]*types.Frontend{}, - expectedBackends: map[string]*types.Backend{}, + container: containerJSON(), + labelName: label.TraefikWeight, + defaultValue: label.DefaultWeight, + expected: "0", }, { - containers: []docker.ContainerJSON{ - containerJSON( - name("test"), - ports(nat.PortMap{ - "80/tcp": {}, - }), - withNetwork("bridge", ipv4("127.0.0.1")), - ), - }, - expectedFrontends: map[string]*types.Frontend{ - "frontend-Host-test-docker-localhost-0": { - Backend: "backend-test", - PassHostHeader: true, - EntryPoints: []string{}, - BasicAuth: []string{}, - Redirect: "", - Routes: map[string]types.Route{ - "route-frontend-Host-test-docker-localhost-0": { - Rule: "Host:test.docker.localhost", - }, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-test": { - Servers: map[string]types.Server{ - "server-test": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - }, - CircuitBreaker: nil, - }, - }, - }, - { - containers: []docker.ContainerJSON{ - containerJSON( - name("test1"), - labels(map[string]string{ - types.LabelBackend: "foobar", - types.LabelFrontendEntryPoints: "http,https", - types.LabelFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", - types.LabelFrontendRedirect: "https", - }), - ports(nat.PortMap{ - "80/tcp": {}, - }), - withNetwork("bridge", ipv4("127.0.0.1")), - ), - containerJSON( - name("test2"), - labels(map[string]string{ - types.LabelBackend: "foobar", - }), - ports(nat.PortMap{ - "80/tcp": {}, - }), - withNetwork("bridge", ipv4("127.0.0.1")), - ), - }, - expectedFrontends: map[string]*types.Frontend{ - "frontend-Host-test1-docker-localhost-0": { - Backend: "backend-foobar", - PassHostHeader: true, - EntryPoints: []string{"http", "https"}, - BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, - Redirect: "https", - Routes: map[string]types.Route{ - "route-frontend-Host-test1-docker-localhost-0": { - Rule: "Host:test1.docker.localhost", - }, - }, - }, - "frontend-Host-test2-docker-localhost-1": { - Backend: "backend-foobar", - PassHostHeader: true, - EntryPoints: []string{}, - BasicAuth: []string{}, - Redirect: "", - Routes: map[string]types.Route{ - "route-frontend-Host-test2-docker-localhost-1": { - Rule: "Host:test2.docker.localhost", - }, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-foobar": { - Servers: map[string]types.Server{ - "server-test1": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - "server-test2": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - }, - CircuitBreaker: nil, - }, - }, - }, - { - containers: []docker.ContainerJSON{ - containerJSON( - name("test1"), - labels(map[string]string{ - types.LabelBackend: "foobar", - types.LabelFrontendEntryPoints: "http,https", - types.LabelBackendMaxconnAmount: "1000", - types.LabelBackendMaxconnExtractorfunc: "somethingelse", - types.LabelBackendLoadbalancerMethod: "drr", - types.LabelBackendCircuitbreakerExpression: "NetworkErrorRatio() > 0.5", - }), - ports(nat.PortMap{ - "80/tcp": {}, - }), - withNetwork("bridge", ipv4("127.0.0.1")), - ), - }, - expectedFrontends: map[string]*types.Frontend{ - "frontend-Host-test1-docker-localhost-0": { - Backend: "backend-foobar", - PassHostHeader: true, - EntryPoints: []string{"http", "https"}, - BasicAuth: []string{}, - Redirect: "", - Routes: map[string]types.Route{ - "route-frontend-Host-test1-docker-localhost-0": { - Rule: "Host:test1.docker.localhost", - }, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-foobar": { - Servers: map[string]types.Server{ - "server-test1": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - }, - CircuitBreaker: &types.CircuitBreaker{ - Expression: "NetworkErrorRatio() > 0.5", - }, - LoadBalancer: &types.LoadBalancer{ - Method: "drr", - }, - MaxConn: &types.MaxConn{ - Amount: 1000, - ExtractorFunc: "somethingelse", - }, - }, - }, + container: containerJSON(labels(map[string]string{ + label.TraefikWeight: "10", + })), + labelName: label.TraefikWeight, + defaultValue: label.DefaultWeight, + expected: "10", }, } - for caseID, test := range testCases { + for containerID, test := range testCases { test := test - t.Run(strconv.Itoa(caseID), func(t *testing.T) { + t.Run(test.labelName+strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() - var dockerDataList []dockerData - for _, cont := range test.containers { - dockerData := parseContainer(cont) - dockerDataList = append(dockerDataList, dockerData) - } - provider := &Provider{ - Domain: "docker.localhost", - ExposedByDefault: true, - } - actualConfig := provider.loadDockerConfig(dockerDataList) - // Compare backends - if !reflect.DeepEqual(actualConfig.Backends, test.expectedBackends) { - t.Errorf("expected %#v, got %#v", test.expectedBackends, actualConfig.Backends) - } - if !reflect.DeepEqual(actualConfig.Frontends, test.expectedFrontends) { - t.Errorf("expected %#v, got %#v", test.expectedFrontends, actualConfig.Frontends) + dData := parseContainer(test.container) + + actual := getFuncStringLabel(test.labelName, test.defaultValue)(dData) + + if actual != test.expected { + t.Errorf("got %q, expected %q", actual, test.expected) } }) } } -func TestDockerHasStickinessLabel(t *testing.T) { +func TestDockerGetSliceStringLabel(t *testing.T) { testCases := []struct { desc string container docker.ContainerJSON - expected bool + labelName string + expected []string }{ { - desc: "no stickiness-label", + desc: "no whitelist-label", container: containerJSON(), - expected: false, + expected: nil, }, { - desc: "stickiness true", + desc: "whitelist-label with empty string", container: containerJSON(labels(map[string]string{ - types.LabelBackendLoadbalancerStickiness: "true", + label.TraefikFrontendWhitelistSourceRange: "", })), - expected: true, + labelName: label.TraefikFrontendWhitelistSourceRange, + expected: nil, }, { - desc: "stickiness false", + desc: "whitelist-label with IPv4 mask", container: containerJSON(labels(map[string]string{ - types.LabelBackendLoadbalancerStickiness: "false", + label.TraefikFrontendWhitelistSourceRange: "1.2.3.4/16", })), - expected: false, + labelName: label.TraefikFrontendWhitelistSourceRange, + expected: []string{ + "1.2.3.4/16", + }, + }, + { + desc: "whitelist-label with IPv6 mask", + container: containerJSON(labels(map[string]string{ + label.TraefikFrontendWhitelistSourceRange: "fe80::/16", + })), + labelName: label.TraefikFrontendWhitelistSourceRange, + expected: []string{ + "fe80::/16", + }, + }, + { + desc: "whitelist-label with multiple masks", + container: containerJSON(labels(map[string]string{ + label.TraefikFrontendWhitelistSourceRange: "1.1.1.1/24, 1234:abcd::42/32", + })), + labelName: label.TraefikFrontendWhitelistSourceRange, + expected: []string{ + "1.1.1.1/24", + "1234:abcd::42/32", + }, }, } @@ -912,43 +616,56 @@ func TestDockerHasStickinessLabel(t *testing.T) { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() - dockerData := parseContainer(test.container) - actual := hasStickinessLabel(dockerData) - assert.Equal(t, actual, test.expected) + dData := parseContainer(test.container) + + actual := getFuncSliceStringLabel(test.labelName)(dData) + + if !reflect.DeepEqual(actual, test.expected) { + t.Errorf("expected %q, got %q", test.expected, actual) + } }) } } -func TestDockerCheckPortLabels(t *testing.T) { +func TestDockerGetFrontendName(t *testing.T) { testCases := []struct { - container docker.ContainerJSON - expectedError bool + container docker.ContainerJSON + expected string }{ { - container: containerJSON(labels(map[string]string{ - types.LabelPort: "80", - })), - expectedError: false, + container: containerJSON(name("foo")), + expected: "Host-foo-docker-localhost-0", }, { container: containerJSON(labels(map[string]string{ - types.LabelPrefix + "servicename.protocol": "http", - types.LabelPrefix + "servicename.port": "80", + label.TraefikFrontendRule: "Headers:User-Agent,bat/0.1.0", })), - expectedError: false, + expected: "Headers-User-Agent-bat-0-1-0-0", }, { container: containerJSON(labels(map[string]string{ - types.LabelPrefix + "servicename.protocol": "http", - types.LabelPort: "80", + "com.docker.compose.project": "foo", + "com.docker.compose.service": "bar", })), - expectedError: false, + expected: "Host-bar-foo-docker-localhost-0", }, { container: containerJSON(labels(map[string]string{ - types.LabelPrefix + "servicename.protocol": "http", + label.TraefikFrontendRule: "Host:foo.bar", })), - expectedError: true, + expected: "Host-foo-bar-0", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikFrontendRule: "Path:/test", + })), + expected: "Path-test-0", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikFrontendRule: "PathPrefix:/test2", + })), + expected: "PathPrefix-test2-0", }, } @@ -956,14 +673,228 @@ func TestDockerCheckPortLabels(t *testing.T) { test := test t.Run(strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() - - dockerData := parseContainer(test.container) - err := checkServiceLabelPort(dockerData) - - if test.expectedError && err == nil { - t.Error("expected an error but got nil") - } else if !test.expectedError && err != nil { - t.Errorf("expected no error, got %q", err) + dData := parseContainer(test.container) + provider := &Provider{ + Domain: "docker.localhost", + } + actual := provider.getFrontendName(dData, 0) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestDockerGetFrontendRule(t *testing.T) { + testCases := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(name("foo")), + expected: "Host:foo.docker.localhost", + }, + { + container: containerJSON(name("bar")), + expected: "Host:bar.docker.localhost", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikFrontendRule: "Host:foo.bar", + })), + expected: "Host:foo.bar", + }, { + container: containerJSON(labels(map[string]string{ + "com.docker.compose.project": "foo", + "com.docker.compose.service": "bar", + })), + expected: "Host:bar.foo.docker.localhost", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikFrontendRule: "Path:/test", + })), + expected: "Path:/test", + }, + } + + for containerID, test := range testCases { + test := test + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dData := parseContainer(test.container) + provider := &Provider{ + Domain: "docker.localhost", + } + actual := provider.getFrontendRule(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestDockerGetBackend(t *testing.T) { + testCases := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(name("foo")), + expected: "foo", + }, + { + container: containerJSON(name("bar")), + expected: "bar", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikBackend: "foobar", + })), + expected: "foobar", + }, + { + container: containerJSON(labels(map[string]string{ + "com.docker.compose.project": "foo", + "com.docker.compose.service": "bar", + })), + expected: "bar-foo", + }, + } + + for containerID, test := range testCases { + test := test + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dData := parseContainer(test.container) + actual := getBackend(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestDockerGetIPAddress(t *testing.T) { + testCases := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(withNetwork("testnet", ipv4("10.11.12.13"))), + expected: "10.11.12.13", + }, + { + container: containerJSON( + labels(map[string]string{ + labelDockerNetwork: "testnet", + }), + withNetwork("testnet", ipv4("10.11.12.13")), + ), + expected: "10.11.12.13", + }, + { + container: containerJSON( + labels(map[string]string{ + labelDockerNetwork: "testnet2", + }), + withNetwork("testnet", ipv4("10.11.12.13")), + withNetwork("testnet2", ipv4("10.11.12.14")), + ), + expected: "10.11.12.14", + }, + { + container: containerJSON( + networkMode("host"), + withNetwork("testnet", ipv4("10.11.12.13")), + withNetwork("testnet2", ipv4("10.11.12.14")), + ), + expected: "127.0.0.1", + }, + { + container: containerJSON( + networkMode("host"), + ), + expected: "127.0.0.1", + }, + { + container: containerJSON( + networkMode("host"), + nodeIP("10.0.0.5"), + ), + expected: "10.0.0.5", + }, + } + + for containerID, test := range testCases { + test := test + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dData := parseContainer(test.container) + provider := &Provider{} + actual := provider.getIPAddress(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestDockerGetPort(t *testing.T) { + testCases := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(name("foo")), + expected: "", + }, + { + container: containerJSON(ports(nat.PortMap{ + "80/tcp": {}, + })), + expected: "80", + }, + { + container: containerJSON(ports(nat.PortMap{ + "80/tcp": {}, + "443/tcp": {}, + })), + expected: "80", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikPort: "8080", + })), + expected: "8080", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikPort: "8080", + }), ports(nat.PortMap{ + "80/tcp": {}, + })), + expected: "8080", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikPort: "8080", + }), ports(nat.PortMap{ + "8080/tcp": {}, + "80/tcp": {}, + })), + expected: "8080", + }, + } + + for containerID, e := range testCases { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dData := parseContainer(e.container) + actual := getPort(dData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) } }) } diff --git a/provider/docker/config_container_swarm_test.go b/provider/docker/config_container_swarm_test.go new file mode 100644 index 000000000..ed3bb18f0 --- /dev/null +++ b/provider/docker/config_container_swarm_test.go @@ -0,0 +1,649 @@ +package docker + +import ( + "reflect" + "strconv" + "testing" + + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + docker "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" +) + +func TestSwarmGetFrontendName(t *testing.T) { + testCases := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(serviceName("foo")), + expected: "Host-foo-docker-localhost-0", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Headers:User-Agent,bat/0.1.0", + })), + expected: "Headers-User-Agent-bat-0-1-0-0", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Host:foo.bar", + })), + expected: "Host-foo-bar-0", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Path:/test", + })), + expected: "Path-test-0", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService( + serviceName("test"), + serviceLabels(map[string]string{ + label.TraefikFrontendRule: "PathPrefix:/test2", + }), + ), + expected: "PathPrefix-test2-0", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dData := parseService(test.service, test.networks) + provider := &Provider{ + Domain: "docker.localhost", + SwarmMode: true, + } + actual := provider.getFrontendName(dData, 0) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestSwarmGetFrontendRule(t *testing.T) { + testCases := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(serviceName("foo")), + expected: "Host:foo.docker.localhost", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceName("bar")), + expected: "Host:bar.docker.localhost", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Host:foo.bar", + })), + expected: "Host:foo.bar", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Path:/test", + })), + expected: "Path:/test", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dData := parseService(test.service, test.networks) + provider := &Provider{ + Domain: "docker.localhost", + SwarmMode: true, + } + actual := provider.getFrontendRule(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestSwarmGetBackend(t *testing.T) { + testCases := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(serviceName("foo")), + expected: "foo", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceName("bar")), + expected: "bar", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikBackend: "foobar", + })), + expected: "foobar", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dData := parseService(test.service, test.networks) + actual := getBackend(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestSwarmGetIPAddress(t *testing.T) { + testCases := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(withEndpointSpec(modeDNSSR)), + expected: "", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService( + withEndpointSpec(modeVIP), + withEndpoint(virtualIP("1", "10.11.12.13/24")), + ), + expected: "10.11.12.13", + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foo", + }, + }, + }, + { + service: swarmService( + serviceLabels(map[string]string{ + labelDockerNetwork: "barnet", + }), + withEndpointSpec(modeVIP), + withEndpoint( + virtualIP("1", "10.11.12.13/24"), + virtualIP("2", "10.11.12.99/24"), + ), + ), + expected: "10.11.12.99", + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foonet", + }, + "2": { + Name: "barnet", + }, + }, + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dData := parseService(test.service, test.networks) + provider := &Provider{ + SwarmMode: true, + } + actual := provider.getIPAddress(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestSwarmGetPort(t *testing.T) { + testCases := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService( + serviceLabels(map[string]string{ + label.TraefikPort: "8080", + }), + withEndpointSpec(modeDNSSR), + ), + expected: "8080", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dData := parseService(test.service, test.networks) + actual := getPort(dData) + if actual != test.expected { + t.Errorf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +func TestSwarmTraefikFilter(t *testing.T) { + testCases := []struct { + service swarm.Service + expected bool + networks map[string]*docker.NetworkResource + provider *Provider + }{ + { + service: swarmService(), + expected: false, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: true, + }, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikEnable: "false", + label.TraefikPort: "80", + })), + expected: false, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: true, + }, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Host:foo.bar", + label.TraefikPort: "80", + })), + expected: true, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: true, + }, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikPort: "80", + })), + expected: true, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: true, + }, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikEnable: "true", + label.TraefikPort: "80", + })), + expected: true, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: true, + }, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikEnable: "anything", + label.TraefikPort: "80", + })), + expected: true, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: true, + }, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikFrontendRule: "Host:foo.bar", + label.TraefikPort: "80", + })), + expected: true, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: true, + }, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikPort: "80", + })), + expected: false, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: false, + }, + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikEnable: "true", + label.TraefikPort: "80", + })), + expected: true, + networks: map[string]*docker.NetworkResource{}, + provider: &Provider{ + SwarmMode: true, + Domain: "test", + ExposedByDefault: false, + }, + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dData := parseService(test.service, test.networks) + actual := test.provider.containerFilter(dData) + if actual != test.expected { + t.Errorf("expected %v for %+v, got %+v", test.expected, test, actual) + } + }) + } +} + +func TestSwarmLoadDockerConfig(t *testing.T) { + testCases := []struct { + services []swarm.Service + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + networks map[string]*docker.NetworkResource + }{ + { + services: []swarm.Service{}, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + networks: map[string]*docker.NetworkResource{}, + }, + { + services: []swarm.Service{ + swarmService( + serviceName("test"), + serviceLabels(map[string]string{ + label.TraefikPort: "80", + }), + withEndpointSpec(modeVIP), + withEndpoint(virtualIP("1", "127.0.0.1/24")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-test-docker-localhost-0": { + Backend: "backend-test", + PassHostHeader: true, + EntryPoints: []string{}, + BasicAuth: []string{}, + Redirect: "", + Routes: map[string]types.Route{ + "route-frontend-Host-test-docker-localhost-0": { + Rule: "Host:test.docker.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test": { + Servers: map[string]types.Server{ + "server-test": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + CircuitBreaker: nil, + LoadBalancer: nil, + }, + }, + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foo", + }, + }, + }, + { + services: []swarm.Service{ + swarmService( + serviceName("test1"), + serviceLabels(map[string]string{ + label.TraefikPort: "80", + label.TraefikBackend: "foobar", + label.TraefikFrontendEntryPoints: "http,https", + label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.TraefikFrontendRedirect: "https", + }), + withEndpointSpec(modeVIP), + withEndpoint(virtualIP("1", "127.0.0.1/24")), + ), + swarmService( + serviceName("test2"), + serviceLabels(map[string]string{ + label.TraefikPort: "80", + label.TraefikBackend: "foobar", + }), + withEndpointSpec(modeVIP), + withEndpoint(virtualIP("1", "127.0.0.1/24")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-test1-docker-localhost-0": { + Backend: "backend-foobar", + PassHostHeader: true, + EntryPoints: []string{"http", "https"}, + BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + Redirect: "https", + Routes: map[string]types.Route{ + "route-frontend-Host-test1-docker-localhost-0": { + Rule: "Host:test1.docker.localhost", + }, + }, + }, + "frontend-Host-test2-docker-localhost-1": { + Backend: "backend-foobar", + PassHostHeader: true, + EntryPoints: []string{}, + BasicAuth: []string{}, + Redirect: "", + Routes: map[string]types.Route{ + "route-frontend-Host-test2-docker-localhost-1": { + Rule: "Host:test2.docker.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foobar": { + Servers: map[string]types.Server{ + "server-test1": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + "server-test2": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + CircuitBreaker: nil, + LoadBalancer: nil, + }, + }, + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foo", + }, + }, + }, + } + + for caseID, test := range testCases { + test := test + t.Run(strconv.Itoa(caseID), func(t *testing.T) { + t.Parallel() + var dockerDataList []dockerData + for _, service := range test.services { + dData := parseService(service, test.networks) + dockerDataList = append(dockerDataList, dData) + } + + provider := &Provider{ + Domain: "docker.localhost", + ExposedByDefault: true, + SwarmMode: true, + } + actualConfig := provider.buildConfiguration(dockerDataList) + // Compare backends + if !reflect.DeepEqual(actualConfig.Backends, test.expectedBackends) { + t.Errorf("expected %#v, got %#v", test.expectedBackends, actualConfig.Backends) + } + if !reflect.DeepEqual(actualConfig.Frontends, test.expectedFrontends) { + t.Errorf("expected %#v, got %#v", test.expectedFrontends, actualConfig.Frontends) + } + }) + } +} + +func TestSwarmTaskParsing(t *testing.T) { + testCases := []struct { + service swarm.Service + tasks []swarm.Task + isGlobalSVC bool + expectedNames map[string]string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(serviceName("container")), + tasks: []swarm.Task{ + swarmTask("id1", taskSlot(1)), + swarmTask("id2", taskSlot(2)), + swarmTask("id3", taskSlot(3)), + }, + isGlobalSVC: false, + expectedNames: map[string]string{ + "id1": "container.1", + "id2": "container.2", + "id3": "container.3", + }, + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foo", + }, + }, + }, + { + service: swarmService(serviceName("container")), + tasks: []swarm.Task{ + swarmTask("id1"), + swarmTask("id2"), + swarmTask("id3"), + }, + isGlobalSVC: true, + expectedNames: map[string]string{ + "id1": "container.id1", + "id2": "container.id2", + "id3": "container.id3", + }, + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foo", + }, + }, + }, + } + + for caseID, test := range testCases { + test := test + t.Run(strconv.Itoa(caseID), func(t *testing.T) { + t.Parallel() + dData := parseService(test.service, test.networks) + + for _, task := range test.tasks { + taskDockerData := parseTasks(task, dData, map[string]*docker.NetworkResource{}, test.isGlobalSVC) + if !reflect.DeepEqual(taskDockerData.Name, test.expectedNames[task.ID]) { + t.Errorf("expect %v, got %v", test.expectedNames[task.ID], taskDockerData.Name) + } + } + }) + } +} + +func TestSwarmGetFuncStringLabel(t *testing.T) { + testCases := []struct { + service swarm.Service + labelName string + defaultValue string + networks map[string]*docker.NetworkResource + expected string + }{ + { + service: swarmService(), + labelName: label.TraefikWeight, + defaultValue: label.DefaultWeight, + networks: map[string]*docker.NetworkResource{}, + expected: "0", + }, + { + service: swarmService(serviceLabels(map[string]string{ + label.TraefikWeight: "10", + })), + labelName: label.TraefikWeight, + defaultValue: label.DefaultWeight, + networks: map[string]*docker.NetworkResource{}, + expected: "10", + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(test.labelName+strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + + dData := parseService(test.service, test.networks) + + actual := getFuncStringLabel(test.labelName, test.defaultValue)(dData) + if actual != test.expected { + t.Errorf("got %q, expected %q", actual, test.expected) + } + }) + } +} diff --git a/provider/docker/config_service.go b/provider/docker/config_service.go new file mode 100644 index 000000000..ab0a5244f --- /dev/null +++ b/provider/docker/config_service.go @@ -0,0 +1,133 @@ +package docker + +import ( + "errors" + "strconv" + "strings" + + "github.com/containous/traefik/provider" + "github.com/containous/traefik/provider/label" +) + +// Specific functions + +// Extract rule from labels for a given service and a given docker container +func (p Provider) getServiceFrontendRule(container dockerData, serviceName string) string { + if value, ok := getServiceLabels(container, serviceName)[label.SuffixFrontendRule]; ok { + return value + } + return p.getFrontendRule(container) +} + +// Check if for the given container, we find labels that are defining services +func hasServices(container dockerData) bool { + return len(label.ExtractServiceProperties(container.Labels)) > 0 +} + +// Gets array of service names for a given container +func getServiceNames(container dockerData) []string { + labelServiceProperties := label.ExtractServiceProperties(container.Labels) + keys := make([]string, 0, len(labelServiceProperties)) + for k := range labelServiceProperties { + keys = append(keys, k) + } + return keys +} + +// checkServiceLabelPort checks if all service names have a port service label +// or if port container label exists for default value +func checkServiceLabelPort(container dockerData) error { + // If port container label is present, there is a default values for all ports, use it for the check + _, err := strconv.Atoi(container.Labels[label.TraefikPort]) + if err != nil { + serviceLabelPorts := make(map[string]struct{}) + serviceLabels := make(map[string]struct{}) + for lbl := range container.Labels { + // Get all port service labels + portLabel := label.PortRegexp.FindStringSubmatch(lbl) + if len(portLabel) > 0 { + serviceLabelPorts[portLabel[0]] = struct{}{} + } + // Get only one instance of all service names from service labels + servicesLabelNames := label.ServicesPropertiesRegexp.FindStringSubmatch(lbl) + if len(servicesLabelNames) > 0 { + serviceLabels[strings.Split(servicesLabelNames[0], ".")[1]] = struct{}{} + } + } + // If the number of service labels is different than the number of port services label + // there is an error + if len(serviceLabels) == len(serviceLabelPorts) { + for labelPort := range serviceLabelPorts { + _, err = strconv.Atoi(container.Labels[labelPort]) + if err != nil { + break + } + } + } else { + err = errors.New("port service labels missing, please use traefik.port as default value or define all port service labels") + } + } + return err +} + +// Extract backend from labels for a given service and a given docker container +func getServiceBackend(container dockerData, serviceName string) string { + if value, ok := getServiceLabels(container, serviceName)[label.SuffixFrontendBackend]; ok { + return container.ServiceName + "-" + value + } + return strings.TrimPrefix(container.ServiceName, "/") + "-" + getBackend(container) + "-" + provider.Normalize(serviceName) +} + +// Extract port from labels for a given service and a given docker container +func getServicePort(container dockerData, serviceName string) string { + if value, ok := getServiceLabels(container, serviceName)[label.SuffixPort]; ok { + return value + } + return getPort(container) +} + +// Service label functions + +func getFuncServiceSliceStringLabel(labelSuffix string) func(container dockerData, serviceName string) []string { + return func(container dockerData, serviceName string) []string { + return getServiceSliceStringLabel(container, serviceName, labelSuffix) + } +} + +func getFuncServiceStringLabel(labelSuffix string, defaultValue string) func(container dockerData, serviceName string) string { + return func(container dockerData, serviceName string) string { + return getServiceStringLabel(container, serviceName, labelSuffix, defaultValue) + } +} + +func hasFuncServiceLabel(labelSuffix string) func(container dockerData, serviceName string) bool { + return func(container dockerData, serviceName string) bool { + return hasServiceLabel(container, serviceName, labelSuffix) + } +} + +func hasServiceLabel(container dockerData, serviceName string, labelSuffix string) bool { + value, ok := getServiceLabels(container, serviceName)[labelSuffix] + if ok && len(value) > 0 { + return true + } + return label.Has(container.Labels, label.Prefix+labelSuffix) +} + +func getServiceSliceStringLabel(container dockerData, serviceName string, labelSuffix string) []string { + if value, ok := getServiceLabels(container, serviceName)[labelSuffix]; ok { + return label.SplitAndTrimString(value, ",") + } + return label.GetSliceStringValue(container.Labels, label.Prefix+labelSuffix) +} + +func getServiceStringLabel(container dockerData, serviceName string, labelSuffix string, defaultValue string) string { + if value, ok := getServiceLabels(container, serviceName)[labelSuffix]; ok { + return value + } + return label.GetStringValue(container.Labels, label.Prefix+labelSuffix, defaultValue) +} + +func getServiceLabels(container dockerData, serviceName string) label.ServicePropertyValues { + return label.ExtractServiceProperties(container.Labels)[serviceName] +} diff --git a/provider/docker/config_service_test.go b/provider/docker/config_service_test.go new file mode 100644 index 000000000..54808dc77 --- /dev/null +++ b/provider/docker/config_service_test.go @@ -0,0 +1,149 @@ +package docker + +import ( + "reflect" + "strconv" + "testing" + + "github.com/containous/traefik/provider/label" + docker "github.com/docker/docker/api/types" +) + +func TestDockerGetFuncServiceStringLabel(t *testing.T) { + testCases := []struct { + container docker.ContainerJSON + suffixLabel string + defaultValue string + expected string + }{ + { + container: containerJSON(), + suffixLabel: label.SuffixWeight, + defaultValue: label.DefaultWeight, + expected: "0", + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikWeight: "200", + })), + suffixLabel: label.SuffixWeight, + defaultValue: label.DefaultWeight, + expected: "200", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.myservice.weight": "31337", + })), + suffixLabel: label.SuffixWeight, + defaultValue: label.DefaultWeight, + expected: "31337", + }, + } + + for containerID, test := range testCases { + test := test + t.Run(test.suffixLabel+strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := getFuncServiceStringLabel(test.suffixLabel, test.defaultValue)(dData, "myservice") + if actual != test.expected { + t.Fatalf("got %q, expected %q", actual, test.expected) + } + }) + } +} + +func TestDockerGetFuncServiceSliceStringLabel(t *testing.T) { + testCases := []struct { + container docker.ContainerJSON + suffixLabel string + expected []string + }{ + { + container: containerJSON(), + suffixLabel: label.SuffixFrontendEntryPoints, + expected: nil, + }, + { + container: containerJSON(labels(map[string]string{ + label.TraefikFrontendEntryPoints: "http,https", + })), + suffixLabel: label.SuffixFrontendEntryPoints, + expected: []string{"http", "https"}, + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.myservice.frontend.entryPoints": "http,https", + })), + suffixLabel: label.SuffixFrontendEntryPoints, + expected: []string{"http", "https"}, + }, + } + + for containerID, test := range testCases { + test := test + t.Run(test.suffixLabel+strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := getFuncServiceSliceStringLabel(test.suffixLabel)(dData, "myservice") + + if !reflect.DeepEqual(actual, test.expected) { + t.Fatalf("for container %q: got %q, expected %q", dData.Name, actual, test.expected) + } + }) + } +} + +func TestDockerCheckPortLabels(t *testing.T) { + testCases := []struct { + container docker.ContainerJSON + expectedError bool + }{ + { + container: containerJSON(labels(map[string]string{ + label.TraefikPort: "80", + })), + expectedError: false, + }, + { + container: containerJSON(labels(map[string]string{ + label.Prefix + "servicename.protocol": "http", + label.Prefix + "servicename.port": "80", + })), + expectedError: false, + }, + { + container: containerJSON(labels(map[string]string{ + label.Prefix + "servicename.protocol": "http", + label.TraefikPort: "80", + })), + expectedError: false, + }, + { + container: containerJSON(labels(map[string]string{ + label.Prefix + "servicename.protocol": "http", + })), + expectedError: true, + }, + } + + for containerID, test := range testCases { + test := test + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + err := checkServiceLabelPort(dData) + + if test.expectedError && err == nil { + t.Error("expected an error but got nil") + } else if !test.expectedError && err != nil { + t.Errorf("expected no error, got %q", err) + } + }) + } +} diff --git a/provider/docker/docker.go b/provider/docker/docker.go index 721953425..d6e26da05 100644 --- a/provider/docker/docker.go +++ b/provider/docker/docker.go @@ -2,16 +2,12 @@ package docker import ( "context" - "math" "net" "net/http" - "regexp" "strconv" "strings" - "text/template" "time" - "github.com/BurntSushi/ty/fun" "github.com/cenk/backoff" "github.com/containous/traefik/job" "github.com/containous/traefik/log" @@ -28,7 +24,6 @@ import ( "github.com/docker/docker/client" "github.com/docker/go-connections/nat" "github.com/docker/go-connections/sockets" - "github.com/pkg/errors" ) const ( @@ -36,15 +31,6 @@ const ( SwarmAPIVersion = "1.24" // SwarmDefaultWatchTime is the duration of the interval when polling docker SwarmDefaultWatchTime = 15 * time.Second - - defaultWeight = "0" - defaultProtocol = "http" - defaultPassHostHeader = "true" - defaultFrontendPriority = "0" - defaultCircuitBreakerExpression = "NetworkErrorRatio() > 1" - defaultFrontendRedirect = "" - defaultBackendLoadBalancerMethod = "wrr" - defaultBackendMaxconnExtractorfunc = "request.host" ) var _ provider.Provider = (*Provider)(nil) @@ -160,7 +146,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s } } - configuration := p.loadDockerConfig(dockerDataList) + configuration := p.buildConfiguration(dockerDataList) configurationChan <- types.ConfigMessage{ ProviderName: "docker", Configuration: configuration, @@ -182,7 +168,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s errChan <- err return } - configuration := p.loadDockerConfig(services) + configuration := p.buildConfiguration(services) if configuration != nil { configurationChan <- types.ConfigMessage{ ProviderName: "docker", @@ -227,7 +213,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s cancel() return } - configuration := p.loadDockerConfig(containers) + configuration := p.buildConfiguration(containers) if configuration != nil { configurationChan <- types.ConfigMessage{ ProviderName: "docker", @@ -263,406 +249,10 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s return nil } -func (p *Provider) loadDockerConfig(containersInspected []dockerData) *types.Configuration { - var DockerFuncMap = template.FuncMap{ - "getBackend": getBackend, - "getIPAddress": p.getIPAddress, - "getPort": getPort, - "getWeight": getFuncStringLabel(types.LabelWeight, defaultWeight), - "getDomain": getFuncStringLabel(types.LabelDomain, p.Domain), - "getProtocol": getFuncStringLabel(types.LabelProtocol, defaultProtocol), - "getPassHostHeader": getFuncStringLabel(types.LabelFrontendPassHostHeader, defaultPassHostHeader), - "getPriority": getFuncStringLabel(types.LabelFrontendPriority, defaultFrontendPriority), - "getEntryPoints": getFuncSliceStringLabel(types.LabelFrontendEntryPoints), - "getBasicAuth": getFuncSliceStringLabel(types.LabelFrontendAuthBasic), - "getFrontendRule": p.getFrontendRule, - "getRedirect": getFuncStringLabel(types.LabelFrontendRedirect, ""), - "hasCircuitBreakerLabel": hasLabel(types.LabelBackendCircuitbreakerExpression), - "getCircuitBreakerExpression": getFuncStringLabel(types.LabelBackendCircuitbreakerExpression, defaultCircuitBreakerExpression), - "hasLoadBalancerLabel": hasLoadBalancerLabel, - "getLoadBalancerMethod": getFuncStringLabel(types.LabelBackendLoadbalancerMethod, defaultBackendLoadBalancerMethod), - "hasMaxConnLabels": hasMaxConnLabels, - "getMaxConnAmount": getFuncInt64Label(types.LabelBackendMaxconnAmount, math.MaxInt64), - "getMaxConnExtractorFunc": getFuncStringLabel(types.LabelBackendMaxconnExtractorfunc, defaultBackendMaxconnExtractorfunc), - "getSticky": getSticky, - "hasStickinessLabel": hasStickinessLabel, - "getStickinessCookieName": getFuncStringLabel(types.LabelBackendLoadbalancerStickinessCookieName, ""), - "getIsBackendLBSwarm": getIsBackendLBSwarm, - "getServiceBackend": getServiceBackend, - "getServiceRedirect": getFuncServiceStringLabel(types.SuffixFrontendRedirect, defaultFrontendRedirect), - "getWhitelistSourceRange": getFuncSliceStringLabel(types.LabelTraefikFrontendWhitelistSourceRange), - - "hasRequestHeaders": hasLabel(types.LabelFrontendRequestHeaders), - "getRequestHeaders": getFuncMapLabel(types.LabelFrontendRequestHeaders), - "hasResponseHeaders": hasLabel(types.LabelFrontendResponseHeaders), - "getResponseHeaders": getFuncMapLabel(types.LabelFrontendResponseHeaders), - "hasAllowedHostsHeaders": hasLabel(types.LabelFrontendAllowedHosts), - "getAllowedHostsHeaders": getFuncSliceStringLabel(types.LabelFrontendAllowedHosts), - "hasHostsProxyHeaders": hasLabel(types.LabelFrontendHostsProxyHeaders), - "getHostsProxyHeaders": getFuncSliceStringLabel(types.LabelFrontendHostsProxyHeaders), - "hasSSLRedirectHeaders": hasLabel(types.LabelFrontendSSLRedirect), - "getSSLRedirectHeaders": getFuncBoolLabel(types.LabelFrontendSSLRedirect), - "hasSSLTemporaryRedirectHeaders": hasLabel(types.LabelFrontendSSLTemporaryRedirect), - "getSSLTemporaryRedirectHeaders": getFuncBoolLabel(types.LabelFrontendSSLTemporaryRedirect), - "hasSSLHostHeaders": hasLabel(types.LabelFrontendSSLHost), - "getSSLHostHeaders": getFuncStringLabel(types.LabelFrontendSSLHost, ""), - "hasSSLProxyHeaders": hasLabel(types.LabelFrontendSSLProxyHeaders), - "getSSLProxyHeaders": getFuncMapLabel(types.LabelFrontendSSLProxyHeaders), - "hasSTSSecondsHeaders": hasLabel(types.LabelFrontendSTSSeconds), - "getSTSSecondsHeaders": getFuncInt64Label(types.LabelFrontendSTSSeconds, 0), - "hasSTSIncludeSubdomainsHeaders": hasLabel(types.LabelFrontendSTSIncludeSubdomains), - "getSTSIncludeSubdomainsHeaders": getFuncBoolLabel(types.LabelFrontendSTSIncludeSubdomains), - "hasSTSPreloadHeaders": hasLabel(types.LabelFrontendSTSPreload), - "getSTSPreloadHeaders": getFuncBoolLabel(types.LabelFrontendSTSPreload), - "hasForceSTSHeaderHeaders": hasLabel(types.LabelFrontendForceSTSHeader), - "getForceSTSHeaderHeaders": getFuncBoolLabel(types.LabelFrontendForceSTSHeader), - "hasFrameDenyHeaders": hasLabel(types.LabelFrontendFrameDeny), - "getFrameDenyHeaders": getFuncBoolLabel(types.LabelFrontendFrameDeny), - "hasCustomFrameOptionsValueHeaders": hasLabel(types.LabelFrontendCustomFrameOptionsValue), - "getCustomFrameOptionsValueHeaders": getFuncStringLabel(types.LabelFrontendCustomFrameOptionsValue, ""), - "hasContentTypeNosniffHeaders": hasLabel(types.LabelFrontendContentTypeNosniff), - "getContentTypeNosniffHeaders": getFuncBoolLabel(types.LabelFrontendContentTypeNosniff), - "hasBrowserXSSFilterHeaders": hasLabel(types.LabelFrontendBrowserXSSFilter), - "getBrowserXSSFilterHeaders": getFuncBoolLabel(types.LabelFrontendBrowserXSSFilter), - "hasContentSecurityPolicyHeaders": hasLabel(types.LabelFrontendContentSecurityPolicy), - "getContentSecurityPolicyHeaders": getFuncStringLabel(types.LabelFrontendContentSecurityPolicy, ""), - "hasPublicKeyHeaders": hasLabel(types.LabelFrontendPublicKey), - "getPublicKeyHeaders": getFuncStringLabel(types.LabelFrontendPublicKey, ""), - "hasReferrerPolicyHeaders": hasLabel(types.LabelFrontendReferrerPolicy), - "getReferrerPolicyHeaders": getFuncStringLabel(types.LabelFrontendReferrerPolicy, ""), - "hasIsDevelopmentHeaders": hasLabel(types.LabelFrontendIsDevelopment), - "getIsDevelopmentHeaders": getFuncBoolLabel(types.LabelFrontendIsDevelopment), - - "hasServices": hasServices, - "getServiceNames": getServiceNames, - "getServicePort": getServicePort, - "getServiceWeight": getFuncServiceStringLabel(types.SuffixWeight, defaultWeight), - "getServiceProtocol": getFuncServiceStringLabel(types.SuffixProtocol, defaultProtocol), - "getServiceEntryPoints": getFuncServiceSliceStringLabel(types.SuffixFrontendEntryPoints), - "getServiceBasicAuth": getFuncServiceSliceStringLabel(types.SuffixFrontendAuthBasic), - "getServiceFrontendRule": p.getServiceFrontendRule, - "getServicePassHostHeader": getFuncServiceStringLabel(types.SuffixFrontendPassHostHeader, defaultPassHostHeader), - "getServicePriority": getFuncServiceStringLabel(types.SuffixFrontendPriority, defaultFrontendPriority), - } - // filter containers - filteredContainers := fun.Filter(func(container dockerData) bool { - return p.containerFilter(container) - }, containersInspected).([]dockerData) - - frontends := map[string][]dockerData{} - backends := map[string]dockerData{} - servers := map[string][]dockerData{} - serviceNames := make(map[string]struct{}) - for idx, container := range filteredContainers { - if _, exists := serviceNames[container.ServiceName]; !exists { - frontendName := p.getFrontendName(container, idx) - frontends[frontendName] = append(frontends[frontendName], container) - if len(container.ServiceName) > 0 { - serviceNames[container.ServiceName] = struct{}{} - } - } - backendName := getBackend(container) - backends[backendName] = container - servers[backendName] = append(servers[backendName], container) - } - - templateObjects := struct { - Containers []dockerData - Frontends map[string][]dockerData - Backends map[string]dockerData - Servers map[string][]dockerData - Domain string - }{ - filteredContainers, - frontends, - backends, - servers, - p.Domain, - } - - configuration, err := p.GetConfiguration("templates/docker.tmpl", DockerFuncMap, templateObjects) - if err != nil { - log.Error(err) - } - - return configuration -} - -// Regexp used to extract the name of the service and the name of the property for this service -// All properties are under the format traefik..frontent.*= except the port/weight/protocol directly after traefik.. -var servicesPropertiesRegexp = regexp.MustCompile(`^traefik\.(?P.+?)\.(?Pport|weight|protocol|frontend\.(.*))$`) - -// Check if for the given container, we find labels that are defining services -func hasServices(container dockerData) bool { - return len(extractServicesLabels(container.Labels)) > 0 -} - -// Gets array of service names for a given container -func getServiceNames(container dockerData) []string { - labelServiceProperties := extractServicesLabels(container.Labels) - keys := make([]string, 0, len(labelServiceProperties)) - for k := range labelServiceProperties { - keys = append(keys, k) - } - return keys -} - -// Extract backend from labels for a given service and a given docker container -func getServiceBackend(container dockerData, serviceName string) string { - if value, ok := getContainerServiceLabel(container, serviceName, types.SuffixFrontendBackend); ok { - return container.ServiceName + "-" + value - } - return strings.TrimPrefix(container.ServiceName, "/") + "-" + getBackend(container) + "-" + provider.Normalize(serviceName) -} - -// Extract rule from labels for a given service and a given docker container -func (p Provider) getServiceFrontendRule(container dockerData, serviceName string) string { - if value, ok := getContainerServiceLabel(container, serviceName, types.SuffixFrontendRule); ok { - return value - } - return p.getFrontendRule(container) -} - -// Extract port from labels for a given service and a given docker container -func getServicePort(container dockerData, serviceName string) string { - if value, ok := getContainerServiceLabel(container, serviceName, types.SuffixPort); ok { - return value - } - return getPort(container) -} - -func hasLoadBalancerLabel(container dockerData) bool { - _, errMethod := getLabel(container, types.LabelBackendLoadbalancerMethod) - _, errSticky := getLabel(container, types.LabelBackendLoadbalancerSticky) - _, errStickiness := getLabel(container, types.LabelBackendLoadbalancerStickiness) - _, errCookieName := getLabel(container, types.LabelBackendLoadbalancerStickinessCookieName) - - return errMethod == nil || errSticky == nil || errStickiness == nil || errCookieName == nil -} - -func hasMaxConnLabels(container dockerData) bool { - if _, err := getLabel(container, types.LabelBackendMaxconnAmount); err != nil { - return false - } - if _, err := getLabel(container, types.LabelBackendMaxconnExtractorfunc); err != nil { - return false - } - return true -} - -func (p Provider) containerFilter(container dockerData) bool { - if !isContainerEnabled(container, p.ExposedByDefault) { - log.Debugf("Filtering disabled container %s", container.Name) - return false - } - - var err error - portLabel := "traefik.port label" - if hasServices(container) { - portLabel = "traefik..port or " + portLabel + "s" - err = checkServiceLabelPort(container) - } else { - _, err = strconv.Atoi(container.Labels[types.LabelPort]) - } - if len(container.NetworkSettings.Ports) == 0 && err != nil { - log.Debugf("Filtering container without port and no %s %s : %s", portLabel, container.Name, err.Error()) - return false - } - - constraintTags := strings.Split(container.Labels[types.LabelTags], ",") - if ok, failingConstraint := p.MatchConstraints(constraintTags); !ok { - if failingConstraint != nil { - log.Debugf("Container %v pruned by '%v' constraint", container.Name, failingConstraint.String()) - } - return false - } - - if container.Health != "" && container.Health != "healthy" { - log.Debugf("Filtering unhealthy or starting container %s", container.Name) - return false - } - - if len(p.getFrontendRule(container)) == 0 { - log.Debugf("Filtering container with empty frontend rule %s", container.Name) - return false - } - - return true -} - -// checkServiceLabelPort checks if all service names have a port service label -// or if port container label exists for default value -func checkServiceLabelPort(container dockerData) error { - // If port container label is present, there is a default values for all ports, use it for the check - _, err := strconv.Atoi(container.Labels[types.LabelPort]) - if err != nil { - serviceLabelPorts := make(map[string]struct{}) - serviceLabels := make(map[string]struct{}) - portRegexp := regexp.MustCompile(`^traefik\.(?P.+?)\.port$`) - for label := range container.Labels { - // Get all port service labels - portLabel := portRegexp.FindStringSubmatch(label) - if portLabel != nil && len(portLabel) > 0 { - serviceLabelPorts[portLabel[0]] = struct{}{} - } - // Get only one instance of all service names from service labels - servicesLabelNames := servicesPropertiesRegexp.FindStringSubmatch(label) - if servicesLabelNames != nil && len(servicesLabelNames) > 0 { - serviceLabels[strings.Split(servicesLabelNames[0], ".")[1]] = struct{}{} - } - } - // If the number of service labels is different than the number of port services label - // there is an error - if len(serviceLabels) == len(serviceLabelPorts) { - for labelPort := range serviceLabelPorts { - _, err = strconv.Atoi(container.Labels[labelPort]) - if err != nil { - break - } - } - } else { - err = errors.New("Port service labels missing, please use traefik.port as default value or define all port service labels") - } - } - return err -} - -func (p Provider) getFrontendName(container dockerData, idx int) string { - // Replace '.' with '-' in quoted keys because of this issue https://github.com/BurntSushi/toml/issues/78 - return provider.Normalize(p.getFrontendRule(container) + "-" + strconv.Itoa(idx)) -} - -// GetFrontendRule returns the frontend rule for the specified container, using -// it's label. It returns a default one (Host) if the label is not present. -func (p Provider) getFrontendRule(container dockerData) string { - if label, err := getLabel(container, types.LabelFrontendRule); err == nil { - return label - } - if labels, err := getLabels(container, []string{labelDockerComposeProject, labelDockerComposeService}); err == nil { - return "Host:" + getSubDomain(labels[labelDockerComposeService]+"."+labels[labelDockerComposeProject]) + "." + p.Domain - } - if len(p.Domain) > 0 { - return "Host:" + getSubDomain(container.ServiceName) + "." + p.Domain - } - return "" -} - -func getBackend(container dockerData) string { - if label, err := getLabel(container, types.LabelBackend); err == nil { - return provider.Normalize(label) - } - if labels, err := getLabels(container, []string{labelDockerComposeProject, labelDockerComposeService}); err == nil { - return provider.Normalize(labels[labelDockerComposeService] + "_" + labels[labelDockerComposeProject]) - } - return provider.Normalize(container.ServiceName) -} - -func (p Provider) getIPAddress(container dockerData) string { - if label, err := getLabel(container, labelDockerNetwork); err == nil && label != "" { - networkSettings := container.NetworkSettings - if networkSettings.Networks != nil { - network := networkSettings.Networks[label] - if network != nil { - return network.Addr - } - - log.Warnf("Could not find network named '%s' for container '%s'! Maybe you're missing the project's prefix in the label? Defaulting to first available network.", label, container.Name) - } - } - - if container.NetworkSettings.NetworkMode.IsHost() { - if container.Node != nil { - if container.Node.IPAddress != "" { - return container.Node.IPAddress - } - } - return "127.0.0.1" - } - - if container.NetworkSettings.NetworkMode.IsContainer() { - dockerClient, err := p.createClient() - if err != nil { - log.Warnf("Unable to get IP address for container %s, error: %s", container.Name, err) - return "" - } - ctx := context.Background() - containerInspected, err := dockerClient.ContainerInspect(ctx, container.NetworkSettings.NetworkMode.ConnectedContainer()) - if err != nil { - log.Warnf("Unable to get IP address for container %s : Failed to inspect container ID %s, error: %s", container.Name, container.NetworkSettings.NetworkMode.ConnectedContainer(), err) - return "" - } - return p.getIPAddress(parseContainer(containerInspected)) - } - - if p.UseBindPortIP { - port := getPort(container) - for netPort, portBindings := range container.NetworkSettings.Ports { - if string(netPort) == port+"/TCP" || string(netPort) == port+"/UDP" { - for _, p := range portBindings { - return p.HostIP - } - } - } - } - - for _, network := range container.NetworkSettings.Networks { - return network.Addr - } - return "" -} - -func getPort(container dockerData) string { - if label, err := getLabel(container, types.LabelPort); err == nil { - return label - } - - // See iteration order in https://blog.golang.org/go-maps-in-action - var ports []nat.Port - for port := range container.NetworkSettings.Ports { - ports = append(ports, port) - } - - less := func(i, j nat.Port) bool { - return i.Int() < j.Int() - } - nat.Sort(ports, less) - - if len(ports) > 0 { - min := ports[0] - return min.Port() - } - - return "" -} - -func hasStickinessLabel(container dockerData) bool { - labelStickiness, errStickiness := getLabel(container, types.LabelBackendLoadbalancerStickiness) - return errStickiness == nil && len(labelStickiness) > 0 && strings.EqualFold(strings.TrimSpace(labelStickiness), "true") -} - -// Deprecated replaced by Stickiness -func getSticky(container dockerData) string { - if label, err := getLabel(container, types.LabelBackendLoadbalancerSticky); err == nil { - if len(label) > 0 { - log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) - } - return label - } - return "false" -} - -func getIsBackendLBSwarm(container dockerData) string { - return getStringLabel(container, labelBackendLoadBalancerSwarm, "false") -} - -func isContainerEnabled(container dockerData, exposedByDefault bool) bool { - return exposedByDefault && container.Labels[types.LabelEnable] != "false" || container.Labels[types.LabelEnable] == "true" -} - func listContainers(ctx context.Context, dockerClient client.ContainerAPIClient) ([]dockerData, error) { containerList, err := dockerClient.ContainerList(ctx, dockertypes.ContainerListOptions{}) if err != nil { - return []dockerData{}, err + return nil, err } var containersInspected []dockerData @@ -675,8 +265,8 @@ func listContainers(ctx context.Context, dockerClient client.ContainerAPIClient) // This condition is here to avoid to have empty IP https://github.com/containous/traefik/issues/2459 // We register only container which are running if containerInspected.ContainerJSONBase != nil && containerInspected.ContainerJSONBase.State != nil && containerInspected.ContainerJSONBase.State.Running { - dockerData := parseContainer(containerInspected) - containersInspected = append(containersInspected, dockerData) + dData := parseContainer(containerInspected) + containersInspected = append(containersInspected, dData) } } } @@ -684,36 +274,36 @@ func listContainers(ctx context.Context, dockerClient client.ContainerAPIClient) } func parseContainer(container dockertypes.ContainerJSON) dockerData { - dockerData := dockerData{ + dData := dockerData{ NetworkSettings: networkSettings{}, } if container.ContainerJSONBase != nil { - dockerData.Name = container.ContainerJSONBase.Name - dockerData.ServiceName = dockerData.Name //Default ServiceName to be the container's Name. - dockerData.Node = container.ContainerJSONBase.Node + dData.Name = container.ContainerJSONBase.Name + dData.ServiceName = dData.Name //Default ServiceName to be the container's Name. + dData.Node = container.ContainerJSONBase.Node if container.ContainerJSONBase.HostConfig != nil { - dockerData.NetworkSettings.NetworkMode = container.ContainerJSONBase.HostConfig.NetworkMode + dData.NetworkSettings.NetworkMode = container.ContainerJSONBase.HostConfig.NetworkMode } if container.State != nil && container.State.Health != nil { - dockerData.Health = container.State.Health.Status + dData.Health = container.State.Health.Status } } if container.Config != nil && container.Config.Labels != nil { - dockerData.Labels = container.Config.Labels + dData.Labels = container.Config.Labels } if container.NetworkSettings != nil { if container.NetworkSettings.Ports != nil { - dockerData.NetworkSettings.Ports = container.NetworkSettings.Ports + dData.NetworkSettings.Ports = container.NetworkSettings.Ports } if container.NetworkSettings.Networks != nil { - dockerData.NetworkSettings.Networks = make(map[string]*networkData) + dData.NetworkSettings.Networks = make(map[string]*networkData) for name, containerNetwork := range container.NetworkSettings.Networks { - dockerData.NetworkSettings.Networks[name] = &networkData{ + dData.NetworkSettings.Networks[name] = &networkData{ ID: containerNetwork.NetworkID, Name: name, Addr: containerNetwork.IPAddress, @@ -721,21 +311,19 @@ func parseContainer(container dockertypes.ContainerJSON) dockerData { } } } - return dockerData -} - -// Escape beginning slash "/", convert all others to dash "-", and convert underscores "_" to dash "-" -func getSubDomain(name string) string { - return strings.Replace(strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1), "_", "-", -1) + return dData } func listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerData, error) { serviceList, err := dockerClient.ServiceList(ctx, dockertypes.ServiceListOptions{}) if err != nil { - return []dockerData{}, err + return nil, err } serverVersion, err := dockerClient.ServerVersion(ctx) + if err != nil { + return nil, err + } networkListArgs := filters.NewArgs() // https://docs.docker.com/engine/api/v1.29/#tag/Network (Docker 17.06) @@ -746,12 +334,12 @@ func listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerD } networkList, err := dockerClient.NetworkList(ctx, dockertypes.NetworkListOptions{Filters: networkListArgs}) - - networkMap := make(map[string]*dockertypes.NetworkResource) if err != nil { log.Debugf("Failed to network inspect on client for docker, error: %s", err) - return []dockerData{}, err + return nil, err } + + networkMap := make(map[string]*dockertypes.NetworkResource) for _, network := range networkList { networkToAdd := network networkMap[network.ID] = &networkToAdd @@ -761,18 +349,20 @@ func listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerD var dockerDataListTasks []dockerData for _, service := range serviceList { - dockerData := parseService(service, networkMap) - if len(dockerData.NetworkSettings.Networks) > 0 { - useSwarmLB, _ := strconv.ParseBool(getIsBackendLBSwarm(dockerData)) + dData := parseService(service, networkMap) + if len(dData.NetworkSettings.Networks) > 0 { + useSwarmLB := isBackendLBSwarm(dData) if useSwarmLB { - dockerDataList = append(dockerDataList, dockerData) + dockerDataList = append(dockerDataList, dData) } else { isGlobalSvc := service.Spec.Mode.Global != nil - dockerDataListTasks, err = listTasks(ctx, dockerClient, service.ID, dockerData, networkMap, isGlobalSvc) - for _, dockerDataTask := range dockerDataListTasks { - dockerDataList = append(dockerDataList, dockerDataTask) + dockerDataListTasks, err = listTasks(ctx, dockerClient, service.ID, dData, networkMap, isGlobalSvc) + if err != nil { + log.Warn(err) + } else { + dockerDataList = append(dockerDataList, dockerDataListTasks...) } } } @@ -781,7 +371,7 @@ func listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerD } func parseService(service swarmtypes.Service, networkMap map[string]*dockertypes.NetworkResource) dockerData { - dockerData := dockerData{ + dData := dockerData{ ServiceName: service.Spec.Annotations.Name, Name: service.Spec.Annotations.Name, Labels: service.Spec.Annotations.Labels, @@ -789,10 +379,11 @@ func parseService(service swarmtypes.Service, networkMap map[string]*dockertypes } if service.Spec.EndpointSpec != nil { - if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeDNSRR { + switch service.Spec.EndpointSpec.Mode { + case swarmtypes.ResolutionModeDNSRR: log.Warnf("Ignored endpoint-mode not supported, service name: %s", service.Spec.Annotations.Name) - } else if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeVIP { - dockerData.NetworkSettings.Networks = make(map[string]*networkData) + case swarmtypes.ResolutionModeVIP: + dData.NetworkSettings.Networks = make(map[string]*networkData) for _, virtualIP := range service.Endpoint.VirtualIPs { networkService := networkMap[virtualIP.NetworkID] if networkService != nil { @@ -802,14 +393,14 @@ func parseService(service swarmtypes.Service, networkMap map[string]*dockertypes ID: virtualIP.NetworkID, Addr: ip.String(), } - dockerData.NetworkSettings.Networks[network.Name] = network + dData.NetworkSettings.Networks[network.Name] = network } else { log.Debugf("Network not found, id: %s", virtualIP.NetworkID) } } } } - return dockerData + return dData } func listTasks(ctx context.Context, dockerClient client.APIClient, serviceID string, @@ -817,25 +408,25 @@ func listTasks(ctx context.Context, dockerClient client.APIClient, serviceID str serviceIDFilter := filters.NewArgs() serviceIDFilter.Add("service", serviceID) serviceIDFilter.Add("desired-state", "running") + taskList, err := dockerClient.TaskList(ctx, dockertypes.TaskListOptions{Filters: serviceIDFilter}) - if err != nil { - return []dockerData{}, err + return nil, err } - var dockerDataList []dockerData + var dockerDataList []dockerData for _, task := range taskList { if task.Status.State != swarmtypes.TaskStateRunning { continue } - dockerData := parseTasks(task, serviceDockerData, networkMap, isGlobalSvc) - dockerDataList = append(dockerDataList, dockerData) + dData := parseTasks(task, serviceDockerData, networkMap, isGlobalSvc) + dockerDataList = append(dockerDataList, dData) } return dockerDataList, err } func parseTasks(task swarmtypes.Task, serviceDockerData dockerData, networkMap map[string]*dockertypes.NetworkResource, isGlobalSvc bool) dockerData { - dockerData := dockerData{ + dData := dockerData{ ServiceName: serviceDockerData.Name, Name: serviceDockerData.Name + "." + strconv.Itoa(task.Slot), Labels: serviceDockerData.Labels, @@ -843,11 +434,11 @@ func parseTasks(task swarmtypes.Task, serviceDockerData dockerData, networkMap m } if isGlobalSvc { - dockerData.Name = serviceDockerData.Name + "." + task.ID + dData.Name = serviceDockerData.Name + "." + task.ID } if task.NetworksAttachments != nil { - dockerData.NetworkSettings.Networks = make(map[string]*networkData) + dData.NetworkSettings.Networks = make(map[string]*networkData) for _, virtualIP := range task.NetworksAttachments { if networkService, present := networkMap[virtualIP.Network.ID]; present { // Not sure about this next loop - when would a task have multiple IP's for the same network? @@ -858,10 +449,10 @@ func parseTasks(task swarmtypes.Task, serviceDockerData dockerData, networkMap m Name: networkService.Name, Addr: ip.String(), } - dockerData.NetworkSettings.Networks[network.Name] = network + dData.NetworkSettings.Networks[network.Name] = network } } } } - return dockerData + return dData } diff --git a/provider/docker/labels.go b/provider/docker/labels.go deleted file mode 100644 index ad12a271b..000000000 --- a/provider/docker/labels.go +++ /dev/null @@ -1,203 +0,0 @@ -package docker - -import ( - "fmt" - "net/http" - "strconv" - "strings" - - "github.com/containous/traefik/log" - "github.com/containous/traefik/types" -) - -const ( - labelDockerNetwork = "traefik.docker.network" - labelBackendLoadBalancerSwarm = "traefik.backend.loadbalancer.swarm" - labelDockerComposeProject = "com.docker.compose.project" - labelDockerComposeService = "com.docker.compose.service" -) - -// Map of services properties -// we can get it with label[serviceName][propertyName] and we got the propertyValue -type labelServiceProperties map[string]map[string]string - -// Label functions - -func getFuncInt64Label(labelName string, defaultValue int64) func(container dockerData) int64 { - return func(container dockerData) int64 { - if rawValue, err := getLabel(container, labelName); err == nil { - value, errConv := strconv.ParseInt(rawValue, 10, 64) - if errConv == nil { - return value - } - log.Errorf("Unable to parse %q: %q", labelName, rawValue) - } - return defaultValue - } -} - -func getFuncMapLabel(labelName string) func(container dockerData) map[string]string { - return func(container dockerData) map[string]string { - return parseMapLabel(container, labelName) - } -} - -func parseMapLabel(container dockerData, labelName string) map[string]string { - if parts, err := getLabel(container, labelName); err == nil { - if len(parts) == 0 { - log.Errorf("Could not load %q", labelName) - return nil - } - - values := make(map[string]string) - for _, headers := range strings.Split(parts, "||") { - pair := strings.SplitN(headers, ":", 2) - if len(pair) != 2 { - log.Warnf("Could not load %q: %v, skipping...", labelName, pair) - } else { - values[http.CanonicalHeaderKey(strings.TrimSpace(pair[0]))] = strings.TrimSpace(pair[1]) - } - } - - if len(values) == 0 { - log.Errorf("Could not load %q", labelName) - return nil - } - return values - } - - return nil -} - -func getFuncStringLabel(label string, defaultValue string) func(container dockerData) string { - return func(container dockerData) string { - return getStringLabel(container, label, defaultValue) - } -} - -func getStringLabel(container dockerData, label string, defaultValue string) string { - if lbl, err := getLabel(container, label); err == nil { - return lbl - } - return defaultValue -} - -func getFuncBoolLabel(label string) func(container dockerData) bool { - return func(container dockerData) bool { - return getBoolLabel(container, label) - } -} - -func getBoolLabel(container dockerData, label string) bool { - lbl, err := getLabel(container, label) - return err == nil && len(lbl) > 0 && strings.EqualFold(strings.TrimSpace(lbl), "true") -} - -func getFuncSliceStringLabel(label string) func(container dockerData) []string { - return func(container dockerData) []string { - return getSliceStringLabel(container, label) - } -} - -func getSliceStringLabel(container dockerData, labelName string) []string { - var value []string - - if label, err := getLabel(container, labelName); err == nil { - value = types.SplitAndTrimString(label) - } - - if len(value) == 0 { - log.Debugf("Could not load %v labels", labelName) - } - return value -} - -// Service label functions - -func getFuncServiceSliceStringLabel(labelSuffix string) func(container dockerData, serviceName string) []string { - return func(container dockerData, serviceName string) []string { - return getServiceSliceStringLabel(container, serviceName, labelSuffix) - } -} - -func getServiceSliceStringLabel(container dockerData, serviceName string, labelSuffix string) []string { - if value, ok := getContainerServiceLabel(container, serviceName, labelSuffix); ok { - return strings.Split(value, ",") - } - return getSliceStringLabel(container, types.LabelPrefix+labelSuffix) -} - -func getFuncServiceStringLabel(labelSuffix string, defaultValue string) func(container dockerData, serviceName string) string { - return func(container dockerData, serviceName string) string { - return getServiceStringLabel(container, serviceName, labelSuffix, defaultValue) - } -} - -func getServiceStringLabel(container dockerData, serviceName string, labelSuffix string, defaultValue string) string { - if value, ok := getContainerServiceLabel(container, serviceName, labelSuffix); ok { - return value - } - return getStringLabel(container, types.LabelPrefix+labelSuffix, defaultValue) -} - -// Base functions - -// Gets the entry for a service label searching in all labels of the given container -func getContainerServiceLabel(container dockerData, serviceName string, entry string) (string, bool) { - value, ok := extractServicesLabels(container.Labels)[serviceName][entry] - return value, ok -} - -// Extract the service labels from container labels of dockerData struct -func extractServicesLabels(labels map[string]string) labelServiceProperties { - v := make(labelServiceProperties) - - for index, serviceProperty := range labels { - matches := servicesPropertiesRegexp.FindStringSubmatch(index) - if matches != nil { - result := make(map[string]string) - for i, name := range servicesPropertiesRegexp.SubexpNames() { - if i != 0 { - result[name] = matches[i] - } - } - serviceName := result["service_name"] - if _, ok := v[serviceName]; !ok { - v[serviceName] = make(map[string]string) - } - v[serviceName][result["property_name"]] = serviceProperty - } - } - - return v -} - -func hasLabel(label string) func(container dockerData) bool { - return func(container dockerData) bool { - lbl, err := getLabel(container, label) - return err == nil && len(lbl) > 0 - } -} - -func getLabel(container dockerData, label string) (string, error) { - if value, ok := container.Labels[label]; ok { - return value, nil - } - return "", fmt.Errorf("label not found: %s", label) -} - -func getLabels(container dockerData, labels []string) (map[string]string, error) { - var globalErr error - foundLabels := map[string]string{} - for _, label := range labels { - foundLabel, err := getLabel(container, label) - // Error out only if one of them is defined. - if err != nil { - globalErr = fmt.Errorf("label not found: %s", label) - continue - } - foundLabels[label] = foundLabel - - } - return foundLabels, globalErr -} diff --git a/provider/docker/labels_test.go b/provider/docker/labels_test.go deleted file mode 100644 index fa45a6bc6..000000000 --- a/provider/docker/labels_test.go +++ /dev/null @@ -1,248 +0,0 @@ -package docker - -import ( - "reflect" - "strconv" - "testing" - - "github.com/containous/traefik/types" - docker "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" -) - -func TestDockerGetFuncStringLabel(t *testing.T) { - testCases := []struct { - container docker.ContainerJSON - labelName string - defaultValue string - expected string - }{ - { - container: containerJSON(), - labelName: types.LabelWeight, - defaultValue: defaultWeight, - expected: "0", - }, - { - container: containerJSON(labels(map[string]string{ - types.LabelWeight: "10", - })), - labelName: types.LabelWeight, - defaultValue: defaultWeight, - expected: "10", - }, - } - - for containerID, test := range testCases { - test := test - t.Run(test.labelName+strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - - dockerData := parseContainer(test.container) - - actual := getFuncStringLabel(test.labelName, test.defaultValue)(dockerData) - - if actual != test.expected { - t.Errorf("got %q, expected %q", actual, test.expected) - } - }) - } -} - -func TestDockerGetSliceStringLabel(t *testing.T) { - testCases := []struct { - desc string - container docker.ContainerJSON - labelName string - expected []string - }{ - { - desc: "no whitelist-label", - container: containerJSON(), - expected: nil, - }, - { - desc: "whitelist-label with empty string", - container: containerJSON(labels(map[string]string{ - types.LabelTraefikFrontendWhitelistSourceRange: "", - })), - labelName: types.LabelTraefikFrontendWhitelistSourceRange, - expected: nil, - }, - { - desc: "whitelist-label with IPv4 mask", - container: containerJSON(labels(map[string]string{ - types.LabelTraefikFrontendWhitelistSourceRange: "1.2.3.4/16", - })), - labelName: types.LabelTraefikFrontendWhitelistSourceRange, - expected: []string{ - "1.2.3.4/16", - }, - }, - { - desc: "whitelist-label with IPv6 mask", - container: containerJSON(labels(map[string]string{ - types.LabelTraefikFrontendWhitelistSourceRange: "fe80::/16", - })), - labelName: types.LabelTraefikFrontendWhitelistSourceRange, - expected: []string{ - "fe80::/16", - }, - }, - { - desc: "whitelist-label with multiple masks", - container: containerJSON(labels(map[string]string{ - types.LabelTraefikFrontendWhitelistSourceRange: "1.1.1.1/24, 1234:abcd::42/32", - })), - labelName: types.LabelTraefikFrontendWhitelistSourceRange, - expected: []string{ - "1.1.1.1/24", - "1234:abcd::42/32", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(test.container) - - actual := getFuncSliceStringLabel(test.labelName)(dockerData) - - if !reflect.DeepEqual(actual, test.expected) { - t.Errorf("expected %q, got %q", test.expected, actual) - } - }) - } -} - -func TestDockerGetFuncServiceStringLabel(t *testing.T) { - testCases := []struct { - container docker.ContainerJSON - suffixLabel string - defaultValue string - expected string - }{ - { - container: containerJSON(), - suffixLabel: types.SuffixWeight, - defaultValue: defaultWeight, - expected: "0", - }, - { - container: containerJSON(labels(map[string]string{ - types.LabelWeight: "200", - })), - suffixLabel: types.SuffixWeight, - defaultValue: defaultWeight, - expected: "200", - }, - { - container: containerJSON(labels(map[string]string{ - "traefik.myservice.weight": "31337", - })), - suffixLabel: types.SuffixWeight, - defaultValue: defaultWeight, - expected: "31337", - }, - } - - for containerID, test := range testCases { - test := test - t.Run(test.suffixLabel+strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - - dockerData := parseContainer(test.container) - - actual := getFuncServiceStringLabel(test.suffixLabel, test.defaultValue)(dockerData, "myservice") - if actual != test.expected { - t.Fatalf("got %q, expected %q", actual, test.expected) - } - }) - } -} - -func TestDockerGetFuncServiceSliceStringLabel(t *testing.T) { - testCases := []struct { - container docker.ContainerJSON - suffixLabel string - expected []string - }{ - { - container: containerJSON(), - suffixLabel: types.SuffixFrontendEntryPoints, - expected: nil, - }, - { - container: containerJSON(labels(map[string]string{ - types.LabelFrontendEntryPoints: "http,https", - })), - suffixLabel: types.SuffixFrontendEntryPoints, - expected: []string{"http", "https"}, - }, - { - container: containerJSON(labels(map[string]string{ - "traefik.myservice.frontend.entryPoints": "http,https", - })), - suffixLabel: types.SuffixFrontendEntryPoints, - expected: []string{"http", "https"}, - }, - } - - for containerID, test := range testCases { - test := test - t.Run(test.suffixLabel+strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - - dockerData := parseContainer(test.container) - - actual := getFuncServiceSliceStringLabel(test.suffixLabel)(dockerData, "myservice") - - if !reflect.DeepEqual(actual, test.expected) { - t.Fatalf("for container %q: got %q, expected %q", dockerData.Name, actual, test.expected) - } - }) - } -} - -func TestSwarmGetFuncStringLabel(t *testing.T) { - testCases := []struct { - service swarm.Service - labelName string - defaultValue string - networks map[string]*docker.NetworkResource - expected string - }{ - { - service: swarmService(), - labelName: types.LabelWeight, - defaultValue: defaultWeight, - networks: map[string]*docker.NetworkResource{}, - expected: "0", - }, - { - service: swarmService(serviceLabels(map[string]string{ - types.LabelWeight: "10", - })), - labelName: types.LabelWeight, - defaultValue: defaultWeight, - networks: map[string]*docker.NetworkResource{}, - expected: "10", - }, - } - - for serviceID, test := range testCases { - test := test - t.Run(test.labelName+strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - - dockerData := parseService(test.service, test.networks) - - actual := getFuncStringLabel(test.labelName, test.defaultValue)(dockerData) - if actual != test.expected { - t.Errorf("got %q, expected %q", actual, test.expected) - } - }) - } -} diff --git a/provider/docker/service_test.go b/provider/docker/service_test.go index 08625e7f8..440ebccbb 100644 --- a/provider/docker/service_test.go +++ b/provider/docker/service_test.go @@ -5,6 +5,7 @@ import ( "strconv" "testing" + "github.com/containous/traefik/provider/label" "github.com/containous/traefik/types" docker "github.com/docker/docker/api/types" "github.com/docker/go-connections/nat" @@ -21,7 +22,7 @@ func TestDockerGetServicePort(t *testing.T) { }, { container: containerJSON(labels(map[string]string{ - types.LabelPort: "2500", + label.TraefikPort: "2500", })), expected: "2500", }, @@ -37,8 +38,8 @@ func TestDockerGetServicePort(t *testing.T) { test := test t.Run(strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() - dockerData := parseContainer(test.container) - actual := getServicePort(dockerData, "myservice") + dData := parseContainer(test.container) + actual := getServicePort(dData, "myservice") if actual != test.expected { t.Fatalf("expected %q, got %q", test.expected, actual) } @@ -59,7 +60,7 @@ func TestDockerGetServiceFrontendRule(t *testing.T) { }, { container: containerJSON(labels(map[string]string{ - types.LabelFrontendRule: "Path:/helloworld", + label.TraefikFrontendRule: "Path:/helloworld", })), expected: "Path:/helloworld", }, @@ -75,8 +76,8 @@ func TestDockerGetServiceFrontendRule(t *testing.T) { test := test t.Run(strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() - dockerData := parseContainer(test.container) - actual := provider.getServiceFrontendRule(dockerData, "myservice") + dData := parseContainer(test.container) + actual := provider.getServiceFrontendRule(dData, "myservice") if actual != test.expected { t.Fatalf("expected %q, got %q", test.expected, actual) } @@ -95,7 +96,7 @@ func TestDockerGetServiceBackend(t *testing.T) { }, { container: containerJSON(labels(map[string]string{ - types.LabelBackend: "another-backend", + label.TraefikBackend: "another-backend", })), expected: "fake-another-backend-myservice", }, @@ -111,8 +112,8 @@ func TestDockerGetServiceBackend(t *testing.T) { test := test t.Run(strconv.Itoa(containerID), func(t *testing.T) { t.Parallel() - dockerData := parseContainer(test.container) - actual := getServiceBackend(dockerData, "myservice") + dData := parseContainer(test.container) + actual := getServiceBackend(dData, "myservice") if actual != test.expected { t.Fatalf("expected %q, got %q", test.expected, actual) } @@ -268,11 +269,11 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) { t.Parallel() var dockerDataList []dockerData for _, container := range test.containers { - dockerData := parseContainer(container) - dockerDataList = append(dockerDataList, dockerData) + dData := parseContainer(container) + dockerDataList = append(dockerDataList, dData) } - actualConfig := provider.loadDockerConfig(dockerDataList) + actualConfig := provider.buildConfiguration(dockerDataList) // Compare backends if !reflect.DeepEqual(actualConfig.Backends, test.expectedBackends) { t.Fatalf("expected %#v, got %#v", test.expectedBackends, actualConfig.Backends) diff --git a/provider/docker/swarm_test.go b/provider/docker/swarm_test.go index 32f95cf74..851d4376a 100644 --- a/provider/docker/swarm_test.go +++ b/provider/docker/swarm_test.go @@ -1,13 +1,10 @@ package docker import ( - "reflect" "strconv" - "strings" "testing" "time" - "github.com/containous/traefik/types" "github.com/davecgh/go-spew/spew" docker "github.com/docker/docker/api/types" dockertypes "github.com/docker/docker/api/types" @@ -17,696 +14,6 @@ import ( "golang.org/x/net/context" ) -func TestSwarmGetFrontendName(t *testing.T) { - testCases := []struct { - service swarm.Service - expected string - networks map[string]*docker.NetworkResource - }{ - { - service: swarmService(serviceName("foo")), - expected: "Host-foo-docker-localhost-0", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceLabels(map[string]string{ - types.LabelFrontendRule: "Headers:User-Agent,bat/0.1.0", - })), - expected: "Headers-User-Agent-bat-0-1-0-0", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceLabels(map[string]string{ - types.LabelFrontendRule: "Host:foo.bar", - })), - expected: "Host-foo-bar-0", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceLabels(map[string]string{ - types.LabelFrontendRule: "Path:/test", - })), - expected: "Path-test-0", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService( - serviceName("test"), - serviceLabels(map[string]string{ - types.LabelFrontendRule: "PathPrefix:/test2", - }), - ), - expected: "PathPrefix-test2-0", - networks: map[string]*docker.NetworkResource{}, - }, - } - - for serviceID, test := range testCases { - test := test - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(test.service, test.networks) - provider := &Provider{ - Domain: "docker.localhost", - SwarmMode: true, - } - actual := provider.getFrontendName(dockerData, 0) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } - }) - } -} - -func TestSwarmGetFrontendRule(t *testing.T) { - testCases := []struct { - service swarm.Service - expected string - networks map[string]*docker.NetworkResource - }{ - { - service: swarmService(serviceName("foo")), - expected: "Host:foo.docker.localhost", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceName("bar")), - expected: "Host:bar.docker.localhost", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceLabels(map[string]string{ - types.LabelFrontendRule: "Host:foo.bar", - })), - expected: "Host:foo.bar", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceLabels(map[string]string{ - types.LabelFrontendRule: "Path:/test", - })), - expected: "Path:/test", - networks: map[string]*docker.NetworkResource{}, - }, - } - - for serviceID, test := range testCases { - test := test - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(test.service, test.networks) - provider := &Provider{ - Domain: "docker.localhost", - SwarmMode: true, - } - actual := provider.getFrontendRule(dockerData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } - }) - } -} - -func TestSwarmGetBackend(t *testing.T) { - testCases := []struct { - service swarm.Service - expected string - networks map[string]*docker.NetworkResource - }{ - { - service: swarmService(serviceName("foo")), - expected: "foo", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceName("bar")), - expected: "bar", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceLabels(map[string]string{ - types.LabelBackend: "foobar", - })), - expected: "foobar", - networks: map[string]*docker.NetworkResource{}, - }, - } - - for serviceID, test := range testCases { - test := test - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(test.service, test.networks) - actual := getBackend(dockerData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } - }) - } -} - -func TestSwarmGetIPAddress(t *testing.T) { - testCases := []struct { - service swarm.Service - expected string - networks map[string]*docker.NetworkResource - }{ - { - service: swarmService(withEndpointSpec(modeDNSSR)), - expected: "", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService( - withEndpointSpec(modeVIP), - withEndpoint(virtualIP("1", "10.11.12.13/24")), - ), - expected: "10.11.12.13", - networks: map[string]*docker.NetworkResource{ - "1": { - Name: "foo", - }, - }, - }, - { - service: swarmService( - serviceLabels(map[string]string{ - labelDockerNetwork: "barnet", - }), - withEndpointSpec(modeVIP), - withEndpoint( - virtualIP("1", "10.11.12.13/24"), - virtualIP("2", "10.11.12.99/24"), - ), - ), - expected: "10.11.12.99", - networks: map[string]*docker.NetworkResource{ - "1": { - Name: "foonet", - }, - "2": { - Name: "barnet", - }, - }, - }, - } - - for serviceID, test := range testCases { - test := test - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(test.service, test.networks) - provider := &Provider{ - SwarmMode: true, - } - actual := provider.getIPAddress(dockerData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } - }) - } -} - -func TestSwarmGetPort(t *testing.T) { - testCases := []struct { - service swarm.Service - expected string - networks map[string]*docker.NetworkResource - }{ - { - service: swarmService( - serviceLabels(map[string]string{ - types.LabelPort: "8080", - }), - withEndpointSpec(modeDNSSR), - ), - expected: "8080", - networks: map[string]*docker.NetworkResource{}, - }, - } - - for serviceID, test := range testCases { - test := test - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(test.service, test.networks) - actual := getPort(dockerData) - if actual != test.expected { - t.Errorf("expected %q, got %q", test.expected, actual) - } - }) - } -} - -func TestSwarmGetLabel(t *testing.T) { - testCases := []struct { - service swarm.Service - expected string - networks map[string]*docker.NetworkResource - }{ - { - service: swarmService(), - expected: "label not found:", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceLabels(map[string]string{ - "foo": "bar", - })), - expected: "", - networks: map[string]*docker.NetworkResource{}, - }, - } - - for serviceID, test := range testCases { - test := test - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(test.service, test.networks) - label, err := getLabel(dockerData, "foo") - if test.expected != "" { - if err == nil || !strings.Contains(err.Error(), test.expected) { - t.Errorf("expected an error with %q, got %v", test.expected, err) - } - } else { - if label != "bar" { - t.Errorf("expected label 'bar', got '%s'", label) - } - } - }) - } -} - -func TestSwarmGetLabels(t *testing.T) { - testCases := []struct { - service swarm.Service - expectedLabels map[string]string - expectedError string - networks map[string]*docker.NetworkResource - }{ - { - service: swarmService(), - expectedLabels: map[string]string{}, - expectedError: "label not found:", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceLabels(map[string]string{ - "foo": "fooz", - })), - expectedLabels: map[string]string{ - "foo": "fooz", - }, - expectedError: "label not found: bar", - networks: map[string]*docker.NetworkResource{}, - }, - { - service: swarmService(serviceLabels(map[string]string{ - "foo": "fooz", - "bar": "barz", - })), - expectedLabels: map[string]string{ - "foo": "fooz", - "bar": "barz", - }, - expectedError: "", - networks: map[string]*docker.NetworkResource{}, - }, - } - - for serviceID, test := range testCases { - test := test - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(test.service, test.networks) - labels, err := getLabels(dockerData, []string{"foo", "bar"}) - if !reflect.DeepEqual(labels, test.expectedLabels) { - t.Errorf("expect %v, got %v", test.expectedLabels, labels) - } - if test.expectedError != "" { - if err == nil || !strings.Contains(err.Error(), test.expectedError) { - t.Errorf("expected an error with %q, got %v", test.expectedError, err) - } - } - }) - } -} - -func TestSwarmTraefikFilter(t *testing.T) { - testCases := []struct { - service swarm.Service - expected bool - networks map[string]*docker.NetworkResource - provider *Provider - }{ - { - service: swarmService(), - expected: false, - networks: map[string]*docker.NetworkResource{}, - provider: &Provider{ - SwarmMode: true, - Domain: "test", - ExposedByDefault: true, - }, - }, - { - service: swarmService(serviceLabels(map[string]string{ - types.LabelEnable: "false", - types.LabelPort: "80", - })), - expected: false, - networks: map[string]*docker.NetworkResource{}, - provider: &Provider{ - SwarmMode: true, - Domain: "test", - ExposedByDefault: true, - }, - }, - { - service: swarmService(serviceLabels(map[string]string{ - types.LabelFrontendRule: "Host:foo.bar", - types.LabelPort: "80", - })), - expected: true, - networks: map[string]*docker.NetworkResource{}, - provider: &Provider{ - SwarmMode: true, - Domain: "test", - ExposedByDefault: true, - }, - }, - { - service: swarmService(serviceLabels(map[string]string{ - types.LabelPort: "80", - })), - expected: true, - networks: map[string]*docker.NetworkResource{}, - provider: &Provider{ - SwarmMode: true, - Domain: "test", - ExposedByDefault: true, - }, - }, - { - service: swarmService(serviceLabels(map[string]string{ - types.LabelEnable: "true", - types.LabelPort: "80", - })), - expected: true, - networks: map[string]*docker.NetworkResource{}, - provider: &Provider{ - SwarmMode: true, - Domain: "test", - ExposedByDefault: true, - }, - }, - { - service: swarmService(serviceLabels(map[string]string{ - types.LabelEnable: "anything", - types.LabelPort: "80", - })), - expected: true, - networks: map[string]*docker.NetworkResource{}, - provider: &Provider{ - SwarmMode: true, - Domain: "test", - ExposedByDefault: true, - }, - }, - { - service: swarmService(serviceLabels(map[string]string{ - types.LabelFrontendRule: "Host:foo.bar", - types.LabelPort: "80", - })), - expected: true, - networks: map[string]*docker.NetworkResource{}, - provider: &Provider{ - SwarmMode: true, - Domain: "test", - ExposedByDefault: true, - }, - }, - { - service: swarmService(serviceLabels(map[string]string{ - types.LabelPort: "80", - })), - expected: false, - networks: map[string]*docker.NetworkResource{}, - provider: &Provider{ - SwarmMode: true, - Domain: "test", - ExposedByDefault: false, - }, - }, - { - service: swarmService(serviceLabels(map[string]string{ - types.LabelEnable: "true", - types.LabelPort: "80", - })), - expected: true, - networks: map[string]*docker.NetworkResource{}, - provider: &Provider{ - SwarmMode: true, - Domain: "test", - ExposedByDefault: false, - }, - }, - } - - for serviceID, test := range testCases { - test := test - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(test.service, test.networks) - actual := test.provider.containerFilter(dockerData) - if actual != test.expected { - t.Errorf("expected %v for %+v, got %+v", test.expected, test, actual) - } - }) - } -} - -func TestSwarmLoadDockerConfig(t *testing.T) { - testCases := []struct { - services []swarm.Service - expectedFrontends map[string]*types.Frontend - expectedBackends map[string]*types.Backend - networks map[string]*docker.NetworkResource - }{ - { - services: []swarm.Service{}, - expectedFrontends: map[string]*types.Frontend{}, - expectedBackends: map[string]*types.Backend{}, - networks: map[string]*docker.NetworkResource{}, - }, - { - services: []swarm.Service{ - swarmService( - serviceName("test"), - serviceLabels(map[string]string{ - types.LabelPort: "80", - }), - withEndpointSpec(modeVIP), - withEndpoint(virtualIP("1", "127.0.0.1/24")), - ), - }, - expectedFrontends: map[string]*types.Frontend{ - "frontend-Host-test-docker-localhost-0": { - Backend: "backend-test", - PassHostHeader: true, - EntryPoints: []string{}, - BasicAuth: []string{}, - Redirect: "", - Routes: map[string]types.Route{ - "route-frontend-Host-test-docker-localhost-0": { - Rule: "Host:test.docker.localhost", - }, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-test": { - Servers: map[string]types.Server{ - "server-test": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - }, - CircuitBreaker: nil, - LoadBalancer: nil, - }, - }, - networks: map[string]*docker.NetworkResource{ - "1": { - Name: "foo", - }, - }, - }, - { - services: []swarm.Service{ - swarmService( - serviceName("test1"), - serviceLabels(map[string]string{ - types.LabelPort: "80", - types.LabelBackend: "foobar", - types.LabelFrontendEntryPoints: "http,https", - types.LabelFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", - types.LabelFrontendRedirect: "https", - }), - withEndpointSpec(modeVIP), - withEndpoint(virtualIP("1", "127.0.0.1/24")), - ), - swarmService( - serviceName("test2"), - serviceLabels(map[string]string{ - types.LabelPort: "80", - types.LabelBackend: "foobar", - }), - withEndpointSpec(modeVIP), - withEndpoint(virtualIP("1", "127.0.0.1/24")), - ), - }, - expectedFrontends: map[string]*types.Frontend{ - "frontend-Host-test1-docker-localhost-0": { - Backend: "backend-foobar", - PassHostHeader: true, - EntryPoints: []string{"http", "https"}, - BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, - Redirect: "https", - Routes: map[string]types.Route{ - "route-frontend-Host-test1-docker-localhost-0": { - Rule: "Host:test1.docker.localhost", - }, - }, - }, - "frontend-Host-test2-docker-localhost-1": { - Backend: "backend-foobar", - PassHostHeader: true, - EntryPoints: []string{}, - BasicAuth: []string{}, - Redirect: "", - Routes: map[string]types.Route{ - "route-frontend-Host-test2-docker-localhost-1": { - Rule: "Host:test2.docker.localhost", - }, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-foobar": { - Servers: map[string]types.Server{ - "server-test1": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - "server-test2": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - }, - CircuitBreaker: nil, - LoadBalancer: nil, - }, - }, - networks: map[string]*docker.NetworkResource{ - "1": { - Name: "foo", - }, - }, - }, - } - - for caseID, test := range testCases { - test := test - t.Run(strconv.Itoa(caseID), func(t *testing.T) { - t.Parallel() - var dockerDataList []dockerData - for _, service := range test.services { - dockerData := parseService(service, test.networks) - dockerDataList = append(dockerDataList, dockerData) - } - - provider := &Provider{ - Domain: "docker.localhost", - ExposedByDefault: true, - SwarmMode: true, - } - actualConfig := provider.loadDockerConfig(dockerDataList) - // Compare backends - if !reflect.DeepEqual(actualConfig.Backends, test.expectedBackends) { - t.Errorf("expected %#v, got %#v", test.expectedBackends, actualConfig.Backends) - } - if !reflect.DeepEqual(actualConfig.Frontends, test.expectedFrontends) { - t.Errorf("expected %#v, got %#v", test.expectedFrontends, actualConfig.Frontends) - } - }) - } -} - -func TestSwarmTaskParsing(t *testing.T) { - testCases := []struct { - service swarm.Service - tasks []swarm.Task - isGlobalSVC bool - expectedNames map[string]string - networks map[string]*docker.NetworkResource - }{ - { - service: swarmService(serviceName("container")), - tasks: []swarm.Task{ - swarmTask("id1", taskSlot(1)), - swarmTask("id2", taskSlot(2)), - swarmTask("id3", taskSlot(3)), - }, - isGlobalSVC: false, - expectedNames: map[string]string{ - "id1": "container.1", - "id2": "container.2", - "id3": "container.3", - }, - networks: map[string]*docker.NetworkResource{ - "1": { - Name: "foo", - }, - }, - }, - { - service: swarmService(serviceName("container")), - tasks: []swarm.Task{ - swarmTask("id1"), - swarmTask("id2"), - swarmTask("id3"), - }, - isGlobalSVC: true, - expectedNames: map[string]string{ - "id1": "container.id1", - "id2": "container.id2", - "id3": "container.id3", - }, - networks: map[string]*docker.NetworkResource{ - "1": { - Name: "foo", - }, - }, - }, - } - - for caseID, test := range testCases { - test := test - t.Run(strconv.Itoa(caseID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(test.service, test.networks) - - for _, task := range test.tasks { - taskDockerData := parseTasks(task, dockerData, map[string]*docker.NetworkResource{}, test.isGlobalSVC) - if !reflect.DeepEqual(taskDockerData.Name, test.expectedNames[task.ID]) { - t.Errorf("expect %v, got %v", test.expectedNames[task.ID], taskDockerData.Name) - } - } - }) - } -} - type fakeTasksClient struct { dockerclient.APIClient tasks []swarm.Task @@ -874,7 +181,9 @@ func TestListServices(t *testing.T) { t.Run(strconv.Itoa(caseID), func(t *testing.T) { t.Parallel() dockerClient := &fakeServicesClient{services: test.services, dockerVersion: test.dockerVersion, networks: test.networks} - serviceDockerData, _ := listServices(context.Background(), dockerClient) + + serviceDockerData, err := listServices(context.Background(), dockerClient) + assert.NoError(t, err) assert.Equal(t, len(test.expectedServices), len(serviceDockerData)) for i, serviceName := range test.expectedServices {