82 lines
2.7 KiB
Python
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]
|