Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 47 additions & 23 deletions src/connectedk8s/azext_connectedk8s/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from __future__ import annotations

import contextlib
import ipaddress
import json
import os
import re
Expand Down Expand Up @@ -1045,31 +1046,54 @@ 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", 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<addresses>[^\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 = (
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 ( # 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
):
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
if dns_check_failed:
assert latest_failure is not None
dns_error = latest_failure[1]

details = (
"Review Kubernetes DNS debugging guidance at "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1637,6 +1637,68 @@ 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]")

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",
[
Expand Down
Loading