From bba7f4db1d321d39ddeb867ef188c126d2591efc Mon Sep 17 00:00:00 2001 From: John McCormick Date: Tue, 22 Sep 2026 17:12:13 -0400 Subject: [PATCH 1/3] update dns result parsing to ignore transient issues --- src/connectedk8s/azext_connectedk8s/_utils.py | 38 +++++++++++++++---- .../tests/unittests/test_utils_.py | 26 +++++++++++++ 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/_utils.py b/src/connectedk8s/azext_connectedk8s/_utils.py index aaeeeadfbcc..2adcc333814 100644 --- a/src/connectedk8s/azext_connectedk8s/_utils.py +++ b/src/connectedk8s/azext_connectedk8s/_utils.py @@ -1045,16 +1045,38 @@ def check_cluster_DNS( if consts.DNS_Check_Result_String not in dns_check_log: return consts.Diagnostic_Check_Incomplete, storage_space_available formatted_dns_log = dns_check_log.replace("\t", "") + failure_markers = ( + "NXDOMAIN", + "SERVFAIL", + "connection timed out", + "no servers could be reached", + "communications error", + "timed out", + ) + last_failure_position = max( + formatted_dns_log.rfind(marker) for marker in failure_markers + ) + successful_resolution_matches = list( + re.finditer( + r"\bName:\s*kubernetes\.default(?:\.\S+)?\s+" + r"Address(?:es)?:\s+\S+", + formatted_dns_log, + flags=re.IGNORECASE, + ) + ) + last_success_position = ( + successful_resolution_matches[-1].end() + if successful_resolution_matches + else -1 + ) + dns_check_failed = ( + last_failure_position >= 0 + and last_success_position < last_failure_position + ) + # Validating if DNS is working or not and displaying proper result # These are standard error strings from DNS tools (nslookup/dig) indicating resolution failures - if ( # pylint: disable=too-many-boolean-expressions - "NXDOMAIN" in formatted_dns_log - or "SERVFAIL" in formatted_dns_log - or "connection timed out" in formatted_dns_log - or "no servers could be reached" in formatted_dns_log - or "communications error" in formatted_dns_log - or "timed out" in formatted_dns_log - ): + if dns_check_failed: dns_error = errors.DNS_TIMEOUT # Prefer specific DNS responses when one log contains multiple signals. if "NXDOMAIN" in formatted_dns_log: diff --git a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py index 4bdee1076b4..b05748d0878 100644 --- a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py +++ b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py @@ -1637,6 +1637,32 @@ def test_passed(self): assert result == "Passed" assert diag == [] + def test_transient_timeout_followed_by_success_passes(self): + log = """\ +DNS Result:;; communications error to 10.0.0.10#53: timed out +Server: 10.0.0.10 +Address: 10.0.0.10#53 + +Name: kubernetes.default.svc.cluster.local +Address: 10.0.0.1""" + + result, diag = self._run(log) + + assert result == consts.Diagnostic_Check_Passed + assert diag == [] + + def test_timeout_after_success_fails(self): + log = """\ +DNS Result: +Name: kubernetes.default.svc.cluster.local +Address: 10.0.0.1 +;; communications error to 10.0.0.10#53: timed out""" + + result, diag = self._run(log) + + assert result == consts.Diagnostic_Check_Failed + assert diag[0].startswith("[AZK8S0302]") + @pytest.mark.parametrize( "dns_log, expected_code, expected_fault_type", [ From b889f4570f7e9f40daf0da240a70311577786599 Mon Sep 17 00:00:00 2001 From: John McCormick Date: Tue, 22 Sep 2026 18:32:46 -0400 Subject: [PATCH 2/3] ruff --- src/connectedk8s/azext_connectedk8s/_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/_utils.py b/src/connectedk8s/azext_connectedk8s/_utils.py index 2adcc333814..d1ecfb2c7ae 100644 --- a/src/connectedk8s/azext_connectedk8s/_utils.py +++ b/src/connectedk8s/azext_connectedk8s/_utils.py @@ -1070,8 +1070,7 @@ def check_cluster_DNS( else -1 ) dns_check_failed = ( - last_failure_position >= 0 - and last_success_position < last_failure_position + last_failure_position >= 0 and last_success_position < last_failure_position ) # Validating if DNS is working or not and displaying proper result From 8e309701829db6ca6b684452cc38e509e7ebf3db Mon Sep 17 00:00:00 2001 From: John McCormick Date: Thu, 24 Sep 2026 10:01:52 -0400 Subject: [PATCH 3/3] reject invalid IP and take last error --- src/connectedk8s/azext_connectedk8s/_utils.py | 81 ++++++++++--------- .../tests/unittests/test_utils_.py | 36 +++++++++ 2 files changed, 78 insertions(+), 39 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/_utils.py b/src/connectedk8s/azext_connectedk8s/_utils.py index d1ecfb2c7ae..f0b31540852 100644 --- a/src/connectedk8s/azext_connectedk8s/_utils.py +++ b/src/connectedk8s/azext_connectedk8s/_utils.py @@ -5,6 +5,7 @@ from __future__ import annotations import contextlib +import ipaddress import json import os import re @@ -1046,51 +1047,53 @@ def check_cluster_DNS( return consts.Diagnostic_Check_Incomplete, storage_space_available formatted_dns_log = dns_check_log.replace("\t", "") failure_markers = ( - "NXDOMAIN", - "SERVFAIL", - "connection timed out", - "no servers could be reached", - "communications error", - "timed out", - ) - last_failure_position = max( - formatted_dns_log.rfind(marker) for marker in failure_markers - ) - successful_resolution_matches = list( - re.finditer( - r"\bName:\s*kubernetes\.default(?:\.\S+)?\s+" - r"Address(?:es)?:\s+\S+", - formatted_dns_log, - flags=re.IGNORECASE, - ) - ) - last_success_position = ( - successful_resolution_matches[-1].end() - if successful_resolution_matches - else -1 - ) + ("NXDOMAIN", errors.DNS_NXDOMAIN), + ("SERVFAIL", errors.DNS_SERVFAIL), + ("connection timed out", errors.DNS_TIMEOUT), + ("no servers could be reached", errors.DNS_NO_SERVERS_REACHABLE), + ("communications error", errors.DNS_COMMUNICATIONS_ERROR), + ("timed out", errors.DNS_TIMEOUT), + ) + latest_failure = max( + ( + (match.start(), dns_error) + for marker, dns_error in failure_markers + for match in re.finditer( + re.escape(marker), formatted_dns_log, flags=re.IGNORECASE + ) + ), + default=None, + key=lambda failure: failure[0], + ) + resolution_matches = re.finditer( + r"\bName:\s*kubernetes\.default(?:\.\S+)?[ \r\n]+" + r"Address(?:es)?:[ ]*(?P[^\r\n]+)", + formatted_dns_log, + flags=re.IGNORECASE, + ) + successful_resolution_positions = [] + for resolution_match in resolution_matches: + addresses = re.split(r"[\s,]+", resolution_match.group("addresses")) + for address in addresses: + try: + ipaddress.ip_address(address) + successful_resolution_positions.append(resolution_match.end()) + break + except ValueError: + pass + + last_success_position = max(successful_resolution_positions, default=-1) + if latest_failure is None and last_success_position < 0: + return consts.Diagnostic_Check_Incomplete, storage_space_available dns_check_failed = ( - last_failure_position >= 0 and last_success_position < last_failure_position + latest_failure is not None and last_success_position < latest_failure[0] ) # Validating if DNS is working or not and displaying proper result # These are standard error strings from DNS tools (nslookup/dig) indicating resolution failures if dns_check_failed: - dns_error = errors.DNS_TIMEOUT - # Prefer specific DNS responses when one log contains multiple signals. - if "NXDOMAIN" in formatted_dns_log: - dns_error = errors.DNS_NXDOMAIN - elif "SERVFAIL" in formatted_dns_log: - dns_error = errors.DNS_SERVFAIL - elif "no servers could be reached" in formatted_dns_log: - dns_error = errors.DNS_NO_SERVERS_REACHABLE - elif ( - "connection timed out" in formatted_dns_log - or "timed out" in formatted_dns_log - ): - dns_error = errors.DNS_TIMEOUT - elif "communications error" in formatted_dns_log: - dns_error = errors.DNS_COMMUNICATIONS_ERROR + assert latest_failure is not None + dns_error = latest_failure[1] details = ( "Review Kubernetes DNS debugging guidance at " diff --git a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py index b05748d0878..4138ca9ed12 100644 --- a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py +++ b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py @@ -1663,6 +1663,42 @@ def test_timeout_after_success_fails(self): assert result == consts.Diagnostic_Check_Failed assert diag[0].startswith("[AZK8S0302]") + def test_latest_failure_is_reported_as_root_cause(self): + log = """\ +DNS Result: server returned SERVFAIL +** server can't find kubernetes.default.svc.cluster.local: NXDOMAIN""" + + result, diag = self._run(log) + + assert result == consts.Diagnostic_Check_Failed + assert diag[0].startswith("[AZK8S0301]") + + @pytest.mark.parametrize("address", ["10.96.0.1", "2001:db8::1"]) + def test_valid_ip_address_passes(self, address): + log = ( + "DNS Result:\n" + "Name: kubernetes.default.svc.cluster.local\n" + f"Address: {address}" + ) + + result, diag = self._run(log) + + assert result == consts.Diagnostic_Check_Passed + assert diag == [] + + @pytest.mark.parametrize("address", ["not-an-ip", "10.96.0.999", "10.96.0.1#53"]) + def test_invalid_resolved_address_is_incomplete(self, address): + log = ( + "DNS Result:\n" + "Name: kubernetes.default.svc.cluster.local\n" + f"Address: {address}" + ) + + result, diag = self._run(log) + + assert result == consts.Diagnostic_Check_Incomplete + assert diag == [] + @pytest.mark.parametrize( "dns_log, expected_code, expected_fault_type", [