From d50b6a34bc30c1004e9493f95644dcaf8c4c7cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Cro=C3=ABs?= Date: Thu, 19 Jul 2018 16:40:03 +0200 Subject: [PATCH] Uses both binded HostIP and HostPort when useBindPortIP=true --- provider/docker/config.go | 70 ++++++-- .../docker/config_container_docker_test.go | 163 +++++++++++++++++- .../docker/config_container_swarm_test.go | 2 +- provider/docker/deprecated_config.go | 61 ++++++- .../deprecated_container_docker_test.go | 2 +- .../docker/deprecated_container_swarm_test.go | 2 +- 6 files changed, 279 insertions(+), 21 deletions(-) diff --git a/provider/docker/config.go b/provider/docker/config.go index 002d1f115..546e34c1e 100644 --- a/provider/docker/config.go +++ b/provider/docker/config.go @@ -33,7 +33,7 @@ func (p *Provider) buildConfigurationV2(containersInspected []dockerData) *types "getDomain": label.GetFuncString(label.TraefikDomain, p.Domain), // Backend functions - "getIPAddress": p.getIPAddress, + "getIPAddress": p.getDeprecatedIPAddress, // TODO: Should we expose getIPPort instead? "getServers": p.getServers, "getMaxConn": label.GetMaxConn, "getHealthCheck": label.GetHealthCheck, @@ -235,17 +235,6 @@ func (p Provider) getIPAddress(container dockerData) string { return p.getIPAddress(parseContainer(containerInspected)) } - if p.UseBindPortIP { - port := getPortV1(container) - for netPort, portBindings := range container.NetworkSettings.Ports { - if string(netPort) == port+"/TCP" || string(netPort) == port+"/UDP" { - for _, p := range portBindings { - return p.HostIP - } - } - } - } - for _, network := range container.NetworkSettings.Networks { return network.Addr } @@ -254,6 +243,16 @@ func (p Provider) getIPAddress(container dockerData) string { 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) @@ -322,13 +321,53 @@ func getPort(container dockerData) string { 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 + + if p.UseBindPortIP { + portBinding, err := p.getPortBinding(container) + if err != nil { + return "", "", fmt.Errorf("unable to find a binding for the container %q: ignoring server", container.Name) + } + + if portBinding.HostIP == "0.0.0.0" { + return "", "", fmt.Errorf("cannot determine the IP address (got 0.0.0.0) for the container %q: ignoring server", container.Name) + } + + ip = portBinding.HostIP + port = portBinding.HostPort + + } else { + 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 := p.getIPAddress(container) - if len(ip) == 0 { - log.Warnf("Unable to find the IP address for the container %q: the server is ignored.", container.Name) + ip, port, err := p.getIPPort(container) + if err != nil { + log.Warn(err) continue } @@ -337,7 +376,6 @@ func (p *Provider) getServers(containers []dockerData) map[string]types.Server { } protocol := label.GetStringValue(container.SegmentLabels, label.TraefikProtocol, label.DefaultProtocol) - port := getPort(container) serverURL := fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(ip, port)) diff --git a/provider/docker/config_container_docker_test.go b/provider/docker/config_container_docker_test.go index 457a195ce..6e340c74a 100644 --- a/provider/docker/config_container_docker_test.go +++ b/provider/docker/config_container_docker_test.go @@ -1287,12 +1287,173 @@ func TestDockerGetIPAddress(t *testing.T) { Network: "webnet", } - actual := provider.getIPAddress(dData) + actual := provider.getDeprecatedIPAddress(dData) assert.Equal(t, test.expected, actual) }) } } +func TestDockerGetIPPort(t *testing.T) { + testCases := []struct { + desc string + container docker.ContainerJSON + ip, port string + expectsError bool + }{ + { + desc: "label traefik.port not set, binding with ip:port should create a route to the bound ip:port", + container: containerJSON( + ports(nat.PortMap{ + "80/tcp": []nat.PortBinding{ + { + HostIP: "1.2.3.4", + HostPort: "8081", + }, + }, + }), + withNetwork("testnet", ipv4("10.11.12.13"))), + ip: "1.2.3.4", + port: "8081", + }, + { + desc: "label traefik.port set, multiple bindings on different ports, uses the label to select the correct (first) binding", + container: containerJSON( + labels(map[string]string{ + label.TraefikPort: "80", + }), + ports(nat.PortMap{ + "80/tcp": []nat.PortBinding{ + { + HostIP: "1.2.3.4", + HostPort: "8081", + }, + }, + "443/tcp": []nat.PortBinding{ + { + HostIP: "5.6.7.8", + HostPort: "8082", + }, + }, + }), + withNetwork("testnet", ipv4("10.11.12.13"))), + ip: "1.2.3.4", + port: "8081", + }, + { + desc: "label traefik.port set, multiple bindings on different ports, uses the label to select the correct (second) binding", + container: containerJSON( + labels(map[string]string{ + label.TraefikPort: "443", + }), + ports(nat.PortMap{ + "80/tcp": []nat.PortBinding{ + { + HostIP: "1.2.3.4", + HostPort: "8081", + }, + }, + "443/tcp": []nat.PortBinding{ + { + HostIP: "5.6.7.8", + HostPort: "8082", + }, + }, + }), + withNetwork("testnet", ipv4("10.11.12.13"))), + ip: "5.6.7.8", + port: "8082", + }, + { + desc: "label traefik.port set, single binding with ip:port for the label, creates the route", + container: containerJSON( + labels(map[string]string{ + label.TraefikPort: "443", + }), + ports(nat.PortMap{ + "443/tcp": []nat.PortBinding{ + { + HostIP: "5.6.7.8", + HostPort: "8082", + }, + }, + }), + withNetwork("testnet", ipv4("10.11.12.13"))), + ip: "5.6.7.8", + port: "8082", + }, + { + desc: "label traefik.port not set, single binding with port only, server ignored", + container: containerJSON( + ports(nat.PortMap{ + "80/tcp": []nat.PortBinding{ + { + HostPort: "8082", + }, + }, + }), + withNetwork("testnet", ipv4("10.11.12.13"))), + expectsError: true, + }, + { + desc: "label traefik.port not set, no binding, server ignored", + container: containerJSON( + withNetwork("testnet", ipv4("10.11.12.13"))), + expectsError: true, + }, + { + desc: "label traefik.port set, no binding on the corresponding port, server ignored", + container: containerJSON( + labels(map[string]string{ + label.TraefikPort: "80", + }), + ports(nat.PortMap{ + "443/tcp": []nat.PortBinding{ + { + HostIP: "5.6.7.8", + HostPort: "8082", + }, + }, + }), + withNetwork("testnet", ipv4("10.11.12.13"))), + expectsError: true, + }, + { + desc: "label traefik.port set, no binding, server ignored", + container: containerJSON( + labels(map[string]string{ + label.TraefikPort: "80", + }), + withNetwork("testnet", ipv4("10.11.12.13"))), + expectsError: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + segmentProperties := label.ExtractTraefikLabels(dData.Labels) + dData.SegmentLabels = segmentProperties[""] + + provider := &Provider{ + Network: "webnet", + UseBindPortIP: true, + } + + actualIP, actualPort, actualError := provider.getIPPort(dData) + if test.expectsError { + require.Error(t, actualError) + } else { + require.NoError(t, actualError) + } + assert.Equal(t, test.ip, actualIP) + assert.Equal(t, test.port, actualPort) + }) + } +} + func TestDockerGetPort(t *testing.T) { testCases := []struct { container docker.ContainerJSON diff --git a/provider/docker/config_container_swarm_test.go b/provider/docker/config_container_swarm_test.go index ced5b0dc2..a8cbdca4e 100644 --- a/provider/docker/config_container_swarm_test.go +++ b/provider/docker/config_container_swarm_test.go @@ -933,7 +933,7 @@ func TestSwarmGetIPAddress(t *testing.T) { segmentProperties := label.ExtractTraefikLabels(dData.Labels) dData.SegmentLabels = segmentProperties[""] - actual := provider.getIPAddress(dData) + actual := provider.getDeprecatedIPAddress(dData) assert.Equal(t, test.expected, actual) }) } diff --git a/provider/docker/deprecated_config.go b/provider/docker/deprecated_config.go index 0954466a1..32f90c1f6 100644 --- a/provider/docker/deprecated_config.go +++ b/provider/docker/deprecated_config.go @@ -1,8 +1,10 @@ package docker import ( + "context" "math" "strconv" + "strings" "text/template" "github.com/BurntSushi/ty/fun" @@ -19,7 +21,7 @@ func (p *Provider) buildConfigurationV1(containersInspected []dockerData) *types "isBackendLBSwarm": isBackendLBSwarm, // Backend functions - "getIPAddress": p.getIPAddress, + "getIPAddress": p.getIPAddressV1, "getPort": getPortV1, "getWeight": getFuncIntLabelV1(label.TraefikWeight, label.DefaultWeight), "getProtocol": getFuncStringLabelV1(label.TraefikProtocol, label.DefaultProtocol), @@ -202,3 +204,60 @@ func (p Provider) containerFilterV1(container dockerData) bool { return true } + +func (p Provider) getIPAddressV1(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)) + } + + if p.UseBindPortIP { + port := getPortV1(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.HostIP + } + } + } + } + + for _, network := range container.NetworkSettings.Networks { + return network.Addr + } + + log.Warnf("Unable to find the IP address for the container %q.", container.Name) + return "" +} diff --git a/provider/docker/deprecated_container_docker_test.go b/provider/docker/deprecated_container_docker_test.go index 67771473e..b7359980c 100644 --- a/provider/docker/deprecated_container_docker_test.go +++ b/provider/docker/deprecated_container_docker_test.go @@ -898,7 +898,7 @@ func TestDockerGetIPAddressV1(t *testing.T) { t.Parallel() dData := parseContainer(test.container) provider := &Provider{} - actual := provider.getIPAddress(dData) + actual := provider.getDeprecatedIPAddress(dData) if actual != test.expected { t.Errorf("expected %q, got %q", test.expected, actual) } diff --git a/provider/docker/deprecated_container_swarm_test.go b/provider/docker/deprecated_container_swarm_test.go index 7803e91b0..d53adf1ea 100644 --- a/provider/docker/deprecated_container_swarm_test.go +++ b/provider/docker/deprecated_container_swarm_test.go @@ -667,7 +667,7 @@ func TestSwarmGetIPAddressV1(t *testing.T) { SwarmMode: true, } - actual := provider.getIPAddress(dData) + actual := provider.getDeprecatedIPAddress(dData) if actual != test.expected { t.Errorf("expected %q, got %q", test.expected, actual) }