From 62bd0debffdb762714de853a069e3b63414fa789 Mon Sep 17 00:00:00 2001 From: Ewoud Samuels Date: Sun, 8 Sep 2024 08:05:55 +0200 Subject: [PATCH] fix(keycloak): Add support for Keycloak version >=25 (#694) Keycloak changed the health endpoint to a separate "management port" starting version 25.0.0 as can be read in the [changelogs](https://www.keycloak.org/docs/25.0.0/release_notes/#management-port-for-metrics-and-health-endpoints). The keycloak module in testcontainers-python uses this endpoint for the readiness_probe. Currently compatibility with Keycloak >= 25 is broken. This MR adds compatibility with Keycloak >= 25 by changing the readiness probe to do the following: - Try the health endpoint on the management port - If that gives a ConnectionError try the legacy health endpoint An alternative approach would have been to create a separate LegacyKeycloakContainer class as has been done in other modules, however I think this unnecessarily complicates the user experience. Additionally a public `get_management_url` method has been added to give end-users convenient access to the new Keycloak management endpoint. --- .../testcontainers/keycloak/__init__.py | 18 +++++++++++++++--- modules/keycloak/tests/test_keycloak.py | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/modules/keycloak/testcontainers/keycloak/__init__.py b/modules/keycloak/testcontainers/keycloak/__init__.py index 6addf09a..e7a06521 100644 --- a/modules/keycloak/testcontainers/keycloak/__init__.py +++ b/modules/keycloak/testcontainers/keycloak/__init__.py @@ -34,7 +34,7 @@ class KeycloakContainer(DockerContainer): >>> from testcontainers.keycloak import KeycloakContainer - >>> with KeycloakContainer(f"quay.io/keycloak/keycloak:24.0.1") as keycloak: + >>> with KeycloakContainer(f"quay.io/keycloak/keycloak:25.0.4") as keycloak: ... keycloak.get_client().users_count() 1 """ @@ -45,13 +45,15 @@ def __init__( username: Optional[str] = None, password: Optional[str] = None, port: int = 8080, + management_port: int = 9000, cmd: Optional[str] = _DEFAULT_DEV_COMMAND, ) -> None: super().__init__(image=image) self.username = username or os.environ.get("KEYCLOAK_ADMIN", "test") self.password = password or os.environ.get("KEYCLOAK_ADMIN_PASSWORD", "test") self.port = port - self.with_exposed_ports(self.port) + self.management_port = management_port + self.with_exposed_ports(self.port, self.management_port) self.cmd = cmd def _configure(self) -> None: @@ -71,10 +73,20 @@ def get_url(self) -> str: port = self.get_exposed_port(self.port) return f"http://{host}:{port}" + def get_management_url(self) -> str: + host = self.get_container_host_ip() + port = self.get_exposed_port(self.management_port) + return f"http://{host}:{port}" + @wait_container_is_ready(requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout) def _readiness_probe(self) -> None: # Keycloak provides REST API endpoints for health checks: https://www.keycloak.org/server/health - response = requests.get(f"{self.get_url()}/health/ready", timeout=1) + try: + # Try the new health endpoint for keycloak 25.0.0 and above + # See https://www.keycloak.org/docs/25.0.0/release_notes/#management-port-for-metrics-and-health-endpoints + response = requests.get(f"{self.get_management_url()}/health/ready", timeout=1) + except requests.exceptions.ConnectionError: + response = requests.get(f"{self.get_url()}/health/ready", timeout=1) response.raise_for_status() if _DEFAULT_DEV_COMMAND in self._command: wait_for_logs(self, "Added user .* to realm .*") diff --git a/modules/keycloak/tests/test_keycloak.py b/modules/keycloak/tests/test_keycloak.py index ce54e467..6bf003b7 100644 --- a/modules/keycloak/tests/test_keycloak.py +++ b/modules/keycloak/tests/test_keycloak.py @@ -2,7 +2,7 @@ from testcontainers.keycloak import KeycloakContainer -@pytest.mark.parametrize("image_version", ["24.0.1", "18.0"]) +@pytest.mark.parametrize("image_version", ["25.0", "24.0.1", "18.0"]) def test_docker_run_keycloak(image_version: str): with KeycloakContainer(f"quay.io/keycloak/keycloak:{image_version}") as keycloak_admin: assert keycloak_admin.get_client().users_count() == 1