Skip to content

Add backend_port_range attribute to haproxy-route-tcp relation#526

Open
Copilot wants to merge 9 commits into
mainfrom
copilot/add-port-range-attribute-haproxy
Open

Add backend_port_range attribute to haproxy-route-tcp relation#526
Copilot wants to merge 9 commits into
mainfrom
copilot/add-port-range-attribute-haproxy

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 26, 2026

What this PR does

Adds a backend_port_range field to TcpRequirerApplicationData in the haproxy-route-tcp library (v1, LIBPATCH 4). When set (e.g., "4000-4005"), it:

  • Creates a single dedicated HAProxy frontend with bind :<start_port>-<end_port> for the entire range
  • Omits the port from server entries in backend config (HAProxy binds the frontend port range directly)
  • Exposes all ports in the range via open_ports (expanded via covered_ports property)

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 with port/backend_port
  • requested_ports_in_range property returning the expanded list of ports
  • configure_port_range() chainable method on the requirer
  • Updated check_ports_unique to detect conflicts across entire ranges (overlap detection)

Operator state changes:

  • HaproxyRouteTcpServer.port is now Optional[int] (None in port-range mode)
  • HaproxyRouteTcpServer.server_address property encapsulates address formatting logic (returns address:port or just address depending on mode)
  • HAProxyRouteTcpFrontend gains port_range_end, bind_port, and covered_ports properties
  • parse_haproxy_route_tcp_requirers_data creates a single frontend per port-range backend (no expansion into N frontends)
  • HAProxyRouteTcpFrontend.from_backends determines port_range_end from the backend's range
  • Conflict detection uses covered_ports for overlap checking between port-range frontends and other TCP/HTTP/gRPC ports

Template: render_tcp_server uses server.server_address property directly; frontend bind uses frontend.bind_port — no conditional logic in the template.

# Requirer usage
self.haproxy_route_tcp_requirer = HaproxyRouteTcpRequirer(
    self, "haproxy-route-tcp", backend_port_range="5000-5010"
)
# Or via chaining
self.haproxy_route_tcp_requirer.configure_port_range("5000-5010")

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

  • I followed the contributing guide
  • I added or updated the documentation (if applicable)
  • I updated docs/changelog.md with user-relevant changes
  • I added a change artifact for user-relevant changes in docs/release-notes/artifacts.
  • I used AI to assist with preparing this PR
  • I added or updated tests as needed (unit and integration)
  • If integration test modules are used: I updated the workflow configuration
    (e.g., in .github/workflows/integration_tests.yaml, ensure the modules list is correct)
  • If this PR involves a Grafana dashboard: I added a screenshot of the dashboard
  • If this PR involves Terraform: terraform fmt passes and tflint reports no errors

Test plan

  • 7 new unit tests covering: valid port range parsing, single frontend creation from range, conflict detection (range overlapping single port), invalid format rejection, mutual exclusivity with port/backend_port, reversed range rejection
  • Template rendering test verifying correct HAProxy config output (single frontend with bind [::]:5000-5002, servers without :port suffix)
  • Full existing test suite (247 tests) passes without regression

Review focus

  • Validator ordering in TcpRequirerApplicationData — validators run sequentially; validate_port_range_exclusivity depends on validate_backend_port_range_format having already validated the format
  • Making port field Optional[int] is a library API change — existing requirers always pass port, so backwards-compatible, but worth confirming no downstream assumptions on port being non-None
  • The bind_port property returns "4000-4005" for range frontends or "4000" for single-port frontends — used directly in the template's bind directive
  • The covered_ports property returns all ports in a range — used for conflict detection and open_ports expansion
  • Port range format validation uses simple string split on - delimiter (no regex) — validates exactly 2 elements and both are valid port numbers
  • The server_address property on HaproxyRouteTcpServer keeps address formatting logic in the charmstate rather than in the Jinja2 template

Potential breaking changes

  • TcpRequirerApplicationData.port is now Optional[int] (was int). All existing usage passes port, so no runtime breakage, but type checkers downstream may flag this.
  • HaproxyRouteTcpServer.port is now Optional[int]. The server_address property handles both cases.

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>
Copilot AI changed the title [WIP] Add port-range attribute to haproxy-route-tcp Add backend_port_range attribute to haproxy-route-tcp relation May 26, 2026
Copilot AI requested a review from Thanhphan1147 May 26, 2026 09:15
Comment thread haproxy-operator/lib/charms/haproxy/v1/haproxy_route_tcp.py
return self

@property
def port_range_ports(self) -> list[int]:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot rename this attribute to requested_ports_in_range

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed in 5c6a117.

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>
Comment on lines +4 to +8
{% if server.port %}
{{ server.address }}:{{ server.port }}
{% else %}
{{ server.address }}
{% endif %}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored into a server_address property on HaproxyRouteTcpServer in the charmstate. The template now just renders {{ server.server_address }} with no conditional logic.

Comment on lines +797 to +798
for port in endpoint.application_data.requested_ports_in_range:
port_to_backends_mapping[port].append(endpoint)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@Thanhphan1147 Thanhphan1147 marked this pull request as ready for review May 28, 2026 10:28
@Thanhphan1147 Thanhphan1147 requested a review from a team as a code owner May 28, 2026 10:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a port-range attribute to haproxy-route-tcp to allow mapping from mutliple frontend ports to multiple backend ports

2 participants