1
0
Fork 0

Support containers sharing a network namespace (issue #47)

This commit is contained in:
badblocks 2026-09-01 16:11:31 -07:00
parent 8bd9f06f12
commit 463e2023c8
Signed by: badblocks
SSH key fingerprint: SHA256:hEcM6BP4hKm9F7WsomNuXSBpPn2BSDnFzPSl3y1NerQ
8 changed files with 404 additions and 16 deletions

View file

@ -38,6 +38,8 @@ class ContainerEnv:
env_vars["lookup_label"] = os.getenv("EASYHAPROXY_LABEL_PREFIX") if os.getenv(
"EASYHAPROXY_LABEL_PREFIX") else "easyhaproxy"
env_vars["host_network_ip"] = os.getenv("EASYHAPROXY_HOST_NETWORK_IP", "")
env_vars["logLevel"] = {
"easyhaproxy": os.getenv("EASYHAPROXY_LOG_LEVEL") if os.getenv(
"EASYHAPROXY_LOG_LEVEL") else Functions.DEBUG,

View file

@ -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)