Skip to content

Commit 45808f7

Browse files
committed
fix(k8s): fail fast in /info when the API server does not answer
The /info probe inherited the 60 s K8S_HTTP_TIMEOUT. AppAPI waits 5 s for /info in its daemon checks, so an unreachable API server surfaced as "Cannot reach HaRP" after a 5 s hang on every admin page load. The probe now has its own 3 s timeout and /info reports why the cluster is unreachable in a new "error" field. Signed-off-by: Oleksandr Piskun <oleksandr2088@icloud.com>
1 parent 93afc12 commit 45808f7

1 file changed

Lines changed: 43 additions & 6 deletions

File tree

‎haproxy_agent.py‎

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
K8S_API_SERVER = f"https://{host}:{port}"
5959

6060
K8S_HTTP_TIMEOUT = aiohttp.ClientTimeout(total=60.0)
61+
# `/info` probes the API server with its own short timeout: AppAPI waits 5 s for `/info` in its daemon checks, so a
62+
# probe that inherits K8S_HTTP_TIMEOUT makes an unreachable API server look like an unreachable HaRP.
63+
K8S_PROBE_TIMEOUT = aiohttp.ClientTimeout(total=3.0)
6164
_k8s_session: aiohttp.ClientSession | None = None
6265
K8S_NAME_MAX_LENGTH = 63
6366
# Set up the logging configuration
@@ -832,12 +835,10 @@ async def get_info(request: web.Request):
832835
k8s_status: dict[str, Any] = {"enabled": K8S_ENABLED}
833836
if K8S_ENABLED:
834837
k8s_status["api_server"] = K8S_API_SERVER or ""
835-
try:
836-
_ensure_k8s_configured()
837-
status, _, _ = await _k8s_request("GET", "/api")
838-
k8s_status["reachable"] = status == 200
839-
except Exception:
840-
k8s_status["reachable"] = False
838+
reachable, error = await _k8s_probe()
839+
k8s_status["reachable"] = reachable
840+
if error:
841+
k8s_status["error"] = error
841842

842843
return web.json_response({
843844
"version": HARP_VERSION,
@@ -2272,6 +2273,42 @@ async def _k8s_request(
22722273
raise web.HTTPServiceUnavailable(text="Error communicating with Kubernetes API") from e
22732274

22742275

2276+
async def _k8s_probe() -> tuple[bool, str]:
2277+
"""Check whether the Kubernetes API server answers; returns ``(reachable, error)``.
2278+
2279+
Used by ``/info`` only. Unlike ``_k8s_request`` it fails fast (K8S_PROBE_TIMEOUT) and keeps the reason, so the
2280+
AppAPI daemon check can tell a broken cluster connection from a broken HaRP one.
2281+
"""
2282+
try:
2283+
_ensure_k8s_configured()
2284+
except web.HTTPServiceUnavailable as e:
2285+
return False, e.text or "Kubernetes backend is not configured."
2286+
url = f"{K8S_API_SERVER}/api"
2287+
headers = {"Authorization": f"Bearer {_get_k8s_token()}", "Accept": "application/json"}
2288+
try:
2289+
session = _get_k8s_session() # may raise on a broken HP_K8S_CA_FILE
2290+
async with session.get(url, headers=headers, timeout=K8S_PROBE_TIMEOUT) as resp:
2291+
if resp.status == 200:
2292+
return True, ""
2293+
if resp.status in (401, 403):
2294+
error = (
2295+
f"Kubernetes API server answered HTTP {resp.status}; check the bearer token "
2296+
"(HP_K8S_BEARER_TOKEN or HP_K8S_BEARER_TOKEN_FILE)."
2297+
)
2298+
else:
2299+
error = f"Kubernetes API server answered HTTP {resp.status}."
2300+
except TimeoutError:
2301+
error = f"Kubernetes API server did not answer within {K8S_PROBE_TIMEOUT.total:g}s (DNS, connect or request)."
2302+
except aiohttp.ClientSSLError as e:
2303+
error = f"TLS error connecting to the Kubernetes API server: {e}"
2304+
except aiohttp.ClientError as e:
2305+
error = f"Cannot connect to the Kubernetes API server: {e}"
2306+
except Exception as e: # `/info` must always answer
2307+
error = f"Kubernetes API probe failed: {e}"
2308+
LOGGER.warning("Kubernetes API probe (%s) failed: %s", url, error)
2309+
return False, error
2310+
2311+
22752312
def _k8s_parse_env(env_list: list[str]) -> list[dict[str, str]]:
22762313
"""Convert ['KEY=VALUE', ...] to Kubernetes env entries."""
22772314
result: list[dict[str, str]] = []

0 commit comments

Comments
 (0)