refactor(docker): rewrite configuration system.

This commit is contained in:
Fernandez Ludovic 2017-12-02 19:26:44 +01:00 committed by Traefiker
parent ce6bbbaa33
commit ae2ae85070
12 changed files with 1892 additions and 2197 deletions

View file

@ -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")

177
provider/docker/config.go Normal file
View file

@ -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.<serviceName>.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
}

View file

@ -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)
}
}

View file

@ -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)
}
})
}
}

View file

@ -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]
}

View file

@ -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)
}
})
}
}

View file

@ -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.<servicename>.frontent.*= except the port/weight/protocol directly after traefik.<servicename>.
var servicesPropertiesRegexp = regexp.MustCompile(`^traefik\.(?P<service_name>.+?)\.(?P<property_name>port|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.<serviceName>.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<service_name>.+?)\.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
}

View file

@ -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
}

View file

@ -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)
}
})
}
}

View file

@ -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)

View file

@ -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 {