From c340de1d9db22cf78f79566c3170738f0259e221 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Mon, 16 Feb 2026 14:48:39 -0500 Subject: [PATCH] Refactor ACME E2E tests: add Pebble health check container, simplify startup logic, and replace Python-based health check --- .gitignore | 2 +- tests_e2e/docker/docker-compose-acme-e2e.yml | 22 ++- tests_e2e/docker/pebble-config.json | 12 ++ tests_e2e/test_docker_compose.py | 143 ++----------------- 4 files changed, 46 insertions(+), 133 deletions(-) create mode 100644 tests_e2e/docker/pebble-config.json diff --git a/.gitignore b/.gitignore index f81b658..785652d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,4 @@ build/ /tests_e2e/docker/jwt_private.pem /tests_e2e/docker/jwt_pubkey.pem /tests_e2e/docker/cloudflare_ips.lst -/tests_e2e/docker/pebble-* +/tests_e2e/docker/pebble-ca.pem diff --git a/tests_e2e/docker/docker-compose-acme-e2e.yml b/tests_e2e/docker/docker-compose-acme-e2e.yml index 01434e7..21bc259 100644 --- a/tests_e2e/docker/docker-compose-acme-e2e.yml +++ b/tests_e2e/docker/docker-compose-acme-e2e.yml @@ -64,6 +64,22 @@ services: networks: - acme-test + # Pebble health check sidecar + pebble_health: + image: curlimages/curl:8.6.0 + depends_on: + - pebble + healthcheck: + test: ["CMD", "curl", "-skf", "https://pebble:14000/dir"] + interval: 1s + timeout: 3s + start_period: 5s # Sufficient time for CI environments + retries: 20 # Enough retries for slower CI + networks: + - acme-test + restart: "no" + command: ["tail", "-f", "/dev/null"] + # Backend web server backend: image: byjg/static-httpserver @@ -82,8 +98,10 @@ services: context: ../.. dockerfile: build/Dockerfile depends_on: - - pebble - - backend + pebble_health: + condition: service_healthy + backend: + condition: service_started healthcheck: test: ["CMD", "curl", "-f", "-u", "admin:password", "http://localhost:1936"] interval: 10s diff --git a/tests_e2e/docker/pebble-config.json b/tests_e2e/docker/pebble-config.json new file mode 100644 index 0000000..fd933a3 --- /dev/null +++ b/tests_e2e/docker/pebble-config.json @@ -0,0 +1,12 @@ +{ + "pebble": { + "listenAddress": "0.0.0.0:14000", + "managementListenAddress": "0.0.0.0:15000", + "certificate": "test/certs/localhost/cert.pem", + "privateKey": "test/certs/localhost/key.pem", + "httpPort": 80, + "tlsPort": 443, + "ocspResponderURL": "", + "externalAccountBindingRequired": false + } +} \ No newline at end of file diff --git a/tests_e2e/test_docker_compose.py b/tests_e2e/test_docker_compose.py index 6e30ae4..1c850b7 100644 --- a/tests_e2e/test_docker_compose.py +++ b/tests_e2e/test_docker_compose.py @@ -34,10 +34,6 @@ import requests import jwt as jwt_lib from typing import Generator from utils import extract_backend_block, DockerComposeFixture -import urllib3 - -# Disable SSL warnings for Pebble health checks (self-signed certs) -urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # Base directory for docker-compose files BASE_DIR = Path(__file__).parent.absolute() @@ -847,148 +843,35 @@ class TestChangedLabel: # Test: docker-compose-acme-e2e.yml - ACME/Certbot with Pebble # ============================================================================= -def wait_for_pebble() -> bool: - """ - Health check function to verify Pebble ACME server is ready. - - Returns True if Pebble is responding to the /dir endpoint. - This is used instead of Docker healthchecks for more reliable startup - detection in CI environments. - """ - try: - # Pebble uses self-signed certificates, so we need verify=False - response = requests.get("https://127.0.0.1:14000/dir", verify=False, timeout=5) - return response.status_code == 200 - except Exception: - return False - - @pytest.fixture def docker_compose_acme() -> Generator[None, None, None]: - """ - Fixture for docker-compose-acme-e2e.yml - ACME/Certbot E2E test - - Uses staged startup to ensure services start in the correct order: - 1. Pebble ACME server first - 2. HAProxy (after Pebble is ready) - 3. Backend - """ + """Fixture for docker-compose-acme-e2e.yml - ACME/Certbot E2E test""" volume_name = "docker_certbot-certs" - compose_file = str(DOCKER_DIR / "docker-compose-acme-e2e.yml") # Download Pebble CA certificate (only once per test session) create_pebble_ca_file() # Clean up volume from previous test runs (ensures fresh start) + subprocess.run( + ["docker", "volume", "rm", volume_name], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL # Ignore error if volume doesn't exist + ) + + fixture = DockerComposeFixture(str(DOCKER_DIR / "docker-compose-acme-e2e.yml"), startup_wait=0) + fixture.up() + yield + fixture.down() + + # Clean up volume after test subprocess.run( ["docker", "volume", "rm", volume_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) - print() - print(" → Starting ACME test environment in stages...") - - # Change to docker directory so relative paths in docker-compose work correctly - original_dir = os.getcwd() - docker_dir = DOCKER_DIR - - try: - os.chdir(docker_dir) - - # Stage 1: Start Pebble ACME server first - print(" → Stage 1: Starting Pebble ACME server...") - result = subprocess.run( - ["docker", "compose", "-f", "docker-compose-acme-e2e.yml", "up", "-d", "pebble"], - capture_output=True, - text=True, - check=True - ) - - # Wait for Pebble to be ready - print(" → Waiting for Pebble to be ready (timeout: 90s)...") - start_time = time.time() - pebble_ready = False - while time.time() - start_time < 90: - if wait_for_pebble(): - pebble_ready = True - break - time.sleep(1) - - if not pebble_ready: - # Debug: Check container status and logs - print(" ✗ Pebble did not become ready, checking container status...") - status_result = subprocess.run( - ["docker", "ps", "-a", "--filter", "name=pebble", "--format", "{{.Names}}\t{{.Status}}"], - capture_output=True, - text=True - ) - print(f" Container status: {status_result.stdout.strip()}") - - # Get container logs - logs_result = subprocess.run( - ["docker", "compose", "-f", "docker-compose-acme-e2e.yml", "logs", "pebble"], - capture_output=True, - text=True - ) - print(f" Pebble logs:\n{logs_result.stdout}") - if logs_result.stderr: - print(f" Pebble stderr:\n{logs_result.stderr}") - - raise TimeoutError("Pebble did not become ready within 90 seconds") - - # Stage 2: Start HAProxy - print(" → Stage 2: Starting HAProxy...") - result = subprocess.run( - ["docker", "compose", "-f", "docker-compose-acme-e2e.yml", "up", "-d", "haproxy"], - capture_output=True, - text=True, - check=True - ) - print(" → Waiting for HAProxy to initialize...") - time.sleep(5) - - # Stage 3: Start Backend - print(" → Stage 3: Starting backend...") - result = subprocess.run( - ["docker", "compose", "-f", "docker-compose-acme-e2e.yml", "up", "-d", "backend"], - capture_output=True, - text=True, - check=True - ) - print(" → Waiting for backend to initialize...") - time.sleep(3) - - print(" ✓ All services started and ready") - - yield - - finally: - # Cleanup - print(" → Stopping ACME test environment...") - subprocess.run( - ["docker", "compose", "-f", "docker-compose-acme-e2e.yml", "down", "--remove-orphans", "-t", "0"], - capture_output=True, - text=True - ) - print(" ✓ Services stopped and cleaned up") - - # Restore original directory - os.chdir(original_dir) - - # Clean up volume - subprocess.run( - ["docker", "volume", "rm", volume_name], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - @pytest.mark.acme -@pytest.mark.skipif( - os.getenv("CI") == "true" and os.getenv("SKIP_ACME_TESTS") == "true", - reason="ACME tests skipped in CI (set SKIP_ACME_TESTS=false to enable)" -) class TestACME: """Tests for docker-compose-acme-e2e.yml - ACME/Certbot with Pebble test server"""