From d838243fa72294277aabc549037b5de80952b31f Mon Sep 17 00:00:00 2001 From: Alexander Cristurean Date: Wed, 13 May 2026 17:20:41 +0200 Subject: [PATCH 01/22] feat: scaffold pipeline policy. Signed-off-by: Alexander Cristurean --- .../kuadrant/extensions/pipeline_policy.py | 90 +++++++++++++++++++ .../extensions/pipeline_policy/__init__.py | 0 .../extensions/pipeline_policy/conftest.py | 19 ++++ .../test_pipeline_policy_basic.py | 26 ++++++ 4 files changed, 135 insertions(+) create mode 100644 testsuite/kuadrant/extensions/pipeline_policy.py create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/__init__.py create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py diff --git a/testsuite/kuadrant/extensions/pipeline_policy.py b/testsuite/kuadrant/extensions/pipeline_policy.py new file mode 100644 index 000000000..cc669a254 --- /dev/null +++ b/testsuite/kuadrant/extensions/pipeline_policy.py @@ -0,0 +1,90 @@ +"""Module containing classes related to PipelinePolicy""" + +from typing import Dict, List, Optional + +from testsuite.gateway import Referencable +from testsuite.kubernetes import modify +from testsuite.kubernetes.client import KubernetesClient +from testsuite.kuadrant.policy import Policy + + +class PipelinePolicy(Policy): + """PipelinePolicy for defining declarative action pipelines (request/response actions) on routes""" + + @classmethod + def create_instance( + cls, + cluster: KubernetesClient, + name: str, + target: Referencable, + labels: Dict[str, str] = None, + section_name: str = None, + ): + """Creates base instance""" + model: Dict = { + "apiVersion": "extensions.kuadrant.io/v1alpha1", + "kind": "PipelinePolicy", + "metadata": {"name": name, "namespace": cluster.project, "labels": labels}, + "spec": { + "targetRef": target.reference, + }, + } + if section_name: + model["spec"]["targetRef"]["sectionName"] = section_name + + return cls(model, context=cluster.context) + + @modify + def add_action_method(self, name: str, url: str, service: str, method: str, message_template: str): + """Add a gRPC upstream action method definition""" + self.model.spec.setdefault("actionMethods", []).append( + { + "name": name, + "url": url, + "service": service, + "method": method, + "messageTemplate": message_template, + } + ) + + @modify + def add_request_allow(self, intention: str, predicate: Optional[str] = None): + """Add an allow request action with a CEL predicate""" + action: Dict = {"type": "allow", "intention": intention} + if predicate: + action["predicate"] = predicate + self.model.spec.setdefault("request", []).append(action) + + @modify + def add_request_grpc_method( + self, + method: str, + var: Optional[str] = None, + intention: Optional[str] = None, + predicate: Optional[str] = None, + ): + """Add a grpc_method request action that calls an upstream""" + action: Dict = {"type": "grpc_method", "method": method} + if var: + action["var"] = var + if intention: + action["intention"] = intention + if predicate: + action["predicate"] = predicate + self.model.spec.setdefault("request", []).append(action) + + @modify + def add_response_headers(self, headers: List[List[str]], predicate: Optional[str] = None): + """Add an add_headers response action""" + action: Dict = {"type": "add_headers", "headersToAdd": str(headers)} + if predicate: + action["predicate"] = predicate + self.model.spec.setdefault("response", []).append(action) + + @modify + def add_response_code(self, response_code: int, predicate: Optional[str] = None): + """Add a with_response_code response action""" + action: Dict = {"type": "with_response_code", "responseCode": response_code} + if predicate: + action["predicate"] = predicate + self.model.spec.setdefault("response", []).append(action) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/__init__.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py new file mode 100644 index 000000000..77d412b6c --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py @@ -0,0 +1,19 @@ +"""Shared fixtures for PipelinePolicy testing.""" + +import pytest + +from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy + + +@pytest.fixture(scope="module") +def pipeline_policy(cluster, blame, route): + """PipelinePolicy targeting the test HTTPRoute""" + return PipelinePolicy.create_instance(cluster, blame("pipeline"), route) + + +@pytest.fixture(scope="module", autouse=True) +def commit(request, pipeline_policy): + """Commit and wait for PipelinePolicy to be ready.""" + request.addfinalizer(pipeline_policy.delete) + pipeline_policy.commit() + pipeline_policy.wait_for_ready() diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py new file mode 100644 index 000000000..00f301164 --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py @@ -0,0 +1,26 @@ +"""Basic happy-path tests for PipelinePolicy: allow action and response headers.""" + +import pytest + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + + +@pytest.fixture(scope="module") +def pipeline_policy(pipeline_policy): + """Configure PipelinePolicy with allow action and response headers.""" + pipeline_policy.add_request_allow('request.url_path != "/blocked"') + pipeline_policy.add_response_headers([["x-pipeline-policy", "active"]]) + return pipeline_policy + + +def test_allowed_path(client): + """Request to an allowed path returns 200 with the custom response header.""" + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-pipeline-policy") == "active" + + +def test_blocked_path(client): + """Request to /blocked is denied by the allow action.""" + response = client.get("/blocked") + assert response.status_code == 403 From 3052701fdb240fd03fbb287cd705016b61c2870d Mon Sep 17 00:00:00 2001 From: Alexander Cristurean Date: Fri, 15 May 2026 17:09:59 +0200 Subject: [PATCH 02/22] feat: added more tests. Signed-off-by: Alexander Cristurean --- .../extensions/pipeline_policy/conftest.py | 16 +++- .../test_pipeline_policy_action_method.py | 93 +++++++++++++++++++ .../test_pipeline_policy_basic.py | 3 +- 3 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py index 77d412b6c..482c50d43 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py @@ -4,6 +4,13 @@ from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy +# no wasm plugin gets created without having +@pytest.fixture(scope="module") +def authorization(authorization): + """Setup AuthConfig for test""" + authorization.identity.add_anonymous("anonymous") + authorization.responses.add_simple("auth.identity.anonymous") + return authorization @pytest.fixture(scope="module") def pipeline_policy(cluster, blame, route): @@ -12,8 +19,9 @@ def pipeline_policy(cluster, blame, route): @pytest.fixture(scope="module", autouse=True) -def commit(request, pipeline_policy): +def commit(request, authorization, pipeline_policy): """Commit and wait for PipelinePolicy to be ready.""" - request.addfinalizer(pipeline_policy.delete) - pipeline_policy.commit() - pipeline_policy.wait_for_ready() + for component in [authorization, pipeline_policy]: + request.addfinalizer(component.delete) + component.commit() + component.wait_for_ready() diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py new file mode 100644 index 000000000..368b6ce37 --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py @@ -0,0 +1,93 @@ +import pytest + +from testsuite.kubernetes import Selector +from testsuite.kubernetes.deployment import Deployment +from testsuite.kubernetes.service import Service, ServicePort + + +@pytest.fixture(scope="module") +def threat_assessment_service(request, cluster, blame, module_label): + """Deploys the ThreatAssessmentService gRPC backend""" + name = blame("threat") + match_labels = {"app": module_label, "deployment": name} + + deployment = Deployment.create_instance( + cluster, + name, + container_name="threat-assessment", + image="quay.io/kuadrant/threat-assessment-service:latest", + ports={"grpc": 8080}, + selector=Selector(matchLabels=match_labels), + labels={"app": module_label}, + ) + request.addfinalizer(deployment.delete) + deployment.commit() + deployment.wait_for_ready() + + service = Service.create_instance( + cluster, + name, + selector=match_labels, + ports=[ServicePort(name="grpc", port=8080, targetPort="grpc")], + labels={"app": module_label}, + ) + request.addfinalizer(service.delete) + service.commit() + return service + + +THREAT_THRESHOLD = 50 + + +@pytest.fixture(scope="module") +def pipeline_policy(pipeline_policy, threat_assessment_service): # pylint: disable=unused-argument + """Configure PipelinePolicy with threat assessment gRPC action and conditional headers.""" + svc_url = f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" + pipeline_policy.add_action_method( + name="assess-threat", + url=svc_url, + service="threat.v1.ThreatAssessmentService", + method="AssessRequest", + message_template="threat.v1.ThreatRequest{uri: request.path, source_ip: source.address}", + ) + + pipeline_policy.add_request_allow('request.url_path != "/blocked"') + pipeline_policy.add_request_grpc_method( + method="assess-threat", + var="threatResponse", + intention=f"threatResponse.threat_level < {THREAT_THRESHOLD}", + predicate='"x-assess-threat" in request.headers', + ) + + pipeline_policy.add_response_headers( + [["x-threat-assessed", "true"]], + predicate='"x-assess-threat" in request.headers', + ) + pipeline_policy.add_response_headers( + [["x-threat-assessed", "false"]], + predicate='!("x-assess-threat" in request.headers)', + ) + pipeline_policy.add_response_headers([["x-threat-threshold", str(THREAT_THRESHOLD)]]) + return pipeline_policy + + +def test_allowed_path(client): + """Request to an allowed path returns 200 without threat assessment.""" + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-threat-assessed") == "false" + assert response.headers.get("x-threat-threshold") == str(THREAT_THRESHOLD) + + +def test_blocked_path(client): + """Request to /blocked is denied by the allow rule.""" + response = client.get("/blocked") + assert response.status_code == 403 + + +def test_threat_assessment_safe(client): + """Request with x-assess-threat header to a safe path passes threat check.""" + response = client.get("/get", headers={"x-assess-threat": "true"}) + assert response.status_code == 200 + assert response.headers.get("x-threat-assessed") == "true" + assert response.headers.get("x-threat-threshold") == str(THREAT_THRESHOLD) \ No newline at end of file diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py index 00f301164..ef6a32599 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py @@ -4,7 +4,8 @@ pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] - +# if not action method is configured, these cel expression won't be evaluated +# need to talk with the team about this.. @pytest.fixture(scope="module") def pipeline_policy(pipeline_policy): """Configure PipelinePolicy with allow action and response headers.""" From eea48d29a867c610de8de3a9aa5a9b4bafb1906f Mon Sep 17 00:00:00 2001 From: Alexander Cristurean Date: Fri, 15 May 2026 17:12:36 +0200 Subject: [PATCH 03/22] feat: grpc connection tests. Signed-off-by: Alexander Cristurean --- .../test_pipeline_policy_error_status.py | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py new file mode 100644 index 000000000..0c0193b2e --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py @@ -0,0 +1,127 @@ +"""Tests that PipelinePolicy surfaces error status conditions for invalid action method configurations.""" + +import pytest + +from testsuite.kubernetes import Selector +from testsuite.kubernetes.deployment import Deployment +from testsuite.kubernetes.service import Service, ServicePort +from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy +from testsuite.kuadrant.policy import has_condition + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + + +@pytest.fixture(scope="module", autouse=True) +def commit(request, authorization): + """Only commit authorization; each test creates its own policy with bad configuration.""" + request.addfinalizer(authorization.delete) + authorization.commit() + authorization.wait_for_ready() + + +@pytest.fixture(scope="module") +def threat_assessment_service(request, cluster, blame, module_label): + """Deploys the ThreatAssessmentService gRPC backend""" + name = blame("threat") + match_labels = {"app": module_label, "deployment": name} + + deployment = Deployment.create_instance( + cluster, + name, + container_name="threat-assessment", + image="quay.io/kuadrant/threat-assessment-service:latest", + ports={"grpc": 8080}, + selector=Selector(matchLabels=match_labels), + labels={"app": module_label}, + ) + request.addfinalizer(deployment.delete) + deployment.commit() + deployment.wait_for_ready() + + service = Service.create_instance( + cluster, + name, + selector=match_labels, + ports=[ServicePort(name="grpc", port=8080, targetPort="grpc")], + labels={"app": module_label}, + ) + request.addfinalizer(service.delete) + service.commit() + return service + + +def test_nonexistent_url(request, cluster, blame, route): + """PipelinePolicy reports error when action method URL points to a non-existent service.""" + policy = PipelinePolicy.create_instance(cluster, blame("bad-url"), route) + policy.add_action_method( + name="bad-method", + url="grpc://does-not-exist.default.svc.cluster.local:8080", + service="threat.v1.ThreatAssessmentService", + method="AssessRequest", + message_template="threat.v1.ThreatRequest{uri: request.path}", + ) + policy.add_request_allow('request.url_path != "/blocked"') + policy.add_request_grpc_method(method="bad-method") + + request.addfinalizer(policy.delete) + policy.commit() + + # TODO: add expected message assertion + assert policy.wait_until( + has_condition("Accepted", "False", "Unknown"), + timelimit=60, + ), f"Policy did not reach expected error status, instead: {policy.refresh().model.status.conditions}" + + +def test_wrong_service_name(request, cluster, blame, route, threat_assessment_service): + """PipelinePolicy reports error when action method references a non-existent gRPC service name.""" + svc_url = ( + f"grpc://{threat_assessment_service.name()}" + f".{threat_assessment_service.namespace()}.svc.cluster.local:8080" + ) + policy = PipelinePolicy.create_instance(cluster, blame("bad-svc"), route) + policy.add_action_method( + name="bad-service", + url=svc_url, + service="nonexistent.v1.FakeService", + method="DoSomething", + message_template="nonexistent.v1.FakeRequest{uri: request.path}", + ) + policy.add_request_allow('request.url_path != "/blocked"') + policy.add_request_grpc_method(method="bad-service") + + request.addfinalizer(policy.delete) + policy.commit() + + # TODO: add expected message assertion + assert policy.wait_until( + has_condition("Accepted", "False", "Unknown"), + timelimit=60, + ), f"Policy did not reach expected error status, instead: {policy.refresh().model.status.conditions}" + + +def test_wrong_method_name(request, cluster, blame, route, threat_assessment_service): + """PipelinePolicy reports error when action method references a non-existent gRPC method.""" + svc_url = ( + f"grpc://{threat_assessment_service.name()}" + f".{threat_assessment_service.namespace()}.svc.cluster.local:8080" + ) + policy = PipelinePolicy.create_instance(cluster, blame("bad-meth"), route) + policy.add_action_method( + name="wrong-method", + url=svc_url, + service="threat.v1.ThreatAssessmentService", + method="NonExistentMethod", + message_template="threat.v1.ThreatRequest{uri: request.path}", + ) + policy.add_request_allow('request.url_path != "/blocked"') + policy.add_request_grpc_method(method="wrong-method") + + request.addfinalizer(policy.delete) + policy.commit() + + # TODO: add expected message assertion + assert policy.wait_until( + has_condition("Accepted", "False", "Unknown"), + timelimit=60, + ), f"Policy did not reach expected error status, instead: {policy.refresh().model.status.conditions}" From a5306bf5e3d1721cfc0314b345dceb47cdcb6256 Mon Sep 17 00:00:00 2001 From: Alexander Cristurean Date: Mon, 18 May 2026 11:35:35 +0200 Subject: [PATCH 04/22] fix: removed authorization feature. Signed-off-by: Alexander Cristurean --- .../extensions/pipeline_policy/conftest.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py index 482c50d43..23f9a69b8 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py @@ -4,14 +4,6 @@ from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy -# no wasm plugin gets created without having -@pytest.fixture(scope="module") -def authorization(authorization): - """Setup AuthConfig for test""" - authorization.identity.add_anonymous("anonymous") - authorization.responses.add_simple("auth.identity.anonymous") - return authorization - @pytest.fixture(scope="module") def pipeline_policy(cluster, blame, route): """PipelinePolicy targeting the test HTTPRoute""" @@ -19,9 +11,9 @@ def pipeline_policy(cluster, blame, route): @pytest.fixture(scope="module", autouse=True) -def commit(request, authorization, pipeline_policy): +def commit(request, pipeline_policy): """Commit and wait for PipelinePolicy to be ready.""" - for component in [authorization, pipeline_policy]: + for component in [pipeline_policy]: request.addfinalizer(component.delete) component.commit() component.wait_for_ready() From 49bca67ed5b84f6f61b8051f59668f6b1b30ef96 Mon Sep 17 00:00:00 2001 From: Alexander Cristurean Date: Mon, 18 May 2026 11:45:16 +0200 Subject: [PATCH 05/22] fix: cosmetic changes. Signed-off-by: Alexander Cristurean --- .../singlecluster/extensions/pipeline_policy/conftest.py | 1 + .../pipeline_policy/test_pipeline_policy_action_method.py | 6 ++++-- .../pipeline_policy/test_pipeline_policy_basic.py | 1 + .../pipeline_policy/test_pipeline_policy_error_status.py | 6 ++---- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py index 23f9a69b8..12f08845d 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py @@ -4,6 +4,7 @@ from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy + @pytest.fixture(scope="module") def pipeline_policy(cluster, blame, route): """PipelinePolicy targeting the test HTTPRoute""" diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py index 368b6ce37..2b4f194c0 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py @@ -42,7 +42,9 @@ def threat_assessment_service(request, cluster, blame, module_label): @pytest.fixture(scope="module") def pipeline_policy(pipeline_policy, threat_assessment_service): # pylint: disable=unused-argument """Configure PipelinePolicy with threat assessment gRPC action and conditional headers.""" - svc_url = f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" + svc_url = ( + f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" + ) pipeline_policy.add_action_method( name="assess-threat", url=svc_url, @@ -90,4 +92,4 @@ def test_threat_assessment_safe(client): response = client.get("/get", headers={"x-assess-threat": "true"}) assert response.status_code == 200 assert response.headers.get("x-threat-assessed") == "true" - assert response.headers.get("x-threat-threshold") == str(THREAT_THRESHOLD) \ No newline at end of file + assert response.headers.get("x-threat-threshold") == str(THREAT_THRESHOLD) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py index ef6a32599..cd4b4a84f 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py @@ -4,6 +4,7 @@ pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + # if not action method is configured, these cel expression won't be evaluated # need to talk with the team about this.. @pytest.fixture(scope="module") diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py index 0c0193b2e..29d1cbbf6 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py @@ -76,8 +76,7 @@ def test_nonexistent_url(request, cluster, blame, route): def test_wrong_service_name(request, cluster, blame, route, threat_assessment_service): """PipelinePolicy reports error when action method references a non-existent gRPC service name.""" svc_url = ( - f"grpc://{threat_assessment_service.name()}" - f".{threat_assessment_service.namespace()}.svc.cluster.local:8080" + f"grpc://{threat_assessment_service.name()}" f".{threat_assessment_service.namespace()}.svc.cluster.local:8080" ) policy = PipelinePolicy.create_instance(cluster, blame("bad-svc"), route) policy.add_action_method( @@ -103,8 +102,7 @@ def test_wrong_service_name(request, cluster, blame, route, threat_assessment_se def test_wrong_method_name(request, cluster, blame, route, threat_assessment_service): """PipelinePolicy reports error when action method references a non-existent gRPC method.""" svc_url = ( - f"grpc://{threat_assessment_service.name()}" - f".{threat_assessment_service.namespace()}.svc.cluster.local:8080" + f"grpc://{threat_assessment_service.name()}" f".{threat_assessment_service.namespace()}.svc.cluster.local:8080" ) policy = PipelinePolicy.create_instance(cluster, blame("bad-meth"), route) policy.add_action_method( From f00f7ffd9364e591a5db839f30ba8af4c622bf63 Mon Sep 17 00:00:00 2001 From: Alexander Cristurean Date: Mon, 18 May 2026 12:57:59 +0200 Subject: [PATCH 06/22] fix: commit acceptance. Signed-off-by: Alexander Cristurean --- .../tests/singlecluster/authorino/dinosaur/conftest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/testsuite/tests/singlecluster/authorino/dinosaur/conftest.py b/testsuite/tests/singlecluster/authorino/dinosaur/conftest.py index 8e9cea3bc..bec43b086 100644 --- a/testsuite/tests/singlecluster/authorino/dinosaur/conftest.py +++ b/testsuite/tests/singlecluster/authorino/dinosaur/conftest.py @@ -228,13 +228,15 @@ def authorization(authorization, keycloak, terms_and_conditions, cluster_info, a authorization.responses.set_unauthorized( DenyResponse( headers={"content-type": Value("application/json")}, - body=Value("""{ + body=Value( + """{ "kind": "Error", "id": "403", "href": "/api/dinosaurs_mgmt/v1/errors/403", "code": "DINOSAURS-MGMT-403", "reason": "Forbidden" -}"""), +}""" + ), ) ) From b43d2fe037d2b903d979b59d1b7d019344223a14 Mon Sep 17 00:00:00 2001 From: Alexander Cristurean Date: Mon, 18 May 2026 12:58:32 +0200 Subject: [PATCH 07/22] Revert "fix: commit acceptance." This reverts commit 468dddcf1b06e4a1d855bd5356fa424c651b9031. Signed-off-by: Alexander Cristurean --- .../tests/singlecluster/authorino/dinosaur/conftest.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/testsuite/tests/singlecluster/authorino/dinosaur/conftest.py b/testsuite/tests/singlecluster/authorino/dinosaur/conftest.py index bec43b086..8e9cea3bc 100644 --- a/testsuite/tests/singlecluster/authorino/dinosaur/conftest.py +++ b/testsuite/tests/singlecluster/authorino/dinosaur/conftest.py @@ -228,15 +228,13 @@ def authorization(authorization, keycloak, terms_and_conditions, cluster_info, a authorization.responses.set_unauthorized( DenyResponse( headers={"content-type": Value("application/json")}, - body=Value( - """{ + body=Value("""{ "kind": "Error", "id": "403", "href": "/api/dinosaurs_mgmt/v1/errors/403", "code": "DINOSAURS-MGMT-403", "reason": "Forbidden" -}""" - ), +}"""), ) ) From 970aaaeb0e6e00435a1a0a5b4d3252c134573594 Mon Sep 17 00:00:00 2001 From: Alexander Cristurean Date: Mon, 18 May 2026 12:58:52 +0200 Subject: [PATCH 08/22] fix: module comment. Signed-off-by: Alexander Cristurean --- .../pipeline_policy/test_pipeline_policy_action_method.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py index 2b4f194c0..3b9233fb2 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py @@ -1,3 +1,4 @@ +"""Basic happy-path tests for PipelinePolicy: action method calling a gRPC backend and conditional response headers.""" import pytest from testsuite.kubernetes import Selector From 601cf8ff77a9a6845500a2282e808b984bd766d2 Mon Sep 17 00:00:00 2001 From: Alexander Cristurean Date: Mon, 18 May 2026 12:59:33 +0200 Subject: [PATCH 09/22] fix: reformat. Signed-off-by: Alexander Cristurean --- .../pipeline_policy/test_pipeline_policy_action_method.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py index 3b9233fb2..32f9580a5 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py @@ -1,4 +1,5 @@ """Basic happy-path tests for PipelinePolicy: action method calling a gRPC backend and conditional response headers.""" + import pytest from testsuite.kubernetes import Selector From 4f704e7e6e9bdbd7e8e3cab303ad6ce378876dc0 Mon Sep 17 00:00:00 2001 From: Alexander Cristurean Date: Mon, 25 May 2026 16:19:19 +0200 Subject: [PATCH 10/22] feat: updated pipeline extensions with updates from the sdk. Signed-off-by: Alexander Cristurean --- .../kuadrant/extensions/pipeline_policy.py | 84 +++++++++++++++---- .../test_pipeline_policy_action_method.py | 11 ++- .../test_pipeline_policy_basic.py | 10 +-- .../test_pipeline_policy_error_status.py | 6 +- 4 files changed, 82 insertions(+), 29 deletions(-) diff --git a/testsuite/kuadrant/extensions/pipeline_policy.py b/testsuite/kuadrant/extensions/pipeline_policy.py index cc669a254..2149a7492 100644 --- a/testsuite/kuadrant/extensions/pipeline_policy.py +++ b/testsuite/kuadrant/extensions/pipeline_policy.py @@ -48,43 +48,93 @@ def add_action_method(self, name: str, url: str, service: str, method: str, mess ) @modify - def add_request_allow(self, intention: str, predicate: Optional[str] = None): - """Add an allow request action with a CEL predicate""" - action: Dict = {"type": "allow", "intention": intention} + def add_request_grpc_method(self, method: str, var: Optional[str] = None, predicate: Optional[str] = None): + """Add a grpc_method request action that calls an upstream""" + action: Dict = {"type": "grpc_method", "method": method} + if var: + action["var"] = var if predicate: action["predicate"] = predicate self.model.spec.setdefault("request", []).append(action) @modify - def add_request_grpc_method( + def add_request_deny( self, - method: str, - var: Optional[str] = None, - intention: Optional[str] = None, predicate: Optional[str] = None, + with_status: Optional[int] = None, + with_headers: Optional[str] = None, + with_body: Optional[str] = None, ): - """Add a grpc_method request action that calls an upstream""" + """Add a deny request action""" + action: Dict = {"type": "deny"} + if predicate: + action["predicate"] = predicate + if with_status: + action["withStatus"] = with_status + if with_headers: + action["withHeaders"] = with_headers + if with_body: + action["withBody"] = with_body + self.model.spec.setdefault("request", []).append(action) + + @modify + def add_request_fail(self, log_message: str, predicate: Optional[str] = None): + """Add a fail request action""" + action: Dict = {"type": "fail", "logMessage": log_message} + if predicate: + action["predicate"] = predicate + self.model.spec.setdefault("request", []).append(action) + + @modify + def add_request_headers(self, headers: List[List[str]], predicate: Optional[str] = None): + """Add an add_headers request action""" + action: Dict = {"type": "add_headers", "headersToAdd": str(headers)} + if predicate: + action["predicate"] = predicate + self.model.spec.setdefault("request", []).append(action) + + @modify + def add_response_grpc_method(self, method: str, var: Optional[str] = None, predicate: Optional[str] = None): + """Add a grpc_method response action that calls an upstream""" action: Dict = {"type": "grpc_method", "method": method} if var: action["var"] = var - if intention: - action["intention"] = intention if predicate: action["predicate"] = predicate - self.model.spec.setdefault("request", []).append(action) + self.model.spec.setdefault("response", []).append(action) @modify - def add_response_headers(self, headers: List[List[str]], predicate: Optional[str] = None): - """Add an add_headers response action""" - action: Dict = {"type": "add_headers", "headersToAdd": str(headers)} + def add_response_deny( + self, + predicate: Optional[str] = None, + with_status: Optional[int] = None, + with_headers: Optional[str] = None, + with_body: Optional[str] = None, + ): + """Add a deny response action""" + action: Dict = {"type": "deny"} if predicate: action["predicate"] = predicate + if with_status: + action["withStatus"] = with_status + if with_headers: + action["withHeaders"] = with_headers + if with_body: + action["withBody"] = with_body self.model.spec.setdefault("response", []).append(action) @modify - def add_response_code(self, response_code: int, predicate: Optional[str] = None): - """Add a with_response_code response action""" - action: Dict = {"type": "with_response_code", "responseCode": response_code} + def add_response_fail(self, log_message: str, predicate: Optional[str] = None): + """Add a fail response action""" + action: Dict = {"type": "fail", "logMessage": log_message} + if predicate: + action["predicate"] = predicate + self.model.spec.setdefault("response", []).append(action) + + @modify + def add_response_headers(self, headers: List[List[str]], predicate: Optional[str] = None): + """Add an add_headers response action""" + action: Dict = {"type": "add_headers", "headersToAdd": str(headers)} if predicate: action["predicate"] = predicate self.model.spec.setdefault("response", []).append(action) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py index 32f9580a5..e643b1ae9 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py @@ -1,4 +1,4 @@ -"""Basic happy-path tests for PipelinePolicy: action method calling a gRPC backend and conditional response headers.""" +"""Tests for PipelinePolicy: action method calling a gRPC backend and conditional response headers.""" import pytest @@ -55,13 +55,16 @@ def pipeline_policy(pipeline_policy, threat_assessment_service): # pylint: disa message_template="threat.v1.ThreatRequest{uri: request.path, source_ip: source.address}", ) - pipeline_policy.add_request_allow('request.url_path != "/blocked"') pipeline_policy.add_request_grpc_method( method="assess-threat", var="threatResponse", - intention=f"threatResponse.threat_level < {THREAT_THRESHOLD}", predicate='"x-assess-threat" in request.headers', ) + pipeline_policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + pipeline_policy.add_request_deny( + predicate=f"threatResponse.threat_level >= {THREAT_THRESHOLD}", + with_status=403, + ) pipeline_policy.add_response_headers( [["x-threat-assessed", "true"]], @@ -72,6 +75,7 @@ def pipeline_policy(pipeline_policy, threat_assessment_service): # pylint: disa predicate='!("x-assess-threat" in request.headers)', ) pipeline_policy.add_response_headers([["x-threat-threshold", str(THREAT_THRESHOLD)]]) + return pipeline_policy @@ -79,6 +83,7 @@ def test_allowed_path(client): """Request to an allowed path returns 200 without threat assessment.""" response = client.get("/get") assert response.status_code == 200 + assert response.headers.get("x-threat-assessed") == "false" assert response.headers.get("x-threat-threshold") == str(THREAT_THRESHOLD) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py index cd4b4a84f..3b02ef654 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py @@ -1,16 +1,14 @@ -"""Basic happy-path tests for PipelinePolicy: allow action and response headers.""" +"""Basic happy-path tests for PipelinePolicy: deny action and response headers.""" import pytest pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] -# if not action method is configured, these cel expression won't be evaluated -# need to talk with the team about this.. @pytest.fixture(scope="module") def pipeline_policy(pipeline_policy): - """Configure PipelinePolicy with allow action and response headers.""" - pipeline_policy.add_request_allow('request.url_path != "/blocked"') + """Configure PipelinePolicy with deny action and response headers.""" + pipeline_policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) pipeline_policy.add_response_headers([["x-pipeline-policy", "active"]]) return pipeline_policy @@ -23,6 +21,6 @@ def test_allowed_path(client): def test_blocked_path(client): - """Request to /blocked is denied by the allow action.""" + """Request to /blocked is denied by the deny action.""" response = client.get("/blocked") assert response.status_code == 403 diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py index 29d1cbbf6..befe11990 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py @@ -60,7 +60,7 @@ def test_nonexistent_url(request, cluster, blame, route): method="AssessRequest", message_template="threat.v1.ThreatRequest{uri: request.path}", ) - policy.add_request_allow('request.url_path != "/blocked"') + policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) policy.add_request_grpc_method(method="bad-method") request.addfinalizer(policy.delete) @@ -86,7 +86,7 @@ def test_wrong_service_name(request, cluster, blame, route, threat_assessment_se method="DoSomething", message_template="nonexistent.v1.FakeRequest{uri: request.path}", ) - policy.add_request_allow('request.url_path != "/blocked"') + policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) policy.add_request_grpc_method(method="bad-service") request.addfinalizer(policy.delete) @@ -112,7 +112,7 @@ def test_wrong_method_name(request, cluster, blame, route, threat_assessment_ser method="NonExistentMethod", message_template="threat.v1.ThreatRequest{uri: request.path}", ) - policy.add_request_allow('request.url_path != "/blocked"') + policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) policy.add_request_grpc_method(method="wrong-method") request.addfinalizer(policy.delete) From 17e9b07daa29f088547598cc359d209090f091c6 Mon Sep 17 00:00:00 2001 From: Alexander Cristurean Date: Wed, 27 May 2026 12:46:19 +0200 Subject: [PATCH 11/22] feat: add pipelinepolicy to clean target. Signed-off-by: Alexander Cristurean --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a998cd2bf..0b5ba92e9 100644 --- a/Makefile +++ b/Makefile @@ -163,7 +163,8 @@ podmonitors.monitoring.coreos.com,$\ apiservices.apiregistration.k8s.io,$\ horizontalpodautoscalers.autoscaling,$\ oidcpolicies.extensions.kuadrant.io,$\ -planpolicies.extensions.kuadrant.io +planpolicies.extensions.kuadrant.io,$\ +pipelinepolicies.extensions.kuadrant.io clean: ## Clean all objects on cluster created by running this testsuite. Set the env variable USER to delete after someone else @echo "Deleting objects for user: $(USER)" From 2bbba01457624420a0568406b7b10801c8473061 Mon Sep 17 00:00:00 2001 From: Silvia Tarabova Date: Mon, 1 Jun 2026 10:45:19 +0200 Subject: [PATCH 12/22] feat: add PipelinePolicy tests Signed-off-by: Silvia Tarabova --- .../extensions/pipeline_policy/conftest.py | 34 +++++ .../test_pipeline_policy_auth.py | 57 ++++++++ .../test_pipeline_policy_auth_ratelimit.py | 77 +++++++++++ .../test_pipeline_policy_composition.py | 118 ++++++++++++++++ .../test_pipeline_policy_deny.py | 127 ++++++++++++++++++ .../test_pipeline_policy_error_status.py | 7 +- .../test_pipeline_policy_fail.py | 47 +++++++ .../test_pipeline_policy_grpc.py | 67 +++++++++ .../test_pipeline_policy_grpc_errors.py | 112 +++++++++++++++ .../test_pipeline_policy_isolation.py | 116 ++++++++++++++++ .../test_pipeline_policy_lifecycle.py | 58 ++++++++ .../test_pipeline_policy_ratelimit.py | 42 ++++++ .../test_pipeline_policy_response.py | 80 +++++++++++ .../test_pipeline_policy_targeting.py | 70 ++++++++++ .../test_pipeline_policy_validation.py | 98 ++++++++++++++ 15 files changed, 1105 insertions(+), 5 deletions(-) create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth.py create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth_ratelimit.py create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_fail.py create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc.py create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_ratelimit.py create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_response.py create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_targeting.py create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py index 12f08845d..d1894f424 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py @@ -2,6 +2,9 @@ import pytest +from testsuite.kubernetes import Selector +from testsuite.kubernetes.deployment import Deployment +from testsuite.kubernetes.service import Service, ServicePort from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy @@ -11,6 +14,37 @@ def pipeline_policy(cluster, blame, route): return PipelinePolicy.create_instance(cluster, blame("pipeline"), route) +@pytest.fixture(scope="module") +def threat_assessment_service(request, cluster, blame, module_label): + """Deploys the ThreatAssessmentService gRPC backend""" + name = blame("threat") + match_labels = {"app": module_label, "deployment": name} + + deployment = Deployment.create_instance( + cluster, + name, + container_name="threat-assessment", + image="quay.io/kuadrant/threat-assessment-service:latest", + ports={"grpc": 8080}, + selector=Selector(matchLabels=match_labels), + labels={"app": module_label}, + ) + request.addfinalizer(deployment.delete) + deployment.commit() + deployment.wait_for_ready() + + service = Service.create_instance( + cluster, + name, + selector=match_labels, + ports=[ServicePort(name="grpc", port=8080, targetPort="grpc")], + labels={"app": module_label}, + ) + request.addfinalizer(service.delete) + service.commit() + return service + + @pytest.fixture(scope="module", autouse=True) def commit(request, pipeline_policy): """Commit and wait for PipelinePolicy to be ready.""" diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth.py new file mode 100644 index 000000000..4975ecc57 --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth.py @@ -0,0 +1,57 @@ +"""Tests for PipelinePolicy interaction with AuthPolicy.""" + +import pytest + +from testsuite.httpx.auth import HttpxOidcClientAuth + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + + +@pytest.fixture(scope="module") +def authorization(authorization, oidc_provider): + """AuthPolicy with OIDC identity verification.""" + authorization.identity.add_oidc("default", oidc_provider.well_known["issuer"]) + return authorization + + +@pytest.fixture(scope="module") +def auth(oidc_provider): + """Valid OIDC authentication for requests.""" + return HttpxOidcClientAuth(oidc_provider.get_token, "authorization") + + +@pytest.fixture(scope="module") +def pipeline_policy(pipeline_policy): + """PipelinePolicy with deny action and response header.""" + pipeline_policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + pipeline_policy.add_response_headers([["x-pipeline-policy", "active"]]) + return pipeline_policy + + +@pytest.fixture(scope="module", autouse=True) +def commit(request, authorization, pipeline_policy): + """Commit AuthPolicy and PipelinePolicy.""" + for component in [authorization, pipeline_policy]: + request.addfinalizer(component.delete) + component.commit() + component.wait_for_ready() + + +def test_auth_and_pipeline_allowed(client, auth): + """Authenticated request to allowed path passes both AuthPolicy and PipelinePolicy.""" + response = client.get("/get", auth=auth) + assert response.status_code == 200 + assert response.headers.get("x-pipeline-policy") == "active" + + +def test_auth_and_pipeline_unauthorized(client): + """Unauthenticated request is rejected by AuthPolicy before PipelinePolicy runs.""" + response = client.get("/get") + assert response.status_code == 401 + assert response.headers.get("x-pipeline-policy") is None + + +def test_auth_and_pipeline_blocked_path(client, auth): + """Authenticated request to blocked path is denied by PipelinePolicy deny action.""" + response = client.get("/blocked", auth=auth) + assert response.status_code == 403 \ No newline at end of file diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth_ratelimit.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth_ratelimit.py new file mode 100644 index 000000000..0dcef6954 --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth_ratelimit.py @@ -0,0 +1,77 @@ +"""Tests for PipelinePolicy interaction with both AuthPolicy and RateLimitPolicy.""" + +import pytest + +from testsuite.httpx.auth import HttpxOidcClientAuth +from testsuite.kuadrant.policy.rate_limit import Limit + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + + +@pytest.fixture(scope="module") +def authorization(authorization, oidc_provider): + """AuthPolicy with OIDC identity verification.""" + authorization.identity.add_oidc("default", oidc_provider.well_known["issuer"]) + return authorization + + +@pytest.fixture(scope="module") +def auth(oidc_provider): + """Valid OIDC authentication for requests.""" + return HttpxOidcClientAuth(oidc_provider.get_token, "authorization") + + +@pytest.fixture(scope="module") +def rate_limit(rate_limit): + """RateLimitPolicy with a low limit for testing.""" + rate_limit.add_limit("basic", [Limit(3, "10s")]) + return rate_limit + + +@pytest.fixture(scope="module") +def pipeline_policy(pipeline_policy): + """PipelinePolicy with deny action and response header.""" + pipeline_policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + pipeline_policy.add_response_headers([["x-pipeline-policy", "active"]]) + return pipeline_policy + + +@pytest.fixture(scope="module", autouse=True) +def commit(request, authorization, rate_limit, pipeline_policy): + """Commit AuthPolicy, RateLimitPolicy, and PipelinePolicy.""" + for component in [authorization, rate_limit, pipeline_policy]: + request.addfinalizer(component.delete) + component.commit() + component.wait_for_ready() + + +def test_all_policies_allowed(client, auth): + """Authenticated request passes auth, rate limit, and gets PipelinePolicy header.""" + response = client.get("/get", auth=auth) + assert response.status_code == 200 + assert response.headers.get("x-pipeline-policy") == "active" + + +def test_all_policies_unauthorized(client): + """Unauthenticated request is rejected by AuthPolicy before other policies run.""" + response = client.get("/get") + assert response.status_code == 401 + assert response.headers.get("x-pipeline-policy") is None + + +def test_all_policies_blocked_path(client, auth): + """Authenticated request to blocked path is denied by PipelinePolicy deny action.""" + response = client.get("/blocked", auth=auth) + assert response.status_code == 403 + + +@pytest.mark.flaky(reruns=3, reruns_delay=15) +def test_all_policies_rate_limited(client, auth): + """Rate limit is enforced alongside AuthPolicy and PipelinePolicy.""" + responses = client.get_many("/get", 3, auth=auth) + responses.assert_all(status_code=200) + for resp in responses: + assert resp.headers.get("x-pipeline-policy") == "active" + + response = client.get("/get", auth=auth) + assert response.status_code == 429 \ No newline at end of file diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py new file mode 100644 index 000000000..6b41236ef --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py @@ -0,0 +1,118 @@ +"""Tests for PipelinePolicy composition: action ordering, empty and partial pipelines.""" + +import pytest + +from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + + +@pytest.fixture(scope="module", autouse=True) +def commit(): + """No module-level policy; each test creates its own.""" + + +def test_first_deny_wins(request, cluster, blame, route, client): + """When two deny actions match, the first one in spec order determines the response status.""" + policy = PipelinePolicy.create_instance(cluster, blame("order"), route) + policy.add_request_deny(predicate='request.url_path == "/order-test"', with_status=403) + policy.add_request_deny(predicate='request.url_path == "/order-test"', with_status=429) + request.addfinalizer(policy.delete) + policy.commit() + policy.wait_for_ready() + + response = client.get("/order-test") + assert response.status_code == 403 + + +def test_fail_before_deny(request, cluster, blame, route, client, threat_assessment_service): + """Fail action before deny takes precedence when gRPC response triggers the fail predicate.""" + svc_url = ( + f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" + ) + policy = PipelinePolicy.create_instance(cluster, blame("failord"), route) + policy.add_action_method( + name="assess", + url=svc_url, + service="threat.v1.ThreatAssessmentService", + method="AssessRequest", + message_template="threat.v1.ThreatRequest{uri: request.path}", + ) + policy.add_request_grpc_method(method="assess", var="threat") + policy.add_request_fail("threat too high", predicate="threat.threat_level >= 4") + # policy.add_request_deny(predicate="true", with_status=403) + policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + request.addfinalizer(policy.delete) + policy.commit() + policy.wait_for_ready() + + response = client.get("/admin") + assert response.status_code == 500 + + +def test_response_action_ordering(request, cluster, blame, route, client): + """Response actions execute in spec order; both headers from separate actions are present.""" + policy = PipelinePolicy.create_instance(cluster, blame("respord"), route) + policy.add_response_headers([["x-first", "alpha"]]) + policy.add_response_headers([["x-second", "bravo"]]) + request.addfinalizer(policy.delete) + policy.commit() + policy.wait_for_ready() + + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-first") == "alpha" + assert response.headers.get("x-second") == "bravo" + + +def test_empty_pipeline(request, cluster, blame, route, client): + """PipelinePolicy with no actions passes requests through unmodified.""" + policy = PipelinePolicy.create_instance(cluster, blame("empty"), route) + request.addfinalizer(policy.delete) + policy.commit() + policy.wait_for_ready() + + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-pipeline-policy") is None + + +def test_request_only_pipeline(request, cluster, blame, route, client): + """Pipeline with only request actions: deny works, no response modifications.""" + policy = PipelinePolicy.create_instance(cluster, blame("reqonly"), route) + policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + request.addfinalizer(policy.delete) + policy.commit() + policy.wait_for_ready() + + assert client.get("/get").status_code == 200 + assert client.get("/blocked").status_code == 403 + + +def test_response_only_pipeline(request, cluster, blame, route, client): + """Pipeline with only response actions: all requests pass, headers are modified.""" + policy = PipelinePolicy.create_instance(cluster, blame("resonly"), route) + policy.add_response_headers([["x-resp-only", "true"]]) + request.addfinalizer(policy.delete) + policy.commit() + policy.wait_for_ready() + + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-resp-only") == "true" + + +def test_mixed_pipeline(request, cluster, blame, route, client): + """Pipeline with both request deny and response headers executes in full.""" + policy = PipelinePolicy.create_instance(cluster, blame("mixed"), route) + policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + policy.add_response_headers([["x-mixed", "true"]]) + request.addfinalizer(policy.delete) + policy.commit() + policy.wait_for_ready() + + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-mixed") == "true" + + assert client.get("/blocked").status_code == 403 \ No newline at end of file diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py new file mode 100644 index 000000000..ac4d5ff93 --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py @@ -0,0 +1,127 @@ +"""Tests for PipelinePolicy deny action variants in request and response phases.""" + +import pytest + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + + +@pytest.fixture(scope="module") +def pipeline_policy(pipeline_policy): + """PipelinePolicy with multiple deny configurations covering all deny variants.""" + # Path-based deny + pipeline_policy.add_request_deny(predicate='request.url_path == "/deny-path"', with_status=403) + # Header-based deny + pipeline_policy.add_request_deny(predicate='"x-deny-me" in request.headers', with_status=403) + # Custom status code + pipeline_policy.add_request_deny(predicate='request.url_path == "/custom-status"', with_status=429) + # Custom headers + pipeline_policy.add_request_deny( + predicate='request.url_path == "/custom-headers"', + with_status=403, + with_headers='[["x-deny-reason", "blocked"]]', + ) + # Custom body + pipeline_policy.add_request_deny( + predicate='request.url_path == "/custom-body"', + with_status=403, + with_body="Access denied", + ) + # All response fields + pipeline_policy.add_request_deny( + predicate='request.url_path == "/custom-all"', + with_status=451, + with_headers='[["x-deny-reason", "full-custom"]]', + with_body="Fully customized denial", + ) + # Response phase deny — status override + pipeline_policy.add_response_deny( + predicate='"x-override-code" in request.headers', + with_status=503, + ) + # Response phase deny — all fields + pipeline_policy.add_response_deny( + predicate='"x-resp-deny" in request.headers', + with_status=418, + with_headers='[["x-deny-phase", "response"]]', + with_body="Teapot response", + ) + return pipeline_policy + + +def test_path_based_deny(client): + """Request to a path matching the deny predicate is denied with 403.""" + response = client.get("/deny-path") + assert response.status_code == 403 + + +def test_cel_predicate_does_not_deny(client): + """Request to a path not matching any deny predicate passes through.""" + response = client.get("/get") + assert response.status_code == 200 + + +def test_header_based_deny(client): + """Request with a header matching the deny predicate is denied.""" + response = client.get("/get", headers={"x-deny-me": "true"}) + assert response.status_code == 403 + + +def test_header_based_deny_absent(client): + """Request without the blocked header passes through.""" + response = client.get("/get") + assert response.status_code == 200 + + +def test_multiple_deny_actions_or_behavior(client): + """Multiple deny actions behave as OR: any matching predicate denies the request.""" + assert client.get("/deny-path").status_code == 403 + assert client.get("/get", headers={"x-deny-me": "true"}).status_code == 403 + assert client.get("/get").status_code == 200 + + +def test_deny_custom_status_code(client): + """Deny with custom withStatus returns that status code.""" + response = client.get("/custom-status") + assert response.status_code == 429 + + +def test_deny_custom_headers(client): + """Deny with withHeaders includes custom headers in the denied response.""" + response = client.get("/custom-headers") + assert response.status_code == 403 + assert response.headers.get("x-deny-reason") == "blocked" + + +def test_deny_custom_body(client): + """Deny with withBody returns custom body text.""" + response = client.get("/custom-body") + assert response.status_code == 403 + assert response.text == "Access denied" + + +def test_deny_all_response_fields(client): + """Deny with withStatus, withHeaders, and withBody all set returns all fields.""" + response = client.get("/custom-all") + assert response.status_code == 451 + assert response.headers.get("x-deny-reason") == "full-custom" + assert response.text == "Fully customized denial" + + +def test_response_deny_override_status(client): + """Response deny overrides the backend response status code.""" + response = client.get("/get", headers={"x-override-code": "true"}) + assert response.status_code == 503 + + +def test_response_deny_no_override(client): + """Response without the override header passes through normally.""" + response = client.get("/get") + assert response.status_code == 200 + + +def test_response_deny_with_headers_and_body(client): + """Response deny with all fields replaces the backend response.""" + response = client.get("/get", headers={"x-resp-deny": "true"}) + assert response.status_code == 418 + assert response.headers.get("x-deny-phase") == "response" + assert response.text == "Teapot response" diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py index befe11990..9326324ba 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py @@ -12,11 +12,8 @@ @pytest.fixture(scope="module", autouse=True) -def commit(request, authorization): - """Only commit authorization; each test creates its own policy with bad configuration.""" - request.addfinalizer(authorization.delete) - authorization.commit() - authorization.wait_for_ready() +def commit(): + """No module-level policy; each test creates its own policy with bad configuration.""" @pytest.fixture(scope="module") diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_fail.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_fail.py new file mode 100644 index 000000000..0fc67cb81 --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_fail.py @@ -0,0 +1,47 @@ +"""Tests for PipelinePolicy fail action triggered by gRPC response variables.""" + +import pytest + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + + +@pytest.fixture(scope="module") +def pipeline_policy(pipeline_policy, threat_assessment_service): + """PipelinePolicy with gRPC call and fail action based on threat level.""" + svc_url = ( + f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" + ) + pipeline_policy.add_action_method( + name="assess", + url=svc_url, + service="threat.v1.ThreatAssessmentService", + method="AssessRequest", + message_template="threat.v1.ThreatRequest{uri: request.path}", + ) + pipeline_policy.add_request_grpc_method(method="assess", var="threat") + pipeline_policy.add_request_fail( + "threat level too high", + predicate="threat.threat_level >= 4", + ) + pipeline_policy.add_response_headers([["x-pipeline-active", "true"]]) + return pipeline_policy + + +def test_fail_triggers_on_high_threat(client): + """Fail action triggers when gRPC response threat_level meets the threshold.""" + response = client.get("/admin") + assert response.status_code == 500 + + +def test_fail_does_not_trigger_on_low_threat(client): + """Fail action does not trigger when gRPC response threat_level is below the threshold.""" + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-pipeline-active") == "true" + + +def test_fail_no_response_headers_on_failure(client): + """Response headers are not added when fail action terminates the chain.""" + response = client.get("/admin") + assert response.status_code == 500 + assert response.headers.get("x-pipeline-active") is None \ No newline at end of file diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc.py new file mode 100644 index 000000000..51644625c --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc.py @@ -0,0 +1,67 @@ +"""Tests for PipelinePolicy grpc_method action: upstream calls and conditional execution.""" + +import pytest + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + +THREAT_THRESHOLD = 50 + + +@pytest.fixture(scope="module") +def pipeline_policy(pipeline_policy, threat_assessment_service): # pylint: disable=unused-argument + """PipelinePolicy with threat assessment gRPC action and conditional headers.""" + svc_url = ( + f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" + ) + pipeline_policy.add_action_method( + name="assess-threat", + url=svc_url, + service="threat.v1.ThreatAssessmentService", + method="AssessRequest", + message_template="threat.v1.ThreatRequest{uri: request.path, source_ip: source.address}", + ) + + pipeline_policy.add_request_grpc_method( + method="assess-threat", + var="threatResponse", + predicate='"x-assess-threat" in request.headers', + ) + pipeline_policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + pipeline_policy.add_request_deny( + predicate=f"threatResponse.threat_level >= {THREAT_THRESHOLD}", + with_status=403, + ) + + pipeline_policy.add_response_headers( + [["x-threat-assessed", "true"]], + predicate='"x-assess-threat" in request.headers', + ) + pipeline_policy.add_response_headers( + [["x-threat-assessed", "false"]], + predicate='!("x-assess-threat" in request.headers)', + ) + pipeline_policy.add_response_headers([["x-threat-threshold", str(THREAT_THRESHOLD)]]) + + return pipeline_policy + + +def test_basic_grpc_upstream_call(client): + """gRPC upstream is called when predicate matches, response var is available to subsequent actions.""" + response = client.get("/get", headers={"x-assess-threat": "true"}) + assert response.status_code == 200 + assert response.headers.get("x-threat-assessed") == "true" + assert response.headers.get("x-threat-threshold") == str(THREAT_THRESHOLD) + + +def test_conditional_grpc_call_skipped(client): + """gRPC upstream is not called when predicate does not match.""" + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-threat-assessed") == "false" + assert response.headers.get("x-threat-threshold") == str(THREAT_THRESHOLD) + + +def test_grpc_response_var_deny(client): + """Request to /blocked is denied by path-based deny rule.""" + response = client.get("/blocked") + assert response.status_code == 403 diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py new file mode 100644 index 000000000..98172206b --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py @@ -0,0 +1,112 @@ +"""Tests for PipelinePolicy gRPC error handling: unavailable services, wrong names, and fail actions.""" + +import pytest + +from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy +from testsuite.kuadrant.policy import has_condition + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + + +@pytest.fixture(scope="module", autouse=True) +def commit(): + """No module-level policy; each test creates its own PipelinePolicy.""" + + +def test_grpc_upstream_unavailable(request, cluster, blame, route): + """PipelinePolicy reports error when action method URL points to a non-existent service.""" + policy = PipelinePolicy.create_instance(cluster, blame("bad-url"), route) + policy.add_action_method( + name="bad-method", + url="grpc://does-not-exist.default.svc.cluster.local:8080", + service="threat.v1.ThreatAssessmentService", + method="AssessRequest", + message_template="threat.v1.ThreatRequest{uri: request.path}", + ) + policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + policy.add_request_grpc_method(method="bad-method") + + request.addfinalizer(policy.delete) + policy.commit() + + assert policy.wait_until( + has_condition("Accepted", "False", "Unknown"), + timelimit=60, + ), f"Policy did not reach expected error status, instead: {policy.refresh().model.status.conditions}" + + +def test_grpc_wrong_service_name(request, cluster, blame, route, threat_assessment_service): + """PipelinePolicy reports error when action method references a non-existent gRPC service name.""" + svc_url = ( + f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" + ) + policy = PipelinePolicy.create_instance(cluster, blame("bad-svc"), route) + policy.add_action_method( + name="bad-service", + url=svc_url, + service="nonexistent.v1.FakeService", + method="DoSomething", + message_template="nonexistent.v1.FakeRequest{uri: request.path}", + ) + policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + policy.add_request_grpc_method(method="bad-service") + + request.addfinalizer(policy.delete) + policy.commit() + + assert policy.wait_until( + has_condition("Accepted", "False", "Unknown"), + timelimit=60, + ), f"Policy did not reach expected error status, instead: {policy.refresh().model.status.conditions}" + + +def test_grpc_wrong_method_name(request, cluster, blame, route, threat_assessment_service): + """PipelinePolicy reports error when action method references a non-existent gRPC method.""" + svc_url = ( + f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" + ) + policy = PipelinePolicy.create_instance(cluster, blame("bad-meth"), route) + policy.add_action_method( + name="wrong-method", + url=svc_url, + service="threat.v1.ThreatAssessmentService", + method="NonExistentMethod", + message_template="threat.v1.ThreatRequest{uri: request.path}", + ) + policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + policy.add_request_grpc_method(method="wrong-method") + + request.addfinalizer(policy.delete) + policy.commit() + + assert policy.wait_until( + has_condition("Accepted", "False", "Unknown"), + timelimit=60, + ), f"Policy did not reach expected error status, instead: {policy.refresh().model.status.conditions}" + + +def test_fail_after_grpc_call(request, cluster, blame, route, client, threat_assessment_service): + """Fail action after gRPC call terminates the chain when variable indicates invalid state.""" + svc_url = ( + f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" + ) + policy = PipelinePolicy.create_instance(cluster, blame("grpc-fail"), route) + policy.add_action_method( + name="assess", + url=svc_url, + service="threat.v1.ThreatAssessmentService", + method="AssessRequest", + message_template="threat.v1.ThreatRequest{uri: request.path}", + ) + policy.add_request_grpc_method(method="assess", var="threat") + policy.add_request_fail( + "threat assessment returned invalid level", + predicate="threat.threat_level < 0", + ) + + request.addfinalizer(policy.delete) + policy.commit() + policy.wait_for_ready() + + response = client.get("/get") + assert response.status_code == 200 \ No newline at end of file diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py new file mode 100644 index 000000000..987943d7a --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py @@ -0,0 +1,116 @@ +"""Tests for PipelinePolicy isolation: policy should not leak to untargeted routes or gateways.""" + +import time + +import pytest + +from testsuite.gateway import GatewayListener +from testsuite.gateway.gateway_api.gateway import KuadrantGateway +from testsuite.gateway.gateway_api.route import HTTPRoute + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + +PROPAGATION_WAIT = 10 + + +# --- Same gateway, different route --- + + +@pytest.fixture(scope="module") +def hostname2(gateway, exposer, blame): + """Second hostname for the unaffected route on the same gateway""" + return exposer.expose_hostname(blame("no-pp"), gateway) + + +@pytest.fixture(scope="module") +def route2(request, gateway, blame, hostname2, backend, module_label): + """Second HTTPRoute on the same gateway without any PipelinePolicy""" + route = HTTPRoute.create_instance(gateway.cluster, blame("route2"), gateway, {"app": module_label}) + route.add_hostname(hostname2.hostname) + route.add_backend(backend) + request.addfinalizer(route.delete) + route.commit() + return route + + +@pytest.fixture(scope="module") +def client2(route2, hostname2): # pylint: disable=unused-argument + """Client for the route without PipelinePolicy""" + client = hostname2.client() + yield client + client.close() + + +# --- Different gateway --- + + +@pytest.fixture(scope="module") +def gateway2(request, cluster, blame, wildcard_domain, module_label): + """Second gateway without any PipelinePolicy""" + gw = KuadrantGateway.create_instance(cluster, blame("gw2"), {"app": module_label}) + gw.add_listener(GatewayListener(hostname=wildcard_domain)) + request.addfinalizer(gw.delete) + gw.commit() + gw.wait_for_ready() + return gw + + +@pytest.fixture(scope="module") +def hostname3(gateway2, exposer, blame): + """Hostname for the route on the second gateway""" + return exposer.expose_hostname(blame("no-pp-gw"), gateway2) + + +@pytest.fixture(scope="module") +def route3(request, gateway2, blame, hostname3, backend, module_label): + """HTTPRoute on the second gateway without any PipelinePolicy""" + route = HTTPRoute.create_instance(gateway2.cluster, blame("route3"), gateway2, {"app": module_label}) + route.add_hostname(hostname3.hostname) + route.add_backend(backend) + request.addfinalizer(route.delete) + route.commit() + return route + + +@pytest.fixture(scope="module") +def client3(route3, hostname3): # pylint: disable=unused-argument + """Client for the route on the second gateway""" + client = hostname3.client() + yield client + client.close() + + +# --- Policy --- + + +@pytest.fixture(scope="module") +def pipeline_policy(pipeline_policy): + """PipelinePolicy with response header targeting only the first route.""" + pipeline_policy.add_response_headers([["x-pipeline-policy", "active"]]) + return pipeline_policy + + +# --- Tests --- + + +def test_policy_affects_targeted_route(client): + """Route with PipelinePolicy gets the response header.""" + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-pipeline-policy") == "active" + + +def test_policy_does_not_affect_other_route(client2): + """Route without PipelinePolicy on the same gateway does not get the response header.""" + time.sleep(PROPAGATION_WAIT) + response = client2.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-pipeline-policy") is None + + +def test_policy_does_not_affect_other_gateway(client3): + """Route on a different gateway does not get the response header.""" + time.sleep(PROPAGATION_WAIT) + response = client3.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-pipeline-policy") is None \ No newline at end of file diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py new file mode 100644 index 000000000..3425b261a --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py @@ -0,0 +1,58 @@ +"""Tests for PipelinePolicy lifecycle: status conditions, delete, and update.""" + +import pytest + +from testsuite.kuadrant.policy import has_condition + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + + +@pytest.fixture(scope="module") +def pipeline_policy(pipeline_policy): + """PipelinePolicy with deny action and response header.""" + pipeline_policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + pipeline_policy.add_response_headers([["x-pipeline-policy", "active"]]) + return pipeline_policy + + +def test_status_accepted(pipeline_policy): + """PipelinePolicy reports Accepted: True after commit.""" + assert pipeline_policy.wait_until( + has_condition("Accepted", "True"), + timelimit=30, + ), f"Policy not Accepted, status: {pipeline_policy.refresh().model.status.conditions}" + + +def test_status_enforced(pipeline_policy): + """PipelinePolicy reports Enforced: True after commit.""" + assert pipeline_policy.wait_until( + has_condition("Enforced", "True"), + timelimit=30, + ), f"Policy not Enforced, status: {pipeline_policy.refresh().model.status.conditions}" + + +def test_update_policy(client, pipeline_policy): + """Adding a new response header via policy update propagates to traffic.""" + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-pipeline-policy") == "active" + assert response.headers.get("x-pipeline-updated") is None + + pipeline_policy.add_response_headers([["x-pipeline-updated", "true"]]) + pipeline_policy.wait_for_ready() + + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-pipeline-policy") == "active" + assert response.headers.get("x-pipeline-updated") == "true" + + +@pytest.mark.flaky(reruns=0) +def test_delete_policy(client, pipeline_policy): + """After deleting the PipelinePolicy, the CR is removed from the cluster.""" + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-pipeline-policy") == "active" + + pipeline_policy.delete() + assert pipeline_policy.wait_until(lambda obj: not obj.exists(), timelimit=30), "PipelinePolicy was not deleted" diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_ratelimit.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_ratelimit.py new file mode 100644 index 000000000..7ed1d5bfc --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_ratelimit.py @@ -0,0 +1,42 @@ +"""Tests for PipelinePolicy interaction with RateLimitPolicy.""" + +import pytest + +from testsuite.kuadrant.policy.rate_limit import Limit + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + + +@pytest.fixture(scope="module") +def rate_limit(rate_limit): + """RateLimitPolicy with a low limit for testing.""" + rate_limit.add_limit("basic", [Limit(3, "10s")]) + return rate_limit + + +@pytest.fixture(scope="module") +def pipeline_policy(pipeline_policy): + """PipelinePolicy with response header.""" + pipeline_policy.add_response_headers([["x-pipeline-policy", "active"]]) + return pipeline_policy + + +@pytest.fixture(scope="module", autouse=True) +def commit(request, rate_limit, pipeline_policy): + """Commit RateLimitPolicy and PipelinePolicy.""" + for component in [rate_limit, pipeline_policy]: + request.addfinalizer(component.delete) + component.commit() + component.wait_for_ready() + + +@pytest.mark.flaky(reruns=3, reruns_delay=15) +def test_rate_limit_and_pipeline(client): + """Rate limit is enforced alongside PipelinePolicy actions.""" + responses = client.get_many("/get", 3) + responses.assert_all(status_code=200) + for resp in responses: + assert resp.headers.get("x-pipeline-policy") == "active" + + response = client.get("/get") + assert response.status_code == 429 \ No newline at end of file diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_response.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_response.py new file mode 100644 index 000000000..96b370757 --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_response.py @@ -0,0 +1,80 @@ +"""Tests for PipelinePolicy response add_headers actions.""" + +import pytest + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + + +@pytest.fixture(scope="module") +def pipeline_policy(pipeline_policy): + """PipelinePolicy with various add_headers response actions for testing.""" + pipeline_policy.add_response_headers([["x-single", "one"]]) + pipeline_policy.add_response_headers([["x-multi-a", "alpha"], ["x-multi-b", "bravo"]]) + pipeline_policy.add_response_headers([["x-separate", "separate-value"]]) + + pipeline_policy.add_response_headers( + [["x-conditional", "present"]], + predicate='"x-trigger" in request.headers', + ) + + pipeline_policy.add_response_headers( + [["x-mode", "active"]], + predicate='"x-trigger" in request.headers', + ) + pipeline_policy.add_response_headers( + [["x-mode", "inactive"]], + predicate='!("x-trigger" in request.headers)', + ) + + return pipeline_policy + + +def test_single_response_header(client): + """Single response header is added to the response.""" + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-single") == "one" + + +def test_multiple_headers_in_one_action(client): + """Multiple headers added in a single add_headers action are all present.""" + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-multi-a") == "alpha" + assert response.headers.get("x-multi-b") == "bravo" + + +def test_separate_add_headers_actions(client): + """Headers from two separate add_headers actions are all present.""" + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-single") == "one" + assert response.headers.get("x-separate") == "separate-value" + + +def test_conditional_header_present(client): + """Header with predicate is added when the condition is met.""" + response = client.get("/get", headers={"x-trigger": "true"}) + assert response.status_code == 200 + assert response.headers.get("x-conditional") == "present" + + +def test_conditional_header_absent(client): + """Header with predicate is not added when the condition is not met.""" + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-conditional") is None + + +def test_mutually_exclusive_headers_active(client): + """Mutually exclusive headers: active mode when trigger header is present.""" + response = client.get("/get", headers={"x-trigger": "true"}) + assert response.status_code == 200 + assert response.headers.get("x-mode") == "active" + + +def test_mutually_exclusive_headers_inactive(client): + """Mutually exclusive headers: inactive mode when trigger header is absent.""" + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-mode") == "inactive" diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_targeting.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_targeting.py new file mode 100644 index 000000000..4b15ab04e --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_targeting.py @@ -0,0 +1,70 @@ +"""Tests for PipelinePolicy Gateway-level targeting. + +HTTPRoute targeting is the default and is covered by test_pipeline_policy_basic.py. +""" + +import pytest + +from testsuite.gateway.gateway_api.route import HTTPRoute +from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + + +@pytest.fixture(scope="module") +def hostname2(gateway, exposer, blame): + """Second hostname for the second route""" + return exposer.expose_hostname(blame("hostname2"), gateway) + + +@pytest.fixture(scope="module") +def route2(request, gateway, blame, hostname2, backend, module_label): + """Second HTTPRoute on the same gateway with a different hostname""" + route = HTTPRoute.create_instance(gateway.cluster, blame("route2"), gateway, {"app": module_label}) + route.add_hostname(hostname2.hostname) + route.add_backend(backend) + request.addfinalizer(route.delete) + route.commit() + return route + + +@pytest.fixture(scope="module") +def client2(route2, hostname2): # pylint: disable=unused-argument + """Client for the second route""" + client = hostname2.client() + yield client + client.close() + + +@pytest.fixture(scope="module") +def gateway_policy(cluster, blame, gateway): + """PipelinePolicy targeting the Gateway with deny and response headers.""" + policy = PipelinePolicy.create_instance(cluster, blame("gw-pp"), gateway) + policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + policy.add_response_headers([["x-gateway-policy", "active"]]) + return policy + + +@pytest.fixture(scope="module", autouse=True) +def commit(request, route2, gateway_policy): # pylint: disable=unused-argument + """Commit gateway policy after route2 is ready.""" + request.addfinalizer(gateway_policy.delete) + gateway_policy.commit() + gateway_policy.wait_for_ready() + + +@pytest.mark.parametrize("client_fixture", ["client", "client2"]) +def test_gateway_target_affects_routes(request, client_fixture): + """Request to each route gets pipeline response header when policy targets the gateway.""" + http_client = request.getfixturevalue(client_fixture) + response = http_client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-gateway-policy") == "active" + + +@pytest.mark.parametrize("client_fixture", ["client", "client2"]) +def test_gateway_target_blocked_path(request, client_fixture): + """Blocked path is denied on both routes when policy targets the gateway.""" + http_client = request.getfixturevalue(client_fixture) + response = http_client.get("/blocked") + assert response.status_code == 403 \ No newline at end of file diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py new file mode 100644 index 000000000..6582751ee --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py @@ -0,0 +1,98 @@ +"""Tests for PipelinePolicy validation: rejected configurations and error conditions.""" + +import pytest + +from testsuite.gateway import CustomReference +from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy +from testsuite.kuadrant.policy import has_condition + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + + +@pytest.fixture(scope="module", autouse=True) +def commit(): + """No module-level policy; each test creates its own with bad configuration.""" + + +def test_invalid_target_ref(request, cluster, blame): + """PipelinePolicy targeting a non-existent HTTPRoute does not reach Enforced state.""" + target = CustomReference( + group="gateway.networking.k8s.io", + kind="HTTPRoute", + name="does-not-exist", + ) + policy = PipelinePolicy.create_instance(cluster, blame("bad-target"), target) + policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + + request.addfinalizer(policy.delete) + policy.commit() + + assert policy.wait_until( + has_condition("Accepted", "False", "TargetNotFound"), + timelimit=30, + ), f"Policy did not report TargetNotFound, status: {policy.refresh().model.status.conditions}" + + +def test_invalid_gateway_target_ref(request, cluster, blame): + """PipelinePolicy targeting a non-existent Gateway does not reach Enforced state.""" + target = CustomReference( + group="gateway.networking.k8s.io", + kind="Gateway", + name="does-not-exist", + ) + policy = PipelinePolicy.create_instance(cluster, blame("bad-gw"), target) + policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + + request.addfinalizer(policy.delete) + policy.commit() + + assert policy.wait_until( + has_condition("Accepted", "False", "TargetNotFound"), + timelimit=30, + ), f"Policy did not report TargetNotFound, status: {policy.refresh().model.status.conditions}" + + +def test_invalid_cel_expression(request, cluster, blame, route): + """PipelinePolicy with malformed CEL predicate fails to enforce.""" + policy = PipelinePolicy.create_instance(cluster, blame("bad-cel"), route) + policy.add_request_deny(predicate="INVALID CEL !!!", with_status=403) + + request.addfinalizer(policy.delete) + policy.commit() + + assert policy.wait_until( + has_condition("Accepted", "False", message="invalid CEL expression"), + timelimit=30, + ), f"Policy did not report error for invalid CEL, status: {policy.refresh().model.status.conditions}" + + +def test_variable_forward_reference(request, cluster, blame, route): + """PipelinePolicy referencing a variable before it is defined should fail validation.""" + var_name = "threatResponse" + policy = PipelinePolicy.create_instance(cluster, blame("fwd-ref"), route) + policy.add_request_deny(predicate=f"{var_name}.threat_level >= 50", with_status=403) + policy.add_request_grpc_method(method="nonexistent-method", var=var_name) + + request.addfinalizer(policy.delete) + policy.commit() + + assert policy.wait_until( + has_condition("Accepted", "False", message=f'action references variable "{var_name}" before it is populated'), + timelimit=60, + ), f"Policy did not reject forward reference, status: {policy.refresh().model.status.conditions}" + + +def test_duplicate_variable_name(request, cluster, blame, route): + """PipelinePolicy with two gRPC methods using the same variable name should fail validation.""" + var_name = "dupVar" + policy = PipelinePolicy.create_instance(cluster, blame("dup-var"), route) + policy.add_request_grpc_method(method="method-a", var=var_name) + policy.add_request_grpc_method(method="method-b", var=var_name) + + request.addfinalizer(policy.delete) + policy.commit() + + assert policy.wait_until( + has_condition("Accepted", "False", message=f'duplicate variable name "{var_name}"'), + timelimit=60, + ), f"Policy did not reject duplicate var, status: {policy.refresh().model.status.conditions}" From b2d80a4cd16038051b67cacb35dae659e07c364c Mon Sep 17 00:00:00 2001 From: Alexander Cristurean Date: Mon, 1 Jun 2026 12:58:19 +0200 Subject: [PATCH 13/22] fix: improved pipeline policy api. Signed-off-by: Alexander Cristurean --- .../kuadrant/extensions/pipeline_policy.py | 167 +++++++++--------- .../test_pipeline_policy_action_method.py | 12 +- .../test_pipeline_policy_auth.py | 6 +- .../test_pipeline_policy_auth_ratelimit.py | 6 +- .../test_pipeline_policy_basic.py | 4 +- .../test_pipeline_policy_composition.py | 26 +-- .../test_pipeline_policy_deny.py | 16 +- .../test_pipeline_policy_error_status.py | 12 +- .../test_pipeline_policy_fail.py | 8 +- .../test_pipeline_policy_grpc.py | 12 +- .../test_pipeline_policy_grpc_errors.py | 18 +- .../test_pipeline_policy_isolation.py | 4 +- .../test_pipeline_policy_lifecycle.py | 6 +- .../test_pipeline_policy_ratelimit.py | 4 +- .../test_pipeline_policy_response.py | 12 +- .../test_pipeline_policy_targeting.py | 6 +- .../test_pipeline_policy_validation.py | 14 +- 17 files changed, 163 insertions(+), 170 deletions(-) diff --git a/testsuite/kuadrant/extensions/pipeline_policy.py b/testsuite/kuadrant/extensions/pipeline_policy.py index 2149a7492..078978568 100644 --- a/testsuite/kuadrant/extensions/pipeline_policy.py +++ b/testsuite/kuadrant/extensions/pipeline_policy.py @@ -1,5 +1,8 @@ """Module containing classes related to PipelinePolicy""" +from __future__ import annotations + +from functools import cached_property from typing import Dict, List, Optional from testsuite.gateway import Referencable @@ -8,64 +11,50 @@ from testsuite.kuadrant.policy import Policy -class PipelinePolicy(Policy): - """PipelinePolicy for defining declarative action pipelines (request/response actions) on routes""" +class ActionSection: + """Section for request/response actions in a PipelinePolicy, mirrors ActionSpec from the Go API""" - @classmethod - def create_instance( - cls, - cluster: KubernetesClient, - name: str, - target: Referencable, - labels: Dict[str, str] = None, - section_name: str = None, - ): - """Creates base instance""" - model: Dict = { - "apiVersion": "extensions.kuadrant.io/v1alpha1", - "kind": "PipelinePolicy", - "metadata": {"name": name, "namespace": cluster.project, "labels": labels}, - "spec": { - "targetRef": target.reference, - }, - } - if section_name: - model["spec"]["targetRef"]["sectionName"] = section_name + def __init__(self, obj: "PipelinePolicy", section_name: str) -> None: + self.obj = obj + self.section_name = section_name - return cls(model, context=cluster.context) + def modify_and_apply(self, modifier_func, retries=2, cmd_args=None): + """Delegates modify_and_apply to the parent PipelinePolicy""" - @modify - def add_action_method(self, name: str, url: str, service: str, method: str, message_template: str): - """Add a gRPC upstream action method definition""" - self.model.spec.setdefault("actionMethods", []).append( - { - "name": name, - "url": url, - "service": service, - "method": method, - "messageTemplate": message_template, - } - ) + def _new_modifier(obj): + modifier_func(ActionSection(obj, self.section_name)) + + return self.obj.modify_and_apply(_new_modifier, retries, cmd_args) + + @property + def committed(self): + """Delegates committed check to the parent PipelinePolicy""" + return self.obj.committed + + @property + def section(self): + """Returns the action list for this section""" + return self.obj.model.spec.setdefault(self.section_name, []) @modify - def add_request_grpc_method(self, method: str, var: Optional[str] = None, predicate: Optional[str] = None): - """Add a grpc_method request action that calls an upstream""" + def add_grpc_method(self, method: str, var: Optional[str] = None, predicate: Optional[str] = None): + """Add a grpc_method action that calls an upstream""" action: Dict = {"type": "grpc_method", "method": method} if var: action["var"] = var if predicate: action["predicate"] = predicate - self.model.spec.setdefault("request", []).append(action) + self.section.append(action) @modify - def add_request_deny( + def add_deny( self, predicate: Optional[str] = None, with_status: Optional[int] = None, with_headers: Optional[str] = None, with_body: Optional[str] = None, ): - """Add a deny request action""" + """Add a deny action""" action: Dict = {"type": "deny"} if predicate: action["predicate"] = predicate @@ -75,66 +64,70 @@ def add_request_deny( action["withHeaders"] = with_headers if with_body: action["withBody"] = with_body - self.model.spec.setdefault("request", []).append(action) + self.section.append(action) @modify - def add_request_fail(self, log_message: str, predicate: Optional[str] = None): - """Add a fail request action""" + def add_fail(self, log_message: str, predicate: Optional[str] = None): + """Add a fail action""" action: Dict = {"type": "fail", "logMessage": log_message} if predicate: action["predicate"] = predicate - self.model.spec.setdefault("request", []).append(action) + self.section.append(action) @modify - def add_request_headers(self, headers: List[List[str]], predicate: Optional[str] = None): - """Add an add_headers request action""" + def add_headers(self, headers: List[List[str]], predicate: Optional[str] = None): + """Add an add_headers action""" action: Dict = {"type": "add_headers", "headersToAdd": str(headers)} if predicate: action["predicate"] = predicate - self.model.spec.setdefault("request", []).append(action) + self.section.append(action) - @modify - def add_response_grpc_method(self, method: str, var: Optional[str] = None, predicate: Optional[str] = None): - """Add a grpc_method response action that calls an upstream""" - action: Dict = {"type": "grpc_method", "method": method} - if var: - action["var"] = var - if predicate: - action["predicate"] = predicate - self.model.spec.setdefault("response", []).append(action) - @modify - def add_response_deny( - self, - predicate: Optional[str] = None, - with_status: Optional[int] = None, - with_headers: Optional[str] = None, - with_body: Optional[str] = None, +class PipelinePolicy(Policy): + """PipelinePolicy for defining declarative action pipelines (request/response actions) on routes""" + + @classmethod + def create_instance( + cls, + cluster: KubernetesClient, + name: str, + target: Referencable, + labels: Dict[str, str] = None, + section_name: str = None, ): - """Add a deny response action""" - action: Dict = {"type": "deny"} - if predicate: - action["predicate"] = predicate - if with_status: - action["withStatus"] = with_status - if with_headers: - action["withHeaders"] = with_headers - if with_body: - action["withBody"] = with_body - self.model.spec.setdefault("response", []).append(action) + """Creates base instance""" + model: Dict = { + "apiVersion": "extensions.kuadrant.io/v1alpha1", + "kind": "PipelinePolicy", + "metadata": {"name": name, "namespace": cluster.project, "labels": labels}, + "spec": { + "targetRef": target.reference, + }, + } + if section_name: + model["spec"]["targetRef"]["sectionName"] = section_name - @modify - def add_response_fail(self, log_message: str, predicate: Optional[str] = None): - """Add a fail response action""" - action: Dict = {"type": "fail", "logMessage": log_message} - if predicate: - action["predicate"] = predicate - self.model.spec.setdefault("response", []).append(action) + return cls(model, context=cluster.context) + + @cached_property + def on_http_request(self) -> ActionSection: + """Gives access to request actions""" + return ActionSection(self, "request") + + @cached_property + def on_http_response(self) -> ActionSection: + """Gives access to response actions""" + return ActionSection(self, "response") @modify - def add_response_headers(self, headers: List[List[str]], predicate: Optional[str] = None): - """Add an add_headers response action""" - action: Dict = {"type": "add_headers", "headersToAdd": str(headers)} - if predicate: - action["predicate"] = predicate - self.model.spec.setdefault("response", []).append(action) + def add_action_method(self, name: str, url: str, service: str, method: str, message_template: str): + """Add a gRPC upstream action method definition""" + self.model.spec.setdefault("actionMethods", []).append( + { + "name": name, + "url": url, + "service": service, + "method": method, + "messageTemplate": message_template, + } + ) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py index e643b1ae9..de7253c07 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py @@ -55,26 +55,26 @@ def pipeline_policy(pipeline_policy, threat_assessment_service): # pylint: disa message_template="threat.v1.ThreatRequest{uri: request.path, source_ip: source.address}", ) - pipeline_policy.add_request_grpc_method( + pipeline_policy.on_http_request.add_grpc_method( method="assess-threat", var="threatResponse", predicate='"x-assess-threat" in request.headers', ) - pipeline_policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) - pipeline_policy.add_request_deny( + pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + pipeline_policy.on_http_request.add_deny( predicate=f"threatResponse.threat_level >= {THREAT_THRESHOLD}", with_status=403, ) - pipeline_policy.add_response_headers( + pipeline_policy.on_http_response.add_headers( [["x-threat-assessed", "true"]], predicate='"x-assess-threat" in request.headers', ) - pipeline_policy.add_response_headers( + pipeline_policy.on_http_response.add_headers( [["x-threat-assessed", "false"]], predicate='!("x-assess-threat" in request.headers)', ) - pipeline_policy.add_response_headers([["x-threat-threshold", str(THREAT_THRESHOLD)]]) + pipeline_policy.on_http_response.add_headers([["x-threat-threshold", str(THREAT_THRESHOLD)]]) return pipeline_policy diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth.py index 4975ecc57..c8de923f5 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth.py @@ -23,8 +23,8 @@ def auth(oidc_provider): @pytest.fixture(scope="module") def pipeline_policy(pipeline_policy): """PipelinePolicy with deny action and response header.""" - pipeline_policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) - pipeline_policy.add_response_headers([["x-pipeline-policy", "active"]]) + pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + pipeline_policy.on_http_response.add_headers([["x-pipeline-policy", "active"]]) return pipeline_policy @@ -54,4 +54,4 @@ def test_auth_and_pipeline_unauthorized(client): def test_auth_and_pipeline_blocked_path(client, auth): """Authenticated request to blocked path is denied by PipelinePolicy deny action.""" response = client.get("/blocked", auth=auth) - assert response.status_code == 403 \ No newline at end of file + assert response.status_code == 403 diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth_ratelimit.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth_ratelimit.py index 0dcef6954..07202016d 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth_ratelimit.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth_ratelimit.py @@ -31,8 +31,8 @@ def rate_limit(rate_limit): @pytest.fixture(scope="module") def pipeline_policy(pipeline_policy): """PipelinePolicy with deny action and response header.""" - pipeline_policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) - pipeline_policy.add_response_headers([["x-pipeline-policy", "active"]]) + pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + pipeline_policy.on_http_response.add_headers([["x-pipeline-policy", "active"]]) return pipeline_policy @@ -74,4 +74,4 @@ def test_all_policies_rate_limited(client, auth): assert resp.headers.get("x-pipeline-policy") == "active" response = client.get("/get", auth=auth) - assert response.status_code == 429 \ No newline at end of file + assert response.status_code == 429 diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py index 3b02ef654..a1bc22b70 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py @@ -8,8 +8,8 @@ @pytest.fixture(scope="module") def pipeline_policy(pipeline_policy): """Configure PipelinePolicy with deny action and response headers.""" - pipeline_policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) - pipeline_policy.add_response_headers([["x-pipeline-policy", "active"]]) + pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + pipeline_policy.on_http_response.add_headers([["x-pipeline-policy", "active"]]) return pipeline_policy diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py index 6b41236ef..0adb353c9 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py @@ -15,8 +15,8 @@ def commit(): def test_first_deny_wins(request, cluster, blame, route, client): """When two deny actions match, the first one in spec order determines the response status.""" policy = PipelinePolicy.create_instance(cluster, blame("order"), route) - policy.add_request_deny(predicate='request.url_path == "/order-test"', with_status=403) - policy.add_request_deny(predicate='request.url_path == "/order-test"', with_status=429) + policy.on_http_request.add_deny(predicate='request.url_path == "/order-test"', with_status=403) + policy.on_http_request.add_deny(predicate='request.url_path == "/order-test"', with_status=429) request.addfinalizer(policy.delete) policy.commit() policy.wait_for_ready() @@ -38,10 +38,10 @@ def test_fail_before_deny(request, cluster, blame, route, client, threat_assessm method="AssessRequest", message_template="threat.v1.ThreatRequest{uri: request.path}", ) - policy.add_request_grpc_method(method="assess", var="threat") - policy.add_request_fail("threat too high", predicate="threat.threat_level >= 4") - # policy.add_request_deny(predicate="true", with_status=403) - policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + policy.on_http_request.add_grpc_method(method="assess", var="threat") + policy.on_http_request.add_fail("threat too high", predicate="threat.threat_level >= 4") + # policy.request.add_deny(predicate="true", with_status=403) + policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) request.addfinalizer(policy.delete) policy.commit() policy.wait_for_ready() @@ -53,8 +53,8 @@ def test_fail_before_deny(request, cluster, blame, route, client, threat_assessm def test_response_action_ordering(request, cluster, blame, route, client): """Response actions execute in spec order; both headers from separate actions are present.""" policy = PipelinePolicy.create_instance(cluster, blame("respord"), route) - policy.add_response_headers([["x-first", "alpha"]]) - policy.add_response_headers([["x-second", "bravo"]]) + policy.on_http_response.add_headers([["x-first", "alpha"]]) + policy.on_http_response.add_headers([["x-second", "bravo"]]) request.addfinalizer(policy.delete) policy.commit() policy.wait_for_ready() @@ -80,7 +80,7 @@ def test_empty_pipeline(request, cluster, blame, route, client): def test_request_only_pipeline(request, cluster, blame, route, client): """Pipeline with only request actions: deny works, no response modifications.""" policy = PipelinePolicy.create_instance(cluster, blame("reqonly"), route) - policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) request.addfinalizer(policy.delete) policy.commit() policy.wait_for_ready() @@ -92,7 +92,7 @@ def test_request_only_pipeline(request, cluster, blame, route, client): def test_response_only_pipeline(request, cluster, blame, route, client): """Pipeline with only response actions: all requests pass, headers are modified.""" policy = PipelinePolicy.create_instance(cluster, blame("resonly"), route) - policy.add_response_headers([["x-resp-only", "true"]]) + policy.on_http_response.add_headers([["x-resp-only", "true"]]) request.addfinalizer(policy.delete) policy.commit() policy.wait_for_ready() @@ -105,8 +105,8 @@ def test_response_only_pipeline(request, cluster, blame, route, client): def test_mixed_pipeline(request, cluster, blame, route, client): """Pipeline with both request deny and response headers executes in full.""" policy = PipelinePolicy.create_instance(cluster, blame("mixed"), route) - policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) - policy.add_response_headers([["x-mixed", "true"]]) + policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + policy.on_http_response.add_headers([["x-mixed", "true"]]) request.addfinalizer(policy.delete) policy.commit() policy.wait_for_ready() @@ -115,4 +115,4 @@ def test_mixed_pipeline(request, cluster, blame, route, client): assert response.status_code == 200 assert response.headers.get("x-mixed") == "true" - assert client.get("/blocked").status_code == 403 \ No newline at end of file + assert client.get("/blocked").status_code == 403 diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py index ac4d5ff93..2a9437e6f 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py @@ -9,37 +9,37 @@ def pipeline_policy(pipeline_policy): """PipelinePolicy with multiple deny configurations covering all deny variants.""" # Path-based deny - pipeline_policy.add_request_deny(predicate='request.url_path == "/deny-path"', with_status=403) + pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/deny-path"', with_status=403) # Header-based deny - pipeline_policy.add_request_deny(predicate='"x-deny-me" in request.headers', with_status=403) + pipeline_policy.on_http_request.add_deny(predicate='"x-deny-me" in request.headers', with_status=403) # Custom status code - pipeline_policy.add_request_deny(predicate='request.url_path == "/custom-status"', with_status=429) + pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/custom-status"', with_status=429) # Custom headers - pipeline_policy.add_request_deny( + pipeline_policy.on_http_request.add_deny( predicate='request.url_path == "/custom-headers"', with_status=403, with_headers='[["x-deny-reason", "blocked"]]', ) # Custom body - pipeline_policy.add_request_deny( + pipeline_policy.on_http_request.add_deny( predicate='request.url_path == "/custom-body"', with_status=403, with_body="Access denied", ) # All response fields - pipeline_policy.add_request_deny( + pipeline_policy.on_http_request.add_deny( predicate='request.url_path == "/custom-all"', with_status=451, with_headers='[["x-deny-reason", "full-custom"]]', with_body="Fully customized denial", ) # Response phase deny — status override - pipeline_policy.add_response_deny( + pipeline_policy.on_http_response.add_deny( predicate='"x-override-code" in request.headers', with_status=503, ) # Response phase deny — all fields - pipeline_policy.add_response_deny( + pipeline_policy.on_http_response.add_deny( predicate='"x-resp-deny" in request.headers', with_status=418, with_headers='[["x-deny-phase", "response"]]', diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py index 9326324ba..3929b1e40 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py @@ -57,8 +57,8 @@ def test_nonexistent_url(request, cluster, blame, route): method="AssessRequest", message_template="threat.v1.ThreatRequest{uri: request.path}", ) - policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) - policy.add_request_grpc_method(method="bad-method") + policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + policy.on_http_request.add_grpc_method(method="bad-method") request.addfinalizer(policy.delete) policy.commit() @@ -83,8 +83,8 @@ def test_wrong_service_name(request, cluster, blame, route, threat_assessment_se method="DoSomething", message_template="nonexistent.v1.FakeRequest{uri: request.path}", ) - policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) - policy.add_request_grpc_method(method="bad-service") + policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + policy.on_http_request.add_grpc_method(method="bad-service") request.addfinalizer(policy.delete) policy.commit() @@ -109,8 +109,8 @@ def test_wrong_method_name(request, cluster, blame, route, threat_assessment_ser method="NonExistentMethod", message_template="threat.v1.ThreatRequest{uri: request.path}", ) - policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) - policy.add_request_grpc_method(method="wrong-method") + policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + policy.on_http_request.add_grpc_method(method="wrong-method") request.addfinalizer(policy.delete) policy.commit() diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_fail.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_fail.py index 0fc67cb81..b92189af5 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_fail.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_fail.py @@ -18,12 +18,12 @@ def pipeline_policy(pipeline_policy, threat_assessment_service): method="AssessRequest", message_template="threat.v1.ThreatRequest{uri: request.path}", ) - pipeline_policy.add_request_grpc_method(method="assess", var="threat") - pipeline_policy.add_request_fail( + pipeline_policy.on_http_request.add_grpc_method(method="assess", var="threat") + pipeline_policy.on_http_request.add_fail( "threat level too high", predicate="threat.threat_level >= 4", ) - pipeline_policy.add_response_headers([["x-pipeline-active", "true"]]) + pipeline_policy.on_http_response.add_headers([["x-pipeline-active", "true"]]) return pipeline_policy @@ -44,4 +44,4 @@ def test_fail_no_response_headers_on_failure(client): """Response headers are not added when fail action terminates the chain.""" response = client.get("/admin") assert response.status_code == 500 - assert response.headers.get("x-pipeline-active") is None \ No newline at end of file + assert response.headers.get("x-pipeline-active") is None diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc.py index 51644625c..25530cdcd 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc.py @@ -21,26 +21,26 @@ def pipeline_policy(pipeline_policy, threat_assessment_service): # pylint: disa message_template="threat.v1.ThreatRequest{uri: request.path, source_ip: source.address}", ) - pipeline_policy.add_request_grpc_method( + pipeline_policy.on_http_request.add_grpc_method( method="assess-threat", var="threatResponse", predicate='"x-assess-threat" in request.headers', ) - pipeline_policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) - pipeline_policy.add_request_deny( + pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + pipeline_policy.on_http_request.add_deny( predicate=f"threatResponse.threat_level >= {THREAT_THRESHOLD}", with_status=403, ) - pipeline_policy.add_response_headers( + pipeline_policy.on_http_response.add_headers( [["x-threat-assessed", "true"]], predicate='"x-assess-threat" in request.headers', ) - pipeline_policy.add_response_headers( + pipeline_policy.on_http_response.add_headers( [["x-threat-assessed", "false"]], predicate='!("x-assess-threat" in request.headers)', ) - pipeline_policy.add_response_headers([["x-threat-threshold", str(THREAT_THRESHOLD)]]) + pipeline_policy.on_http_response.add_headers([["x-threat-threshold", str(THREAT_THRESHOLD)]]) return pipeline_policy diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py index 98172206b..d897b398a 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py @@ -23,8 +23,8 @@ def test_grpc_upstream_unavailable(request, cluster, blame, route): method="AssessRequest", message_template="threat.v1.ThreatRequest{uri: request.path}", ) - policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) - policy.add_request_grpc_method(method="bad-method") + policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + policy.on_http_request.add_grpc_method(method="bad-method") request.addfinalizer(policy.delete) policy.commit() @@ -48,8 +48,8 @@ def test_grpc_wrong_service_name(request, cluster, blame, route, threat_assessme method="DoSomething", message_template="nonexistent.v1.FakeRequest{uri: request.path}", ) - policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) - policy.add_request_grpc_method(method="bad-service") + policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + policy.on_http_request.add_grpc_method(method="bad-service") request.addfinalizer(policy.delete) policy.commit() @@ -73,8 +73,8 @@ def test_grpc_wrong_method_name(request, cluster, blame, route, threat_assessmen method="NonExistentMethod", message_template="threat.v1.ThreatRequest{uri: request.path}", ) - policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) - policy.add_request_grpc_method(method="wrong-method") + policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + policy.on_http_request.add_grpc_method(method="wrong-method") request.addfinalizer(policy.delete) policy.commit() @@ -98,8 +98,8 @@ def test_fail_after_grpc_call(request, cluster, blame, route, client, threat_ass method="AssessRequest", message_template="threat.v1.ThreatRequest{uri: request.path}", ) - policy.add_request_grpc_method(method="assess", var="threat") - policy.add_request_fail( + policy.on_http_request.add_grpc_method(method="assess", var="threat") + policy.on_http_request.add_fail( "threat assessment returned invalid level", predicate="threat.threat_level < 0", ) @@ -109,4 +109,4 @@ def test_fail_after_grpc_call(request, cluster, blame, route, client, threat_ass policy.wait_for_ready() response = client.get("/get") - assert response.status_code == 200 \ No newline at end of file + assert response.status_code == 200 diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py index 987943d7a..fa116bbe9 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py @@ -86,7 +86,7 @@ def client3(route3, hostname3): # pylint: disable=unused-argument @pytest.fixture(scope="module") def pipeline_policy(pipeline_policy): """PipelinePolicy with response header targeting only the first route.""" - pipeline_policy.add_response_headers([["x-pipeline-policy", "active"]]) + pipeline_policy.on_http_response.add_headers([["x-pipeline-policy", "active"]]) return pipeline_policy @@ -113,4 +113,4 @@ def test_policy_does_not_affect_other_gateway(client3): time.sleep(PROPAGATION_WAIT) response = client3.get("/get") assert response.status_code == 200 - assert response.headers.get("x-pipeline-policy") is None \ No newline at end of file + assert response.headers.get("x-pipeline-policy") is None diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py index 3425b261a..be500d099 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py @@ -10,8 +10,8 @@ @pytest.fixture(scope="module") def pipeline_policy(pipeline_policy): """PipelinePolicy with deny action and response header.""" - pipeline_policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) - pipeline_policy.add_response_headers([["x-pipeline-policy", "active"]]) + pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + pipeline_policy.on_http_response.add_headers([["x-pipeline-policy", "active"]]) return pipeline_policy @@ -38,7 +38,7 @@ def test_update_policy(client, pipeline_policy): assert response.headers.get("x-pipeline-policy") == "active" assert response.headers.get("x-pipeline-updated") is None - pipeline_policy.add_response_headers([["x-pipeline-updated", "true"]]) + pipeline_policy.on_http_response.add_headers([["x-pipeline-updated", "true"]]) pipeline_policy.wait_for_ready() response = client.get("/get") diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_ratelimit.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_ratelimit.py index 7ed1d5bfc..608d3575d 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_ratelimit.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_ratelimit.py @@ -17,7 +17,7 @@ def rate_limit(rate_limit): @pytest.fixture(scope="module") def pipeline_policy(pipeline_policy): """PipelinePolicy with response header.""" - pipeline_policy.add_response_headers([["x-pipeline-policy", "active"]]) + pipeline_policy.on_http_response.add_headers([["x-pipeline-policy", "active"]]) return pipeline_policy @@ -39,4 +39,4 @@ def test_rate_limit_and_pipeline(client): assert resp.headers.get("x-pipeline-policy") == "active" response = client.get("/get") - assert response.status_code == 429 \ No newline at end of file + assert response.status_code == 429 diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_response.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_response.py index 96b370757..7d6b5c4d1 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_response.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_response.py @@ -8,20 +8,20 @@ @pytest.fixture(scope="module") def pipeline_policy(pipeline_policy): """PipelinePolicy with various add_headers response actions for testing.""" - pipeline_policy.add_response_headers([["x-single", "one"]]) - pipeline_policy.add_response_headers([["x-multi-a", "alpha"], ["x-multi-b", "bravo"]]) - pipeline_policy.add_response_headers([["x-separate", "separate-value"]]) + pipeline_policy.on_http_response.add_headers([["x-single", "one"]]) + pipeline_policy.on_http_response.add_headers([["x-multi-a", "alpha"], ["x-multi-b", "bravo"]]) + pipeline_policy.on_http_response.add_headers([["x-separate", "separate-value"]]) - pipeline_policy.add_response_headers( + pipeline_policy.on_http_response.add_headers( [["x-conditional", "present"]], predicate='"x-trigger" in request.headers', ) - pipeline_policy.add_response_headers( + pipeline_policy.on_http_response.add_headers( [["x-mode", "active"]], predicate='"x-trigger" in request.headers', ) - pipeline_policy.add_response_headers( + pipeline_policy.on_http_response.add_headers( [["x-mode", "inactive"]], predicate='!("x-trigger" in request.headers)', ) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_targeting.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_targeting.py index 4b15ab04e..748d9868c 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_targeting.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_targeting.py @@ -40,8 +40,8 @@ def client2(route2, hostname2): # pylint: disable=unused-argument def gateway_policy(cluster, blame, gateway): """PipelinePolicy targeting the Gateway with deny and response headers.""" policy = PipelinePolicy.create_instance(cluster, blame("gw-pp"), gateway) - policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) - policy.add_response_headers([["x-gateway-policy", "active"]]) + policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + policy.on_http_response.add_headers([["x-gateway-policy", "active"]]) return policy @@ -67,4 +67,4 @@ def test_gateway_target_blocked_path(request, client_fixture): """Blocked path is denied on both routes when policy targets the gateway.""" http_client = request.getfixturevalue(client_fixture) response = http_client.get("/blocked") - assert response.status_code == 403 \ No newline at end of file + assert response.status_code == 403 diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py index 6582751ee..aca1f11a7 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py @@ -22,7 +22,7 @@ def test_invalid_target_ref(request, cluster, blame): name="does-not-exist", ) policy = PipelinePolicy.create_instance(cluster, blame("bad-target"), target) - policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) request.addfinalizer(policy.delete) policy.commit() @@ -41,7 +41,7 @@ def test_invalid_gateway_target_ref(request, cluster, blame): name="does-not-exist", ) policy = PipelinePolicy.create_instance(cluster, blame("bad-gw"), target) - policy.add_request_deny(predicate='request.url_path == "/blocked"', with_status=403) + policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) request.addfinalizer(policy.delete) policy.commit() @@ -55,7 +55,7 @@ def test_invalid_gateway_target_ref(request, cluster, blame): def test_invalid_cel_expression(request, cluster, blame, route): """PipelinePolicy with malformed CEL predicate fails to enforce.""" policy = PipelinePolicy.create_instance(cluster, blame("bad-cel"), route) - policy.add_request_deny(predicate="INVALID CEL !!!", with_status=403) + policy.on_http_request.add_deny(predicate="INVALID CEL !!!", with_status=403) request.addfinalizer(policy.delete) policy.commit() @@ -70,8 +70,8 @@ def test_variable_forward_reference(request, cluster, blame, route): """PipelinePolicy referencing a variable before it is defined should fail validation.""" var_name = "threatResponse" policy = PipelinePolicy.create_instance(cluster, blame("fwd-ref"), route) - policy.add_request_deny(predicate=f"{var_name}.threat_level >= 50", with_status=403) - policy.add_request_grpc_method(method="nonexistent-method", var=var_name) + policy.on_http_request.add_deny(predicate=f"{var_name}.threat_level >= 50", with_status=403) + policy.on_http_request.add_grpc_method(method="nonexistent-method", var=var_name) request.addfinalizer(policy.delete) policy.commit() @@ -86,8 +86,8 @@ def test_duplicate_variable_name(request, cluster, blame, route): """PipelinePolicy with two gRPC methods using the same variable name should fail validation.""" var_name = "dupVar" policy = PipelinePolicy.create_instance(cluster, blame("dup-var"), route) - policy.add_request_grpc_method(method="method-a", var=var_name) - policy.add_request_grpc_method(method="method-b", var=var_name) + policy.on_http_request.add_grpc_method(method="method-a", var=var_name) + policy.on_http_request.add_grpc_method(method="method-b", var=var_name) request.addfinalizer(policy.delete) policy.commit() From 0fdd9aa5f2424dcf865ec2b6eaad2dd1f5fcbd4f Mon Sep 17 00:00:00 2001 From: Silvia Tarabova Date: Mon, 1 Jun 2026 16:41:44 +0200 Subject: [PATCH 14/22] refactor: remove duplicate PipelinePolicy tests and move interaction tests to subdirectory Signed-off-by: Silvia Tarabova --- config/settings.local.yaml.tpl | 2 + config/settings.yaml | 2 + .../extensions/pipeline_policy/conftest.py | 4 +- .../pipeline_policy/interactions/__init__.py | 0 .../pipeline_policy/interactions/conftest.py | 26 ++++ .../test_pipeline_policy_auth.py | 15 --- .../test_pipeline_policy_auth_ratelimit.py | 34 +++++ .../test_pipeline_policy_ratelimit.py | 9 -- .../test_pipeline_policy_action_method.py | 102 --------------- .../test_pipeline_policy_auth_ratelimit.py | 77 ----------- .../test_pipeline_policy_error_status.py | 122 ------------------ .../test_pipeline_policy_grpc_errors.py | 6 +- 12 files changed, 69 insertions(+), 330 deletions(-) create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/__init__.py create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/conftest.py rename testsuite/tests/singlecluster/extensions/pipeline_policy/{ => interactions}/test_pipeline_policy_auth.py (76%) create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_auth_ratelimit.py rename testsuite/tests/singlecluster/extensions/pipeline_policy/{ => interactions}/test_pipeline_policy_ratelimit.py (81%) delete mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py delete mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth_ratelimit.py delete mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py diff --git a/config/settings.local.yaml.tpl b/config/settings.local.yaml.tpl index 3760d9a06..2b32269a0 100644 --- a/config/settings.local.yaml.tpl +++ b/config/settings.local.yaml.tpl @@ -16,6 +16,8 @@ # password: "testPassword" # spicedb: # image: "SPICEDB_IMAGE" +# pipeline_policy_extension_service: +# image: "PIPELINE_POLICY_EXTENSION_SERVICE_IMAGE" # auth0: # client_id: "CLIENT_ID" # client_secret: "CLIENT_SECRET" diff --git a/config/settings.yaml b/config/settings.yaml index 22e67d4c8..824c57908 100644 --- a/config/settings.yaml +++ b/config/settings.yaml @@ -18,6 +18,8 @@ default: image: "quay.io/rhn_support_azgabur/mockserver:latest" spicedb: image: "quay.io/authzed/spicedb:latest" + pipeline_policy_extension_service: + image: "quay.io/kuadrant/threat-assessment-service:latest" prometheus: project: "openshift-monitoring" service: "thanos-querier" diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py index d1894f424..b02a6e1d5 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py @@ -15,7 +15,7 @@ def pipeline_policy(cluster, blame, route): @pytest.fixture(scope="module") -def threat_assessment_service(request, cluster, blame, module_label): +def threat_assessment_service(request, cluster, blame, module_label, testconfig): """Deploys the ThreatAssessmentService gRPC backend""" name = blame("threat") match_labels = {"app": module_label, "deployment": name} @@ -24,7 +24,7 @@ def threat_assessment_service(request, cluster, blame, module_label): cluster, name, container_name="threat-assessment", - image="quay.io/kuadrant/threat-assessment-service:latest", + image=testconfig["pipeline_policy_extension_service"]["image"], ports={"grpc": 8080}, selector=Selector(matchLabels=match_labels), labels={"app": module_label}, diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/__init__.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/conftest.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/conftest.py new file mode 100644 index 000000000..01958d124 --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/conftest.py @@ -0,0 +1,26 @@ +"""Fixtures for PipelinePolicy interaction tests with AuthPolicy and RateLimitPolicy.""" + +import pytest + +from testsuite.httpx.auth import HttpxOidcClientAuth +from testsuite.kuadrant.policy.rate_limit import Limit + + +@pytest.fixture(scope="module") +def authorization(authorization, oidc_provider): + """AuthPolicy with OIDC identity verification.""" + authorization.identity.add_oidc("default", oidc_provider.well_known["issuer"]) + return authorization + + +@pytest.fixture(scope="module") +def auth(oidc_provider): + """Valid OIDC authentication for requests.""" + return HttpxOidcClientAuth(oidc_provider.get_token, "authorization") + + +@pytest.fixture(scope="module") +def rate_limit(rate_limit): + """RateLimitPolicy with a low limit for testing.""" + rate_limit.add_limit("basic", [Limit(3, "10s")]) + return rate_limit diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_auth.py similarity index 76% rename from testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth.py rename to testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_auth.py index c8de923f5..4ace7ebcd 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_auth.py @@ -2,24 +2,9 @@ import pytest -from testsuite.httpx.auth import HttpxOidcClientAuth - pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] -@pytest.fixture(scope="module") -def authorization(authorization, oidc_provider): - """AuthPolicy with OIDC identity verification.""" - authorization.identity.add_oidc("default", oidc_provider.well_known["issuer"]) - return authorization - - -@pytest.fixture(scope="module") -def auth(oidc_provider): - """Valid OIDC authentication for requests.""" - return HttpxOidcClientAuth(oidc_provider.get_token, "authorization") - - @pytest.fixture(scope="module") def pipeline_policy(pipeline_policy): """PipelinePolicy with deny action and response header.""" diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_auth_ratelimit.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_auth_ratelimit.py new file mode 100644 index 000000000..bb5f1bad5 --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_auth_ratelimit.py @@ -0,0 +1,34 @@ +"""Tests for PipelinePolicy interaction with both AuthPolicy and RateLimitPolicy.""" + +import pytest + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + + +@pytest.fixture(scope="module") +def pipeline_policy(pipeline_policy): + """PipelinePolicy with deny action and response header.""" + pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + pipeline_policy.on_http_response.add_headers([["x-pipeline-policy", "active"]]) + return pipeline_policy + + +@pytest.fixture(scope="module", autouse=True) +def commit(request, authorization, rate_limit, pipeline_policy): + """Commit AuthPolicy, RateLimitPolicy, and PipelinePolicy.""" + for component in [authorization, rate_limit, pipeline_policy]: + request.addfinalizer(component.delete) + component.commit() + component.wait_for_ready() + + +@pytest.mark.flaky(reruns=3, reruns_delay=15) +def test_all_policies_rate_limited(client, auth): + """Rate limit is enforced alongside AuthPolicy and PipelinePolicy.""" + responses = client.get_many("/get", 3, auth=auth) + responses.assert_all(status_code=200) + for resp in responses: + assert resp.headers.get("x-pipeline-policy") == "active" + + response = client.get("/get", auth=auth) + assert response.status_code == 429 diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_ratelimit.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_ratelimit.py similarity index 81% rename from testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_ratelimit.py rename to testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_ratelimit.py index 608d3575d..e4f7d42db 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_ratelimit.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_ratelimit.py @@ -2,18 +2,9 @@ import pytest -from testsuite.kuadrant.policy.rate_limit import Limit - pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] -@pytest.fixture(scope="module") -def rate_limit(rate_limit): - """RateLimitPolicy with a low limit for testing.""" - rate_limit.add_limit("basic", [Limit(3, "10s")]) - return rate_limit - - @pytest.fixture(scope="module") def pipeline_policy(pipeline_policy): """PipelinePolicy with response header.""" diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py deleted file mode 100644 index de7253c07..000000000 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_action_method.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Tests for PipelinePolicy: action method calling a gRPC backend and conditional response headers.""" - -import pytest - -from testsuite.kubernetes import Selector -from testsuite.kubernetes.deployment import Deployment -from testsuite.kubernetes.service import Service, ServicePort - - -@pytest.fixture(scope="module") -def threat_assessment_service(request, cluster, blame, module_label): - """Deploys the ThreatAssessmentService gRPC backend""" - name = blame("threat") - match_labels = {"app": module_label, "deployment": name} - - deployment = Deployment.create_instance( - cluster, - name, - container_name="threat-assessment", - image="quay.io/kuadrant/threat-assessment-service:latest", - ports={"grpc": 8080}, - selector=Selector(matchLabels=match_labels), - labels={"app": module_label}, - ) - request.addfinalizer(deployment.delete) - deployment.commit() - deployment.wait_for_ready() - - service = Service.create_instance( - cluster, - name, - selector=match_labels, - ports=[ServicePort(name="grpc", port=8080, targetPort="grpc")], - labels={"app": module_label}, - ) - request.addfinalizer(service.delete) - service.commit() - return service - - -THREAT_THRESHOLD = 50 - - -@pytest.fixture(scope="module") -def pipeline_policy(pipeline_policy, threat_assessment_service): # pylint: disable=unused-argument - """Configure PipelinePolicy with threat assessment gRPC action and conditional headers.""" - svc_url = ( - f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" - ) - pipeline_policy.add_action_method( - name="assess-threat", - url=svc_url, - service="threat.v1.ThreatAssessmentService", - method="AssessRequest", - message_template="threat.v1.ThreatRequest{uri: request.path, source_ip: source.address}", - ) - - pipeline_policy.on_http_request.add_grpc_method( - method="assess-threat", - var="threatResponse", - predicate='"x-assess-threat" in request.headers', - ) - pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) - pipeline_policy.on_http_request.add_deny( - predicate=f"threatResponse.threat_level >= {THREAT_THRESHOLD}", - with_status=403, - ) - - pipeline_policy.on_http_response.add_headers( - [["x-threat-assessed", "true"]], - predicate='"x-assess-threat" in request.headers', - ) - pipeline_policy.on_http_response.add_headers( - [["x-threat-assessed", "false"]], - predicate='!("x-assess-threat" in request.headers)', - ) - pipeline_policy.on_http_response.add_headers([["x-threat-threshold", str(THREAT_THRESHOLD)]]) - - return pipeline_policy - - -def test_allowed_path(client): - """Request to an allowed path returns 200 without threat assessment.""" - response = client.get("/get") - assert response.status_code == 200 - - assert response.headers.get("x-threat-assessed") == "false" - assert response.headers.get("x-threat-threshold") == str(THREAT_THRESHOLD) - - -def test_blocked_path(client): - """Request to /blocked is denied by the allow rule.""" - response = client.get("/blocked") - assert response.status_code == 403 - - -def test_threat_assessment_safe(client): - """Request with x-assess-threat header to a safe path passes threat check.""" - response = client.get("/get", headers={"x-assess-threat": "true"}) - assert response.status_code == 200 - assert response.headers.get("x-threat-assessed") == "true" - assert response.headers.get("x-threat-threshold") == str(THREAT_THRESHOLD) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth_ratelimit.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth_ratelimit.py deleted file mode 100644 index 07202016d..000000000 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_auth_ratelimit.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Tests for PipelinePolicy interaction with both AuthPolicy and RateLimitPolicy.""" - -import pytest - -from testsuite.httpx.auth import HttpxOidcClientAuth -from testsuite.kuadrant.policy.rate_limit import Limit - -pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] - - -@pytest.fixture(scope="module") -def authorization(authorization, oidc_provider): - """AuthPolicy with OIDC identity verification.""" - authorization.identity.add_oidc("default", oidc_provider.well_known["issuer"]) - return authorization - - -@pytest.fixture(scope="module") -def auth(oidc_provider): - """Valid OIDC authentication for requests.""" - return HttpxOidcClientAuth(oidc_provider.get_token, "authorization") - - -@pytest.fixture(scope="module") -def rate_limit(rate_limit): - """RateLimitPolicy with a low limit for testing.""" - rate_limit.add_limit("basic", [Limit(3, "10s")]) - return rate_limit - - -@pytest.fixture(scope="module") -def pipeline_policy(pipeline_policy): - """PipelinePolicy with deny action and response header.""" - pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) - pipeline_policy.on_http_response.add_headers([["x-pipeline-policy", "active"]]) - return pipeline_policy - - -@pytest.fixture(scope="module", autouse=True) -def commit(request, authorization, rate_limit, pipeline_policy): - """Commit AuthPolicy, RateLimitPolicy, and PipelinePolicy.""" - for component in [authorization, rate_limit, pipeline_policy]: - request.addfinalizer(component.delete) - component.commit() - component.wait_for_ready() - - -def test_all_policies_allowed(client, auth): - """Authenticated request passes auth, rate limit, and gets PipelinePolicy header.""" - response = client.get("/get", auth=auth) - assert response.status_code == 200 - assert response.headers.get("x-pipeline-policy") == "active" - - -def test_all_policies_unauthorized(client): - """Unauthenticated request is rejected by AuthPolicy before other policies run.""" - response = client.get("/get") - assert response.status_code == 401 - assert response.headers.get("x-pipeline-policy") is None - - -def test_all_policies_blocked_path(client, auth): - """Authenticated request to blocked path is denied by PipelinePolicy deny action.""" - response = client.get("/blocked", auth=auth) - assert response.status_code == 403 - - -@pytest.mark.flaky(reruns=3, reruns_delay=15) -def test_all_policies_rate_limited(client, auth): - """Rate limit is enforced alongside AuthPolicy and PipelinePolicy.""" - responses = client.get_many("/get", 3, auth=auth) - responses.assert_all(status_code=200) - for resp in responses: - assert resp.headers.get("x-pipeline-policy") == "active" - - response = client.get("/get", auth=auth) - assert response.status_code == 429 diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py deleted file mode 100644 index 3929b1e40..000000000 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_error_status.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Tests that PipelinePolicy surfaces error status conditions for invalid action method configurations.""" - -import pytest - -from testsuite.kubernetes import Selector -from testsuite.kubernetes.deployment import Deployment -from testsuite.kubernetes.service import Service, ServicePort -from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy -from testsuite.kuadrant.policy import has_condition - -pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] - - -@pytest.fixture(scope="module", autouse=True) -def commit(): - """No module-level policy; each test creates its own policy with bad configuration.""" - - -@pytest.fixture(scope="module") -def threat_assessment_service(request, cluster, blame, module_label): - """Deploys the ThreatAssessmentService gRPC backend""" - name = blame("threat") - match_labels = {"app": module_label, "deployment": name} - - deployment = Deployment.create_instance( - cluster, - name, - container_name="threat-assessment", - image="quay.io/kuadrant/threat-assessment-service:latest", - ports={"grpc": 8080}, - selector=Selector(matchLabels=match_labels), - labels={"app": module_label}, - ) - request.addfinalizer(deployment.delete) - deployment.commit() - deployment.wait_for_ready() - - service = Service.create_instance( - cluster, - name, - selector=match_labels, - ports=[ServicePort(name="grpc", port=8080, targetPort="grpc")], - labels={"app": module_label}, - ) - request.addfinalizer(service.delete) - service.commit() - return service - - -def test_nonexistent_url(request, cluster, blame, route): - """PipelinePolicy reports error when action method URL points to a non-existent service.""" - policy = PipelinePolicy.create_instance(cluster, blame("bad-url"), route) - policy.add_action_method( - name="bad-method", - url="grpc://does-not-exist.default.svc.cluster.local:8080", - service="threat.v1.ThreatAssessmentService", - method="AssessRequest", - message_template="threat.v1.ThreatRequest{uri: request.path}", - ) - policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) - policy.on_http_request.add_grpc_method(method="bad-method") - - request.addfinalizer(policy.delete) - policy.commit() - - # TODO: add expected message assertion - assert policy.wait_until( - has_condition("Accepted", "False", "Unknown"), - timelimit=60, - ), f"Policy did not reach expected error status, instead: {policy.refresh().model.status.conditions}" - - -def test_wrong_service_name(request, cluster, blame, route, threat_assessment_service): - """PipelinePolicy reports error when action method references a non-existent gRPC service name.""" - svc_url = ( - f"grpc://{threat_assessment_service.name()}" f".{threat_assessment_service.namespace()}.svc.cluster.local:8080" - ) - policy = PipelinePolicy.create_instance(cluster, blame("bad-svc"), route) - policy.add_action_method( - name="bad-service", - url=svc_url, - service="nonexistent.v1.FakeService", - method="DoSomething", - message_template="nonexistent.v1.FakeRequest{uri: request.path}", - ) - policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) - policy.on_http_request.add_grpc_method(method="bad-service") - - request.addfinalizer(policy.delete) - policy.commit() - - # TODO: add expected message assertion - assert policy.wait_until( - has_condition("Accepted", "False", "Unknown"), - timelimit=60, - ), f"Policy did not reach expected error status, instead: {policy.refresh().model.status.conditions}" - - -def test_wrong_method_name(request, cluster, blame, route, threat_assessment_service): - """PipelinePolicy reports error when action method references a non-existent gRPC method.""" - svc_url = ( - f"grpc://{threat_assessment_service.name()}" f".{threat_assessment_service.namespace()}.svc.cluster.local:8080" - ) - policy = PipelinePolicy.create_instance(cluster, blame("bad-meth"), route) - policy.add_action_method( - name="wrong-method", - url=svc_url, - service="threat.v1.ThreatAssessmentService", - method="NonExistentMethod", - message_template="threat.v1.ThreatRequest{uri: request.path}", - ) - policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) - policy.on_http_request.add_grpc_method(method="wrong-method") - - request.addfinalizer(policy.delete) - policy.commit() - - # TODO: add expected message assertion - assert policy.wait_until( - has_condition("Accepted", "False", "Unknown"), - timelimit=60, - ), f"Policy did not reach expected error status, instead: {policy.refresh().model.status.conditions}" diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py index d897b398a..d38d5b232 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py @@ -30,7 +30,7 @@ def test_grpc_upstream_unavailable(request, cluster, blame, route): policy.commit() assert policy.wait_until( - has_condition("Accepted", "False", "Unknown"), + has_condition("Accepted", "False", "Unknown", message="produced zero addresses"), timelimit=60, ), f"Policy did not reach expected error status, instead: {policy.refresh().model.status.conditions}" @@ -55,7 +55,7 @@ def test_grpc_wrong_service_name(request, cluster, blame, route, threat_assessme policy.commit() assert policy.wait_until( - has_condition("Accepted", "False", "Unknown"), + has_condition("Accepted", "False", "Unknown", message="failed to fetch service"), timelimit=60, ), f"Policy did not reach expected error status, instead: {policy.refresh().model.status.conditions}" @@ -80,7 +80,7 @@ def test_grpc_wrong_method_name(request, cluster, blame, route, threat_assessmen policy.commit() assert policy.wait_until( - has_condition("Accepted", "False", "Unknown"), + has_condition("Accepted", "False", "Unknown", message='method "NonExistentMethod" not found in service'), timelimit=60, ), f"Policy did not reach expected error status, instead: {policy.refresh().model.status.conditions}" From 15b09fde7f396cb3bf252100175048da5b00e82b Mon Sep 17 00:00:00 2001 From: Silvia Tarabova Date: Mon, 1 Jun 2026 18:08:28 +0200 Subject: [PATCH 15/22] refactor: deduplicate PipelinePolicy tests, extract constants, improve test isolation Signed-off-by: Silvia Tarabova --- .../extensions/pipeline_policy/conftest.py | 9 +++ .../test_pipeline_policy_basic.py | 20 +++++- .../test_pipeline_policy_composition.py | 25 ++----- .../test_pipeline_policy_deny.py | 6 +- .../test_pipeline_policy_fail.py | 10 +-- .../test_pipeline_policy_grpc.py | 18 ++--- .../test_pipeline_policy_grpc_errors.py | 27 ------- .../test_pipeline_policy_isolation.py | 19 +---- .../test_pipeline_policy_lifecycle.py | 70 +++++++++---------- testsuite/utils/constants.py | 13 ++++ 10 files changed, 93 insertions(+), 124 deletions(-) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py index b02a6e1d5..997b14635 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py @@ -8,6 +8,15 @@ from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy +@pytest.fixture(scope="session", autouse=True) +def check_pipeline_policy_crd(cluster, skip_or_fail): + """Skip all PipelinePolicy tests if the CRD is not installed on the cluster.""" + try: + cluster.do_action("get", "crd/pipelinepolicies.extensions.kuadrant.io") + except Exception: # pylint: disable=broad-except + skip_or_fail("PipelinePolicy CRD is not installed on the cluster") + + @pytest.fixture(scope="module") def pipeline_policy(cluster, blame, route): """PipelinePolicy targeting the test HTTPRoute""" diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py index a1bc22b70..017391768 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py @@ -1,7 +1,9 @@ -"""Basic happy-path tests for PipelinePolicy: deny action and response headers.""" +"""Basic happy-path tests for PipelinePolicy: status conditions, deny action and response headers.""" import pytest +from testsuite.kuadrant.policy import has_condition + pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] @@ -13,6 +15,22 @@ def pipeline_policy(pipeline_policy): return pipeline_policy +def test_status_accepted(pipeline_policy): + """PipelinePolicy reports Accepted: True after commit.""" + assert pipeline_policy.wait_until( + has_condition("Accepted", "True"), + timelimit=30, + ), f"Policy not Accepted, status: {pipeline_policy.refresh().model.status.conditions}" + + +def test_status_enforced(pipeline_policy): + """PipelinePolicy reports Enforced: True after commit.""" + assert pipeline_policy.wait_until( + has_condition("Enforced", "True"), + timelimit=30, + ), f"Policy not Enforced, status: {pipeline_policy.refresh().model.status.conditions}" + + def test_allowed_path(client): """Request to an allowed path returns 200 with the custom response header.""" response = client.get("/get") diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py index 0adb353c9..b16e0ec84 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py @@ -13,7 +13,7 @@ def commit(): def test_first_deny_wins(request, cluster, blame, route, client): - """When two deny actions match, the first one in spec order determines the response status.""" + """First deny action terminates the chain; the second deny with the same predicate never executes.""" policy = PipelinePolicy.create_instance(cluster, blame("order"), route) policy.on_http_request.add_deny(predicate='request.url_path == "/order-test"', with_status=403) policy.on_http_request.add_deny(predicate='request.url_path == "/order-test"', with_status=429) @@ -26,7 +26,7 @@ def test_first_deny_wins(request, cluster, blame, route, client): def test_fail_before_deny(request, cluster, blame, route, client, threat_assessment_service): - """Fail action before deny takes precedence when gRPC response triggers the fail predicate.""" + """Fail action terminates the chain before the deny action when gRPC response triggers the fail predicate.""" svc_url = ( f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" ) @@ -40,7 +40,6 @@ def test_fail_before_deny(request, cluster, blame, route, client, threat_assessm ) policy.on_http_request.add_grpc_method(method="assess", var="threat") policy.on_http_request.add_fail("threat too high", predicate="threat.threat_level >= 4") - # policy.request.add_deny(predicate="true", with_status=403) policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) request.addfinalizer(policy.delete) policy.commit() @@ -78,7 +77,7 @@ def test_empty_pipeline(request, cluster, blame, route, client): def test_request_only_pipeline(request, cluster, blame, route, client): - """Pipeline with only request actions: deny works, no response modifications.""" + """PipelinePolicy with only request actions and no response section works correctly.""" policy = PipelinePolicy.create_instance(cluster, blame("reqonly"), route) policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) request.addfinalizer(policy.delete) @@ -90,7 +89,7 @@ def test_request_only_pipeline(request, cluster, blame, route, client): def test_response_only_pipeline(request, cluster, blame, route, client): - """Pipeline with only response actions: all requests pass, headers are modified.""" + """PipelinePolicy with only response actions and no request section works correctly.""" policy = PipelinePolicy.create_instance(cluster, blame("resonly"), route) policy.on_http_response.add_headers([["x-resp-only", "true"]]) request.addfinalizer(policy.delete) @@ -100,19 +99,3 @@ def test_response_only_pipeline(request, cluster, blame, route, client): response = client.get("/get") assert response.status_code == 200 assert response.headers.get("x-resp-only") == "true" - - -def test_mixed_pipeline(request, cluster, blame, route, client): - """Pipeline with both request deny and response headers executes in full.""" - policy = PipelinePolicy.create_instance(cluster, blame("mixed"), route) - policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) - policy.on_http_response.add_headers([["x-mixed", "true"]]) - request.addfinalizer(policy.delete) - policy.commit() - policy.wait_for_ready() - - response = client.get("/get") - assert response.status_code == 200 - assert response.headers.get("x-mixed") == "true" - - assert client.get("/blocked").status_code == 403 diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py index 2a9437e6f..9ef4d59b8 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py @@ -11,7 +11,7 @@ def pipeline_policy(pipeline_policy): # Path-based deny pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/deny-path"', with_status=403) # Header-based deny - pipeline_policy.on_http_request.add_deny(predicate='"x-deny-me" in request.headers', with_status=403) + pipeline_policy.on_http_request.add_deny(predicate='"x-deny-me" in request.headers', with_status=401) # Custom status code pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/custom-status"', with_status=429) # Custom headers @@ -63,7 +63,7 @@ def test_cel_predicate_does_not_deny(client): def test_header_based_deny(client): """Request with a header matching the deny predicate is denied.""" response = client.get("/get", headers={"x-deny-me": "true"}) - assert response.status_code == 403 + assert response.status_code == 401 def test_header_based_deny_absent(client): @@ -75,7 +75,7 @@ def test_header_based_deny_absent(client): def test_multiple_deny_actions_or_behavior(client): """Multiple deny actions behave as OR: any matching predicate denies the request.""" assert client.get("/deny-path").status_code == 403 - assert client.get("/get", headers={"x-deny-me": "true"}).status_code == 403 + assert client.get("/get", headers={"x-deny-me": "true"}).status_code == 401 assert client.get("/get").status_code == 200 diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_fail.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_fail.py index b92189af5..25b55abc9 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_fail.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_fail.py @@ -28,9 +28,10 @@ def pipeline_policy(pipeline_policy, threat_assessment_service): def test_fail_triggers_on_high_threat(client): - """Fail action triggers when gRPC response threat_level meets the threshold.""" + """Fail action triggers when gRPC response threat_level meets the threshold and terminates the chain.""" response = client.get("/admin") assert response.status_code == 500 + assert response.headers.get("x-pipeline-active") is None def test_fail_does_not_trigger_on_low_threat(client): @@ -38,10 +39,3 @@ def test_fail_does_not_trigger_on_low_threat(client): response = client.get("/get") assert response.status_code == 200 assert response.headers.get("x-pipeline-active") == "true" - - -def test_fail_no_response_headers_on_failure(client): - """Response headers are not added when fail action terminates the chain.""" - response = client.get("/admin") - assert response.status_code == 500 - assert response.headers.get("x-pipeline-active") is None diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc.py index 25530cdcd..fc060f3a5 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc.py @@ -2,9 +2,9 @@ import pytest -pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] +from testsuite.utils.constants import THREAT_ASSESSMENT_THRESHOLD -THREAT_THRESHOLD = 50 +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] @pytest.fixture(scope="module") @@ -28,7 +28,7 @@ def pipeline_policy(pipeline_policy, threat_assessment_service): # pylint: disa ) pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) pipeline_policy.on_http_request.add_deny( - predicate=f"threatResponse.threat_level >= {THREAT_THRESHOLD}", + predicate=f"threatResponse.threat_level >= {THREAT_ASSESSMENT_THRESHOLD}", with_status=403, ) @@ -40,7 +40,7 @@ def pipeline_policy(pipeline_policy, threat_assessment_service): # pylint: disa [["x-threat-assessed", "false"]], predicate='!("x-assess-threat" in request.headers)', ) - pipeline_policy.on_http_response.add_headers([["x-threat-threshold", str(THREAT_THRESHOLD)]]) + pipeline_policy.on_http_response.add_headers([["x-threat-threshold", str(THREAT_ASSESSMENT_THRESHOLD)]]) return pipeline_policy @@ -50,7 +50,7 @@ def test_basic_grpc_upstream_call(client): response = client.get("/get", headers={"x-assess-threat": "true"}) assert response.status_code == 200 assert response.headers.get("x-threat-assessed") == "true" - assert response.headers.get("x-threat-threshold") == str(THREAT_THRESHOLD) + assert response.headers.get("x-threat-threshold") == str(THREAT_ASSESSMENT_THRESHOLD) def test_conditional_grpc_call_skipped(client): @@ -58,10 +58,4 @@ def test_conditional_grpc_call_skipped(client): response = client.get("/get") assert response.status_code == 200 assert response.headers.get("x-threat-assessed") == "false" - assert response.headers.get("x-threat-threshold") == str(THREAT_THRESHOLD) - - -def test_grpc_response_var_deny(client): - """Request to /blocked is denied by path-based deny rule.""" - response = client.get("/blocked") - assert response.status_code == 403 + assert response.headers.get("x-threat-threshold") == str(THREAT_ASSESSMENT_THRESHOLD) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py index d38d5b232..a30b32d2f 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py @@ -83,30 +83,3 @@ def test_grpc_wrong_method_name(request, cluster, blame, route, threat_assessmen has_condition("Accepted", "False", "Unknown", message='method "NonExistentMethod" not found in service'), timelimit=60, ), f"Policy did not reach expected error status, instead: {policy.refresh().model.status.conditions}" - - -def test_fail_after_grpc_call(request, cluster, blame, route, client, threat_assessment_service): - """Fail action after gRPC call terminates the chain when variable indicates invalid state.""" - svc_url = ( - f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" - ) - policy = PipelinePolicy.create_instance(cluster, blame("grpc-fail"), route) - policy.add_action_method( - name="assess", - url=svc_url, - service="threat.v1.ThreatAssessmentService", - method="AssessRequest", - message_template="threat.v1.ThreatRequest{uri: request.path}", - ) - policy.on_http_request.add_grpc_method(method="assess", var="threat") - policy.on_http_request.add_fail( - "threat assessment returned invalid level", - predicate="threat.threat_level < 0", - ) - - request.addfinalizer(policy.delete) - policy.commit() - policy.wait_for_ready() - - response = client.get("/get") - assert response.status_code == 200 diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py index fa116bbe9..3527a663d 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py @@ -7,14 +7,10 @@ from testsuite.gateway import GatewayListener from testsuite.gateway.gateway_api.gateway import KuadrantGateway from testsuite.gateway.gateway_api.route import HTTPRoute +from testsuite.utils.constants import EXTENSION_POLICY_PROPAGATION_WAIT pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] -PROPAGATION_WAIT = 10 - - -# --- Same gateway, different route --- - @pytest.fixture(scope="module") def hostname2(gateway, exposer, blame): @@ -41,9 +37,6 @@ def client2(route2, hostname2): # pylint: disable=unused-argument client.close() -# --- Different gateway --- - - @pytest.fixture(scope="module") def gateway2(request, cluster, blame, wildcard_domain, module_label): """Second gateway without any PipelinePolicy""" @@ -80,9 +73,6 @@ def client3(route3, hostname3): # pylint: disable=unused-argument client.close() -# --- Policy --- - - @pytest.fixture(scope="module") def pipeline_policy(pipeline_policy): """PipelinePolicy with response header targeting only the first route.""" @@ -90,9 +80,6 @@ def pipeline_policy(pipeline_policy): return pipeline_policy -# --- Tests --- - - def test_policy_affects_targeted_route(client): """Route with PipelinePolicy gets the response header.""" response = client.get("/get") @@ -102,7 +89,7 @@ def test_policy_affects_targeted_route(client): def test_policy_does_not_affect_other_route(client2): """Route without PipelinePolicy on the same gateway does not get the response header.""" - time.sleep(PROPAGATION_WAIT) + time.sleep(EXTENSION_POLICY_PROPAGATION_WAIT) response = client2.get("/get") assert response.status_code == 200 assert response.headers.get("x-pipeline-policy") is None @@ -110,7 +97,7 @@ def test_policy_does_not_affect_other_route(client2): def test_policy_does_not_affect_other_gateway(client3): """Route on a different gateway does not get the response header.""" - time.sleep(PROPAGATION_WAIT) + time.sleep(EXTENSION_POLICY_PROPAGATION_WAIT) response = client3.get("/get") assert response.status_code == 200 assert response.headers.get("x-pipeline-policy") is None diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py index be500d099..efccfb3f6 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py @@ -1,58 +1,56 @@ -"""Tests for PipelinePolicy lifecycle: status conditions, delete, and update.""" +"""Tests for PipelinePolicy lifecycle: update and delete.""" import pytest -from testsuite.kuadrant.policy import has_condition +from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] -@pytest.fixture(scope="module") -def pipeline_policy(pipeline_policy): - """PipelinePolicy with deny action and response header.""" - pipeline_policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) - pipeline_policy.on_http_response.add_headers([["x-pipeline-policy", "active"]]) - return pipeline_policy +@pytest.fixture(scope="module", autouse=True) +def commit(): + """No module-level policy; each test creates its own.""" -def test_status_accepted(pipeline_policy): - """PipelinePolicy reports Accepted: True after commit.""" - assert pipeline_policy.wait_until( - has_condition("Accepted", "True"), - timelimit=30, - ), f"Policy not Accepted, status: {pipeline_policy.refresh().model.status.conditions}" - - -def test_status_enforced(pipeline_policy): - """PipelinePolicy reports Enforced: True after commit.""" - assert pipeline_policy.wait_until( - has_condition("Enforced", "True"), - timelimit=30, - ), f"Policy not Enforced, status: {pipeline_policy.refresh().model.status.conditions}" - - -def test_update_policy(client, pipeline_policy): +@pytest.mark.flaky(reruns=0) +def test_update_policy(request, cluster, blame, route, client): """Adding a new response header via policy update propagates to traffic.""" + policy = PipelinePolicy.create_instance(cluster, blame("upd-pp"), route) + policy.on_http_response.add_headers([["x-update-test", "active"]]) + request.addfinalizer(lambda: policy.delete(ignore_not_found=True)) + policy.commit() + policy.wait_for_ready() + response = client.get("/get") assert response.status_code == 200 - assert response.headers.get("x-pipeline-policy") == "active" - assert response.headers.get("x-pipeline-updated") is None + assert response.headers.get("x-update-test") == "active" + assert response.headers.get("x-update-new") is None - pipeline_policy.on_http_response.add_headers([["x-pipeline-updated", "true"]]) - pipeline_policy.wait_for_ready() + policy.on_http_response.add_headers([["x-update-new", "true"]]) + policy.wait_for_ready() response = client.get("/get") assert response.status_code == 200 - assert response.headers.get("x-pipeline-policy") == "active" - assert response.headers.get("x-pipeline-updated") == "true" + assert response.headers.get("x-update-test") == "active" + assert response.headers.get("x-update-new") == "true" @pytest.mark.flaky(reruns=0) -def test_delete_policy(client, pipeline_policy): - """After deleting the PipelinePolicy, the CR is removed from the cluster.""" +def test_delete_policy(request, cluster, blame, route, client): + """After deleting the PipelinePolicy, the CR is removed and the actions stop being enforced.""" + policy = PipelinePolicy.create_instance(cluster, blame("del-pp"), route) + policy.on_http_response.add_headers([["x-delete-test", "active"]]) + request.addfinalizer(lambda: policy.delete(ignore_not_found=True)) + policy.commit() + policy.wait_for_ready() + response = client.get("/get") assert response.status_code == 200 - assert response.headers.get("x-pipeline-policy") == "active" + assert response.headers.get("x-delete-test") == "active" + + policy.delete() + assert policy.wait_until(lambda obj: not obj.exists(), timelimit=30), "PipelinePolicy was not deleted" - pipeline_policy.delete() - assert pipeline_policy.wait_until(lambda obj: not obj.exists(), timelimit=30), "PipelinePolicy was not deleted" + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-delete-test") is None diff --git a/testsuite/utils/constants.py b/testsuite/utils/constants.py index 26c6a718d..87af044b0 100644 --- a/testsuite/utils/constants.py +++ b/testsuite/utils/constants.py @@ -165,6 +165,19 @@ # DNS propagation wait. DNS_PROPAGATION_WAIT = 300 # 5 minutes +# --- Threat Assessment Service --- + +# Threat level threshold for deny actions in PipelinePolicy gRPC tests. +# Set intentionally high (max possible score is ~13) so the threat-level-based +# deny never triggers, allowing tests to verify gRPC call execution and +# response variable propagation without interference from the deny action. +THREAT_ASSESSMENT_THRESHOLD = 50 + +# --- Extension Policy --- + +# Wait for EnvoyFilter to propagate before checking isolation (testing absence of leaking). +EXTENSION_POLICY_PROPAGATION_WAIT = 10 + # --- Miscellaneous Workarounds (seconds) --- # Workaround for https://github.com/Kuadrant/testsuite/issues/884 — remove when fixed From 9c589fd05e6cbcfb4dd22bbaf5164991a0306bc3 Mon Sep 17 00:00:00 2001 From: Silvia Tarabova Date: Tue, 2 Jun 2026 10:38:43 +0200 Subject: [PATCH 16/22] feat: add xfail markers for known issues, add deny-with-CEL-body and top-level-fail validation tests Signed-off-by: Silvia Tarabova --- .../interactions/test_pipeline_policy_auth.py | 2 + .../test_pipeline_policy_composition.py | 81 +++++++++++++++++++ .../test_pipeline_policy_deny.py | 24 +++--- .../test_pipeline_policy_isolation.py | 4 + .../test_pipeline_policy_lifecycle.py | 6 +- .../test_pipeline_policy_validation.py | 21 +++++ 6 files changed, 127 insertions(+), 11 deletions(-) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_auth.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_auth.py index 4ace7ebcd..4b8e2028c 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_auth.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_auth.py @@ -36,6 +36,8 @@ def test_auth_and_pipeline_unauthorized(client): assert response.headers.get("x-pipeline-policy") is None +@pytest.mark.issue("https://github.com/Kuadrant/wasm-shim/issues/371") +@pytest.mark.xfail(reason="https://github.com/Kuadrant/wasm-shim/issues/371") def test_auth_and_pipeline_blocked_path(client, auth): """Authenticated request to blocked path is denied by PipelinePolicy deny action.""" response = client.get("/blocked", auth=auth) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py index b16e0ec84..a704945d3 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py @@ -49,6 +49,87 @@ def test_fail_before_deny(request, cluster, blame, route, client, threat_assessm assert response.status_code == 500 +@pytest.mark.issue("https://github.com/Kuadrant/wasm-shim/issues/371") +@pytest.mark.xfail(reason="https://github.com/Kuadrant/wasm-shim/issues/371") +def test_deny_after_grpc_call(request, cluster, blame, route, client, threat_assessment_service): + """Deny action after gRPC call works when the deny predicate matches.""" + svc_url = ( + f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" + ) + policy = PipelinePolicy.create_instance(cluster, blame("grpc-deny"), route) + policy.add_action_method( + name="assess", + url=svc_url, + service="threat.v1.ThreatAssessmentService", + method="AssessRequest", + message_template="threat.v1.ThreatRequest{uri: request.path}", + ) + policy.on_http_request.add_grpc_method(method="assess", var="threat") + policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + request.addfinalizer(policy.delete) + policy.commit() + policy.wait_for_ready() + + response = client.get("/blocked") + assert response.status_code == 403 + + +def test_deny_based_on_grpc_var(request, cluster, blame, route, client, threat_assessment_service): + """Deny action using gRPC response variable denies requests when threat level is high.""" + svc_url = ( + f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" + ) + policy = PipelinePolicy.create_instance(cluster, blame("grpc-var-deny"), route) + policy.add_action_method( + name="assess", + url=svc_url, + service="threat.v1.ThreatAssessmentService", + method="AssessRequest", + message_template="threat.v1.ThreatRequest{uri: request.path}", + ) + policy.on_http_request.add_grpc_method(method="assess", var="threat") + policy.on_http_request.add_deny(predicate="threat.threat_level >= 4", with_status=403) + request.addfinalizer(policy.delete) + policy.commit() + policy.wait_for_ready() + + response = client.get("/admin") + assert response.status_code == 403 + + response = client.get("/get") + assert response.status_code == 200 + + +@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2018") +@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2018") +def test_deny_with_dynamic_body(request, cluster, blame, route, client, threat_assessment_service): + """Deny action with CEL expression in withBody interpolates gRPC response variable.""" + svc_url = ( + f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" + ) + policy = PipelinePolicy.create_instance(cluster, blame("dyn-body"), route) + policy.add_action_method( + name="assess", + url=svc_url, + service="threat.v1.ThreatAssessmentService", + method="AssessRequest", + message_template="threat.v1.ThreatRequest{uri: request.path}", + ) + policy.on_http_request.add_grpc_method(method="assess", var="threat") + policy.on_http_request.add_deny( + predicate="threat.threat_level >= 4", + with_status=403, + with_body="'blocked: threat level ' + string(threat.threat_level)", + ) + request.addfinalizer(policy.delete) + policy.commit() + policy.wait_for_ready() + + response = client.get("/admin") + assert response.status_code == 403 + assert "blocked: threat level" in response.text + + def test_response_action_ordering(request, cluster, blame, route, client): """Response actions execute in spec order; both headers from separate actions are present.""" policy = PipelinePolicy.create_instance(cluster, blame("respord"), route) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py index 9ef4d59b8..cfe55b1b5 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py @@ -20,30 +20,30 @@ def pipeline_policy(pipeline_policy): with_status=403, with_headers='[["x-deny-reason", "blocked"]]', ) - # Custom body + # Custom body (CEL expression) pipeline_policy.on_http_request.add_deny( predicate='request.url_path == "/custom-body"', with_status=403, - with_body="Access denied", + with_body="'Access denied'", ) - # All response fields + # All response fields (CEL expression in body) pipeline_policy.on_http_request.add_deny( predicate='request.url_path == "/custom-all"', with_status=451, with_headers='[["x-deny-reason", "full-custom"]]', - with_body="Fully customized denial", + with_body="'Fully customized denial'", ) # Response phase deny — status override pipeline_policy.on_http_response.add_deny( predicate='"x-override-code" in request.headers', with_status=503, ) - # Response phase deny — all fields + # Response phase deny — all fields (CEL expression in body) pipeline_policy.on_http_response.add_deny( predicate='"x-resp-deny" in request.headers', with_status=418, with_headers='[["x-deny-phase", "response"]]', - with_body="Teapot response", + with_body="'Teapot response'", ) return pipeline_policy @@ -92,15 +92,19 @@ def test_deny_custom_headers(client): assert response.headers.get("x-deny-reason") == "blocked" +@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2018") +@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2018") def test_deny_custom_body(client): - """Deny with withBody returns custom body text.""" + """Deny with withBody as CEL expression returns custom body text.""" response = client.get("/custom-body") assert response.status_code == 403 assert response.text == "Access denied" +@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2018") +@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2018") def test_deny_all_response_fields(client): - """Deny with withStatus, withHeaders, and withBody all set returns all fields.""" + """Deny with withStatus, withHeaders, and withBody as CEL expression returns all fields.""" response = client.get("/custom-all") assert response.status_code == 451 assert response.headers.get("x-deny-reason") == "full-custom" @@ -119,8 +123,10 @@ def test_response_deny_no_override(client): assert response.status_code == 200 +@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2018") +@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2018") def test_response_deny_with_headers_and_body(client): - """Response deny with all fields replaces the backend response.""" + """Response deny with all fields as CEL expressions replaces the backend response.""" response = client.get("/get", headers={"x-resp-deny": "true"}) assert response.status_code == 418 assert response.headers.get("x-deny-phase") == "response" diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py index 3527a663d..e6c88f11e 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py @@ -87,6 +87,8 @@ def test_policy_affects_targeted_route(client): assert response.headers.get("x-pipeline-policy") == "active" +@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2023") +@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2023") def test_policy_does_not_affect_other_route(client2): """Route without PipelinePolicy on the same gateway does not get the response header.""" time.sleep(EXTENSION_POLICY_PROPAGATION_WAIT) @@ -95,6 +97,8 @@ def test_policy_does_not_affect_other_route(client2): assert response.headers.get("x-pipeline-policy") is None +@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2023") +@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2023") def test_policy_does_not_affect_other_gateway(client3): """Route on a different gateway does not get the response header.""" time.sleep(EXTENSION_POLICY_PROPAGATION_WAIT) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py index efccfb3f6..e9d9947f6 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py @@ -17,7 +17,7 @@ def test_update_policy(request, cluster, blame, route, client): """Adding a new response header via policy update propagates to traffic.""" policy = PipelinePolicy.create_instance(cluster, blame("upd-pp"), route) policy.on_http_response.add_headers([["x-update-test", "active"]]) - request.addfinalizer(lambda: policy.delete(ignore_not_found=True)) + request.addfinalizer(policy.delete) policy.commit() policy.wait_for_ready() @@ -35,12 +35,14 @@ def test_update_policy(request, cluster, blame, route, client): assert response.headers.get("x-update-new") == "true" +@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2009") +@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2009") @pytest.mark.flaky(reruns=0) def test_delete_policy(request, cluster, blame, route, client): """After deleting the PipelinePolicy, the CR is removed and the actions stop being enforced.""" policy = PipelinePolicy.create_instance(cluster, blame("del-pp"), route) policy.on_http_response.add_headers([["x-delete-test", "active"]]) - request.addfinalizer(lambda: policy.delete(ignore_not_found=True)) + request.addfinalizer(policy.delete) policy.commit() policy.wait_for_ready() diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py index aca1f11a7..d580318d9 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py @@ -14,6 +14,8 @@ def commit(): """No module-level policy; each test creates its own with bad configuration.""" +@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2022") +@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2022") def test_invalid_target_ref(request, cluster, blame): """PipelinePolicy targeting a non-existent HTTPRoute does not reach Enforced state.""" target = CustomReference( @@ -33,6 +35,8 @@ def test_invalid_target_ref(request, cluster, blame): ), f"Policy did not report TargetNotFound, status: {policy.refresh().model.status.conditions}" +@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2022") +@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2022") def test_invalid_gateway_target_ref(request, cluster, blame): """PipelinePolicy targeting a non-existent Gateway does not reach Enforced state.""" target = CustomReference( @@ -52,6 +56,23 @@ def test_invalid_gateway_target_ref(request, cluster, blame): ), f"Policy did not report TargetNotFound, status: {policy.refresh().model.status.conditions}" +@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2015") +@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2015") +def test_top_level_fail_action(request, cluster, blame, route): + """PipelinePolicy with a top-level fail action (not inside gRPC onReply) should not be accepted.""" + policy = PipelinePolicy.create_instance(cluster, blame("top-fail"), route) + policy.on_http_request.add_fail("top-level fail", predicate='request.url_path == "/fail"') + + request.addfinalizer(policy.delete) + policy.commit() + + # TODO: add expected message assertion once the validation is implemented + assert policy.wait_until( + has_condition("Accepted", "False"), + timelimit=30, + ), f"Policy with top-level fail was accepted, status: {policy.refresh().model.status.conditions}" + + def test_invalid_cel_expression(request, cluster, blame, route): """PipelinePolicy with malformed CEL predicate fails to enforce.""" policy = PipelinePolicy.create_instance(cluster, blame("bad-cel"), route) From 6981df1e90dc2978bc20e6236152aa02c2c17af7 Mon Sep 17 00:00:00 2001 From: Silvia Tarabova Date: Tue, 2 Jun 2026 11:45:45 +0200 Subject: [PATCH 17/22] fix: remove redundant status tests Signed-off-by: Silvia Tarabova --- .../test_pipeline_policy_basic.py | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py index 017391768..a1bc22b70 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_basic.py @@ -1,9 +1,7 @@ -"""Basic happy-path tests for PipelinePolicy: status conditions, deny action and response headers.""" +"""Basic happy-path tests for PipelinePolicy: deny action and response headers.""" import pytest -from testsuite.kuadrant.policy import has_condition - pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] @@ -15,22 +13,6 @@ def pipeline_policy(pipeline_policy): return pipeline_policy -def test_status_accepted(pipeline_policy): - """PipelinePolicy reports Accepted: True after commit.""" - assert pipeline_policy.wait_until( - has_condition("Accepted", "True"), - timelimit=30, - ), f"Policy not Accepted, status: {pipeline_policy.refresh().model.status.conditions}" - - -def test_status_enforced(pipeline_policy): - """PipelinePolicy reports Enforced: True after commit.""" - assert pipeline_policy.wait_until( - has_condition("Enforced", "True"), - timelimit=30, - ), f"Policy not Enforced, status: {pipeline_policy.refresh().model.status.conditions}" - - def test_allowed_path(client): """Request to an allowed path returns 200 with the custom response header.""" response = client.get("/get") From e876c5a468e21bbd398e3097c1b1f090bf38c30f Mon Sep 17 00:00:00 2001 From: Silvia Tarabova Date: Thu, 4 Jun 2026 09:20:21 +0200 Subject: [PATCH 18/22] refactor: remove xfails and incorporate review comments Signed-off-by: Silvia Tarabova --- .../extensions/pipeline_policy/conftest.py | 11 ++--- .../interactions/test_pipeline_policy_auth.py | 2 - .../test_pipeline_policy_composition.py | 4 -- .../test_pipeline_policy_deny.py | 6 --- .../test_pipeline_policy_isolation.py | 21 +++++----- .../test_pipeline_policy_lifecycle.py | 8 ++-- .../test_pipeline_policy_validation.py | 42 ++++--------------- 7 files changed, 30 insertions(+), 64 deletions(-) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py index 997b14635..ec7bfd4e2 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py @@ -2,6 +2,8 @@ import pytest +from openshift_client import OpenShiftPythonException + from testsuite.kubernetes import Selector from testsuite.kubernetes.deployment import Deployment from testsuite.kubernetes.service import Service, ServicePort @@ -13,7 +15,7 @@ def check_pipeline_policy_crd(cluster, skip_or_fail): """Skip all PipelinePolicy tests if the CRD is not installed on the cluster.""" try: cluster.do_action("get", "crd/pipelinepolicies.extensions.kuadrant.io") - except Exception: # pylint: disable=broad-except + except OpenShiftPythonException: skip_or_fail("PipelinePolicy CRD is not installed on the cluster") @@ -57,7 +59,6 @@ def threat_assessment_service(request, cluster, blame, module_label, testconfig) @pytest.fixture(scope="module", autouse=True) def commit(request, pipeline_policy): """Commit and wait for PipelinePolicy to be ready.""" - for component in [pipeline_policy]: - request.addfinalizer(component.delete) - component.commit() - component.wait_for_ready() + request.addfinalizer(pipeline_policy.delete) + pipeline_policy.commit() + pipeline_policy.wait_for_ready() diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_auth.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_auth.py index 4b8e2028c..4ace7ebcd 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_auth.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/interactions/test_pipeline_policy_auth.py @@ -36,8 +36,6 @@ def test_auth_and_pipeline_unauthorized(client): assert response.headers.get("x-pipeline-policy") is None -@pytest.mark.issue("https://github.com/Kuadrant/wasm-shim/issues/371") -@pytest.mark.xfail(reason="https://github.com/Kuadrant/wasm-shim/issues/371") def test_auth_and_pipeline_blocked_path(client, auth): """Authenticated request to blocked path is denied by PipelinePolicy deny action.""" response = client.get("/blocked", auth=auth) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py index a704945d3..1a8b2869a 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py @@ -49,8 +49,6 @@ def test_fail_before_deny(request, cluster, blame, route, client, threat_assessm assert response.status_code == 500 -@pytest.mark.issue("https://github.com/Kuadrant/wasm-shim/issues/371") -@pytest.mark.xfail(reason="https://github.com/Kuadrant/wasm-shim/issues/371") def test_deny_after_grpc_call(request, cluster, blame, route, client, threat_assessment_service): """Deny action after gRPC call works when the deny predicate matches.""" svc_url = ( @@ -100,8 +98,6 @@ def test_deny_based_on_grpc_var(request, cluster, blame, route, client, threat_a assert response.status_code == 200 -@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2018") -@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2018") def test_deny_with_dynamic_body(request, cluster, blame, route, client, threat_assessment_service): """Deny action with CEL expression in withBody interpolates gRPC response variable.""" svc_url = ( diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py index cfe55b1b5..1094c06fa 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_deny.py @@ -92,8 +92,6 @@ def test_deny_custom_headers(client): assert response.headers.get("x-deny-reason") == "blocked" -@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2018") -@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2018") def test_deny_custom_body(client): """Deny with withBody as CEL expression returns custom body text.""" response = client.get("/custom-body") @@ -101,8 +99,6 @@ def test_deny_custom_body(client): assert response.text == "Access denied" -@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2018") -@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2018") def test_deny_all_response_fields(client): """Deny with withStatus, withHeaders, and withBody as CEL expression returns all fields.""" response = client.get("/custom-all") @@ -123,8 +119,6 @@ def test_response_deny_no_override(client): assert response.status_code == 200 -@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2018") -@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2018") def test_response_deny_with_headers_and_body(client): """Response deny with all fields as CEL expressions replaces the backend response.""" response = client.get("/get", headers={"x-resp-deny": "true"}) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py index e6c88f11e..69e3ef4b0 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py @@ -80,28 +80,27 @@ def pipeline_policy(pipeline_policy): return pipeline_policy -def test_policy_affects_targeted_route(client): - """Route with PipelinePolicy gets the response header.""" +def test_policy_does_not_affect_other_route(client, client2): + """Route without PipelinePolicy on the same gateway does not get the response header.""" + time.sleep(EXTENSION_POLICY_PROPAGATION_WAIT) + response = client.get("/get") assert response.status_code == 200 assert response.headers.get("x-pipeline-policy") == "active" - -@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2023") -@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2023") -def test_policy_does_not_affect_other_route(client2): - """Route without PipelinePolicy on the same gateway does not get the response header.""" - time.sleep(EXTENSION_POLICY_PROPAGATION_WAIT) response = client2.get("/get") assert response.status_code == 200 assert response.headers.get("x-pipeline-policy") is None -@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2023") -@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2023") -def test_policy_does_not_affect_other_gateway(client3): +def test_policy_does_not_affect_other_gateway(client, client3): """Route on a different gateway does not get the response header.""" time.sleep(EXTENSION_POLICY_PROPAGATION_WAIT) + + response = client.get("/get") + assert response.status_code == 200 + assert response.headers.get("x-pipeline-policy") == "active" + response = client3.get("/get") assert response.status_code == 200 assert response.headers.get("x-pipeline-policy") is None diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py index e9d9947f6..3558b89f7 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py @@ -1,8 +1,11 @@ """Tests for PipelinePolicy lifecycle: update and delete.""" +import time + import pytest from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy +from testsuite.utils.constants import EXTENSION_POLICY_PROPAGATION_WAIT pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] @@ -35,8 +38,6 @@ def test_update_policy(request, cluster, blame, route, client): assert response.headers.get("x-update-new") == "true" -@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2009") -@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2009") @pytest.mark.flaky(reruns=0) def test_delete_policy(request, cluster, blame, route, client): """After deleting the PipelinePolicy, the CR is removed and the actions stop being enforced.""" @@ -51,7 +52,8 @@ def test_delete_policy(request, cluster, blame, route, client): assert response.headers.get("x-delete-test") == "active" policy.delete() - assert policy.wait_until(lambda obj: not obj.exists(), timelimit=30), "PipelinePolicy was not deleted" + assert not policy.committed, "PipelinePolicy was not deleted" + time.sleep(EXTENSION_POLICY_PROPAGATION_WAIT) response = client.get("/get") assert response.status_code == 200 diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py index d580318d9..900fc632a 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py @@ -14,14 +14,14 @@ def commit(): """No module-level policy; each test creates its own with bad configuration.""" -@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2022") -@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2022") -def test_invalid_target_ref(request, cluster, blame): - """PipelinePolicy targeting a non-existent HTTPRoute does not reach Enforced state.""" +@pytest.mark.parametrize("kind", ["HTTPRoute", "Gateway"]) +def test_invalid_target_ref(request, cluster, blame, kind): + """PipelinePolicy targeting a non-existent resource does not reach Accepted state.""" + target_name = "does-not-exist" target = CustomReference( group="gateway.networking.k8s.io", - kind="HTTPRoute", - name="does-not-exist", + kind=kind, + name=target_name, ) policy = PipelinePolicy.create_instance(cluster, blame("bad-target"), target) policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) @@ -30,34 +30,11 @@ def test_invalid_target_ref(request, cluster, blame): policy.commit() assert policy.wait_until( - has_condition("Accepted", "False", "TargetNotFound"), + has_condition("Accepted", "False", message=f"targetRef {kind} {cluster.project}/{target_name} not found"), timelimit=30, - ), f"Policy did not report TargetNotFound, status: {policy.refresh().model.status.conditions}" + ), f"Policy did not report target not found, status: {policy.refresh().model.status.conditions}" -@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2022") -@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2022") -def test_invalid_gateway_target_ref(request, cluster, blame): - """PipelinePolicy targeting a non-existent Gateway does not reach Enforced state.""" - target = CustomReference( - group="gateway.networking.k8s.io", - kind="Gateway", - name="does-not-exist", - ) - policy = PipelinePolicy.create_instance(cluster, blame("bad-gw"), target) - policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) - - request.addfinalizer(policy.delete) - policy.commit() - - assert policy.wait_until( - has_condition("Accepted", "False", "TargetNotFound"), - timelimit=30, - ), f"Policy did not report TargetNotFound, status: {policy.refresh().model.status.conditions}" - - -@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/2015") -@pytest.mark.xfail(reason="https://github.com/Kuadrant/kuadrant-operator/issues/2015") def test_top_level_fail_action(request, cluster, blame, route): """PipelinePolicy with a top-level fail action (not inside gRPC onReply) should not be accepted.""" policy = PipelinePolicy.create_instance(cluster, blame("top-fail"), route) @@ -66,9 +43,8 @@ def test_top_level_fail_action(request, cluster, blame, route): request.addfinalizer(policy.delete) policy.commit() - # TODO: add expected message assertion once the validation is implemented assert policy.wait_until( - has_condition("Accepted", "False"), + has_condition("Accepted", "False", message="fail action must reference a gRPC response variable"), timelimit=30, ), f"Policy with top-level fail was accepted, status: {policy.refresh().model.status.conditions}" From 581dd537ec125707babe8c2c428f7c822c9b5621 Mon Sep 17 00:00:00 2001 From: Silvia Tarabova Date: Tue, 14 Jul 2026 12:53:41 +0200 Subject: [PATCH 19/22] fix: address review feedback Signed-off-by: Silvia Tarabova --- .../extensions/pipeline_policy/conftest.py | 34 ------ .../pipeline_policy/grpc/__init__.py | 0 .../pipeline_policy/grpc/conftest.py | 59 ++++++++++ .../test_grpc_basic.py} | 17 +-- .../grpc/test_grpc_composition.py | 88 +++++++++++++++ .../test_grpc_errors.py} | 14 +-- .../test_grpc_fail.py} | 12 +-- .../test_pipeline_policy_composition.py | 101 ------------------ 8 files changed, 155 insertions(+), 170 deletions(-) create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/__init__.py create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/conftest.py rename testsuite/tests/singlecluster/extensions/pipeline_policy/{test_pipeline_policy_grpc.py => grpc/test_grpc_basic.py} (74%) create mode 100644 testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_composition.py rename testsuite/tests/singlecluster/extensions/pipeline_policy/{test_pipeline_policy_grpc_errors.py => grpc/test_grpc_errors.py} (89%) rename testsuite/tests/singlecluster/extensions/pipeline_policy/{test_pipeline_policy_fail.py => grpc/test_grpc_fail.py} (72%) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py index ec7bfd4e2..b03cb3619 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py @@ -4,9 +4,6 @@ from openshift_client import OpenShiftPythonException -from testsuite.kubernetes import Selector -from testsuite.kubernetes.deployment import Deployment -from testsuite.kubernetes.service import Service, ServicePort from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy @@ -25,37 +22,6 @@ def pipeline_policy(cluster, blame, route): return PipelinePolicy.create_instance(cluster, blame("pipeline"), route) -@pytest.fixture(scope="module") -def threat_assessment_service(request, cluster, blame, module_label, testconfig): - """Deploys the ThreatAssessmentService gRPC backend""" - name = blame("threat") - match_labels = {"app": module_label, "deployment": name} - - deployment = Deployment.create_instance( - cluster, - name, - container_name="threat-assessment", - image=testconfig["pipeline_policy_extension_service"]["image"], - ports={"grpc": 8080}, - selector=Selector(matchLabels=match_labels), - labels={"app": module_label}, - ) - request.addfinalizer(deployment.delete) - deployment.commit() - deployment.wait_for_ready() - - service = Service.create_instance( - cluster, - name, - selector=match_labels, - ports=[ServicePort(name="grpc", port=8080, targetPort="grpc")], - labels={"app": module_label}, - ) - request.addfinalizer(service.delete) - service.commit() - return service - - @pytest.fixture(scope="module", autouse=True) def commit(request, pipeline_policy): """Commit and wait for PipelinePolicy to be ready.""" diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/__init__.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/conftest.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/conftest.py new file mode 100644 index 000000000..8f7b46aa0 --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/conftest.py @@ -0,0 +1,59 @@ +"""Shared fixtures for PipelinePolicy gRPC tests.""" + +import pytest + +from testsuite.kubernetes import Selector +from testsuite.kubernetes.deployment import Deployment +from testsuite.kubernetes.service import Service, ServicePort + + +@pytest.fixture(scope="module") +def threat_assessment_service(request, cluster, blame, module_label, testconfig): + """Deploys the ThreatAssessmentService gRPC backend""" + testconfig.validators.validate(only="pipeline_policy_extension_service") + name = blame("threat") + match_labels = {"app": module_label, "deployment": name} + + deployment = Deployment.create_instance( + cluster, + name, + container_name="threat-assessment", + image=testconfig["pipeline_policy_extension_service"]["image"], + ports={"grpc": 8080}, + selector=Selector(matchLabels=match_labels), + labels={"app": module_label}, + ) + request.addfinalizer(deployment.delete) + deployment.commit() + deployment.wait_for_ready() + + service = Service.create_instance( + cluster, + name, + selector=match_labels, + ports=[ServicePort(name="grpc", port=8080, targetPort="grpc")], + labels={"app": module_label}, + ) + request.addfinalizer(service.delete) + service.commit() + return service + + +@pytest.fixture(scope="module") +def threat_service_url(threat_assessment_service): + """gRPC URL for the threat assessment service.""" + svc = threat_assessment_service + return f"grpc://{svc.name()}.{svc.namespace()}.svc.cluster.local:8080" + + +@pytest.fixture(scope="module") +def pipeline_policy(pipeline_policy, threat_service_url): + """PipelinePolicy with the threat assessment gRPC action method pre-registered.""" + pipeline_policy.add_action_method( + name="assess", + url=threat_service_url, + service="threat.v1.ThreatAssessmentService", + method="AssessRequest", + message_template="threat.v1.ThreatRequest{uri: request.path}", + ) + return pipeline_policy diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_basic.py similarity index 74% rename from testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc.py rename to testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_basic.py index fc060f3a5..32c2e81f2 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_basic.py @@ -8,21 +8,10 @@ @pytest.fixture(scope="module") -def pipeline_policy(pipeline_policy, threat_assessment_service): # pylint: disable=unused-argument - """PipelinePolicy with threat assessment gRPC action and conditional headers.""" - svc_url = ( - f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" - ) - pipeline_policy.add_action_method( - name="assess-threat", - url=svc_url, - service="threat.v1.ThreatAssessmentService", - method="AssessRequest", - message_template="threat.v1.ThreatRequest{uri: request.path, source_ip: source.address}", - ) - +def pipeline_policy(pipeline_policy): + """PipelinePolicy with conditional gRPC execution and threat-level deny.""" pipeline_policy.on_http_request.add_grpc_method( - method="assess-threat", + method="assess", var="threatResponse", predicate='"x-assess-threat" in request.headers', ) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_composition.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_composition.py new file mode 100644 index 000000000..28fee6efd --- /dev/null +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_composition.py @@ -0,0 +1,88 @@ +"""Tests for PipelinePolicy composition of gRPC actions with deny and fail.""" + +import pytest + +from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] + + +@pytest.fixture(scope="module", autouse=True) +def commit(): + """No module-level policy; each test creates its own.""" + + +@pytest.fixture(scope="module") +def create_grpc_policy(cluster, blame, route, threat_service_url): + """Factory for creating a PipelinePolicy with the assess gRPC action pre-registered.""" + + def _create(name): + policy = PipelinePolicy.create_instance(cluster, blame(name), route) + policy.add_action_method( + name="assess", + url=threat_service_url, + service="threat.v1.ThreatAssessmentService", + method="AssessRequest", + message_template="threat.v1.ThreatRequest{uri: request.path}", + ) + policy.on_http_request.add_grpc_method(method="assess", var="threat") + return policy + + return _create + + +def test_fail_before_deny(request, create_grpc_policy, client): + """Fail action terminates the chain before the deny action when gRPC response triggers the fail predicate.""" + policy = create_grpc_policy("failord") + policy.on_http_request.add_fail("threat too high", predicate="threat.threat_level >= 4") + policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + request.addfinalizer(policy.delete) + policy.commit() + policy.wait_for_ready() + + response = client.get("/admin") + assert response.status_code == 500 + + +def test_deny_after_grpc_call(request, create_grpc_policy, client): + """Deny action after gRPC call works when the deny predicate matches.""" + policy = create_grpc_policy("grpc-deny") + policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) + request.addfinalizer(policy.delete) + policy.commit() + policy.wait_for_ready() + + response = client.get("/blocked") + assert response.status_code == 403 + + +def test_deny_based_on_grpc_var(request, create_grpc_policy, client): + """Deny action using gRPC response variable denies requests when threat level is high.""" + policy = create_grpc_policy("grpc-var-deny") + policy.on_http_request.add_deny(predicate="threat.threat_level >= 4", with_status=403) + request.addfinalizer(policy.delete) + policy.commit() + policy.wait_for_ready() + + response = client.get("/admin") + assert response.status_code == 403 + + response = client.get("/get") + assert response.status_code == 200 + + +def test_deny_with_dynamic_body(request, create_grpc_policy, client): + """Deny action with CEL expression in withBody interpolates gRPC response variable.""" + policy = create_grpc_policy("dyn-body") + policy.on_http_request.add_deny( + predicate="threat.threat_level >= 4", + with_status=403, + with_body="'blocked: threat level ' + string(threat.threat_level)", + ) + request.addfinalizer(policy.delete) + policy.commit() + policy.wait_for_ready() + + response = client.get("/admin") + assert response.status_code == 403 + assert "blocked: threat level" in response.text diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_errors.py similarity index 89% rename from testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py rename to testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_errors.py index a30b32d2f..c4b8dfe43 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_grpc_errors.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_errors.py @@ -35,15 +35,12 @@ def test_grpc_upstream_unavailable(request, cluster, blame, route): ), f"Policy did not reach expected error status, instead: {policy.refresh().model.status.conditions}" -def test_grpc_wrong_service_name(request, cluster, blame, route, threat_assessment_service): +def test_grpc_wrong_service_name(request, cluster, blame, route, threat_service_url): """PipelinePolicy reports error when action method references a non-existent gRPC service name.""" - svc_url = ( - f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" - ) policy = PipelinePolicy.create_instance(cluster, blame("bad-svc"), route) policy.add_action_method( name="bad-service", - url=svc_url, + url=threat_service_url, service="nonexistent.v1.FakeService", method="DoSomething", message_template="nonexistent.v1.FakeRequest{uri: request.path}", @@ -60,15 +57,12 @@ def test_grpc_wrong_service_name(request, cluster, blame, route, threat_assessme ), f"Policy did not reach expected error status, instead: {policy.refresh().model.status.conditions}" -def test_grpc_wrong_method_name(request, cluster, blame, route, threat_assessment_service): +def test_grpc_wrong_method_name(request, cluster, blame, route, threat_service_url): """PipelinePolicy reports error when action method references a non-existent gRPC method.""" - svc_url = ( - f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" - ) policy = PipelinePolicy.create_instance(cluster, blame("bad-meth"), route) policy.add_action_method( name="wrong-method", - url=svc_url, + url=threat_service_url, service="threat.v1.ThreatAssessmentService", method="NonExistentMethod", message_template="threat.v1.ThreatRequest{uri: request.path}", diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_fail.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_fail.py similarity index 72% rename from testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_fail.py rename to testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_fail.py index 25b55abc9..1975a539e 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_fail.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_fail.py @@ -6,18 +6,8 @@ @pytest.fixture(scope="module") -def pipeline_policy(pipeline_policy, threat_assessment_service): +def pipeline_policy(pipeline_policy): """PipelinePolicy with gRPC call and fail action based on threat level.""" - svc_url = ( - f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" - ) - pipeline_policy.add_action_method( - name="assess", - url=svc_url, - service="threat.v1.ThreatAssessmentService", - method="AssessRequest", - message_template="threat.v1.ThreatRequest{uri: request.path}", - ) pipeline_policy.on_http_request.add_grpc_method(method="assess", var="threat") pipeline_policy.on_http_request.add_fail( "threat level too high", diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py index 1a8b2869a..47792954d 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py @@ -25,107 +25,6 @@ def test_first_deny_wins(request, cluster, blame, route, client): assert response.status_code == 403 -def test_fail_before_deny(request, cluster, blame, route, client, threat_assessment_service): - """Fail action terminates the chain before the deny action when gRPC response triggers the fail predicate.""" - svc_url = ( - f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" - ) - policy = PipelinePolicy.create_instance(cluster, blame("failord"), route) - policy.add_action_method( - name="assess", - url=svc_url, - service="threat.v1.ThreatAssessmentService", - method="AssessRequest", - message_template="threat.v1.ThreatRequest{uri: request.path}", - ) - policy.on_http_request.add_grpc_method(method="assess", var="threat") - policy.on_http_request.add_fail("threat too high", predicate="threat.threat_level >= 4") - policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) - request.addfinalizer(policy.delete) - policy.commit() - policy.wait_for_ready() - - response = client.get("/admin") - assert response.status_code == 500 - - -def test_deny_after_grpc_call(request, cluster, blame, route, client, threat_assessment_service): - """Deny action after gRPC call works when the deny predicate matches.""" - svc_url = ( - f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" - ) - policy = PipelinePolicy.create_instance(cluster, blame("grpc-deny"), route) - policy.add_action_method( - name="assess", - url=svc_url, - service="threat.v1.ThreatAssessmentService", - method="AssessRequest", - message_template="threat.v1.ThreatRequest{uri: request.path}", - ) - policy.on_http_request.add_grpc_method(method="assess", var="threat") - policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) - request.addfinalizer(policy.delete) - policy.commit() - policy.wait_for_ready() - - response = client.get("/blocked") - assert response.status_code == 403 - - -def test_deny_based_on_grpc_var(request, cluster, blame, route, client, threat_assessment_service): - """Deny action using gRPC response variable denies requests when threat level is high.""" - svc_url = ( - f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" - ) - policy = PipelinePolicy.create_instance(cluster, blame("grpc-var-deny"), route) - policy.add_action_method( - name="assess", - url=svc_url, - service="threat.v1.ThreatAssessmentService", - method="AssessRequest", - message_template="threat.v1.ThreatRequest{uri: request.path}", - ) - policy.on_http_request.add_grpc_method(method="assess", var="threat") - policy.on_http_request.add_deny(predicate="threat.threat_level >= 4", with_status=403) - request.addfinalizer(policy.delete) - policy.commit() - policy.wait_for_ready() - - response = client.get("/admin") - assert response.status_code == 403 - - response = client.get("/get") - assert response.status_code == 200 - - -def test_deny_with_dynamic_body(request, cluster, blame, route, client, threat_assessment_service): - """Deny action with CEL expression in withBody interpolates gRPC response variable.""" - svc_url = ( - f"grpc://{threat_assessment_service.name()}.{threat_assessment_service.namespace()}.svc.cluster.local:8080" - ) - policy = PipelinePolicy.create_instance(cluster, blame("dyn-body"), route) - policy.add_action_method( - name="assess", - url=svc_url, - service="threat.v1.ThreatAssessmentService", - method="AssessRequest", - message_template="threat.v1.ThreatRequest{uri: request.path}", - ) - policy.on_http_request.add_grpc_method(method="assess", var="threat") - policy.on_http_request.add_deny( - predicate="threat.threat_level >= 4", - with_status=403, - with_body="'blocked: threat level ' + string(threat.threat_level)", - ) - request.addfinalizer(policy.delete) - policy.commit() - policy.wait_for_ready() - - response = client.get("/admin") - assert response.status_code == 403 - assert "blocked: threat level" in response.text - - def test_response_action_ordering(request, cluster, blame, route, client): """Response actions execute in spec order; both headers from separate actions are present.""" policy = PipelinePolicy.create_instance(cluster, blame("respord"), route) From 87c77224935c3676a7343b52e1e19f3228f12a79 Mon Sep 17 00:00:00 2001 From: Silvia Tarabova Date: Wed, 15 Jul 2026 15:17:07 +0200 Subject: [PATCH 20/22] fix: add module_label to PipelinePolicy Signed-off-by: Silvia Tarabova --- .../extensions/pipeline_policy/conftest.py | 4 ++-- .../pipeline_policy/grpc/conftest.py | 7 ++++--- .../grpc/test_grpc_composition.py | 4 ++-- .../pipeline_policy/grpc/test_grpc_errors.py | 15 +++++++------- .../test_pipeline_policy_composition.py | 20 +++++++++---------- .../test_pipeline_policy_isolation.py | 6 ++++++ .../test_pipeline_policy_lifecycle.py | 8 ++++---- .../test_pipeline_policy_targeting.py | 4 ++-- .../test_pipeline_policy_validation.py | 20 +++++++++---------- 9 files changed, 48 insertions(+), 40 deletions(-) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py index b03cb3619..3db89a902 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/conftest.py @@ -17,9 +17,9 @@ def check_pipeline_policy_crd(cluster, skip_or_fail): @pytest.fixture(scope="module") -def pipeline_policy(cluster, blame, route): +def pipeline_policy(cluster, blame, route, module_label): """PipelinePolicy targeting the test HTTPRoute""" - return PipelinePolicy.create_instance(cluster, blame("pipeline"), route) + return PipelinePolicy.create_instance(cluster, blame("pipeline"), route, labels={"testRun": module_label}) @pytest.fixture(scope="module", autouse=True) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/conftest.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/conftest.py index 8f7b46aa0..4e68991c8 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/conftest.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/conftest.py @@ -5,6 +5,7 @@ from testsuite.kubernetes import Selector from testsuite.kubernetes.deployment import Deployment from testsuite.kubernetes.service import Service, ServicePort +from testsuite.utils.constants import HTTP_API_PORT @pytest.fixture(scope="module") @@ -19,7 +20,7 @@ def threat_assessment_service(request, cluster, blame, module_label, testconfig) name, container_name="threat-assessment", image=testconfig["pipeline_policy_extension_service"]["image"], - ports={"grpc": 8080}, + ports={"grpc": HTTP_API_PORT}, selector=Selector(matchLabels=match_labels), labels={"app": module_label}, ) @@ -31,7 +32,7 @@ def threat_assessment_service(request, cluster, blame, module_label, testconfig) cluster, name, selector=match_labels, - ports=[ServicePort(name="grpc", port=8080, targetPort="grpc")], + ports=[ServicePort(name="grpc", port=HTTP_API_PORT, targetPort="grpc")], labels={"app": module_label}, ) request.addfinalizer(service.delete) @@ -43,7 +44,7 @@ def threat_assessment_service(request, cluster, blame, module_label, testconfig) def threat_service_url(threat_assessment_service): """gRPC URL for the threat assessment service.""" svc = threat_assessment_service - return f"grpc://{svc.name()}.{svc.namespace()}.svc.cluster.local:8080" + return f"grpc://{svc.name()}.{svc.namespace()}.svc.cluster.local:{HTTP_API_PORT}" @pytest.fixture(scope="module") diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_composition.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_composition.py index 28fee6efd..854d5a492 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_composition.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_composition.py @@ -13,11 +13,11 @@ def commit(): @pytest.fixture(scope="module") -def create_grpc_policy(cluster, blame, route, threat_service_url): +def create_grpc_policy(cluster, blame, route, threat_service_url, module_label): """Factory for creating a PipelinePolicy with the assess gRPC action pre-registered.""" def _create(name): - policy = PipelinePolicy.create_instance(cluster, blame(name), route) + policy = PipelinePolicy.create_instance(cluster, blame(name), route, labels={"testRun": module_label}) policy.add_action_method( name="assess", url=threat_service_url, diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_errors.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_errors.py index c4b8dfe43..7471b63e3 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_errors.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/test_grpc_errors.py @@ -4,6 +4,7 @@ from testsuite.kuadrant.extensions.pipeline_policy import PipelinePolicy from testsuite.kuadrant.policy import has_condition +from testsuite.utils.constants import HTTP_API_PORT pytestmark = [pytest.mark.kuadrant_only, pytest.mark.extensions] @@ -13,12 +14,12 @@ def commit(): """No module-level policy; each test creates its own PipelinePolicy.""" -def test_grpc_upstream_unavailable(request, cluster, blame, route): +def test_grpc_upstream_unavailable(request, cluster, blame, route, module_label): """PipelinePolicy reports error when action method URL points to a non-existent service.""" - policy = PipelinePolicy.create_instance(cluster, blame("bad-url"), route) + policy = PipelinePolicy.create_instance(cluster, blame("bad-url"), route, labels={"testRun": module_label}) policy.add_action_method( name="bad-method", - url="grpc://does-not-exist.default.svc.cluster.local:8080", + url=f"grpc://does-not-exist.default.svc.cluster.local:{HTTP_API_PORT}", service="threat.v1.ThreatAssessmentService", method="AssessRequest", message_template="threat.v1.ThreatRequest{uri: request.path}", @@ -35,9 +36,9 @@ def test_grpc_upstream_unavailable(request, cluster, blame, route): ), f"Policy did not reach expected error status, instead: {policy.refresh().model.status.conditions}" -def test_grpc_wrong_service_name(request, cluster, blame, route, threat_service_url): +def test_grpc_wrong_service_name(request, cluster, blame, route, threat_service_url, module_label): """PipelinePolicy reports error when action method references a non-existent gRPC service name.""" - policy = PipelinePolicy.create_instance(cluster, blame("bad-svc"), route) + policy = PipelinePolicy.create_instance(cluster, blame("bad-svc"), route, labels={"testRun": module_label}) policy.add_action_method( name="bad-service", url=threat_service_url, @@ -57,9 +58,9 @@ def test_grpc_wrong_service_name(request, cluster, blame, route, threat_service_ ), f"Policy did not reach expected error status, instead: {policy.refresh().model.status.conditions}" -def test_grpc_wrong_method_name(request, cluster, blame, route, threat_service_url): +def test_grpc_wrong_method_name(request, cluster, blame, route, threat_service_url, module_label): """PipelinePolicy reports error when action method references a non-existent gRPC method.""" - policy = PipelinePolicy.create_instance(cluster, blame("bad-meth"), route) + policy = PipelinePolicy.create_instance(cluster, blame("bad-meth"), route, labels={"testRun": module_label}) policy.add_action_method( name="wrong-method", url=threat_service_url, diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py index 47792954d..bc927fd54 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_composition.py @@ -12,9 +12,9 @@ def commit(): """No module-level policy; each test creates its own.""" -def test_first_deny_wins(request, cluster, blame, route, client): +def test_first_deny_wins(request, cluster, blame, route, client, module_label): """First deny action terminates the chain; the second deny with the same predicate never executes.""" - policy = PipelinePolicy.create_instance(cluster, blame("order"), route) + policy = PipelinePolicy.create_instance(cluster, blame("order"), route, labels={"testRun": module_label}) policy.on_http_request.add_deny(predicate='request.url_path == "/order-test"', with_status=403) policy.on_http_request.add_deny(predicate='request.url_path == "/order-test"', with_status=429) request.addfinalizer(policy.delete) @@ -25,9 +25,9 @@ def test_first_deny_wins(request, cluster, blame, route, client): assert response.status_code == 403 -def test_response_action_ordering(request, cluster, blame, route, client): +def test_response_action_ordering(request, cluster, blame, route, client, module_label): """Response actions execute in spec order; both headers from separate actions are present.""" - policy = PipelinePolicy.create_instance(cluster, blame("respord"), route) + policy = PipelinePolicy.create_instance(cluster, blame("respord"), route, labels={"testRun": module_label}) policy.on_http_response.add_headers([["x-first", "alpha"]]) policy.on_http_response.add_headers([["x-second", "bravo"]]) request.addfinalizer(policy.delete) @@ -40,9 +40,9 @@ def test_response_action_ordering(request, cluster, blame, route, client): assert response.headers.get("x-second") == "bravo" -def test_empty_pipeline(request, cluster, blame, route, client): +def test_empty_pipeline(request, cluster, blame, route, client, module_label): """PipelinePolicy with no actions passes requests through unmodified.""" - policy = PipelinePolicy.create_instance(cluster, blame("empty"), route) + policy = PipelinePolicy.create_instance(cluster, blame("empty"), route, labels={"testRun": module_label}) request.addfinalizer(policy.delete) policy.commit() policy.wait_for_ready() @@ -52,9 +52,9 @@ def test_empty_pipeline(request, cluster, blame, route, client): assert response.headers.get("x-pipeline-policy") is None -def test_request_only_pipeline(request, cluster, blame, route, client): +def test_request_only_pipeline(request, cluster, blame, route, client, module_label): """PipelinePolicy with only request actions and no response section works correctly.""" - policy = PipelinePolicy.create_instance(cluster, blame("reqonly"), route) + policy = PipelinePolicy.create_instance(cluster, blame("reqonly"), route, labels={"testRun": module_label}) policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) request.addfinalizer(policy.delete) policy.commit() @@ -64,9 +64,9 @@ def test_request_only_pipeline(request, cluster, blame, route, client): assert client.get("/blocked").status_code == 403 -def test_response_only_pipeline(request, cluster, blame, route, client): +def test_response_only_pipeline(request, cluster, blame, route, client, module_label): """PipelinePolicy with only response actions and no request section works correctly.""" - policy = PipelinePolicy.create_instance(cluster, blame("resonly"), route) + policy = PipelinePolicy.create_instance(cluster, blame("resonly"), route, labels={"testRun": module_label}) policy.on_http_response.add_headers([["x-resp-only", "true"]]) request.addfinalizer(policy.delete) policy.commit() diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py index 69e3ef4b0..e869b945d 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_isolation.py @@ -82,6 +82,9 @@ def pipeline_policy(pipeline_policy): def test_policy_does_not_affect_other_route(client, client2): """Route without PipelinePolicy on the same gateway does not get the response header.""" + # Wait for EnvoyFilter propagation after route2/client2 fixtures are created. + # Without this delay, a leak might not have propagated yet, causing the test to pass even when + # the policy is incorrectly leaking to the second route. time.sleep(EXTENSION_POLICY_PROPAGATION_WAIT) response = client.get("/get") @@ -95,6 +98,9 @@ def test_policy_does_not_affect_other_route(client, client2): def test_policy_does_not_affect_other_gateway(client, client3): """Route on a different gateway does not get the response header.""" + # Wait for EnvoyFilter propagation after gateway2/route3/client3 fixtures are created. + # Without this delay, a leak might not have propagated yet, causing the test to pass even when + # the policy is incorrectly leaking to the second gateway. time.sleep(EXTENSION_POLICY_PROPAGATION_WAIT) response = client.get("/get") diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py index 3558b89f7..9f846d252 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_lifecycle.py @@ -16,9 +16,9 @@ def commit(): @pytest.mark.flaky(reruns=0) -def test_update_policy(request, cluster, blame, route, client): +def test_update_policy(request, cluster, blame, route, client, module_label): """Adding a new response header via policy update propagates to traffic.""" - policy = PipelinePolicy.create_instance(cluster, blame("upd-pp"), route) + policy = PipelinePolicy.create_instance(cluster, blame("upd-pp"), route, labels={"testRun": module_label}) policy.on_http_response.add_headers([["x-update-test", "active"]]) request.addfinalizer(policy.delete) policy.commit() @@ -39,9 +39,9 @@ def test_update_policy(request, cluster, blame, route, client): @pytest.mark.flaky(reruns=0) -def test_delete_policy(request, cluster, blame, route, client): +def test_delete_policy(request, cluster, blame, route, client, module_label): """After deleting the PipelinePolicy, the CR is removed and the actions stop being enforced.""" - policy = PipelinePolicy.create_instance(cluster, blame("del-pp"), route) + policy = PipelinePolicy.create_instance(cluster, blame("del-pp"), route, labels={"testRun": module_label}) policy.on_http_response.add_headers([["x-delete-test", "active"]]) request.addfinalizer(policy.delete) policy.commit() diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_targeting.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_targeting.py index 748d9868c..c04fb899a 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_targeting.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_targeting.py @@ -37,9 +37,9 @@ def client2(route2, hostname2): # pylint: disable=unused-argument @pytest.fixture(scope="module") -def gateway_policy(cluster, blame, gateway): +def gateway_policy(cluster, blame, gateway, module_label): """PipelinePolicy targeting the Gateway with deny and response headers.""" - policy = PipelinePolicy.create_instance(cluster, blame("gw-pp"), gateway) + policy = PipelinePolicy.create_instance(cluster, blame("gw-pp"), gateway, labels={"testRun": module_label}) policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) policy.on_http_response.add_headers([["x-gateway-policy", "active"]]) return policy diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py index 900fc632a..be3c90191 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/test_pipeline_policy_validation.py @@ -15,7 +15,7 @@ def commit(): @pytest.mark.parametrize("kind", ["HTTPRoute", "Gateway"]) -def test_invalid_target_ref(request, cluster, blame, kind): +def test_invalid_target_ref(request, cluster, blame, kind, module_label): """PipelinePolicy targeting a non-existent resource does not reach Accepted state.""" target_name = "does-not-exist" target = CustomReference( @@ -23,7 +23,7 @@ def test_invalid_target_ref(request, cluster, blame, kind): kind=kind, name=target_name, ) - policy = PipelinePolicy.create_instance(cluster, blame("bad-target"), target) + policy = PipelinePolicy.create_instance(cluster, blame("bad-target"), target, labels={"testRun": module_label}) policy.on_http_request.add_deny(predicate='request.url_path == "/blocked"', with_status=403) request.addfinalizer(policy.delete) @@ -35,9 +35,9 @@ def test_invalid_target_ref(request, cluster, blame, kind): ), f"Policy did not report target not found, status: {policy.refresh().model.status.conditions}" -def test_top_level_fail_action(request, cluster, blame, route): +def test_top_level_fail_action(request, cluster, blame, route, module_label): """PipelinePolicy with a top-level fail action (not inside gRPC onReply) should not be accepted.""" - policy = PipelinePolicy.create_instance(cluster, blame("top-fail"), route) + policy = PipelinePolicy.create_instance(cluster, blame("top-fail"), route, labels={"testRun": module_label}) policy.on_http_request.add_fail("top-level fail", predicate='request.url_path == "/fail"') request.addfinalizer(policy.delete) @@ -49,9 +49,9 @@ def test_top_level_fail_action(request, cluster, blame, route): ), f"Policy with top-level fail was accepted, status: {policy.refresh().model.status.conditions}" -def test_invalid_cel_expression(request, cluster, blame, route): +def test_invalid_cel_expression(request, cluster, blame, route, module_label): """PipelinePolicy with malformed CEL predicate fails to enforce.""" - policy = PipelinePolicy.create_instance(cluster, blame("bad-cel"), route) + policy = PipelinePolicy.create_instance(cluster, blame("bad-cel"), route, labels={"testRun": module_label}) policy.on_http_request.add_deny(predicate="INVALID CEL !!!", with_status=403) request.addfinalizer(policy.delete) @@ -63,10 +63,10 @@ def test_invalid_cel_expression(request, cluster, blame, route): ), f"Policy did not report error for invalid CEL, status: {policy.refresh().model.status.conditions}" -def test_variable_forward_reference(request, cluster, blame, route): +def test_variable_forward_reference(request, cluster, blame, route, module_label): """PipelinePolicy referencing a variable before it is defined should fail validation.""" var_name = "threatResponse" - policy = PipelinePolicy.create_instance(cluster, blame("fwd-ref"), route) + policy = PipelinePolicy.create_instance(cluster, blame("fwd-ref"), route, labels={"testRun": module_label}) policy.on_http_request.add_deny(predicate=f"{var_name}.threat_level >= 50", with_status=403) policy.on_http_request.add_grpc_method(method="nonexistent-method", var=var_name) @@ -79,10 +79,10 @@ def test_variable_forward_reference(request, cluster, blame, route): ), f"Policy did not reject forward reference, status: {policy.refresh().model.status.conditions}" -def test_duplicate_variable_name(request, cluster, blame, route): +def test_duplicate_variable_name(request, cluster, blame, route, module_label): """PipelinePolicy with two gRPC methods using the same variable name should fail validation.""" var_name = "dupVar" - policy = PipelinePolicy.create_instance(cluster, blame("dup-var"), route) + policy = PipelinePolicy.create_instance(cluster, blame("dup-var"), route, labels={"testRun": module_label}) policy.on_http_request.add_grpc_method(method="method-a", var=var_name) policy.on_http_request.add_grpc_method(method="method-b", var=var_name) From 9b34adb0df7c216cc38cf93a023b6d948ea30cb4 Mon Sep 17 00:00:00 2001 From: Silvia Tarabova Date: Thu, 16 Jul 2026 13:03:36 +0200 Subject: [PATCH 21/22] fix: use explicit None checks in PipelinePolicy Signed-off-by: Silvia Tarabova --- testsuite/kuadrant/extensions/pipeline_policy.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/testsuite/kuadrant/extensions/pipeline_policy.py b/testsuite/kuadrant/extensions/pipeline_policy.py index 078978568..f1f6e879e 100644 --- a/testsuite/kuadrant/extensions/pipeline_policy.py +++ b/testsuite/kuadrant/extensions/pipeline_policy.py @@ -40,9 +40,9 @@ def section(self): def add_grpc_method(self, method: str, var: Optional[str] = None, predicate: Optional[str] = None): """Add a grpc_method action that calls an upstream""" action: Dict = {"type": "grpc_method", "method": method} - if var: + if var is not None: action["var"] = var - if predicate: + if predicate is not None: action["predicate"] = predicate self.section.append(action) @@ -56,13 +56,13 @@ def add_deny( ): """Add a deny action""" action: Dict = {"type": "deny"} - if predicate: + if predicate is not None: action["predicate"] = predicate - if with_status: + if with_status is not None: action["withStatus"] = with_status - if with_headers: + if with_headers is not None: action["withHeaders"] = with_headers - if with_body: + if with_body is not None: action["withBody"] = with_body self.section.append(action) @@ -70,7 +70,7 @@ def add_deny( def add_fail(self, log_message: str, predicate: Optional[str] = None): """Add a fail action""" action: Dict = {"type": "fail", "logMessage": log_message} - if predicate: + if predicate is not None: action["predicate"] = predicate self.section.append(action) @@ -78,7 +78,7 @@ def add_fail(self, log_message: str, predicate: Optional[str] = None): def add_headers(self, headers: List[List[str]], predicate: Optional[str] = None): """Add an add_headers action""" action: Dict = {"type": "add_headers", "headersToAdd": str(headers)} - if predicate: + if predicate is not None: action["predicate"] = predicate self.section.append(action) From 81235388aa61c738551ad6f7ed87f90156eadb6d Mon Sep 17 00:00:00 2001 From: Silvia Tarabova Date: Fri, 17 Jul 2026 14:07:31 +0200 Subject: [PATCH 22/22] fix: add readiness probe for threat service Signed-off-by: Silvia Tarabova --- .../singlecluster/extensions/pipeline_policy/grpc/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/conftest.py b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/conftest.py index 4e68991c8..afab11236 100644 --- a/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/conftest.py +++ b/testsuite/tests/singlecluster/extensions/pipeline_policy/grpc/conftest.py @@ -23,6 +23,7 @@ def threat_assessment_service(request, cluster, blame, module_label, testconfig) ports={"grpc": HTTP_API_PORT}, selector=Selector(matchLabels=match_labels), labels={"app": module_label}, + readiness_probe={"grpc": {"port": HTTP_API_PORT}, "initialDelaySeconds": 3, "periodSeconds": 5}, ) request.addfinalizer(deployment.delete) deployment.commit()