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)