From c2cf4e0e4a06001ca8b48c3d4785b0c2b740a2d7 Mon Sep 17 00:00:00 2001 From: jansdhillon Date: Mon, 29 Jun 2026 21:17:10 -0600 Subject: [PATCH 1/7] fix: activate healthchecks for HAProxy backends, use ports instead of paths --- src/charm.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/charm.py b/src/charm.py index 0e8755b1..afa8bffc 100755 --- a/src/charm.py +++ b/src/charm.py @@ -1324,7 +1324,10 @@ def _provide_all_haproxy_route_requirements(self) -> None: ports=appserver_ports, paths=appserver_paths, protocol="http", - check_path="/", + check_port=cfg.appserver_base_port, + check_interval=2000, + check_rise=2, + check_fall=3, header_rewrite_expressions=forwarded_proto_https, allow_http=allow_http_default, unit_address=unit_ip, @@ -1347,7 +1350,10 @@ def _provide_all_haproxy_route_requirements(self) -> None: ports=pingserver_ports, paths=["/ping"], protocol="http", - check_path="/ping", + check_port=cfg.pingserver_base_port, + check_interval=2000, + check_rise=2, + check_fall=3, header_rewrite_expressions=forwarded_proto_https, allow_http=allow_http_always, unit_address=unit_ip, @@ -1358,7 +1364,10 @@ def _provide_all_haproxy_route_requirements(self) -> None: ports=message_server_ports, paths=["/message-system", "/attachment"], protocol="http", - check_path="/message-system", + check_port=cfg.message_server_base_port, + check_interval=2000, + check_rise=2, + check_fall=3, header_rewrite_expressions=forwarded_proto_https, allow_http=allow_http_default, unit_address=unit_ip, @@ -1369,7 +1378,10 @@ def _provide_all_haproxy_route_requirements(self) -> None: ports=api_ports, paths=["/api"], protocol="http", - check_path="/api", + check_port=cfg.api_base_port, + check_interval=2000, + check_rise=2, + check_fall=3, header_rewrite_expressions=forwarded_proto_https, allow_http=allow_http_default, unit_address=unit_ip, @@ -1380,7 +1392,7 @@ def _provide_all_haproxy_route_requirements(self) -> None: ports=[cfg.package_upload_base_port], paths=["/upload"], protocol="http", - check_path="/upload", + check_port=cfg.package_upload_base_port, check_interval=2000, check_rise=2, check_fall=3, @@ -1398,7 +1410,7 @@ def _provide_all_haproxy_route_requirements(self) -> None: ports=appserver_ports, paths=["/repository"], protocol="http", - check_path="/", + check_port=cfg.appserver_base_port, header_rewrite_expressions=forwarded_proto_https, allow_http=allow_http_always, unit_address=unit_ip, From bcda76d72f827a53e749692202f9dde40b86b3d2 Mon Sep 17 00:00:00 2001 From: jansdhillon Date: Mon, 29 Jun 2026 21:22:27 -0600 Subject: [PATCH 2/7] tc --- tests/unit/test_haproxy_route.py | 108 +++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/tests/unit/test_haproxy_route.py b/tests/unit/test_haproxy_route.py index 1e40ed68..e8c664ff 100644 --- a/tests/unit/test_haproxy_route.py +++ b/tests/unit/test_haproxy_route.py @@ -252,6 +252,114 @@ def test_pingserver_service_name_prefix(self, replicas_network_state): assert calls[0].kwargs["service"].startswith("landscape-pingserver-") +class TestHealthChecks: + """Each route uses check_port (not check_path) with the correct port number.""" + + SERVICES = [ + ("appserver", 8080), + ("pingserver", 8070), + ("message-server", 8090), + ("api", 9080), + ("package-upload", 9100), + ("repository", 8080), + ] + + def _check_port_for(self, mock, service_fragment): + calls = _calls_for(mock, service_fragment) + assert calls, f"No calls found for service {service_fragment!r}" + return calls[0].kwargs.get("check_port") + + def test_no_check_path_on_any_route(self, replicas_network_state): + context = Context(LandscapeServerCharm) + state = State(**replicas_network_state) + mock = _run_provide(context, state) + for c in mock.call_args_list: + assert "check_path" not in c.kwargs, ( + f"check_path should not be set for {c.kwargs.get('service')}" + ) + + def test_appserver_check_port(self, replicas_network_state): + context = Context(LandscapeServerCharm) + state = State(**replicas_network_state) + mock = _run_provide(context, state) + assert self._check_port_for(mock, "appserver") == 8080 + + def test_pingserver_check_port(self, replicas_network_state): + context = Context(LandscapeServerCharm) + state = State(**replicas_network_state) + mock = _run_provide(context, state) + assert self._check_port_for(mock, "pingserver") == 8070 + + def test_message_server_check_port(self, replicas_network_state): + context = Context(LandscapeServerCharm) + state = State(**replicas_network_state) + mock = _run_provide(context, state) + assert self._check_port_for(mock, "message-server") == 8090 + + def test_api_check_port(self, replicas_network_state): + context = Context(LandscapeServerCharm) + state = State(**replicas_network_state) + mock = _run_provide(context, state) + assert self._check_port_for(mock, "api") == 9080 + + def test_package_upload_check_port(self, replicas_network_state): + context = Context(LandscapeServerCharm) + state = State(**replicas_network_state) + mock = _run_provide(context, state) + assert self._check_port_for(mock, "package-upload") == 9100 + + def test_repository_check_port(self, replicas_network_state): + context = Context(LandscapeServerCharm) + state = State(**replicas_network_state) + mock = _run_provide(context, state) + assert self._check_port_for(mock, "repository") == 8080 + + def test_appserver_has_health_check_timing(self, replicas_network_state): + context = Context(LandscapeServerCharm) + state = State(**replicas_network_state) + mock = _run_provide(context, state) + calls = _calls_for(mock, "appserver") + assert calls[0].kwargs.get("check_interval") == 2000 + assert calls[0].kwargs.get("check_rise") == 2 + assert calls[0].kwargs.get("check_fall") == 3 + + def test_pingserver_has_health_check_timing(self, replicas_network_state): + context = Context(LandscapeServerCharm) + state = State(**replicas_network_state) + mock = _run_provide(context, state) + calls = _calls_for(mock, "pingserver") + assert calls[0].kwargs.get("check_interval") == 2000 + assert calls[0].kwargs.get("check_rise") == 2 + assert calls[0].kwargs.get("check_fall") == 3 + + def test_message_server_has_health_check_timing(self, replicas_network_state): + context = Context(LandscapeServerCharm) + state = State(**replicas_network_state) + mock = _run_provide(context, state) + calls = _calls_for(mock, "message-server") + assert calls[0].kwargs.get("check_interval") == 2000 + assert calls[0].kwargs.get("check_rise") == 2 + assert calls[0].kwargs.get("check_fall") == 3 + + def test_api_has_health_check_timing(self, replicas_network_state): + context = Context(LandscapeServerCharm) + state = State(**replicas_network_state) + mock = _run_provide(context, state) + calls = _calls_for(mock, "api") + assert calls[0].kwargs.get("check_interval") == 2000 + assert calls[0].kwargs.get("check_rise") == 2 + assert calls[0].kwargs.get("check_fall") == 3 + + def test_custom_base_port_reflected_in_check_port(self, replicas_network_state): + """check_port follows config, not a hardcoded value.""" + context = Context(LandscapeServerCharm) + state = State( + config={"appserver_base_port": 18080}, **replicas_network_state + ) + mock = _run_provide(context, state) + assert self._check_port_for(mock, "appserver") == 18080 + + class TestConditionalRoutes: """hostagent-messenger and ubuntu-installer-attach routes are conditional.""" From 8c1be0b62167377310c50d7818ca4e6316f9abba Mon Sep 17 00:00:00 2001 From: jansdhillon Date: Mon, 29 Jun 2026 21:24:05 -0600 Subject: [PATCH 3/7] lint --- tests/unit/test_haproxy_route.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unit/test_haproxy_route.py b/tests/unit/test_haproxy_route.py index e8c664ff..d0d7a998 100644 --- a/tests/unit/test_haproxy_route.py +++ b/tests/unit/test_haproxy_route.py @@ -353,9 +353,7 @@ def test_api_has_health_check_timing(self, replicas_network_state): def test_custom_base_port_reflected_in_check_port(self, replicas_network_state): """check_port follows config, not a hardcoded value.""" context = Context(LandscapeServerCharm) - state = State( - config={"appserver_base_port": 18080}, **replicas_network_state - ) + state = State(config={"appserver_base_port": 18080}, **replicas_network_state) mock = _run_provide(context, state) assert self._check_port_for(mock, "appserver") == 18080 From 877a259eb43ebe8f8c91149597ec42dc165e4f0f Mon Sep 17 00:00:00 2001 From: jansdhillon Date: Mon, 29 Jun 2026 21:41:45 -0600 Subject: [PATCH 4/7] seconds not milliseconds --- src/charm.py | 10 +++++----- tests/unit/test_haproxy_route.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/charm.py b/src/charm.py index 14236482..102c6921 100755 --- a/src/charm.py +++ b/src/charm.py @@ -1359,7 +1359,7 @@ def _provide_all_haproxy_route_requirements(self) -> None: paths=appserver_paths, protocol="http", check_port=cfg.appserver_base_port, - check_interval=2000, + check_interval=2, check_rise=2, check_fall=3, header_rewrite_expressions=forwarded_proto_https, @@ -1385,7 +1385,7 @@ def _provide_all_haproxy_route_requirements(self) -> None: paths=["/ping"], protocol="http", check_port=cfg.pingserver_base_port, - check_interval=2000, + check_interval=2, check_rise=2, check_fall=3, header_rewrite_expressions=forwarded_proto_https, @@ -1399,7 +1399,7 @@ def _provide_all_haproxy_route_requirements(self) -> None: paths=["/message-system", "/attachment"], protocol="http", check_port=cfg.message_server_base_port, - check_interval=2000, + check_interval=2, check_rise=2, check_fall=3, header_rewrite_expressions=forwarded_proto_https, @@ -1413,7 +1413,7 @@ def _provide_all_haproxy_route_requirements(self) -> None: paths=["/api"], protocol="http", check_port=cfg.api_base_port, - check_interval=2000, + check_interval=2, check_rise=2, check_fall=3, header_rewrite_expressions=forwarded_proto_https, @@ -1427,7 +1427,7 @@ def _provide_all_haproxy_route_requirements(self) -> None: paths=["/upload"], protocol="http", check_port=cfg.package_upload_base_port, - check_interval=2000, + check_interval=2, check_rise=2, check_fall=3, header_rewrite_expressions=forwarded_proto_https, diff --git a/tests/unit/test_haproxy_route.py b/tests/unit/test_haproxy_route.py index d0d7a998..64e4abae 100644 --- a/tests/unit/test_haproxy_route.py +++ b/tests/unit/test_haproxy_route.py @@ -183,7 +183,7 @@ def test_package_upload_has_health_checks(self, replicas_network_state): mock = _run_provide(context, state) calls = _calls_for(mock, "package-upload") assert calls - assert calls[0].kwargs.get("check_interval") == 2000 + assert calls[0].kwargs.get("check_interval") == 2 assert calls[0].kwargs.get("check_rise") == 2 assert calls[0].kwargs.get("check_fall") == 3 @@ -319,7 +319,7 @@ def test_appserver_has_health_check_timing(self, replicas_network_state): state = State(**replicas_network_state) mock = _run_provide(context, state) calls = _calls_for(mock, "appserver") - assert calls[0].kwargs.get("check_interval") == 2000 + assert calls[0].kwargs.get("check_interval") == 2 assert calls[0].kwargs.get("check_rise") == 2 assert calls[0].kwargs.get("check_fall") == 3 @@ -328,7 +328,7 @@ def test_pingserver_has_health_check_timing(self, replicas_network_state): state = State(**replicas_network_state) mock = _run_provide(context, state) calls = _calls_for(mock, "pingserver") - assert calls[0].kwargs.get("check_interval") == 2000 + assert calls[0].kwargs.get("check_interval") == 2 assert calls[0].kwargs.get("check_rise") == 2 assert calls[0].kwargs.get("check_fall") == 3 @@ -337,7 +337,7 @@ def test_message_server_has_health_check_timing(self, replicas_network_state): state = State(**replicas_network_state) mock = _run_provide(context, state) calls = _calls_for(mock, "message-server") - assert calls[0].kwargs.get("check_interval") == 2000 + assert calls[0].kwargs.get("check_interval") == 2 assert calls[0].kwargs.get("check_rise") == 2 assert calls[0].kwargs.get("check_fall") == 3 @@ -346,7 +346,7 @@ def test_api_has_health_check_timing(self, replicas_network_state): state = State(**replicas_network_state) mock = _run_provide(context, state) calls = _calls_for(mock, "api") - assert calls[0].kwargs.get("check_interval") == 2000 + assert calls[0].kwargs.get("check_interval") == 2 assert calls[0].kwargs.get("check_rise") == 2 assert calls[0].kwargs.get("check_fall") == 3 From 517f785d448ab7a646c9191ba9d33518761939f7 Mon Sep 17 00:00:00 2001 From: jansdhillon Date: Mon, 29 Jun 2026 22:12:10 -0600 Subject: [PATCH 5/7] bring back ports --- src/charm.py | 12 +++---- tests/unit/test_haproxy_route.py | 59 ++++++++++++-------------------- 2 files changed, 28 insertions(+), 43 deletions(-) diff --git a/src/charm.py b/src/charm.py index 102c6921..9630bef4 100755 --- a/src/charm.py +++ b/src/charm.py @@ -1358,7 +1358,7 @@ def _provide_all_haproxy_route_requirements(self) -> None: ports=appserver_ports, paths=appserver_paths, protocol="http", - check_port=cfg.appserver_base_port, + check_path="/", check_interval=2, check_rise=2, check_fall=3, @@ -1384,7 +1384,7 @@ def _provide_all_haproxy_route_requirements(self) -> None: ports=pingserver_ports, paths=["/ping"], protocol="http", - check_port=cfg.pingserver_base_port, + check_path="/ping", check_interval=2, check_rise=2, check_fall=3, @@ -1398,7 +1398,7 @@ def _provide_all_haproxy_route_requirements(self) -> None: ports=message_server_ports, paths=["/message-system", "/attachment"], protocol="http", - check_port=cfg.message_server_base_port, + check_path="/message-system", check_interval=2, check_rise=2, check_fall=3, @@ -1412,7 +1412,7 @@ def _provide_all_haproxy_route_requirements(self) -> None: ports=api_ports, paths=["/api"], protocol="http", - check_port=cfg.api_base_port, + check_path="/api", check_interval=2, check_rise=2, check_fall=3, @@ -1426,7 +1426,7 @@ def _provide_all_haproxy_route_requirements(self) -> None: ports=[cfg.package_upload_base_port], paths=["/upload"], protocol="http", - check_port=cfg.package_upload_base_port, + check_path="/upload", check_interval=2, check_rise=2, check_fall=3, @@ -1444,7 +1444,7 @@ def _provide_all_haproxy_route_requirements(self) -> None: ports=appserver_ports, paths=["/repository"], protocol="http", - check_port=cfg.appserver_base_port, + check_path="/", header_rewrite_expressions=forwarded_proto_https, allow_http=allow_http_always, unit_address=unit_ip, diff --git a/tests/unit/test_haproxy_route.py b/tests/unit/test_haproxy_route.py index 64e4abae..1e192b19 100644 --- a/tests/unit/test_haproxy_route.py +++ b/tests/unit/test_haproxy_route.py @@ -253,66 +253,58 @@ def test_pingserver_service_name_prefix(self, replicas_network_state): class TestHealthChecks: - """Each route uses check_port (not check_path) with the correct port number.""" - - SERVICES = [ - ("appserver", 8080), - ("pingserver", 8070), - ("message-server", 8090), - ("api", 9080), - ("package-upload", 9100), - ("repository", 8080), - ] - - def _check_port_for(self, mock, service_fragment): - calls = _calls_for(mock, service_fragment) - assert calls, f"No calls found for service {service_fragment!r}" - return calls[0].kwargs.get("check_port") + """Each route uses check_path with the path the backend actually serves.""" - def test_no_check_path_on_any_route(self, replicas_network_state): + def test_no_check_port_on_any_route(self, replicas_network_state): context = Context(LandscapeServerCharm) state = State(**replicas_network_state) mock = _run_provide(context, state) for c in mock.call_args_list: - assert "check_path" not in c.kwargs, ( - f"check_path should not be set for {c.kwargs.get('service')}" + assert "check_port" not in c.kwargs, ( + f"check_port should not be set for {c.kwargs.get('service')}" ) - def test_appserver_check_port(self, replicas_network_state): + def test_appserver_check_path(self, replicas_network_state): context = Context(LandscapeServerCharm) state = State(**replicas_network_state) mock = _run_provide(context, state) - assert self._check_port_for(mock, "appserver") == 8080 + calls = _calls_for(mock, "appserver") + assert calls[0].kwargs.get("check_path") == "/" - def test_pingserver_check_port(self, replicas_network_state): + def test_pingserver_check_path(self, replicas_network_state): context = Context(LandscapeServerCharm) state = State(**replicas_network_state) mock = _run_provide(context, state) - assert self._check_port_for(mock, "pingserver") == 8070 + calls = _calls_for(mock, "pingserver") + assert calls[0].kwargs.get("check_path") == "/ping" - def test_message_server_check_port(self, replicas_network_state): + def test_message_server_check_path(self, replicas_network_state): context = Context(LandscapeServerCharm) state = State(**replicas_network_state) mock = _run_provide(context, state) - assert self._check_port_for(mock, "message-server") == 8090 + calls = _calls_for(mock, "message-server") + assert calls[0].kwargs.get("check_path") == "/message-system" - def test_api_check_port(self, replicas_network_state): + def test_api_check_path(self, replicas_network_state): context = Context(LandscapeServerCharm) state = State(**replicas_network_state) mock = _run_provide(context, state) - assert self._check_port_for(mock, "api") == 9080 + calls = _calls_for(mock, "api") + assert calls[0].kwargs.get("check_path") == "/api" - def test_package_upload_check_port(self, replicas_network_state): + def test_package_upload_check_path(self, replicas_network_state): context = Context(LandscapeServerCharm) state = State(**replicas_network_state) mock = _run_provide(context, state) - assert self._check_port_for(mock, "package-upload") == 9100 + calls = _calls_for(mock, "package-upload") + assert calls[0].kwargs.get("check_path") == "/upload" - def test_repository_check_port(self, replicas_network_state): + def test_repository_check_path(self, replicas_network_state): context = Context(LandscapeServerCharm) state = State(**replicas_network_state) mock = _run_provide(context, state) - assert self._check_port_for(mock, "repository") == 8080 + calls = _calls_for(mock, "repository") + assert calls[0].kwargs.get("check_path") == "/" def test_appserver_has_health_check_timing(self, replicas_network_state): context = Context(LandscapeServerCharm) @@ -350,13 +342,6 @@ def test_api_has_health_check_timing(self, replicas_network_state): assert calls[0].kwargs.get("check_rise") == 2 assert calls[0].kwargs.get("check_fall") == 3 - def test_custom_base_port_reflected_in_check_port(self, replicas_network_state): - """check_port follows config, not a hardcoded value.""" - context = Context(LandscapeServerCharm) - state = State(config={"appserver_base_port": 18080}, **replicas_network_state) - mock = _run_provide(context, state) - assert self._check_port_for(mock, "appserver") == 18080 - class TestConditionalRoutes: """hostagent-messenger and ubuntu-installer-attach routes are conditional.""" From 4a093ce9dfdbd86ec6a55b6a11ebfe3f31ab0197 Mon Sep 17 00:00:00 2001 From: jansdhillon Date: Mon, 29 Jun 2026 22:32:20 -0600 Subject: [PATCH 6/7] fix api health cehck --- src/charm.py | 2 +- tests/unit/test_haproxy_route.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/charm.py b/src/charm.py index 9630bef4..6c2daa73 100755 --- a/src/charm.py +++ b/src/charm.py @@ -1412,7 +1412,7 @@ def _provide_all_haproxy_route_requirements(self) -> None: ports=api_ports, paths=["/api"], protocol="http", - check_path="/api", + check_path="/api/about", check_interval=2, check_rise=2, check_fall=3, diff --git a/tests/unit/test_haproxy_route.py b/tests/unit/test_haproxy_route.py index 1e192b19..a534f0b4 100644 --- a/tests/unit/test_haproxy_route.py +++ b/tests/unit/test_haproxy_route.py @@ -290,7 +290,7 @@ def test_api_check_path(self, replicas_network_state): state = State(**replicas_network_state) mock = _run_provide(context, state) calls = _calls_for(mock, "api") - assert calls[0].kwargs.get("check_path") == "/api" + assert calls[0].kwargs.get("check_path") == "/api/about" def test_package_upload_check_path(self, replicas_network_state): context = Context(LandscapeServerCharm) From eead10fbfaf658aa5bcf0a3a2e431aaa7c28cbf2 Mon Sep 17 00:00:00 2001 From: jansdhillon Date: Mon, 29 Jun 2026 22:57:35 -0600 Subject: [PATCH 7/7] rm redundant repository check --- src/charm.py | 1 - tests/unit/test_haproxy_route.py | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/charm.py b/src/charm.py index 6c2daa73..b684d40c 100755 --- a/src/charm.py +++ b/src/charm.py @@ -1444,7 +1444,6 @@ def _provide_all_haproxy_route_requirements(self) -> None: ports=appserver_ports, paths=["/repository"], protocol="http", - check_path="/", header_rewrite_expressions=forwarded_proto_https, allow_http=allow_http_always, unit_address=unit_ip, diff --git a/tests/unit/test_haproxy_route.py b/tests/unit/test_haproxy_route.py index a534f0b4..884014ff 100644 --- a/tests/unit/test_haproxy_route.py +++ b/tests/unit/test_haproxy_route.py @@ -299,12 +299,13 @@ def test_package_upload_check_path(self, replicas_network_state): calls = _calls_for(mock, "package-upload") assert calls[0].kwargs.get("check_path") == "/upload" - def test_repository_check_path(self, replicas_network_state): + def test_repository_has_no_health_check(self, replicas_network_state): context = Context(LandscapeServerCharm) state = State(**replicas_network_state) mock = _run_provide(context, state) calls = _calls_for(mock, "repository") - assert calls[0].kwargs.get("check_path") == "/" + assert "check_path" not in calls[0].kwargs + assert "check_interval" not in calls[0].kwargs def test_appserver_has_health_check_timing(self, replicas_network_state): context = Context(LandscapeServerCharm)