Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions haproxy-operator/tests/integration/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -33,14 +51,12 @@ 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.
nc_result = juju.exec("printf 'ping\\n' | nc -p 45555 127.0.0.1 4444", unit=unit)
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)
2 changes: 1 addition & 1 deletion haproxy-route-policy-operator/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down
18 changes: 9 additions & 9 deletions haproxy-route-policy-operator/src/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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:
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion haproxy-route-policy-operator/src/state/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 4 additions & 7 deletions haproxy-route-policy-operator/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion haproxy-spoe-auth-operator/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 4 additions & 7 deletions haproxy-spoe-auth-operator/src/haproxy_spoe_auth_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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),
Expand All @@ -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
Expand Down
11 changes: 4 additions & 7 deletions haproxy-spoe-auth-operator/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading