From f7da39c6173ec7ef41e18227c96b7e75db625638 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:04:37 +0000 Subject: [PATCH 1/3] Update dependency charmlibs-snap to v2 --- haproxy-route-policy-operator/pyproject.toml | 2 +- haproxy-route-policy-operator/uv.lock | 11 ++++------- haproxy-spoe-auth-operator/pyproject.toml | 2 +- haproxy-spoe-auth-operator/uv.lock | 11 ++++------- 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/haproxy-route-policy-operator/pyproject.toml b/haproxy-route-policy-operator/pyproject.toml index 39f9f0a0c..1ce0631ba 100644 --- a/haproxy-route-policy-operator/pyproject.toml +++ b/haproxy-route-policy-operator/pyproject.toml @@ -16,7 +16,7 @@ classifiers = [ dependencies = [ "ops==3.9.0", "requests==2.34.2", - "charmlibs-snap==1.0.1.post0", + "charmlibs-snap==2.0.0", "pydantic>=2.12.5", "validators>=0.35.0", ] diff --git a/haproxy-route-policy-operator/uv.lock b/haproxy-route-policy-operator/uv.lock index 0b5971d02..3bf32234e 100644 --- a/haproxy-route-policy-operator/uv.lock +++ b/haproxy-route-policy-operator/uv.lock @@ -250,14 +250,11 @@ wheels = [ [[package]] name = "charmlibs-snap" -version = "1.0.1.post0" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/85/33/5043f1dd01e485e1c855516e41694fae48f48e94dd52e6081e5b61eee649/charmlibs_snap-1.0.1.post0.tar.gz", hash = "sha256:1e8da752ee405aa981665da8c4772016236baf80d2d902b02ec8c1c640fb2844", size = 29393, upload-time = "2026-06-16T05:39:54.911Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/f1/b1af81b093f4b94f957a2d2efb20b78975f615a6384d44917c633f77ce82/charmlibs_snap-2.0.0.tar.gz", hash = "sha256:82bc863919614436bf4e8e38cedaa4d809c3d4e1eedab10b2eeb97006116cd29", size = 125387, upload-time = "2026-09-02T21:32:27.485Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/71/fa658e05748a2ffd1185aeec43aefb286c33c2767e28919f0445b464244a/charmlibs_snap-1.0.1.post0-py3-none-any.whl", hash = "sha256:655041096952c63e92c8d25a6183149f0594d4888be4be62a9635cb96897aa01", size = 15416, upload-time = "2026-06-16T05:39:53.629Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/65d7e73532c50f2533b8f1a735894b5d260202f60d5ab05e367b93382e13/charmlibs_snap-2.0.0-py3-none-any.whl", hash = "sha256:6e479163ede22f9ab4b66adead2d4ce4cdc6eb77f36f88c50c4d3d3b075da040", size = 38470, upload-time = "2026-09-02T21:32:26.129Z" }, ] [[package]] @@ -531,7 +528,7 @@ unit = [ [package.metadata] requires-dist = [ - { name = "charmlibs-snap", specifier = "==1.0.1.post0" }, + { name = "charmlibs-snap", specifier = "==2.0.0" }, { name = "ops", specifier = "==3.9.0" }, { name = "pydantic", specifier = ">=2.12.5" }, { name = "requests", specifier = "==2.34.2" }, diff --git a/haproxy-spoe-auth-operator/pyproject.toml b/haproxy-spoe-auth-operator/pyproject.toml index b671a9507..01e371756 100644 --- a/haproxy-spoe-auth-operator/pyproject.toml +++ b/haproxy-spoe-auth-operator/pyproject.toml @@ -14,7 +14,7 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = [ - "charmlibs-snap==1.0.1.post0", + "charmlibs-snap==2.0.0", "jinja2==3.1.6", "jsonschema==4.26.0", "ops==3.9.0", diff --git a/haproxy-spoe-auth-operator/uv.lock b/haproxy-spoe-auth-operator/uv.lock index f835037d8..5e621743c 100644 --- a/haproxy-spoe-auth-operator/uv.lock +++ b/haproxy-spoe-auth-operator/uv.lock @@ -217,14 +217,11 @@ wheels = [ [[package]] name = "charmlibs-snap" -version = "1.0.1.post0" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/85/33/5043f1dd01e485e1c855516e41694fae48f48e94dd52e6081e5b61eee649/charmlibs_snap-1.0.1.post0.tar.gz", hash = "sha256:1e8da752ee405aa981665da8c4772016236baf80d2d902b02ec8c1c640fb2844", size = 29393, upload-time = "2026-06-16T05:39:54.911Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/f1/b1af81b093f4b94f957a2d2efb20b78975f615a6384d44917c633f77ce82/charmlibs_snap-2.0.0.tar.gz", hash = "sha256:82bc863919614436bf4e8e38cedaa4d809c3d4e1eedab10b2eeb97006116cd29", size = 125387, upload-time = "2026-09-02T21:32:27.485Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/71/fa658e05748a2ffd1185aeec43aefb286c33c2767e28919f0445b464244a/charmlibs_snap-1.0.1.post0-py3-none-any.whl", hash = "sha256:655041096952c63e92c8d25a6183149f0594d4888be4be62a9635cb96897aa01", size = 15416, upload-time = "2026-06-16T05:39:53.629Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/65d7e73532c50f2533b8f1a735894b5d260202f60d5ab05e367b93382e13/charmlibs_snap-2.0.0-py3-none-any.whl", hash = "sha256:6e479163ede22f9ab4b66adead2d4ce4cdc6eb77f36f88c50c4d3d3b075da040", size = 38470, upload-time = "2026-09-02T21:32:26.129Z" }, ] [[package]] @@ -515,7 +512,7 @@ unit = [ [package.metadata] requires-dist = [ - { name = "charmlibs-snap", specifier = "==1.0.1.post0" }, + { name = "charmlibs-snap", specifier = "==2.0.0" }, { name = "jinja2", specifier = "==3.1.6" }, { name = "jsonschema", specifier = "==4.26.0" }, { name = "ops", specifier = "==3.9.0" }, From 18b0f64f4db3a8a56499569588bbfd43912946e6 Mon Sep 17 00:00:00 2001 From: "Sebastien Georget (via charmkeeper)" Date: Fri, 16 Oct 2026 08:53:25 +0200 Subject: [PATCH 2/3] Migrate haproxy-route-policy-operator and haproxy-spoe-auth-operator to charmlibs-snap v2 API --- haproxy-route-policy-operator/src/policy.py | 18 +++++++++--------- .../src/state/validation.py | 2 +- .../src/haproxy_spoe_auth_service.py | 11 ++++------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/haproxy-route-policy-operator/src/policy.py b/haproxy-route-policy-operator/src/policy.py index e3422993c..c7ec5ea21 100644 --- a/haproxy-route-policy-operator/src/policy.py +++ b/haproxy-route-policy-operator/src/policy.py @@ -17,6 +17,7 @@ from pydantic.dataclasses import dataclass SNAP_NAME = "haproxy-route-policy" +GUNICORN_SERVICE_NAME = "gunicorn" DEFAULT_ENDPOINT = "http://localhost:8080" logger = logging.getLogger(__name__) @@ -27,15 +28,12 @@ class HaproxyRoutePolicyDatabaseMigrationError(Exception): def install_snap(channel: str = "latest/edge") -> None: """Install or refresh the route-policy snap.""" - cache = snap.SnapCache() - package = cache[SNAP_NAME] - package.ensure(snap.SnapState.Latest, channel=channel) + snap.ensure_installed(SNAP_NAME, channel=channel) def configure_snap(config: dict[str, Any]) -> None: """Apply snap configuration if any value changed.""" - package = snap.SnapCache()[SNAP_NAME] - package.set(config, typed=True) + snap.set(SNAP_NAME, config) def run_migrations() -> None: @@ -55,14 +53,16 @@ def run_migrations() -> None: def start_gunicorn_service() -> None: """Ensure the snap gunicorn app is running.""" - package = snap.SnapCache()[SNAP_NAME] - package.start() + snap.start(SNAP_NAME, GUNICORN_SERVICE_NAME) def is_service_active() -> bool: """Check if the snap gunicorn app is active.""" - package = snap.SnapCache()[SNAP_NAME] - return package.services["gunicorn"].get("active", False) + result = subprocess.run( # nosec + ["systemctl", "is-active", "--quiet", f"snap.{SNAP_NAME}.{GUNICORN_SERVICE_NAME}.service"], + check=False, + ) + return result.returncode == 0 def create_or_update_user(username: str, password: str) -> None: diff --git a/haproxy-route-policy-operator/src/state/validation.py b/haproxy-route-policy-operator/src/state/validation.py index 5d0cd5422..c3cfc64a0 100644 --- a/haproxy-route-policy-operator/src/state/validation.py +++ b/haproxy-route-policy-operator/src/state/validation.py @@ -8,7 +8,7 @@ import typing import ops -from charmlibs.snap import SnapError +from charmlibs.snap import Error as SnapError from pydantic import ValidationError from policy import HaproxyRoutePolicyAPIError, HaproxyRoutePolicyDatabaseMigrationError diff --git a/haproxy-spoe-auth-operator/src/haproxy_spoe_auth_service.py b/haproxy-spoe-auth-operator/src/haproxy_spoe_auth_service.py index affacfa02..bf705866f 100644 --- a/haproxy-spoe-auth-operator/src/haproxy_spoe_auth_service.py +++ b/haproxy-spoe-auth-operator/src/haproxy_spoe_auth_service.py @@ -44,8 +44,6 @@ class SpoeAuthService: def __init__(self) -> None: """Initialize the service.""" self._template_dir = Path(__file__).parent.parent / "templates" - cache = snap.SnapCache() - self.haproxy_spoe_auth_snap = cache[SNAP_NAME] def install(self) -> None: """Install the haproxy-spoe-auth snap. @@ -54,9 +52,8 @@ def install(self) -> None: SpoeAuthServiceInstallError: When snap installation fails. """ try: - if not self.haproxy_spoe_auth_snap.present: - self.haproxy_spoe_auth_snap.ensure(snap.SnapState.Latest, channel=SNAP_CHANNEL) - except snap.SnapError as exc: + snap.ensure_installed(SNAP_NAME, channel=SNAP_CHANNEL) + except snap.Error as exc: logger.error( "An exception occurred when installing the haproxy-spoe-auth snap: %s", str(exc), @@ -75,10 +72,10 @@ def reconcile(self, charm_state: CharmState, oauth_information: OauthInformation """ try: self._render_config(charm_state, oauth_information) - self.haproxy_spoe_auth_snap.restart(reload=True) + snap.restart(SNAP_NAME) # Ignore bandit rule as this is only used to reset the service restart counter. subprocess.check_call(["/usr/bin/systemctl", "reset-failed", SNAP_SERVICE_NAME]) # nosec subprocess_without_shell_equals_true - except (snap.SnapError, subprocess.CalledProcessError) as exc: + except (snap.Error, subprocess.CalledProcessError) as exc: raise SpoeAuthServiceConfigError( f"Failed to reconcile the haproxy-spoe-auth service: {exc}" ) from exc From bb489ac54889cd4c01c888645ca7f172c959b6bc Mon Sep 17 00:00:00 2001 From: "Sebastien Georget (via charmkeeper)" Date: Sat, 17 Oct 2026 21:04:52 +0200 Subject: [PATCH 3/3] Fix flaky test_client_ip_hash_salt integration test by retrying the journalctl log assertion for up to 30s, since haproxy logs reach journald asynchronously via syslog --- .../tests/integration/test_config.py | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/haproxy-operator/tests/integration/test_config.py b/haproxy-operator/tests/integration/test_config.py index 33c04475a..78596a080 100644 --- a/haproxy-operator/tests/integration/test_config.py +++ b/haproxy-operator/tests/integration/test_config.py @@ -7,10 +7,28 @@ import jubilant import pytest +import tenacity from .conftest import LOG_HASH_SALT, all_active_and_idle +def _assert_log_contains(juju: jubilant.Juju, unit: str, expected: str) -> None: + """Assert that the expected string eventually appears in haproxy's journal. + + HAProxy sends logs to journald asynchronously via syslog, so the log line + for a request may not be visible immediately after the request completes. + Retry for a few seconds to avoid flaky failures. + """ + for attempt in tenacity.Retrying( + stop=tenacity.stop_after_delay(30), + wait=tenacity.wait_fixed(2), + reraise=True, + ): + with attempt: + haproxy_logs = juju.exec("journalctl -u haproxy --no-pager -n 50", unit=unit).stdout + assert expected in haproxy_logs + + def test_client_ip_hash_salt( configured_application_with_tls: str, log_hash_secret, @@ -33,8 +51,7 @@ def test_client_ip_hash_salt( # HAProxy's hex converter outputs uppercase. expected_hash = hashlib.sha256(("127.0.0.1" + LOG_HASH_SALT).encode()).hexdigest().upper() - haproxy_logs = juju.exec("journalctl -u haproxy --no-pager -n 50", unit=unit).stdout - assert expected_hash in haproxy_logs + _assert_log_contains(juju, unit, expected_hash) request.getfixturevalue("haproxy_route_tcp_plain_tcp_relation") # Fix the source port because %cp logs the client port, not the listener port. @@ -42,5 +59,4 @@ def test_client_ip_hash_salt( assert "pong" in nc_result.stdout expected_tcp_log = f"{expected_hash}:45555" - haproxy_logs = juju.exec("journalctl -u haproxy --no-pager -n 50", unit=unit).stdout - assert expected_tcp_log in haproxy_logs + _assert_log_contains(juju, unit, expected_tcp_log)