ariadne/ariadne/k8s/pods.py

82 lines
2.7 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any
from urllib.parse import quote
from .client import get_json
@dataclass(frozen=True)
class PodRef:
name: str
namespace: str
node: str | None = None
class PodSelectionError(RuntimeError):
pass
def _parse_start_time(value: str | None) -> float:
if not value:
return 0.0
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
return 0.0
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return parsed.timestamp()
def _is_ready(pod: dict[str, Any]) -> bool:
status = pod.get("status") if isinstance(pod.get("status"), dict) else {}
if status.get("phase") != "Running":
return False
conditions = status.get("conditions")
if not isinstance(conditions, list):
return False
for cond in conditions:
if not isinstance(cond, dict):
continue
if cond.get("type") == "Ready":
return cond.get("status") == "True"
return False
def list_pods(namespace: str, label_selector: str) -> list[dict[str, Any]]:
namespace = (namespace or "").strip()
if not namespace:
raise PodSelectionError("pod namespace missing")
label_selector = (label_selector or "").strip()
selector = quote(label_selector, safe=",.-")
payload = get_json(f"/api/v1/namespaces/{namespace}/pods?labelSelector={selector}")
items = payload.get("items") if isinstance(payload.get("items"), list) else []
return [item for item in items if isinstance(item, dict)]
def select_pod(namespace: str, label_selector: str) -> PodRef:
pods = list_pods(namespace, label_selector)
candidates: list[tuple[float, PodRef]] = []
for pod in pods:
metadata = pod.get("metadata") if isinstance(pod.get("metadata"), dict) else {}
if metadata.get("deletionTimestamp"):
continue
name = metadata.get("name")
if not isinstance(name, str) or not name.strip():
continue
if not _is_ready(pod):
continue
status = pod.get("status") if isinstance(pod.get("status"), dict) else {}
started_at = _parse_start_time(status.get("startTime"))
node_name = status.get("nodeName") if isinstance(status.get("nodeName"), str) else None
candidates.append((started_at, PodRef(name=name, namespace=namespace, node=node_name)))
if not candidates:
raise PodSelectionError(f"no ready pods found for {namespace} {label_selector}")
candidates.sort(key=lambda item: item[0], reverse=True)
return candidates[0][1]