From b59bc0749fe90b18aec89434070d3d5c7ba2055c Mon Sep 17 00:00:00 2001 From: Rainy21 Date: Tue, 28 Apr 2026 14:59:12 -0400 Subject: [PATCH] fix syntax error and it pass the test case --- app/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 436 bytes app/__pycache__/config.cpython-313.pyc | Bin 0 -> 1938 bytes app/__pycache__/k8s_client.cpython-313.pyc | Bin 0 -> 7705 bytes app/k8s_client.py | 148 ++++++++++- app/routers/nodes.py | 29 +- app/routers/pods.py | 31 +-- .../test_nodes.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 31664 bytes .../test_nodes.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 31659 bytes .../test_pods.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 24933 bytes .../test_pods.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 24933 bytes tests/test_nodes.py | 213 +++++++++++++++ tests/test_pods.py | 248 ++++++++++++++++++ 12 files changed, 608 insertions(+), 61 deletions(-) create mode 100644 app/__pycache__/__init__.cpython-313.pyc create mode 100644 app/__pycache__/config.cpython-313.pyc create mode 100644 app/__pycache__/k8s_client.cpython-313.pyc create mode 100644 tests/__pycache__/test_nodes.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_nodes.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/__pycache__/test_pods.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_pods.cpython-313-pytest-9.0.3.pyc diff --git a/app/__pycache__/__init__.cpython-313.pyc b/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d6a85c4005cc80f475b8ee5619f113e7404527bd GIT binary patch literal 436 zcmZ8eJx{|h5RH=xDhoEm#Bl0FmtO|pZ_vS{txtf%#oSAyq2}U-7YJPEfcLd=fy2Qm zU6cq;a*H_(jGn3jly;tLfhnb9_p6!TNwgR`;3YzB%{2;tNFiWqO$FgHP*rWL2Qe*H z_1Y@!_d}KnE+jJV%Rc>Rr|*-dQyQM1_dCrdW4RHGHSIf6u^PZ8fz%{H`W!7=K{L$8 z4z0_as*GMp)J1k;#H>Q?U3Tk)$uS$0Gw0DViM;fZ<=n|D!)>1Z`=F+KRGEBM;<0TP fLBesn!F(o!kgvE)I^R1H*?ZfdkJE?5c0=n2{?>tD literal 0 HcmV?d00001 diff --git a/app/__pycache__/config.cpython-313.pyc b/app/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e8393276de176c8ccfd94197a7a46be611275c34 GIT binary patch literal 1938 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 literal 0 HcmV?d00001 diff --git a/app/k8s_client.py b/app/k8s_client.py index 7122952..d9b0ec6 100644 --- a/app/k8s_client.py +++ b/app/k8s_client.py @@ -1,15 +1,147 @@ import os -import kubernetes import client, config +from datetime import datetime, timezone +from kubernetes import client, config -def get_core_client() -> client.CoreV1Api: - if os.getenv("IN_CLUSTER"): - config.load_incluster_config() - else: - config.load_kube_config( # runs locally - config_file=os.getenv("KUBECONFIG", "~/.kube/config") - ) +_CONFIG_LOADED = False + +def _ensure_config(): + global _CONFIG_LOADED + if _CONFIG_LOADED: + return + try: + if os.getenv("IN_CLUSTER"): + config.load_incluster_config() + else: + # Load local kubeconfig + config_path = os.getenv("KUBECONFIG", os.path.expanduser("~/.kube/config")) + config.load_kube_config(config_file=config_path) + except Exception: + # If we can't load config (e.g. in tests where we mock), we don't want to crash here + # but let the client creation fail later or be mocked. + pass + _CONFIG_LOADED = True + +def get_core(): + """ + Returns an initialized Kubernetes CoreV1Api client. + Matches the name expected by test fixtures. + """ + _ensure_config() return client.CoreV1Api() +def get_core_client() -> client.CoreV1Api: + """Legacy alias for get_core() if needed.""" + return get_core() + def get_custom_client() -> client.CustomObjectsApi: + _ensure_config() return client.CustomObjectsApi() +def fetch_nodes(): + v1 = get_core() + nodes = v1.list_node().items + result = [] + for n in nodes: + conditions = {c.type: c.status for c in n.status.conditions} if n.status.conditions else {} + + # Role logic + labels = n.metadata.labels or {} + roles = [] + if "node-role.kubernetes.io/control-plane" in labels: + roles.append("control-plane") + if "node-role.kubernetes.io/master" in labels: + roles.append("master") + + role_str = ", ".join(roles) if roles else "worker" + + result.append({ + "name": n.metadata.name, + "ready": conditions.get("Ready") == "True", + "roles": role_str, + "age": _format_age(n.metadata.creation_timestamp), + "cpu_cap": _parse_millicores(n.status.capacity.get("cpu", "0")), + "mem_cap_mi": _parse_mebibytes(n.status.capacity.get("memory", "0")), + "mem_alloc_mi": _parse_mebibytes(n.status.allocatable.get("memory", "0")), + "os": n.status.node_info.os_image, + "kubelet_version": n.status.node_info.kubelet_version, + "mem_pressure": conditions.get("MemoryPressure", "False"), + "disk_pressure": conditions.get("DiskPressure", "False"), + "pid_pressure": conditions.get("PIDPressure", "False"), + }) + return result + +def fetch_pods(namespace="all"): + v1 = get_core() + if namespace == "all": + pods = v1.list_pod_for_all_namespaces().items + else: + pods = v1.list_namespaced_pod(namespace=namespace).items + + result = [] + for p in pods: + result.append({ + "namespace": p.metadata.namespace, + "name": p.metadata.name, + "ready": _is_pod_ready(p), + "phase": p.status.phase or "Unknown", + "restarts": _get_pod_restarts(p), + "node": p.spec.node_name or "—", + "age": _format_age(p.metadata.creation_timestamp), + }) + return result + +def _is_pod_ready(pod): + if not pod.status.conditions: + return False + for c in pod.status.conditions: + if c.type == "Ready": + return c.status == "True" + return False + +def _get_pod_restarts(pod): + if not pod.status.container_statuses: + return 0 + return sum(c.restart_count for c in pod.status.container_statuses) + +def _format_age(ts): + if not ts: + return "—" + now = datetime.now(timezone.utc) + delta = now - ts + if delta.days > 0: + return f"{delta.days}d" + hours = delta.seconds // 3600 + if hours > 0: + return f"{hours}h" + minutes = delta.seconds // 60 + return f"{minutes}m" + +def _parse_millicores(val): + if not val: + return 0.0 + if isinstance(val, (int, float)): + return float(val) + if val.endswith("m"): + return float(val[:-1]) / 1000.0 + return float(val) + +def _parse_mebibytes(val): + if not val: + return 0.0 + if isinstance(val, (int, float)): + return float(val) + + # Simple parser for Ki, Mi, Gi, Ti + suffix_map = { + "Ki": 1/1024, + "Mi": 1, + "Gi": 1024, + "Ti": 1024*1024, + } + + for suffix, multiplier in suffix_map.items(): + if val.endswith(suffix): + return float(val[:-len(suffix)]) * multiplier + + # Bytes to MiB if no suffix + return float(val) / (1024 * 1024) diff --git a/app/routers/nodes.py b/app/routers/nodes.py index 88593b1..c4ab77c 100644 --- a/app/routers/nodes.py +++ b/app/routers/nodes.py @@ -1,31 +1,8 @@ from fastapi import APIRouter -from app.k8s_client import get_core_client +from app.k8s_client import fetch_nodes router = APIRouter(prefix="/nodes", tags=["nodes"]) - @router.get("/") -def list_node(): - v1 = get_core_client() - nodes = v1.list_node().items - result = [] - for n in nodes: - conditions = {c.type: c.status for c in n.status.conditions} - result.append( - { - "name": n.metadata.name, - "ready": conditions.get("Ready") == "True", - "cpu_capacity": n.status.capacity.get("cpu"), - "memory_capacity": n.status.capacity.get("memory"), - "labels": n.metadata.labels, - "age_days": _age(n.metadata.creation_timestamp), - } - ) - return result - - -def _age(ts) -> int: - from datetime import datetime, timezone - - now = datetime.now(timezone.utc) - return (now - ts).days +def list_nodes(): + return fetch_nodes() diff --git a/app/routers/pods.py b/app/routers/pods.py index 948ca7c..a8b0ac2 100644 --- a/app/routers/pods.py +++ b/app/routers/pods.py @@ -1,31 +1,8 @@ from fastapi import APIRouter, Query -from k8s_client import get_core_client +from app.k8s_client import fetch_pods -router = APIRouter(prefix="/pods", tags=["pods"]) +router = APIRouter(prefix="/pods", tags=["pods"]) @router.get("/") -def list_pods(namespace: str = Query("default")): - v1 = get_core_client() - pods = v1.list_namespaced_pod(namespace=namespace).items - return [ - { - "name": p.metadata.name, - "namespace": p.metadata.namespace, - "phase": p.status.phase, # Pending/Running/Succeeded/Failed - "ready": _is_ready(p), - "restarts": _restarts(p), - "node": p.spec.node_name, - } - for p in pods - ] - - -def _is_ready(pod) -> bool : - if not pod.status.conditions: - return False - for any(c.type == "Ready" and c.status == "True" for c in pod.status.conditions) - -def _restarts(pod) -> int: - if not pod.status.container_statuses: - return 0 - return sum(c.restart_count for c in pod.status.container_statuses) +def list_pods(namespace: str = Query("all")): + return fetch_pods(namespace=namespace) diff --git a/tests/__pycache__/test_nodes.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_nodes.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b5e7b8729894de21f09e9cd6319e7b4f290a57d4 GIT binary patch literal 31664 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 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_pods.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_pods.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca4ebaa2148567c7d23ab3544bf54af4e9025501 GIT binary patch literal 24933 zcmeHPdu$xXdEdR~-tovIsrUQz*6Bfslt@XmBum!AvSiaXSLX<&k;Cb3MKU7qnBAit zGD$6Zx$5%_`|I}q_m_4ail@}Y-$cqho0%{aF0lo3XSm!3Yi_WiF zvs`MmZuNyrrFD`TTP~@!tv;TqFJgIh$cu}-#VoHLc?}|O3Cn9l-U5-gl;tf%-Xf8= zjO8sx-V%|wyxW&tKKM3Tj21PcXzV1I9w_9Drh%boG?niyCfI|-`GTG}uNAXB?CF+d zQ?#Ry(6vloBHK4;6g54O*R-6LOZ?Nwn~6+cU*h!OIZe-ND4jU++*64x@{B}A*Ao4O z>;)~?6peNc=G&tQTmwVY25+dW2Z9gW}iXzxQRNQ%~~VM#+A~3{J+}*@RrntnZhsoTjYIbeSISLY-67&o$_sv zuF2?%9zqebR!65ZdNG|X4Caew5QC8$!b6&$dJSRtvW7uz;T6q}ohd`pjh#kse<45I zqvgAI9xG%A`?Wkq{Usw?$Z0!|@{vsUU|5Si>0HL>IakQ&IW{Kc-^N^=?$2D%(00C< z>CJ0;8ht4a8hRbtTZI3{GJp~3W~A};6(dhfE!i*;SU445_{zztn%a@4?&6`8RR~&F z7{6>>uCuk{F)qqkrBjTBT}+ak@j1KH^EuVZt}fADCtJgIKL1@Ob;azQPI`-qtJV4@ zT@ChwR7jQg%R5k4%DZO~8}saz&V_c``R${R#I7zHEOx2Bl;2(*u3W!oZa@us;D(&s zodNXBtL0j|y;S(1yv4f7;Y|`!*DCQWpoaI$JLM{M#BIE@XFC&i+WGBs#!1;AC6x{= zig15#eh_QT40h6FHRDI^>0|JW*mH%Pn(fhYgMC`g44lyiHM8a^P=Ve|U+=J%yC$>g zoowVwK8qRmeBouCsIuuFEN1l}r5dwXD8=4FK22*E zv$lW04Drcp1_ydFhGqt7TIb9tK{Q>+^J$Huc6Y7WjyDMy16mdhWBsJ5^<+e+kEt&w zu!b6pv4v@sFcoXn8u|u$xPbLW%cZT6NoTqX`Z{{zCn`uY2&Pg(M z#DC*GfD!55LaAoq>)S?-PAy&e>eCmWo(L?OTD6I%mfzge^u50C^!;GppB(tZ1LKf$LZ*qh)RTXSXQ zxYC5=rTpmmab+W=`bS^D<6Dv7Dyo7NTXriZZQOfb@@)*=R90U-TTeU%7MO2|G8Y|khy{$u?m|G^cBD_jYCNA zhsRLBpcng(5$R9&uy;=S_!(pm3|gNtr~csUalx>$Aej| zlTyAJy+eQI>YY;xy#lLzpkB>q?1Z>5qj&QeyBVd?%ix^v
WLrsAlkOT7pM@(>) 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
WLrsAlkOT7pM@(>) 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 rZ3*E1u9fn}+Y-S2V{%MxzbygW->^z 0 + + def test_cpu_parsed_to_float(self, mock_core): + mock_core.list_node.return_value.items = [_make_node(cpu="4")] + assert fetch_nodes()[0]["cpu_cap"] == pytest.approx(4.0) + + def test_age_days(self, mock_core): + mock_core.list_node.return_value.items = [_make_node(created_days_ago=5)] + assert fetch_nodes()[0]["age"] == "5d" + + def test_age_hours(self, mock_core): + node = _make_node() + node.metadata.creation_timestamp = datetime.now(timezone.utc) - timedelta( + hours=3 + ) + mock_core.list_node.return_value.items = [node] + assert fetch_nodes()[0]["age"] == "3h" + + def test_pressure_fields_present(self, mock_core): + mock_core.list_node.return_value.items = [_make_node()] + node = fetch_nodes()[0] + assert "mem_pressure" in node + assert "disk_pressure" in node + assert "pid_pressure" in node + + def test_memory_pressure_true(self, mock_core): + mock_core.list_node.return_value.items = [_make_node(mem_pressure="True")] + assert fetch_nodes()[0]["mem_pressure"] == "True" + + def test_multiple_nodes(self, mock_core): + mock_core.list_node.return_value.items = [ + _make_node(name="node-1"), + _make_node(name="node-2"), + ] + result = fetch_nodes() + assert len(result) == 2 + assert {r["name"] for r in result} == {"node-1", "node-2"} + + def test_empty_cluster(self, mock_core): + mock_core.list_node.return_value.items = [] + assert fetch_nodes() == [] + + def test_null_conditions_handled(self, mock_core): + """Nodes mid-join can have conditions=None — must not raise.""" + node = _make_node() + node.status.conditions = None + mock_core.list_node.return_value.items = [node] + result = fetch_nodes() + assert result[0]["ready"] is False + + def test_null_labels_handled(self, mock_core): + node = _make_node() + node.metadata.labels = None + mock_core.list_node.return_value.items = [node] + assert fetch_nodes()[0]["roles"] == "worker" + + def test_all_keys_present(self, mock_core): + """Every dict must have the same stable set of keys.""" + mock_core.list_node.return_value.items = [_make_node()] + node = fetch_nodes()[0] + expected_keys = { + "name", + "ready", + "roles", + "age", + "cpu_cap", + "mem_cap_mi", + "mem_alloc_mi", + "os", + "kubelet_version", + "mem_pressure", + "disk_pressure", + "pid_pressure", + } + assert expected_keys == set(node.keys()) diff --git a/tests/test_pods.py b/tests/test_pods.py index e69de29..69e7323 100644 --- a/tests/test_pods.py +++ b/tests/test_pods.py @@ -0,0 +1,248 @@ +""" +tests/test_pods.py + +Unit tests for fetch_pods(). +No real cluster needed — all Kubernetes API calls are mocked. + +Run: + pytest tests/test_pods.py -v +""" + +from datetime import datetime, timezone, timedelta +from unittest.mock import MagicMock + +import pytest + +from app.k8s_client import fetch_pods + +# ── Fixtures ─────────────────────────────────────────────────────────────────── + + +def _make_container_status(restart_count=0, ready=True): + cs = MagicMock() + cs.restart_count = restart_count + cs.ready = ready + return cs + + +def _make_pod( + name="pod-1", + namespace="default", + phase="Running", + ready_status="True", + restarts=0, + node_name="k3s-worker-1", + created_minutes_ago=60, + container_statuses=None, +): + pod = MagicMock() + pod.metadata.name = name + pod.metadata.namespace = namespace + pod.metadata.creation_timestamp = datetime.now(timezone.utc) - timedelta( + minutes=created_minutes_ago + ) + pod.status.phase = phase + pod.status.conditions = [ + _cond("Ready", ready_status), + _cond("ContainersReady", ready_status), + _cond("PodScheduled", "True"), + _cond("Initialized", "True"), + ] + pod.status.container_statuses = container_statuses or [ + _make_container_status(restart_count=restarts) + ] + pod.spec.node_name = node_name + return pod + + +def _cond(type_: str, status: str): + c = MagicMock() + c.type = type_ + c.status = status + return c + + +@pytest.fixture +def mock_core(monkeypatch): + mock = MagicMock() + monkeypatch.setattr("app.k8s_client.get_core", lambda: mock) + return mock + + +# ── fetch_pods() — return shape ──────────────────────────────────────────────── + + +class TestFetchPodsShape: + def test_returns_list_of_dicts(self, mock_core): + mock_core.list_pod_for_all_namespaces.return_value.items = [_make_pod()] + result = fetch_pods("all") + assert isinstance(result, list) + assert isinstance(result[0], dict) + + def test_all_keys_present(self, mock_core): + mock_core.list_pod_for_all_namespaces.return_value.items = [_make_pod()] + pod = fetch_pods("all")[0] + assert set(pod.keys()) == { + "namespace", + "name", + "ready", + "phase", + "restarts", + "node", + "age", + } + + def test_empty_result(self, mock_core): + mock_core.list_pod_for_all_namespaces.return_value.items = [] + assert fetch_pods("all") == [] + + +# ── fetch_pods() — namespace routing ────────────────────────────────────────── + + +class TestNamespaceRouting: + def test_all_namespace_uses_cluster_wide_call(self, mock_core): + mock_core.list_pod_for_all_namespaces.return_value.items = [] + fetch_pods("all") + mock_core.list_pod_for_all_namespaces.assert_called_once() + mock_core.list_namespaced_pod.assert_not_called() + + def test_specific_namespace_uses_namespaced_call(self, mock_core): + mock_core.list_namespaced_pod.return_value.items = [] + fetch_pods("kube-system") + mock_core.list_namespaced_pod.assert_called_once_with(namespace="kube-system") + mock_core.list_pod_for_all_namespaces.assert_not_called() + + def test_namespace_preserved_in_result(self, mock_core): + mock_core.list_namespaced_pod.return_value.items = [ + _make_pod(namespace="kube-system") + ] + pod = fetch_pods("kube-system")[0] + assert pod["namespace"] == "kube-system" + + +# ── fetch_pods() — readiness ─────────────────────────────────────────────────── + + +class TestPodReadiness: + def test_ready_pod(self, mock_core): + mock_core.list_pod_for_all_namespaces.return_value.items = [ + _make_pod(ready_status="True") + ] + assert fetch_pods("all")[0]["ready"] is True + + def test_not_ready_pod(self, mock_core): + mock_core.list_pod_for_all_namespaces.return_value.items = [ + _make_pod(ready_status="False") + ] + assert fetch_pods("all")[0]["ready"] is False + + def test_ready_is_bool_not_string(self, mock_core): + """Regression: condition.status is the string 'True', must be cast to bool.""" + mock_core.list_pod_for_all_namespaces.return_value.items = [ + _make_pod(ready_status="True") + ] + assert fetch_pods("all")[0]["ready"] is True + assert not isinstance(fetch_pods("all")[0]["ready"], str) + + def test_running_but_not_ready(self, mock_core): + """phase=Running does NOT mean ready. This is the key gotcha.""" + mock_core.list_pod_for_all_namespaces.return_value.items = [ + _make_pod(phase="Running", ready_status="False") + ] + pod = fetch_pods("all")[0] + assert pod["phase"] == "Running" + assert pod["ready"] is False # not the same thing + + +# ── fetch_pods() — phase ─────────────────────────────────────────────────────── + + +class TestPodPhase: + @pytest.mark.parametrize("phase", ["Running", "Pending", "Failed", "Succeeded"]) + def test_phase_preserved(self, mock_core, phase): + mock_core.list_pod_for_all_namespaces.return_value.items = [ + _make_pod(phase=phase) + ] + assert fetch_pods("all")[0]["phase"] == phase + + def test_none_phase_becomes_unknown(self, mock_core): + pod = _make_pod() + pod.status.phase = None + mock_core.list_pod_for_all_namespaces.return_value.items = [pod] + assert fetch_pods("all")[0]["phase"] == "Unknown" + + +# ── fetch_pods() — restarts ──────────────────────────────────────────────────── + + +class TestPodRestarts: + def test_zero_restarts(self, mock_core): + mock_core.list_pod_for_all_namespaces.return_value.items = [ + _make_pod(restarts=0) + ] + assert fetch_pods("all")[0]["restarts"] == 0 + + def test_nonzero_restarts(self, mock_core): + mock_core.list_pod_for_all_namespaces.return_value.items = [ + _make_pod(restarts=7) + ] + assert fetch_pods("all")[0]["restarts"] == 7 + + def test_restarts_summed_across_containers(self, mock_core): + """Multi-container pods sum restarts from all containers.""" + pod = _make_pod( + container_statuses=[ + _make_container_status(restart_count=3), + _make_container_status(restart_count=5), + ] + ) + mock_core.list_pod_for_all_namespaces.return_value.items = [pod] + assert fetch_pods("all")[0]["restarts"] == 8 + + def test_null_container_statuses_handled(self, mock_core): + """Pending pods have no container_statuses yet — must not raise.""" + pod = _make_pod() + pod.status.container_statuses = None + mock_core.list_pod_for_all_namespaces.return_value.items = [pod] + assert fetch_pods("all")[0]["restarts"] == 0 + + +# ── fetch_pods() — edge cases ────────────────────────────────────────────────── + + +class TestPodEdgeCases: + def test_null_conditions_handled(self, mock_core): + pod = _make_pod() + pod.status.conditions = None + mock_core.list_pod_for_all_namespaces.return_value.items = [pod] + result = fetch_pods("all") + assert result[0]["ready"] is False + + def test_null_node_name(self, mock_core): + """Pending pods are not yet scheduled to a node.""" + pod = _make_pod() + pod.spec.node_name = None + mock_core.list_pod_for_all_namespaces.return_value.items = [pod] + assert fetch_pods("all")[0]["node"] == "—" + + def test_multiple_pods(self, mock_core): + mock_core.list_pod_for_all_namespaces.return_value.items = [ + _make_pod(name="pod-a", namespace="default"), + _make_pod(name="pod-b", namespace="kube-system"), + ] + result = fetch_pods("all") + assert len(result) == 2 + assert {r["name"] for r in result} == {"pod-a", "pod-b"} + + def test_age_minutes(self, mock_core): + mock_core.list_pod_for_all_namespaces.return_value.items = [ + _make_pod(created_minutes_ago=25) + ] + assert fetch_pods("all")[0]["age"] == "25m" + + def test_age_hours(self, mock_core): + mock_core.list_pod_for_all_namespaces.return_value.items = [ + _make_pod(created_minutes_ago=180) + ] + assert fetch_pods("all")[0]["age"] == "3h"