Coverage for fake_kubernetes_client/status_templates.py: 11%
103 statements
« prev ^ index » next coverage.py v7.10.1, created at 2025-07-29 12:31 +0300
« prev ^ index » next coverage.py v7.10.1, created at 2025-07-29 12:31 +0300
1"""Status template methods for fake Kubernetes resources"""
3from datetime import datetime, timezone
4from typing import Any, Union
6from fake_kubernetes_client.status_schema_parser import StatusSchemaParser
9def _get_ready_status_config(body: dict[str, Any]) -> tuple[str, str, str]:
10 """
11 Get ready status configuration from resource annotations or spec.
13 Returns:
14 tuple: (status, reason, message) - status is "True" or "False"
15 """
16 # Default to ready
17 status = "True"
18 reason = "ResourceReady"
19 message = "Resource is ready"
21 # Check annotations for test configuration
22 metadata = body.get("metadata", {})
23 annotations = metadata.get("annotations", {})
25 # Allow configuration via annotation "fake-client.io/ready"
26 if annotations.get("fake-client.io/ready", "").lower() == "false":
27 status = "False"
28 reason = "ResourceNotReady"
29 message = "Resource is not ready"
31 # Also check for a more specific ready status in spec
32 if "readyStatus" in body.get("spec", {}):
33 if body["spec"]["readyStatus"]:
34 status = "True"
35 reason = "ResourceReady"
36 message = "Resource is ready"
37 else:
38 status = "False"
39 reason = "ResourceNotReady"
40 message = "Resource is not ready"
42 return status, reason, message
45def add_realistic_status(body: dict[str, Any], resource_mappings: Union[dict[str, Any], None] = None) -> None:
46 """Add realistic status to resources that need it"""
47 kind = body.get("kind", "")
49 # First check if we have a hardcoded template
50 if kind == "Pod":
51 status = get_pod_status_template(body=body)
52 elif kind == "Deployment":
53 status = get_deployment_status_template(body=body)
54 elif kind == "Service":
55 status = get_service_status_template(body=body)
56 elif kind == "Namespace":
57 status = get_namespace_status_template(body=body)
58 else:
59 # Try schema-based generation if mappings are available
60 if resource_mappings:
61 status = generate_dynamic_status(body=body, resource_mappings=resource_mappings)
62 else:
63 # Fallback to generic status
64 status = get_generic_status_template(body=body)
66 if status:
67 body["status"] = status
70def generate_dynamic_status(body: dict[str, Any], resource_mappings: dict[str, Any]) -> dict[str, Any]:
71 """Generate status dynamically based on resource schema"""
72 kind = body.get("kind", "")
73 api_version = body.get("apiVersion", "v1")
75 parser = StatusSchemaParser(resource_mappings=resource_mappings)
76 status_schema = parser.get_status_schema_for_resource(kind=kind, api_version=api_version)
78 if status_schema:
79 return parser.generate_status_from_schema(schema=status_schema, resource_body=body)
80 else:
81 # Fallback to generic status
82 return get_generic_status_template(body=body)
85def get_pod_status_template(body: dict[str, Any]) -> dict[str, Any]:
86 """Get realistic Pod status"""
87 container_name = "test-container"
88 if "spec" in body and "containers" in body["spec"] and body["spec"]["containers"]:
89 container_name = body["spec"]["containers"][0].get("name", "test-container")
91 # For Pods, we need more specific configuration
92 # Check for pod-specific annotation first, fall back to general ready annotation
93 metadata = body.get("metadata", {})
94 annotations = metadata.get("annotations", {})
96 # Default to ready
97 ready_status = "True"
98 ready_reason = "ContainersReady"
99 ready_message = "All containers are ready"
100 container_ready = True
101 container_started = True
102 container_state = {"running": {"startedAt": datetime.now(timezone.utc).isoformat()}}
104 # Check pod-specific annotation first
105 if "fake-client.io/pod-ready" in annotations:
106 if annotations["fake-client.io/pod-ready"].lower() == "false":
107 ready_status = "False"
108 ready_reason = "ContainersNotReady"
109 ready_message = f"containers with unready status: [{container_name}]"
110 container_ready = False
111 container_started = False
112 container_state = {"waiting": {"reason": "ContainerCreating"}}
113 # Fall back to general ready annotation
114 elif annotations.get("fake-client.io/ready", "").lower() == "false":
115 ready_status = "False"
116 ready_reason = "ContainersNotReady"
117 ready_message = f"containers with unready status: [{container_name}]"
118 container_ready = False
119 container_started = False
120 container_state = {"waiting": {"reason": "ContainerCreating"}}
122 # Check spec.readyStatus
123 if "readyStatus" in body.get("spec", {}):
124 ready_status = "True" if body["spec"]["readyStatus"] else "False"
125 if ready_status == "False":
126 ready_reason = "ContainersNotReady"
127 ready_message = f"containers with unready status: [{container_name}]"
128 container_ready = False
129 container_started = False
130 container_state = {"waiting": {"reason": "ContainerCreating"}}
131 else:
132 ready_reason = "ContainersReady"
133 ready_message = "All containers are ready"
134 container_ready = True
135 container_started = True
136 container_state = {"running": {"startedAt": datetime.now(timezone.utc).isoformat()}}
138 return {
139 "phase": "Running",
140 "conditions": [
141 {
142 "type": "Initialized",
143 "status": "True",
144 "lastProbeTime": None,
145 "lastTransitionTime": datetime.now(timezone.utc).isoformat(),
146 "reason": "PodCompleted",
147 },
148 {
149 "type": "Ready",
150 "status": ready_status, # Now configurable, defaults to True
151 "lastProbeTime": None,
152 "lastTransitionTime": datetime.now(timezone.utc).isoformat(),
153 "reason": ready_reason,
154 "message": ready_message,
155 },
156 {
157 "type": "ContainersReady",
158 "status": ready_status, # Should match Ready status
159 "lastProbeTime": None,
160 "lastTransitionTime": datetime.now(timezone.utc).isoformat(),
161 "reason": ready_reason,
162 "message": ready_message,
163 },
164 {
165 "type": "PodScheduled",
166 "status": "True",
167 "lastProbeTime": None,
168 "lastTransitionTime": datetime.now(timezone.utc).isoformat(),
169 "reason": "PodScheduled",
170 },
171 ],
172 "hostIP": "10.0.0.1",
173 "podIP": "10.244.0.2",
174 "podIPs": [{"ip": "10.244.0.2"}],
175 "startTime": datetime.now(timezone.utc).isoformat(),
176 "containerStatuses": [
177 {
178 "name": container_name,
179 "state": container_state,
180 "lastState": {},
181 "ready": container_ready,
182 "restartCount": 0,
183 "image": "nginx:latest",
184 "imageID": "docker://sha256:nginx",
185 "containerID": "docker://1234567890abcdef" if container_ready else "",
186 "started": container_started,
187 }
188 ],
189 }
192def get_deployment_status_template(body: dict[str, Any]) -> dict[str, Any]:
193 """Get realistic Deployment status"""
194 # Get ready status configuration
195 ready_status, ready_reason, ready_message = _get_ready_status_config(body=body)
197 # Adjust deployment-specific values based on ready status
198 if ready_status == "True":
199 replicas = body.get("spec", {}).get("replicas", 1)
200 return {
201 "replicas": replicas,
202 "updatedReplicas": replicas,
203 "readyReplicas": replicas,
204 "availableReplicas": replicas,
205 "observedGeneration": 1,
206 "conditions": [
207 {
208 "type": "Available",
209 "status": "True",
210 "lastUpdateTime": datetime.now(timezone.utc).isoformat(),
211 "lastTransitionTime": datetime.now(timezone.utc).isoformat(),
212 "reason": "MinimumReplicasAvailable",
213 "message": "Deployment has minimum availability.",
214 },
215 {
216 "type": "Progressing",
217 "status": "True",
218 "lastUpdateTime": datetime.now(timezone.utc).isoformat(),
219 "lastTransitionTime": datetime.now(timezone.utc).isoformat(),
220 "reason": "NewReplicaSetAvailable",
221 "message": "ReplicaSet has successfully progressed.",
222 },
223 ],
224 }
225 else:
226 replicas = body.get("spec", {}).get("replicas", 1)
227 return {
228 "replicas": replicas,
229 "updatedReplicas": 0,
230 "readyReplicas": 0,
231 "availableReplicas": 0,
232 "unavailableReplicas": replicas,
233 "observedGeneration": 1,
234 "conditions": [
235 {
236 "type": "Available",
237 "status": "False",
238 "lastUpdateTime": datetime.now(timezone.utc).isoformat(),
239 "lastTransitionTime": datetime.now(timezone.utc).isoformat(),
240 "reason": "MinimumReplicasUnavailable",
241 "message": "Deployment does not have minimum availability.",
242 },
243 {
244 "type": "Progressing",
245 "status": "False",
246 "lastUpdateTime": datetime.now(timezone.utc).isoformat(),
247 "lastTransitionTime": datetime.now(timezone.utc).isoformat(),
248 "reason": "ProgressDeadlineExceeded",
249 "message": "ReplicaSet has timed out progressing.",
250 },
251 ],
252 }
255def get_service_status_template(body: dict[str, Any]) -> dict[str, Any]:
256 """Get realistic Service status"""
257 # Services don't typically have complex status
258 return {"loadBalancer": {}}
261def get_namespace_status_template(body: dict[str, Any]) -> dict[str, Any]:
262 """Get realistic Namespace status"""
263 # Check if namespace should be terminating based on ready status
264 ready_status, _, _ = _get_ready_status_config(body=body)
266 if ready_status == "True":
267 return {
268 "phase": "Active",
269 "conditions": [
270 {
271 "type": "NamespaceDeletionDiscoveryFailure",
272 "status": "False",
273 "lastTransitionTime": datetime.now(timezone.utc).isoformat(),
274 "reason": "ResourcesDiscovered",
275 "message": "All resources successfully discovered",
276 },
277 {
278 "type": "NamespaceDeletionContentFailure",
279 "status": "False",
280 "lastTransitionTime": datetime.now(timezone.utc).isoformat(),
281 "reason": "ContentDeleted",
282 "message": "All content successfully deleted",
283 },
284 ],
285 }
286 else:
287 # Namespace not ready could mean it's terminating
288 return {
289 "phase": "Terminating",
290 "conditions": [
291 {
292 "type": "NamespaceDeletionDiscoveryFailure",
293 "status": "True",
294 "lastTransitionTime": datetime.now(timezone.utc).isoformat(),
295 "reason": "DiscoveryFailed",
296 "message": "Discovery failed for some resources",
297 },
298 {
299 "type": "NamespaceDeletionContentFailure",
300 "status": "True",
301 "lastTransitionTime": datetime.now(timezone.utc).isoformat(),
302 "reason": "ContentDeletionFailed",
303 "message": "Failed to delete all content",
304 },
305 ],
306 }
309def get_generic_status_template(body: dict[str, Any]) -> dict[str, Any]:
310 """Get generic status for unknown resource types"""
311 # Use the general ready status configuration
312 ready_status, ready_reason, ready_message = _get_ready_status_config(body=body)
314 return {
315 "conditions": [
316 {
317 "type": "Ready",
318 "status": ready_status,
319 "lastTransitionTime": datetime.now(timezone.utc).isoformat(),
320 "reason": ready_reason,
321 "message": ready_message,
322 }
323 ]
324 }