Skip to content
Draft
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
6 changes: 4 additions & 2 deletions haproxy-operator/src/haproxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ def reconcile_ingress(
"config_external_hostname": external_hostname,
"haproxy_crt_dir": HAPROXY_CERTS_DIR,
"ddos_protection_config": ddos_protection_config,
"peer_units_address": ingress_requirers_information.peers,
"peer_units_address": ingress_requirers_information.formatted_peer_entries,
"peer_tcp_port": ingress_requirers_information.peer_tcp_port,
"ip_allow_list_file": IP_ALLOW_LIST_FILE,
"deny_paths_file": DENY_PATHS_FILE,
}
Expand Down Expand Up @@ -191,7 +192,8 @@ def reconcile_haproxy_route(
if backend.application_data.external_grpc_port
],
"stick_table_entries": haproxy_route_requirers_information.stick_table_entries,
"peer_units_address": haproxy_route_requirers_information.peers,
"peer_units_address": haproxy_route_requirers_information.formatted_peer_entries,
"peer_tcp_port": haproxy_route_requirers_information.peer_tcp_port,
"haproxy_crt_dir": HAPROXY_CERTS_DIR,
"acls_for_allow_http": haproxy_route_requirers_information.acls_for_allow_http,
"spoe_auth_info_list": spoe_oauth_info_list,
Expand Down
3 changes: 2 additions & 1 deletion haproxy-operator/src/state/haproxy_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
HAProxyRouteTcpFrontend,
HAProxyRouteTcpFrontendValidationError,
)
from .peers import PeersInformation

HAPROXY_ROUTE_RELATION = "haproxy-route"
HAPROXY_PEER_INTEGRATION = "haproxy-peers"
Expand Down Expand Up @@ -272,7 +273,7 @@ def enable_http_check(self) -> bool:

# pylint: disable=too-many-locals
@dataclass(frozen=True)
class HaproxyRouteRequirersInformation:
class HaproxyRouteRequirersInformation(PeersInformation):
"""A component of charm state containing haproxy-route requirers information.

Attrs:
Expand Down
3 changes: 2 additions & 1 deletion haproxy-operator/src/state/ingress.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pydantic import IPvAnyAddress

from .exception import CharmStateValidationBaseError
from .peers import PeersInformation

INGRESS_RELATION = "ingress"

Expand Down Expand Up @@ -49,7 +50,7 @@ class HAProxyBackend:


@dataclasses.dataclass(frozen=True)
class IngressRequirersInformation:
class IngressRequirersInformation(PeersInformation):
"""A component of charm state containing ingress requirers information.

Attrs:
Expand Down
3 changes: 2 additions & 1 deletion haproxy-operator/src/state/ingress_per_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pydantic.dataclasses import dataclass

from .exception import CharmStateValidationBaseError
from .peers import PeersInformation

INGRESS_PER_UNIT_RELATION = "ingress_per_unit"

Expand Down Expand Up @@ -42,7 +43,7 @@ class HAProxyBackend:


@dataclass(frozen=True)
class IngressPerUnitRequirersInformation:
class IngressPerUnitRequirersInformation(PeersInformation):
"""A component of charm state containing ingress per unit requirers information.

Attrs:
Expand Down
47 changes: 47 additions & 0 deletions haproxy-operator/src/state/peers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Copyright 2025 Canonical Ltd.
# See LICENSE file for licensing details.

"""HAProxy peers information mixin for charm state components."""

from pydantic import IPvAnyAddress

HAPROXY_PEER_PORT = 10000


class PeersInformation:
"""Mixin providing HAProxy peer formatting properties.

Subclasses must define a ``peers: list[IPvAnyAddress]`` field.

Attrs:
formatted_peer_entries: Pre-rendered peer entry strings for the HAProxy config.
peer_tcp_port: The TCP port used for HAProxy peer communication.
"""

peers: list[IPvAnyAddress]

@property
def formatted_peer_entries(self) -> list[str]:
"""Format peer IP addresses into HAProxy peer entry strings.

Each entry is formatted as ``<name> <address>:<port>`` where ``<name>``
is derived from the IP address with non-alphanumeric characters replaced
by hyphens.

Returns:
list[str]: Formatted peer entry strings.
"""
entries: list[str] = []
for addr in self.peers:
name = str(addr).replace(".", "-").replace(":", "-")
entries.append(f"{name} {addr}:{HAPROXY_PEER_PORT}")
return entries

@property
def peer_tcp_port(self) -> int:
"""Return the TCP port used for HAProxy peer communication.

Returns:
int: The peer TCP port.
"""
return HAPROXY_PEER_PORT
5 changes: 3 additions & 2 deletions haproxy-operator/templates/haproxy_ingress.cfg.j2
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ frontend ingress

{% if ddos_protection_config.has_rate_limiting %}
peers haproxy_peers
{% for address in peer_units_address %}
peer {{ address }}
bind *:{{ peer_tcp_port }}
{% for entry in peer_units_address %}
peer {{ entry }}
{% endfor %}
table ddos_protection_ip type ip size 100k expire 2m store http_req_rate(1m),conn_rate(1m),conn_cur
{% endif %}
Expand Down
5 changes: 3 additions & 2 deletions haproxy-operator/templates/haproxy_ingress_per_unit.cfg.j2
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ frontend ingress_per_unit

{% if ddos_protection_config.has_rate_limiting %}
peers haproxy_peers
{% for address in peer_units_address %}
peer {{ address }}
bind *:{{ peer_tcp_port }}
{% for entry in peer_units_address %}
peer {{ entry }}
{% endfor %}
table ddos_protection_ip type ip size 100k expire 2m store http_req_rate(1m),conn_rate(1m),http_err_rate(1m),conn_cur
{% endif %}
Expand Down
5 changes: 3 additions & 2 deletions haproxy-operator/templates/haproxy_route.cfg.j2
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ frontend haproxy
default_backend default

peers haproxy_peers
{% for address in peer_units_address %}
peer {{ address }}
bind *:{{ peer_tcp_port }}
{% for entry in peer_units_address %}
peer {{ entry }}
{% endfor %}
{% for entry in stick_table_entries %}
table {{ entry }} type ip size 100k expire 2m store http_req_rate(1m)
Expand Down
72 changes: 72 additions & 0 deletions haproxy-operator/tests/unit/test_peers_information.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Copyright 2025 Canonical Ltd.
# See LICENSE file for licensing details.

"""Unit tests for the PeersInformation mixin."""

from ipaddress import IPv4Address, IPv6Address
from typing import cast

from pydantic import IPvAnyAddress

from state.peers import HAPROXY_PEER_PORT, PeersInformation


class _PeersInfoStub(PeersInformation):
"""Minimal concrete class for testing the mixin."""

def __init__(self, peers: list[IPvAnyAddress]):
self.peers = peers


def test_formatted_peer_entries_ipv4():
"""
arrange: A PeersInformation instance with IPv4 peer addresses.
act: Access the formatted_peer_entries property.
assert: Each entry has the format '<name> <address>:<port>'.
"""
info = _PeersInfoStub(
[
cast(IPvAnyAddress, IPv4Address("10.68.79.144")),
cast(IPvAnyAddress, IPv4Address("192.168.1.10")),
]
)

assert info.formatted_peer_entries == [
f"10-68-79-144 10.68.79.144:{HAPROXY_PEER_PORT}",
f"192-168-1-10 192.168.1.10:{HAPROXY_PEER_PORT}",
]


def test_formatted_peer_entries_ipv6():
"""
arrange: A PeersInformation instance with an IPv6 peer address.
act: Access the formatted_peer_entries property.
assert: Colons are replaced by hyphens in the peer name.
"""
info = _PeersInfoStub([cast(IPvAnyAddress, IPv6Address("fd42:bc01:a5e3:f4e2::1"))])

assert info.formatted_peer_entries == [
f"fd42-bc01-a5e3-f4e2--1 fd42:bc01:a5e3:f4e2::1:{HAPROXY_PEER_PORT}",
]


def test_formatted_peer_entries_empty():
"""
arrange: A PeersInformation instance with no peers.
act: Access the formatted_peer_entries property.
assert: Returns an empty list.
"""
info = _PeersInfoStub([])

assert info.formatted_peer_entries == []


def test_peer_tcp_port():
"""
arrange: A PeersInformation instance.
act: Access the peer_tcp_port property.
assert: Returns the constant HAPROXY_PEER_PORT.
"""
info = _PeersInfoStub([])

assert info.peer_tcp_port == HAPROXY_PEER_PORT