1
0
Fork 0

Adds Docker provider support

Co-authored-by: Julien Salleyron <julien@containo.us>
This commit is contained in:
Ludovic Fernandez 2019-01-18 15:18:04 +01:00 committed by Traefiker Bot
parent 8735263930
commit b54c956c5e
78 changed files with 3476 additions and 5587 deletions

View file

@ -17,12 +17,10 @@ import (
"github.com/containous/traefik/old/provider/boltdb"
"github.com/containous/traefik/old/provider/consul"
"github.com/containous/traefik/old/provider/consulcatalog"
"github.com/containous/traefik/old/provider/docker"
"github.com/containous/traefik/old/provider/dynamodb"
"github.com/containous/traefik/old/provider/ecs"
"github.com/containous/traefik/old/provider/etcd"
"github.com/containous/traefik/old/provider/eureka"
"github.com/containous/traefik/old/provider/file"
"github.com/containous/traefik/old/provider/kubernetes"
"github.com/containous/traefik/old/provider/marathon"
"github.com/containous/traefik/old/provider/mesos"
@ -32,6 +30,8 @@ import (
"github.com/containous/traefik/old/tls"
"github.com/containous/traefik/old/types"
acmeprovider "github.com/containous/traefik/provider/acme"
"github.com/containous/traefik/provider/docker"
"github.com/containous/traefik/provider/file"
newtypes "github.com/containous/traefik/types"
"github.com/pkg/errors"
"github.com/xenolf/lego/challenge/dns01"

View file

@ -4,11 +4,8 @@ import (
"github.com/containous/traefik/config/static"
"github.com/containous/traefik/old/api"
"github.com/containous/traefik/old/middlewares/tracing"
"github.com/containous/traefik/old/provider/file"
"github.com/containous/traefik/old/types"
"github.com/containous/traefik/ping"
"github.com/containous/traefik/provider"
file2 "github.com/containous/traefik/provider/file"
"github.com/containous/traefik/tracing/datadog"
"github.com/containous/traefik/tracing/jaeger"
"github.com/containous/traefik/tracing/zipkin"
@ -38,7 +35,6 @@ func ConvertStaticConf(globalConfiguration GlobalConfiguration) static.Configura
}
staticConfiguration.API = convertAPI(globalConfiguration.API)
staticConfiguration.Providers.File = convertFile(globalConfiguration.File)
staticConfiguration.Metrics = ConvertMetrics(globalConfiguration.Metrics)
staticConfiguration.AccessLog = ConvertAccessLog(globalConfiguration.AccessLog)
staticConfiguration.Tracing = ConvertTracing(globalConfiguration.Tracing)
@ -207,26 +203,6 @@ func convertConstraints(oldConstraints types.Constraints) types2.Constraints {
return constraints
}
func convertFile(old *file.Provider) *file2.Provider {
if old == nil {
return nil
}
f := &file2.Provider{
BaseProvider: provider.BaseProvider{
Watch: old.Watch,
Filename: old.Filename,
Trace: old.Trace,
},
Directory: old.Directory,
TraefikFile: old.TraefikFile,
}
f.DebugLogGeneratedTemplate = old.DebugLogGeneratedTemplate
f.Constraints = convertConstraints(old.Constraints)
return f
}
// ConvertHostResolverConfig FIXME
// Deprecated
func ConvertHostResolverConfig(oldconfig *HostResolverConfig) *static.HostResolverConfig {

View file

@ -1,111 +0,0 @@
package configuration
import (
"encoding/json"
"github.com/containous/traefik/old/log"
"github.com/containous/traefik/old/provider"
"github.com/containous/traefik/old/types"
"github.com/containous/traefik/safe"
)
// ProviderAggregator aggregate providers
type ProviderAggregator struct {
providers []provider.Provider
constraints types.Constraints
}
// NewProviderAggregator return an aggregate of all the providers configured in GlobalConfiguration
func NewProviderAggregator(gc *GlobalConfiguration) ProviderAggregator {
provider := ProviderAggregator{
constraints: gc.Constraints,
}
if gc.Docker != nil {
provider.quietAddProvider(gc.Docker)
}
if gc.Marathon != nil {
provider.quietAddProvider(gc.Marathon)
}
if gc.File != nil {
provider.quietAddProvider(gc.File)
}
if gc.Rest != nil {
provider.quietAddProvider(gc.Rest)
}
if gc.Consul != nil {
provider.quietAddProvider(gc.Consul)
}
if gc.ConsulCatalog != nil {
provider.quietAddProvider(gc.ConsulCatalog)
}
if gc.Etcd != nil {
provider.quietAddProvider(gc.Etcd)
}
if gc.Zookeeper != nil {
provider.quietAddProvider(gc.Zookeeper)
}
if gc.Boltdb != nil {
provider.quietAddProvider(gc.Boltdb)
}
if gc.Kubernetes != nil {
provider.quietAddProvider(gc.Kubernetes)
}
if gc.Mesos != nil {
provider.quietAddProvider(gc.Mesos)
}
if gc.Eureka != nil {
provider.quietAddProvider(gc.Eureka)
}
if gc.ECS != nil {
provider.quietAddProvider(gc.ECS)
}
if gc.Rancher != nil {
provider.quietAddProvider(gc.Rancher)
}
if gc.DynamoDB != nil {
provider.quietAddProvider(gc.DynamoDB)
}
return provider
}
func (p *ProviderAggregator) quietAddProvider(provider provider.Provider) {
err := p.AddProvider(provider)
if err != nil {
log.Errorf("Error initializing provider %T: %v", provider, err)
}
}
// AddProvider add a provider in the providers map
func (p *ProviderAggregator) AddProvider(provider provider.Provider) error {
err := provider.Init(p.constraints)
if err != nil {
return err
}
p.providers = append(p.providers, provider)
return nil
}
// Init the provider
func (p ProviderAggregator) Init(_ types.Constraints) error {
return nil
}
// Provide call the provide method of every providers
func (p ProviderAggregator) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error {
for _, p := range p.providers {
jsonConf, err := json.Marshal(p)
if err != nil {
log.Debugf("Unable to marshal provider conf %T with error: %v", p, err)
}
log.Infof("Starting provider %T %s", p, jsonConf)
currentProvider := p
safe.Go(func() {
err := currentProvider.Provide(configurationChan, pool)
if err != nil {
log.Errorf("Error starting provider %T: %v", p, err)
}
})
}
return nil
}

View file

@ -1,214 +0,0 @@
package docker
import (
docker "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/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 nodeIP(ip string) func(*docker.ContainerJSON) {
return func(c *docker.ContainerJSON) {
c.ContainerJSONBase.Node = &docker.ContainerNode{
IPAddress: ip,
}
}
}
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 taskNetworkAttachment(id string, name string, driver string, addresses []string) func(*swarm.Task) {
return func(task *swarm.Task) {
task.NetworksAttachments = append(task.NetworksAttachments, swarm.NetworkAttachment{
Network: swarm.Network{
ID: id,
Spec: swarm.NetworkSpec{
Annotations: swarm.Annotations{
Name: name,
},
DriverConfiguration: &swarm.Driver{
Name: driver,
},
},
},
Addresses: addresses,
})
}
}
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 taskContainerStatus(id string) func(*swarm.TaskStatus) {
return func(status *swarm.TaskStatus) {
status.ContainerStatus = swarm.ContainerStatus{
ContainerID: id,
}
}
}
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
}

View file

@ -1,413 +0,0 @@
package docker
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"net"
"strconv"
"strings"
"text/template"
"github.com/BurntSushi/ty/fun"
"github.com/containous/traefik/old/log"
"github.com/containous/traefik/old/provider"
"github.com/containous/traefik/old/provider/label"
"github.com/containous/traefik/old/types"
"github.com/docker/go-connections/nat"
)
const (
labelDockerNetwork = "traefik.docker.network"
labelBackendLoadBalancerSwarm = "traefik.backend.loadbalancer.swarm"
labelDockerComposeProject = "com.docker.compose.project"
labelDockerComposeService = "com.docker.compose.service"
)
func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.Configuration {
dockerFuncMap := template.FuncMap{
"getLabelValue": label.GetStringValue,
"getSubDomain": getSubDomain,
"isBackendLBSwarm": isBackendLBSwarm,
"getDomain": label.GetFuncString(label.TraefikDomain, p.Domain),
// Backend functions
"getIPAddress": p.getDeprecatedIPAddress, // TODO: Should we expose getIPPort instead?
"getServers": p.getServers,
"getMaxConn": label.GetMaxConn,
"getHealthCheck": label.GetHealthCheck,
"getBuffering": label.GetBuffering,
"getResponseForwarding": label.GetResponseForwarding,
"getCircuitBreaker": label.GetCircuitBreaker,
"getLoadBalancer": label.GetLoadBalancer,
// Frontend functions
"getBackendName": getBackendName,
"getPriority": label.GetFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriority),
"getPassHostHeader": label.GetFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader),
"getPassTLSCert": label.GetFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert),
"getPassTLSClientCert": label.GetTLSClientCert,
"getEntryPoints": label.GetFuncSliceString(label.TraefikFrontendEntryPoints),
"getBasicAuth": label.GetFuncSliceString(label.TraefikFrontendAuthBasic), // Deprecated
"getAuth": label.GetAuth,
"getFrontendRule": p.getFrontendRule,
"getRedirect": label.GetRedirect,
"getErrorPages": label.GetErrorPages,
"getRateLimit": label.GetRateLimit,
"getHeaders": label.GetHeaders,
"getWhiteList": label.GetWhiteList,
}
// filter containers
filteredContainers := fun.Filter(p.containerFilter, containersInspected).([]dockerData)
frontends := map[string][]dockerData{}
servers := map[string][]dockerData{}
serviceNames := make(map[string]struct{})
for idx, container := range filteredContainers {
segmentProperties := label.ExtractTraefikLabels(container.Labels)
for segmentName, labels := range segmentProperties {
container.SegmentLabels = labels
container.SegmentName = segmentName
serviceNamesKey := getServiceNameKey(container, p.SwarmMode, segmentName)
if _, exists := serviceNames[serviceNamesKey]; !exists {
frontendName := p.getFrontendName(container, idx)
frontends[frontendName] = append(frontends[frontendName], container)
if len(serviceNamesKey) > 0 {
serviceNames[serviceNamesKey] = struct{}{}
}
}
// Backends
backendName := getBackendName(container)
// Servers
servers[backendName] = append(servers[backendName], container)
}
}
templateObjects := struct {
Containers []dockerData
Frontends map[string][]dockerData
Servers map[string][]dockerData
Domain string
}{
Containers: filteredContainers,
Frontends: frontends,
Servers: servers,
Domain: p.Domain,
}
configuration, err := p.GetConfiguration("templates/docker.tmpl", dockerFuncMap, templateObjects)
if err != nil {
log.Error(err)
}
return configuration
}
func getServiceNameKey(container dockerData, swarmMode bool, segmentName string) string {
if swarmMode {
return container.ServiceName + segmentName
}
return getServiceName(container) + segmentName
}
func (p *Provider) containerFilter(container dockerData) bool {
if !label.IsEnabled(container.Labels, p.ExposedByDefault) {
log.Debugf("Filtering disabled container %s", container.Name)
return false
}
segmentProperties := label.ExtractTraefikLabels(container.Labels)
var errPort error
for segmentName, labels := range segmentProperties {
errPort = checkSegmentPort(labels, segmentName)
if len(p.getFrontendRule(container, labels)) == 0 {
log.Debugf("Filtering container with empty frontend rule %s %s", container.Name, segmentName)
return false
}
}
if len(container.NetworkSettings.Ports) == 0 && errPort != nil {
log.Debugf("Filtering container without port, %s: %v", container.Name, errPort)
return false
}
constraintTags := label.SplitAndTrimString(container.Labels[label.TraefikTags], ",")
if ok, failingConstraint := p.MatchConstraints(constraintTags); !ok {
if failingConstraint != nil {
log.Debugf("Container %s pruned by %q constraint", container.Name, failingConstraint.String())
}
return false
}
if container.Health != "" && container.Health != "healthy" {
log.Debugf("Filtering unhealthy or starting container %s", container.Name)
return false
}
return true
}
func checkSegmentPort(labels map[string]string, segmentName string) error {
if port, ok := labels[label.TraefikPort]; ok {
_, err := strconv.Atoi(port)
if err != nil {
return fmt.Errorf("invalid port value %q for the segment %q: %v", port, segmentName, err)
}
} else {
return fmt.Errorf("port label is missing, please use %s as default value or define port label for all segments ('traefik.<segment_name>.port')", label.TraefikPort)
}
return nil
}
func (p *Provider) getFrontendName(container dockerData, idx int) string {
var name string
if len(container.SegmentName) > 0 {
name = container.SegmentName + "-" + getBackendName(container)
} else {
name = p.getFrontendRule(container, container.SegmentLabels) + "-" + strconv.Itoa(idx)
}
return provider.Normalize(name)
}
func (p *Provider) getFrontendRule(container dockerData, segmentLabels map[string]string) string {
if value := label.GetStringValue(segmentLabels, label.TraefikFrontendRule, ""); len(value) != 0 {
return value
}
domain := label.GetStringValue(segmentLabels, label.TraefikDomain, p.Domain)
if len(domain) > 0 {
domain = "." + domain
}
if values, err := label.GetStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil {
return "Host:" + getSubDomain(values[labelDockerComposeService]+"."+values[labelDockerComposeProject]) + domain
}
if len(domain) > 0 {
return "Host:" + getSubDomain(container.ServiceName) + domain
}
return ""
}
func (p Provider) getIPAddress(container dockerData) string {
if value := label.GetStringValue(container.Labels, labelDockerNetwork, p.Network); value != "" {
networkSettings := container.NetworkSettings
if networkSettings.Networks != nil {
network := networkSettings.Networks[value]
if network != nil {
return network.Addr
}
log.Warnf("Could not find network named '%s' for container '%s'! Maybe you're missing the project's prefix in the label? Defaulting to first available network.", value, container.Name)
}
}
if container.NetworkSettings.NetworkMode.IsHost() {
if container.Node != nil {
if container.Node.IPAddress != "" {
return container.Node.IPAddress
}
}
return "127.0.0.1"
}
if container.NetworkSettings.NetworkMode.IsContainer() {
dockerClient, err := p.createClient()
if err != nil {
log.Warnf("Unable to get IP address for container %s, error: %s", container.Name, err)
return ""
}
connectedContainer := container.NetworkSettings.NetworkMode.ConnectedContainer()
containerInspected, err := dockerClient.ContainerInspect(context.Background(), connectedContainer)
if err != nil {
log.Warnf("Unable to get IP address for container %s : Failed to inspect container ID %s, error: %s", container.Name, connectedContainer, err)
return ""
}
return p.getIPAddress(parseContainer(containerInspected))
}
for _, network := range container.NetworkSettings.Networks {
return network.Addr
}
log.Warnf("Unable to find the IP address for the container %q.", container.Name)
return ""
}
// Deprecated: Please use getIPPort instead
func (p *Provider) getDeprecatedIPAddress(container dockerData) string {
ip, _, err := p.getIPPort(container)
if err != nil {
log.Warn(err)
return ""
}
return ip
}
// Escape beginning slash "/", convert all others to dash "-", and convert underscores "_" to dash "-"
func getSubDomain(name string) string {
return strings.Replace(strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1), "_", "-", -1)
}
func isBackendLBSwarm(container dockerData) bool {
return label.GetBoolValue(container.Labels, labelBackendLoadBalancerSwarm, false)
}
func getBackendName(container dockerData) string {
if len(container.SegmentName) > 0 {
return getSegmentBackendName(container)
}
return getDefaultBackendName(container)
}
func getSegmentBackendName(container dockerData) string {
serviceName := getServiceName(container)
if value := label.GetStringValue(container.SegmentLabels, label.TraefikBackend, ""); len(value) > 0 {
return provider.Normalize(serviceName + "-" + value)
}
return provider.Normalize(serviceName + "-" + container.SegmentName)
}
func getDefaultBackendName(container dockerData) string {
if value := label.GetStringValue(container.SegmentLabels, label.TraefikBackend, ""); len(value) != 0 {
return provider.Normalize(value)
}
return provider.Normalize(getServiceName(container))
}
func getServiceName(container dockerData) string {
serviceName := container.ServiceName
if values, err := label.GetStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil {
serviceName = values[labelDockerComposeService] + "_" + values[labelDockerComposeProject]
}
return serviceName
}
func getPort(container dockerData) string {
if value := label.GetStringValue(container.SegmentLabels, label.TraefikPort, ""); len(value) != 0 {
return value
}
// See iteration order in https://blog.golang.org/go-maps-in-action
var ports []nat.Port
for port := range container.NetworkSettings.Ports {
ports = append(ports, port)
}
less := func(i, j nat.Port) bool {
return i.Int() < j.Int()
}
nat.Sort(ports, less)
if len(ports) > 0 {
min := ports[0]
return min.Port()
}
return ""
}
func (p *Provider) getPortBinding(container dockerData) (*nat.PortBinding, error) {
port := getPort(container)
for netPort, portBindings := range container.NetworkSettings.Ports {
if strings.EqualFold(string(netPort), port+"/TCP") || strings.EqualFold(string(netPort), port+"/UDP") {
for _, p := range portBindings {
return &p, nil
}
}
}
return nil, fmt.Errorf("unable to find the external IP:Port for the container %q", container.Name)
}
func (p *Provider) getIPPort(container dockerData) (string, string, error) {
var ip, port string
usedBound := false
if p.UseBindPortIP {
portBinding, err := p.getPortBinding(container)
if err != nil {
log.Infof("Unable to find a binding for container %q, falling back on its internal IP/Port.", container.Name)
} else if (portBinding.HostIP == "0.0.0.0") || (len(portBinding.HostIP) == 0) {
log.Infof("Cannot determine the IP address (got %q) for %q's binding, falling back on its internal IP/Port.", portBinding.HostIP, container.Name)
} else {
ip = portBinding.HostIP
port = portBinding.HostPort
usedBound = true
}
}
if !usedBound {
ip = p.getIPAddress(container)
port = getPort(container)
}
if len(ip) == 0 {
return "", "", fmt.Errorf("unable to find the IP address for the container %q: the server is ignored", container.Name)
}
return ip, port, nil
}
func (p *Provider) getServers(containers []dockerData) map[string]types.Server {
var servers map[string]types.Server
for _, container := range containers {
ip, port, err := p.getIPPort(container)
if err != nil {
log.Warn(err)
continue
}
if servers == nil {
servers = make(map[string]types.Server)
}
protocol := label.GetStringValue(container.SegmentLabels, label.TraefikProtocol, label.DefaultProtocol)
serverURL := fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(ip, port))
serverName := getServerName(container.Name, serverURL)
if _, exist := servers[serverName]; exist {
log.Debugf("Skipping server %q with the same URL.", serverName)
continue
}
servers[serverName] = types.Server{
URL: serverURL,
Weight: label.GetIntValue(container.SegmentLabels, label.TraefikWeight, label.DefaultWeight),
}
}
return servers
}
func getServerName(containerName, url string) string {
hash := md5.New()
_, err := hash.Write([]byte(url))
if err != nil {
// Impossible case
log.Errorf("Fail to hash server URL %q", url)
}
return provider.Normalize("server-" + containerName + "-" + hex.EncodeToString(hash.Sum(nil)))
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,853 +0,0 @@
package docker
import (
"testing"
"time"
"github.com/containous/flaeg/parse"
"github.com/containous/traefik/old/provider/label"
"github.com/containous/traefik/old/types"
docker "github.com/docker/docker/api/types"
"github.com/docker/go-connections/nat"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSegmentBuildConfiguration(t *testing.T) {
testCases := []struct {
desc string
containers []docker.ContainerJSON
expectedFrontends map[string]*types.Frontend
expectedBackends map[string]*types.Backend
}{
{
desc: "when no container",
containers: []docker.ContainerJSON{},
expectedFrontends: map[string]*types.Frontend{},
expectedBackends: map[string]*types.Backend{},
},
{
desc: "simple configuration",
containers: []docker.ContainerJSON{
containerJSON(
name("foo"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.frontend.entryPoints": "http,https",
}),
ports(nat.PortMap{
"80/tcp": {},
}),
withNetwork("bridge", ipv4("127.0.0.1")),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
PassHostHeader: true,
EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.docker.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-863563a2e23c95502862016417ee95ea": {
URL: "http://127.0.0.1:2503",
Weight: label.DefaultWeight,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "pass tls client cert",
containers: []docker.ContainerJSON{
containerJSON(
name("foo"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.frontend.entryPoints": "http,https",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertPem: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosNotAfter: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosNotBefore: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSans: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectCommonName: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectCountry: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectDomainComponent: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectLocality: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectOrganization: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectProvince: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectSerialNumber: "true",
}),
ports(nat.PortMap{
"80/tcp": {},
}),
withNetwork("bridge", ipv4("127.0.0.1")),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
PassHostHeader: true,
EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.docker.localhost",
},
},
PassTLSClientCert: &types.TLSClientHeaders{
PEM: true,
Infos: &types.TLSClientCertificateInfos{
NotBefore: true,
Sans: true,
NotAfter: true,
Subject: &types.TLSCLientCertificateDNInfos{
CommonName: true,
Country: true,
DomainComponent: true,
Locality: true,
Organization: true,
Province: true,
SerialNumber: true,
},
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-863563a2e23c95502862016417ee95ea": {
URL: "http://127.0.0.1:2503",
Weight: label.DefaultWeight,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "auth basic",
containers: []docker.ContainerJSON{
containerJSON(
name("foo"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.frontend.entryPoints": "http,https",
label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicRealm: "myRealm",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsersFile: ".htpasswd",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicRemoveHeader: "true",
}),
ports(nat.PortMap{
"80/tcp": {},
}),
withNetwork("bridge", ipv4("127.0.0.1")),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
PassHostHeader: true,
EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.docker.localhost",
},
},
Auth: &types.Auth{
HeaderField: "X-WebAuth-User",
Basic: &types.Basic{
RemoveHeader: true,
Realm: "myRealm",
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
UsersFile: ".htpasswd",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-863563a2e23c95502862016417ee95ea": {
URL: "http://127.0.0.1:2503",
Weight: label.DefaultWeight,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "auth basic backward compatibility",
containers: []docker.ContainerJSON{
containerJSON(
name("foo"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.frontend.entryPoints": "http,https",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
}),
ports(nat.PortMap{
"80/tcp": {},
}),
withNetwork("bridge", ipv4("127.0.0.1")),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
PassHostHeader: true,
EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.docker.localhost",
},
},
Auth: &types.Auth{
Basic: &types.Basic{
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-863563a2e23c95502862016417ee95ea": {
URL: "http://127.0.0.1:2503",
Weight: label.DefaultWeight,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "auth digest",
containers: []docker.ContainerJSON{
containerJSON(
name("foo"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.frontend.entryPoints": "http,https",
label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsersFile: ".htpasswd",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestRemoveHeader: "true",
}),
ports(nat.PortMap{
"80/tcp": {},
}),
withNetwork("bridge", ipv4("127.0.0.1")),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
PassHostHeader: true,
EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.docker.localhost",
},
},
Auth: &types.Auth{
HeaderField: "X-WebAuth-User",
Digest: &types.Digest{
RemoveHeader: true,
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
UsersFile: ".htpasswd",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-863563a2e23c95502862016417ee95ea": {
URL: "http://127.0.0.1:2503",
Weight: label.DefaultWeight,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "auth forward",
containers: []docker.ContainerJSON{
containerJSON(
name("foo"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.frontend.entryPoints": "http,https",
label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardAddress: "auth.server",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTrustForwardHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCa: "ca.crt",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCaOptional: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCert: "server.crt",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSKey: "server.key",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSInsecureSkipVerify: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardAuthResponseHeaders: "X-Auth-User,X-Auth-Token",
}),
ports(nat.PortMap{
"80/tcp": {},
}),
withNetwork("bridge", ipv4("127.0.0.1")),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
PassHostHeader: true,
EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.docker.localhost",
},
},
Auth: &types.Auth{
HeaderField: "X-WebAuth-User",
Forward: &types.Forward{
Address: "auth.server",
TLS: &types.ClientTLS{
CA: "ca.crt",
CAOptional: true,
Cert: "server.crt",
Key: "server.key",
InsecureSkipVerify: true,
},
TrustForwardHeader: true,
AuthResponseHeaders: []string{"X-Auth-User", "X-Auth-Token"},
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-863563a2e23c95502862016417ee95ea": {
URL: "http://127.0.0.1:2503",
Weight: label.DefaultWeight,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "when all labels are set",
containers: []docker.ContainerJSON{
containerJSON(
name("foo"),
labels(map[string]string{
label.Prefix + "sauternes." + label.SuffixPort: "666",
label.Prefix + "sauternes." + label.SuffixProtocol: "https",
label.Prefix + "sauternes." + label.SuffixWeight: "12",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertPem: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosNotAfter: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosNotBefore: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSans: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosIssuerCommonName: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosIssuerCountry: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosIssuerDomainComponent: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosIssuerLocality: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosIssuerOrganization: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosIssuerProvince: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosIssuerSerialNumber: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectCommonName: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectCountry: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectDomainComponent: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectLocality: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectOrganization: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectProvince: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSClientCertInfosSubjectSerialNumber: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicRemoveHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicRealm: "myRealm",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsersFile: ".htpasswd",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestRemoveHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsersFile: ".htpasswd",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardAddress: "auth.server",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTrustForwardHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCa: "ca.crt",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCaOptional: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCert: "server.crt",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSKey: "server.key",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSInsecureSkipVerify: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.Prefix + "sauternes." + label.SuffixFrontendEntryPoints: "http,https",
label.Prefix + "sauternes." + label.SuffixFrontendPassHostHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSCert: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPriority: "666",
label.Prefix + "sauternes." + label.SuffixFrontendRedirectEntryPoint: "https",
label.Prefix + "sauternes." + label.SuffixFrontendRedirectRegex: "nope",
label.Prefix + "sauternes." + label.SuffixFrontendRedirectReplacement: "nope",
label.Prefix + "sauternes." + label.SuffixFrontendRedirectPermanent: "true",
label.Prefix + "sauternes." + label.SuffixFrontendWhiteListSourceRange: "10.10.10.10",
label.Prefix + "sauternes." + label.SuffixFrontendWhiteListIPStrategyExcludedIPS: "10.10.10.10,10.10.10.11",
label.Prefix + "sauternes." + label.SuffixFrontendWhiteListIPStrategyDepth: "5",
label.Prefix + "sauternes." + label.SuffixFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8",
label.Prefix + "sauternes." + label.SuffixFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLProxyHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersAllowedHosts: "foo,bar,bor",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersHostsProxyHeaders: "foo,bar,bor",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLHost: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersCustomFrameOptionsValue: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersContentSecurityPolicy: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersPublicKey: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersReferrerPolicy: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersCustomBrowserXSSValue: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSSeconds: "666",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLForceHost: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLRedirect: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLTemporaryRedirect: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSIncludeSubdomains: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSPreload: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersForceSTSHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersFrameDeny: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersContentTypeNosniff: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersBrowserXSSFilter: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersIsDevelopment: "true",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: "404",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: "foobar",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: "foo_query",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: "500,600",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: "foobar",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: "bar_query",
label.Prefix + "sauternes." + label.SuffixFrontendRateLimitExtractorFunc: "client.ip",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9",
}),
ports(nat.PortMap{
"80/tcp": {},
}),
withNetwork("bridge", ipv4("127.0.0.1")),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
EntryPoints: []string{
"http",
"https",
},
PassHostHeader: true,
PassTLSCert: true,
Priority: 666,
PassTLSClientCert: &types.TLSClientHeaders{
PEM: true,
Infos: &types.TLSClientCertificateInfos{
NotBefore: true,
Sans: true,
NotAfter: true,
Subject: &types.TLSCLientCertificateDNInfos{
CommonName: true,
Country: true,
DomainComponent: true,
Locality: true,
Organization: true,
Province: true,
SerialNumber: true,
},
Issuer: &types.TLSCLientCertificateDNInfos{
CommonName: true,
Country: true,
DomainComponent: true,
Locality: true,
Organization: true,
Province: true,
SerialNumber: true,
},
},
},
Auth: &types.Auth{
HeaderField: "X-WebAuth-User",
Basic: &types.Basic{
RemoveHeader: true,
Realm: "myRealm",
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
UsersFile: ".htpasswd",
},
},
WhiteList: &types.WhiteList{
SourceRange: []string{"10.10.10.10"},
IPStrategy: &types.IPStrategy{
Depth: 5,
ExcludedIPs: []string{"10.10.10.10", "10.10.10.11"},
},
},
Headers: &types.Headers{
CustomRequestHeaders: map[string]string{
"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
"Content-Type": "application/json; charset=utf-8",
},
CustomResponseHeaders: map[string]string{
"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
"Content-Type": "application/json; charset=utf-8",
},
AllowedHosts: []string{
"foo",
"bar",
"bor",
},
HostsProxyHeaders: []string{
"foo",
"bar",
"bor",
},
SSLRedirect: true,
SSLTemporaryRedirect: true,
SSLForceHost: true,
SSLHost: "foo",
SSLProxyHeaders: map[string]string{
"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
"Content-Type": "application/json; charset=utf-8",
},
STSSeconds: 666,
STSIncludeSubdomains: true,
STSPreload: true,
ForceSTSHeader: true,
FrameDeny: true,
CustomFrameOptionsValue: "foo",
ContentTypeNosniff: true,
BrowserXSSFilter: true,
CustomBrowserXSSValue: "foo",
ContentSecurityPolicy: "foo",
PublicKey: "foo",
ReferrerPolicy: "foo",
IsDevelopment: true,
},
Errors: map[string]*types.ErrorPage{
"foo": {
Status: []string{"404"},
Query: "foo_query",
Backend: "backend-foobar",
},
"bar": {
Status: []string{"500", "600"},
Query: "bar_query",
Backend: "backend-foobar",
},
},
RateLimit: &types.RateLimit{
ExtractorFunc: "client.ip",
RateSet: map[string]*types.Rate{
"foo": {
Period: parse.Duration(6 * time.Second),
Average: 12,
Burst: 18,
},
"bar": {
Period: parse.Duration(3 * time.Second),
Average: 6,
Burst: 9,
},
},
},
Redirect: &types.Redirect{
EntryPoint: "https",
Regex: "",
Replacement: "",
Permanent: true,
},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.docker.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-7f6444e0dff3330c8b0ad2bbbd383b0f": {
URL: "https://127.0.0.1:666",
Weight: 12,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "several containers",
containers: []docker.ContainerJSON{
containerJSON(
name("test1"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.protocol": "https",
"traefik.sauternes.weight": "80",
"traefik.sauternes.backend": "foobar",
"traefik.sauternes.frontend.passHostHeader": "false",
"traefik.sauternes.frontend.rule": "Path:/mypath",
"traefik.sauternes.frontend.priority": "5000",
"traefik.sauternes.frontend.entryPoints": "http,https,ws",
"traefik.sauternes.frontend.auth.basic": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
"traefik.sauternes.frontend.redirect.entryPoint": "https",
}),
ports(nat.PortMap{
"80/tcp": {},
}),
withNetwork("bridge", ipv4("127.0.0.1")),
),
containerJSON(
name("test2"),
labels(map[string]string{
"traefik.anothersauternes.port": "8079",
"traefik.anothersauternes.weight": "33",
"traefik.anothersauternes.frontend.rule": "Path:/anotherpath",
}),
ports(nat.PortMap{
"80/tcp": {},
}),
withNetwork("bridge", ipv4("127.0.0.1")),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: false,
Priority: 5000,
EntryPoints: []string{"http", "https", "ws"},
Auth: &types.Auth{
Basic: &types.Basic{
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
},
},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-sauternes-test1-foobar": {
Rule: "Path:/mypath",
},
},
},
"frontend-anothersauternes-test2-anothersauternes": {
Backend: "backend-test2-anothersauternes",
PassHostHeader: true,
EntryPoints: []string{},
Routes: map[string]types.Route{
"route-frontend-anothersauternes-test2-anothersauternes": {
Rule: "Path:/anotherpath",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test1-foobar": {
Servers: map[string]types.Server{
"server-test1-79533a101142718f0fdf84c42593c41e": {
URL: "https://127.0.0.1:2503",
Weight: 80,
},
},
CircuitBreaker: nil,
},
"backend-test2-anothersauternes": {
Servers: map[string]types.Server{
"server-test2-e9c1b66f9af919aa46053fbc2391bb4a": {
URL: "http://127.0.0.1:8079",
Weight: 33,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "several segments with the same backend name and same port",
containers: []docker.ContainerJSON{
containerJSON(
name("test1"),
labels(map[string]string{
"traefik.port": "2503",
"traefik.protocol": "https",
"traefik.weight": "80",
"traefik.frontend.entryPoints": "http,https",
"traefik.frontend.redirect.entryPoint": "https",
"traefik.sauternes.backend": "foobar",
"traefik.sauternes.frontend.rule": "Path:/sauternes",
"traefik.sauternes.frontend.priority": "5000",
"traefik.arbois.backend": "foobar",
"traefik.arbois.frontend.rule": "Path:/arbois",
"traefik.arbois.frontend.priority": "3000",
}),
ports(nat.PortMap{
"80/tcp": {},
}),
withNetwork("bridge", ipv4("127.0.0.1")),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 5000,
EntryPoints: []string{"http", "https"},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-sauternes-test1-foobar": {
Rule: "Path:/sauternes",
},
},
},
"frontend-arbois-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 3000,
EntryPoints: []string{"http", "https"},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-arbois-test1-foobar": {
Rule: "Path:/arbois",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test1-foobar": {
Servers: map[string]types.Server{
"server-test1-79533a101142718f0fdf84c42593c41e": {
URL: "https://127.0.0.1:2503",
Weight: 80,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "several segments with the same backend name and different port (wrong behavior)",
containers: []docker.ContainerJSON{
containerJSON(
name("test1"),
labels(map[string]string{
"traefik.protocol": "https",
"traefik.frontend.entryPoints": "http,https",
"traefik.frontend.redirect.entryPoint": "https",
"traefik.sauternes.port": "2503",
"traefik.sauternes.weight": "80",
"traefik.sauternes.backend": "foobar",
"traefik.sauternes.frontend.rule": "Path:/sauternes",
"traefik.sauternes.frontend.priority": "5000",
"traefik.arbois.port": "2504",
"traefik.arbois.weight": "90",
"traefik.arbois.backend": "foobar",
"traefik.arbois.frontend.rule": "Path:/arbois",
"traefik.arbois.frontend.priority": "3000",
}),
ports(nat.PortMap{
"80/tcp": {},
}),
withNetwork("bridge", ipv4("127.0.0.1")),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 5000,
EntryPoints: []string{"http", "https"},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-sauternes-test1-foobar": {
Rule: "Path:/sauternes",
},
},
},
"frontend-arbois-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 3000,
EntryPoints: []string{"http", "https"},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-arbois-test1-foobar": {
Rule: "Path:/arbois",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test1-foobar": {
Servers: map[string]types.Server{
"server-test1-79533a101142718f0fdf84c42593c41e": {
URL: "https://127.0.0.1:2503",
Weight: 80,
},
"server-test1-315a41140f1bd825b066e39686c18482": {
URL: "https://127.0.0.1:2504",
Weight: 90,
},
},
CircuitBreaker: nil,
},
},
},
}
provider := &Provider{
Domain: "docker.localhost",
ExposedByDefault: true,
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var dockerDataList []dockerData
for _, container := range test.containers {
dData := parseContainer(container)
dockerDataList = append(dockerDataList, dData)
}
actualConfig := provider.buildConfiguration(dockerDataList)
require.NotNil(t, actualConfig, "actualConfig")
assert.EqualValues(t, test.expectedBackends, actualConfig.Backends)
assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends)
})
}
}

View file

@ -1,485 +0,0 @@
package docker
import (
"context"
"io"
"net"
"net/http"
"strconv"
"strings"
"time"
"github.com/cenk/backoff"
"github.com/containous/traefik/job"
"github.com/containous/traefik/old/log"
"github.com/containous/traefik/old/provider"
"github.com/containous/traefik/old/types"
"github.com/containous/traefik/safe"
"github.com/containous/traefik/version"
dockertypes "github.com/docker/docker/api/types"
dockercontainertypes "github.com/docker/docker/api/types/container"
eventtypes "github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
swarmtypes "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"github.com/docker/go-connections/sockets"
)
const (
// SwarmAPIVersion is a constant holding the version of the Provider API traefik will use
SwarmAPIVersion = "1.24"
)
var _ provider.Provider = (*Provider)(nil)
// Provider holds configurations of the provider.
type Provider struct {
provider.BaseProvider `mapstructure:",squash" export:"true"`
Endpoint string `description:"Docker server endpoint. Can be a tcp or a unix socket endpoint"`
Domain string `description:"Default domain used"`
TLS *types.ClientTLS `description:"Enable Docker TLS support" export:"true"`
ExposedByDefault bool `description:"Expose containers by default" export:"true"`
UseBindPortIP bool `description:"Use the ip address from the bound port, rather than from the inner network" export:"true"`
SwarmMode bool `description:"Use Docker on Swarm Mode" export:"true"`
Network string `description:"Default Docker network used" export:"true"`
SwarmModeRefreshSeconds int `description:"Polling interval for swarm mode (in seconds)" export:"true"`
}
// Init the provider
func (p *Provider) Init(constraints types.Constraints) error {
return p.BaseProvider.Init(constraints)
}
// dockerData holds the need data to the Provider p
type dockerData struct {
ServiceName string
Name string
Labels map[string]string // List of labels set to container or service
NetworkSettings networkSettings
Health string
Node *dockertypes.ContainerNode
SegmentLabels map[string]string
SegmentName string
}
// 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 Provider p
type networkData struct {
Name string
Addr string
Port int
Protocol string
ID string
}
func (p *Provider) createClient() (client.APIClient, error) {
var httpClient *http.Client
if p.TLS != nil {
config, err := p.TLS.CreateTLSConfig()
if err != nil {
return nil, err
}
tr := &http.Transport{
TLSClientConfig: config,
}
hostURL, err := client.ParseHostURL(p.Endpoint)
if err != nil {
return nil, err
}
if err := sockets.ConfigureTransport(tr, hostURL.Scheme, hostURL.Host); err != nil {
return nil, err
}
httpClient = &http.Client{
Transport: tr,
}
}
httpHeaders := map[string]string{
"User-Agent": "Traefik " + version.Version,
}
var apiVersion string
if p.SwarmMode {
apiVersion = SwarmAPIVersion
} else {
apiVersion = DockerAPIVersion
}
return client.NewClient(p.Endpoint, apiVersion, httpClient, httpHeaders)
}
// Provide allows the docker provider to provide configurations to traefik
// using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error {
pool.GoCtx(func(routineCtx context.Context) {
operation := func() error {
var err error
ctx, cancel := context.WithCancel(routineCtx)
defer cancel()
dockerClient, err := p.createClient()
if err != nil {
log.Errorf("Failed to create a client for docker, error: %s", err)
return err
}
serverVersion, err := dockerClient.ServerVersion(ctx)
if err != nil {
log.Errorf("Failed to retrieve information of the docker client and server host: %s", err)
return err
}
log.Debugf("Provider connection established with docker %s (API %s)", serverVersion.Version, serverVersion.APIVersion)
var dockerDataList []dockerData
if p.SwarmMode {
dockerDataList, err = listServices(ctx, dockerClient)
if err != nil {
log.Errorf("Failed to list services for docker swarm mode, error %s", err)
return err
}
} else {
dockerDataList, err = listContainers(ctx, dockerClient)
if err != nil {
log.Errorf("Failed to list containers for docker, error %s", err)
return err
}
}
configuration := p.buildConfiguration(dockerDataList)
configurationChan <- types.ConfigMessage{
ProviderName: "docker",
Configuration: configuration,
}
if p.Watch {
if p.SwarmMode {
errChan := make(chan error)
// TODO: This need to be change. Linked to Swarm events docker/docker#23827
ticker := time.NewTicker(time.Second * time.Duration(p.SwarmModeRefreshSeconds))
pool.GoCtx(func(ctx context.Context) {
defer close(errChan)
for {
select {
case <-ticker.C:
services, err := listServices(ctx, dockerClient)
if err != nil {
log.Errorf("Failed to list services for docker, error %s", err)
errChan <- err
return
}
configuration := p.buildConfiguration(services)
if configuration != nil {
configurationChan <- types.ConfigMessage{
ProviderName: "docker",
Configuration: configuration,
}
}
case <-ctx.Done():
ticker.Stop()
return
}
}
})
if err, ok := <-errChan; ok {
return err
}
// channel closed
} else {
f := filters.NewArgs()
f.Add("type", "container")
options := dockertypes.EventsOptions{
Filters: f,
}
startStopHandle := func(m eventtypes.Message) {
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)
// Call cancel to get out of the monitor
return
}
configuration := p.buildConfiguration(containers)
if configuration != nil {
message := types.ConfigMessage{
ProviderName: "docker",
Configuration: configuration,
}
select {
case configurationChan <- message:
case <-ctx.Done():
}
}
}
eventsc, errc := dockerClient.Events(ctx, options)
for {
select {
case event := <-eventsc:
if event.Action == "start" ||
event.Action == "die" ||
strings.HasPrefix(event.Action, "health_status") {
startStopHandle(event)
}
case err := <-errc:
if err == io.EOF {
log.Debug("Provider event stream closed")
}
return err
case <-ctx.Done():
return nil
}
}
}
}
return nil
}
notify := func(err error, time time.Duration) {
log.Errorf("Provider connection error %+v, retrying in %s", err, time)
}
err := backoff.RetryNotify(safe.OperationWithRecover(operation), backoff.WithContext(job.NewBackOff(backoff.NewExponentialBackOff()), routineCtx), notify)
if err != nil {
log.Errorf("Cannot connect to docker server %+v", err)
}
})
return nil
}
func listContainers(ctx context.Context, dockerClient client.ContainerAPIClient) ([]dockerData, error) {
containerList, err := dockerClient.ContainerList(ctx, dockertypes.ContainerListOptions{})
if err != nil {
return nil, err
}
var containersInspected []dockerData
// get inspect containers
for _, container := range containerList {
dData := inspectContainers(ctx, dockerClient, container.ID)
if len(dData.Name) > 0 {
containersInspected = append(containersInspected, dData)
}
}
return containersInspected, nil
}
func inspectContainers(ctx context.Context, dockerClient client.ContainerAPIClient, containerID string) dockerData {
dData := dockerData{}
containerInspected, err := dockerClient.ContainerInspect(ctx, containerID)
if err != nil {
log.Warnf("Failed to inspect container %s, error: %s", containerID, err)
} else {
// This condition is here to avoid to have empty IP https://github.com/containous/traefik/issues/2459
// We register only container which are running
if containerInspected.ContainerJSONBase != nil && containerInspected.ContainerJSONBase.State != nil && containerInspected.ContainerJSONBase.State.Running {
dData = parseContainer(containerInspected)
}
}
return dData
}
func parseContainer(container dockertypes.ContainerJSON) dockerData {
dData := dockerData{
NetworkSettings: networkSettings{},
}
if container.ContainerJSONBase != nil {
dData.Name = container.ContainerJSONBase.Name
dData.ServiceName = dData.Name // Default ServiceName to be the container's Name.
dData.Node = container.ContainerJSONBase.Node
if container.ContainerJSONBase.HostConfig != nil {
dData.NetworkSettings.NetworkMode = container.ContainerJSONBase.HostConfig.NetworkMode
}
if container.State != nil && container.State.Health != nil {
dData.Health = container.State.Health.Status
}
}
if container.Config != nil && container.Config.Labels != nil {
dData.Labels = container.Config.Labels
}
if container.NetworkSettings != nil {
if container.NetworkSettings.Ports != nil {
dData.NetworkSettings.Ports = container.NetworkSettings.Ports
}
if container.NetworkSettings.Networks != nil {
dData.NetworkSettings.Networks = make(map[string]*networkData)
for name, containerNetwork := range container.NetworkSettings.Networks {
dData.NetworkSettings.Networks[name] = &networkData{
ID: containerNetwork.NetworkID,
Name: name,
Addr: containerNetwork.IPAddress,
}
}
}
}
return dData
}
func listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerData, error) {
serviceList, err := dockerClient.ServiceList(ctx, dockertypes.ServiceListOptions{})
if err != nil {
return nil, err
}
serverVersion, err := dockerClient.ServerVersion(ctx)
if err != nil {
return nil, err
}
networkListArgs := filters.NewArgs()
// https://docs.docker.com/engine/api/v1.29/#tag/Network (Docker 17.06)
if versions.GreaterThanOrEqualTo(serverVersion.APIVersion, "1.29") {
networkListArgs.Add("scope", "swarm")
} else {
networkListArgs.Add("driver", "overlay")
}
networkList, err := dockerClient.NetworkList(ctx, dockertypes.NetworkListOptions{Filters: networkListArgs})
if err != nil {
log.Debugf("Failed to network inspect on client for docker, error: %s", err)
return nil, err
}
networkMap := make(map[string]*dockertypes.NetworkResource)
for _, network := range networkList {
networkToAdd := network
networkMap[network.ID] = &networkToAdd
}
var dockerDataList []dockerData
var dockerDataListTasks []dockerData
for _, service := range serviceList {
dData := parseService(service, networkMap)
if isBackendLBSwarm(dData) {
if len(dData.NetworkSettings.Networks) > 0 {
dockerDataList = append(dockerDataList, dData)
}
} else {
isGlobalSvc := service.Spec.Mode.Global != nil
dockerDataListTasks, err = listTasks(ctx, dockerClient, service.ID, dData, networkMap, isGlobalSvc)
if err != nil {
log.Warn(err)
} else {
dockerDataList = append(dockerDataList, dockerDataListTasks...)
}
}
}
return dockerDataList, err
}
func parseService(service swarmtypes.Service, networkMap map[string]*dockertypes.NetworkResource) dockerData {
dData := dockerData{
ServiceName: service.Spec.Annotations.Name,
Name: service.Spec.Annotations.Name,
Labels: service.Spec.Annotations.Labels,
NetworkSettings: networkSettings{},
}
if service.Spec.EndpointSpec != nil {
if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeDNSRR {
if isBackendLBSwarm(dData) {
log.Warnf("Ignored %s endpoint-mode not supported, service name: %s. Fallback to Traefik load balancing", swarmtypes.ResolutionModeDNSRR, service.Spec.Annotations.Name)
}
} else if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeVIP {
dData.NetworkSettings.Networks = make(map[string]*networkData)
for _, virtualIP := range service.Endpoint.VirtualIPs {
networkService := networkMap[virtualIP.NetworkID]
if networkService != nil {
if len(virtualIP.Addr) > 0 {
ip, _, _ := net.ParseCIDR(virtualIP.Addr)
network := &networkData{
Name: networkService.Name,
ID: virtualIP.NetworkID,
Addr: ip.String(),
}
dData.NetworkSettings.Networks[network.Name] = network
} else {
log.Debugf("No virtual IPs found in network %s", virtualIP.NetworkID)
}
} else {
log.Debugf("Network not found, id: %s", virtualIP.NetworkID)
}
}
}
}
return dData
}
func listTasks(ctx context.Context, dockerClient client.APIClient, serviceID string,
serviceDockerData dockerData, networkMap map[string]*dockertypes.NetworkResource, isGlobalSvc bool) ([]dockerData, error) {
serviceIDFilter := filters.NewArgs()
serviceIDFilter.Add("service", serviceID)
serviceIDFilter.Add("desired-state", "running")
taskList, err := dockerClient.TaskList(ctx, dockertypes.TaskListOptions{Filters: serviceIDFilter})
if err != nil {
return nil, err
}
var dockerDataList []dockerData
for _, task := range taskList {
if task.Status.State != swarmtypes.TaskStateRunning {
continue
}
dData := parseTasks(task, serviceDockerData, networkMap, isGlobalSvc)
if len(dData.NetworkSettings.Networks) > 0 {
dockerDataList = append(dockerDataList, dData)
}
}
return dockerDataList, err
}
func parseTasks(task swarmtypes.Task, serviceDockerData dockerData,
networkMap map[string]*dockertypes.NetworkResource, isGlobalSvc bool) dockerData {
dData := dockerData{
ServiceName: serviceDockerData.Name,
Name: serviceDockerData.Name + "." + strconv.Itoa(task.Slot),
Labels: serviceDockerData.Labels,
NetworkSettings: networkSettings{},
}
if isGlobalSvc {
dData.Name = serviceDockerData.Name + "." + task.ID
}
if task.NetworksAttachments != nil {
dData.NetworkSettings.Networks = make(map[string]*networkData)
for _, virtualIP := range task.NetworksAttachments {
if networkService, present := networkMap[virtualIP.Network.ID]; present {
if len(virtualIP.Addresses) > 0 {
// Not sure about this next loop - when would a task have multiple IP's for the same network?
for _, addr := range virtualIP.Addresses {
ip, _, _ := net.ParseCIDR(addr)
network := &networkData{
ID: virtualIP.Network.ID,
Name: networkService.Name,
Addr: ip.String(),
}
dData.NetworkSettings.Networks[network.Name] = network
}
} else {
log.Debugf("No IP addresses found for network %s", virtualIP.Network.ID)
}
}
}
}
return dData
}

View file

@ -1,8 +0,0 @@
// +build !windows
package docker
const (
// DockerAPIVersion is a constant holding the version of the Provider API traefik will use
DockerAPIVersion = "1.21"
)

View file

@ -1,6 +0,0 @@
package docker
const (
// DockerAPIVersion is a constant holding the version of the Provider API traefik will use
DockerAPIVersion string = "1.24"
)

View file

@ -1,397 +0,0 @@
package docker
import (
"context"
"strconv"
"testing"
"time"
"github.com/davecgh/go-spew/spew"
docker "github.com/docker/docker/api/types"
dockertypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
dockerclient "github.com/docker/docker/client"
"github.com/stretchr/testify/assert"
)
type fakeTasksClient struct {
dockerclient.APIClient
tasks []swarm.Task
container dockertypes.ContainerJSON
err error
}
func (c *fakeTasksClient) TaskList(ctx context.Context, options dockertypes.TaskListOptions) ([]swarm.Task, error) {
return c.tasks, c.err
}
func (c *fakeTasksClient) ContainerInspect(ctx context.Context, container string) (dockertypes.ContainerJSON, error) {
return c.container, c.err
}
func TestListTasks(t *testing.T) {
testCases := []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),
taskNetworkAttachment("1", "network1", "overlay", []string{"127.0.0.1"}),
taskStatus(taskState(swarm.TaskStateRunning)),
),
swarmTask("id2",
taskSlot(2),
taskNetworkAttachment("1", "network1", "overlay", []string{"127.0.0.2"}),
taskStatus(taskState(swarm.TaskStatePending)),
),
swarmTask("id3",
taskSlot(3),
taskNetworkAttachment("1", "network1", "overlay", []string{"127.0.0.3"}),
),
swarmTask("id4",
taskSlot(4),
taskNetworkAttachment("1", "network1", "overlay", []string{"127.0.0.4"}),
taskStatus(taskState(swarm.TaskStateRunning)),
),
swarmTask("id5",
taskSlot(5),
taskNetworkAttachment("1", "network1", "overlay", []string{"127.0.0.5"}),
taskStatus(taskState(swarm.TaskStateFailed)),
),
},
isGlobalSVC: false,
expectedTasks: []string{
"container.1",
"container.4",
},
networks: map[string]*docker.NetworkResource{
"1": {
Name: "foo",
},
},
},
}
for caseID, test := range testCases {
test := test
t.Run(strconv.Itoa(caseID), func(t *testing.T) {
t.Parallel()
dockerData := parseService(test.service, test.networks)
dockerClient := &fakeTasksClient{tasks: test.tasks}
taskDockerData, _ := listTasks(context.Background(), dockerClient, test.service.ID, dockerData, test.networks, test.isGlobalSVC)
if len(test.expectedTasks) != len(taskDockerData) {
t.Errorf("expected tasks %v, got %v", spew.Sdump(test.expectedTasks), spew.Sdump(taskDockerData))
}
for i, taskID := range test.expectedTasks {
if taskDockerData[i].Name != taskID {
t.Errorf("expect task id %v, got %v", taskID, taskDockerData[i].Name)
}
}
})
}
}
type fakeServicesClient struct {
dockerclient.APIClient
dockerVersion string
networks []dockertypes.NetworkResource
services []swarm.Service
tasks []swarm.Task
err error
}
func (c *fakeServicesClient) ServiceList(ctx context.Context, options dockertypes.ServiceListOptions) ([]swarm.Service, error) {
return c.services, c.err
}
func (c *fakeServicesClient) ServerVersion(ctx context.Context) (dockertypes.Version, error) {
return dockertypes.Version{APIVersion: c.dockerVersion}, c.err
}
func (c *fakeServicesClient) NetworkList(ctx context.Context, options dockertypes.NetworkListOptions) ([]dockertypes.NetworkResource, error) {
return c.networks, c.err
}
func (c *fakeServicesClient) TaskList(ctx context.Context, options dockertypes.TaskListOptions) ([]swarm.Task, error) {
return c.tasks, c.err
}
func TestListServices(t *testing.T) {
testCases := []struct {
desc string
services []swarm.Service
tasks []swarm.Task
dockerVersion string
networks []dockertypes.NetworkResource
expectedServices []string
}{
{
desc: "Should return no service due to no networks defined",
services: []swarm.Service{
swarmService(
serviceName("service1"),
serviceLabels(map[string]string{
labelDockerNetwork: "barnet",
labelBackendLoadBalancerSwarm: "true",
}),
withEndpointSpec(modeVIP),
withEndpoint(
virtualIP("1", "10.11.12.13/24"),
virtualIP("2", "10.11.12.99/24"),
)),
swarmService(
serviceName("service2"),
serviceLabels(map[string]string{
labelDockerNetwork: "barnet",
labelBackendLoadBalancerSwarm: "true",
}),
withEndpointSpec(modeDNSSR)),
},
dockerVersion: "1.30",
networks: []dockertypes.NetworkResource{},
expectedServices: []string{},
},
{
desc: "Should return only service1",
services: []swarm.Service{
swarmService(
serviceName("service1"),
serviceLabels(map[string]string{
labelDockerNetwork: "barnet",
labelBackendLoadBalancerSwarm: "true",
}),
withEndpointSpec(modeVIP),
withEndpoint(
virtualIP("yk6l57rfwizjzxxzftn4amaot", "10.11.12.13/24"),
virtualIP("2", "10.11.12.99/24"),
)),
swarmService(
serviceName("service2"),
serviceLabels(map[string]string{
labelDockerNetwork: "barnet",
labelBackendLoadBalancerSwarm: "true",
}),
withEndpointSpec(modeDNSSR)),
},
dockerVersion: "1.30",
networks: []dockertypes.NetworkResource{
{
Name: "network_name",
ID: "yk6l57rfwizjzxxzftn4amaot",
Created: time.Now(),
Scope: "swarm",
Driver: "overlay",
EnableIPv6: false,
Internal: true,
Ingress: false,
ConfigOnly: false,
Options: map[string]string{
"com.docker.network.driver.overlay.vxlanid_list": "4098",
"com.docker.network.enable_ipv6": "false",
},
Labels: map[string]string{
"com.docker.stack.namespace": "test",
},
},
},
expectedServices: []string{
"service1",
},
},
{
desc: "Should return service1 and service2",
services: []swarm.Service{
swarmService(
serviceName("service1"),
serviceLabels(map[string]string{
labelDockerNetwork: "barnet",
}),
withEndpointSpec(modeVIP),
withEndpoint(
virtualIP("yk6l57rfwizjzxxzftn4amaot", "10.11.12.13/24"),
virtualIP("2", "10.11.12.99/24"),
)),
swarmService(
serviceName("service2"),
serviceLabels(map[string]string{
labelDockerNetwork: "barnet",
}),
withEndpointSpec(modeDNSSR)),
},
tasks: []swarm.Task{
swarmTask("id1",
taskNetworkAttachment("yk6l57rfwizjzxxzftn4amaot", "network_name", "overlay", []string{"127.0.0.1"}),
taskStatus(taskState(swarm.TaskStateRunning)),
),
swarmTask("id2",
taskNetworkAttachment("yk6l57rfwizjzxxzftn4amaot", "network_name", "overlay", []string{"127.0.0.1"}),
taskStatus(taskState(swarm.TaskStateRunning)),
),
},
dockerVersion: "1.30",
networks: []dockertypes.NetworkResource{
{
Name: "network_name",
ID: "yk6l57rfwizjzxxzftn4amaot",
Created: time.Now(),
Scope: "swarm",
Driver: "overlay",
EnableIPv6: false,
Internal: true,
Ingress: false,
ConfigOnly: false,
Options: map[string]string{
"com.docker.network.driver.overlay.vxlanid_list": "4098",
"com.docker.network.enable_ipv6": "false",
},
Labels: map[string]string{
"com.docker.stack.namespace": "test",
},
},
},
expectedServices: []string{
"service1.0",
"service1.0",
"service2.0",
"service2.0",
},
},
}
for caseID, test := range testCases {
test := test
t.Run(strconv.Itoa(caseID), func(t *testing.T) {
t.Parallel()
dockerClient := &fakeServicesClient{services: test.services, tasks: test.tasks, dockerVersion: test.dockerVersion, networks: test.networks}
serviceDockerData, err := listServices(context.Background(), dockerClient)
assert.NoError(t, err)
assert.Equal(t, len(test.expectedServices), len(serviceDockerData))
for i, serviceName := range test.expectedServices {
assert.Equal(t, serviceName, serviceDockerData[i].Name)
}
})
}
}
func TestSwarmTaskParsing(t *testing.T) {
testCases := []struct {
service swarm.Service
tasks []swarm.Task
isGlobalSVC bool
expected map[string]dockerData
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,
expected: map[string]dockerData{
"id1": {
Name: "container.1",
},
"id2": {
Name: "container.2",
},
"id3": {
Name: "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,
expected: map[string]dockerData{
"id1": {
Name: "container.id1",
},
"id2": {
Name: "container.id2",
},
"id3": {
Name: "container.id3",
},
},
networks: map[string]*docker.NetworkResource{
"1": {
Name: "foo",
},
},
},
{
service: swarmService(
serviceName("container"),
withEndpointSpec(modeVIP),
withEndpoint(
virtualIP("1", ""),
),
),
tasks: []swarm.Task{
swarmTask(
"id1",
taskNetworkAttachment("1", "vlan", "macvlan", []string{"127.0.0.1"}),
taskStatus(
taskState(swarm.TaskStateRunning),
taskContainerStatus("c1"),
),
),
},
isGlobalSVC: true,
expected: map[string]dockerData{
"id1": {
Name: "container.id1",
NetworkSettings: networkSettings{
Networks: map[string]*networkData{
"vlan": {
Name: "vlan",
Addr: "10.11.12.13",
},
},
},
},
},
networks: map[string]*docker.NetworkResource{
"1": {
Name: "vlan",
},
},
},
}
for caseID, test := range testCases {
test := test
t.Run(strconv.Itoa(caseID), func(t *testing.T) {
t.Parallel()
dData := parseService(test.service, test.networks)
for _, task := range test.tasks {
taskDockerData := parseTasks(task, dData, test.networks, test.isGlobalSVC)
expected := test.expected[task.ID]
assert.Equal(t, expected.Name, taskDockerData.Name)
}
})
}
}

View file

@ -1,253 +0,0 @@
package file
import (
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"text/template"
"github.com/containous/traefik/old/log"
"github.com/containous/traefik/old/provider"
"github.com/containous/traefik/old/types"
"github.com/containous/traefik/safe"
"github.com/containous/traefik/tls"
"github.com/pkg/errors"
"gopkg.in/fsnotify.v1"
)
var _ provider.Provider = (*Provider)(nil)
// Provider holds configurations of the provider.
type Provider struct {
provider.BaseProvider `mapstructure:",squash" export:"true"`
Directory string `description:"Load configuration from one or more .toml files in a directory" export:"true"`
TraefikFile string
}
// Init the provider
func (p *Provider) Init(constraints types.Constraints) error {
return p.BaseProvider.Init(constraints)
}
// Provide allows the file provider to provide configurations to traefik
// using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error {
configuration, err := p.BuildConfiguration()
if err != nil {
return err
}
if p.Watch {
var watchItem string
if len(p.Directory) > 0 {
watchItem = p.Directory
} else if len(p.Filename) > 0 {
watchItem = filepath.Dir(p.Filename)
} else {
watchItem = filepath.Dir(p.TraefikFile)
}
if err := p.addWatcher(pool, watchItem, configurationChan, p.watcherCallback); err != nil {
return err
}
}
sendConfigToChannel(configurationChan, configuration)
return nil
}
// BuildConfiguration loads configuration either from file or a directory specified by 'Filename'/'Directory'
// and returns a 'Configuration' object
func (p *Provider) BuildConfiguration() (*types.Configuration, error) {
if len(p.Directory) > 0 {
return p.loadFileConfigFromDirectory(p.Directory, nil)
}
if len(p.Filename) > 0 {
return p.loadFileConfig(p.Filename, true)
}
if len(p.TraefikFile) > 0 {
return p.loadFileConfig(p.TraefikFile, false)
}
return nil, errors.New("error using file configuration backend, no filename defined")
}
func (p *Provider) addWatcher(pool *safe.Pool, directory string, configurationChan chan<- types.ConfigMessage, callback func(chan<- types.ConfigMessage, fsnotify.Event)) error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("error creating file watcher: %s", err)
}
err = watcher.Add(directory)
if err != nil {
return fmt.Errorf("error adding file watcher: %s", err)
}
// Process events
pool.Go(func(stop chan bool) {
defer watcher.Close()
for {
select {
case <-stop:
return
case evt := <-watcher.Events:
if p.Directory == "" {
var filename string
if len(p.Filename) > 0 {
filename = p.Filename
} else {
filename = p.TraefikFile
}
_, evtFileName := filepath.Split(evt.Name)
_, confFileName := filepath.Split(filename)
if evtFileName == confFileName {
callback(configurationChan, evt)
}
} else {
callback(configurationChan, evt)
}
case err := <-watcher.Errors:
log.Errorf("Watcher event error: %s", err)
}
}
})
return nil
}
func (p *Provider) watcherCallback(configurationChan chan<- types.ConfigMessage, event fsnotify.Event) {
watchItem := p.TraefikFile
if len(p.Directory) > 0 {
watchItem = p.Directory
} else if len(p.Filename) > 0 {
watchItem = p.Filename
}
if _, err := os.Stat(watchItem); err != nil {
log.Debugf("Unable to watch %s : %v", watchItem, err)
return
}
configuration, err := p.BuildConfiguration()
if err != nil {
log.Errorf("Error occurred during watcher callback: %s", err)
return
}
sendConfigToChannel(configurationChan, configuration)
}
func sendConfigToChannel(configurationChan chan<- types.ConfigMessage, configuration *types.Configuration) {
configurationChan <- types.ConfigMessage{
ProviderName: "file",
Configuration: configuration,
}
}
func readFile(filename string) (string, error) {
if len(filename) > 0 {
buf, err := ioutil.ReadFile(filename)
if err != nil {
return "", err
}
return string(buf), nil
}
return "", fmt.Errorf("invalid filename: %s", filename)
}
func (p *Provider) loadFileConfig(filename string, parseTemplate bool) (*types.Configuration, error) {
fileContent, err := readFile(filename)
if err != nil {
return nil, fmt.Errorf("error reading configuration file: %s - %s", filename, err)
}
var configuration *types.Configuration
if parseTemplate {
configuration, err = p.CreateConfiguration(fileContent, template.FuncMap{}, false)
} else {
configuration, err = p.DecodeConfiguration(fileContent)
}
if err != nil {
return nil, err
}
if configuration == nil || configuration.Backends == nil && configuration.Frontends == nil && configuration.TLS == nil {
configuration = &types.Configuration{
Frontends: make(map[string]*types.Frontend),
Backends: make(map[string]*types.Backend),
}
}
return configuration, err
}
func (p *Provider) loadFileConfigFromDirectory(directory string, configuration *types.Configuration) (*types.Configuration, error) {
fileList, err := ioutil.ReadDir(directory)
if err != nil {
return configuration, fmt.Errorf("unable to read directory %s: %v", directory, err)
}
if configuration == nil {
configuration = &types.Configuration{
Frontends: make(map[string]*types.Frontend),
Backends: make(map[string]*types.Backend),
}
}
configTLSMaps := make(map[*tls.Configuration]struct{})
for _, item := range fileList {
if item.IsDir() {
configuration, err = p.loadFileConfigFromDirectory(filepath.Join(directory, item.Name()), configuration)
if err != nil {
return configuration, fmt.Errorf("unable to load content configuration from subdirectory %s: %v", item, err)
}
continue
} else if !strings.HasSuffix(item.Name(), ".toml") && !strings.HasSuffix(item.Name(), ".tmpl") {
continue
}
var c *types.Configuration
c, err = p.loadFileConfig(path.Join(directory, item.Name()), true)
if err != nil {
return configuration, err
}
for backendName, backend := range c.Backends {
if _, exists := configuration.Backends[backendName]; exists {
log.Warnf("Backend %s already configured, skipping", backendName)
} else {
configuration.Backends[backendName] = backend
}
}
for frontendName, frontend := range c.Frontends {
if _, exists := configuration.Frontends[frontendName]; exists {
log.Warnf("Frontend %s already configured, skipping", frontendName)
} else {
configuration.Frontends[frontendName] = frontend
}
}
for _, conf := range c.TLS {
if _, exists := configTLSMaps[conf]; exists {
log.Warnf("TLS Configuration %v already configured, skipping", conf)
} else {
configTLSMaps[conf] = struct{}{}
}
}
}
for conf := range configTLSMaps {
configuration.TLS = append(configuration.TLS, conf)
}
return configuration, nil
}

View file

@ -1,338 +0,0 @@
package file
import (
"context"
"fmt"
"io/ioutil"
"os"
"path"
"testing"
"time"
"github.com/containous/traefik/old/types"
"github.com/containous/traefik/safe"
"github.com/stretchr/testify/assert"
)
// createRandomFile Helper
func createRandomFile(t *testing.T, tempDir string, contents ...string) *os.File {
return createFile(t, tempDir, fmt.Sprintf("temp%d.toml", time.Now().UnixNano()), contents...)
}
// createFile Helper
func createFile(t *testing.T, tempDir string, name string, contents ...string) *os.File {
t.Helper()
fileName := path.Join(tempDir, name)
tempFile, err := os.Create(fileName)
if err != nil {
t.Fatal(err)
}
for _, content := range contents {
_, err := tempFile.WriteString(content)
if err != nil {
t.Fatal(err)
}
}
err = tempFile.Close()
if err != nil {
t.Fatal(err)
}
return tempFile
}
// createTempDir Helper
func createTempDir(t *testing.T, dir string) string {
t.Helper()
d, err := ioutil.TempDir("", dir)
if err != nil {
t.Fatal(err)
}
return d
}
// createFrontendConfiguration Helper
func createFrontendConfiguration(n int) string {
conf := "[frontends]\n"
for i := 1; i <= n; i++ {
conf += fmt.Sprintf(` [frontends."frontend%[1]d"]
backend = "backend%[1]d"
`, i)
}
return conf
}
// createBackendConfiguration Helper
func createBackendConfiguration(n int) string {
conf := "[backends]\n"
for i := 1; i <= n; i++ {
conf += fmt.Sprintf(` [backends.backend%[1]d]
[backends.backend%[1]d.servers.server1]
url = "http://172.17.0.%[1]d:80"
`, i)
}
return conf
}
// createTLS Helper
func createTLS(n int) string {
var conf string
for i := 1; i <= n; i++ {
conf += fmt.Sprintf(`[[TLS]]
EntryPoints = ["https"]
[TLS.Certificate]
CertFile = "integration/fixtures/https/snitest%[1]d.com.cert"
KeyFile = "integration/fixtures/https/snitest%[1]d.com.key"
`, i)
}
return conf
}
type ProvideTestCase struct {
desc string
directoryContent []string
fileContent string
traefikFileContent string
expectedNumFrontend int
expectedNumBackend int
expectedNumTLSConf int
}
func getTestCases() []ProvideTestCase {
return []ProvideTestCase{
{
desc: "simple file",
fileContent: createFrontendConfiguration(2) + createBackendConfiguration(3) + createTLS(4),
expectedNumFrontend: 2,
expectedNumBackend: 3,
expectedNumTLSConf: 4,
},
{
desc: "simple file and a traefik file",
fileContent: createFrontendConfiguration(2) + createBackendConfiguration(3) + createTLS(4),
traefikFileContent: `
debug=true
`,
expectedNumFrontend: 2,
expectedNumBackend: 3,
expectedNumTLSConf: 4,
},
{
desc: "template file",
fileContent: `
[frontends]
{{ range $i, $e := until 20 }}
[frontends.frontend{{ $e }}]
backend = "backend"
{{ end }}
`,
expectedNumFrontend: 20,
},
{
desc: "simple directory",
directoryContent: []string{
createFrontendConfiguration(2),
createBackendConfiguration(3),
createTLS(4),
},
expectedNumFrontend: 2,
expectedNumBackend: 3,
expectedNumTLSConf: 4,
},
{
desc: "template in directory",
directoryContent: []string{
`
[frontends]
{{ range $i, $e := until 20 }}
[frontends.frontend{{ $e }}]
backend = "backend"
{{ end }}
`,
`
[backends]
{{ range $i, $e := until 20 }}
[backends.backend{{ $e }}]
[backends.backend{{ $e }}.servers.server1]
url="http://127.0.0.1"
{{ end }}
`,
},
expectedNumFrontend: 20,
expectedNumBackend: 20,
},
{
desc: "simple traefik file",
traefikFileContent: `
debug=true
[file]
` + createFrontendConfiguration(2) + createBackendConfiguration(3) + createTLS(4),
expectedNumFrontend: 2,
expectedNumBackend: 3,
expectedNumTLSConf: 4,
},
{
desc: "simple traefik file with templating",
traefikFileContent: `
temp="{{ getTag \"test\" }}"
[file]
` + createFrontendConfiguration(2) + createBackendConfiguration(3) + createTLS(4),
expectedNumFrontend: 2,
expectedNumBackend: 3,
expectedNumTLSConf: 4,
},
}
}
func TestProvideWithoutWatch(t *testing.T) {
for _, test := range getTestCases() {
test := test
t.Run(test.desc+" without watch", func(t *testing.T) {
t.Parallel()
provider, clean := createProvider(t, test, false)
defer clean()
configChan := make(chan types.ConfigMessage)
go func() {
err := provider.Provide(configChan, safe.NewPool(context.Background()))
assert.NoError(t, err)
}()
timeout := time.After(time.Second)
select {
case config := <-configChan:
assert.Len(t, config.Configuration.Backends, test.expectedNumBackend)
assert.Len(t, config.Configuration.Frontends, test.expectedNumFrontend)
assert.Len(t, config.Configuration.TLS, test.expectedNumTLSConf)
case <-timeout:
t.Errorf("timeout while waiting for config")
}
})
}
}
func TestProvideWithWatch(t *testing.T) {
for _, test := range getTestCases() {
test := test
t.Run(test.desc+" with watch", func(t *testing.T) {
t.Parallel()
provider, clean := createProvider(t, test, true)
defer clean()
configChan := make(chan types.ConfigMessage)
go func() {
err := provider.Provide(configChan, safe.NewPool(context.Background()))
assert.NoError(t, err)
}()
timeout := time.After(time.Second)
select {
case config := <-configChan:
assert.Len(t, config.Configuration.Backends, 0)
assert.Len(t, config.Configuration.Frontends, 0)
assert.Len(t, config.Configuration.TLS, 0)
case <-timeout:
t.Errorf("timeout while waiting for config")
}
if len(test.fileContent) > 0 {
if err := ioutil.WriteFile(provider.Filename, []byte(test.fileContent), 0755); err != nil {
t.Error(err)
}
}
if len(test.traefikFileContent) > 0 {
if err := ioutil.WriteFile(provider.TraefikFile, []byte(test.traefikFileContent), 0755); err != nil {
t.Error(err)
}
}
if len(test.directoryContent) > 0 {
for _, fileContent := range test.directoryContent {
createRandomFile(t, provider.Directory, fileContent)
}
}
timeout = time.After(time.Second * 1)
var numUpdates, numBackends, numFrontends, numTLSConfs int
for {
select {
case config := <-configChan:
numUpdates++
numBackends = len(config.Configuration.Backends)
numFrontends = len(config.Configuration.Frontends)
numTLSConfs = len(config.Configuration.TLS)
t.Logf("received update #%d: backends %d/%d, frontends %d/%d, TLS configs %d/%d", numUpdates, numBackends, test.expectedNumBackend, numFrontends, test.expectedNumFrontend, numTLSConfs, test.expectedNumTLSConf)
if numBackends == test.expectedNumBackend && numFrontends == test.expectedNumFrontend && numTLSConfs == test.expectedNumTLSConf {
return
}
case <-timeout:
t.Fatal("timeout while waiting for config")
}
}
})
}
}
func TestErrorWhenEmptyConfig(t *testing.T) {
provider := &Provider{}
configChan := make(chan types.ConfigMessage)
errorChan := make(chan struct{})
go func() {
err := provider.Provide(configChan, safe.NewPool(context.Background()))
assert.Error(t, err)
close(errorChan)
}()
timeout := time.After(time.Second)
select {
case <-configChan:
t.Fatal("We should not receive config message")
case <-timeout:
t.Fatal("timeout while waiting for config")
case <-errorChan:
}
}
func createProvider(t *testing.T, test ProvideTestCase, watch bool) (*Provider, func()) {
tempDir := createTempDir(t, "testdir")
provider := &Provider{}
provider.Watch = watch
if len(test.directoryContent) > 0 {
if !watch {
for _, fileContent := range test.directoryContent {
createRandomFile(t, tempDir, fileContent)
}
}
provider.Directory = tempDir
}
if len(test.fileContent) > 0 {
if watch {
test.fileContent = ""
}
filename := createRandomFile(t, tempDir, test.fileContent)
provider.Filename = filename.Name()
}
if len(test.traefikFileContent) > 0 {
if watch {
test.traefikFileContent = ""
}
filename := createRandomFile(t, tempDir, test.traefikFileContent)
provider.TraefikFile = filename.Name()
}
return provider, func() {
os.Remove(tempDir)
}
}