|
58 | 58 | K8S_API_SERVER = f"https://{host}:{port}" |
59 | 59 |
|
60 | 60 | 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) |
61 | 64 | _k8s_session: aiohttp.ClientSession | None = None |
62 | 65 | K8S_NAME_MAX_LENGTH = 63 |
63 | 66 | # Set up the logging configuration |
@@ -832,12 +835,10 @@ async def get_info(request: web.Request): |
832 | 835 | k8s_status: dict[str, Any] = {"enabled": K8S_ENABLED} |
833 | 836 | if K8S_ENABLED: |
834 | 837 | 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 |
841 | 842 |
|
842 | 843 | return web.json_response({ |
843 | 844 | "version": HARP_VERSION, |
@@ -2272,6 +2273,42 @@ async def _k8s_request( |
2272 | 2273 | raise web.HTTPServiceUnavailable(text="Error communicating with Kubernetes API") from e |
2273 | 2274 |
|
2274 | 2275 |
|
| 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 | + |
2275 | 2312 | def _k8s_parse_env(env_list: list[str]) -> list[dict[str, str]]: |
2276 | 2313 | """Convert ['KEY=VALUE', ...] to Kubernetes env entries.""" |
2277 | 2314 | result: list[dict[str, str]] = [] |
|
0 commit comments