From 42b9629af22f54a02b99285e23b1a7503e6d720c Mon Sep 17 00:00:00 2001 From: averevki Date: Tue, 26 Nov 2024 12:37:02 +0100 Subject: [PATCH 1/2] Configure mockserver backend for health check additional headers test Signed-off-by: averevki --- testsuite/backend/__init__.py | 35 ++++++++++++++++++++++++++- testsuite/backend/httpbin.py | 33 +------------------------- testsuite/backend/mockserver.py | 42 +++++++-------------------------- testsuite/kubernetes/service.py | 2 +- 4 files changed, 44 insertions(+), 68 deletions(-) diff --git a/testsuite/backend/__init__.py b/testsuite/backend/__init__.py index df7fd49d..fa45f0ff 100644 --- a/testsuite/backend/__init__.py +++ b/testsuite/backend/__init__.py @@ -1,15 +1,48 @@ """Module containing all the Backends""" from abc import abstractmethod +from functools import cached_property from testsuite.gateway import Referencable from testsuite.lifecycle import LifecycleObject +from testsuite.kubernetes.client import KubernetesClient class Backend(LifecycleObject, Referencable): """Backend (workload) deployed in Kubernetes""" + def __init__(self, cluster: KubernetesClient, name: str, label: str): + self.cluster = cluster + self.name = name + self.label = label + + self.deployment = None + self.service = None + + @property + def reference(self): + return {"group": "", "kind": "Service", "port": 8080, "name": self.name, "namespace": self.cluster.project} + @property - @abstractmethod def url(self): """Returns internal URL for this backend""" + return f"{self.name}.{self.cluster.project}.svc.cluster.local" + + @cached_property + def port(self): + """Service port that httpbin listens on""" + return self.service.get_port("http").port + + @abstractmethod + def commit(self): + """Deploys the backend""" + + def delete(self): + """Clean-up the backend""" + with self.cluster.context: + if self.service: + self.service.delete() + self.service = None + if self.deployment: + self.deployment.delete() + self.deployment = None diff --git a/testsuite/backend/httpbin.py b/testsuite/backend/httpbin.py index 17fe3ec1..7aec943f 100644 --- a/testsuite/backend/httpbin.py +++ b/testsuite/backend/httpbin.py @@ -1,7 +1,5 @@ """Httpbin implementation of Backend""" -from functools import cached_property - from testsuite.backend import Backend from testsuite.kubernetes import Selector from testsuite.kubernetes.client import KubernetesClient @@ -13,25 +11,10 @@ class Httpbin(Backend): """Httpbin deployed in Kubernetes as Backend""" def __init__(self, cluster: KubernetesClient, name, label, image, replicas=1) -> None: - super().__init__() - self.cluster = cluster - self.name = name - self.label = label + super().__init__(cluster, name, label) self.replicas = replicas self.image = image - self.deployment = None - self.service = None - - @property - def reference(self): - return {"group": "", "kind": "Service", "port": 8080, "name": self.name, "namespace": self.cluster.project} - - @property - def url(self): - """URL for the httpbin service""" - return f"{self.name}.{self.cluster.project}.svc.cluster.local" - def commit(self): match_labels = {"app": self.label, "deployment": self.name} self.deployment = Deployment.create_instance( @@ -53,17 +36,3 @@ def commit(self): ports=[ServicePort(name="http", port=8080, targetPort="api")], ) self.service.commit() - - def delete(self): - with self.cluster.context: - if self.service: - self.service.delete() - self.service = None - if self.deployment: - self.deployment.delete() - self.deployment = None - - @cached_property - def port(self): - """Service port that httpbin listens on""" - return self.service.get_port("http").port diff --git a/testsuite/backend/mockserver.py b/testsuite/backend/mockserver.py index 06b5db7d..d4875311 100644 --- a/testsuite/backend/mockserver.py +++ b/testsuite/backend/mockserver.py @@ -2,7 +2,6 @@ from testsuite.backend import Backend from testsuite.kubernetes import Selector -from testsuite.kubernetes.client import KubernetesClient from testsuite.kubernetes.deployment import Deployment, ContainerResources from testsuite.kubernetes.service import Service, ServicePort @@ -10,30 +9,6 @@ class MockserverBackend(Backend): """Mockserver deployed as backend in Kubernetes""" - PORT = 8080 - - def __init__(self, cluster: KubernetesClient, name: str, label: str): - self.cluster = cluster - self.name = name - self.label = label - - self.deployment = None - self.service = None - - @property - def reference(self): - return { - "group": "", - "kind": "Service", - "port": self.PORT, - "name": self.name, - "namespace": self.cluster.project, - } - - @property - def url(self): - return f"{self.name}.{self.cluster.project}.svc.cluster.local" - def commit(self): match_labels = {"app": self.label, "deployment": self.name} self.deployment = Deployment.create_instance( @@ -54,16 +29,15 @@ def commit(self): self.cluster, self.name, selector=match_labels, - ports=[ServicePort(name="1080-tcp", port=self.PORT, targetPort="api")], + ports=[ServicePort(name="1080-tcp", port=8080, targetPort="api")], labels={"app": self.label}, + service_type="LoadBalancer", ) self.service.commit() - def delete(self): - with self.cluster.context: - if self.service: - self.service.delete() - self.service = None - if self.deployment: - self.deployment.delete() - self.deployment = None + def wait_for_ready(self, timeout=300): + """Waits until Deployment is marked as ready""" + success = self.service.wait_until( + lambda obj: "ip" in self.service.refresh().model.status.loadBalancer.ingress[0], timelimit=timeout + ) + assert success, f"Service {self.name} did not get ready in time" diff --git a/testsuite/kubernetes/service.py b/testsuite/kubernetes/service.py index eedc4157..d2199d36 100644 --- a/testsuite/kubernetes/service.py +++ b/testsuite/kubernetes/service.py @@ -64,7 +64,7 @@ def external_ip(self): if ip is Missing: ip = self.model.status.loadBalancer.ingress[0].hostname if ip is Missing: - raise AttributeError(f"Neither External IP nor Hostname found in status of {self.model.spec.name} service") + raise AttributeError(f"Neither External IP nor Hostname found in status of {self.kind()}/{self.name()}") return ip From 813b6ac58dbebed7795d1a64f93c673153fefe6f Mon Sep 17 00:00:00 2001 From: averevki Date: Tue, 26 Nov 2024 12:37:42 +0100 Subject: [PATCH 2/2] Add health check additional headers test Signed-off-by: averevki --- .../health_check/test_additional_headers.py | 88 +++++++++++++++++++ .../health_check/test_healthy_endpoint.py | 2 +- .../health_check/test_remove_endpoint.py | 2 +- .../health_check/test_unhealthy_endpoint.py | 2 +- 4 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 testsuite/tests/singlecluster/gateway/dnspolicy/health_check/test_additional_headers.py diff --git a/testsuite/tests/singlecluster/gateway/dnspolicy/health_check/test_additional_headers.py b/testsuite/tests/singlecluster/gateway/dnspolicy/health_check/test_additional_headers.py new file mode 100644 index 00000000..c6061d83 --- /dev/null +++ b/testsuite/tests/singlecluster/gateway/dnspolicy/health_check/test_additional_headers.py @@ -0,0 +1,88 @@ +"""Tests for DNSPolicy health checks - additional authentication headers sent with health check requests""" + +import pytest + +from testsuite.httpx import KuadrantClient +from testsuite.mockserver import Mockserver +from testsuite.gateway import GatewayListener +from testsuite.gateway.gateway_api.gateway import KuadrantGateway +from testsuite.kubernetes.secret import Secret +from testsuite.kuadrant.policy.dns import HealthCheck, AdditionalHeadersRef +from testsuite.backend.mockserver import MockserverBackend + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.dnspolicy] + +HEADER_NAME = "test-header" +HEADER_VALUE = "test-value" + + +@pytest.fixture(scope="module") +def health_check(headers_secret, module_label): + """Returns healthy endpoint specification with additional authentication header for DNSPolicy health check""" + return HealthCheck( + additionalHeadersRef=AdditionalHeadersRef(name=headers_secret), + path=f"/{module_label}", + interval="5s", + protocol="HTTP", + port=80, + ) + + +@pytest.fixture(scope="module") +def gateway(request, cluster, blame, base_domain, module_label, subdomain): + """Create gateway without TLS enabled""" + gw = KuadrantGateway.create_instance(cluster, blame("gw"), {"app": module_label}) + gw.add_listener(GatewayListener(hostname=f"{subdomain}.{base_domain}")) + request.addfinalizer(gw.delete) + gw.commit() + gw.wait_for_ready() + return gw + + +@pytest.fixture(scope="module") +def backend(request, cluster, blame, label): + """Use mockserver as backend for health check requests to verify additional headers""" + mockserver = MockserverBackend(cluster, blame("mocksrv"), label) + request.addfinalizer(mockserver.delete) + mockserver.commit() + mockserver.wait_for_ready() + return mockserver + + +@pytest.fixture(scope="module") +def headers_secret(request, cluster, blame): + """Creates Secret with additional headers for DNSPolicy health check""" + secret_name = blame("headers") + headers_secret = Secret.create_instance(cluster, secret_name, {HEADER_NAME: HEADER_VALUE}) + request.addfinalizer(headers_secret.delete) + headers_secret.commit() + return secret_name + + +@pytest.fixture(scope="module") +def mockserver_client(backend): + """Returns Mockserver client from load-balanced service IP""" + return Mockserver(KuadrantClient(base_url=f"http://{backend.service.refresh().external_ip}: 8080")) + + +@pytest.fixture(scope="module") +def mockserver_backend_expectation(mockserver_client, module_label): + """Creates Mockserver Expectation which requires additional headers for successful request""" + mockserver_client.create_request_expectation(module_label, headers={HEADER_NAME: [HEADER_VALUE]}) + + +@pytest.fixture(scope="module", autouse=True) +def commit(request, route, dns_policy, mockserver_backend_expectation): # pylint: disable=unused-argument + """Commits dnspolicy only""" + request.addfinalizer(dns_policy.delete) + dns_policy.commit() + dns_policy.wait_for_ready() + + +def test_additional_headers(dns_health_probe, mockserver_client, module_label): + """Test if additional headers in health check requests are used""" + assert dns_health_probe.is_healthy() + + requests = mockserver_client.retrieve_requests(module_label) + assert len(requests) > 0 + assert requests[0]["headers"].get(HEADER_NAME) == [HEADER_VALUE] diff --git a/testsuite/tests/singlecluster/gateway/dnspolicy/health_check/test_healthy_endpoint.py b/testsuite/tests/singlecluster/gateway/dnspolicy/health_check/test_healthy_endpoint.py index 5e59f649..716bbb44 100644 --- a/testsuite/tests/singlecluster/gateway/dnspolicy/health_check/test_healthy_endpoint.py +++ b/testsuite/tests/singlecluster/gateway/dnspolicy/health_check/test_healthy_endpoint.py @@ -4,7 +4,7 @@ from testsuite.kuadrant.policy.dns import HealthCheck -pytestmark = [pytest.mark.kuadrant_only, pytest.mark.dnspolicy] +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.dnspolicy, pytest.mark.tlspolicy] @pytest.fixture(scope="module") diff --git a/testsuite/tests/singlecluster/gateway/dnspolicy/health_check/test_remove_endpoint.py b/testsuite/tests/singlecluster/gateway/dnspolicy/health_check/test_remove_endpoint.py index c4770d1d..5e119202 100644 --- a/testsuite/tests/singlecluster/gateway/dnspolicy/health_check/test_remove_endpoint.py +++ b/testsuite/tests/singlecluster/gateway/dnspolicy/health_check/test_remove_endpoint.py @@ -5,7 +5,7 @@ from testsuite.kuadrant.policy import has_condition from testsuite.kuadrant.policy.dns import HealthCheck, has_record_condition -pytestmark = [pytest.mark.kuadrant_only, pytest.mark.dnspolicy] +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.dnspolicy, pytest.mark.tlspolicy] @pytest.fixture(scope="module") diff --git a/testsuite/tests/singlecluster/gateway/dnspolicy/health_check/test_unhealthy_endpoint.py b/testsuite/tests/singlecluster/gateway/dnspolicy/health_check/test_unhealthy_endpoint.py index f9de3515..174824bc 100644 --- a/testsuite/tests/singlecluster/gateway/dnspolicy/health_check/test_unhealthy_endpoint.py +++ b/testsuite/tests/singlecluster/gateway/dnspolicy/health_check/test_unhealthy_endpoint.py @@ -5,7 +5,7 @@ from testsuite.kuadrant.policy import has_condition from testsuite.kuadrant.policy.dns import HealthCheck, has_record_condition -pytestmark = [pytest.mark.kuadrant_only, pytest.mark.dnspolicy] +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.dnspolicy, pytest.mark.tlspolicy] @pytest.fixture(scope="module")