mirror of
https://github.com/dredx/prole.git
synced 2026-09-27 03:04:30 +00:00
- detect local k3s node kubeconfig and skip kubectx/use-context mutation when already targeting local API\n- add configurable KUBE_CONTEXT_NAME resolution with compatibility fallbacks and switch only when required\n- update init scripts to use ensure_kube_context helper naming\n- broaden monitoring eligibility to discovered /synology/d### mounts so /synology/d004 qualifies\n- add focused kube-context and topology tests covering local/remote and read-only kubeconfig cases Co-authored-by: Junie <junie@jetbrains.com>
247 lines
8.9 KiB
Python
247 lines
8.9 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
|
|
from knoe.core import topology as topology_mod
|
|
|
|
|
|
def _completed(args: list[str], rc: int = 0, out: str = "", err: str = "") -> subprocess.CompletedProcess[str]:
|
|
return subprocess.CompletedProcess(args=args, returncode=rc, stdout=out, stderr=err)
|
|
|
|
|
|
def test_extract_inventory_from_marked_log_payload() -> None:
|
|
payload = {
|
|
"nodeName": "myrddin",
|
|
"cpuModel": "Intel",
|
|
"filesystems": [{"mountpoint": "/synology/d001", "availableBytes": 123}],
|
|
}
|
|
text = (
|
|
"hello\n"
|
|
f"{topology_mod.INVENTORY_BEGIN_MARKER}\n"
|
|
f"{json.dumps(payload)}\n"
|
|
f"{topology_mod.INVENTORY_END_MARKER}\n"
|
|
)
|
|
parsed = topology_mod._extract_inventory_from_log(text)
|
|
assert parsed == payload
|
|
|
|
|
|
def test_merge_node_monitoring_eligible_with_synology_d004() -> None:
|
|
k8s_node = {
|
|
"name": "merlin",
|
|
"roles": ("worker",),
|
|
"labels": {
|
|
"kubernetes.io/arch": "amd64",
|
|
"kubernetes.io/os": "linux",
|
|
},
|
|
"taints": (),
|
|
"ready": True,
|
|
"internal_ip": "10.0.0.11",
|
|
"architecture": "amd64",
|
|
"operating_system": "linux",
|
|
"allocatable_cpu": "4",
|
|
"allocatable_memory_bytes": 16 * 1024 * 1024 * 1024,
|
|
"allocatable_ephemeral_storage_bytes": 100 * 1024 * 1024 * 1024,
|
|
}
|
|
inventory = {
|
|
"filesystems": [
|
|
{
|
|
"device": "/dev/sdb",
|
|
"mountpoint": "/synology/d004",
|
|
"fstype": "ext4",
|
|
"sizeBytes": 512 * 1024 * 1024 * 1024,
|
|
"usedBytes": 128 * 1024 * 1024 * 1024,
|
|
"availableBytes": 384 * 1024 * 1024 * 1024,
|
|
"transport": "sata",
|
|
"rotational": True,
|
|
"storageClass": "synology",
|
|
}
|
|
]
|
|
}
|
|
|
|
merged = topology_mod._merge_node(
|
|
k8s_node,
|
|
inventory=inventory,
|
|
required_monitoring_mountpoints=("/synology/d001",),
|
|
monitoring_min_free_bytes=200 * 1024 * 1024 * 1024,
|
|
)
|
|
|
|
assert merged.capabilities.matching_mountpoints == ("/synology/d004",)
|
|
assert merged.capabilities.monitoring_eligible is True
|
|
|
|
|
|
def test_discover_cluster_topology_partial_results_and_xml(tmp_path, monkeypatch) -> None:
|
|
nodes_payload = {
|
|
"items": [
|
|
{
|
|
"metadata": {
|
|
"name": "myrddin",
|
|
"labels": {
|
|
"kubernetes.io/arch": "amd64",
|
|
"kubernetes.io/os": "linux",
|
|
"node-role.kubernetes.io/control-plane": "",
|
|
},
|
|
},
|
|
"spec": {},
|
|
"status": {
|
|
"conditions": [{"type": "Ready", "status": "True"}],
|
|
"addresses": [{"type": "InternalIP", "address": "10.0.0.10"}],
|
|
"allocatable": {
|
|
"cpu": "4",
|
|
"memory": "16Gi",
|
|
"ephemeral-storage": "100Gi",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"metadata": {
|
|
"name": "merlin",
|
|
"labels": {
|
|
"kubernetes.io/arch": "amd64",
|
|
"kubernetes.io/os": "linux",
|
|
},
|
|
},
|
|
"spec": {},
|
|
"status": {
|
|
"conditions": [{"type": "Ready", "status": "True"}],
|
|
"addresses": [{"type": "InternalIP", "address": "10.0.0.11"}],
|
|
"allocatable": {
|
|
"cpu": "4",
|
|
"memory": "16Gi",
|
|
"ephemeral-storage": "100Gi",
|
|
},
|
|
},
|
|
},
|
|
]
|
|
}
|
|
pods_payload = {
|
|
"items": [
|
|
{"metadata": {"name": "collector-myrddin"}, "spec": {"nodeName": "myrddin"}},
|
|
{"metadata": {"name": "collector-merlin"}, "spec": {"nodeName": "merlin"}},
|
|
]
|
|
}
|
|
|
|
def fake_kubectl_json(_base_cmd, args, **_kwargs):
|
|
if args[:2] == ["get", "nodes"]:
|
|
return nodes_payload
|
|
if "pods" in args:
|
|
return pods_payload
|
|
raise AssertionError(f"unexpected kubectl json args: {args}")
|
|
|
|
def fake_run_kubectl(_base_cmd, args, **_kwargs):
|
|
if args[:4] == ["-n", "kube-system", "apply", "-f"]:
|
|
return _completed(args)
|
|
if args[:4] == ["-n", "kube-system", "logs", "collector-myrddin"]:
|
|
body = {
|
|
"nodeName": "myrddin",
|
|
"cpuModel": "Intel i7",
|
|
"physicalCores": 6,
|
|
"logicalCores": 12,
|
|
"memoryBytes": 17179869184,
|
|
"filesystems": [
|
|
{
|
|
"device": "/dev/nvme0n1p1",
|
|
"mountpoint": "/synology/d001",
|
|
"fstype": "ext4",
|
|
"sizeBytes": 1000,
|
|
"usedBytes": 100,
|
|
"availableBytes": 900,
|
|
"transport": "nvme",
|
|
"rotational": False,
|
|
"storageClass": "nvme",
|
|
}
|
|
],
|
|
}
|
|
body_json = json.dumps(body, separators=(",", ":"), sort_keys=True)
|
|
text = (
|
|
f"{topology_mod.INVENTORY_BEGIN_MARKER}\n"
|
|
f"{body_json}\n"
|
|
f"{topology_mod.INVENTORY_END_MARKER}\n"
|
|
)
|
|
return _completed(args, out=text)
|
|
if args[:4] == ["-n", "kube-system", "logs", "collector-merlin"]:
|
|
return _completed(args, out="collector still warming up\n")
|
|
if args[:4] == ["-n", "kube-system", "delete", "daemonset"]:
|
|
return _completed(args)
|
|
raise AssertionError(f"unexpected kubectl run args: {args}")
|
|
|
|
clock = {"now": 0.0}
|
|
|
|
def fake_monotonic():
|
|
clock["now"] += 1.0
|
|
return clock["now"]
|
|
|
|
monkeypatch.setattr(topology_mod, "_kubectl_json", fake_kubectl_json)
|
|
monkeypatch.setattr(topology_mod, "_run_kubectl", fake_run_kubectl)
|
|
monkeypatch.setattr(topology_mod.time, "monotonic", fake_monotonic)
|
|
monkeypatch.setattr(topology_mod.time, "sleep", lambda _s: None)
|
|
|
|
result = topology_mod.discover_cluster_topology(
|
|
kubectl_base_cmd=["kubectl"],
|
|
topology_root=tmp_path / "topology",
|
|
config=topology_mod.TopologyDiscoveryConfig(
|
|
timeout_seconds=1,
|
|
poll_interval_seconds=0.01,
|
|
monitoring_min_free_bytes=100,
|
|
),
|
|
)
|
|
|
|
assert result.topology.collection_status == "partial"
|
|
assert result.reported_nodes == ("myrddin",)
|
|
assert result.missing_nodes == ("merlin",)
|
|
|
|
myrddin = next(node for node in result.topology.nodes if node.name == "myrddin")
|
|
merlin = next(node for node in result.topology.nodes if node.name == "merlin")
|
|
assert myrddin.capabilities.monitoring_eligible is True
|
|
assert merlin.capabilities.monitoring_eligible is None
|
|
assert "topology_unavailable" in merlin.capabilities.reasons
|
|
|
|
assert (tmp_path / "topology" / "cluster.xml").is_file()
|
|
assert (tmp_path / "topology" / "nodes" / "myrddin.xml").is_file()
|
|
assert (tmp_path / "topology" / "nodes" / "merlin.xml").is_file()
|
|
|
|
|
|
def test_discover_cluster_topology_failed_collector_keeps_k8s_rows(tmp_path, monkeypatch) -> None:
|
|
nodes_payload = {
|
|
"items": [
|
|
{
|
|
"metadata": {
|
|
"name": "pi",
|
|
"labels": {"kubernetes.io/arch": "arm64", "kubernetes.io/os": "linux"},
|
|
},
|
|
"spec": {},
|
|
"status": {
|
|
"conditions": [{"type": "Ready", "status": "True"}],
|
|
"addresses": [{"type": "InternalIP", "address": "10.0.0.12"}],
|
|
"allocatable": {"cpu": "2", "memory": "4Gi", "ephemeral-storage": "20Gi"},
|
|
},
|
|
}
|
|
]
|
|
}
|
|
|
|
def fake_kubectl_json(_base_cmd, args, **_kwargs):
|
|
if args[:2] == ["get", "nodes"]:
|
|
return nodes_payload
|
|
raise AssertionError(f"unexpected kubectl json args: {args}")
|
|
|
|
def fake_run_kubectl(_base_cmd, args, **_kwargs):
|
|
if args[:4] == ["-n", "kube-system", "apply", "-f"]:
|
|
return _completed(args, rc=1, err="image pull failed")
|
|
return _completed(args)
|
|
|
|
monkeypatch.setattr(topology_mod, "_kubectl_json", fake_kubectl_json)
|
|
monkeypatch.setattr(topology_mod, "_run_kubectl", fake_run_kubectl)
|
|
|
|
result = topology_mod.discover_cluster_topology(
|
|
kubectl_base_cmd=["kubectl"],
|
|
topology_root=tmp_path / "topology",
|
|
)
|
|
|
|
assert result.topology.collection_status == "failed"
|
|
assert result.topology.discovered_nodes == 1
|
|
node = result.topology.nodes[0]
|
|
assert node.name == "pi"
|
|
assert node.capabilities.monitoring_eligible is None
|
|
assert "topology_unavailable" in node.capabilities.reasons
|
|
assert (tmp_path / "topology" / "cluster.xml").is_file()
|