Files

150 lines
4.5 KiB
Python

import json
from types import SimpleNamespace
import conftest as k8s_conftest
import pytest
def _pod(name: str, phase: str, ready: bool) -> dict:
return {
"metadata": {"name": name},
"status": {
"phase": phase,
"conditions": [
{
"type": "Ready",
"status": "True" if ready else "False",
}
],
},
}
def test_wait_for_replacement_pod_ignores_old_and_pending_pods(monkeypatch):
old_pod = "sgl-router-old"
new_pod = "sgl-router-new"
responses = iter(
[
[_pod(old_pod, "Running", True)],
[
_pod(old_pod, "Running", True),
_pod(new_pod, "Running", True),
],
[_pod(new_pod, "Pending", False)],
[_pod(new_pod, "Running", True)],
]
)
calls = []
def fake_kubectl(*args, **kwargs):
calls.append((args, kwargs))
return SimpleNamespace(stdout=json.dumps({"items": next(responses)}))
monkeypatch.setattr(k8s_conftest, "_kubectl", fake_kubectl)
monkeypatch.setattr(k8s_conftest.time, "sleep", lambda _: None)
replacement = k8s_conftest._wait_for_replacement_pod_ready(
old_pod,
"app=sgl-router",
timeout=5,
interval=0,
)
assert replacement == new_pod
assert len(calls) == 4
assert all("-o" in args and "json" in args for args, _ in calls)
def _stub_kubectl(monkeypatch, payload):
"""Point every conftest helper at a canned kubectl response. `payload` is
the object kubectl would have printed."""
monkeypatch.setattr(
k8s_conftest,
"_kubectl",
lambda *args, **kwargs: SimpleNamespace(stdout=json.dumps(payload)),
)
def test_pod_names_excludes_terminating_pods(monkeypatch):
terminating = _pod("sgl-router-old", "Running", True)
terminating["metadata"]["deletionTimestamp"] = "2026-01-01T00:00:00Z"
_stub_kubectl(
monkeypatch,
{"items": [terminating, _pod("sgl-router-new", "Running", True)]},
)
assert k8s_conftest._pod_names("app=sgl-router") == ["sgl-router-new"]
def test_pods_returns_empty_on_a_failed_kubectl(monkeypatch):
"""The poll loops call this with `check=False` precisely because the API
server can be briefly unavailable mid-rollout; a non-zero return must read
as "nothing observed", not raise out of the loop."""
monkeypatch.setattr(
k8s_conftest,
"_kubectl",
lambda *args, **kwargs: SimpleNamespace(stdout="", returncode=1),
)
assert k8s_conftest._pods("app=sgl-router", check=False) == []
def test_pods_tolerates_empty_stdout(monkeypatch):
monkeypatch.setattr(
k8s_conftest,
"_kubectl",
lambda *args, **kwargs: SimpleNamespace(stdout=""),
)
assert k8s_conftest._pods("app=sgl-router") == []
def test_container_restart_count_reads_the_named_container(monkeypatch):
_stub_kubectl(
monkeypatch,
{
"status": {
"containerStatuses": [
{"name": "sidecar", "restartCount": 9},
{"name": "router", "restartCount": 3},
]
}
},
)
assert k8s_conftest._container_restart_count("sgl-router-0", "router") == 3
def test_container_restart_count_rejects_a_missing_container(monkeypatch):
"""`containerStatuses` lags during a restart, so the absent case is a real
state — and the drain test reads its whole timing floor off this number.
Returning 0 there would silently read as "never restarted"."""
_stub_kubectl(monkeypatch, {"status": {"containerStatuses": []}})
with pytest.raises(AssertionError, match="router"):
k8s_conftest._container_restart_count("sgl-router-0", "router")
def test_pod_ready_condition_reports_the_ready_status(monkeypatch):
_stub_kubectl(
monkeypatch,
{
"status": {
"conditions": [
{"type": "Initialized", "status": "True"},
{"type": "Ready", "status": "False"},
]
}
},
)
assert k8s_conftest._pod_ready_condition("sgl-router-0") == "False"
def test_pod_ready_condition_is_unknown_before_the_condition_exists(monkeypatch):
"""A pod whose Ready condition has not been written yet must read as
"Unknown" rather than crash the drain test's diagnostic logging."""
_stub_kubectl(monkeypatch, {"status": {"conditions": []}})
assert k8s_conftest._pod_ready_condition("sgl-router-0") == "Unknown"