Add backend_port_range attribute to haproxy-route-tcp relation#526
Add backend_port_range attribute to haproxy-route-tcp relation#526Copilot wants to merge 9 commits into
Conversation
Add a backend_port_range attribute to the haproxy-route-tcp relation data model that allows mapping from multiple frontend ports to multiple backend ports. When set, one frontend is created per port in the range and the port is omitted from server entries in the backend config. Includes: - Pydantic validation for format and exclusivity with port/backend_port - Port conflict detection across entire port range - Template update to handle servers without port - Unit tests for validation, parsing, conflict detection, and rendering - Change artifact and changelog entry Agent-Logs-Url: https://github.com/canonical/haproxy-operator/sessions/d5cb1c4e-cd2d-413e-92de-c2f2312d9ffd Co-authored-by: Thanhphan1147 <42444001+Thanhphan1147@users.noreply.github.com>
Agent-Logs-Url: https://github.com/canonical/haproxy-operator/sessions/99bf8fb4-a9fe-494a-ad1b-655c689f9596 Co-authored-by: Thanhphan1147 <42444001+Thanhphan1147@users.noreply.github.com>
Agent-Logs-Url: https://github.com/canonical/haproxy-operator/sessions/c77af75b-61df-4b3a-a029-93e8f3367f1d Co-authored-by: Thanhphan1147 <42444001+Thanhphan1147@users.noreply.github.com>
| {% if server.port %} | ||
| {{ server.address }}:{{ server.port }} | ||
| {% else %} | ||
| {{ server.address }} | ||
| {% endif %} |
There was a problem hiding this comment.
@copilot We try not to have too much logic in the j2 templates. Refactor this to a property in the haproxy-route charmstate and render only the value here.
There was a problem hiding this comment.
Refactored into a server_address property on HaproxyRouteTcpServer in the charmstate. The template now just renders {{ server.server_address }} with no conditional logic.
| for port in endpoint.application_data.requested_ports_in_range: | ||
| port_to_backends_mapping[port].append(endpoint) |
There was a problem hiding this comment.
@copilot Don't do this because it will cause the haproxy config to become too big. Every backend with a port range will have its dedicated frontend that has bind :<start_port>-<endport>. A frontend with a port range will still need to be checked for conflicts against other TCP frontends. Conflicts will be determined by checking for overlap.
There was a problem hiding this comment.
Refactored in the latest commit. Port-range backends now get a single dedicated frontend with bind [::]:4000-4005 v4v6 instead of being expanded into N separate frontends. Added bind_port property (returns "4000-4005" or "4000") and covered_ports property (returns all ports in the range) to HAProxyRouteTcpFrontend. Conflict detection uses covered_ports to check for overlap against other TCP frontends and HTTP/gRPC ports.
Instead of expanding a port range into N separate frontends, each port-range backend now gets a single dedicated frontend with bind :<start_port>-<end_port>. Conflict detection checks for overlap between port ranges and individual ports via covered_ports.
What this PR does
Adds a
backend_port_rangefield toTcpRequirerApplicationDatain thehaproxy-route-tcplibrary (v1, LIBPATCH 4). When set (e.g.,"4000-4005"), it:bind :<start_port>-<end_port>for the entire rangeopen_ports(expanded viacovered_portsproperty)Library changes:
backend_port_range: Optional[str]field with pydantic validators for format (split on-delimiter, validate exactly 2 valid port numbers), range bounds, and mutual exclusivity withport/backend_portrequested_ports_in_rangeproperty returning the expanded list of portsconfigure_port_range()chainable method on the requirercheck_ports_uniqueto detect conflicts across entire ranges (overlap detection)Operator state changes:
HaproxyRouteTcpServer.portis nowOptional[int](None in port-range mode)HaproxyRouteTcpServer.server_addressproperty encapsulates address formatting logic (returnsaddress:portor justaddressdepending on mode)HAProxyRouteTcpFrontendgainsport_range_end,bind_port, andcovered_portspropertiesparse_haproxy_route_tcp_requirers_datacreates a single frontend per port-range backend (no expansion into N frontends)HAProxyRouteTcpFrontend.from_backendsdeterminesport_range_endfrom the backend's rangecovered_portsfor overlap checking between port-range frontends and other TCP/HTTP/gRPC portsTemplate:
render_tcp_serverusesserver.server_addressproperty directly; frontend bind usesfrontend.bind_port— no conditional logic in the template.Why we need it
Workloads that listen on a contiguous port range (e.g., database shards, media servers) need all ports exposed through HAProxy without requiring N separate relations or manual per-port configuration. Using a single frontend with
bind :<start>-<end>keeps the HAProxy config compact rather than creating one frontend per port.Checklist
docs/changelog.mdwith user-relevant changesdocs/release-notes/artifacts.(e.g., in
.github/workflows/integration_tests.yaml, ensure themoduleslist is correct)terraform fmtpasses andtflintreports no errorsTest plan
port/backend_port, reversed range rejectionbind [::]:5000-5002, servers without:portsuffix)Review focus
TcpRequirerApplicationData— validators run sequentially;validate_port_range_exclusivitydepends onvalidate_backend_port_range_formathaving already validated the formatportfieldOptional[int]is a library API change — existing requirers always passport, so backwards-compatible, but worth confirming no downstream assumptions onportbeing non-Nonebind_portproperty returns"4000-4005"for range frontends or"4000"for single-port frontends — used directly in the template's bind directivecovered_portsproperty returns all ports in a range — used for conflict detection andopen_portsexpansion-delimiter (no regex) — validates exactly 2 elements and both are valid port numbersserver_addressproperty onHaproxyRouteTcpServerkeeps address formatting logic in the charmstate rather than in the Jinja2 templatePotential breaking changes
TcpRequirerApplicationData.portis nowOptional[int](wasint). All existing usage passesport, so no runtime breakage, but type checkers downstream may flag this.HaproxyRouteTcpServer.portis nowOptional[int]. Theserver_addressproperty handles both cases.