Some checks failed
Docker / Test (push) Has been cancelled
Docker / Tests-E2E-Docker (push) Has been cancelled
Docker / Tests-E2E-Kubernetes (push) Has been cancelled
Docker / Tests-E2E-Additional (push) Has been cancelled
Docker / Tests-E2E-Swarm (push) Has been cancelled
Docker / Build (push) Has been cancelled
Docker / Publish-PyPI (push) Has been cancelled
Docker / Helm (push) Has been cancelled
Docker / HelmDeploy (push) Has been cancelled
Docker / Documentation (push) Has been cancelled
159 lines
6.7 KiB
Python
159 lines
6.7 KiB
Python
import ipaddress
|
|
import socket
|
|
|
|
import docker
|
|
|
|
from functions import ContainerEnv, logger_easyhaproxy
|
|
|
|
from .interface import ProcessorInterface
|
|
|
|
|
|
class Docker(ProcessorInterface):
|
|
def __init__(self, filename=None):
|
|
self.parsed_object = None
|
|
self.client = docker.from_env()
|
|
super().__init__()
|
|
|
|
def inspect_network(self):
|
|
self.parsed_object = {}
|
|
own_container = None
|
|
try:
|
|
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
|
|
own_container = None
|
|
ha_proxy_network_name = self._first_attachable_network()
|
|
if ha_proxy_network_name is None:
|
|
return
|
|
|
|
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)
|
|
|
|
for container in self.client.containers.list():
|
|
if not any(self.label in key for key in container.labels):
|
|
continue
|
|
|
|
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)
|
|
|
|
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)
|