Compare commits
4 commits
8bd9f06f12
...
1302425d09
| Author | SHA1 | Date | |
|---|---|---|---|
| 1302425d09 | |||
| 53104905b9 | |||
| e7a0f7b930 | |||
| 463e2023c8 |
8 changed files with 450 additions and 16 deletions
|
|
@ -8,9 +8,7 @@ sidebar_label: "Limitations"
|
||||||
## EasyHAProxy will not work with --network=host
|
## EasyHAProxy will not work with --network=host
|
||||||
|
|
||||||
:::danger Network Mode Incompatibility
|
:::danger Network Mode Incompatibility
|
||||||
The `--network=host` option **cannot** be used with EasyHAProxy due to its networking requirements.
|
The `--network=host` option **cannot** be used with the EasyHAProxy container itself due to its networking requirements.
|
||||||
|
|
||||||
EasyHAProxy needs to inspect and interact with Docker containers from within the Docker network where it's running. Using the `--network=host` option bypasses Docker networking, preventing EasyHAProxy from accessing and configuring containers effectively.
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Considerations for Multiple Replica Deployments in EasyHAProxy
|
## Considerations for Multiple Replica Deployments in EasyHAProxy
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ EasyHAProxy inspects running Docker containers, reads their labels, and configur
|
||||||
|
|
||||||
:::warning Limitations
|
:::warning Limitations
|
||||||
- You cannot mix Docker containers with Swarm containers.
|
- You cannot mix Docker containers with Swarm containers.
|
||||||
- This method does not work with containers that use the `--network=host` option. See [limitations](../concepts/limitations.md) for details.
|
- EasyHAProxy itself cannot run with the `--network=host` option. See [limitations](../concepts/limitations.md) for details.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Step 1 — Create a shared network
|
## Step 1 — Create a shared network
|
||||||
|
|
@ -49,6 +49,32 @@ docker run -d \
|
||||||
|
|
||||||
EasyHAProxy detects this container automatically and routes traffic from `example.org:80` to port 8080 in your container. You do not need to expose any container ports.
|
EasyHAProxy detects this container automatically and routes traffic from `example.org:80` to port 8080 in your container. You do not need to expose any container ports.
|
||||||
|
|
||||||
|
### Containers using `--network=host`
|
||||||
|
|
||||||
|
Containers that share the host network namespace (`--network=host`, or `network_mode: host` in
|
||||||
|
Compose) cannot join the EasyHAProxy network, so EasyHAProxy reaches them through the gateway of
|
||||||
|
its own network instead. Label them as usual, but set `localport` to the port the service binds
|
||||||
|
**on the host**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
myapp:
|
||||||
|
image: my/image:tag
|
||||||
|
network_mode: host
|
||||||
|
labels:
|
||||||
|
easyhaproxy.myapp.host: example.org
|
||||||
|
easyhaproxy.myapp.port: 80
|
||||||
|
easyhaproxy.myapp.localport: 8080 # the port on the host
|
||||||
|
```
|
||||||
|
|
||||||
|
The same applies to containers sharing another container's namespace
|
||||||
|
(`network_mode: "container:xxx"`, as used by VPN sidecars); those are reached at the address of
|
||||||
|
the container owning the namespace.
|
||||||
|
|
||||||
|
The service must bind `0.0.0.0` rather than `127.0.0.1`, otherwise it is unreachable from the
|
||||||
|
EasyHAProxy container. If your host firewall blocks the Docker bridge, or the detected gateway is
|
||||||
|
not the address you want, override it with `EASYHAPROXY_HOST_NETWORK_IP`.
|
||||||
|
|
||||||
## Step 4 — Verify
|
## Step 4 — Verify
|
||||||
|
|
||||||
Open `http://example.org` in your browser (or `curl http://example.org`). Traffic should reach your container.
|
Open `http://example.org` in your browser (or `curl http://example.org`). Traffic should reach your container.
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ sidebar_label: "Environment Variables"
|
||||||
|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
|
|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
|
||||||
| EASYHAPROXY_DISCOVER | How the services will be discovered to create `haproxy.cfg`: `static`, `docker`, `swarm` or `kubernetes` | **required** |
|
| EASYHAPROXY_DISCOVER | How the services will be discovered to create `haproxy.cfg`: `static`, `docker`, `swarm` or `kubernetes` | **required** |
|
||||||
| EASYHAPROXY_LABEL_PREFIX | (Optional) The key will search for matching resources. | `easyhaproxy` |
|
| EASYHAPROXY_LABEL_PREFIX | (Optional) The key will search for matching resources. | `easyhaproxy` |
|
||||||
|
| EASYHAPROXY_HOST_NETWORK_IP | (Optional) Address used to reach containers running with `--network=host`. Defaults to the gateway of the EasyHAProxy network. | *auto* |
|
||||||
| EASYHAPROXY_BASE_PATH | (Optional) Base directory for all EasyHAProxy files. All paths (config, certs, plugins, www) are constructed relative to this base. | `/etc/easyhaproxy` |
|
| EASYHAPROXY_BASE_PATH | (Optional) Base directory for all EasyHAProxy files. All paths (config, certs, plugins, www) are constructed relative to this base. | `/etc/easyhaproxy` |
|
||||||
| EASYHAPROXY_CERTBOT_* | (Optional) Enable Let's Encrypt or any other ACME certificate. See more: [acme](../guides/acme.md) | *empty* |
|
| EASYHAPROXY_CERTBOT_* | (Optional) Enable Let's Encrypt or any other ACME certificate. See more: [acme](../guides/acme.md) | *empty* |
|
||||||
| EASYHAPROXY_SSL_MODE | (Optional) `strict` supports only the most recent TLS version; `default` good SSL integration with recent browsers; `loose` supports all old SSL protocols for old browsers (not recommended). | `default` |
|
| EASYHAPROXY_SSL_MODE | (Optional) `strict` supports only the most recent TLS version; `default` good SSL integration with recent browsers; `loose` supports all old SSL protocols for old browsers (not recommended). | `default` |
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ class ContainerEnv:
|
||||||
env_vars["lookup_label"] = os.getenv("EASYHAPROXY_LABEL_PREFIX") if os.getenv(
|
env_vars["lookup_label"] = os.getenv("EASYHAPROXY_LABEL_PREFIX") if os.getenv(
|
||||||
"EASYHAPROXY_LABEL_PREFIX") else "easyhaproxy"
|
"EASYHAPROXY_LABEL_PREFIX") else "easyhaproxy"
|
||||||
|
|
||||||
|
env_vars["host_network_ip"] = os.getenv("EASYHAPROXY_HOST_NETWORK_IP", "")
|
||||||
|
|
||||||
env_vars["logLevel"] = {
|
env_vars["logLevel"] = {
|
||||||
"easyhaproxy": os.getenv("EASYHAPROXY_LOG_LEVEL") if os.getenv(
|
"easyhaproxy": os.getenv("EASYHAPROXY_LOG_LEVEL") if os.getenv(
|
||||||
"EASYHAPROXY_LOG_LEVEL") else Functions.DEBUG,
|
"EASYHAPROXY_LOG_LEVEL") else Functions.DEBUG,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
|
import ipaddress
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
|
|
||||||
|
from functions import ContainerEnv, logger_easyhaproxy
|
||||||
|
|
||||||
from .interface import ProcessorInterface
|
from .interface import ProcessorInterface
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -12,24 +15,145 @@ class Docker(ProcessorInterface):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def inspect_network(self):
|
def inspect_network(self):
|
||||||
|
self.parsed_object = {}
|
||||||
|
own_container = None
|
||||||
try:
|
try:
|
||||||
ha_proxy_network_name = next(
|
own_container = self.client.containers.get(socket.gethostname())
|
||||||
iter(self.client.containers.get(socket.gethostname()).attrs["NetworkSettings"]["Networks"]))
|
ha_proxy_network_name = next(iter(own_container.attrs["NetworkSettings"]["Networks"]))
|
||||||
except Exception:
|
except Exception:
|
||||||
# HAProxy is not running in a container, get first container network
|
# 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
|
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)
|
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():
|
for container in self.client.containers.list():
|
||||||
# Issue 32 - Docker container cannot connect to containers in different network.
|
if not any(self.label in key for key in container.labels):
|
||||||
if ha_proxy_network_name not in container.attrs["NetworkSettings"]["Networks"].keys():
|
continue
|
||||||
ha_proxy_network.connect(container.name)
|
|
||||||
container = self.client.containers.get(container.name) # refresh object
|
|
||||||
|
|
||||||
ip_address = container.attrs["NetworkSettings"]["Networks"][ha_proxy_network_name]["IPAddress"]
|
ip_address = self._container_address(container, ha_proxy_network, ha_proxy_network_name, host_address)
|
||||||
self.parsed_object[ip_address] = container.labels
|
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)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ _CONTAINER_ENV_VARS = [
|
||||||
"HAPROXY_CUSTOMERRORS",
|
"HAPROXY_CUSTOMERRORS",
|
||||||
"EASYHAPROXY_SSL_MODE",
|
"EASYHAPROXY_SSL_MODE",
|
||||||
"EASYHAPROXY_LABEL_PREFIX",
|
"EASYHAPROXY_LABEL_PREFIX",
|
||||||
|
"EASYHAPROXY_HOST_NETWORK_IP",
|
||||||
"EASYHAPROXY_LOG_LEVEL",
|
"EASYHAPROXY_LOG_LEVEL",
|
||||||
"HAPROXY_LOG_LEVEL",
|
"HAPROXY_LOG_LEVEL",
|
||||||
"CERTBOT_LOG_LEVEL",
|
"CERTBOT_LOG_LEVEL",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ def test_container_env_empty():
|
||||||
"customerrors": False,
|
"customerrors": False,
|
||||||
"ssl_mode": "default",
|
"ssl_mode": "default",
|
||||||
"lookup_label": "easyhaproxy",
|
"lookup_label": "easyhaproxy",
|
||||||
|
"host_network_ip": "",
|
||||||
"logLevel": {
|
"logLevel": {
|
||||||
"easyhaproxy": Functions.DEBUG,
|
"easyhaproxy": Functions.DEBUG,
|
||||||
"haproxy": Functions.INFO,
|
"haproxy": Functions.INFO,
|
||||||
|
|
@ -42,6 +43,7 @@ def test_container_env_customerrors():
|
||||||
"customerrors": True,
|
"customerrors": True,
|
||||||
"ssl_mode": "default",
|
"ssl_mode": "default",
|
||||||
"lookup_label": "easyhaproxy",
|
"lookup_label": "easyhaproxy",
|
||||||
|
"host_network_ip": "",
|
||||||
"logLevel": {
|
"logLevel": {
|
||||||
"easyhaproxy": Functions.DEBUG,
|
"easyhaproxy": Functions.DEBUG,
|
||||||
"haproxy": Functions.INFO,
|
"haproxy": Functions.INFO,
|
||||||
|
|
@ -76,6 +78,7 @@ def test_container_env_sslmode():
|
||||||
"customerrors": False,
|
"customerrors": False,
|
||||||
"ssl_mode": "strict",
|
"ssl_mode": "strict",
|
||||||
"lookup_label": "easyhaproxy",
|
"lookup_label": "easyhaproxy",
|
||||||
|
"host_network_ip": "",
|
||||||
"logLevel": {
|
"logLevel": {
|
||||||
"easyhaproxy": Functions.DEBUG,
|
"easyhaproxy": Functions.DEBUG,
|
||||||
"haproxy": Functions.INFO,
|
"haproxy": Functions.INFO,
|
||||||
|
|
@ -111,6 +114,7 @@ def test_container_env_stats():
|
||||||
"customerrors": False,
|
"customerrors": False,
|
||||||
"ssl_mode": "default",
|
"ssl_mode": "default",
|
||||||
"lookup_label": "easyhaproxy",
|
"lookup_label": "easyhaproxy",
|
||||||
|
"host_network_ip": "",
|
||||||
"logLevel": {
|
"logLevel": {
|
||||||
"easyhaproxy": Functions.DEBUG,
|
"easyhaproxy": Functions.DEBUG,
|
||||||
"haproxy": Functions.INFO,
|
"haproxy": Functions.INFO,
|
||||||
|
|
@ -146,6 +150,7 @@ def test_container_env_stats_password():
|
||||||
"customerrors": False,
|
"customerrors": False,
|
||||||
"ssl_mode": "default",
|
"ssl_mode": "default",
|
||||||
"lookup_label": "easyhaproxy",
|
"lookup_label": "easyhaproxy",
|
||||||
|
"host_network_ip": "",
|
||||||
"stats": {
|
"stats": {
|
||||||
"username": "admin",
|
"username": "admin",
|
||||||
"password": "xyz",
|
"password": "xyz",
|
||||||
|
|
@ -188,6 +193,7 @@ def test_container_env_stats_password_2():
|
||||||
"customerrors": False,
|
"customerrors": False,
|
||||||
"ssl_mode": "default",
|
"ssl_mode": "default",
|
||||||
"lookup_label": "easyhaproxy",
|
"lookup_label": "easyhaproxy",
|
||||||
|
"host_network_ip": "",
|
||||||
"stats": {
|
"stats": {
|
||||||
"username": "abc",
|
"username": "abc",
|
||||||
"password": "xyz",
|
"password": "xyz",
|
||||||
|
|
@ -230,6 +236,7 @@ def test_container_env_certbot_email():
|
||||||
"customerrors": False,
|
"customerrors": False,
|
||||||
"ssl_mode": "default",
|
"ssl_mode": "default",
|
||||||
"lookup_label": "easyhaproxy",
|
"lookup_label": "easyhaproxy",
|
||||||
|
"host_network_ip": "",
|
||||||
"logLevel": {
|
"logLevel": {
|
||||||
"easyhaproxy": Functions.DEBUG,
|
"easyhaproxy": Functions.DEBUG,
|
||||||
"haproxy": Functions.INFO,
|
"haproxy": Functions.INFO,
|
||||||
|
|
@ -272,6 +279,7 @@ def test_container_env_certbot_full():
|
||||||
"customerrors": False,
|
"customerrors": False,
|
||||||
"ssl_mode": "default",
|
"ssl_mode": "default",
|
||||||
"lookup_label": "easyhaproxy",
|
"lookup_label": "easyhaproxy",
|
||||||
|
"host_network_ip": "",
|
||||||
"logLevel": {
|
"logLevel": {
|
||||||
"easyhaproxy": Functions.DEBUG,
|
"easyhaproxy": Functions.DEBUG,
|
||||||
"haproxy": Functions.INFO,
|
"haproxy": Functions.INFO,
|
||||||
|
|
@ -316,6 +324,7 @@ def test_container_log_level():
|
||||||
"customerrors": False,
|
"customerrors": False,
|
||||||
"ssl_mode": "default",
|
"ssl_mode": "default",
|
||||||
"lookup_label": "easyhaproxy",
|
"lookup_label": "easyhaproxy",
|
||||||
|
"host_network_ip": "",
|
||||||
"logLevel": {
|
"logLevel": {
|
||||||
"easyhaproxy": Functions.ERROR,
|
"easyhaproxy": Functions.ERROR,
|
||||||
"haproxy": Functions.FATAL,
|
"haproxy": Functions.FATAL,
|
||||||
|
|
|
||||||
|
|
@ -105,4 +105,277 @@ def test_processor_docker():
|
||||||
container2.stop()
|
container2.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def _skip_unless_docker_is_idle():
|
||||||
|
try:
|
||||||
|
client = docker.from_env()
|
||||||
|
except docker.errors.DockerException:
|
||||||
|
pytest.skip("There is no docker environment")
|
||||||
|
|
||||||
|
if len(client.containers.list()) > 0:
|
||||||
|
pytest.skip("I cannot run this test with other containers running.")
|
||||||
|
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
def _bridge_gateway(client):
|
||||||
|
return client.networks.get("bridge").attrs["IPAM"]["Config"][0]["Gateway"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_processor_docker_host_network():
|
||||||
|
client = _skip_unless_docker_is_idle()
|
||||||
|
|
||||||
|
container = client.containers.run("byjg/static-httpserver",
|
||||||
|
name="test_processor_docker_host",
|
||||||
|
detach=True,
|
||||||
|
auto_remove=True,
|
||||||
|
remove=True,
|
||||||
|
network_mode="host",
|
||||||
|
labels={
|
||||||
|
"easyhaproxy.hostmode.port": "80",
|
||||||
|
"easyhaproxy.hostmode.localport": "8080",
|
||||||
|
"easyhaproxy.hostmode.host": "hostmode.local",
|
||||||
|
})
|
||||||
|
container2 = client.containers.run("byjg/static-httpserver",
|
||||||
|
name="test_processor_docker_bridge",
|
||||||
|
detach=True,
|
||||||
|
auto_remove=True,
|
||||||
|
remove=True,
|
||||||
|
labels={
|
||||||
|
"easyhaproxy.bridged.port": "80",
|
||||||
|
"easyhaproxy.bridged.localport": "8080",
|
||||||
|
"easyhaproxy.bridged.host": "bridged.local",
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
static = ProcessorInterface.factory(ProcessorInterface.DOCKER)
|
||||||
|
|
||||||
|
assert {
|
||||||
|
'easyhaproxy.hostmode.host': 'hostmode.local',
|
||||||
|
'easyhaproxy.hostmode.localport': '8080',
|
||||||
|
'easyhaproxy.hostmode.port': '80',
|
||||||
|
} == _get_hydrated_object(static.get_parsed_object(), "easyhaproxy.hostmode")
|
||||||
|
|
||||||
|
# The container shares the host network namespace, so it is served through the gateway.
|
||||||
|
assert _get_ip_host(static.get_parsed_object(), "easyhaproxy.hostmode") == _bridge_gateway(client)
|
||||||
|
|
||||||
|
# The container on the bridge network keeps being served through its own address.
|
||||||
|
bridged_ip = client.containers.get(container2.name).attrs["NetworkSettings"]["Networks"]["bridge"]["IPAddress"]
|
||||||
|
assert _get_ip_host(static.get_parsed_object(), "easyhaproxy.bridged") == bridged_ip
|
||||||
|
|
||||||
|
static.get_haproxy_conf()
|
||||||
|
assert static.get_hosts() == [
|
||||||
|
'bridged.local:80',
|
||||||
|
'hostmode.local:80'
|
||||||
|
]
|
||||||
|
finally:
|
||||||
|
container.stop()
|
||||||
|
container2.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def test_processor_docker_host_network_merges_labels():
|
||||||
|
client = _skip_unless_docker_is_idle()
|
||||||
|
|
||||||
|
container = client.containers.run("byjg/static-httpserver",
|
||||||
|
name="test_processor_docker_host1",
|
||||||
|
detach=True,
|
||||||
|
auto_remove=True,
|
||||||
|
remove=True,
|
||||||
|
network_mode="host",
|
||||||
|
labels={
|
||||||
|
"easyhaproxy.first.port": "80",
|
||||||
|
"easyhaproxy.first.localport": "8080",
|
||||||
|
"easyhaproxy.first.host": "first.local",
|
||||||
|
})
|
||||||
|
container2 = client.containers.run("byjg/static-httpserver",
|
||||||
|
name="test_processor_docker_host2",
|
||||||
|
detach=True,
|
||||||
|
auto_remove=True,
|
||||||
|
remove=True,
|
||||||
|
network_mode="host",
|
||||||
|
environment={"PORT": "9000", "TLS_PORT": "9443"},
|
||||||
|
labels={
|
||||||
|
"easyhaproxy.second.port": "80",
|
||||||
|
"easyhaproxy.second.localport": "9000",
|
||||||
|
"easyhaproxy.second.host": "second.local",
|
||||||
|
})
|
||||||
|
container3 = client.containers.run("byjg/static-httpserver",
|
||||||
|
name="test_processor_docker_bridge_merge",
|
||||||
|
detach=True,
|
||||||
|
auto_remove=True,
|
||||||
|
remove=True,
|
||||||
|
labels={
|
||||||
|
"easyhaproxy.bridged.port": "80",
|
||||||
|
"easyhaproxy.bridged.localport": "8080",
|
||||||
|
"easyhaproxy.bridged.host": "bridged.local",
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
static = ProcessorInterface.factory(ProcessorInterface.DOCKER)
|
||||||
|
gateway = _bridge_gateway(client)
|
||||||
|
|
||||||
|
# Both containers resolve to the same address, so their labels are merged, not replaced.
|
||||||
|
assert _get_ip_host(static.get_parsed_object(), "easyhaproxy.first") == gateway
|
||||||
|
assert _get_ip_host(static.get_parsed_object(), "easyhaproxy.second") == gateway
|
||||||
|
merged = {key: value for key, value in static.get_parsed_object()[gateway].items()
|
||||||
|
if key.startswith("easyhaproxy.")}
|
||||||
|
assert {
|
||||||
|
'easyhaproxy.first.host': 'first.local',
|
||||||
|
'easyhaproxy.first.localport': '8080',
|
||||||
|
'easyhaproxy.first.port': '80',
|
||||||
|
'easyhaproxy.second.host': 'second.local',
|
||||||
|
'easyhaproxy.second.localport': '9000',
|
||||||
|
'easyhaproxy.second.port': '80',
|
||||||
|
} == merged
|
||||||
|
|
||||||
|
haproxy_cfg = static.get_haproxy_conf()
|
||||||
|
assert f"server srv-0 {gateway}:8080" in haproxy_cfg
|
||||||
|
assert f"server srv-0 {gateway}:9000" in haproxy_cfg
|
||||||
|
finally:
|
||||||
|
container.stop()
|
||||||
|
container2.stop()
|
||||||
|
container3.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def test_processor_docker_host_network_ip_override():
|
||||||
|
client = _skip_unless_docker_is_idle()
|
||||||
|
|
||||||
|
container = client.containers.run("byjg/static-httpserver",
|
||||||
|
name="test_processor_docker_host_override",
|
||||||
|
detach=True,
|
||||||
|
auto_remove=True,
|
||||||
|
remove=True,
|
||||||
|
network_mode="host",
|
||||||
|
labels={
|
||||||
|
"easyhaproxy.hostmode.port": "80",
|
||||||
|
"easyhaproxy.hostmode.localport": "8080",
|
||||||
|
"easyhaproxy.hostmode.host": "hostmode.local",
|
||||||
|
})
|
||||||
|
container2 = client.containers.run("byjg/static-httpserver",
|
||||||
|
name="test_processor_docker_bridge_override",
|
||||||
|
detach=True,
|
||||||
|
auto_remove=True,
|
||||||
|
remove=True,
|
||||||
|
labels={
|
||||||
|
"easyhaproxy.bridged.port": "80",
|
||||||
|
"easyhaproxy.bridged.localport": "8080",
|
||||||
|
"easyhaproxy.bridged.host": "bridged.local",
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
os.environ['EASYHAPROXY_HOST_NETWORK_IP'] = '10.20.30.40'
|
||||||
|
|
||||||
|
static = ProcessorInterface.factory(ProcessorInterface.DOCKER)
|
||||||
|
|
||||||
|
assert _get_ip_host(static.get_parsed_object(), "easyhaproxy.hostmode") == '10.20.30.40'
|
||||||
|
finally:
|
||||||
|
del os.environ['EASYHAPROXY_HOST_NETWORK_IP']
|
||||||
|
container.stop()
|
||||||
|
container2.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def test_processor_docker_shared_namespace():
|
||||||
|
client = _skip_unless_docker_is_idle()
|
||||||
|
|
||||||
|
owner = client.containers.run("byjg/static-httpserver",
|
||||||
|
name="test_processor_docker_owner",
|
||||||
|
detach=True,
|
||||||
|
auto_remove=True,
|
||||||
|
remove=True)
|
||||||
|
time.sleep(1)
|
||||||
|
container = client.containers.run("byjg/static-httpserver",
|
||||||
|
name="test_processor_docker_sidecar",
|
||||||
|
detach=True,
|
||||||
|
auto_remove=True,
|
||||||
|
remove=True,
|
||||||
|
environment={"PORT": "9000", "TLS_PORT": "9443"},
|
||||||
|
network_mode="container:test_processor_docker_owner",
|
||||||
|
labels={
|
||||||
|
"easyhaproxy.sidecar.port": "80",
|
||||||
|
"easyhaproxy.sidecar.localport": "9000",
|
||||||
|
"easyhaproxy.sidecar.host": "sidecar.local",
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
static = ProcessorInterface.factory(ProcessorInterface.DOCKER)
|
||||||
|
|
||||||
|
# The sidecar is reachable at the address of the container owning the network namespace.
|
||||||
|
owner_ip = client.containers.get(owner.name).attrs["NetworkSettings"]["Networks"]["bridge"]["IPAddress"]
|
||||||
|
assert _get_ip_host(static.get_parsed_object(), "easyhaproxy.sidecar") == owner_ip
|
||||||
|
finally:
|
||||||
|
container.stop()
|
||||||
|
owner.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def test_processor_docker_host_network_only():
|
||||||
|
client = _skip_unless_docker_is_idle()
|
||||||
|
|
||||||
|
container = client.containers.run("byjg/static-httpserver",
|
||||||
|
name="test_processor_docker_host_only",
|
||||||
|
detach=True,
|
||||||
|
auto_remove=True,
|
||||||
|
remove=True,
|
||||||
|
network_mode="host",
|
||||||
|
labels={
|
||||||
|
"easyhaproxy.hostmode.port": "80",
|
||||||
|
"easyhaproxy.hostmode.localport": "8080",
|
||||||
|
"easyhaproxy.hostmode.host": "hostmode.local",
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# There is no network to borrow, but the discovery must not fail.
|
||||||
|
static = ProcessorInterface.factory(ProcessorInterface.DOCKER)
|
||||||
|
assert static.get_parsed_object() == {}
|
||||||
|
assert static.get_haproxy_conf() != ""
|
||||||
|
finally:
|
||||||
|
container.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def test_processor_docker_ignores_unlabeled():
|
||||||
|
client = _skip_unless_docker_is_idle()
|
||||||
|
|
||||||
|
network = client.networks.create("test_processor_docker_network", driver="bridge")
|
||||||
|
# The most recent container is inspected first, so the labeled one defines the network to use.
|
||||||
|
unlabeled = client.containers.run("byjg/static-httpserver",
|
||||||
|
name="test_processor_docker_unlabeled",
|
||||||
|
detach=True,
|
||||||
|
auto_remove=True,
|
||||||
|
remove=True)
|
||||||
|
time.sleep(1)
|
||||||
|
container = client.containers.run("byjg/static-httpserver",
|
||||||
|
name="test_processor_docker_labeled",
|
||||||
|
detach=True,
|
||||||
|
auto_remove=True,
|
||||||
|
remove=True,
|
||||||
|
network=network.name,
|
||||||
|
labels={
|
||||||
|
"easyhaproxy.labeled.port": "80",
|
||||||
|
"easyhaproxy.labeled.localport": "8080",
|
||||||
|
"easyhaproxy.labeled.host": "labeled.local",
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
static = ProcessorInterface.factory(ProcessorInterface.DOCKER)
|
||||||
|
|
||||||
|
# The labeled container is served through the network it already belongs to.
|
||||||
|
labeled_ip = client.containers.get(container.name).attrs["NetworkSettings"]["Networks"][network.name][
|
||||||
|
"IPAddress"]
|
||||||
|
assert _get_ip_host(static.get_parsed_object(), "easyhaproxy.labeled") == labeled_ip
|
||||||
|
|
||||||
|
# The container without any label is neither inspected nor connected to the network.
|
||||||
|
unlabeled_networks = client.containers.get(unlabeled.name).attrs["NetworkSettings"]["Networks"]
|
||||||
|
assert list(unlabeled_networks.keys()) == ["bridge"]
|
||||||
|
assert unlabeled_networks["bridge"]["IPAddress"] not in static.get_parsed_object()
|
||||||
|
finally:
|
||||||
|
container.stop()
|
||||||
|
unlabeled.stop()
|
||||||
|
time.sleep(1)
|
||||||
|
network.remove()
|
||||||
|
|
||||||
|
|
||||||
# test_processor_docker()
|
# test_processor_docker()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue