From 99c8bffcbf6200bd7bdb3862f28e416fe1f5bb6c Mon Sep 17 00:00:00 2001 From: Diego Osse Fernandes Date: Fri, 5 Aug 2016 11:02:46 -0300 Subject: [PATCH] Initial support for Docker 1.12 Swarm Mode --- configuration.go | 8 +- docs/toml.md | 8 + provider/docker.go | 347 ++++++++++--- provider/docker_test.go | 1079 ++++++++++++++++++++++++++++++++++++++- server.go | 3 +- traefik.sample.toml | 60 +++ 6 files changed, 1406 insertions(+), 99 deletions(-) diff --git a/configuration.go b/configuration.go index 406d0f88f..9235eb723 100644 --- a/configuration.go +++ b/configuration.go @@ -4,13 +4,14 @@ import ( "crypto/tls" "errors" "fmt" - "github.com/containous/traefik/acme" - "github.com/containous/traefik/provider" - "github.com/containous/traefik/types" "os" "regexp" "strings" "time" + + "github.com/containous/traefik/acme" + "github.com/containous/traefik/provider" + "github.com/containous/traefik/types" ) // TraefikConfiguration holds GlobalConfiguration and other stuff @@ -269,6 +270,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { defaultDocker.Watch = true defaultDocker.ExposedByDefault = true defaultDocker.Endpoint = "unix:///var/run/docker.sock" + defaultDocker.SwarmMode = false // default File var defaultFile provider.File diff --git a/docs/toml.md b/docs/toml.md index d4b761a77..fc5b9f293 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -664,11 +664,19 @@ watch = true exposedbydefault = true # Use the IP address from the binded port instead of the inner network one. For specific use-case :) + # # Optional # Default: false # usebindportip = true +# Use Swarm Mode services as data provider +# +# Optional +# Default: false +# +swarmmode = false + # Enable docker TLS connection # diff --git a/provider/docker.go b/provider/docker.go index f6aedc975..9b2862f34 100644 --- a/provider/docker.go +++ b/provider/docker.go @@ -3,6 +3,7 @@ package provider import ( "errors" "math" + "net" "net/http" "strconv" "strings" @@ -18,15 +19,25 @@ import ( "github.com/containous/traefik/version" "github.com/docker/engine-api/client" dockertypes "github.com/docker/engine-api/types" + dockercontainertypes "github.com/docker/engine-api/types/container" eventtypes "github.com/docker/engine-api/types/events" "github.com/docker/engine-api/types/filters" + "github.com/docker/engine-api/types/swarm" + swarmtypes "github.com/docker/engine-api/types/swarm" + "github.com/docker/go-connections/nat" "github.com/docker/go-connections/sockets" "github.com/emilevauge/backoff" "github.com/vdemeester/docker-events" ) -// DockerAPIVersion is a constant holding the version of the Docker API traefik will use -const DockerAPIVersion string = "1.21" +const ( + // DockerAPIVersion is a constant holding the version of the Docker API traefik will use + DockerAPIVersion string = "1.21" + // SwarmAPIVersion is a constant holding the version of the Docker API traefik will use + SwarmAPIVersion string = "1.24" + // SwarmDefaultWatchTime is the duration of the interval when polling docker + SwarmDefaultWatchTime = 15 * time.Second +) // Docker holds configurations of the Docker provider. type Docker struct { @@ -36,6 +47,30 @@ type Docker struct { TLS *ClientTLS `description:"Enable Docker TLS support"` ExposedByDefault bool `description:"Expose containers by default"` UseBindPortIP bool `description:"Use the ip address from the bound port, rather than from the inner network"` + SwarmMode bool `description:"Use Docker on Swarm Mode"` +} + +// dockerData holds the need data to the Docker provider +type dockerData struct { + Name string + Labels map[string]string // List of labels set to container or service + NetworkSettings networkSettings +} + +// NetworkSettings holds the networks data to the Docker provider +type networkSettings struct { + NetworkMode dockercontainertypes.NetworkMode + Ports nat.PortMap + Networks map[string]*networkData +} + +// Network holds the network data to the Docker provider +type networkData struct { + Name string + Addr string + Port int + Protocol string + ID string } func (provider *Docker) createClient() (client.APIClient, error) { @@ -63,7 +98,14 @@ func (provider *Docker) createClient() (client.APIClient, error) { } } - return client.NewClient(provider.Endpoint, DockerAPIVersion, httpClient, httpHeaders) + var version string + if provider.SwarmMode { + version = SwarmAPIVersion + } else { + version = DockerAPIVersion + } + return client.NewClient(provider.Endpoint, version, httpClient, httpHeaders) + } // Provide allows the provider to provide configurations to traefik @@ -84,56 +126,96 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, po ctx := context.Background() version, err := dockerClient.ServerVersion(ctx) log.Debugf("Docker connection established with docker %s (API %s)", version.Version, version.APIVersion) - containers, err := listContainers(ctx, dockerClient) - if err != nil { - log.Errorf("Failed to list containers for docker, error %s", err) - return err + var dockerDataList []dockerData + if provider.SwarmMode { + dockerDataList, err = listServices(ctx, dockerClient) + if err != nil { + log.Errorf("Failed to list services for docker swarm mode, error %s", err) + return err + } + } else { + dockerDataList, err = listContainers(ctx, dockerClient) + if err != nil { + log.Errorf("Failed to list containers for docker, error %s", err) + return err + } } - configuration := provider.loadDockerConfig(containers) + + configuration := provider.loadDockerConfig(dockerDataList) configurationChan <- types.ConfigMessage{ ProviderName: "docker", Configuration: configuration, } if provider.Watch { ctx, cancel := context.WithCancel(ctx) - pool.Go(func(stop chan bool) { - for { - select { - case <-stop: + if provider.SwarmMode { + // TODO: This need to be change. Linked to Swarm events docker/docker#23827 + ticker := time.NewTicker(SwarmDefaultWatchTime) + pool.Go(func(stop chan bool) { + for { + select { + case <-ticker.C: + services, err := listServices(ctx, dockerClient) + if err != nil { + log.Errorf("Failed to list services for docker, error %s", err) + return + } + configuration := provider.loadDockerConfig(services) + if configuration != nil { + configurationChan <- types.ConfigMessage{ + ProviderName: "docker", + Configuration: configuration, + } + } + + case <-stop: + ticker.Stop() + cancel() + return + } + } + }) + + } else { + pool.Go(func(stop chan bool) { + for { + select { + case <-stop: + cancel() + return + } + } + }) + f := filters.NewArgs() + f.Add("type", "container") + options := dockertypes.EventsOptions{ + Filters: f, + } + eventHandler := events.NewHandler(events.ByAction) + startStopHandle := func(m eventtypes.Message) { + log.Debugf("Docker event received %+v", m) + containers, err := listContainers(ctx, dockerClient) + if err != nil { + log.Errorf("Failed to list containers for docker, error %s", err) + // Call cancel to get out of the monitor cancel() return } - } - }) - f := filters.NewArgs() - f.Add("type", "container") - options := dockertypes.EventsOptions{ - Filters: f, - } - eventHandler := events.NewHandler(events.ByAction) - startStopHandle := func(m eventtypes.Message) { - log.Debugf("Docker event received %+v", m) - containers, err := listContainers(ctx, dockerClient) - if err != nil { - log.Errorf("Failed to list containers for docker, error %s", err) - // Call cancel to get out of the monitor - cancel() - return - } - configuration := provider.loadDockerConfig(containers) - if configuration != nil { - configurationChan <- types.ConfigMessage{ - ProviderName: "docker", - Configuration: configuration, + configuration := provider.loadDockerConfig(containers) + if configuration != nil { + configurationChan <- types.ConfigMessage{ + ProviderName: "docker", + Configuration: configuration, + } } } - } - eventHandler.Handle("start", startStopHandle) - eventHandler.Handle("die", startStopHandle) + eventHandler.Handle("start", startStopHandle) + eventHandler.Handle("die", startStopHandle) - errChan := events.MonitorWithHandler(ctx, dockerClient, options, eventHandler) - if err := <-errChan; err != nil { - return err + errChan := events.MonitorWithHandler(ctx, dockerClient, options, eventHandler) + if err := <-errChan; err != nil { + return err + } } } return nil @@ -150,7 +232,7 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, po return nil } -func (provider *Docker) loadDockerConfig(containersInspected []dockertypes.ContainerJSON) *types.Configuration { +func (provider *Docker) loadDockerConfig(containersInspected []dockerData) *types.Configuration { var DockerFuncMap = template.FuncMap{ "getBackend": provider.getBackend, "getIPAddress": provider.getIPAddress, @@ -173,19 +255,19 @@ func (provider *Docker) loadDockerConfig(containersInspected []dockertypes.Conta } // filter containers - filteredContainers := fun.Filter(func(container dockertypes.ContainerJSON) bool { + filteredContainers := fun.Filter(func(container dockerData) bool { return provider.containerFilter(container) - }, containersInspected).([]dockertypes.ContainerJSON) + }, containersInspected).([]dockerData) - frontends := map[string][]dockertypes.ContainerJSON{} + frontends := map[string][]dockerData{} for _, container := range filteredContainers { frontendName := provider.getFrontendName(container) frontends[frontendName] = append(frontends[frontendName], container) } templateObjects := struct { - Containers []dockertypes.ContainerJSON - Frontends map[string][]dockertypes.ContainerJSON + Containers []dockerData + Frontends map[string][]dockerData Domain string }{ filteredContainers, @@ -200,21 +282,21 @@ func (provider *Docker) loadDockerConfig(containersInspected []dockertypes.Conta return configuration } -func (provider *Docker) hasCircuitBreakerLabel(container dockertypes.ContainerJSON) bool { +func (provider *Docker) hasCircuitBreakerLabel(container dockerData) bool { if _, err := getLabel(container, "traefik.backend.circuitbreaker.expression"); err != nil { return false } return true } -func (provider *Docker) hasLoadBalancerLabel(container dockertypes.ContainerJSON) bool { +func (provider *Docker) hasLoadBalancerLabel(container dockerData) bool { if _, err := getLabel(container, "traefik.backend.loadbalancer.method"); err != nil { return false } return true } -func (provider *Docker) hasMaxConnLabels(container dockertypes.ContainerJSON) bool { +func (provider *Docker) hasMaxConnLabels(container dockerData) bool { if _, err := getLabel(container, "traefik.backend.maxconn.amount"); err != nil { return false } @@ -224,21 +306,21 @@ func (provider *Docker) hasMaxConnLabels(container dockertypes.ContainerJSON) bo return true } -func (provider *Docker) getCircuitBreakerExpression(container dockertypes.ContainerJSON) string { +func (provider *Docker) getCircuitBreakerExpression(container dockerData) string { if label, err := getLabel(container, "traefik.backend.circuitbreaker.expression"); err == nil { return label } return "NetworkErrorRatio() > 1" } -func (provider *Docker) getLoadBalancerMethod(container dockertypes.ContainerJSON) string { +func (provider *Docker) getLoadBalancerMethod(container dockerData) string { if label, err := getLabel(container, "traefik.backend.loadbalancer.method"); err == nil { return label } return "wrr" } -func (provider *Docker) getMaxConnAmount(container dockertypes.ContainerJSON) int64 { +func (provider *Docker) getMaxConnAmount(container dockerData) int64 { if label, err := getLabel(container, "traefik.backend.maxconn.amount"); err == nil { i, errConv := strconv.ParseInt(label, 10, 64) if errConv != nil { @@ -250,15 +332,15 @@ func (provider *Docker) getMaxConnAmount(container dockertypes.ContainerJSON) in return math.MaxInt64 } -func (provider *Docker) getMaxConnExtractorFunc(container dockertypes.ContainerJSON) string { +func (provider *Docker) getMaxConnExtractorFunc(container dockerData) string { if label, err := getLabel(container, "traefik.backend.maxconn.extractorfunc"); err == nil { return label } return "request.host" } -func (provider *Docker) containerFilter(container dockertypes.ContainerJSON) bool { - _, err := strconv.Atoi(container.Config.Labels["traefik.port"]) +func (provider *Docker) containerFilter(container dockerData) bool { + _, err := strconv.Atoi(container.Labels["traefik.port"]) if len(container.NetworkSettings.Ports) == 0 && err != nil { log.Debugf("Filtering container without port and no traefik.port label %s", container.Name) return false @@ -273,7 +355,7 @@ func (provider *Docker) containerFilter(container dockertypes.ContainerJSON) boo return false } - constraintTags := strings.Split(container.Config.Labels["traefik.tags"], ",") + constraintTags := strings.Split(container.Labels["traefik.tags"], ",") if ok, failingConstraint := provider.MatchConstraints(constraintTags); !ok { if failingConstraint != nil { log.Debugf("Container %v pruned by '%v' constraint", container.Name, failingConstraint.String()) @@ -284,41 +366,41 @@ func (provider *Docker) containerFilter(container dockertypes.ContainerJSON) boo return true } -func (provider *Docker) getFrontendName(container dockertypes.ContainerJSON) string { +func (provider *Docker) getFrontendName(container dockerData) string { // Replace '.' with '-' in quoted keys because of this issue https://github.com/BurntSushi/toml/issues/78 return normalize(provider.getFrontendRule(container)) } // 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 (provider *Docker) getFrontendRule(container dockertypes.ContainerJSON) string { +func (provider *Docker) getFrontendRule(container dockerData) string { if label, err := getLabel(container, "traefik.frontend.rule"); err == nil { return label } return "Host:" + provider.getSubDomain(container.Name) + "." + provider.Domain } -func (provider *Docker) getBackend(container dockertypes.ContainerJSON) string { +func (provider *Docker) getBackend(container dockerData) string { if label, err := getLabel(container, "traefik.backend"); err == nil { return label } return normalize(container.Name) } -func (provider *Docker) getIPAddress(container dockertypes.ContainerJSON) string { +func (provider *Docker) getIPAddress(container dockerData) string { if label, err := getLabel(container, "traefik.docker.network"); err == nil && label != "" { - networks := container.NetworkSettings.Networks - if networks != nil { - network := networks[label] + networkSettings := container.NetworkSettings + if networkSettings.Networks != nil { + network := networkSettings.Networks[label] if network != nil { - return network.IPAddress + return network.Addr } } } // If net==host, quick n' dirty, we return 127.0.0.1 // This will work locally, but will fail with swarm. - if container.HostConfig != nil && "host" == container.HostConfig.NetworkMode { + if "host" == container.NetworkSettings.NetworkMode { return "127.0.0.1" } @@ -334,12 +416,12 @@ func (provider *Docker) getIPAddress(container dockertypes.ContainerJSON) string } for _, network := range container.NetworkSettings.Networks { - return network.IPAddress + return network.Addr } return "" } -func (provider *Docker) getPort(container dockertypes.ContainerJSON) string { +func (provider *Docker) getPort(container dockerData) string { if label, err := getLabel(container, "traefik.port"); err == nil { return label } @@ -349,54 +431,54 @@ func (provider *Docker) getPort(container dockertypes.ContainerJSON) string { return "" } -func (provider *Docker) getWeight(container dockertypes.ContainerJSON) string { +func (provider *Docker) getWeight(container dockerData) string { if label, err := getLabel(container, "traefik.weight"); err == nil { return label } return "1" } -func (provider *Docker) getDomain(container dockertypes.ContainerJSON) string { +func (provider *Docker) getDomain(container dockerData) string { if label, err := getLabel(container, "traefik.domain"); err == nil { return label } return provider.Domain } -func (provider *Docker) getProtocol(container dockertypes.ContainerJSON) string { +func (provider *Docker) getProtocol(container dockerData) string { if label, err := getLabel(container, "traefik.protocol"); err == nil { return label } return "http" } -func (provider *Docker) getPassHostHeader(container dockertypes.ContainerJSON) string { +func (provider *Docker) getPassHostHeader(container dockerData) string { if passHostHeader, err := getLabel(container, "traefik.frontend.passHostHeader"); err == nil { return passHostHeader } return "true" } -func (provider *Docker) getPriority(container dockertypes.ContainerJSON) string { +func (provider *Docker) getPriority(container dockerData) string { if priority, err := getLabel(container, "traefik.frontend.priority"); err == nil { return priority } return "0" } -func (provider *Docker) getEntryPoints(container dockertypes.ContainerJSON) []string { +func (provider *Docker) getEntryPoints(container dockerData) []string { if entryPoints, err := getLabel(container, "traefik.frontend.entryPoints"); err == nil { return strings.Split(entryPoints, ",") } return []string{} } -func isContainerEnabled(container dockertypes.ContainerJSON, exposedByDefault bool) bool { - return exposedByDefault && container.Config.Labels["traefik.enable"] != "false" || container.Config.Labels["traefik.enable"] == "true" +func isContainerEnabled(container dockerData, exposedByDefault bool) bool { + return exposedByDefault && container.Labels["traefik.enable"] != "false" || container.Labels["traefik.enable"] == "true" } -func getLabel(container dockertypes.ContainerJSON, label string) (string, error) { - for key, value := range container.Config.Labels { +func getLabel(container dockerData, label string) (string, error) { + for key, value := range container.Labels { if key == label { return value, nil } @@ -404,7 +486,7 @@ func getLabel(container dockertypes.ContainerJSON, label string) (string, error) return "", errors.New("Label not found:" + label) } -func getLabels(container dockertypes.ContainerJSON, labels []string) (map[string]string, error) { +func getLabels(container dockerData, labels []string) (map[string]string, error) { var globalErr error foundLabels := map[string]string{} for _, label := range labels { @@ -420,12 +502,12 @@ func getLabels(container dockertypes.ContainerJSON, labels []string) (map[string return foundLabels, globalErr } -func listContainers(ctx context.Context, dockerClient client.ContainerAPIClient) ([]dockertypes.ContainerJSON, error) { +func listContainers(ctx context.Context, dockerClient client.ContainerAPIClient) ([]dockerData, error) { containerList, err := dockerClient.ContainerList(ctx, dockertypes.ContainerListOptions{}) if err != nil { - return []dockertypes.ContainerJSON{}, err + return []dockerData{}, err } - containersInspected := []dockertypes.ContainerJSON{} + containersInspected := []dockerData{} // get inspect containers for _, container := range containerList { @@ -433,13 +515,114 @@ func listContainers(ctx context.Context, dockerClient client.ContainerAPIClient) if err != nil { log.Warnf("Failed to inspect container %s, error: %s", container.ID, err) } else { - containersInspected = append(containersInspected, containerInspected) + dockerData := parseContainer(containerInspected) + containersInspected = append(containersInspected, dockerData) } } return containersInspected, nil } +func parseContainer(container dockertypes.ContainerJSON) dockerData { + dockerData := dockerData{ + NetworkSettings: networkSettings{}, + } + + if container.ContainerJSONBase != nil { + dockerData.Name = container.ContainerJSONBase.Name + + if container.ContainerJSONBase.HostConfig != nil { + dockerData.NetworkSettings.NetworkMode = container.ContainerJSONBase.HostConfig.NetworkMode + } + } + + if container.Config != nil && container.Config.Labels != nil { + dockerData.Labels = container.Config.Labels + } + + if container.NetworkSettings != nil { + if container.NetworkSettings.Ports != nil { + dockerData.NetworkSettings.Ports = container.NetworkSettings.Ports + } + if container.NetworkSettings.Networks != nil { + dockerData.NetworkSettings.Networks = make(map[string]*networkData) + for name, containerNetwork := range container.NetworkSettings.Networks { + dockerData.NetworkSettings.Networks[name] = &networkData{ + ID: containerNetwork.NetworkID, + Name: name, + Addr: containerNetwork.IPAddress, + } + } + } + + } + + return dockerData +} + // Escape beginning slash "/", convert all others to dash "-" func (provider *Docker) getSubDomain(name string) string { return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1) } + +func listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerData, error) { + serviceList, err := dockerClient.ServiceList(ctx, dockertypes.ServiceListOptions{}) + if err != nil { + return []dockerData{}, err + } + networkListArgs := filters.NewArgs() + networkListArgs.Add("driver", "overlay") + + networkList, err := dockerClient.NetworkList(ctx, dockertypes.NetworkListOptions{Filters: networkListArgs}) + + networkMap := make(map[string]*dockertypes.NetworkResource) + if err != nil { + log.Debug("Failed to network inspect on client for docker, error: %s", err) + return []dockerData{}, err + } + for _, network := range networkList { + networkMap[network.ID] = &network + } + + var dockerDataList []dockerData + + for _, service := range serviceList { + dockerData := parseService(service, networkMap) + + dockerDataList = append(dockerDataList, dockerData) + } + return dockerDataList, err + +} + +func parseService(service swarmtypes.Service, networkMap map[string]*dockertypes.NetworkResource) dockerData { + dockerData := dockerData{ + Name: service.Spec.Annotations.Name, + Labels: service.Spec.Annotations.Labels, + NetworkSettings: networkSettings{}, + } + + if service.Spec.EndpointSpec != nil { + switch service.Spec.EndpointSpec.Mode { + case swarm.ResolutionModeDNSRR: + log.Debug("Ignored endpoint-mode not supported, service name: %s", dockerData.Name) + case swarm.ResolutionModeVIP: + dockerData.NetworkSettings.Networks = make(map[string]*networkData) + for _, virtualIP := range service.Endpoint.VirtualIPs { + networkService := networkMap[virtualIP.NetworkID] + if networkService != nil { + ip, _, _ := net.ParseCIDR(virtualIP.Addr) + network := &networkData{ + Name: networkService.Name, + ID: virtualIP.NetworkID, + Addr: ip.String(), + } + dockerData.NetworkSettings.Networks[network.Name] = network + } else { + log.Debug("Network not found, id: %s", virtualIP.NetworkID) + } + + } + } + } + return dockerData +} diff --git a/provider/docker_test.go b/provider/docker_test.go index b5fb67262..c344eb5f1 100644 --- a/provider/docker_test.go +++ b/provider/docker_test.go @@ -10,6 +10,8 @@ import ( "github.com/docker/engine-api/types/container" "github.com/docker/engine-api/types/network" "github.com/docker/go-connections/nat" + + swarm "github.com/docker/engine-api/types/swarm" ) func TestDockerGetFrontendName(t *testing.T) { @@ -85,7 +87,8 @@ func TestDockerGetFrontendName(t *testing.T) { } for _, e := range containers { - actual := provider.getFrontendName(e.container) + dockerData := parseContainer(e.container) + actual := provider.getFrontendName(dockerData) if actual != e.expected { t.Fatalf("expected %q, got %q", e.expected, actual) } @@ -148,7 +151,8 @@ func TestDockerGetFrontendRule(t *testing.T) { } for _, e := range containers { - actual := provider.getFrontendRule(e.container) + dockerData := parseContainer(e.container) + actual := provider.getFrontendRule(dockerData) if actual != e.expected { t.Fatalf("expected %q, got %q", e.expected, actual) } @@ -196,7 +200,8 @@ func TestDockerGetBackend(t *testing.T) { } for _, e := range containers { - actual := provider.getBackend(e.container) + dockerData := parseContainer(e.container) + actual := provider.getBackend(dockerData) if actual != e.expected { t.Fatalf("expected %q, got %q", e.expected, actual) } @@ -296,7 +301,8 @@ func TestDockerGetIPAddress(t *testing.T) { // TODO } for _, e := range containers { - actual := provider.getIPAddress(e.container) + dockerData := parseContainer(e.container) + actual := provider.getIPAddress(dockerData) if actual != e.expected { t.Fatalf("expected %q, got %q", e.expected, actual) } @@ -387,7 +393,8 @@ func TestDockerGetPort(t *testing.T) { } for _, e := range containers { - actual := provider.getPort(e.container) + dockerData := parseContainer(e.container) + actual := provider.getPort(dockerData) if actual != e.expected { t.Fatalf("expected %q, got %q", e.expected, actual) } @@ -426,7 +433,8 @@ func TestDockerGetWeight(t *testing.T) { } for _, e := range containers { - actual := provider.getWeight(e.container) + dockerData := parseContainer(e.container) + actual := provider.getWeight(dockerData) if actual != e.expected { t.Fatalf("expected %q, got %q", e.expected, actual) } @@ -467,7 +475,8 @@ func TestDockerGetDomain(t *testing.T) { } for _, e := range containers { - actual := provider.getDomain(e.container) + dockerData := parseContainer(e.container) + actual := provider.getDomain(dockerData) if actual != e.expected { t.Fatalf("expected %q, got %q", e.expected, actual) } @@ -506,7 +515,8 @@ func TestDockerGetProtocol(t *testing.T) { } for _, e := range containers { - actual := provider.getProtocol(e.container) + dockerData := parseContainer(e.container) + actual := provider.getProtocol(dockerData) if actual != e.expected { t.Fatalf("expected %q, got %q", e.expected, actual) } @@ -544,7 +554,8 @@ func TestDockerGetPassHostHeader(t *testing.T) { } for _, e := range containers { - actual := provider.getPassHostHeader(e.container) + dockerData := parseContainer(e.container) + actual := provider.getPassHostHeader(dockerData) if actual != e.expected { t.Fatalf("expected %q, got %q", e.expected, actual) } @@ -575,7 +586,8 @@ func TestDockerGetLabel(t *testing.T) { } for _, e := range containers { - label, err := getLabel(e.container, "foo") + dockerData := parseContainer(e.container) + label, err := getLabel(dockerData, "foo") if e.expected != "" { if err == nil || !strings.Contains(err.Error(), e.expected) { t.Fatalf("expected an error with %q, got %v", e.expected, err) @@ -632,7 +644,8 @@ func TestDockerGetLabels(t *testing.T) { } for _, e := range containers { - labels, err := getLabels(e.container, []string{"foo", "bar"}) + dockerData := parseContainer(e.container) + labels, err := getLabels(dockerData, []string{"foo", "bar"}) if !reflect.DeepEqual(labels, e.expectedLabels) { t.Fatalf("expect %v, got %v", e.expectedLabels, labels) } @@ -866,7 +879,8 @@ func TestDockerTraefikFilter(t *testing.T) { for _, e := range containers { provider.ExposedByDefault = e.exposedByDefault - actual := provider.containerFilter(e.container) + dockerData := parseContainer(e.container) + actual := provider.containerFilter(dockerData) if actual != e.expected { t.Fatalf("expected %v for %+v, got %+v", e.expected, e, actual) } @@ -1088,7 +1102,1046 @@ func TestDockerLoadDockerConfig(t *testing.T) { } for _, c := range cases { - actualConfig := provider.loadDockerConfig(c.containers) + var dockerDataList []dockerData + for _, container := range c.containers { + dockerData := parseContainer(container) + dockerDataList = append(dockerDataList, dockerData) + } + + actualConfig := provider.loadDockerConfig(dockerDataList) + // Compare backends + if !reflect.DeepEqual(actualConfig.Backends, c.expectedBackends) { + t.Fatalf("expected %#v, got %#v", c.expectedBackends, actualConfig.Backends) + } + if !reflect.DeepEqual(actualConfig.Frontends, c.expectedFrontends) { + t.Fatalf("expected %#v, got %#v", c.expectedFrontends, actualConfig.Frontends) + } + } +} + +func TestSwarmGetFrontendName(t *testing.T) { + provider := &Docker{ + Domain: "docker.localhost", + SwarmMode: true, + } + + services := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "foo", + }, + }, + }, + expected: "Host-foo-docker-localhost", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "bar", + Labels: map[string]string{ + "traefik.frontend.rule": "Headers:User-Agent,bat/0.1.0", + }, + }, + }, + }, + expected: "Headers-User-Agent-bat-0-1-0", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "test", + Labels: map[string]string{ + "traefik.frontend.rule": "Host:foo.bar", + }, + }, + }, + }, + expected: "Host-foo-bar", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "test", + Labels: map[string]string{ + "traefik.frontend.rule": "Path:/test", + }, + }, + }, + }, + expected: "Path-test", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "test", + Labels: map[string]string{ + "traefik.frontend.rule": "PathPrefix:/test2", + }, + }, + }, + }, + expected: "PathPrefix-test2", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for _, e := range services { + dockerData := parseService(e.service, e.networks) + actual := provider.getFrontendName(dockerData) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + +func TestSwarmGetFrontendRule(t *testing.T) { + provider := &Docker{ + Domain: "docker.localhost", + SwarmMode: true, + } + + services := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "foo", + }, + }, + }, + expected: "Host:foo.docker.localhost", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "bar", + }, + }, + }, + expected: "Host:bar.docker.localhost", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "test", + Labels: map[string]string{ + "traefik.frontend.rule": "Host:foo.bar", + }, + }, + }, + }, + expected: "Host:foo.bar", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "test", + Labels: map[string]string{ + "traefik.frontend.rule": "Path:/test", + }, + }, + }, + }, + expected: "Path:/test", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for _, e := range services { + dockerData := parseService(e.service, e.networks) + actual := provider.getFrontendRule(dockerData) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + +func TestSwarmGetBackend(t *testing.T) { + provider := &Docker{ + SwarmMode: true, + } + + services := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "foo", + }, + }, + }, + expected: "foo", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "bar", + }, + }, + }, + expected: "bar", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "test", + Labels: map[string]string{ + "traefik.backend": "foobar", + }, + }, + }, + }, + expected: "foobar", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for _, e := range services { + dockerData := parseService(e.service, e.networks) + actual := provider.getBackend(dockerData) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + +func TestSwarmGetIPAddress(t *testing.T) { + provider := &Docker{ + SwarmMode: true, + } + + services := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "bar", + }, + EndpointSpec: &swarm.EndpointSpec{ + Mode: swarm.ResolutionModeDNSRR, + }, + }, + }, + expected: "", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "bar", + }, + EndpointSpec: &swarm.EndpointSpec{ + Mode: swarm.ResolutionModeVIP, + }, + }, + Endpoint: swarm.Endpoint{ + VirtualIPs: []swarm.EndpointVirtualIP{ + { + NetworkID: "1", + Addr: "10.11.12.13/24", + }, + }, + }, + }, + expected: "10.11.12.13", + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foo", + }, + }, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "bar", + Labels: map[string]string{ + "traefik.docker.network": "barnet", + }, + }, + EndpointSpec: &swarm.EndpointSpec{ + Mode: swarm.ResolutionModeVIP, + }, + }, + Endpoint: swarm.Endpoint{ + VirtualIPs: []swarm.EndpointVirtualIP{ + { + NetworkID: "1", + Addr: "10.11.12.13/24", + }, + { + NetworkID: "2", + Addr: "10.11.12.99/24", + }, + }, + }, + }, + expected: "10.11.12.99", + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foonet", + }, + "2": { + Name: "barnet", + }, + }, + }, + } + + for _, e := range services { + dockerData := parseService(e.service, e.networks) + actual := provider.getIPAddress(dockerData) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + +func TestSwarmGetPort(t *testing.T) { + provider := &Docker{ + SwarmMode: true, + } + + services := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "bar", + Labels: map[string]string{ + "traefik.port": "8080", + }, + }, + EndpointSpec: &swarm.EndpointSpec{ + Mode: swarm.ResolutionModeDNSRR, + }, + }, + }, + expected: "8080", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for _, e := range services { + dockerData := parseService(e.service, e.networks) + actual := provider.getPort(dockerData) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + +func TestSwarmGetWeight(t *testing.T) { + provider := &Docker{ + SwarmMode: true, + } + + services := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "bar", + }, + EndpointSpec: &swarm.EndpointSpec{ + Mode: swarm.ResolutionModeDNSRR, + }, + }, + }, + expected: "1", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "bar", + Labels: map[string]string{ + "traefik.weight": "10", + }, + }, + EndpointSpec: &swarm.EndpointSpec{ + Mode: swarm.ResolutionModeDNSRR, + }, + }, + }, + expected: "10", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for _, e := range services { + dockerData := parseService(e.service, e.networks) + actual := provider.getWeight(dockerData) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + +func TestSwarmGetDomain(t *testing.T) { + provider := &Docker{ + Domain: "docker.localhost", + SwarmMode: true, + } + + services := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "foo", + }, + EndpointSpec: &swarm.EndpointSpec{ + Mode: swarm.ResolutionModeDNSRR, + }, + }, + }, + expected: "docker.localhost", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "bar", + Labels: map[string]string{ + "traefik.domain": "foo.bar", + }, + }, + EndpointSpec: &swarm.EndpointSpec{ + Mode: swarm.ResolutionModeDNSRR, + }, + }, + }, + expected: "foo.bar", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for _, e := range services { + dockerData := parseService(e.service, e.networks) + actual := provider.getDomain(dockerData) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + +func TestSwarmGetProtocol(t *testing.T) { + provider := &Docker{ + SwarmMode: true, + } + + services := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "bar", + }, + EndpointSpec: &swarm.EndpointSpec{ + Mode: swarm.ResolutionModeDNSRR, + }, + }, + }, + expected: "http", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "bar", + Labels: map[string]string{ + "traefik.protocol": "https", + }, + }, + EndpointSpec: &swarm.EndpointSpec{ + Mode: swarm.ResolutionModeDNSRR, + }, + }, + }, + expected: "https", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for _, e := range services { + dockerData := parseService(e.service, e.networks) + actual := provider.getProtocol(dockerData) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + +func TestSwarmGetPassHostHeader(t *testing.T) { + provider := &Docker{ + SwarmMode: true, + } + + services := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "bar", + }, + EndpointSpec: &swarm.EndpointSpec{ + Mode: swarm.ResolutionModeDNSRR, + }, + }, + }, + expected: "true", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "bar", + Labels: map[string]string{ + "traefik.frontend.passHostHeader": "false", + }, + }, + EndpointSpec: &swarm.EndpointSpec{ + Mode: swarm.ResolutionModeDNSRR, + }, + }, + }, + expected: "false", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for _, e := range services { + dockerData := parseService(e.service, e.networks) + actual := provider.getPassHostHeader(dockerData) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + +func TestSwarmGetLabel(t *testing.T) { + + services := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "bar", + }, + EndpointSpec: &swarm.EndpointSpec{ + Mode: swarm.ResolutionModeDNSRR, + }, + }, + }, + expected: "Label not found:", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "test", + Labels: map[string]string{ + "foo": "bar", + }, + }, + EndpointSpec: &swarm.EndpointSpec{ + Mode: swarm.ResolutionModeDNSRR, + }, + }, + }, + expected: "", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for _, e := range services { + dockerData := parseService(e.service, e.networks) + label, err := getLabel(dockerData, "foo") + if e.expected != "" { + if err == nil || !strings.Contains(err.Error(), e.expected) { + t.Fatalf("expected an error with %q, got %v", e.expected, err) + } + } else { + if label != "bar" { + t.Fatalf("expected label 'bar', got %s", label) + } + } + } +} + +func TestSwarmGetLabels(t *testing.T) { + services := []struct { + service swarm.Service + expectedLabels map[string]string + expectedError string + networks map[string]*docker.NetworkResource + }{ + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "test", + }, + }, + }, + expectedLabels: map[string]string{}, + expectedError: "Label not found:", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "test", + Labels: map[string]string{ + "foo": "fooz", + }, + }, + }, + }, + expectedLabels: map[string]string{ + "foo": "fooz", + }, + expectedError: "Label not found: bar", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "test", + Labels: map[string]string{ + "foo": "fooz", + "bar": "barz", + }, + }, + }, + }, + expectedLabels: map[string]string{ + "foo": "fooz", + "bar": "barz", + }, + expectedError: "", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for _, e := range services { + dockerData := parseService(e.service, e.networks) + labels, err := getLabels(dockerData, []string{"foo", "bar"}) + if !reflect.DeepEqual(labels, e.expectedLabels) { + t.Fatalf("expect %v, got %v", e.expectedLabels, labels) + } + if e.expectedError != "" { + if err == nil || !strings.Contains(err.Error(), e.expectedError) { + t.Fatalf("expected an error with %q, got %v", e.expectedError, err) + } + } + } +} + +func TestSwarmTraefikFilter(t *testing.T) { + provider := &Docker{ + SwarmMode: true, + } + services := []struct { + service swarm.Service + exposedByDefault bool + expected bool + networks map[string]*docker.NetworkResource + }{ + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "container", + }, + }, + }, + exposedByDefault: true, + expected: false, + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "container", + Labels: map[string]string{ + "traefik.enable": "false", + "traefik.port": "80", + }, + }, + }, + }, + exposedByDefault: true, + expected: false, + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "container", + Labels: map[string]string{ + "traefik.frontend.rule": "Host:foo.bar", + "traefik.port": "80", + }, + }, + }, + }, + exposedByDefault: true, + expected: true, + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "container", + Labels: map[string]string{ + "traefik.port": "80", + }, + }, + }, + }, + exposedByDefault: true, + expected: true, + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "container", + Labels: map[string]string{ + "traefik.enable": "true", + "traefik.port": "80", + }, + }, + }, + }, + exposedByDefault: true, + expected: true, + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "container", + Labels: map[string]string{ + "traefik.enable": "anything", + "traefik.port": "80", + }, + }, + }, + }, + exposedByDefault: true, + expected: true, + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "container", + Labels: map[string]string{ + "traefik.frontend.rule": "Host:foo.bar", + "traefik.port": "80", + }, + }, + }, + }, + exposedByDefault: true, + expected: true, + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "container", + Labels: map[string]string{ + "traefik.port": "80", + }, + }, + }, + }, + exposedByDefault: false, + expected: false, + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarm.Service{ + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "container", + Labels: map[string]string{ + "traefik.enable": "true", + "traefik.port": "80", + }, + }, + }, + }, + exposedByDefault: false, + expected: true, + networks: map[string]*docker.NetworkResource{}, + }, + } + + for _, e := range services { + dockerData := parseService(e.service, e.networks) + provider.ExposedByDefault = e.exposedByDefault + actual := provider.containerFilter(dockerData) + if actual != e.expected { + t.Fatalf("expected %v for %+v, got %+v", e.expected, e, actual) + } + } +} + +func TestSwarmLoadDockerConfig(t *testing.T) { + cases := []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{ + { + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "test", + Labels: map[string]string{ + "traefik.port": "80", + }, + }, + EndpointSpec: &swarm.EndpointSpec{ + Mode: swarm.ResolutionModeVIP, + }, + }, + Endpoint: swarm.Endpoint{ + VirtualIPs: []swarm.EndpointVirtualIP{ + { + Addr: "127.0.0.1/24", + NetworkID: "1", + }, + }, + }, + }, + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-test-docker-localhost": { + Backend: "backend-test", + PassHostHeader: true, + EntryPoints: []string{}, + Routes: map[string]types.Route{ + "route-frontend-Host-test-docker-localhost": { + 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: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: nil, + }, + }, + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foo", + }, + }, + }, + { + services: []swarm.Service{ + { + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "test1", + Labels: map[string]string{ + "traefik.port": "80", + "traefik.backend": "foobar", + "traefik.frontend.entryPoints": "http,https", + }, + }, + EndpointSpec: &swarm.EndpointSpec{ + Mode: swarm.ResolutionModeVIP, + }, + }, + Endpoint: swarm.Endpoint{ + VirtualIPs: []swarm.EndpointVirtualIP{ + { + Addr: "127.0.0.1/24", + NetworkID: "1", + }, + }, + }, + }, + { + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "test2", + Labels: map[string]string{ + "traefik.port": "80", + "traefik.backend": "foobar", + }, + }, + EndpointSpec: &swarm.EndpointSpec{ + Mode: swarm.ResolutionModeVIP, + }, + }, + Endpoint: swarm.Endpoint{ + VirtualIPs: []swarm.EndpointVirtualIP{ + { + Addr: "127.0.0.1/24", + NetworkID: "1", + }, + }, + }, + }, + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-test1-docker-localhost": { + Backend: "backend-foobar", + PassHostHeader: true, + EntryPoints: []string{"http", "https"}, + Routes: map[string]types.Route{ + "route-frontend-Host-test1-docker-localhost": { + Rule: "Host:test1.docker.localhost", + }, + }, + }, + "frontend-Host-test2-docker-localhost": { + Backend: "backend-foobar", + PassHostHeader: true, + EntryPoints: []string{}, + Routes: map[string]types.Route{ + "route-frontend-Host-test2-docker-localhost": { + 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: 1, + }, + "server-test2": { + URL: "http://127.0.0.1:80", + Weight: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: nil, + }, + }, + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foo", + }, + }, + }, + } + + provider := &Docker{ + Domain: "docker.localhost", + ExposedByDefault: true, + SwarmMode: true, + } + + for _, c := range cases { + var dockerDataList []dockerData + for _, service := range c.services { + dockerData := parseService(service, c.networks) + dockerDataList = append(dockerDataList, dockerData) + } + + actualConfig := provider.loadDockerConfig(dockerDataList) // Compare backends if !reflect.DeepEqual(actualConfig.Backends, c.expectedBackends) { t.Fatalf("expected %#v, got %#v", c.expectedBackends, actualConfig.Backends) diff --git a/server.go b/server.go index 6920e83a4..42c1b464d 100644 --- a/server.go +++ b/server.go @@ -8,7 +8,6 @@ import ( "crypto/x509" "encoding/json" "errors" - "golang.org/x/net/context" "io/ioutil" "net/http" "net/url" @@ -20,6 +19,8 @@ import ( "syscall" "time" + "golang.org/x/net/context" + log "github.com/Sirupsen/logrus" "github.com/codegangsta/negroni" "github.com/containous/mux" diff --git a/traefik.sample.toml b/traefik.sample.toml index c0f6d7186..2369c9fb6 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -311,6 +311,66 @@ # insecureskipverify = true + +################################################################ +# Docker Swarmmode configuration backend +################################################################ + +# Enable Docker configuration backend +# +# Optional +# +# [docker] + +# Docker server endpoint. Can be a tcp or a unix socket endpoint. +# +# Required +# +# endpoint = "tcp://127.0.0.1:2375" + +# Default domain used. +# Can be overridden by setting the "traefik.domain" label on a services. +# +# Required +# +# domain = "docker.localhost" + +# Enable watch docker changes +# +# Optional +# +# watch = true + +# Use Docker Swarm Mode as data provider +# +# Optional +# +# swarmmode = true + +# Override default configuration template. For advanced users :) +# +# Optional +# +# filename = "docker.tmpl" + +# Expose services by default in traefik +# +# Optional +# Default: true +# +# exposedbydefault = true + +# Enable docker TLS connection +# +# Optional +# +# [swarm.tls] +# ca = "/etc/ssl/ca.crt" +# cert = "/etc/ssl/docker.crt" +# key = "/etc/ssl/docker.key" +# insecureskipverify = true + + ################################################################ # Mesos/Marathon configuration backend ################################################################