From 25cd7dbe0973fa8af9ebe964a6775fdc64f50309 Mon Sep 17 00:00:00 2001 From: Martina Fabikova Date: Tue, 9 Dec 2025 12:04:13 +0100 Subject: [PATCH 1/2] test(dns): Verify DNS Endpoint Provider aggregation logic Signed-off-by: Martina Fabikova --- testsuite/kuadrant/policy/dns.py | 31 +++++ .../gateway/dnspolicy/dns_records/__init__.py | 0 .../dns_records/test_dns_endpoint_provider.py | 111 ++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 testsuite/tests/singlecluster/gateway/dnspolicy/dns_records/__init__.py create mode 100644 testsuite/tests/singlecluster/gateway/dnspolicy/dns_records/test_dns_endpoint_provider.py diff --git a/testsuite/kuadrant/policy/dns.py b/testsuite/kuadrant/policy/dns.py index 2de136dfe..5ce0ed1bd 100644 --- a/testsuite/kuadrant/policy/dns.py +++ b/testsuite/kuadrant/policy/dns.py @@ -4,6 +4,7 @@ from typing import Optional, Literal import backoff +import dns.resolver import openshift_client as oc from testsuite.gateway import Referencable @@ -115,6 +116,36 @@ def wait_for_ready(self): ) assert success, f"DNSRecord {self.name()} did not get ready in time" + def wait_for_endpoints_merged(self, expected_ips: set[str]): + """Waits until the specified IPs are present in the DNSRecord endpoints list""" + + def _check_endpoints(obj): + current_endpoints = obj.model.spec.endpoints or [] + found_ips = {target for ep in current_endpoints for target in ep.targets} + return expected_ips.issubset(found_ips) + + success = self.wait_until(_check_endpoints) + if not success: + raise AssertionError( + f"Endpoints merge failed for {self.name()}. " + f"Expected subset: {expected_ips}. Current: {self.model.spec.endpoints}" + ) + + def wait_until_resolves(self, hostname: str, expected_ip: str): + """Waits until the hostname resolves to the expected IP using external DNS""" + + def _check_dns(_): + try: + resolver = dns.resolver.Resolver() + answers = resolver.resolve(hostname, "A") + found_ips = {ip.to_text() for ip in answers} + return expected_ip in found_ips + except Exception: # pylint: disable=broad-exception-caught + return False + + success = self.wait_until(_check_dns) + assert success, f"DNS resolution failed for {hostname}. Expected: {expected_ip}" + def get_authoritative_dns_record(self) -> str: """Returns the authoritative DNS record created by dns operator controller""" with self.context: diff --git a/testsuite/tests/singlecluster/gateway/dnspolicy/dns_records/__init__.py b/testsuite/tests/singlecluster/gateway/dnspolicy/dns_records/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/testsuite/tests/singlecluster/gateway/dnspolicy/dns_records/test_dns_endpoint_provider.py b/testsuite/tests/singlecluster/gateway/dnspolicy/dns_records/test_dns_endpoint_provider.py new file mode 100644 index 000000000..efaf238e3 --- /dev/null +++ b/testsuite/tests/singlecluster/gateway/dnspolicy/dns_records/test_dns_endpoint_provider.py @@ -0,0 +1,111 @@ +""" +Tests the DNS Endpoint Provider aggregation logic. + +Verifies that endpoints from multiple Source DNSRecords are correctly merged into +a single Destination DNSRecord (Zone) and successfully resolved via the upstream provider. +""" + +import pytest +from testsuite.kuadrant.policy.dns import DNSRecord, DNSRecordEndpoint + +SOURCE_IP1 = "91.16.35.100" +SOURCE_IP2 = "172.6.13.223" +DUMMY_IP = "127.0.0.1" + +pytestmark = [pytest.mark.dnspolicy] + + +@pytest.fixture(scope="module") +def endpoint_provider_secret(): + """Returns the name of the endpoint provider secret""" + return "dns-provider-credentials-endpoint" + + +@pytest.fixture(scope="module") +def aws_provider_secret(): + """Returns the name of the AWS provider secret""" + return "aws-credentials" + + +@pytest.fixture(scope="module") +def shared_hostname(base_domain, blame): + """Returns the shared hostname used for aggregation""" + return f"{blame('app')}.{base_domain}" + + +@pytest.fixture(scope="module") +def destination_dnsrecord(cluster, blame, shared_hostname, aws_provider_secret, module_label): + """Destination Record acting as the Zone""" + dummy_endpoint = DNSRecordEndpoint(dnsName=shared_hostname, recordType="A", recordTTL=300, targets=[DUMMY_IP]) + + record = DNSRecord.create_instance( + cluster=cluster, + name=blame("dest-zone"), + root_host=shared_hostname, + endpoints=[dummy_endpoint], + delegate=False, + labels={"app": module_label, "kuadrant.io/zone-record": "true"}, + ) + record.model["spec"]["providerRef"] = {"name": aws_provider_secret} + return record + + +@pytest.fixture(scope="module") +def source_dnsrecords(cluster, blame, shared_hostname, endpoint_provider_secret, module_label): + """Source Records acting as endpoint feeders""" + dns_name_1 = f"src1.{shared_hostname}" + dns_name_2 = f"src2.{shared_hostname}" + + source1 = DNSRecord.create_instance( + cluster=cluster, + name=blame("src-1"), + root_host=shared_hostname, + endpoints=[DNSRecordEndpoint(dnsName=dns_name_1, recordType="A", recordTTL=60, targets=[SOURCE_IP1])], + delegate=False, + labels={"app": module_label}, + ) + source1.model["spec"]["providerRef"] = {"name": endpoint_provider_secret} + + source2 = DNSRecord.create_instance( + cluster=cluster, + name=blame("src-2"), + root_host=shared_hostname, + endpoints=[DNSRecordEndpoint(dnsName=dns_name_2, recordType="A", recordTTL=60, targets=[SOURCE_IP2])], + delegate=False, + labels={"app": module_label}, + ) + source2.model["spec"]["providerRef"] = {"name": endpoint_provider_secret} + + return [source1, source2] + + +@pytest.fixture(scope="module", autouse=True) +def commit(request, destination_dnsrecord, source_dnsrecords): + """Commits the DNSRecords to the cluster and handles cleanup""" + request.addfinalizer(destination_dnsrecord.delete) + destination_dnsrecord.commit() + destination_dnsrecord.wait_for_ready() + + for record in source_dnsrecords: + request.addfinalizer(record.delete) + record.commit() + + +def test_endpoint_provider_configuration(destination_dnsrecord, source_dnsrecords, endpoint_provider_secret): + """Verify configuration and labels""" + destination_dnsrecord.refresh() + assert destination_dnsrecord.model.metadata.labels.get("kuadrant.io/zone-record") == "true" + + for record in source_dnsrecords: + record.refresh() + assert record.model.spec.providerRef.name == endpoint_provider_secret + assert record.model.spec.rootHost == destination_dnsrecord.model.spec.rootHost + + +def test_records_accessible(destination_dnsrecord, shared_hostname): + """Verify that endpoints are merged and accessible via DNS""" + # 1. Verify Merge + destination_dnsrecord.wait_for_endpoints_merged({SOURCE_IP1, SOURCE_IP2}) + # 2. Verify DNS Resolution + destination_dnsrecord.wait_until_resolves(f"src1.{shared_hostname}", SOURCE_IP1) + destination_dnsrecord.wait_until_resolves(f"src2.{shared_hostname}", SOURCE_IP2) From a9ed4ad979ac987d80d6ab8ad17e49d5ea7dc3ce Mon Sep 17 00:00:00 2001 From: Martina Fabikova Date: Fri, 9 Jan 2026 15:39:11 +0100 Subject: [PATCH 2/2] fix addressing review comments Signed-off-by: Martina Fabikova --- testsuite/kuadrant/policy/dns.py | 32 -------- .../dns_records/test_dns_endpoint_provider.py | 75 ++++++++----------- 2 files changed, 31 insertions(+), 76 deletions(-) diff --git a/testsuite/kuadrant/policy/dns.py b/testsuite/kuadrant/policy/dns.py index 5ce0ed1bd..43316d1ff 100644 --- a/testsuite/kuadrant/policy/dns.py +++ b/testsuite/kuadrant/policy/dns.py @@ -4,9 +4,7 @@ from typing import Optional, Literal import backoff -import dns.resolver import openshift_client as oc - from testsuite.gateway import Referencable from testsuite.kubernetes import KubernetesObject from testsuite.kubernetes.client import KubernetesClient @@ -116,36 +114,6 @@ def wait_for_ready(self): ) assert success, f"DNSRecord {self.name()} did not get ready in time" - def wait_for_endpoints_merged(self, expected_ips: set[str]): - """Waits until the specified IPs are present in the DNSRecord endpoints list""" - - def _check_endpoints(obj): - current_endpoints = obj.model.spec.endpoints or [] - found_ips = {target for ep in current_endpoints for target in ep.targets} - return expected_ips.issubset(found_ips) - - success = self.wait_until(_check_endpoints) - if not success: - raise AssertionError( - f"Endpoints merge failed for {self.name()}. " - f"Expected subset: {expected_ips}. Current: {self.model.spec.endpoints}" - ) - - def wait_until_resolves(self, hostname: str, expected_ip: str): - """Waits until the hostname resolves to the expected IP using external DNS""" - - def _check_dns(_): - try: - resolver = dns.resolver.Resolver() - answers = resolver.resolve(hostname, "A") - found_ips = {ip.to_text() for ip in answers} - return expected_ip in found_ips - except Exception: # pylint: disable=broad-exception-caught - return False - - success = self.wait_until(_check_dns) - assert success, f"DNS resolution failed for {hostname}. Expected: {expected_ip}" - def get_authoritative_dns_record(self) -> str: """Returns the authoritative DNS record created by dns operator controller""" with self.context: diff --git a/testsuite/tests/singlecluster/gateway/dnspolicy/dns_records/test_dns_endpoint_provider.py b/testsuite/tests/singlecluster/gateway/dnspolicy/dns_records/test_dns_endpoint_provider.py index efaf238e3..c34453fb0 100644 --- a/testsuite/tests/singlecluster/gateway/dnspolicy/dns_records/test_dns_endpoint_provider.py +++ b/testsuite/tests/singlecluster/gateway/dnspolicy/dns_records/test_dns_endpoint_provider.py @@ -6,7 +6,9 @@ """ import pytest +import dns.resolver from testsuite.kuadrant.policy.dns import DNSRecord, DNSRecordEndpoint +from testsuite.kubernetes.secret import Secret SOURCE_IP1 = "91.16.35.100" SOURCE_IP2 = "172.6.13.223" @@ -16,50 +18,50 @@ @pytest.fixture(scope="module") -def endpoint_provider_secret(): - """Returns the name of the endpoint provider secret""" - return "dns-provider-credentials-endpoint" - - -@pytest.fixture(scope="module") -def aws_provider_secret(): - """Returns the name of the AWS provider secret""" - return "aws-credentials" - +def endpoint_provider_secret(request, cluster, module_label, blame): + """Creates a fresh endpoint provider secret in the test namespace""" + secret_data = {"AWS_ACCESS_KEY_ID": "DUMMYACCESSKEY", "AWS_SECRET_ACCESS_KEY": "DUMMYSECRETKEY"} + + secret = Secret.create_instance( + cluster, + blame("endpoint-creds"), + secret_data, + secret_type="kuadrant.io/endpoint", + labels={"app": module_label}, + ) -@pytest.fixture(scope="module") -def shared_hostname(base_domain, blame): - """Returns the shared hostname used for aggregation""" - return f"{blame('app')}.{base_domain}" + request.addfinalizer(secret.delete) + secret.commit() + return secret.name() @pytest.fixture(scope="module") -def destination_dnsrecord(cluster, blame, shared_hostname, aws_provider_secret, module_label): +def destination_dnsrecord(cluster, blame, hostname, dns_provider_secret, module_label): """Destination Record acting as the Zone""" - dummy_endpoint = DNSRecordEndpoint(dnsName=shared_hostname, recordType="A", recordTTL=300, targets=[DUMMY_IP]) + dummy_endpoint = DNSRecordEndpoint(dnsName=hostname.hostname, recordType="A", recordTTL=300, targets=[DUMMY_IP]) record = DNSRecord.create_instance( cluster=cluster, name=blame("dest-zone"), - root_host=shared_hostname, + root_host=hostname.hostname, endpoints=[dummy_endpoint], delegate=False, labels={"app": module_label, "kuadrant.io/zone-record": "true"}, ) - record.model["spec"]["providerRef"] = {"name": aws_provider_secret} + record.model["spec"]["providerRef"] = {"name": dns_provider_secret} return record @pytest.fixture(scope="module") -def source_dnsrecords(cluster, blame, shared_hostname, endpoint_provider_secret, module_label): +def source_dnsrecords(cluster, blame, hostname, endpoint_provider_secret, module_label): """Source Records acting as endpoint feeders""" - dns_name_1 = f"src1.{shared_hostname}" - dns_name_2 = f"src2.{shared_hostname}" + dns_name_1 = f"src1.{hostname.hostname}" + dns_name_2 = f"src2.{hostname.hostname}" source1 = DNSRecord.create_instance( cluster=cluster, name=blame("src-1"), - root_host=shared_hostname, + root_host=hostname.hostname, endpoints=[DNSRecordEndpoint(dnsName=dns_name_1, recordType="A", recordTTL=60, targets=[SOURCE_IP1])], delegate=False, labels={"app": module_label}, @@ -69,7 +71,7 @@ def source_dnsrecords(cluster, blame, shared_hostname, endpoint_provider_secret, source2 = DNSRecord.create_instance( cluster=cluster, name=blame("src-2"), - root_host=shared_hostname, + root_host=hostname.hostname, endpoints=[DNSRecordEndpoint(dnsName=dns_name_2, recordType="A", recordTTL=60, targets=[SOURCE_IP2])], delegate=False, labels={"app": module_label}, @@ -82,30 +84,15 @@ def source_dnsrecords(cluster, blame, shared_hostname, endpoint_provider_secret, @pytest.fixture(scope="module", autouse=True) def commit(request, destination_dnsrecord, source_dnsrecords): """Commits the DNSRecords to the cluster and handles cleanup""" - request.addfinalizer(destination_dnsrecord.delete) - destination_dnsrecord.commit() - destination_dnsrecord.wait_for_ready() + all_records = [destination_dnsrecord] + source_dnsrecords - for record in source_dnsrecords: + for record in all_records: request.addfinalizer(record.delete) record.commit() + record.wait_for_ready() -def test_endpoint_provider_configuration(destination_dnsrecord, source_dnsrecords, endpoint_provider_secret): - """Verify configuration and labels""" - destination_dnsrecord.refresh() - assert destination_dnsrecord.model.metadata.labels.get("kuadrant.io/zone-record") == "true" - - for record in source_dnsrecords: - record.refresh() - assert record.model.spec.providerRef.name == endpoint_provider_secret - assert record.model.spec.rootHost == destination_dnsrecord.model.spec.rootHost - - -def test_records_accessible(destination_dnsrecord, shared_hostname): +def test_records_accessible(hostname): """Verify that endpoints are merged and accessible via DNS""" - # 1. Verify Merge - destination_dnsrecord.wait_for_endpoints_merged({SOURCE_IP1, SOURCE_IP2}) - # 2. Verify DNS Resolution - destination_dnsrecord.wait_until_resolves(f"src1.{shared_hostname}", SOURCE_IP1) - destination_dnsrecord.wait_until_resolves(f"src2.{shared_hostname}", SOURCE_IP2) + assert SOURCE_IP1 in {r.address for r in dns.resolver.resolve(f"src1.{hostname.hostname}")} + assert SOURCE_IP2 in {r.address for r in dns.resolver.resolve(f"src2.{hostname.hostname}")}