From 15744b617a40658106eabc6721859582981cd7c1 Mon Sep 17 00:00:00 2001 From: Alex Zgabur Date: Thu, 12 Sep 2024 12:21:41 +0200 Subject: [PATCH] Refactor and add gateway listener tests Signed-off-by: Alex Zgabur --- testsuite/gateway/gateway_api/gateway.py | 12 +++- testsuite/httpx/__init__.py | 1 + .../reconciliation/listeners/__init__.py | 0 .../reconciliation/listeners/conftest.py | 65 +++++++++++++++++++ .../listeners/test_gateway_basic_listeners.py | 50 ++++++++++++++ .../listeners/test_gateway_listeners_dns.py | 37 +++++++++++ 6 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 testsuite/tests/singlecluster/gateway/reconciliation/listeners/__init__.py create mode 100644 testsuite/tests/singlecluster/gateway/reconciliation/listeners/conftest.py create mode 100644 testsuite/tests/singlecluster/gateway/reconciliation/listeners/test_gateway_basic_listeners.py create mode 100644 testsuite/tests/singlecluster/gateway/reconciliation/listeners/test_gateway_listeners_dns.py diff --git a/testsuite/gateway/gateway_api/gateway.py b/testsuite/gateway/gateway_api/gateway.py index 2e77591c..b9224d83 100644 --- a/testsuite/gateway/gateway_api/gateway.py +++ b/testsuite/gateway/gateway_api/gateway.py @@ -1,6 +1,6 @@ """Module containing all gateway classes""" -from typing import Any +from typing import Any, Optional import openshift_client as oc @@ -46,6 +46,16 @@ def remove_listener(self, hostname: GatewayListener | str): hostname = hostname.hostname self.model.spec.listeners = list(filter(lambda i: i["hostname"] != hostname, self.model.spec.listeners)) + def get_listener_dns_ttl(self, hostname: Optional[str] = None) -> int: + """Returns TTL stored in DNSRecord CR under the default hostname or specific hostname if argument is defined.""" + listener_name = "api" + if hostname: + listener_name = [i for i in self.model.spec.listeners if i["hostname"] == hostname][0]["name"] + dns_record = self.cluster.do_action( + "get", ["-o", "yaml", f"dnsrecords.kuadrant.io/{self.name()}-{listener_name}"], parse_output=True + ) + return dns_record.model.spec.endpoints[0].recordTTL + @property def service_name(self) -> str: return f"{self.name()}-istio" diff --git a/testsuite/httpx/__init__.py b/testsuite/httpx/__init__.py index 05c06104..90e25ac6 100644 --- a/testsuite/httpx/__init__.py +++ b/testsuite/httpx/__init__.py @@ -50,6 +50,7 @@ def should_backoff(self): or (self.error is None and self.status_code in self.retry_codes) or self.has_error("Server disconnected without sending a response.") or self.has_error("timed out") + or self.has_error("SSL: UNEXPECTED_EOF_WHILE_READING") ) def has_error(self, error_msg: str) -> bool: diff --git a/testsuite/tests/singlecluster/gateway/reconciliation/listeners/__init__.py b/testsuite/tests/singlecluster/gateway/reconciliation/listeners/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/testsuite/tests/singlecluster/gateway/reconciliation/listeners/conftest.py b/testsuite/tests/singlecluster/gateway/reconciliation/listeners/conftest.py new file mode 100644 index 00000000..ef320462 --- /dev/null +++ b/testsuite/tests/singlecluster/gateway/reconciliation/listeners/conftest.py @@ -0,0 +1,65 @@ +""" +Conftest for Gateway listeners tests. +The main change consists of replacing the default wildcard domain for an exact one. +""" + +import pytest + +from testsuite.gateway.gateway_api.hostname import StaticHostname + + +@pytest.fixture(scope="module") +def domain_prefix(): + """Prefix of default domain.""" + return "prefix" + + +@pytest.fixture(scope="module") +def wildcard_domain(base_domain, domain_prefix, blame): + """ + For these tests we want specific default domain, not wildcard. + """ + return f'{blame(domain_prefix + "1")}.{base_domain}' + + +@pytest.fixture(scope="module") +def second_domain(base_domain, domain_prefix, blame): + """Second domain string, not used in any object yet. To be assigned inside test.""" + return f'{blame(domain_prefix + "2")}.{base_domain}' + + +@pytest.fixture(scope="module") +def custom_client(gateway): + """ + While changing TLS listeners the TLS certificate changes so a new client needs to be generated + to fetch newest tls cert from cluster. + """ + + def _client_new(hostname: str): + return StaticHostname(hostname, gateway.get_tls_cert).client() + + return _client_new + + +@pytest.fixture(scope="module") +def check_ok_https(custom_client, auth): + """ + Assert that HTTPS connection to domain works and returns 200. Authorization is used. + Assert that no DNS and TLS errors happened. + """ + + def _check_ok_https(domain: str): + response = custom_client(domain).get("/get", auth=auth) + assert not response.has_dns_error() + assert not response.has_cert_verify_error() + assert response.status_code == 200 + + return _check_ok_https + + +@pytest.fixture(scope="module") +def route(route, wildcard_domain): + """Ensure that route hostname matches the gateway hostname.""" + route.remove_all_hostnames() + route.add_hostname(wildcard_domain) + return route diff --git a/testsuite/tests/singlecluster/gateway/reconciliation/listeners/test_gateway_basic_listeners.py b/testsuite/tests/singlecluster/gateway/reconciliation/listeners/test_gateway_basic_listeners.py new file mode 100644 index 00000000..ca39ef09 --- /dev/null +++ b/testsuite/tests/singlecluster/gateway/reconciliation/listeners/test_gateway_basic_listeners.py @@ -0,0 +1,50 @@ +""" +Test case: +- Add new listener and add it to HTTPRoute and test both work +- Remove the new listener and remove it from HTTPRoute and test removed one is not working +""" + +from time import sleep +import pytest + +from testsuite.gateway import GatewayListenerTls +from testsuite.utils import is_nxdomain + + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.dnspolicy, pytest.mark.tlspolicy] + + +@pytest.fixture(scope="module") +def domain_prefix(): + """Prefix for this test""" + return "listen" + + +def test_listeners(custom_client, check_ok_https, gateway, route, wildcard_domain, second_domain): + """ + This test checks reconciliation of dns/tls policy on addition and removal of listeners in gateway and HTTPRoute. + """ + + # Check the default domain works and second domain does not exist yet + check_ok_https(wildcard_domain) + assert is_nxdomain(second_domain) + assert custom_client(second_domain).get("/get").has_dns_error() + + # Add second domain to gateway and route + gateway.add_listener(GatewayListenerTls(hostname=second_domain, gateway_name=gateway.name())) + route.add_hostname(second_domain) + + # Check both domains work + for domain in [wildcard_domain, second_domain]: + check_ok_https(domain) + + # Remove second domain, store TTL value of to be removed DNS record + second_domain_ttl = gateway.get_listener_dns_ttl(second_domain) + route.remove_hostname(second_domain) + gateway.remove_listener(second_domain) + + # Check the default domain still works and second domain does not exist anymore + check_ok_https(wildcard_domain) + sleep(second_domain_ttl) + assert is_nxdomain(second_domain) + assert custom_client(second_domain).get("/get").has_dns_error() diff --git a/testsuite/tests/singlecluster/gateway/reconciliation/listeners/test_gateway_listeners_dns.py b/testsuite/tests/singlecluster/gateway/reconciliation/listeners/test_gateway_listeners_dns.py new file mode 100644 index 00000000..53959e07 --- /dev/null +++ b/testsuite/tests/singlecluster/gateway/reconciliation/listeners/test_gateway_listeners_dns.py @@ -0,0 +1,37 @@ +"""Testing specific bug that happens when listener hostname in Gateway gets changed while DNSPolicy is applied.""" + +from time import sleep +import pytest + +from testsuite.gateway import GatewayListenerTls +from testsuite.utils import is_nxdomain + +pytestmark = [pytest.mark.kuadrant_only, pytest.mark.dnspolicy, pytest.mark.tlspolicy] + + +@pytest.fixture(scope="module") +def domain_prefix(): + """Prefix for this test""" + return "dnsbug" + + +@pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/794") +def test_change_listener(custom_client, check_ok_https, gateway, route, second_domain, wildcard_domain): + """ + This test checks if after change of listener hostname in a Gateway while having DNSPolicy applied, that + the old hostname gets deleted from DNS provider. After editing the hostname in HTTPRoute to the new value + this test checks the reconciliation of such procedure. + """ + check_ok_https(wildcard_domain) + wildcard_domain_ttl = gateway.get_listener_dns_ttl(wildcard_domain) + + gateway.remove_listener(wildcard_domain) + gateway.add_listener(GatewayListenerTls(hostname=second_domain, gateway_name=gateway.name(), name="api")) + route.remove_hostname(wildcard_domain) + route.add_hostname(second_domain) + + check_ok_https(second_domain) + + sleep(wildcard_domain_ttl) + assert is_nxdomain(wildcard_domain) + assert custom_client(wildcard_domain).get("/get").has_dns_error()