From efbc854f6e69f7d263170a30dcc731eeb1a05ffb Mon Sep 17 00:00:00 2001 From: Alex Zgabur Date: Tue, 2 Dec 2025 01:19:30 +0200 Subject: [PATCH] feat(dns): Add smarter wait for changing dns zone asserts Signed-off-by: Alex Zgabur --- .../load_balanced/test_change_default_geo.py | 8 ++-- .../load_balanced/test_change_strategy.py | 8 ++-- testsuite/utils.py | 42 +++++++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/testsuite/tests/multicluster/load_balanced/test_change_default_geo.py b/testsuite/tests/multicluster/load_balanced/test_change_default_geo.py index 4eacdbc0..7391607f 100644 --- a/testsuite/tests/multicluster/load_balanced/test_change_default_geo.py +++ b/testsuite/tests/multicluster/load_balanced/test_change_default_geo.py @@ -1,10 +1,9 @@ """Test for modification of default geolocation in DNSPolicy""" -from time import sleep - import pytest import dns.resolver from testsuite.config import settings +from testsuite.utils import wait_for_dns pytestmark = [pytest.mark.multicluster] @@ -28,5 +27,6 @@ def test_change_default_geo(hostname, gateway, gateway2, dns_policy, dns_policy2 dns_policy2.apply() dns_policy2.wait_for_ready() - sleep(300) # wait for DNS propagation on providers - assert resolver.resolve(hostname.hostname)[0].address == gateway2.external_ip().split(":")[0] + answer = wait_for_dns(hostname.hostname, gateway2.external_ip().split(":")[0], resolver=resolver) + assert gateway2.external_ip().split(":")[0] in answer.addresses() + assert gateway.external_ip().split(":")[0] not in answer.addresses() diff --git a/testsuite/tests/multicluster/load_balanced/test_change_strategy.py b/testsuite/tests/multicluster/load_balanced/test_change_strategy.py index 66e81611..f6a01da8 100644 --- a/testsuite/tests/multicluster/load_balanced/test_change_strategy.py +++ b/testsuite/tests/multicluster/load_balanced/test_change_strategy.py @@ -1,11 +1,10 @@ """Test changing load-balancing strategy in DNSPolicy""" -from time import sleep - import pytest import dns.resolver from testsuite.kuadrant.policy.dns import has_record_condition +from testsuite.utils import wait_for_dns pytestmark = [pytest.mark.multicluster] @@ -31,5 +30,6 @@ def test_change_lb_strategy(hostname, gateway, gateway2, dns_policy2, dns_server ) ), f"DNSPolicy did not reach expected record status, instead it was: {dns_policy2.model.status.recordConditions}" - sleep(300) # wait for DNS propagation on providers - assert resolver.resolve(hostname.hostname)[0].address == gateway.external_ip().split(":")[0] + answer = wait_for_dns(hostname.hostname, gateway.external_ip().split(":")[0], resolver=resolver) + assert gateway.external_ip().split(":")[0] in answer.addresses() + assert gateway2.external_ip().split(":")[0] not in answer.addresses() diff --git a/testsuite/utils.py b/testsuite/utils.py index c2a2b205..1d5a75a8 100644 --- a/testsuite/utils.py +++ b/testsuite/utils.py @@ -15,6 +15,7 @@ from typing import Dict, Union from urllib.parse import urlparse, ParseResult +import backoff import dns.resolver from weakget import weakget @@ -227,3 +228,44 @@ def domain_match(first: str, second: str): if second[0] == "*": return second[2:] == ".".join(first.split(".")[1:]) return False + + +def wait_for_dns( + hostname: str, + value: str, + not_in: bool = False, + resolver: dns.resolver.Resolver = dns.resolver.get_default_resolver(), +) -> dns.resolver.HostAnswers: + """This method returns dns answer from dns.resolver.resolve_name(hostname) but has backoff functionality + to retry on common errors such as NXDOMAIN or NoAnswer. Additionally retries until specified value is in the DNS + answer. This can be used when a change to a zone is expected and to assert the change. + + :param hostname: Hostname for DNS query + :param value: Expected value in answer + :param not_in: Default false; If true, the method will wait until the value is not in the DNS answer + :param resolver: Default dns.resolver; Specify your own resolver class + + :return: HostAnswers class containing answers for A and AAAA queries + """ + # pylint: disable=unnecessary-lambda-assignment + # it is indeed necessary so it can be overridden + backoff_function = lambda answer: value not in answer.addresses() + if not_in: + backoff_function = lambda answer: value in answer.addresses() + + @backoff.on_predicate( + backoff.runtime, + backoff_function, + value=lambda answer: min(map(lambda rrset: rrset.ttl, answer.values())), + max_time=300, + ) + @backoff.on_exception( + backoff.constant, + (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN, dns.resolver.NoNameservers), + interval=5, + max_time=300, + ) + def _assert_dns(): + return resolver.resolve_name(hostname) + + return _assert_dns()