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

@ -23,6 +23,7 @@ _CONTAINER_ENV_VARS = [
"HAPROXY_CUSTOMERRORS",
"EASYHAPROXY_SSL_MODE",
"EASYHAPROXY_LABEL_PREFIX",
"EASYHAPROXY_HOST_NETWORK_IP",
"EASYHAPROXY_LOG_LEVEL",
"HAPROXY_LOG_LEVEL",
"CERTBOT_LOG_LEVEL",

View file

@ -8,6 +8,7 @@ def test_container_env_empty():
"customerrors": False,
"ssl_mode": "default",
"lookup_label": "easyhaproxy",
"host_network_ip": "",
"logLevel": {
"easyhaproxy": Functions.DEBUG,
"haproxy": Functions.INFO,
@ -42,6 +43,7 @@ def test_container_env_customerrors():
"customerrors": True,
"ssl_mode": "default",
"lookup_label": "easyhaproxy",
"host_network_ip": "",
"logLevel": {
"easyhaproxy": Functions.DEBUG,
"haproxy": Functions.INFO,
@ -76,6 +78,7 @@ def test_container_env_sslmode():
"customerrors": False,
"ssl_mode": "strict",
"lookup_label": "easyhaproxy",
"host_network_ip": "",
"logLevel": {
"easyhaproxy": Functions.DEBUG,
"haproxy": Functions.INFO,
@ -111,6 +114,7 @@ def test_container_env_stats():
"customerrors": False,
"ssl_mode": "default",
"lookup_label": "easyhaproxy",
"host_network_ip": "",
"logLevel": {
"easyhaproxy": Functions.DEBUG,
"haproxy": Functions.INFO,
@ -146,6 +150,7 @@ def test_container_env_stats_password():
"customerrors": False,
"ssl_mode": "default",
"lookup_label": "easyhaproxy",
"host_network_ip": "",
"stats": {
"username": "admin",
"password": "xyz",
@ -188,6 +193,7 @@ def test_container_env_stats_password_2():
"customerrors": False,
"ssl_mode": "default",
"lookup_label": "easyhaproxy",
"host_network_ip": "",
"stats": {
"username": "abc",
"password": "xyz",
@ -230,6 +236,7 @@ def test_container_env_certbot_email():
"customerrors": False,
"ssl_mode": "default",
"lookup_label": "easyhaproxy",
"host_network_ip": "",
"logLevel": {
"easyhaproxy": Functions.DEBUG,
"haproxy": Functions.INFO,
@ -272,6 +279,7 @@ def test_container_env_certbot_full():
"customerrors": False,
"ssl_mode": "default",
"lookup_label": "easyhaproxy",
"host_network_ip": "",
"logLevel": {
"easyhaproxy": Functions.DEBUG,
"haproxy": Functions.INFO,
@ -316,6 +324,7 @@ def test_container_log_level():
"customerrors": False,
"ssl_mode": "default",
"lookup_label": "easyhaproxy",
"host_network_ip": "",
"logLevel": {
"easyhaproxy": Functions.ERROR,
"haproxy": Functions.FATAL,

View file

@ -105,4 +105,234 @@ def test_processor_docker():
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()
# test_processor_docker()