Skip to content

Commit 0c5cdec

Browse files
committed
kube: Migrate Pod parsing to from_json
... and away from the Python kubernetes client. This diff looks big, but about half of it is tests, and most of the rest is straightforward: creating TypedDicts for the responses, pulling the relevant parsing functions out of transform.py and into the respective from_json modules, and updating accesses to pull data from the dict instead of from the kubernetes library models. Then updating api_server.py and controllers.py to make use of the new structures. The goal is to backport this to 2.4, because it's needed to be able to pull out the new pod-level resources field eventually, since the version of the Kubernetes library in 2.4 doesn't provide this field. The new test cases were written by Claude though I have given them a once-over. Change-Id: I1cb1539090e2cbe9f018a247329a9c95a64a9a4d
1 parent 8d140dc commit 0c5cdec

24 files changed

Lines changed: 1141 additions & 656 deletions

cmk/plugins/kube/api_server.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from cmk.plugins.kube.controllers import map_controllers, map_controllers_top_to_down
2222
from cmk.plugins.kube.from_json.deployment import deployment_from_json, JSONDeploymentList
2323
from cmk.plugins.kube.from_json.node import JSONNodeList, node_list_from_json
24+
from cmk.plugins.kube.from_json.pod.pod import JSONPodList, pod_from_client
2425
from cmk.plugins.kube.from_json.statefulset import JSONStatefulSetList, statefulset_list_from_json
2526
from cmk.plugins.kube.schemata import api
2627
from cmk.plugins.kube.transform import (
@@ -30,7 +31,6 @@
3031
namespace_from_client,
3132
parse_object_to_owners,
3233
persistent_volume_claim_from_client,
33-
pod_from_client,
3434
resource_quota_from_client,
3535
)
3636
from cmk.plugins.kube.transform_any import parse_open_metric_samples
@@ -82,10 +82,9 @@ def query_raw_jobs(self) -> Sequence[client.V1Job]:
8282

8383

8484
class ClientCoreAPI(ClientAPI):
85-
def query_raw_pods(self) -> Sequence[client.V1Pod]:
85+
def query_raw_pods(self) -> JSONPodList:
8686
request = requests.Request("GET", self._config.url("/api/v1/pods"))
87-
response = send_request(self._config, self._client, request)
88-
return self._deserializer.run("V1PodList", response).items
87+
return send_request(self._config, self._client, request).json()
8988

9089
def query_raw_resource_quotas(self) -> Sequence[client.V1ResourceQuota]:
9190
request = requests.Request("GET", self._config.url("/api/v1/resourcequotas"))
@@ -347,7 +346,7 @@ class APIData:
347346
class UnparsedAPIData:
348347
raw_jobs: Sequence[client.V1Job]
349348
raw_cron_jobs: Sequence[client.V1CronJob]
350-
raw_pods: Sequence[client.V1Pod]
349+
raw_pods: JSONPodList
351350
raw_nodes: JSONNodeList
352351
raw_namespaces: Sequence[client.V1Namespace]
353352
raw_resource_quotas: Sequence[client.V1ResourceQuota]
@@ -395,7 +394,7 @@ def query_raw_api_data_v2(
395394

396395
def parse_api_data(
397396
raw_cron_jobs: Sequence[client.V1CronJob],
398-
raw_pods: Sequence[client.V1Pod],
397+
raw_pods: JSONPodList,
399398
raw_jobs: Sequence[client.V1Job],
400399
raw_nodes: JSONNodeList,
401400
raw_namespaces: Sequence[client.V1Namespace],
@@ -415,7 +414,7 @@ def parse_api_data(
415414
) -> APIData:
416415
"""Parses the Kubernetes API to the format used"""
417416
job_uids = {raw_job.metadata.uid for raw_job in raw_jobs}
418-
pod_uids = {raw_pod.metadata.uid for raw_pod in raw_pods}
417+
pod_uids = {raw_pod["metadata"]["uid"] for raw_pod in raw_pods["items"]}
419418

420419
cron_jobs = [
421420
cron_job_from_client(
@@ -453,7 +452,10 @@ def parse_api_data(
453452
statefulsets = statefulset_list_from_json(raw_statefulsets, controller_to_pods)
454453
namespaces = [namespace_from_client(raw_namespace) for raw_namespace in raw_namespaces]
455454
nodes = node_list_from_json(raw_nodes, node_to_kubelet_health)
456-
pods = [pod_from_client(pod, pod_to_controllers.get(pod.metadata.uid, [])) for pod in raw_pods]
455+
pods = [
456+
pod_from_client(pod, pod_to_controllers.get(api.PodUID(pod["metadata"]["uid"]), []))
457+
for pod in raw_pods["items"]
458+
]
457459
persistent_volume_claims = [
458460
persistent_volume_claim_from_client(pvc) for pvc in raw_persistent_volume_claims
459461
]
@@ -518,11 +520,11 @@ def create_api_data_v2(
518520
raw_api_data.raw_replica_sets,
519521
raw_api_data.raw_cron_jobs,
520522
raw_api_data.raw_jobs,
521-
raw_api_data.raw_pods,
522523
),
523524
workload_resources_json=itertools.chain(
524525
raw_api_data.raw_statefulsets["items"],
525526
raw_api_data.raw_deployments["items"],
527+
raw_api_data.raw_pods["items"],
526528
),
527529
)
528530
controller_to_pods, pod_to_controllers = map_controllers(

cmk/plugins/kube/controllers.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66
from collections.abc import Iterable, Mapping, Sequence
77
from dataclasses import dataclass
88

9-
from kubernetes import client # type: ignore[import-untyped]
10-
11-
from cmk.plugins.kube.schemata import api
9+
from .from_json.pod.pod import JSONPodList
10+
from .schemata import api
1211

1312

1413
@dataclass(frozen=True)
@@ -104,11 +103,11 @@ def _match_controllers(
104103

105104

106105
def map_controllers(
107-
raw_pods: Sequence[client.V1Pod],
106+
raw_pods: JSONPodList,
108107
object_to_owners: Mapping[str, api.OwnerReferences],
109108
) -> tuple[Mapping[str, Sequence[api.PodUID]], Mapping[api.PodUID, Sequence[api.Controller]]]:
110109
pod_to_controllers = _find_control_chains(
111-
pod_uids=(pod.metadata.uid for pod in raw_pods),
110+
pod_uids=(api.PodUID(pod["metadata"]["uid"]) for pod in raw_pods["items"]),
112111
object_to_owners=object_to_owners,
113112
)
114113
return _match_controllers(pod_to_controllers), pod_to_controllers
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env python3
2+
# Copyright (C) 2026 Checkmk GmbH - License: GNU General Public License v2
3+
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
4+
# conditions defined in the file COPYING, which is part of this source code package.
5+
6+
"""
7+
JSON parsing for Kubernetes Pods.
8+
9+
Separate package down a level from from_json, simply so we can split out
10+
pod-specific objects (like container specs and status), to avoid having a single
11+
large "pod.py".
12+
"""
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright (C) 2026 Checkmk GmbH - License: GNU General Public License v2
2+
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
3+
# conditions defined in the file COPYING, which is part of this source code package.
4+
5+
from collections.abc import Sequence
6+
from typing import Literal, NotRequired, TypedDict
7+
8+
from ...schemata import api
9+
from ..resources import JSONResourceRequirements
10+
11+
12+
class JSONContainerSpec(TypedDict):
13+
name: str
14+
imagePullPolicy: Literal["Always", "IfNotPresent", "Never"]
15+
resources: NotRequired[JSONResourceRequirements]
16+
17+
18+
def container_resources(container: JSONContainerSpec) -> api.ResourceRequirements:
19+
parsed_limits = api.ResourceRequirement()
20+
parsed_requests = api.ResourceRequirement()
21+
if resources := container.get("resources"):
22+
if limits := resources.get("limits"):
23+
parsed_limits = api.ResourceRequirement(
24+
memory=api.parse_resource_value(limits["memory"]) if "memory" in limits else None,
25+
cpu=api.parse_cpu_cores(limits["cpu"]) if "cpu" in limits else None,
26+
)
27+
if requests := resources.get("requests"):
28+
parsed_requests = api.ResourceRequirement(
29+
memory=api.parse_resource_value(requests["memory"])
30+
if "memory" in requests
31+
else None,
32+
cpu=api.parse_cpu_cores(requests["cpu"]) if "cpu" in requests else None,
33+
)
34+
35+
return api.ResourceRequirements(
36+
limits=parsed_limits,
37+
requests=parsed_requests,
38+
)
39+
40+
41+
def containers_spec(containers: Sequence[JSONContainerSpec]) -> Sequence[api.ContainerSpec]:
42+
return [
43+
api.ContainerSpec(
44+
name=api.ContainerName(container["name"]),
45+
resources=container_resources(container),
46+
image_pull_policy=container["imagePullPolicy"],
47+
)
48+
for container in containers
49+
]
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Copyright (C) 2026 Checkmk GmbH - License: GNU General Public License v2
2+
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
3+
# conditions defined in the file COPYING, which is part of this source code package.
4+
5+
from collections.abc import Sequence
6+
from typing import NotRequired, TypedDict
7+
8+
from ...schemata import api
9+
10+
11+
class JSONContainerStateTerminated(TypedDict):
12+
exitCode: int
13+
reason: NotRequired[str]
14+
message: NotRequired[str]
15+
startedAt: NotRequired[str]
16+
finishedAt: NotRequired[str]
17+
18+
19+
class JSONContainerStateRunning(TypedDict):
20+
startedAt: str
21+
22+
23+
class JSONContainerStateWaiting(TypedDict):
24+
reason: NotRequired[str]
25+
message: NotRequired[str]
26+
27+
28+
class JSONContainerState(TypedDict):
29+
terminated: NotRequired[JSONContainerStateTerminated]
30+
running: NotRequired[JSONContainerStateRunning]
31+
waiting: NotRequired[JSONContainerStateWaiting]
32+
33+
34+
class JSONContainerStatus(TypedDict):
35+
state: JSONContainerState
36+
imageID: str
37+
image: str
38+
name: str
39+
ready: bool
40+
restartCount: int
41+
containerID: NotRequired[str]
42+
43+
44+
def pod_containers(
45+
container_statuses: Sequence[JSONContainerStatus] | None,
46+
) -> dict[str, api.ContainerStatus]:
47+
result: dict[str, api.ContainerStatus] = {}
48+
if container_statuses is None:
49+
return {}
50+
for status in container_statuses:
51+
details: (
52+
JSONContainerStateTerminated
53+
| JSONContainerStateWaiting
54+
| JSONContainerStateRunning
55+
| None
56+
)
57+
state: api.ContainerTerminatedState | api.ContainerRunningState | api.ContainerWaitingState
58+
if (details := status["state"].get("terminated")) is not None:
59+
state = api.ContainerTerminatedState(
60+
exit_code=details["exitCode"],
61+
start_time=(
62+
int(api.convert_to_timestamp(started_at))
63+
if (started_at := details.get("startedAt"))
64+
else None
65+
),
66+
end_time=(
67+
int(api.convert_to_timestamp(finished_at))
68+
if (finished_at := details.get("finishedAt"))
69+
else None
70+
),
71+
reason=details.get("reason"),
72+
detail=details.get("message"),
73+
)
74+
elif (details := status["state"].get("running")) is not None:
75+
state = api.ContainerRunningState(
76+
start_time=int(api.convert_to_timestamp(details["startedAt"])),
77+
)
78+
elif (details := status["state"].get("waiting")) is not None:
79+
state = api.ContainerWaitingState(
80+
reason=details.get("reason"),
81+
detail=details.get("message"),
82+
)
83+
else:
84+
raise AssertionError(f"Unknown container state {status['state']}")
85+
86+
result[status["name"]] = api.ContainerStatus(
87+
container_id=status.get("containerID"),
88+
image_id=status["imageID"],
89+
name=status["name"],
90+
image=status["image"],
91+
ready=status["ready"],
92+
state=state,
93+
restart_count=status["restartCount"],
94+
)
95+
return result
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright (C) 2026 Checkmk GmbH - License: GNU General Public License v2
2+
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
3+
# conditions defined in the file COPYING, which is part of this source code package.
4+
5+
from collections.abc import Sequence
6+
from typing import TypedDict
7+
8+
from ...schemata import api
9+
from ..metadata import _metadata_from_json, JSONObjectWithMetadata
10+
from .container_status import pod_containers
11+
from .pod_spec import JSONPodSpec, pod_spec
12+
from .pod_status import JSONPodStatus, pod_status
13+
14+
15+
class JSONPod(JSONObjectWithMetadata):
16+
spec: JSONPodSpec
17+
status: JSONPodStatus
18+
19+
20+
class JSONPodList(TypedDict):
21+
items: Sequence[JSONPod]
22+
23+
24+
def pod_from_client(pod: JSONPod, controllers: Sequence[api.Controller]) -> api.Pod:
25+
return api.Pod(
26+
uid=api.PodUID(pod["metadata"]["uid"]),
27+
metadata=_metadata_from_json(pod["metadata"]),
28+
status=pod_status(pod["status"]),
29+
spec=pod_spec(pod["spec"]),
30+
containers=pod_containers(pod["status"].get("containerStatuses")),
31+
init_containers=pod_containers(pod["status"].get("initContainerStatuses")),
32+
controllers=controllers,
33+
)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright (C) 2026 Checkmk GmbH - License: GNU General Public License v2
2+
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
3+
# conditions defined in the file COPYING, which is part of this source code package.
4+
5+
from collections.abc import Sequence
6+
from typing import NotRequired, TypedDict
7+
8+
from ...schemata import api
9+
10+
11+
class JSONPodCondition(TypedDict):
12+
status: str
13+
type: str
14+
reason: NotRequired[str]
15+
message: NotRequired[str]
16+
lastTransitionTime: NotRequired[str]
17+
18+
19+
def pod_condition(condition: JSONPodCondition) -> api.PodCondition:
20+
type_ = api.ConditionType.from_kube_api(condition["type"])
21+
custom_type = None if type_ is not None else condition["type"]
22+
return api.PodCondition(
23+
# TODO: CMK-33030, the JSON type is right, the api model is wrong
24+
status=condition["status"], # type: ignore[arg-type]
25+
reason=condition.get("reason"),
26+
detail=condition.get("message"),
27+
last_transition_time=(
28+
int(api.convert_to_timestamp(last_transition_time))
29+
if (last_transition_time := condition.get("lastTransitionTime"))
30+
else None
31+
),
32+
type=type_,
33+
custom_type=custom_type,
34+
)
35+
36+
37+
def pod_conditions(conditions: Sequence[JSONPodCondition]) -> list[api.PodCondition]:
38+
return [pod_condition(condition) for condition in conditions]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copyright (C) 2026 Checkmk GmbH - License: GNU General Public License v2
2+
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
3+
# conditions defined in the file COPYING, which is part of this source code package.
4+
5+
from collections.abc import Sequence
6+
from typing import Literal, NotRequired, TypedDict
7+
8+
from ...schemata import api
9+
from .container_spec import containers_spec, JSONContainerSpec
10+
from .volume import JSONPodVolume, parse_pod_volumes
11+
12+
13+
class JSONPodSpec(TypedDict):
14+
nodeName: NotRequired[api.NodeName]
15+
hostNetwork: NotRequired[bool]
16+
dnsPolicy: NotRequired[str]
17+
restartPolicy: Literal["Always", "OnFailure", "Never"]
18+
containers: Sequence[JSONContainerSpec]
19+
initContainers: NotRequired[Sequence[JSONContainerSpec]]
20+
priorityClassName: NotRequired[str]
21+
activeDeadlineSeconds: NotRequired[int]
22+
volumes: NotRequired[Sequence[JSONPodVolume]]
23+
24+
25+
def pod_spec(spec: JSONPodSpec) -> api.PodSpec:
26+
return api.PodSpec(
27+
node=spec.get("nodeName"),
28+
host_network=spec.get("hostNetwork"),
29+
dns_policy=spec.get("dnsPolicy"),
30+
restart_policy=spec["restartPolicy"],
31+
containers=containers_spec(spec["containers"]),
32+
init_containers=containers_spec(spec.get("initContainers", [])),
33+
priority_class_name=spec.get("priorityClassName"),
34+
active_deadline_seconds=spec.get("activeDeadlineSeconds"),
35+
volumes=parse_pod_volumes(volumes) if (volumes := spec.get("volumes")) else None,
36+
)

0 commit comments

Comments
 (0)