1
0
Fork 0

Refactor Swarm E2E tests: optimize timeouts, enhance stack deployment/wait logic, consolidate replicas, and update HAProxy config refresh cycle

This commit is contained in:
Joao Gilberto Magalhaes 2026-02-18 15:55:00 -05:00
parent d9c7ff89cc
commit 5a9f14e980
3 changed files with 26 additions and 22 deletions

View file

@ -63,6 +63,7 @@ services:
replicas: 1 replicas: 1
environment: environment:
EASYHAPROXY_DISCOVER: swarm EASYHAPROXY_DISCOVER: swarm
EASYHAPROXY_REFRESH_CONF: "2"
EASYHAPROXY_SSL_MODE: "loose" EASYHAPROXY_SSL_MODE: "loose"
EASYHAPROXY_CERTBOT_EMAIL: changeme@example.org EASYHAPROXY_CERTBOT_EMAIL: changeme@example.org
HAPROXY_CUSTOMERRORS: "true" HAPROXY_CUSTOMERRORS: "true"

View file

@ -109,6 +109,7 @@ services:
- node.role == manager - node.role == manager
environment: environment:
EASYHAPROXY_DISCOVER: swarm EASYHAPROXY_DISCOVER: swarm
EASYHAPROXY_REFRESH_CONF: "2"
EASYHAPROXY_SSL_MODE: "loose" EASYHAPROXY_SSL_MODE: "loose"
HAPROXY_CUSTOMERRORS: "true" HAPROXY_CUSTOMERRORS: "true"
HAPROXY_USERNAME: admin HAPROXY_USERNAME: admin
@ -127,7 +128,7 @@ services:
environment: environment:
TITLE: "Public Website" TITLE: "Public Website"
deploy: deploy:
replicas: 4 replicas: 1
labels: labels:
easyhaproxy.http.host: "website.example.com" easyhaproxy.http.host: "website.example.com"
easyhaproxy.http.port: "80" easyhaproxy.http.port: "80"
@ -146,7 +147,7 @@ services:
environment: environment:
TITLE: "Protected API" TITLE: "Protected API"
deploy: deploy:
replicas: 6 replicas: 1
labels: labels:
easyhaproxy.http.host: "api.example.com" easyhaproxy.http.host: "api.example.com"
easyhaproxy.http.port: "80" easyhaproxy.http.port: "80"
@ -173,7 +174,7 @@ services:
environment: environment:
TITLE: "Admin Panel" TITLE: "Admin Panel"
deploy: deploy:
replicas: 2 replicas: 1
labels: labels:
easyhaproxy.http.host: "admin.example.com" easyhaproxy.http.host: "admin.example.com"
easyhaproxy.http.port: "80" easyhaproxy.http.port: "80"

View file

@ -192,7 +192,7 @@ def wait_for_http(
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
pass pass
time.sleep(3) time.sleep(1)
return False return False
@ -271,8 +271,8 @@ class SwarmFixture:
self.stack_name = stack_name self.stack_name = stack_name
self.timeout = timeout self.timeout = timeout
def up(self): def deploy(self):
"""Deploy the stack and wait for all services to be running.""" """Issue `docker stack deploy` for each file without waiting for replicas."""
for stack_file in self.stack_files: for stack_file in self.stack_files:
name = Path(stack_file).name name = Path(stack_file).name
print(f"\n → Deploying stack '{self.stack_name}' from {name}...") print(f"\n → Deploying stack '{self.stack_name}' from {name}...")
@ -290,16 +290,23 @@ class SwarmFixture:
result.returncode, result.args, result.stdout, result.stderr result.returncode, result.args, result.stdout, result.stderr
) )
def wait_ready(self):
"""Wait until all services in this stack have reached their target replica count."""
if not wait_for_swarm_services(self.stack_name, self.timeout): if not wait_for_swarm_services(self.stack_name, self.timeout):
raise TimeoutError( raise TimeoutError(
f"Stack '{self.stack_name}' services did not become ready within {self.timeout}s" f"Stack '{self.stack_name}' services did not become ready within {self.timeout}s"
) )
def up(self):
"""Deploy the stack and wait for all services to be running."""
self.deploy()
self.wait_ready()
def down(self): def down(self):
"""Force-kill all stack containers, then remove the stack definition.""" """Force-kill all stack containers, then remove the stack definition."""
print(f"\n → Removing stack '{self.stack_name}'...") print(f"\n → Removing stack '{self.stack_name}'...")
# Force-kill running containers immediately (no graceful shutdown period) # Force-kill all running containers immediately (SIGKILL — no grace period)
result = subprocess.run( result = subprocess.run(
["docker", "ps", "-q", "--filter", f"name={self.stack_name}_"], ["docker", "ps", "-q", "--filter", f"name={self.stack_name}_"],
capture_output=True, text=True capture_output=True, text=True
@ -314,17 +321,7 @@ class SwarmFixture:
capture_output=True, text=True capture_output=True, text=True
) )
# Poll until no containers from this stack remain, so ports are free # Containers are already dead from docker kill above; ports are freed immediately.
start = time.time()
while time.time() - start < 60:
result = subprocess.run(
["docker", "ps", "-q", "--filter", f"name={self.stack_name}_"],
capture_output=True, text=True
)
if not result.stdout.strip():
break
time.sleep(2)
print(f" ✓ Stack '{self.stack_name}' removed") print(f" ✓ Stack '{self.stack_name}' removed")
@ -368,16 +365,21 @@ def swarm_basic_services(generate_ssl_certificates) -> Generator[None, None, Non
easyhaproxy = SwarmFixture(str(SWARM_DIR / "easyhaproxy.yml"), "easyhaproxy", timeout=120) easyhaproxy = SwarmFixture(str(SWARM_DIR / "easyhaproxy.yml"), "easyhaproxy", timeout=120)
services = SwarmFixture(str(SWARM_DIR / "services.yml"), "services", timeout=60) services = SwarmFixture(str(SWARM_DIR / "services.yml"), "services", timeout=60)
easyhaproxy.up() # Deploy both stacks immediately so they start pulling/starting in parallel,
services.up() # then wait for each to reach its target replica count.
easyhaproxy.deploy()
services.deploy()
easyhaproxy.wait_ready()
services.wait_ready()
# Wait for EasyHAProxy to auto-attach services, regenerate config, and # Wait for EasyHAProxy to auto-attach services, regenerate config, and
# for HAProxy to start serving traffic (up to 2 refresh cycles = ~20s). # for HAProxy to start serving traffic (up to 2 refresh cycles = ~20s).
# Do NOT include 503 — that means HAProxy is up but the backend isn't ready yet.
print("\n → Waiting for HAProxy to discover and configure swarm services...") print("\n → Waiting for HAProxy to discover and configure swarm services...")
ready = wait_for_http( ready = wait_for_http(
"https://127.0.0.1/", "https://127.0.0.1/",
headers={"Host": "host1.local"}, headers={"Host": "host1.local"},
expected_status=[200, 301, 302, 503], expected_status=[200, 301, 302],
timeout=120, timeout=120,
) )
if not ready: if not ready:
@ -415,7 +417,7 @@ def swarm_plugins_combined(generate_ssl_certificates) -> Generator[None, None, N
ready = wait_for_http( ready = wait_for_http(
"http://127.0.0.1/", "http://127.0.0.1/",
headers={"Host": "website.example.com"}, headers={"Host": "website.example.com"},
expected_status=[200, 404, 403, 401, 503], expected_status=[200],
timeout=120, timeout=120,
) )
if not ready: if not ready: