From 540f3736c7d2a49c096a1dc17ddb56e5a7365de9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:12:33 +0000 Subject: [PATCH 1/4] Initial plan From 34a5e21c323095bdde3ca10c8c7eb1e023567d09 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:19:20 +0000 Subject: [PATCH 2/4] fix: use named ACLs to avoid exceeding HAProxy's 64-word parser limit Split the inline OR-chained ACL conditions in the redirect rule into separate named ACL lines (do_not_redirect). In HAProxy, multiple ACL lines with the same name are OR'd together, preserving the same logic while keeping each line short. Also refactor the HSTS template to use a named is_allow_http ACL to avoid the same potential word-limit issue. Agent-Logs-Url: https://github.com/canonical/haproxy-operator/sessions/0787275c-fa4f-4c2a-8201-ad38184c5afc Co-authored-by: Thanhphan1147 <42444001+Thanhphan1147@users.noreply.github.com> --- .../templates/haproxy_route.cfg.j2 | 12 +++- haproxy-operator/tests/unit/test_hsts.py | 66 ++++++++++++++++++- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/haproxy-operator/templates/haproxy_route.cfg.j2 b/haproxy-operator/templates/haproxy_route.cfg.j2 index b708d5fbc..61c3589cc 100644 --- a/haproxy-operator/templates/haproxy_route.cfg.j2 +++ b/haproxy-operator/templates/haproxy_route.cfg.j2 @@ -9,11 +9,19 @@ frontend haproxy {{ ddos_protection_rules() }} # Redirect HTTP to HTTPS - http-request redirect scheme https unless { ssl_fc } {% for acl in acls_for_allow_http %} || {{ acl }}{% endfor %} + acl do_not_redirect ssl_fc +{% for acl in acls_for_allow_http %} + acl do_not_redirect {{ acl }} +{% endfor %} + http-request redirect scheme https unless do_not_redirect {% if enable_hsts %} # Add HSTS header for HTTPS connections (only when HTTP is not allowed) - http-response set-header Strict-Transport-Security "max-age=2592000" if { ssl_fc } {% for acl in acls_for_allow_http %} !{{ acl }}{% endfor %} +{% for acl in acls_for_allow_http %} + acl is_allow_http {{ acl }} +{% endfor %} + http-response set-header Strict-Transport-Security "max-age=2592000" if { ssl_fc } {% if acls_for_allow_http %}!is_allow_http{% endif %} + {% endif %} {% for spoe_auth_info in spoe_auth_info_list %} diff --git a/haproxy-operator/tests/unit/test_hsts.py b/haproxy-operator/tests/unit/test_hsts.py index 5098404e6..9310ba7e9 100644 --- a/haproxy-operator/tests/unit/test_hsts.py +++ b/haproxy-operator/tests/unit/test_hsts.py @@ -96,13 +96,77 @@ def test_hsts_disabled_allow_http(monkeypatch: pytest.MonkeyPatch, certificates_ state, ) assert render_file_mock.call_count == 1 + haproxy_conf_contents = render_file_mock.call_args.args[1] + assert ( + "acl is_allow_http { req.hdr(host),field(1,:) -i haproxy.internal }" + in haproxy_conf_contents + ) render_file_mock.assert_any_call( pathlib.Path("/etc/haproxy/haproxy.cfg"), RegexMatcher( re.escape( - 'http-response set-header Strict-Transport-Security "max-age=2592000" if { ssl_fc } !{ req.hdr(host),field(1,:) -i haproxy.internal }\n' + 'http-response set-header Strict-Transport-Security "max-age=2592000" if { ssl_fc } !is_allow_http\n' ) ), ANY, ) assert out.unit_status.name == ops.testing.ActiveStatus.name + + +@pytest.mark.usefixtures("systemd_mock", "mocks_external_calls") +def test_redirect_uses_named_acl(monkeypatch: pytest.MonkeyPatch, certificates_integration): + """ + arrange: Prepare a haproxy with haproxy_route without allow_http. + act: trigger relation changed. + assert: haproxy.conf uses the do_not_redirect named ACL for the redirect rule. + """ + render_file_mock = MagicMock() + monkeypatch.setattr("haproxy.render_file", render_file_mock) + haproxy_route_relation = build_haproxy_route_relation() + + ctx = ops.testing.Context(HAProxyCharm) + state = ops.testing.State( + relations=[certificates_integration, haproxy_route_relation], + ) + out = ctx.run( + ctx.on.relation_changed(haproxy_route_relation), + state, + ) + assert render_file_mock.call_count == 1 + haproxy_conf_contents = render_file_mock.call_args.args[1] + assert "acl do_not_redirect ssl_fc" in haproxy_conf_contents + assert "http-request redirect scheme https unless do_not_redirect" in haproxy_conf_contents + assert out.unit_status.name == ops.testing.ActiveStatus.name + + +@pytest.mark.usefixtures("systemd_mock", "mocks_external_calls") +def test_redirect_allow_http_uses_named_acl( + monkeypatch: pytest.MonkeyPatch, certificates_integration +): + """ + arrange: Prepare a haproxy with haproxy_route with allow_http enabled. + act: trigger relation changed. + assert: haproxy.conf uses do_not_redirect named ACL with the allow_http condition. + """ + render_file_mock = MagicMock() + monkeypatch.setattr("haproxy.render_file", render_file_mock) + haproxy_route_relation = build_haproxy_route_relation() + haproxy_route_relation.remote_app_data["allow_http"] = "true" + + ctx = ops.testing.Context(HAProxyCharm) + state = ops.testing.State( + relations=[certificates_integration, haproxy_route_relation], + ) + out = ctx.run( + ctx.on.relation_changed(haproxy_route_relation), + state, + ) + assert render_file_mock.call_count == 1 + haproxy_conf_contents = render_file_mock.call_args.args[1] + assert "acl do_not_redirect ssl_fc" in haproxy_conf_contents + assert ( + "acl do_not_redirect { req.hdr(host),field(1,:) -i haproxy.internal }" + in haproxy_conf_contents + ) + assert "http-request redirect scheme https unless do_not_redirect" in haproxy_conf_contents + assert out.unit_status.name == ops.testing.ActiveStatus.name From a9ec559adb99dfce13c8812fb0854605d1f4c745 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:20:42 +0000 Subject: [PATCH 3/4] refactor: rename test for clarity Agent-Logs-Url: https://github.com/canonical/haproxy-operator/sessions/0787275c-fa4f-4c2a-8201-ad38184c5afc Co-authored-by: Thanhphan1147 <42444001+Thanhphan1147@users.noreply.github.com> --- haproxy-operator/tests/unit/test_hsts.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/haproxy-operator/tests/unit/test_hsts.py b/haproxy-operator/tests/unit/test_hsts.py index 9310ba7e9..e8f10ba98 100644 --- a/haproxy-operator/tests/unit/test_hsts.py +++ b/haproxy-operator/tests/unit/test_hsts.py @@ -114,7 +114,9 @@ def test_hsts_disabled_allow_http(monkeypatch: pytest.MonkeyPatch, certificates_ @pytest.mark.usefixtures("systemd_mock", "mocks_external_calls") -def test_redirect_uses_named_acl(monkeypatch: pytest.MonkeyPatch, certificates_integration): +def test_redirect_without_allow_http_uses_named_acl( + monkeypatch: pytest.MonkeyPatch, certificates_integration +): """ arrange: Prepare a haproxy with haproxy_route without allow_http. act: trigger relation changed. From 469d6fc8a7feff3016402301df62f09597f5c4b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:14:24 +0000 Subject: [PATCH 4/4] refactor: reuse single is_allow_http ACL for both redirect and HSTS rules Agent-Logs-Url: https://github.com/canonical/haproxy-operator/sessions/9fcbfc56-eb9e-4fd1-9702-50a10c317953 Co-authored-by: Thanhphan1147 <42444001+Thanhphan1147@users.noreply.github.com> --- haproxy-operator/templates/haproxy_route.cfg.j2 | 8 ++------ haproxy-operator/tests/unit/test_hsts.py | 16 +++++++++------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/haproxy-operator/templates/haproxy_route.cfg.j2 b/haproxy-operator/templates/haproxy_route.cfg.j2 index 61c3589cc..9dd6a5f4d 100644 --- a/haproxy-operator/templates/haproxy_route.cfg.j2 +++ b/haproxy-operator/templates/haproxy_route.cfg.j2 @@ -9,17 +9,13 @@ frontend haproxy {{ ddos_protection_rules() }} # Redirect HTTP to HTTPS - acl do_not_redirect ssl_fc {% for acl in acls_for_allow_http %} - acl do_not_redirect {{ acl }} + acl is_allow_http {{ acl }} {% endfor %} - http-request redirect scheme https unless do_not_redirect + http-request redirect scheme https unless { ssl_fc } {% if acls_for_allow_http %}|| is_allow_http{% endif %} {% if enable_hsts %} # Add HSTS header for HTTPS connections (only when HTTP is not allowed) -{% for acl in acls_for_allow_http %} - acl is_allow_http {{ acl }} -{% endfor %} http-response set-header Strict-Transport-Security "max-age=2592000" if { ssl_fc } {% if acls_for_allow_http %}!is_allow_http{% endif %} {% endif %} diff --git a/haproxy-operator/tests/unit/test_hsts.py b/haproxy-operator/tests/unit/test_hsts.py index e8f10ba98..ce4147d63 100644 --- a/haproxy-operator/tests/unit/test_hsts.py +++ b/haproxy-operator/tests/unit/test_hsts.py @@ -120,7 +120,7 @@ def test_redirect_without_allow_http_uses_named_acl( """ arrange: Prepare a haproxy with haproxy_route without allow_http. act: trigger relation changed. - assert: haproxy.conf uses the do_not_redirect named ACL for the redirect rule. + assert: haproxy.conf uses inline ssl_fc for the redirect rule without is_allow_http ACL. """ render_file_mock = MagicMock() monkeypatch.setattr("haproxy.render_file", render_file_mock) @@ -136,8 +136,8 @@ def test_redirect_without_allow_http_uses_named_acl( ) assert render_file_mock.call_count == 1 haproxy_conf_contents = render_file_mock.call_args.args[1] - assert "acl do_not_redirect ssl_fc" in haproxy_conf_contents - assert "http-request redirect scheme https unless do_not_redirect" in haproxy_conf_contents + assert "acl is_allow_http" not in haproxy_conf_contents + assert "http-request redirect scheme https unless { ssl_fc }" in haproxy_conf_contents assert out.unit_status.name == ops.testing.ActiveStatus.name @@ -148,7 +148,7 @@ def test_redirect_allow_http_uses_named_acl( """ arrange: Prepare a haproxy with haproxy_route with allow_http enabled. act: trigger relation changed. - assert: haproxy.conf uses do_not_redirect named ACL with the allow_http condition. + assert: haproxy.conf uses is_allow_http named ACL for both redirect and HSTS rules. """ render_file_mock = MagicMock() monkeypatch.setattr("haproxy.render_file", render_file_mock) @@ -165,10 +165,12 @@ def test_redirect_allow_http_uses_named_acl( ) assert render_file_mock.call_count == 1 haproxy_conf_contents = render_file_mock.call_args.args[1] - assert "acl do_not_redirect ssl_fc" in haproxy_conf_contents assert ( - "acl do_not_redirect { req.hdr(host),field(1,:) -i haproxy.internal }" + "acl is_allow_http { req.hdr(host),field(1,:) -i haproxy.internal }" + in haproxy_conf_contents + ) + assert ( + "http-request redirect scheme https unless { ssl_fc } || is_allow_http" in haproxy_conf_contents ) - assert "http-request redirect scheme https unless do_not_redirect" in haproxy_conf_contents assert out.unit_status.name == ops.testing.ActiveStatus.name