prole/installer/core/policy.py
chrisfu 133b719479 Align CNPG bootstrap placement with Ansible policy
Prefer infra-managed manifests for bootstrap; reconcile CNPG instances based on Ready+schedulable labeled db nodes; update manifests to use node-role affinity + anti-affinity; add policy/tests and config touch-ups (incl. prole.cfg).
2026-03-18 22:06:41 -07:00

93 lines
2.7 KiB
Python

from __future__ import annotations
import json
import subprocess
from typing import Sequence
# Central scheduling policy:
# optional_workloads_allowed = ready_schedulable_nodes >= 2
POLICY_CFG_KEY = "OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES"
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
def _coerce_int(val: object, default: int) -> int:
try:
s = str(val).strip()
if not s:
return default
return int(s)
except Exception:
return default
def evaluate_optional_workloads_allowed(
*,
kubectl_cmd: Sequence[str] = ("kubectl",),
env: dict | None = None,
min_nodes: int = OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES,
timeout_s: int = 15,
) -> tuple[bool, int, str]:
"""Return (allowed, ready_schedulable_nodes, reason).
Definition:
- ready_schedulable_nodes: nodes that are Ready=True and not spec.unschedulable.
- optional_workloads_allowed: ready_schedulable_nodes >= min_nodes
"""
min_nodes = max(0, _coerce_int(min_nodes, OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES))
try:
res = subprocess.run(
[*kubectl_cmd, "get", "nodes", "-o", "json"],
env=env,
capture_output=True,
text=True,
timeout=timeout_s,
)
except Exception as e:
return (
False,
0,
f"optional_workloads_allowed=False (failed to query nodes: {e})",
)
if res.returncode != 0:
err = (res.stderr or "").strip() or (res.stdout or "").strip()
msg = err if err else f"kubectl exited with code {res.returncode}"
return (
False,
0,
f"optional_workloads_allowed=False (failed to query nodes: {msg})",
)
try:
payload = json.loads(res.stdout or "{}")
except Exception as e:
return (
False,
0,
f"optional_workloads_allowed=False (failed to parse kubectl output: {e})",
)
ready_schedulable = 0
for node in (payload.get("items") or []):
spec = node.get("spec") or {}
if spec.get("unschedulable") is True:
continue
conditions = (node.get("status") or {}).get("conditions") or []
is_ready = any(
(c.get("type") == "Ready" and str(c.get("status")).strip() == "True")
for c in conditions
if isinstance(c, dict)
)
if is_ready:
ready_schedulable += 1
allowed = ready_schedulable >= min_nodes
return (
allowed,
ready_schedulable,
f"optional_workloads_allowed={allowed} (ready_schedulable_nodes={ready_schedulable} min_required={min_nodes})",
)