Support containers sharing a network namespace (issue #47)
This commit is contained in:
parent
8bd9f06f12
commit
463e2023c8
8 changed files with 404 additions and 16 deletions
|
|
@ -1,7 +1,10 @@
|
|||
import ipaddress
|
||||
import socket
|
||||
|
||||
import docker
|
||||
|
||||
from functions import ContainerEnv, logger_easyhaproxy
|
||||
|
||||
from .interface import ProcessorInterface
|
||||
|
||||
|
||||
|
|
@ -12,24 +15,142 @@ class Docker(ProcessorInterface):
|
|||
super().__init__()
|
||||
|
||||
def inspect_network(self):
|
||||
self.parsed_object = {}
|
||||
own_container = None
|
||||
try:
|
||||
ha_proxy_network_name = next(
|
||||
iter(self.client.containers.get(socket.gethostname()).attrs["NetworkSettings"]["Networks"]))
|
||||
own_container = self.client.containers.get(socket.gethostname())
|
||||
ha_proxy_network_name = next(iter(own_container.attrs["NetworkSettings"]["Networks"]))
|
||||
except Exception:
|
||||
# HAProxy is not running in a container, get first container network
|
||||
if len(self.client.containers.list()) == 0:
|
||||
own_container = None
|
||||
ha_proxy_network_name = self._first_attachable_network()
|
||||
if ha_proxy_network_name is None:
|
||||
return
|
||||
ha_proxy_network_name = next(iter(
|
||||
self.client.containers.get(self.client.containers.list()[0].name).attrs["NetworkSettings"]["Networks"]))
|
||||
|
||||
ha_proxy_network = self.client.networks.get(ha_proxy_network_name)
|
||||
host_address = self._host_gateway_address(own_container, ha_proxy_network, ha_proxy_network_name)
|
||||
|
||||
self.parsed_object = {}
|
||||
for container in self.client.containers.list():
|
||||
# Issue 32 - Docker container cannot connect to containers in different network.
|
||||
if ha_proxy_network_name not in container.attrs["NetworkSettings"]["Networks"].keys():
|
||||
ha_proxy_network.connect(container.name)
|
||||
container = self.client.containers.get(container.name) # refresh object
|
||||
ip_address = self._container_address(container, ha_proxy_network, ha_proxy_network_name, host_address)
|
||||
if ip_address is None:
|
||||
continue
|
||||
self._merge_labels(ip_address, container)
|
||||
|
||||
ip_address = container.attrs["NetworkSettings"]["Networks"][ha_proxy_network_name]["IPAddress"]
|
||||
self.parsed_object[ip_address] = container.labels
|
||||
def _first_attachable_network(self):
|
||||
# Containers sharing a network namespace have no network of their own to borrow.
|
||||
for container in self.client.containers.list():
|
||||
if self._network_mode(container) in ["host", "none"] or self._namespace_owner_id(container) is not None:
|
||||
continue
|
||||
networks = container.attrs["NetworkSettings"]["Networks"] or {}
|
||||
if networks:
|
||||
return next(iter(networks))
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _network_mode(container):
|
||||
return (container.attrs.get("HostConfig") or {}).get("NetworkMode", "")
|
||||
|
||||
@staticmethod
|
||||
def _namespace_owner_id(container):
|
||||
network_mode = Docker._network_mode(container)
|
||||
if network_mode.startswith("container:"):
|
||||
return network_mode.split(":", 1)[1]
|
||||
return None
|
||||
|
||||
def _container_address(self, container, ha_proxy_network, ha_proxy_network_name, host_address, seen=None):
|
||||
"""
|
||||
Resolve the address HAProxy must use to reach this container.
|
||||
Returns None when the container cannot be reached.
|
||||
"""
|
||||
# Issue 47 - Containers sharing the host network namespace cannot join any other network.
|
||||
if self._network_mode(container) == "host":
|
||||
if host_address is None:
|
||||
logger_easyhaproxy.warning(
|
||||
f"Container '{container.name}' uses the host network, but the address of the host could not be "
|
||||
f"determined. Set EASYHAPROXY_HOST_NETWORK_IP to fix it. Skipping it."
|
||||
)
|
||||
return host_address
|
||||
|
||||
owner_id = self._namespace_owner_id(container)
|
||||
if owner_id is not None:
|
||||
return self._namespace_owner_address(
|
||||
container, owner_id, ha_proxy_network, ha_proxy_network_name, host_address, seen
|
||||
)
|
||||
|
||||
networks = container.attrs["NetworkSettings"]["Networks"] or {}
|
||||
# Issue 32 - Docker container cannot connect to containers in different network.
|
||||
if ha_proxy_network_name not in networks.keys():
|
||||
try:
|
||||
ha_proxy_network.connect(container.name)
|
||||
except docker.errors.APIError as e:
|
||||
logger_easyhaproxy.warning(
|
||||
f"Container '{container.name}' could not be connected to the network "
|
||||
f"'{ha_proxy_network_name}': {e}. Skipping it."
|
||||
)
|
||||
return None
|
||||
container = self.client.containers.get(container.name) # refresh object
|
||||
networks = container.attrs["NetworkSettings"]["Networks"] or {}
|
||||
|
||||
return (networks.get(ha_proxy_network_name) or {}).get("IPAddress") or None
|
||||
|
||||
def _namespace_owner_address(self, container, owner_id, ha_proxy_network, ha_proxy_network_name, host_address,
|
||||
seen=None):
|
||||
"""
|
||||
Containers started with `network_mode: container:xxx` are reachable at the address of the
|
||||
container owning the network namespace.
|
||||
"""
|
||||
seen = seen or set()
|
||||
if container.id in seen:
|
||||
logger_easyhaproxy.warning(
|
||||
f"Container '{container.name}' has a circular network namespace reference. Skipping it."
|
||||
)
|
||||
return None
|
||||
seen.add(container.id)
|
||||
|
||||
try:
|
||||
owner = self.client.containers.get(owner_id)
|
||||
except docker.errors.NotFound:
|
||||
logger_easyhaproxy.warning(
|
||||
f"Container '{container.name}' shares the network namespace of '{owner_id}', which was not found. "
|
||||
f"Skipping it."
|
||||
)
|
||||
return None
|
||||
|
||||
return self._container_address(owner, ha_proxy_network, ha_proxy_network_name, host_address, seen)
|
||||
|
||||
def _host_gateway_address(self, own_container, ha_proxy_network, ha_proxy_network_name):
|
||||
"""
|
||||
Address the host is reachable at from inside the HAProxy network. Every container sharing the
|
||||
host network namespace is served through it.
|
||||
"""
|
||||
host_network_ip = ContainerEnv.read()["host_network_ip"]
|
||||
if host_network_ip:
|
||||
return host_network_ip
|
||||
|
||||
if own_container is not None:
|
||||
gateway = (own_container.attrs["NetworkSettings"]["Networks"].get(ha_proxy_network_name) or {}).get(
|
||||
"Gateway")
|
||||
if gateway:
|
||||
return gateway
|
||||
|
||||
for config in (ha_proxy_network.attrs.get("IPAM") or {}).get("Config") or []:
|
||||
if config.get("Gateway"):
|
||||
return config["Gateway"]
|
||||
try:
|
||||
return str(next(ipaddress.ip_network(config["Subnet"]).hosts()))
|
||||
except (KeyError, ValueError, StopIteration):
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
def _merge_labels(self, ip_address, container):
|
||||
# Every container sharing the host network namespace resolves to the same address, so the
|
||||
# labels are merged instead of replaced.
|
||||
labels = self.parsed_object.setdefault(ip_address, {})
|
||||
for key, value in container.labels.items():
|
||||
if key.startswith(self.label) and key in labels and labels[key] != value:
|
||||
logger_easyhaproxy.warning(
|
||||
f"Container '{container.name}' redefines '{key}' for '{ip_address}' as '{value}' instead of "
|
||||
f"'{labels[key]}'. Give each container a unique definition name."
|
||||
)
|
||||
labels.update(container.labels)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue