diff --git a/configuration.go b/configuration.go index dca77f806..27381a01e 100644 --- a/configuration.go +++ b/configuration.go @@ -12,6 +12,7 @@ import ( "github.com/containous/flaeg" "github.com/containous/traefik/acme" "github.com/containous/traefik/provider" + "github.com/containous/traefik/provider/docker" "github.com/containous/traefik/types" ) @@ -40,7 +41,7 @@ type GlobalConfiguration struct { IdleTimeout flaeg.Duration `description:"maximum amount of time an idle (keep-alive) connection will remain idle before closing itself."` InsecureSkipVerify bool `description:"Disable SSL certificate verification"` Retry *Retry `description:"Enable retry sending request if network error"` - Docker *provider.Docker `description:"Enable Docker backend"` + Docker *docker.Provider `description:"Enable Docker backend"` File *provider.File `description:"Enable File backend"` Web *WebProvider `description:"Enable Web backend"` Marathon *provider.Marathon `description:"Enable Marathon backend"` @@ -328,7 +329,7 @@ type Retry struct { // NewTraefikDefaultPointersConfiguration creates a TraefikConfiguration with pointers default values func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { //default Docker - var defaultDocker provider.Docker + var defaultDocker docker.Provider defaultDocker.Watch = true defaultDocker.ExposedByDefault = true defaultDocker.Endpoint = "unix:///var/run/docker.sock" diff --git a/provider/consul_catalog.go b/provider/consul_catalog.go index 981ecda74..77d8e6667 100644 --- a/provider/consul_catalog.go +++ b/provider/consul_catalog.go @@ -176,7 +176,7 @@ func (provider *ConsulCatalog) getBackendName(node *api.ServiceEntry, index int) serviceName := strings.ToLower(node.Service.Service) + "--" + node.Service.Address + "--" + strconv.Itoa(node.Service.Port) for _, tag := range node.Service.Tags { - serviceName += "--" + normalize(tag) + serviceName += "--" + Normalize(tag) } serviceName = strings.Replace(serviceName, ".", "-", -1) @@ -246,7 +246,7 @@ func (provider *ConsulCatalog) buildConfig(catalog []catalogUpdate) *types.Confi Nodes: allNodes, } - configuration, err := provider.getConfiguration("templates/consul_catalog.tmpl", FuncMap, templateObjects) + configuration, err := provider.GetConfiguration("templates/consul_catalog.tmpl", FuncMap, templateObjects) if err != nil { log.WithError(err).Error("Failed to create config") } diff --git a/provider/docker/builder_test.go b/provider/docker/builder_test.go new file mode 100644 index 000000000..555ce31c6 --- /dev/null +++ b/provider/docker/builder_test.go @@ -0,0 +1,179 @@ +package docker + +import ( + docker "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/container" + "github.com/docker/engine-api/types/network" + "github.com/docker/engine-api/types/swarm" + "github.com/docker/go-connections/nat" +) + +func containerJSON(ops ...func(*docker.ContainerJSON)) docker.ContainerJSON { + c := &docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "fake", + HostConfig: &container.HostConfig{}, + }, + Config: &container.Config{}, + NetworkSettings: &docker.NetworkSettings{ + NetworkSettingsBase: docker.NetworkSettingsBase{}, + }, + } + + for _, op := range ops { + op(c) + } + + return *c +} + +func name(name string) func(*docker.ContainerJSON) { + return func(c *docker.ContainerJSON) { + c.ContainerJSONBase.Name = name + } +} + +func networkMode(mode string) func(*docker.ContainerJSON) { + return func(c *docker.ContainerJSON) { + c.ContainerJSONBase.HostConfig.NetworkMode = container.NetworkMode(mode) + } +} + +func labels(labels map[string]string) func(*docker.ContainerJSON) { + return func(c *docker.ContainerJSON) { + c.Config.Labels = labels + } +} + +func ports(portMap nat.PortMap) func(*docker.ContainerJSON) { + return func(c *docker.ContainerJSON) { + c.NetworkSettings.NetworkSettingsBase.Ports = portMap + } +} + +func withNetwork(name string, ops ...func(*network.EndpointSettings)) func(*docker.ContainerJSON) { + return func(c *docker.ContainerJSON) { + if c.NetworkSettings.Networks == nil { + c.NetworkSettings.Networks = map[string]*network.EndpointSettings{} + } + c.NetworkSettings.Networks[name] = &network.EndpointSettings{} + for _, op := range ops { + op(c.NetworkSettings.Networks[name]) + } + } +} + +func ipv4(ip string) func(*network.EndpointSettings) { + return func(s *network.EndpointSettings) { + s.IPAddress = ip + } +} + +func swarmTask(id string, ops ...func(*swarm.Task)) swarm.Task { + task := &swarm.Task{ + ID: id, + } + + for _, op := range ops { + op(task) + } + + return *task +} + +func taskSlot(slot int) func(*swarm.Task) { + return func(task *swarm.Task) { + task.Slot = slot + } +} + +func taskStatus(ops ...func(*swarm.TaskStatus)) func(*swarm.Task) { + return func(task *swarm.Task) { + status := &swarm.TaskStatus{} + + for _, op := range ops { + op(status) + } + + task.Status = *status + } +} + +func taskState(state swarm.TaskState) func(*swarm.TaskStatus) { + return func(status *swarm.TaskStatus) { + status.State = state + } +} + +func swarmService(ops ...func(*swarm.Service)) swarm.Service { + service := &swarm.Service{ + ID: "serviceID", + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "defaultServiceName", + }, + }, + } + + for _, op := range ops { + op(service) + } + + return *service +} + +func serviceName(name string) func(service *swarm.Service) { + return func(service *swarm.Service) { + service.Spec.Annotations.Name = name + } +} + +func serviceLabels(labels map[string]string) func(service *swarm.Service) { + return func(service *swarm.Service) { + service.Spec.Annotations.Labels = labels + } +} + +func withEndpoint(ops ...func(*swarm.Endpoint)) func(*swarm.Service) { + return func(service *swarm.Service) { + endpoint := &swarm.Endpoint{} + + for _, op := range ops { + op(endpoint) + } + + service.Endpoint = *endpoint + } +} + +func virtualIP(networkID, addr string) func(*swarm.Endpoint) { + return func(endpoint *swarm.Endpoint) { + if endpoint.VirtualIPs == nil { + endpoint.VirtualIPs = []swarm.EndpointVirtualIP{} + } + endpoint.VirtualIPs = append(endpoint.VirtualIPs, swarm.EndpointVirtualIP{ + NetworkID: networkID, + Addr: addr, + }) + } +} + +func withEndpointSpec(ops ...func(*swarm.EndpointSpec)) func(*swarm.Service) { + return func(service *swarm.Service) { + endpointSpec := &swarm.EndpointSpec{} + + for _, op := range ops { + op(endpointSpec) + } + + service.Spec.EndpointSpec = endpointSpec + } +} + +func modeDNSSR(spec *swarm.EndpointSpec) { + spec.Mode = swarm.ResolutionModeDNSRR +} + +func modeVIP(spec *swarm.EndpointSpec) { + spec.Mode = swarm.ResolutionModeVIP +} diff --git a/provider/docker.go b/provider/docker/docker.go similarity index 73% rename from provider/docker.go rename to provider/docker/docker.go index 408e21b0a..0396fc054 100644 --- a/provider/docker.go +++ b/provider/docker/docker.go @@ -1,4 +1,4 @@ -package provider +package docker import ( "context" @@ -6,6 +6,7 @@ import ( "math" "net" "net/http" + "regexp" "strconv" "strings" "text/template" @@ -15,6 +16,7 @@ import ( "github.com/cenk/backoff" "github.com/containous/traefik/job" "github.com/containous/traefik/log" + "github.com/containous/traefik/provider" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" "github.com/containous/traefik/version" @@ -28,30 +30,29 @@ import ( "github.com/docker/go-connections/nat" "github.com/docker/go-connections/sockets" "github.com/vdemeester/docker-events" - "regexp" ) const ( - // SwarmAPIVersion is a constant holding the version of the Docker API traefik will use + // SwarmAPIVersion is a constant holding the version of the Provider API traefik will use SwarmAPIVersion string = "1.24" // SwarmDefaultWatchTime is the duration of the interval when polling docker SwarmDefaultWatchTime = 15 * time.Second ) -var _ Provider = (*Docker)(nil) +var _ provider.Provider = (*Provider)(nil) -// Docker holds configurations of the Docker provider. -type Docker struct { - BaseProvider `mapstructure:",squash"` - Endpoint string `description:"Docker server endpoint. Can be a tcp or a unix socket endpoint"` - Domain string `description:"Default domain used"` - 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"` +// Provider holds configurations of the Provider. +type Provider struct { + provider.BaseProvider `mapstructure:",squash"` + Endpoint string `description:"Provider server endpoint. Can be a tcp or a unix socket endpoint"` + Domain string `description:"Default domain used"` + TLS *provider.ClientTLS `description:"Enable Provider 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 Provider on Swarm Mode"` } -// dockerData holds the need data to the Docker provider +// dockerData holds the need data to the Provider p type dockerData struct { ServiceName string Name string @@ -60,14 +61,14 @@ type dockerData struct { Health string } -// NetworkSettings holds the networks data to the Docker provider +// NetworkSettings holds the networks data to the Provider p type networkSettings struct { NetworkMode dockercontainertypes.NetworkMode Ports nat.PortMap Networks map[string]*networkData } -// Network holds the network data to the Docker provider +// Network holds the network data to the Provider p type networkData struct { Name string Addr string @@ -76,20 +77,20 @@ type networkData struct { ID string } -func (provider *Docker) createClient() (client.APIClient, error) { +func (p *Provider) createClient() (client.APIClient, error) { var httpClient *http.Client httpHeaders := map[string]string{ "User-Agent": "Traefik " + version.Version, } - if provider.TLS != nil { - config, err := provider.TLS.CreateTLSConfig() + if p.TLS != nil { + config, err := p.TLS.CreateTLSConfig() if err != nil { return nil, err } tr := &http.Transport{ TLSClientConfig: config, } - proto, addr, _, err := client.ParseHost(provider.Endpoint) + proto, addr, _, err := client.ParseHost(p.Endpoint) if err != nil { return nil, err } @@ -102,25 +103,25 @@ func (provider *Docker) createClient() (client.APIClient, error) { } var version string - if provider.SwarmMode { + if p.SwarmMode { version = SwarmAPIVersion } else { version = DockerAPIVersion } - return client.NewClient(provider.Endpoint, version, httpClient, httpHeaders) + return client.NewClient(p.Endpoint, version, httpClient, httpHeaders) } -// Provide allows the provider to provide configurations to traefik +// Provide allows the p to provide configurations to traefik // using the given configuration channel. -func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error { - provider.Constraints = append(provider.Constraints, constraints...) +func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error { + p.Constraints = append(p.Constraints, constraints...) // TODO register this routine in pool, and watch for stop channel safe.Go(func() { operation := func() error { var err error - dockerClient, err := provider.createClient() + dockerClient, err := p.createClient() if err != nil { log.Errorf("Failed to create a client for docker, error: %s", err) return err @@ -128,10 +129,10 @@ 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) + log.Debugf("Provider connection established with docker %s (API %s)", version.Version, version.APIVersion) var dockerDataList []dockerData - if provider.SwarmMode { - dockerDataList, err = provider.listServices(ctx, dockerClient) + if p.SwarmMode { + dockerDataList, err = p.listServices(ctx, dockerClient) if err != nil { log.Errorf("Failed to list services for docker swarm mode, error %s", err) return err @@ -144,26 +145,26 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, po } } - configuration := provider.loadDockerConfig(dockerDataList) + configuration := p.loadDockerConfig(dockerDataList) configurationChan <- types.ConfigMessage{ ProviderName: "docker", Configuration: configuration, } - if provider.Watch { + if p.Watch { ctx, cancel := context.WithCancel(ctx) - if provider.SwarmMode { + if p.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 := provider.listServices(ctx, dockerClient) + services, err := p.listServices(ctx, dockerClient) if err != nil { log.Errorf("Failed to list services for docker, error %s", err) return } - configuration := provider.loadDockerConfig(services) + configuration := p.loadDockerConfig(services) if configuration != nil { configurationChan <- types.ConfigMessage{ ProviderName: "docker", @@ -196,7 +197,7 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, po } eventHandler := events.NewHandler(events.ByAction) startStopHandle := func(m eventtypes.Message) { - log.Debugf("Docker event received %+v", m) + log.Debugf("Provider event received %+v", m) containers, err := listContainers(ctx, dockerClient) if err != nil { log.Errorf("Failed to list containers for docker, error %s", err) @@ -204,7 +205,7 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, po cancel() return } - configuration := provider.loadDockerConfig(containers) + configuration := p.loadDockerConfig(containers) if configuration != nil { configurationChan <- types.ConfigMessage{ ProviderName: "docker", @@ -227,7 +228,7 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, po return nil } notify := func(err error, time time.Duration) { - log.Errorf("Docker connection error %+v, retrying in %s", err, time) + log.Errorf("Provider connection error %+v, retrying in %s", err, time) } err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify) if err != nil { @@ -238,50 +239,50 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, po return nil } -func (provider *Docker) loadDockerConfig(containersInspected []dockerData) *types.Configuration { +func (p *Provider) loadDockerConfig(containersInspected []dockerData) *types.Configuration { var DockerFuncMap = template.FuncMap{ - "getBackend": provider.getBackend, - "getIPAddress": provider.getIPAddress, - "getPort": provider.getPort, - "getWeight": provider.getWeight, - "getDomain": provider.getDomain, - "getProtocol": provider.getProtocol, - "getPassHostHeader": provider.getPassHostHeader, - "getPriority": provider.getPriority, - "getEntryPoints": provider.getEntryPoints, - "getFrontendRule": provider.getFrontendRule, - "hasCircuitBreakerLabel": provider.hasCircuitBreakerLabel, - "getCircuitBreakerExpression": provider.getCircuitBreakerExpression, - "hasLoadBalancerLabel": provider.hasLoadBalancerLabel, - "getLoadBalancerMethod": provider.getLoadBalancerMethod, - "hasMaxConnLabels": provider.hasMaxConnLabels, - "getMaxConnAmount": provider.getMaxConnAmount, - "getMaxConnExtractorFunc": provider.getMaxConnExtractorFunc, - "getSticky": provider.getSticky, - "getIsBackendLBSwarm": provider.getIsBackendLBSwarm, - "hasServices": provider.hasServices, - "getServiceNames": provider.getServiceNames, - "getServicePort": provider.getServicePort, - "getServiceWeight": provider.getServiceWeight, - "getServiceProtocol": provider.getServiceProtocol, - "getServiceEntryPoints": provider.getServiceEntryPoints, - "getServiceFrontendRule": provider.getServiceFrontendRule, - "getServicePassHostHeader": provider.getServicePassHostHeader, - "getServicePriority": provider.getServicePriority, - "getServiceBackend": provider.getServiceBackend, + "getBackend": p.getBackend, + "getIPAddress": p.getIPAddress, + "getPort": p.getPort, + "getWeight": p.getWeight, + "getDomain": p.getDomain, + "getProtocol": p.getProtocol, + "getPassHostHeader": p.getPassHostHeader, + "getPriority": p.getPriority, + "getEntryPoints": p.getEntryPoints, + "getFrontendRule": p.getFrontendRule, + "hasCircuitBreakerLabel": p.hasCircuitBreakerLabel, + "getCircuitBreakerExpression": p.getCircuitBreakerExpression, + "hasLoadBalancerLabel": p.hasLoadBalancerLabel, + "getLoadBalancerMethod": p.getLoadBalancerMethod, + "hasMaxConnLabels": p.hasMaxConnLabels, + "getMaxConnAmount": p.getMaxConnAmount, + "getMaxConnExtractorFunc": p.getMaxConnExtractorFunc, + "getSticky": p.getSticky, + "getIsBackendLBSwarm": p.getIsBackendLBSwarm, + "hasServices": p.hasServices, + "getServiceNames": p.getServiceNames, + "getServicePort": p.getServicePort, + "getServiceWeight": p.getServiceWeight, + "getServiceProtocol": p.getServiceProtocol, + "getServiceEntryPoints": p.getServiceEntryPoints, + "getServiceFrontendRule": p.getServiceFrontendRule, + "getServicePassHostHeader": p.getServicePassHostHeader, + "getServicePriority": p.getServicePriority, + "getServiceBackend": p.getServiceBackend, } // filter containers filteredContainers := fun.Filter(func(container dockerData) bool { - return provider.containerFilter(container) + return p.containerFilter(container) }, containersInspected).([]dockerData) frontends := map[string][]dockerData{} backends := map[string]dockerData{} servers := map[string][]dockerData{} for _, container := range filteredContainers { - frontendName := provider.getFrontendName(container) + frontendName := p.getFrontendName(container) frontends[frontendName] = append(frontends[frontendName], container) - backendName := provider.getBackend(container) + backendName := p.getBackend(container) backends[backendName] = container servers[backendName] = append(servers[backendName], container) } @@ -297,17 +298,17 @@ func (provider *Docker) loadDockerConfig(containersInspected []dockerData) *type frontends, backends, servers, - provider.Domain, + p.Domain, } - configuration, err := provider.getConfiguration("templates/docker.tmpl", DockerFuncMap, templateObjects) + configuration, err := p.GetConfiguration("templates/docker.tmpl", DockerFuncMap, templateObjects) if err != nil { log.Error(err) } return configuration } -func (provider *Docker) hasCircuitBreakerLabel(container dockerData) bool { +func (p *Provider) hasCircuitBreakerLabel(container dockerData) bool { if _, err := getLabel(container, "traefik.backend.circuitbreaker.expression"); err != nil { return false } @@ -323,7 +324,7 @@ var servicesPropertiesRegexp = regexp.MustCompile(`^traefik\.(?P.* type labelServiceProperties map[string]map[string]string // Check if for the given container, we find labels that are defining services -func (provider *Docker) hasServices(container dockerData) bool { +func (p *Provider) hasServices(container dockerData) bool { return len(extractServicesLabels(container.Labels)) > 0 } @@ -358,7 +359,7 @@ func getContainerServiceLabel(container dockerData, serviceName string, entry st } // Gets array of service names for a given container -func (provider *Docker) getServiceNames(container dockerData) []string { +func (p *Provider) getServiceNames(container dockerData) []string { labelServiceProperties := extractServicesLabels(container.Labels) keys := make([]string, 0, len(labelServiceProperties)) for k := range labelServiceProperties { @@ -368,73 +369,73 @@ func (provider *Docker) getServiceNames(container dockerData) []string { } // Extract entrypoints from labels for a given service and a given docker container -func (provider *Docker) getServiceEntryPoints(container dockerData, serviceName string) []string { +func (p *Provider) getServiceEntryPoints(container dockerData, serviceName string) []string { if entryPoints, ok := getContainerServiceLabel(container, serviceName, "frontend.entryPoints"); ok { return strings.Split(entryPoints, ",") } - return provider.getEntryPoints(container) + return p.getEntryPoints(container) } // Extract passHostHeader from labels for a given service and a given docker container -func (provider *Docker) getServicePassHostHeader(container dockerData, serviceName string) string { +func (p *Provider) getServicePassHostHeader(container dockerData, serviceName string) string { if servicePassHostHeader, ok := getContainerServiceLabel(container, serviceName, "frontend.passHostHeader"); ok { return servicePassHostHeader } - return provider.getPassHostHeader(container) + return p.getPassHostHeader(container) } // Extract priority from labels for a given service and a given docker container -func (provider *Docker) getServicePriority(container dockerData, serviceName string) string { +func (p *Provider) getServicePriority(container dockerData, serviceName string) string { if value, ok := getContainerServiceLabel(container, serviceName, "frontend.priority"); ok { return value } - return provider.getPriority(container) + return p.getPriority(container) } // Extract backend from labels for a given service and a given docker container -func (provider *Docker) getServiceBackend(container dockerData, serviceName string) string { +func (p *Provider) getServiceBackend(container dockerData, serviceName string) string { if value, ok := getContainerServiceLabel(container, serviceName, "frontend.backend"); ok { return value } - return provider.getBackend(container) + "-" + normalize(serviceName) + return p.getBackend(container) + "-" + provider.Normalize(serviceName) } // Extract rule from labels for a given service and a given docker container -func (provider *Docker) getServiceFrontendRule(container dockerData, serviceName string) string { +func (p *Provider) getServiceFrontendRule(container dockerData, serviceName string) string { if value, ok := getContainerServiceLabel(container, serviceName, "frontend.rule"); ok { return value } - return provider.getFrontendRule(container) + return p.getFrontendRule(container) } // Extract port from labels for a given service and a given docker container -func (provider *Docker) getServicePort(container dockerData, serviceName string) string { +func (p *Provider) getServicePort(container dockerData, serviceName string) string { if value, ok := getContainerServiceLabel(container, serviceName, "port"); ok { return value } - return provider.getPort(container) + return p.getPort(container) } // Extract weight from labels for a given service and a given docker container -func (provider *Docker) getServiceWeight(container dockerData, serviceName string) string { +func (p *Provider) getServiceWeight(container dockerData, serviceName string) string { if value, ok := getContainerServiceLabel(container, serviceName, "weight"); ok { return value } - return provider.getWeight(container) + return p.getWeight(container) } // Extract protocol from labels for a given service and a given docker container -func (provider *Docker) getServiceProtocol(container dockerData, serviceName string) string { +func (p *Provider) getServiceProtocol(container dockerData, serviceName string) string { if value, ok := getContainerServiceLabel(container, serviceName, "protocol"); ok { return value } - return provider.getProtocol(container) + return p.getProtocol(container) } -func (provider *Docker) hasLoadBalancerLabel(container dockerData) bool { +func (p *Provider) hasLoadBalancerLabel(container dockerData) bool { _, errMethod := getLabel(container, "traefik.backend.loadbalancer.method") _, errSticky := getLabel(container, "traefik.backend.loadbalancer.sticky") if errMethod != nil && errSticky != nil { @@ -443,7 +444,7 @@ func (provider *Docker) hasLoadBalancerLabel(container dockerData) bool { return true } -func (provider *Docker) hasMaxConnLabels(container dockerData) bool { +func (p *Provider) hasMaxConnLabels(container dockerData) bool { if _, err := getLabel(container, "traefik.backend.maxconn.amount"); err != nil { return false } @@ -453,21 +454,21 @@ func (provider *Docker) hasMaxConnLabels(container dockerData) bool { return true } -func (provider *Docker) getCircuitBreakerExpression(container dockerData) string { +func (p *Provider) 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 dockerData) string { +func (p *Provider) getLoadBalancerMethod(container dockerData) string { if label, err := getLabel(container, "traefik.backend.loadbalancer.method"); err == nil { return label } return "wrr" } -func (provider *Docker) getMaxConnAmount(container dockerData) int64 { +func (p *Provider) 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 { @@ -479,27 +480,27 @@ func (provider *Docker) getMaxConnAmount(container dockerData) int64 { return math.MaxInt64 } -func (provider *Docker) getMaxConnExtractorFunc(container dockerData) string { +func (p *Provider) 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 dockerData) bool { +func (p *Provider) 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 } - if !isContainerEnabled(container, provider.ExposedByDefault) { + if !isContainerEnabled(container, p.ExposedByDefault) { log.Debugf("Filtering disabled container %s", container.Name) return false } constraintTags := strings.Split(container.Labels["traefik.tags"], ",") - if ok, failingConstraint := provider.MatchConstraints(constraintTags); !ok { + if ok, failingConstraint := p.MatchConstraints(constraintTags); !ok { if failingConstraint != nil { log.Debugf("Container %v pruned by '%v' constraint", container.Name, failingConstraint.String()) } @@ -514,35 +515,35 @@ func (provider *Docker) containerFilter(container dockerData) bool { return true } -func (provider *Docker) getFrontendName(container dockerData) string { +func (p *Provider) 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)) + return provider.Normalize(p.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 dockerData) string { +func (p *Provider) getFrontendRule(container dockerData) string { if label, err := getLabel(container, "traefik.frontend.rule"); err == nil { return label } if labels, err := getLabels(container, []string{"com.docker.compose.project", "com.docker.compose.service"}); err == nil { - return "Host:" + provider.getSubDomain(labels["com.docker.compose.service"]+"."+labels["com.docker.compose.project"]) + "." + provider.Domain + return "Host:" + p.getSubDomain(labels["com.docker.compose.service"]+"."+labels["com.docker.compose.project"]) + "." + p.Domain } - return "Host:" + provider.getSubDomain(container.ServiceName) + "." + provider.Domain + return "Host:" + p.getSubDomain(container.ServiceName) + "." + p.Domain } -func (provider *Docker) getBackend(container dockerData) string { +func (p *Provider) getBackend(container dockerData) string { if label, err := getLabel(container, "traefik.backend"); err == nil { - return normalize(label) + return provider.Normalize(label) } if labels, err := getLabels(container, []string{"com.docker.compose.project", "com.docker.compose.service"}); err == nil { - return normalize(labels["com.docker.compose.service"] + "_" + labels["com.docker.compose.project"]) + return provider.Normalize(labels["com.docker.compose.service"] + "_" + labels["com.docker.compose.project"]) } - return normalize(container.ServiceName) + return provider.Normalize(container.ServiceName) } -func (provider *Docker) getIPAddress(container dockerData) string { +func (p *Provider) getIPAddress(container dockerData) string { if label, err := getLabel(container, "traefik.docker.network"); err == nil && label != "" { networkSettings := container.NetworkSettings if networkSettings.Networks != nil { @@ -561,8 +562,8 @@ func (provider *Docker) getIPAddress(container dockerData) string { return "127.0.0.1" } - if provider.UseBindPortIP { - port := provider.getPort(container) + if p.UseBindPortIP { + port := p.getPort(container) for netport, portBindings := range container.NetworkSettings.Ports { if string(netport) == port+"/TCP" || string(netport) == port+"/UDP" { for _, p := range portBindings { @@ -578,7 +579,7 @@ func (provider *Docker) getIPAddress(container dockerData) string { return "" } -func (provider *Docker) getPort(container dockerData) string { +func (p *Provider) getPort(container dockerData) string { if label, err := getLabel(container, "traefik.port"); err == nil { return label } @@ -588,56 +589,56 @@ func (provider *Docker) getPort(container dockerData) string { return "" } -func (provider *Docker) getWeight(container dockerData) string { +func (p *Provider) getWeight(container dockerData) string { if label, err := getLabel(container, "traefik.weight"); err == nil { return label } return "0" } -func (provider *Docker) getSticky(container dockerData) string { +func (p *Provider) getSticky(container dockerData) string { if label, err := getLabel(container, "traefik.backend.loadbalancer.sticky"); err == nil { return label } return "false" } -func (provider *Docker) getIsBackendLBSwarm(container dockerData) string { +func (p *Provider) getIsBackendLBSwarm(container dockerData) string { if label, err := getLabel(container, "traefik.backend.loadbalancer.swarm"); err == nil { return label } return "false" } -func (provider *Docker) getDomain(container dockerData) string { +func (p *Provider) getDomain(container dockerData) string { if label, err := getLabel(container, "traefik.domain"); err == nil { return label } - return provider.Domain + return p.Domain } -func (provider *Docker) getProtocol(container dockerData) string { +func (p *Provider) getProtocol(container dockerData) string { if label, err := getLabel(container, "traefik.protocol"); err == nil { return label } return "http" } -func (provider *Docker) getPassHostHeader(container dockerData) string { +func (p *Provider) getPassHostHeader(container dockerData) string { if passHostHeader, err := getLabel(container, "traefik.frontend.passHostHeader"); err == nil { return passHostHeader } return "true" } -func (provider *Docker) getPriority(container dockerData) string { +func (p *Provider) getPriority(container dockerData) string { if priority, err := getLabel(container, "traefik.frontend.priority"); err == nil { return priority } return "0" } -func (provider *Docker) getEntryPoints(container dockerData) []string { +func (p *Provider) getEntryPoints(container dockerData) []string { if entryPoints, err := getLabel(container, "traefik.frontend.entryPoints"); err == nil { return strings.Split(entryPoints, ",") } @@ -736,11 +737,11 @@ func parseContainer(container dockertypes.ContainerJSON) dockerData { } // Escape beginning slash "/", convert all others to dash "-", and convert underscores "_" to dash "-" -func (provider *Docker) getSubDomain(name string) string { +func (p *Provider) getSubDomain(name string) string { return strings.Replace(strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1), "_", "-", -1) } -func (provider *Docker) listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerData, error) { +func (p *Provider) listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerData, error) { serviceList, err := dockerClient.ServiceList(ctx, dockertypes.ServiceListOptions{}) if err != nil { return []dockerData{}, err @@ -765,7 +766,7 @@ func (provider *Docker) listServices(ctx context.Context, dockerClient client.AP for _, service := range serviceList { dockerData := parseService(service, networkMap) - useSwarmLB, _ := strconv.ParseBool(provider.getIsBackendLBSwarm(dockerData)) + useSwarmLB, _ := strconv.ParseBool(p.getIsBackendLBSwarm(dockerData)) isGlobalSvc := service.Spec.Mode.Global != nil if useSwarmLB { diff --git a/provider/docker/docker_test.go b/provider/docker/docker_test.go new file mode 100644 index 000000000..5c5960379 --- /dev/null +++ b/provider/docker/docker_test.go @@ -0,0 +1,824 @@ +package docker + +import ( + "reflect" + "strconv" + "strings" + "testing" + + "github.com/containous/traefik/types" + docker "github.com/docker/engine-api/types" + "github.com/docker/go-connections/nat" +) + +func TestDockerGetFrontendName(t *testing.T) { + provider := &Provider{ + Domain: "docker.localhost", + } + + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(name("foo")), + expected: "Host-foo-docker-localhost", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.frontend.rule": "Headers:User-Agent,bat/0.1.0", + })), + expected: "Headers-User-Agent-bat-0-1-0", + }, + { + container: containerJSON(labels(map[string]string{ + "com.docker.compose.project": "foo", + "com.docker.compose.service": "bar", + })), + expected: "Host-bar-foo-docker-localhost", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.frontend.rule": "Host:foo.bar", + })), + expected: "Host-foo-bar", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.frontend.rule": "Path:/test", + })), + expected: "Path-test", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.frontend.rule": "PathPrefix:/test2", + })), + expected: "PathPrefix-test2", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + actual := provider.getFrontendName(dockerData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestDockerGetFrontendRule(t *testing.T) { + provider := &Provider{ + Domain: "docker.localhost", + } + + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(name("foo")), + expected: "Host:foo.docker.localhost", + }, + { + container: containerJSON(name("bar")), + expected: "Host:bar.docker.localhost", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.frontend.rule": "Host:foo.bar", + })), + expected: "Host:foo.bar", + }, { + container: containerJSON(labels(map[string]string{ + "com.docker.compose.project": "foo", + "com.docker.compose.service": "bar", + })), + expected: "Host:bar.foo.docker.localhost", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.frontend.rule": "Path:/test", + })), + expected: "Path:/test", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + actual := provider.getFrontendRule(dockerData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestDockerGetBackend(t *testing.T) { + provider := &Provider{} + + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(name("foo")), + expected: "foo", + }, + { + container: containerJSON(name("bar")), + expected: "bar", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.backend": "foobar", + })), + expected: "foobar", + }, + { + container: containerJSON(labels(map[string]string{ + "com.docker.compose.project": "foo", + "com.docker.compose.service": "bar", + })), + expected: "bar-foo", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + actual := provider.getBackend(dockerData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestDockerGetIPAddress(t *testing.T) { + provider := &Provider{} + + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(withNetwork("testnet", ipv4("10.11.12.13"))), + expected: "10.11.12.13", + }, + { + container: containerJSON( + labels(map[string]string{ + "traefik.docker.network": "testnet", + }), + withNetwork("testnet", ipv4("10.11.12.13")), + ), + expected: "10.11.12.13", + }, + { + container: containerJSON( + labels(map[string]string{ + "traefik.docker.network": "testnet2", + }), + withNetwork("testnet", ipv4("10.11.12.13")), + withNetwork("testnet2", ipv4("10.11.12.14")), + ), + expected: "10.11.12.14", + }, + { + container: containerJSON( + networkMode("host"), + withNetwork("testnet", ipv4("10.11.12.13")), + withNetwork("testnet2", ipv4("10.11.12.14")), + ), + expected: "127.0.0.1", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + actual := provider.getIPAddress(dockerData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestDockerGetPort(t *testing.T) { + provider := &Provider{} + + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(name("foo")), + expected: "", + }, + { + container: containerJSON(ports(nat.PortMap{ + "80/tcp": {}, + })), + expected: "80", + }, + // FIXME handle this better.. + //{ + // container: containerJSON(ports(nat.PortMap{ + // "80/tcp": {}, + // "443/tcp": {}, + // })), + // expected: "80", + //}, + { + container: containerJSON(labels(map[string]string{ + "traefik.port": "8080", + })), + expected: "8080", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.port": "8080", + }), ports(nat.PortMap{ + "80/tcp": {}, + })), + expected: "8080", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.port": "8080", + }), ports(nat.PortMap{ + "8080/tcp": {}, + "80/tcp": {}, + })), + expected: "8080", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + actual := provider.getPort(dockerData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestDockerGetWeight(t *testing.T) { + provider := &Provider{} + + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(), + expected: "0", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.weight": "10", + })), + expected: "10", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + actual := provider.getWeight(dockerData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestDockerGetDomain(t *testing.T) { + provider := &Provider{ + Domain: "docker.localhost", + } + + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(), + expected: "docker.localhost", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.domain": "foo.bar", + })), + expected: "foo.bar", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + actual := provider.getDomain(dockerData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestDockerGetProtocol(t *testing.T) { + provider := &Provider{} + + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(), + expected: "http", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.protocol": "https", + })), + expected: "https", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + actual := provider.getProtocol(dockerData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestDockerGetPassHostHeader(t *testing.T) { + provider := &Provider{} + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(), + expected: "true", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.frontend.passHostHeader": "false", + })), + expected: "false", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + actual := provider.getPassHostHeader(dockerData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestDockerGetLabel(t *testing.T) { + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(), + expected: "Label not found:", + }, + { + container: containerJSON(labels(map[string]string{ + "foo": "bar", + })), + expected: "", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + label, err := getLabel(dockerData, "foo") + if e.expected != "" { + if err == nil || !strings.Contains(err.Error(), e.expected) { + t.Errorf("expected an error with %q, got %v", e.expected, err) + } + } else { + if label != "bar" { + t.Errorf("expected label 'bar', got %s", label) + } + } + }) + } +} + +func TestDockerGetLabels(t *testing.T) { + containers := []struct { + container docker.ContainerJSON + expectedLabels map[string]string + expectedError string + }{ + { + container: containerJSON(), + expectedLabels: map[string]string{}, + expectedError: "Label not found:", + }, + { + container: containerJSON(labels(map[string]string{ + "foo": "fooz", + })), + expectedLabels: map[string]string{ + "foo": "fooz", + }, + expectedError: "Label not found: bar", + }, + { + container: containerJSON(labels(map[string]string{ + "foo": "fooz", + "bar": "barz", + })), + expectedLabels: map[string]string{ + "foo": "fooz", + "bar": "barz", + }, + expectedError: "", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + labels, err := getLabels(dockerData, []string{"foo", "bar"}) + if !reflect.DeepEqual(labels, e.expectedLabels) { + t.Errorf("expect %v, got %v", e.expectedLabels, labels) + } + if e.expectedError != "" { + if err == nil || !strings.Contains(err.Error(), e.expectedError) { + t.Errorf("expected an error with %q, got %v", e.expectedError, err) + } + } + }) + } +} + +func TestDockerTraefikFilter(t *testing.T) { + provider := Provider{} + containers := []struct { + container docker.ContainerJSON + exposedByDefault bool + expected bool + }{ + { + container: containerJSON(), + exposedByDefault: true, + expected: false, + }, + { + container: containerJSON( + labels(map[string]string{ + "traefik.enable": "false", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + ), + exposedByDefault: true, + expected: false, + }, + { + container: containerJSON( + labels(map[string]string{ + "traefik.frontend.rule": "Host:foo.bar", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + ), + exposedByDefault: true, + expected: true, + }, + { + container: containerJSON( + ports(nat.PortMap{ + "80/tcp": {}, + "443/tcp": {}, + }), + ), + exposedByDefault: true, + expected: true, + }, + { + container: containerJSON( + ports(nat.PortMap{ + "80/tcp": {}, + }), + ), + exposedByDefault: true, + expected: true, + }, + { + container: containerJSON( + labels(map[string]string{ + "traefik.port": "80", + }), + ports(nat.PortMap{ + "80/tcp": {}, + "443/tcp": {}, + }), + ), + exposedByDefault: true, + expected: true, + }, + { + container: containerJSON( + labels(map[string]string{ + "traefik.enable": "true", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + ), + exposedByDefault: true, + expected: true, + }, + { + container: containerJSON( + labels(map[string]string{ + "traefik.enable": "anything", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + ), + exposedByDefault: true, + expected: true, + }, + { + container: containerJSON( + labels(map[string]string{ + "traefik.frontend.rule": "Host:foo.bar", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + ), + exposedByDefault: true, + expected: true, + }, + { + container: containerJSON( + ports(nat.PortMap{ + "80/tcp": {}, + }), + ), + exposedByDefault: false, + expected: false, + }, + { + container: containerJSON( + labels(map[string]string{ + "traefik.enable": "true", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + ), + exposedByDefault: false, + expected: true, + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + provider.ExposedByDefault = e.exposedByDefault + dockerData := parseContainer(e.container) + actual := provider.containerFilter(dockerData) + if actual != e.expected { + t.Errorf("expected %v for %+v (%+v, %+v), got %+v", e.expected, e.container, e.container.NetworkSettings, e.container.ContainerJSONBase, actual) + } + }) + } +} + +func TestDockerLoadDockerConfig(t *testing.T) { + cases := []struct { + containers []docker.ContainerJSON + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{ + { + containers: []docker.ContainerJSON{}, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + containers: []docker.ContainerJSON{ + containerJSON( + name("test"), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-test-docker-localhost": { + 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: 0, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + containers: []docker.ContainerJSON{ + containerJSON( + name("test1"), + labels(map[string]string{ + "traefik.backend": "foobar", + "traefik.frontend.entryPoints": "http,https", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + containerJSON( + name("test2"), + labels(map[string]string{ + "traefik.backend": "foobar", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.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: 0, + }, + "server-test2": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + containers: []docker.ContainerJSON{ + containerJSON( + name("test1"), + labels(map[string]string{ + "traefik.backend": "foobar", + "traefik.frontend.entryPoints": "http,https", + "traefik.backend.maxconn.amount": "1000", + "traefik.backend.maxconn.extractorfunc": "somethingelse", + "traefik.backend.loadbalancer.method": "drr", + "traefik.backend.circuitbreaker.expression": "NetworkErrorRatio() > 0.5", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-test1-docker-localhost": { + 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", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foobar": { + Servers: map[string]types.Server{ + "server-test1": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + CircuitBreaker: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", + }, + LoadBalancer: &types.LoadBalancer{ + Method: "drr", + }, + MaxConn: &types.MaxConn{ + Amount: 1000, + ExtractorFunc: "somethingelse", + }, + }, + }, + }, + } + + provider := &Provider{ + Domain: "docker.localhost", + ExposedByDefault: true, + } + + for caseID, c := range cases { + c := c + t.Run(strconv.Itoa(caseID), func(t *testing.T) { + t.Parallel() + 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.Errorf("expected %#v, got %#v", c.expectedBackends, actualConfig.Backends) + } + if !reflect.DeepEqual(actualConfig.Frontends, c.expectedFrontends) { + t.Errorf("expected %#v, got %#v", c.expectedFrontends, actualConfig.Frontends) + } + }) + } +} diff --git a/provider/docker/docker_unix.go b/provider/docker/docker_unix.go new file mode 100644 index 000000000..cfb97121b --- /dev/null +++ b/provider/docker/docker_unix.go @@ -0,0 +1,8 @@ +// +build !windows + +package docker + +const ( + // DockerAPIVersion is a constant holding the version of the Provider API traefik will use + DockerAPIVersion string = "1.21" +) diff --git a/provider/docker/docker_windows.go b/provider/docker/docker_windows.go new file mode 100644 index 000000000..ff0a873cb --- /dev/null +++ b/provider/docker/docker_windows.go @@ -0,0 +1,6 @@ +package docker + +const ( + // DockerAPIVersion is a constant holding the version of the Provider API traefik will use + DockerAPIVersion string = "1.24" +) diff --git a/provider/docker/service_test.go b/provider/docker/service_test.go new file mode 100644 index 000000000..2bbefb2d3 --- /dev/null +++ b/provider/docker/service_test.go @@ -0,0 +1,469 @@ +package docker + +import ( + "reflect" + "strconv" + "testing" + + "github.com/containous/traefik/types" + docker "github.com/docker/engine-api/types" + "github.com/docker/go-connections/nat" +) + +func TestDockerGetServiceProtocol(t *testing.T) { + provider := &Provider{} + + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(), + expected: "http", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.protocol": "https", + })), + expected: "https", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.myservice.protocol": "https", + })), + expected: "https", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + actual := provider.getServiceProtocol(dockerData, "myservice") + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestDockerGetServiceWeight(t *testing.T) { + provider := &Provider{} + + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(), + expected: "0", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.weight": "200", + })), + expected: "200", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.myservice.weight": "31337", + })), + expected: "31337", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + actual := provider.getServiceWeight(dockerData, "myservice") + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestDockerGetServicePort(t *testing.T) { + provider := &Provider{} + + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(), + expected: "", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.port": "2500", + })), + expected: "2500", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.myservice.port": "1234", + })), + expected: "1234", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + actual := provider.getServicePort(dockerData, "myservice") + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestDockerGetServiceFrontendRule(t *testing.T) { + provider := &Provider{} + + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(name("foo")), + expected: "Host:foo.", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.frontend.rule": "Path:/helloworld", + })), + expected: "Path:/helloworld", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.myservice.frontend.rule": "Path:/mycustomservicepath", + })), + expected: "Path:/mycustomservicepath", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + actual := provider.getServiceFrontendRule(dockerData, "myservice") + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestDockerGetServiceBackend(t *testing.T) { + provider := &Provider{} + + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(name("foo")), + expected: "foo-myservice", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.backend": "another-backend", + })), + expected: "another-backend-myservice", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.myservice.frontend.backend": "custom-backend", + })), + expected: "custom-backend", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + actual := provider.getServiceBackend(dockerData, "myservice") + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestDockerGetServicePriority(t *testing.T) { + provider := &Provider{} + + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(), + expected: "0", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.frontend.priority": "33", + })), + expected: "33", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.myservice.frontend.priority": "2503", + })), + expected: "2503", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + actual := provider.getServicePriority(dockerData, "myservice") + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestDockerGetServicePassHostHeader(t *testing.T) { + provider := &Provider{} + + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: containerJSON(), + expected: "true", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.frontend.passHostHeader": "false", + })), + expected: "false", + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.myservice.frontend.passHostHeader": "false", + })), + expected: "false", + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + actual := provider.getServicePassHostHeader(dockerData, "myservice") + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestDockerGetServiceEntryPoints(t *testing.T) { + provider := &Provider{} + + containers := []struct { + container docker.ContainerJSON + expected []string + }{ + { + container: containerJSON(), + expected: []string{}, + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.frontend.entryPoints": "http,https", + })), + expected: []string{"http", "https"}, + }, + { + container: containerJSON(labels(map[string]string{ + "traefik.myservice.frontend.entryPoints": "http,https", + })), + expected: []string{"http", "https"}, + }, + } + + for containerID, e := range containers { + e := e + t.Run(strconv.Itoa(containerID), func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(e.container) + actual := provider.getServiceEntryPoints(dockerData, "myservice") + if !reflect.DeepEqual(actual, e.expected) { + t.Fatalf("expected %q, got %q for container %q", e.expected, actual, dockerData.Name) + } + }) + } +} + +func TestDockerLoadDockerServiceConfig(t *testing.T) { + cases := []struct { + containers []docker.ContainerJSON + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{ + { + containers: []docker.ContainerJSON{}, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + containers: []docker.ContainerJSON{ + containerJSON( + name("foo"), + labels(map[string]string{ + "traefik.service.port": "2503", + "traefik.service.frontend.entryPoints": "http,https", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-foo-service": { + Backend: "backend-foo-service", + PassHostHeader: true, + EntryPoints: []string{"http", "https"}, + Routes: map[string]types.Route{ + "service-service": { + Rule: "Host:foo.docker.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foo-service": { + Servers: map[string]types.Server{ + "service": { + URL: "http://127.0.0.1:2503", + Weight: 0, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + containers: []docker.ContainerJSON{ + containerJSON( + name("test1"), + labels(map[string]string{ + "traefik.service.port": "2503", + "traefik.service.protocol": "https", + "traefik.service.weight": "80", + "traefik.service.frontend.backend": "foobar", + "traefik.service.frontend.passHostHeader": "false", + "traefik.service.frontend.rule": "Path:/mypath", + "traefik.service.frontend.priority": "5000", + "traefik.service.frontend.entryPoints": "http,https,ws", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + containerJSON( + name("test2"), + labels(map[string]string{ + "traefik.anotherservice.port": "8079", + "traefik.anotherservice.weight": "33", + "traefik.anotherservice.frontend.rule": "Path:/anotherpath", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-foobar": { + Backend: "backend-foobar", + PassHostHeader: false, + Priority: 5000, + EntryPoints: []string{"http", "https", "ws"}, + Routes: map[string]types.Route{ + "service-service": { + Rule: "Path:/mypath", + }, + }, + }, + "frontend-test2-anotherservice": { + Backend: "backend-test2-anotherservice", + PassHostHeader: true, + EntryPoints: []string{}, + Routes: map[string]types.Route{ + "service-anotherservice": { + Rule: "Path:/anotherpath", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foobar": { + Servers: map[string]types.Server{ + "service": { + URL: "https://127.0.0.1:2503", + Weight: 80, + }, + }, + CircuitBreaker: nil, + }, + "backend-test2-anotherservice": { + Servers: map[string]types.Server{ + "service": { + URL: "http://127.0.0.1:8079", + Weight: 33, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + } + + provider := &Provider{ + Domain: "docker.localhost", + ExposedByDefault: true, + } + + for caseID, c := range cases { + c := c + t.Run(strconv.Itoa(caseID), func(t *testing.T) { + t.Parallel() + 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) + } + }) + } +} diff --git a/provider/docker/swarm_test.go b/provider/docker/swarm_test.go new file mode 100644 index 000000000..c5763f45f --- /dev/null +++ b/provider/docker/swarm_test.go @@ -0,0 +1,888 @@ +package docker + +import ( + "reflect" + "strconv" + "strings" + "testing" + + "github.com/containous/traefik/types" + "github.com/davecgh/go-spew/spew" + dockerclient "github.com/docker/engine-api/client" + docker "github.com/docker/engine-api/types" + dockertypes "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/swarm" + "golang.org/x/net/context" +) + +func TestSwarmGetFrontendName(t *testing.T) { + provider := &Provider{ + Domain: "docker.localhost", + SwarmMode: true, + } + + services := []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(serviceLabels(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: swarmService(serviceLabels(map[string]string{ + "traefik.frontend.rule": "Host:foo.bar", + })), + expected: "Host-foo-bar", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + "traefik.frontend.rule": "Path:/test", + })), + expected: "Path-test", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService( + serviceName("test"), + serviceLabels(map[string]string{ + "traefik.frontend.rule": "PathPrefix:/test2", + }), + ), + expected: "PathPrefix-test2", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, e := range services { + e := e + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dockerData := parseService(e.service, e.networks) + actual := provider.getFrontendName(dockerData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestSwarmGetFrontendRule(t *testing.T) { + provider := &Provider{ + Domain: "docker.localhost", + SwarmMode: true, + } + + services := []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{ + "traefik.frontend.rule": "Host:foo.bar", + })), + expected: "Host:foo.bar", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + "traefik.frontend.rule": "Path:/test", + })), + expected: "Path:/test", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, e := range services { + e := e + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dockerData := parseService(e.service, e.networks) + actual := provider.getFrontendRule(dockerData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestSwarmGetBackend(t *testing.T) { + provider := &Provider{ + SwarmMode: true, + } + + services := []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{ + "traefik.backend": "foobar", + })), + expected: "foobar", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, e := range services { + e := e + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dockerData := parseService(e.service, e.networks) + actual := provider.getBackend(dockerData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestSwarmGetIPAddress(t *testing.T) { + provider := &Provider{ + SwarmMode: true, + } + + services := []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{ + "traefik.docker.network": "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, e := range services { + e := e + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dockerData := parseService(e.service, e.networks) + actual := provider.getIPAddress(dockerData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestSwarmGetPort(t *testing.T) { + provider := &Provider{ + SwarmMode: true, + } + + services := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService( + serviceLabels(map[string]string{ + "traefik.port": "8080", + }), + withEndpointSpec(modeDNSSR), + ), + expected: "8080", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, e := range services { + e := e + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dockerData := parseService(e.service, e.networks) + actual := provider.getPort(dockerData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestSwarmGetWeight(t *testing.T) { + provider := &Provider{ + SwarmMode: true, + } + + services := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(), + expected: "0", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + "traefik.weight": "10", + })), + expected: "10", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, e := range services { + e := e + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dockerData := parseService(e.service, e.networks) + actual := provider.getWeight(dockerData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestSwarmGetDomain(t *testing.T) { + provider := &Provider{ + Domain: "docker.localhost", + SwarmMode: true, + } + + services := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(serviceName("foo")), + expected: "docker.localhost", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + "traefik.domain": "foo.bar", + })), + expected: "foo.bar", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, e := range services { + e := e + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dockerData := parseService(e.service, e.networks) + actual := provider.getDomain(dockerData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestSwarmGetProtocol(t *testing.T) { + provider := &Provider{ + SwarmMode: true, + } + + services := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(), + expected: "http", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + "traefik.protocol": "https", + })), + expected: "https", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, e := range services { + e := e + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dockerData := parseService(e.service, e.networks) + actual := provider.getProtocol(dockerData) + if actual != e.expected { + t.Errorf("expected %q, got %q", e.expected, actual) + } + }) + } +} + +func TestSwarmGetPassHostHeader(t *testing.T) { + provider := &Provider{ + SwarmMode: true, + } + + services := []struct { + service swarm.Service + expected string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(), + expected: "true", + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + "traefik.frontend.passHostHeader": "false", + })), + expected: "false", + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, e := range services { + e := e + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dockerData := parseService(e.service, e.networks) + actual := provider.getPassHostHeader(dockerData) + if actual != e.expected { + t.Errorf("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: 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, e := range services { + e := e + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dockerData := parseService(e.service, e.networks) + label, err := getLabel(dockerData, "foo") + if e.expected != "" { + if err == nil || !strings.Contains(err.Error(), e.expected) { + t.Errorf("expected an error with %q, got %v", e.expected, err) + } + } else { + if label != "bar" { + t.Errorf("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: 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, e := range services { + e := e + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dockerData := parseService(e.service, e.networks) + labels, err := getLabels(dockerData, []string{"foo", "bar"}) + if !reflect.DeepEqual(labels, e.expectedLabels) { + t.Errorf("expect %v, got %v", e.expectedLabels, labels) + } + if e.expectedError != "" { + if err == nil || !strings.Contains(err.Error(), e.expectedError) { + t.Errorf("expected an error with %q, got %v", e.expectedError, err) + } + } + }) + } +} + +func TestSwarmTraefikFilter(t *testing.T) { + provider := &Provider{ + SwarmMode: true, + } + services := []struct { + service swarm.Service + exposedByDefault bool + expected bool + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(), + exposedByDefault: true, + expected: false, + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + "traefik.enable": "false", + "traefik.port": "80", + })), + exposedByDefault: true, + expected: false, + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + "traefik.frontend.rule": "Host:foo.bar", + "traefik.port": "80", + })), + exposedByDefault: true, + expected: true, + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + "traefik.port": "80", + })), + exposedByDefault: true, + expected: true, + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + "traefik.enable": "true", + "traefik.port": "80", + })), + exposedByDefault: true, + expected: true, + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + "traefik.enable": "anything", + "traefik.port": "80", + })), + exposedByDefault: true, + expected: true, + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + "traefik.frontend.rule": "Host:foo.bar", + "traefik.port": "80", + })), + exposedByDefault: true, + expected: true, + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + "traefik.port": "80", + })), + exposedByDefault: false, + expected: false, + networks: map[string]*docker.NetworkResource{}, + }, + { + service: swarmService(serviceLabels(map[string]string{ + "traefik.enable": "true", + "traefik.port": "80", + })), + exposedByDefault: false, + expected: true, + networks: map[string]*docker.NetworkResource{}, + }, + } + + for serviceID, e := range services { + e := e + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + dockerData := parseService(e.service, e.networks) + provider.ExposedByDefault = e.exposedByDefault + actual := provider.containerFilter(dockerData) + if actual != e.expected { + t.Errorf("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{ + swarmService( + serviceName("test"), + serviceLabels(map[string]string{ + "traefik.port": "80", + }), + withEndpointSpec(modeVIP), + withEndpoint(virtualIP("1", "127.0.0.1/24")), + ), + }, + 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: 0, + }, + }, + CircuitBreaker: nil, + LoadBalancer: nil, + }, + }, + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foo", + }, + }, + }, + { + services: []swarm.Service{ + swarmService( + serviceName("test1"), + serviceLabels(map[string]string{ + "traefik.port": "80", + "traefik.backend": "foobar", + "traefik.frontend.entryPoints": "http,https", + }), + withEndpointSpec(modeVIP), + withEndpoint(virtualIP("1", "127.0.0.1/24")), + ), + swarmService( + serviceName("test2"), + serviceLabels(map[string]string{ + "traefik.port": "80", + "traefik.backend": "foobar", + }), + withEndpointSpec(modeVIP), + withEndpoint(virtualIP("1", "127.0.0.1/24")), + ), + }, + 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: 0, + }, + "server-test2": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + CircuitBreaker: nil, + LoadBalancer: nil, + }, + }, + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foo", + }, + }, + }, + } + + provider := &Provider{ + Domain: "docker.localhost", + ExposedByDefault: true, + SwarmMode: true, + } + + for caseID, c := range cases { + c := c + t.Run(strconv.Itoa(caseID), func(t *testing.T) { + t.Parallel() + 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.Errorf("expected %#v, got %#v", c.expectedBackends, actualConfig.Backends) + } + if !reflect.DeepEqual(actualConfig.Frontends, c.expectedFrontends) { + t.Errorf("expected %#v, got %#v", c.expectedFrontends, actualConfig.Frontends) + } + }) + } +} + +func TestSwarmTaskParsing(t *testing.T) { + cases := []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, e := range cases { + e := e + t.Run(strconv.Itoa(caseID), func(t *testing.T) { + t.Parallel() + dockerData := parseService(e.service, e.networks) + + for _, task := range e.tasks { + taskDockerData := parseTasks(task, dockerData, map[string]*docker.NetworkResource{}, e.isGlobalSVC) + if !reflect.DeepEqual(taskDockerData.Name, e.expectedNames[task.ID]) { + t.Errorf("expect %v, got %v", e.expectedNames[task.ID], taskDockerData.Name) + } + } + }) + } +} + +type fakeTasksClient struct { + dockerclient.APIClient + tasks []swarm.Task + err error +} + +func (c *fakeTasksClient) TaskList(ctx context.Context, options dockertypes.TaskListOptions) ([]swarm.Task, error) { + return c.tasks, c.err +} + +func TestListTasks(t *testing.T) { + cases := []struct { + service swarm.Service + tasks []swarm.Task + isGlobalSVC bool + expectedTasks []string + networks map[string]*docker.NetworkResource + }{ + { + service: swarmService(serviceName("container")), + tasks: []swarm.Task{ + swarmTask("id1", taskSlot(1), taskStatus(taskState(swarm.TaskStateRunning))), + swarmTask("id2", taskSlot(2), taskStatus(taskState(swarm.TaskStatePending))), + swarmTask("id3", taskSlot(3)), + swarmTask("id4", taskSlot(4), taskStatus(taskState(swarm.TaskStateRunning))), + swarmTask("id5", taskSlot(5), taskStatus(taskState(swarm.TaskStateFailed))), + }, + isGlobalSVC: false, + expectedTasks: []string{ + "container.1", + "container.4", + }, + networks: map[string]*docker.NetworkResource{ + "1": { + Name: "foo", + }, + }, + }, + } + + for caseID, e := range cases { + e := e + t.Run(strconv.Itoa(caseID), func(t *testing.T) { + t.Parallel() + dockerData := parseService(e.service, e.networks) + dockerClient := &fakeTasksClient{tasks: e.tasks} + taskDockerData, _ := listTasks(context.Background(), dockerClient, e.service.ID, dockerData, map[string]*docker.NetworkResource{}, e.isGlobalSVC) + + if len(e.expectedTasks) != len(taskDockerData) { + t.Errorf("expected tasks %v, got %v", spew.Sdump(e.expectedTasks), spew.Sdump(taskDockerData)) + } + + for i, taskID := range e.expectedTasks { + if taskDockerData[i].Name != taskID { + t.Errorf("expect task id %v, got %v", taskID, taskDockerData[i].Name) + } + } + }) + } +} diff --git a/provider/docker_test.go b/provider/docker_test.go deleted file mode 100644 index a855f4007..000000000 --- a/provider/docker_test.go +++ /dev/null @@ -1,3130 +0,0 @@ -package provider - -import ( - "reflect" - "strings" - "testing" - - "github.com/containous/traefik/types" - "github.com/davecgh/go-spew/spew" - dockerclient "github.com/docker/engine-api/client" - docker "github.com/docker/engine-api/types" - dockertypes "github.com/docker/engine-api/types" - "github.com/docker/engine-api/types/container" - "github.com/docker/engine-api/types/network" - "github.com/docker/engine-api/types/swarm" - "github.com/docker/go-connections/nat" - "golang.org/x/net/context" - "strconv" -) - -func TestDockerGetFrontendName(t *testing.T) { - provider := &Docker{ - Domain: "docker.localhost", - } - - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - }, - expected: "Host-foo-docker-localhost", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "bar", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.frontend.rule": "Headers:User-Agent,bat/0.1.0", - }, - }, - }, - expected: "Headers-User-Agent-bat-0-1-0", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "mycontainer", - }, - Config: &container.Config{ - Labels: map[string]string{ - "com.docker.compose.project": "foo", - "com.docker.compose.service": "bar", - }, - }, - }, - expected: "Host-bar-foo-docker-localhost", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.frontend.rule": "Host:foo.bar", - }, - }, - }, - expected: "Host-foo-bar", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.frontend.rule": "Path:/test", - }, - }, - }, - expected: "Path-test", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.frontend.rule": "PathPrefix:/test2", - }, - }, - }, - expected: "PathPrefix-test2", - }, - } - - for containerID, e := range containers { - e := e - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(e.container) - actual := provider.getFrontendName(dockerData) - if actual != e.expected { - t.Errorf("expected %q, got %q", e.expected, actual) - } - }) - } -} - -func TestDockerGetFrontendRule(t *testing.T) { - provider := &Docker{ - Domain: "docker.localhost", - } - - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - }, - expected: "Host:foo.docker.localhost", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "bar", - }, - Config: &container.Config{}, - }, - expected: "Host:bar.docker.localhost", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.frontend.rule": "Host:foo.bar", - }, - }, - }, - expected: "Host:foo.bar", - }, { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "com.docker.compose.project": "foo", - "com.docker.compose.service": "bar", - }, - }, - }, - expected: "Host:bar.foo.docker.localhost", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.frontend.rule": "Path:/test", - }, - }, - }, - expected: "Path:/test", - }, - } - - for containerID, e := range containers { - e := e - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(e.container) - actual := provider.getFrontendRule(dockerData) - if actual != e.expected { - t.Errorf("expected %q, got %q", e.expected, actual) - } - }) - } -} - -func TestDockerGetBackend(t *testing.T) { - provider := &Docker{} - - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - }, - expected: "foo", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "bar", - }, - Config: &container.Config{}, - }, - expected: "bar", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.backend": "foobar", - }, - }, - }, - expected: "foobar", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "com.docker.compose.project": "foo", - "com.docker.compose.service": "bar", - }, - }, - }, - expected: "bar-foo", - }, - } - - for containerID, e := range containers { - e := e - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(e.container) - actual := provider.getBackend(dockerData) - if actual != e.expected { - t.Errorf("expected %q, got %q", e.expected, actual) - } - }) - } -} - -func TestDockerGetIPAddress(t *testing.T) { // TODO - provider := &Docker{} - - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "bar", - }, - Config: &container.Config{}, - NetworkSettings: &docker.NetworkSettings{ - Networks: map[string]*network.EndpointSettings{ - "testnet": { - IPAddress: "10.11.12.13", - }, - }, - }, - }, - expected: "10.11.12.13", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "bar", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.docker.network": "testnet", - }, - }, - NetworkSettings: &docker.NetworkSettings{ - Networks: map[string]*network.EndpointSettings{ - "nottestnet": { - IPAddress: "10.11.12.13", - }, - }, - }, - }, - expected: "10.11.12.13", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "bar", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.docker.network": "testnet2", - }, - }, - NetworkSettings: &docker.NetworkSettings{ - Networks: map[string]*network.EndpointSettings{ - "testnet1": { - IPAddress: "10.11.12.13", - }, - "testnet2": { - IPAddress: "10.11.12.14", - }, - }, - }, - }, - expected: "10.11.12.14", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "bar", - HostConfig: &container.HostConfig{ - NetworkMode: "host", - }, - }, - Config: &container.Config{ - Labels: map[string]string{}, - }, - NetworkSettings: &docker.NetworkSettings{ - Networks: map[string]*network.EndpointSettings{ - "testnet1": { - IPAddress: "10.11.12.13", - }, - "testnet2": { - IPAddress: "10.11.12.14", - }, - }, - }, - }, - expected: "127.0.0.1", - }, - } - - for containerID, e := range containers { - e := e - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(e.container) - actual := provider.getIPAddress(dockerData) - if actual != e.expected { - t.Errorf("expected %q, got %q", e.expected, actual) - } - }) - } -} - -func TestDockerGetPort(t *testing.T) { - provider := &Docker{} - - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - NetworkSettings: &docker.NetworkSettings{}, - }, - expected: "", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "bar", - }, - Config: &container.Config{}, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - }, - }, - }, - }, - expected: "80", - }, - // FIXME handle this better.. - // { - // container: docker.ContainerJSON{ - // Name: "bar", - // Config: &container.Config{}, - // NetworkSettings: &docker.NetworkSettings{ - // Ports: map[docker.Port][]docker.PortBinding{ - // "80/tcp": []docker.PortBinding{}, - // "443/tcp": []docker.PortBinding{}, - // }, - // }, - // }, - // expected: "80", - // }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.port": "8080", - }, - }, - NetworkSettings: &docker.NetworkSettings{}, - }, - expected: "8080", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.port": "8080", - }, - }, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - }, - }, - }, - }, - expected: "8080", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test-multi-ports", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.port": "8080", - }, - }, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "8080/tcp": {}, - "80/tcp": {}, - }, - }, - }, - }, - expected: "8080", - }, - } - - for containerID, e := range containers { - e := e - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(e.container) - actual := provider.getPort(dockerData) - if actual != e.expected { - t.Errorf("expected %q, got %q", e.expected, actual) - } - }) - } -} - -func TestDockerGetWeight(t *testing.T) { - provider := &Docker{} - - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - }, - expected: "0", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.weight": "10", - }, - }, - }, - expected: "10", - }, - } - - for containerID, e := range containers { - e := e - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(e.container) - actual := provider.getWeight(dockerData) - if actual != e.expected { - t.Errorf("expected %q, got %q", e.expected, actual) - } - }) - } -} - -func TestDockerGetDomain(t *testing.T) { - provider := &Docker{ - Domain: "docker.localhost", - } - - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - }, - expected: "docker.localhost", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.domain": "foo.bar", - }, - }, - }, - expected: "foo.bar", - }, - } - - for containerID, e := range containers { - e := e - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(e.container) - actual := provider.getDomain(dockerData) - if actual != e.expected { - t.Errorf("expected %q, got %q", e.expected, actual) - } - }) - } -} - -func TestDockerGetProtocol(t *testing.T) { - provider := &Docker{} - - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - }, - expected: "http", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.protocol": "https", - }, - }, - }, - expected: "https", - }, - } - - for containerID, e := range containers { - e := e - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(e.container) - actual := provider.getProtocol(dockerData) - if actual != e.expected { - t.Errorf("expected %q, got %q", e.expected, actual) - } - }) - } -} - -func TestDockerGetPassHostHeader(t *testing.T) { - provider := &Docker{} - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - }, - expected: "true", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.frontend.passHostHeader": "false", - }, - }, - }, - expected: "false", - }, - } - - for containerID, e := range containers { - e := e - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(e.container) - actual := provider.getPassHostHeader(dockerData) - if actual != e.expected { - t.Errorf("expected %q, got %q", e.expected, actual) - } - }) - } -} - -func TestDockerGetLabel(t *testing.T) { - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - }, - expected: "Label not found:", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{ - Labels: map[string]string{ - "foo": "bar", - }, - }, - }, - expected: "", - }, - } - - for containerID, e := range containers { - e := e - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(e.container) - label, err := getLabel(dockerData, "foo") - if e.expected != "" { - if err == nil || !strings.Contains(err.Error(), e.expected) { - t.Errorf("expected an error with %q, got %v", e.expected, err) - } - } else { - if label != "bar" { - t.Errorf("expected label 'bar', got %s", label) - } - } - }) - } -} - -func TestDockerGetLabels(t *testing.T) { - containers := []struct { - container docker.ContainerJSON - expectedLabels map[string]string - expectedError string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - }, - expectedLabels: map[string]string{}, - expectedError: "Label not found:", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{ - Labels: map[string]string{ - "foo": "fooz", - }, - }, - }, - expectedLabels: map[string]string{ - "foo": "fooz", - }, - expectedError: "Label not found: bar", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{ - Labels: map[string]string{ - "foo": "fooz", - "bar": "barz", - }, - }, - }, - expectedLabels: map[string]string{ - "foo": "fooz", - "bar": "barz", - }, - expectedError: "", - }, - } - - for containerID, e := range containers { - e := e - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - dockerData := parseContainer(e.container) - labels, err := getLabels(dockerData, []string{"foo", "bar"}) - if !reflect.DeepEqual(labels, e.expectedLabels) { - t.Errorf("expect %v, got %v", e.expectedLabels, labels) - } - if e.expectedError != "" { - if err == nil || !strings.Contains(err.Error(), e.expectedError) { - t.Errorf("expected an error with %q, got %v", e.expectedError, err) - } - } - }) - } -} - -func TestDockerTraefikFilter(t *testing.T) { - provider := Docker{} - containers := []struct { - container docker.ContainerJSON - exposedByDefault bool - expected bool - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "container", - }, - Config: &container.Config{}, - NetworkSettings: &docker.NetworkSettings{}, - }, - exposedByDefault: true, - expected: false, - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "container", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.enable": "false", - }, - }, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - }, - }, - }, - }, - exposedByDefault: true, - expected: false, - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "container", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.frontend.rule": "Host:foo.bar", - }, - }, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - }, - }, - }, - }, - exposedByDefault: true, - expected: true, - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "container-multi-ports", - }, - Config: &container.Config{}, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - "443/tcp": {}, - }, - }, - }, - }, - exposedByDefault: true, - expected: true, - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "container", - }, - Config: &container.Config{}, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - }, - }, - }, - }, - exposedByDefault: true, - expected: true, - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "container", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.port": "80", - }, - }, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - "443/tcp": {}, - }, - }, - }, - }, - exposedByDefault: true, - expected: true, - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "container", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.enable": "true", - }, - }, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - }, - }, - }, - }, - exposedByDefault: true, - expected: true, - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "container", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.enable": "anything", - }, - }, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - }, - }, - }, - }, - exposedByDefault: true, - expected: true, - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "container", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.frontend.rule": "Host:foo.bar", - }, - }, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - }, - }, - }, - }, - exposedByDefault: true, - expected: true, - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "container", - }, - Config: &container.Config{}, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - }, - }, - }, - }, - exposedByDefault: false, - expected: false, - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "container", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.enable": "true", - }, - }, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - }, - }, - }, - }, - exposedByDefault: false, - expected: true, - }, - } - - for containerID, e := range containers { - e := e - t.Run(strconv.Itoa(containerID), func(t *testing.T) { - t.Parallel() - provider.ExposedByDefault = e.exposedByDefault - dockerData := parseContainer(e.container) - actual := provider.containerFilter(dockerData) - if actual != e.expected { - t.Errorf("expected %v for %+v, got %+v", e.expected, e, actual) - } - }) - } -} - -func TestDockerLoadDockerConfig(t *testing.T) { - cases := []struct { - containers []docker.ContainerJSON - expectedFrontends map[string]*types.Frontend - expectedBackends map[string]*types.Backend - }{ - { - containers: []docker.ContainerJSON{}, - expectedFrontends: map[string]*types.Frontend{}, - expectedBackends: map[string]*types.Backend{}, - }, - { - containers: []docker.ContainerJSON{ - { - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{}, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - }, - }, - Networks: map[string]*network.EndpointSettings{ - "bridge": { - IPAddress: "127.0.0.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: 0, - }, - }, - CircuitBreaker: nil, - }, - }, - }, - { - containers: []docker.ContainerJSON{ - { - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test1", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.backend": "foobar", - "traefik.frontend.entryPoints": "http,https", - }, - }, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - }, - }, - Networks: map[string]*network.EndpointSettings{ - "bridge": { - IPAddress: "127.0.0.1", - }, - }, - }, - }, - { - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test2", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.backend": "foobar", - }, - }, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - }, - }, - Networks: map[string]*network.EndpointSettings{ - "bridge": { - IPAddress: "127.0.0.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: 0, - }, - "server-test2": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - }, - CircuitBreaker: nil, - }, - }, - }, - { - containers: []docker.ContainerJSON{ - { - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test1", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.backend": "foobar", - "traefik.frontend.entryPoints": "http,https", - "traefik.backend.maxconn.amount": "1000", - "traefik.backend.maxconn.extractorfunc": "somethingelse", - "traefik.backend.loadbalancer.method": "drr", - "traefik.backend.circuitbreaker.expression": "NetworkErrorRatio() > 0.5", - }, - }, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - }, - }, - Networks: map[string]*network.EndpointSettings{ - "bridge": { - IPAddress: "127.0.0.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", - }, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-foobar": { - Servers: map[string]types.Server{ - "server-test1": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - }, - CircuitBreaker: &types.CircuitBreaker{ - Expression: "NetworkErrorRatio() > 0.5", - }, - LoadBalancer: &types.LoadBalancer{ - Method: "drr", - }, - MaxConn: &types.MaxConn{ - Amount: 1000, - ExtractorFunc: "somethingelse", - }, - }, - }, - }, - } - - provider := &Docker{ - Domain: "docker.localhost", - ExposedByDefault: true, - } - - for caseID, c := range cases { - c := c - t.Run(strconv.Itoa(caseID), func(t *testing.T) { - t.Parallel() - 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.Errorf("expected %#v, got %#v", c.expectedBackends, actualConfig.Backends) - } - if !reflect.DeepEqual(actualConfig.Frontends, c.expectedFrontends) { - t.Errorf("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 serviceID, e := range services { - e := e - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(e.service, e.networks) - actual := provider.getFrontendName(dockerData) - if actual != e.expected { - t.Errorf("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 serviceID, e := range services { - e := e - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(e.service, e.networks) - actual := provider.getFrontendRule(dockerData) - if actual != e.expected { - t.Errorf("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 serviceID, e := range services { - e := e - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(e.service, e.networks) - actual := provider.getBackend(dockerData) - if actual != e.expected { - t.Errorf("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 serviceID, e := range services { - e := e - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(e.service, e.networks) - actual := provider.getIPAddress(dockerData) - if actual != e.expected { - t.Errorf("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 serviceID, e := range services { - e := e - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(e.service, e.networks) - actual := provider.getPort(dockerData) - if actual != e.expected { - t.Errorf("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: "0", - 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 serviceID, e := range services { - e := e - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(e.service, e.networks) - actual := provider.getWeight(dockerData) - if actual != e.expected { - t.Errorf("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 serviceID, e := range services { - e := e - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(e.service, e.networks) - actual := provider.getDomain(dockerData) - if actual != e.expected { - t.Errorf("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 serviceID, e := range services { - e := e - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(e.service, e.networks) - actual := provider.getProtocol(dockerData) - if actual != e.expected { - t.Errorf("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 serviceID, e := range services { - e := e - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(e.service, e.networks) - actual := provider.getPassHostHeader(dockerData) - if actual != e.expected { - t.Errorf("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 serviceID, e := range services { - e := e - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(e.service, e.networks) - label, err := getLabel(dockerData, "foo") - if e.expected != "" { - if err == nil || !strings.Contains(err.Error(), e.expected) { - t.Errorf("expected an error with %q, got %v", e.expected, err) - } - } else { - if label != "bar" { - t.Errorf("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 serviceID, e := range services { - e := e - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(e.service, e.networks) - labels, err := getLabels(dockerData, []string{"foo", "bar"}) - if !reflect.DeepEqual(labels, e.expectedLabels) { - t.Errorf("expect %v, got %v", e.expectedLabels, labels) - } - if e.expectedError != "" { - if err == nil || !strings.Contains(err.Error(), e.expectedError) { - t.Errorf("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 serviceID, e := range services { - e := e - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(e.service, e.networks) - provider.ExposedByDefault = e.exposedByDefault - actual := provider.containerFilter(dockerData) - if actual != e.expected { - t.Errorf("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: 0, - }, - }, - 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: 0, - }, - "server-test2": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - }, - CircuitBreaker: nil, - LoadBalancer: nil, - }, - }, - networks: map[string]*docker.NetworkResource{ - "1": { - Name: "foo", - }, - }, - }, - } - - provider := &Docker{ - Domain: "docker.localhost", - ExposedByDefault: true, - SwarmMode: true, - } - - for caseID, c := range cases { - c := c - t.Run(strconv.Itoa(caseID), func(t *testing.T) { - t.Parallel() - 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.Errorf("expected %#v, got %#v", c.expectedBackends, actualConfig.Backends) - } - if !reflect.DeepEqual(actualConfig.Frontends, c.expectedFrontends) { - t.Errorf("expected %#v, got %#v", c.expectedFrontends, actualConfig.Frontends) - } - }) - } -} - -func TestSwarmTaskParsing(t *testing.T) { - cases := []struct { - service swarm.Service - tasks []swarm.Task - isGlobalSVC bool - expectedNames map[string]string - networks map[string]*docker.NetworkResource - }{ - { - service: swarm.Service{ - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: "container", - }, - }, - }, - tasks: []swarm.Task{ - { - ID: "id1", - Slot: 1, - }, - { - ID: "id2", - Slot: 2, - }, - { - ID: "id3", - Slot: 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: swarm.Service{ - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: "container", - }, - }, - }, - tasks: []swarm.Task{ - { - ID: "id1", - }, - { - ID: "id2", - }, - { - ID: "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, e := range cases { - e := e - t.Run(strconv.Itoa(caseID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(e.service, e.networks) - - for _, task := range e.tasks { - taskDockerData := parseTasks(task, dockerData, map[string]*docker.NetworkResource{}, e.isGlobalSVC) - if !reflect.DeepEqual(taskDockerData.Name, e.expectedNames[task.ID]) { - t.Errorf("expect %v, got %v", e.expectedNames[task.ID], taskDockerData.Name) - } - } - }) - } -} - -type fakeTasksClient struct { - dockerclient.APIClient - tasks []swarm.Task - err error -} - -func (c *fakeTasksClient) TaskList(ctx context.Context, options dockertypes.TaskListOptions) ([]swarm.Task, error) { - return c.tasks, c.err -} - -func TestListTasks(t *testing.T) { - cases := []struct { - service swarm.Service - tasks []swarm.Task - isGlobalSVC bool - expectedTasks []string - networks map[string]*docker.NetworkResource - }{ - { - service: swarm.Service{ - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: "container", - }, - }, - }, - tasks: []swarm.Task{ - { - ID: "id1", - Slot: 1, - Status: swarm.TaskStatus{ - State: swarm.TaskStateRunning, - }, - }, - { - ID: "id2", - Slot: 2, - Status: swarm.TaskStatus{ - State: swarm.TaskStatePending, - }, - }, - { - ID: "id3", - Slot: 3, - }, - { - ID: "id4", - Slot: 4, - Status: swarm.TaskStatus{ - State: swarm.TaskStateRunning, - }, - }, - { - ID: "id5", - Slot: 5, - Status: swarm.TaskStatus{ - State: swarm.TaskStateFailed, - }, - }, - }, - isGlobalSVC: false, - expectedTasks: []string{ - "container.1", - "container.4", - }, - networks: map[string]*docker.NetworkResource{ - "1": { - Name: "foo", - }, - }, - }, - } - - for caseID, e := range cases { - e := e - t.Run(strconv.Itoa(caseID), func(t *testing.T) { - t.Parallel() - dockerData := parseService(e.service, e.networks) - dockerClient := &fakeTasksClient{tasks: e.tasks} - taskDockerData, _ := listTasks(context.Background(), dockerClient, e.service.ID, dockerData, map[string]*docker.NetworkResource{}, e.isGlobalSVC) - - if len(e.expectedTasks) != len(taskDockerData) { - t.Errorf("expected tasks %v, got %v", spew.Sdump(e.expectedTasks), spew.Sdump(taskDockerData)) - } - - for i, taskID := range e.expectedTasks { - if taskDockerData[i].Name != taskID { - t.Errorf("expect task id %v, got %v", taskID, taskDockerData[i].Name) - } - } - }) - } -} - -func TestDockerGetServiceProtocol(t *testing.T) { - provider := &Docker{} - - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - }, - expected: "http", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "another", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.protocol": "https", - }, - }, - }, - expected: "https", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.myservice.protocol": "https", - }, - }, - }, - expected: "https", - }, - } - - for _, e := range containers { - dockerData := parseContainer(e.container) - actual := provider.getServiceProtocol(dockerData, "myservice") - if actual != e.expected { - t.Fatalf("expected %q, got %q", e.expected, actual) - } - } -} - -func TestDockerGetServiceWeight(t *testing.T) { - provider := &Docker{} - - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - }, - expected: "0", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "another", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.weight": "200", - }, - }, - }, - expected: "200", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.myservice.weight": "31337", - }, - }, - }, - expected: "31337", - }, - } - - for _, e := range containers { - dockerData := parseContainer(e.container) - actual := provider.getServiceWeight(dockerData, "myservice") - if actual != e.expected { - t.Fatalf("expected %q, got %q", e.expected, actual) - } - } -} - -func TestDockerGetServicePort(t *testing.T) { - provider := &Docker{} - - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - }, - expected: "", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "another", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.port": "2500", - }, - }, - }, - expected: "2500", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.myservice.port": "1234", - }, - }, - }, - expected: "1234", - }, - } - - for _, e := range containers { - dockerData := parseContainer(e.container) - actual := provider.getServicePort(dockerData, "myservice") - if actual != e.expected { - t.Fatalf("expected %q, got %q", e.expected, actual) - } - } -} - -func TestDockerGetServiceFrontendRule(t *testing.T) { - provider := &Docker{} - - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - }, - expected: "Host:foo.", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "another", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.frontend.rule": "Path:/helloworld", - }, - }, - }, - expected: "Path:/helloworld", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.myservice.frontend.rule": "Path:/mycustomservicepath", - }, - }, - }, - expected: "Path:/mycustomservicepath", - }, - } - - for _, e := range containers { - dockerData := parseContainer(e.container) - actual := provider.getServiceFrontendRule(dockerData, "myservice") - if actual != e.expected { - t.Fatalf("expected %q, got %q", e.expected, actual) - } - } -} - -func TestDockerGetServiceBackend(t *testing.T) { - provider := &Docker{} - - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - }, - expected: "foo-myservice", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "another", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.backend": "another-backend", - }, - }, - }, - expected: "another-backend-myservice", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.myservice.frontend.backend": "custom-backend", - }, - }, - }, - expected: "custom-backend", - }, - } - - for _, e := range containers { - dockerData := parseContainer(e.container) - actual := provider.getServiceBackend(dockerData, "myservice") - if actual != e.expected { - t.Fatalf("expected %q, got %q", e.expected, actual) - } - } -} - -func TestDockerGetServicePriority(t *testing.T) { - provider := &Docker{} - - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - }, - expected: "0", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "another", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.frontend.priority": "33", - }, - }, - }, - expected: "33", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.myservice.frontend.priority": "2503", - }, - }, - }, - expected: "2503", - }, - } - - for _, e := range containers { - dockerData := parseContainer(e.container) - actual := provider.getServicePriority(dockerData, "myservice") - if actual != e.expected { - t.Fatalf("expected %q, got %q", e.expected, actual) - } - } -} - -func TestDockerGetServicePassHostHeader(t *testing.T) { - provider := &Docker{} - - containers := []struct { - container docker.ContainerJSON - expected string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - }, - expected: "true", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "another", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.frontend.passHostHeader": "false", - }, - }, - }, - expected: "false", - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.myservice.frontend.passHostHeader": "false", - }, - }, - }, - expected: "false", - }, - } - - for _, e := range containers { - dockerData := parseContainer(e.container) - actual := provider.getServicePassHostHeader(dockerData, "myservice") - if actual != e.expected { - t.Fatalf("expected %q, got %q", e.expected, actual) - } - } -} - -func TestDockerGetServiceEntryPoints(t *testing.T) { - provider := &Docker{} - - containers := []struct { - container docker.ContainerJSON - expected []string - }{ - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{}, - }, - expected: []string{}, - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "another", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.frontend.entryPoints": "http,https", - }, - }, - }, - expected: []string{"http", "https"}, - }, - { - container: docker.ContainerJSON{ - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.myservice.frontend.entryPoints": "http,https", - }, - }, - }, - expected: []string{"http", "https"}, - }, - } - - for _, e := range containers { - dockerData := parseContainer(e.container) - actual := provider.getServiceEntryPoints(dockerData, "myservice") - if !reflect.DeepEqual(actual, e.expected) { - t.Fatalf("expected %q, got %q for container %q", e.expected, actual, dockerData.Name) - } - } -} - -func TestDockerLoadDockerServiceConfig(t *testing.T) { - cases := []struct { - containers []docker.ContainerJSON - expectedFrontends map[string]*types.Frontend - expectedBackends map[string]*types.Backend - }{ - { - containers: []docker.ContainerJSON{}, - expectedFrontends: map[string]*types.Frontend{}, - expectedBackends: map[string]*types.Backend{}, - }, - { - containers: []docker.ContainerJSON{ - { - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "foo", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.service.port": "2503", - "traefik.service.frontend.entryPoints": "http,https", - }, - }, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - }, - }, - Networks: map[string]*network.EndpointSettings{ - "bridge": { - IPAddress: "127.0.0.1", - }, - }, - }, - }, - }, - expectedFrontends: map[string]*types.Frontend{ - "frontend-foo-service": { - Backend: "backend-foo-service", - PassHostHeader: true, - EntryPoints: []string{"http", "https"}, - Routes: map[string]types.Route{ - "service-service": { - Rule: "Host:foo.docker.localhost", - }, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-foo-service": { - Servers: map[string]types.Server{ - "service": { - URL: "http://127.0.0.1:2503", - Weight: 0, - }, - }, - CircuitBreaker: nil, - }, - }, - }, - { - containers: []docker.ContainerJSON{ - { - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test1", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.service.port": "2503", - "traefik.service.protocol": "https", - "traefik.service.weight": "80", - "traefik.service.frontend.backend": "foobar", - "traefik.service.frontend.passHostHeader": "false", - "traefik.service.frontend.rule": "Path:/mypath", - "traefik.service.frontend.priority": "5000", - "traefik.service.frontend.entryPoints": "http,https,ws", - }, - }, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - }, - }, - Networks: map[string]*network.EndpointSettings{ - "bridge": { - IPAddress: "127.0.0.1", - }, - }, - }, - }, - { - ContainerJSONBase: &docker.ContainerJSONBase{ - Name: "test2", - }, - Config: &container.Config{ - Labels: map[string]string{ - "traefik.anotherservice.port": "8079", - "traefik.anotherservice.weight": "33", - "traefik.anotherservice.frontend.rule": "Path:/anotherpath", - }, - }, - NetworkSettings: &docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{ - Ports: nat.PortMap{ - "80/tcp": {}, - }, - }, - Networks: map[string]*network.EndpointSettings{ - "bridge": { - IPAddress: "127.0.0.1", - }, - }, - }, - }, - }, - expectedFrontends: map[string]*types.Frontend{ - "frontend-foobar": { - Backend: "backend-foobar", - PassHostHeader: false, - Priority: 5000, - EntryPoints: []string{"http", "https", "ws"}, - Routes: map[string]types.Route{ - "service-service": { - Rule: "Path:/mypath", - }, - }, - }, - "frontend-test2-anotherservice": { - Backend: "backend-test2-anotherservice", - PassHostHeader: true, - EntryPoints: []string{}, - Routes: map[string]types.Route{ - "service-anotherservice": { - Rule: "Path:/anotherpath", - }, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-foobar": { - Servers: map[string]types.Server{ - "service": { - URL: "https://127.0.0.1:2503", - Weight: 80, - }, - }, - CircuitBreaker: nil, - }, - "backend-test2-anotherservice": { - Servers: map[string]types.Server{ - "service": { - URL: "http://127.0.0.1:8079", - Weight: 33, - }, - }, - CircuitBreaker: nil, - }, - }, - }, - } - - provider := &Docker{ - Domain: "docker.localhost", - ExposedByDefault: true, - } - - for _, c := range cases { - 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) - } - } -} diff --git a/provider/docker_unix.go b/provider/docker_unix.go deleted file mode 100644 index 8f901f7b4..000000000 --- a/provider/docker_unix.go +++ /dev/null @@ -1,8 +0,0 @@ -// +build !windows - -package provider - -const ( - // DockerAPIVersion is a constant holding the version of the Docker API traefik will use - DockerAPIVersion string = "1.21" -) diff --git a/provider/docker_windows.go b/provider/docker_windows.go deleted file mode 100644 index dc351fc2a..000000000 --- a/provider/docker_windows.go +++ /dev/null @@ -1,6 +0,0 @@ -package provider - -const ( - // DockerAPIVersion is a constant holding the version of the Docker API traefik will use - DockerAPIVersion string = "1.24" -) diff --git a/provider/ecs.go b/provider/ecs.go index a8426b43d..7089f3c24 100644 --- a/provider/ecs.go +++ b/provider/ecs.go @@ -182,7 +182,7 @@ func (provider *ECS) loadECSConfig(ctx context.Context, client *awsClient) (*typ instances = fun.Filter(provider.filterInstance, instances).([]ecsInstance) - return provider.getConfiguration("templates/ecs.tmpl", ecsFuncMap, struct { + return provider.GetConfiguration("templates/ecs.tmpl", ecsFuncMap, struct { Instances []ecsInstance }{ instances, diff --git a/provider/eureka.go b/provider/eureka.go index 29329a31d..c935e5630 100644 --- a/provider/eureka.go +++ b/provider/eureka.go @@ -108,7 +108,7 @@ func (provider *Eureka) buildConfiguration() (*types.Configuration, error) { applications.Applications, } - configuration, err := provider.getConfiguration("templates/eureka.tmpl", EurekaFuncMap, templateObjects) + configuration, err := provider.GetConfiguration("templates/eureka.tmpl", EurekaFuncMap, templateObjects) if err != nil { log.Error(err) } diff --git a/provider/kubernetes.go b/provider/kubernetes.go index 18f89d6de..6ccdb94bf 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -320,7 +320,7 @@ func (provider *Kubernetes) getPassHostHeader() bool { func (provider *Kubernetes) loadConfig(templateObjects types.Configuration) *types.Configuration { var FuncMap = template.FuncMap{} - configuration, err := provider.getConfiguration("templates/kubernetes.tmpl", FuncMap, templateObjects) + configuration, err := provider.GetConfiguration("templates/kubernetes.tmpl", FuncMap, templateObjects) if err != nil { log.Error(err) } diff --git a/provider/kv.go b/provider/kv.go index 9c99781c7..376c1aa5c 100644 --- a/provider/kv.go +++ b/provider/kv.go @@ -130,7 +130,7 @@ func (provider *Kv) loadConfig() *types.Configuration { "Last": provider.last, } - configuration, err := provider.getConfiguration("templates/kv.tmpl", KvFuncMap, templateObjects) + configuration, err := provider.GetConfiguration("templates/kv.tmpl", KvFuncMap, templateObjects) if err != nil { log.Error(err) } diff --git a/provider/marathon.go b/provider/marathon.go index eef95124d..db58b41f9 100644 --- a/provider/marathon.go +++ b/provider/marathon.go @@ -183,7 +183,7 @@ func (provider *Marathon) loadMarathonConfig() *types.Configuration { provider.Domain, } - configuration, err := provider.getConfiguration("templates/marathon.tmpl", MarathonFuncMap, templateObjects) + configuration, err := provider.GetConfiguration("templates/marathon.tmpl", MarathonFuncMap, templateObjects) if err != nil { log.Error(err) } diff --git a/provider/mesos.go b/provider/mesos.go index 1835266b3..23536f38e 100644 --- a/provider/mesos.go +++ b/provider/mesos.go @@ -167,7 +167,7 @@ func (provider *Mesos) loadMesosConfig() *types.Configuration { provider.Domain, } - configuration, err := provider.getConfiguration("templates/mesos.tmpl", mesosFuncMap, templateObjects) + configuration, err := provider.GetConfiguration("templates/mesos.tmpl", mesosFuncMap, templateObjects) if err != nil { log.Error(err) } diff --git a/provider/provider.go b/provider/provider.go index 2782d3513..e67d77957 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -51,7 +51,8 @@ func (p *BaseProvider) MatchConstraints(tags []string) (bool, *types.Constraint) return true, nil } -func (p *BaseProvider) getConfiguration(defaultTemplateFile string, funcMap template.FuncMap, templateObjects interface{}) (*types.Configuration, error) { +// GetConfiguration return the provider configuration using templating +func (p *BaseProvider) GetConfiguration(defaultTemplateFile string, funcMap template.FuncMap, templateObjects interface{}) (*types.Configuration, error) { var ( buf []byte err error @@ -60,7 +61,7 @@ func (p *BaseProvider) getConfiguration(defaultTemplateFile string, funcMap temp var defaultFuncMap = template.FuncMap{ "replace": replace, "tolower": strings.ToLower, - "normalize": normalize, + "normalize": Normalize, "split": split, "contains": contains, } @@ -112,7 +113,8 @@ func split(sep, s string) []string { return strings.Split(s, sep) } -func normalize(name string) string { +// Normalize transform a string that work with the rest of traefik +func Normalize(name string) string { fargs := func(c rune) bool { return !unicode.IsLetter(c) && !unicode.IsNumber(c) } diff --git a/provider/provider_test.go b/provider/provider_test.go index 79c757755..0e42c6bfd 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -91,7 +91,7 @@ func TestConfigurationErrors(t *testing.T) { } for _, invalid := range invalids { - configuration, err := invalid.provider.getConfiguration(invalid.defaultTemplate, invalid.funcMap, nil) + configuration, err := invalid.provider.GetConfiguration(invalid.defaultTemplate, invalid.funcMap, nil) if err == nil || !strings.Contains(err.Error(), invalid.expectedError) { t.Fatalf("should have generate an error with %q, got %v", invalid.expectedError, err) } @@ -136,7 +136,7 @@ func TestGetConfiguration(t *testing.T) { }, nil, } - configuration, err := provider.getConfiguration(templateFile.Name(), nil, nil) + configuration, err := provider.GetConfiguration(templateFile.Name(), nil, nil) if err != nil { t.Fatalf("Shouldn't have error out, got %v", err) } @@ -198,7 +198,7 @@ func TestGetConfigurationReturnsCorrectMaxConnConfiguration(t *testing.T) { }, nil, } - configuration, err := provider.getConfiguration(templateFile.Name(), nil, nil) + configuration, err := provider.GetConfiguration(templateFile.Name(), nil, nil) if err != nil { t.Fatalf("Shouldn't have error out, got %v", err) } @@ -366,7 +366,7 @@ func TestDefaultFuncMap(t *testing.T) { }, nil, } - configuration, err := provider.getConfiguration(templateFile.Name(), nil, nil) + configuration, err := provider.GetConfiguration(templateFile.Name(), nil, nil) if err != nil { t.Fatalf("Shouldn't have error out, got %v", err) } diff --git a/provider/rancher.go b/provider/rancher.go index 72b2231d4..3b2b95e8b 100644 --- a/provider/rancher.go +++ b/provider/rancher.go @@ -79,7 +79,7 @@ func (provider *Rancher) getFrontendRule(service rancherData) string { func (provider *Rancher) getFrontendName(service rancherData) string { // Replace '.' with '-' in quoted keys because of this issue https://github.com/BurntSushi/toml/issues/78 - return normalize(provider.getFrontendRule(service)) + return Normalize(provider.getFrontendRule(service)) } // Backend Labels @@ -122,9 +122,9 @@ func (provider *Rancher) getSticky(service rancherData) string { func (provider *Rancher) getBackend(service rancherData) string { if label, err := getServiceLabel(service, "traefik.backend"); err == nil { - return normalize(label) + return Normalize(label) } - return normalize(service.Name) + return Normalize(service.Name) } // Generall Application Stuff @@ -436,7 +436,7 @@ func (provider *Rancher) loadRancherConfig(services []rancherData) *types.Config provider.Domain, } - configuration, err := provider.getConfiguration("templates/rancher.tmpl", RancherFuncMap, templateObjects) + configuration, err := provider.GetConfiguration("templates/rancher.tmpl", RancherFuncMap, templateObjects) if err != nil { log.Error(err) }