diff --git a/testsuite/mockserver.py b/testsuite/mockserver.py index 6efde84a..6c3087d1 100644 --- a/testsuite/mockserver.py +++ b/testsuite/mockserver.py @@ -2,10 +2,15 @@ from typing import Union -import httpx from apyproxy import ApyProxy from testsuite.utils import ContentType +from testsuite.httpx import KuadrantClient +from testsuite.openshift import Selector +from testsuite.openshift.backend import Backend +from testsuite.openshift.service import Service, ServicePort +from testsuite.openshift.deployment import Deployment, ContainerResources +from testsuite.openshift.client import OpenShiftClient class Mockserver: @@ -13,21 +18,37 @@ class Mockserver: Mockserver deployed in Openshift (located in Tools or self-managed) """ - def __init__(self, url): - self.client = ApyProxy(url, session=httpx.Client(verify=False, timeout=5)) + def __init__(self, url, client: KuadrantClient = None): + self.client = ApyProxy(url, session=client or KuadrantClient(verify=False)) - def _expectation(self, expectation_id, response_data): + def _expectation(self, expectation_id, json_data): """ - Creates an Expectation with given response_data. + Creates an Expectation from given expectation json. Returns the absolute URL of the expectation """ - json_data = {"id": expectation_id, "httpRequest": {"path": f"/{expectation_id}"}} - json_data.update(response_data) + json_data["id"] = expectation_id + json_data.setdefault("httpRequest", {})["path"] = f"/{expectation_id}" self.client.mockserver.expectation.put(json=json_data) # pylint: disable=protected-access return f"{self.client._url}/{expectation_id}" + def create_request_expectation( + self, + expectation_id, + headers: dict[str, list[str]], + ): + """Creates an Expectation - request with given headers""" + json_data = { + "httpRequest": { + "headers": headers, + }, + "httpResponse": { + "body": "", + }, + } + return self._expectation(expectation_id, json_data) + def create_expectation( self, expectation_id, @@ -56,3 +77,61 @@ def retrieve_requests(self, expectation_id): params={"type": "REQUESTS", "format": "JSON"}, json={"path": "/" + expectation_id}, ).json() + + +class MockserverBackend(Backend): + """Mockserver deployed as backend in Openshift""" + + PORT = 8080 + + def __init__(self, openshift: OpenShiftClient, name: str, label: str): + self.openshift = openshift + 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.openshift.project, + } + + def commit(self): + match_labels = {"app": self.label, "deployment": self.name} + self.deployment = Deployment.create_instance( + self.openshift, + self.name, + container_name="mockserver", + image="quay.io/mganisin/mockserver:latest", + ports={"api": 1080}, + selector=Selector(matchLabels=match_labels), + labels={"app": self.label}, + resources=ContainerResources(limits_memory="2G"), + lifecycle={"postStart": {"exec": {"command": ["/bin/sh", "init-mockserver"]}}}, + ) + self.deployment.commit() + self.deployment.wait_for_ready() + + self.service = Service.create_instance( + self.openshift, + self.name, + selector=match_labels, + ports=[ServicePort(name="1080-tcp", port=self.PORT, targetPort="api")], + labels={"app": self.label}, + ) + self.service.commit() + + def delete(self): + with self.openshift.context: + if self.service: + self.service.delete() + self.service = None + if self.deployment: + self.deployment.delete() + self.deployment = None diff --git a/testsuite/openshift/backend.py b/testsuite/openshift/backend.py new file mode 100644 index 00000000..72244337 --- /dev/null +++ b/testsuite/openshift/backend.py @@ -0,0 +1,8 @@ +"""Backend for Openshift""" + +from testsuite.lifecycle import LifecycleObject +from testsuite.gateway import Referencable + + +class Backend(LifecycleObject, Referencable): + """Backend for Openshift""" diff --git a/testsuite/openshift/deployment.py b/testsuite/openshift/deployment.py index 321f91fc..a60a953b 100644 --- a/testsuite/openshift/deployment.py +++ b/testsuite/openshift/deployment.py @@ -1,7 +1,7 @@ """Deployment related objects""" from dataclasses import dataclass -from typing import Any +from typing import Any, Optional import openshift_client as oc @@ -11,6 +11,25 @@ # pylint: disable=invalid-name +@dataclass +class ContainerResources: + """Deployment ContainerResources object""" + + limits_cpu: Optional[str] = None + limits_memory: Optional[str] = None + requests_cpu: Optional[str] = None + requests_memory: Optional[str] = None + + def asdict(self): + """Remove None pairs and nest limits and requests resources for the result dict""" + result = {} + for key, value in self.__dict__.items(): + if value is not None: + category, resource = key.split("_") + result.setdefault(category, {})[resource] = value + return result + + @dataclass class VolumeMount: """Deployment VolumeMount object""" @@ -72,7 +91,9 @@ def create_instance( volumes: list[Volume] = None, volume_mounts: list[VolumeMount] = None, readiness_probe: dict[str, Any] = None, - ): + resources: Optional[ContainerResources] = None, + lifecycle: dict[str, Any] = None, + ): # pylint: disable=too-many-locals """ Creates new instance of Deployment Supports only single container Deployments everything else should be edited directly @@ -117,6 +138,12 @@ def create_instance( if readiness_probe: container["readinessProbe"] = readiness_probe + if resources: + container["resources"] = asdict(resources) + + if lifecycle: + container["lifecycle"] = lifecycle + return cls(model, context=openshift.context) def wait_for_ready(self, timeout=90): diff --git a/testsuite/openshift/httpbin.py b/testsuite/openshift/httpbin.py index 9ba76c7c..c1349855 100644 --- a/testsuite/openshift/httpbin.py +++ b/testsuite/openshift/httpbin.py @@ -2,15 +2,14 @@ from functools import cached_property -from testsuite.lifecycle import LifecycleObject -from testsuite.gateway import Referencable +from testsuite.openshift.backend import Backend from testsuite.openshift import Selector from testsuite.openshift.client import OpenShiftClient from testsuite.openshift.deployment import Deployment from testsuite.openshift.service import Service, ServicePort -class Httpbin(LifecycleObject, Referencable): +class Httpbin(Backend): """Httpbin deployed in OpenShift""" def __init__(self, openshift: OpenShiftClient, name, label, replicas=1) -> None: diff --git a/testsuite/policy/dns_policy.py b/testsuite/policy/dns_policy.py index f1adba18..3f0af936 100644 --- a/testsuite/policy/dns_policy.py +++ b/testsuite/policy/dns_policy.py @@ -12,10 +12,18 @@ @dataclass -class HealthCheck: # pylint: disable=invalid-name +class AdditionalHeadersRef: + """Object representing DNSPolicy additionalHeadersRef field""" + + name: str + + +@dataclass +class HealthCheck: # pylint: disable=invalid-name,too-many-instance-attributes """Object representing DNSPolicy health check specification""" allowInsecureCertificates: Optional[bool] = None + additionalHeadersRef: Optional[AdditionalHeadersRef] = None endpoint: Optional[str] = None expectedResponses: Optional[list[int]] = None failureThreshold: Optional[int] = None diff --git a/testsuite/tests/mgc/dnspolicy/health_check/test_additional_headers.py b/testsuite/tests/mgc/dnspolicy/health_check/test_additional_headers.py new file mode 100644 index 00000000..d2bb74a7 --- /dev/null +++ b/testsuite/tests/mgc/dnspolicy/health_check/test_additional_headers.py @@ -0,0 +1,69 @@ +"""Tests for DNSPolicy health checks - additional authentication headers sent with health check requests""" + +import pytest + +from testsuite.openshift.secret import Secret +from testsuite.mockserver import Mockserver, MockserverBackend +from testsuite.policy.dns_policy import HealthCheck, AdditionalHeadersRef + +pytestmark = [pytest.mark.mgc] + +HEADER_NAME = "test-header" +HEADER_VALUE = "test-value" + + +@pytest.fixture(scope="module") +def backend(request, openshift, blame, label): + """Use mockserver as backend for health check requests to verify additional headers""" + mockserver = MockserverBackend(openshift, blame("mocksrv"), label) + request.addfinalizer(mockserver.delete) + mockserver.commit() + + return mockserver + + +@pytest.fixture(scope="module") +def mockserver_client(client): + """Returns Mockserver client""" + return Mockserver(str(client.base_url), client=client) + + +@pytest.fixture(scope="module", autouse=True) +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") +def headers_secret(request, hub_openshift, blame): + """Creates Secret with additional headers for DNSPolicy health check""" + secret_name = blame("headers") + headers_secret = Secret.create_instance(hub_openshift, secret_name, {HEADER_NAME: HEADER_VALUE}) + + request.addfinalizer(headers_secret.delete) + headers_secret.commit() + return secret_name + + +@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( + allowInsecureCertificates=True, + additionalHeadersRef=AdditionalHeadersRef(name=headers_secret), + endpoint=f"/{module_label}", + interval="5s", + port=80, + protocol="http", + ) + + +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 + + request_headers = requests[0]["headers"] + assert request_headers.get(HEADER_NAME) == [HEADER_VALUE]