Coverage for fake_kubernetes_client/status_schema_parser.py: 10%
228 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 schema parser for generating dynamic status from resource schemas"""
3import json
4import os
5import random
6from datetime import datetime, timezone
7from typing import Any, Union
10class StatusSchemaParser:
11 """Parser for generating status from resource schemas"""
13 def __init__(self, resource_mappings: dict[str, Any]) -> None:
14 self.resource_mappings = resource_mappings
15 self._definitions_cache: dict[str, Any] = {}
16 self._definitions: dict[str, Any] = {}
17 self._load_definitions()
19 def _load_definitions(self) -> None:
20 """Load the definitions file once"""
21 definitions_path = os.path.join(
22 os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
23 "class_generator",
24 "schema",
25 "_definitions.json",
26 )
28 if os.path.exists(definitions_path):
29 with open(definitions_path, "r") as f:
30 data = json.load(f)
31 self._definitions = data.get("definitions", {})
33 def get_status_schema_for_resource(self, kind: str, api_version: str) -> Union[dict[str, Any], None]:
34 """Get status schema for a specific resource"""
35 kind_lower = kind.lower()
36 resource_schemas = self.resource_mappings.get(kind_lower, [])
38 if not resource_schemas:
39 return None
41 # Find the matching schema for the API version
42 for schema in resource_schemas:
43 gvk_list = schema.get("x-kubernetes-group-version-kind", [])
44 for gvk in gvk_list:
45 if gvk.get("kind") == kind:
46 # Found matching resource, check if it has status
47 properties = schema.get("properties", {})
48 status_ref = properties.get("status", {})
50 if "$ref" in status_ref:
51 # Resolve the reference to get actual status schema
52 return self._resolve_reference(status_ref["$ref"])
53 elif status_ref.get("type") == "object":
54 # Direct status object
55 return status_ref
57 return None
59 def _resolve_reference(self, ref: str) -> Union[dict[str, Any], None]:
60 """Resolve a $ref to get the actual schema definition"""
61 # Check cache first
62 if ref in self._definitions_cache:
63 return self._definitions_cache[ref]
65 # Parse the reference
66 # References are like "#/definitions/io.k8s.api.core.v1.PersistentVolumeClaimStatus"
67 if not ref.startswith("#/"):
68 # External references not supported for now
69 return None
71 # Remove the leading '#/' and split the path
72 ref_path = ref[2:].split("/")
74 # Navigate the schema structure
75 current = None
76 if ref_path[0] == "definitions" and len(ref_path) > 1:
77 definition_name = ref_path[1]
78 if definition_name in self._definitions:
79 current = self._definitions[definition_name]
81 # Navigate any additional path segments
82 for segment in ref_path[2:]:
83 if isinstance(current, dict) and segment in current:
84 current = current[segment]
85 else:
86 current = None
87 break
89 # Cache the result
90 if current is not None:
91 self._definitions_cache[ref] = current
92 return current
94 # Fallback to the old method if definition not found
95 if len(ref_path) > 1:
96 definition_name = ref_path[-1]
97 return self._get_status_schema_by_type(definition_name)
99 return None
101 def _get_status_schema_by_type(self, type_name: str) -> dict[str, Any]:
102 """Get a status schema based on the type name"""
103 # Extract the resource type from the definition name
104 # e.g., "io.k8s.api.core.v1.PersistentVolumeClaimStatus" -> "PersistentVolumeClaim"
105 if "Status" in type_name:
106 resource_type = type_name.split(".")[-1].replace("Status", "")
107 return self._get_default_status_schema_for_type(resource_type)
109 return {"type": "object", "properties": {}}
111 def _get_default_status_schema_for_type(self, resource_type: str) -> dict[str, Any]:
112 """Get default status schema based on resource type patterns"""
113 # Common patterns in Kubernetes status objects
114 base_schema = {"type": "object", "properties": {}}
116 # Add common status fields based on resource type
117 if resource_type == "PersistentVolumeClaim":
118 base_schema["properties"] = {
119 "phase": {"type": "string", "enum": ["Pending", "Bound", "Lost"]},
120 "accessModes": {
121 "type": "array",
122 "items": {"type": "string", "enum": ["ReadWriteOnce", "ReadOnlyMany", "ReadWriteMany"]},
123 },
124 "capacity": {"type": "object", "additionalProperties": {"type": "string"}},
125 "conditions": {
126 "type": "array",
127 "items": {
128 "type": "object",
129 "properties": {
130 "type": {"type": "string"},
131 "status": {"type": "string"},
132 "lastTransitionTime": {"type": "string"},
133 "reason": {"type": "string"},
134 "message": {"type": "string"},
135 },
136 },
137 },
138 }
139 elif resource_type in ["StatefulSet", "DaemonSet", "ReplicaSet"]:
140 base_schema["properties"] = {
141 "replicas": {"type": "integer"},
142 "readyReplicas": {"type": "integer"},
143 "currentReplicas": {"type": "integer"},
144 "updatedReplicas": {"type": "integer"},
145 "observedGeneration": {"type": "integer"},
146 "conditions": {
147 "type": "array",
148 "items": {
149 "type": "object",
150 "properties": {
151 "type": {"type": "string"},
152 "status": {"type": "string", "enum": ["True", "False", "Unknown"]},
153 "lastTransitionTime": {"type": "string"},
154 },
155 },
156 },
157 }
158 elif resource_type == "PersistentVolume":
159 base_schema["properties"] = {
160 "phase": {"type": "string", "enum": ["Pending", "Available", "Bound", "Released", "Failed"]},
161 "message": {"type": "string"},
162 "reason": {"type": "string"},
163 }
164 elif resource_type == "StorageClass":
165 # StorageClass typically doesn't have status
166 base_schema["properties"] = {}
167 else:
168 # Default status with conditions
169 base_schema["properties"] = {
170 "conditions": {
171 "type": "array",
172 "items": {
173 "type": "object",
174 "properties": {
175 "type": {"type": "string"},
176 "status": {"type": "string", "enum": ["True", "False", "Unknown"]},
177 "lastTransitionTime": {"type": "string"},
178 "reason": {"type": "string"},
179 "message": {"type": "string"},
180 },
181 },
182 },
183 "observedGeneration": {"type": "integer"},
184 }
186 # Add phase for resources that commonly have it
187 if resource_type in ["Job", "CronJob", "Backup", "Restore"]:
188 properties = base_schema["properties"]
189 if isinstance(properties, dict):
190 properties["phase"] = {
191 "type": "string",
192 "enum": ["Pending", "Running", "Succeeded", "Failed", "Unknown"],
193 }
195 return base_schema
197 def generate_status_from_schema(self, schema: dict[str, Any], resource_body: dict[str, Any]) -> dict[str, Any]:
198 """Generate a realistic status based on the schema"""
199 if not schema or schema.get("type") != "object":
200 return {}
202 status = {}
203 properties = schema.get("properties", {})
205 # Check if resource is configured as not ready
206 is_ready = self._is_resource_ready(resource_body)
208 for field_name, field_schema in properties.items():
209 # Resolve any $ref in the field schema
210 if "$ref" in field_schema:
211 resolved_schema = self._resolve_reference(field_schema["$ref"])
212 if resolved_schema:
213 field_schema = resolved_schema
215 value = self._generate_value_for_field(field_name, field_schema, resource_body, is_ready)
216 if value is not None:
217 status[field_name] = value
219 return status
221 def _is_resource_ready(self, resource_body: dict[str, Any]) -> bool:
222 """Check if resource should be generated as ready"""
223 # Check annotations for test configuration
224 metadata = resource_body.get("metadata", {})
225 annotations = metadata.get("annotations", {})
227 # Allow configuration via annotation "fake-client.io/ready"
228 if annotations.get("fake-client.io/ready", "").lower() == "false":
229 return False
231 # Also check for a more specific ready status in spec
232 if "readyStatus" in resource_body.get("spec", {}):
233 return bool(resource_body["spec"]["readyStatus"])
235 # Default to ready
236 return True
238 def _generate_value_for_field(
239 self, field_name: str, field_schema: dict[str, Any], resource_body: dict[str, Any], is_ready: bool = True
240 ) -> Any:
241 """Generate a value for a specific field based on its schema"""
242 field_type = field_schema.get("type", "string")
244 if "enum" in field_schema:
245 # For enums, pick an appropriate value
246 return self._pick_enum_value(field_name, field_schema["enum"], is_ready)
248 if field_type == "string":
249 return self._generate_string_value(field_name, is_ready)
250 elif field_type == "integer":
251 return self._generate_integer_value(field_name, resource_body, is_ready)
252 elif field_type == "boolean":
253 return self._generate_boolean_value(field_name, is_ready)
254 elif field_type == "array":
255 return self._generate_array_value(field_name, field_schema, resource_body, is_ready)
256 elif field_type == "object":
257 return self._generate_object_value(field_name, field_schema, resource_body, is_ready)
259 return None
261 def _pick_enum_value(self, field_name: str, enum_values: list[str], is_ready: bool = True) -> str:
262 """Pick an appropriate enum value based on field name"""
263 if not enum_values:
264 return ""
266 # Smart selection based on field name and ready status
267 if field_name == "phase":
268 if is_ready:
269 # Prefer positive states
270 positive_states = ["Bound", "Running", "Active", "Ready", "Available", "Succeeded", "Complete"]
271 for state in positive_states:
272 if state in enum_values:
273 return state
274 else:
275 # Prefer negative/pending states
276 negative_states = ["Failed", "Error", "Terminating", "Pending", "Unknown"]
277 for state in negative_states:
278 if state in enum_values:
279 return state
280 # Fallback to first value
281 return enum_values[0]
283 elif field_name == "status" or field_name.endswith("Status"):
284 # For condition statuses
285 if is_ready and "True" in enum_values:
286 return "True"
287 elif not is_ready and "False" in enum_values:
288 return "False"
289 elif "Unknown" in enum_values:
290 return "Unknown"
292 elif field_name == "type" or field_name.endswith("Type"):
293 # For condition types, prefer "Ready" or "Available"
294 preferred = ["Ready", "Available", "Initialized", "Progressing"]
295 for pref in preferred:
296 if pref in enum_values:
297 return pref
299 # Default to first value
300 return enum_values[0]
302 def _generate_string_value(self, field_name: str, is_ready: bool = True) -> str:
303 """Generate a string value based on field name"""
304 if "time" in field_name.lower() or "timestamp" in field_name.lower():
305 return datetime.now(timezone.utc).isoformat()
306 elif field_name == "message":
307 return "Resource is ready" if is_ready else "Resource is not ready"
308 elif field_name == "reason":
309 return "ResourceReady" if is_ready else "ResourceNotReady"
310 elif field_name.endswith("IP"):
311 return f"10.0.0.{random.randint(1, 254)}"
312 elif field_name == "hostname":
313 return "node-1"
314 elif field_name == "storagePolicyID":
315 return "default-policy"
316 else:
317 return f"test-{field_name}"
319 def _generate_integer_value(self, field_name: str, resource_body: dict[str, Any], is_ready: bool = True) -> int:
320 """Generate an integer value based on field name"""
321 if "replicas" in field_name:
322 # Match the spec replicas if available
323 spec_replicas = resource_body.get("spec", {}).get("replicas", 1)
324 if is_ready:
325 if field_name in ["replicas", "readyReplicas", "availableReplicas", "updatedReplicas"]:
326 return spec_replicas
327 elif field_name == "unavailableReplicas":
328 return 0
329 else:
330 if field_name == "replicas":
331 return spec_replicas
332 elif field_name in ["readyReplicas", "availableReplicas", "updatedReplicas"]:
333 return 0
334 elif field_name == "unavailableReplicas":
335 return spec_replicas
336 elif field_name == "observedGeneration":
337 return resource_body.get("metadata", {}).get("generation", 1)
338 elif field_name == "restartCount":
339 return 0
340 elif field_name.endswith("Count"):
341 return 1
342 else:
343 return 1
344 return 1
346 def _generate_boolean_value(self, field_name: str, is_ready: bool = True) -> bool:
347 """Generate a boolean value based on field name"""
348 # Generally prefer positive values unless not ready
349 negative_indicators = ["disabled", "failed", "error", "unavailable", "notready"]
350 field_lower = field_name.lower()
352 for indicator in negative_indicators:
353 if indicator in field_lower:
354 return False
356 return is_ready
358 def _generate_array_value(
359 self, field_name: str, field_schema: dict[str, Any], resource_body: dict[str, Any], is_ready: bool = True
360 ) -> list[Any]:
361 """Generate an array value based on field schema"""
362 items_schema = field_schema.get("items", {})
364 # Resolve $ref in items if present
365 if "$ref" in items_schema:
366 resolved_items = self._resolve_reference(items_schema["$ref"])
367 if resolved_items:
368 items_schema = resolved_items
370 if field_name == "conditions":
371 # Generate standard conditions
372 return self._generate_conditions(is_ready)
373 elif field_name == "accessModes":
374 # Use access modes from spec if available
375 spec_modes = resource_body.get("spec", {}).get("accessModes", ["ReadWriteOnce"])
376 return spec_modes
377 elif field_name == "addresses":
378 return [{"type": "InternalIP", "address": "10.0.0.1"}]
379 elif field_name == "ports":
380 return resource_body.get("spec", {}).get("ports", [{"port": 80}])
381 else:
382 # Generate a single item based on the items schema
383 item = self._generate_value_for_field(f"{field_name}_item", items_schema, resource_body, is_ready)
384 return [item] if item is not None else []
386 def _generate_object_value(
387 self, field_name: str, field_schema: dict[str, Any], resource_body: dict[str, Any], is_ready: bool = True
388 ) -> dict[str, Any]:
389 """Generate an object value based on field schema"""
390 if field_name == "capacity":
391 # For PVC capacity, match the requested storage
392 requested = resource_body.get("spec", {}).get("resources", {}).get("requests", {})
393 return requested.copy() if requested else {"storage": "1Gi"}
394 elif field_name == "allocatedResources":
395 # Match capacity
396 return self._generate_object_value("capacity", field_schema, resource_body, is_ready)
397 elif "properties" in field_schema:
398 # Generate based on properties
399 obj = {}
400 for prop_name, prop_schema in field_schema["properties"].items():
401 # Resolve any $ref in property schema
402 if "$ref" in prop_schema:
403 resolved = self._resolve_reference(prop_schema["$ref"])
404 if resolved:
405 prop_schema = resolved
407 value = self._generate_value_for_field(prop_name, prop_schema, resource_body, is_ready)
408 if value is not None:
409 obj[prop_name] = value
410 return obj
411 elif "additionalProperties" in field_schema:
412 # For maps like capacity, labels, etc.
413 if field_name in ["capacity", "allocatedResources", "requests", "limits"]:
414 return {"storage": "1Gi", "cpu": "100m", "memory": "128Mi"}
415 else:
416 return {"key1": "value1"}
417 else:
418 return {}
420 def _generate_conditions(self, is_ready: bool = True) -> list[dict[str, Any]]:
421 """Generate standard Kubernetes conditions"""
422 if is_ready:
423 return [
424 {
425 "type": "Ready",
426 "status": "True",
427 "lastTransitionTime": datetime.now(timezone.utc).isoformat(),
428 "reason": "ResourceReady",
429 "message": "Resource is ready",
430 },
431 {
432 "type": "Available",
433 "status": "True",
434 "lastTransitionTime": datetime.now(timezone.utc).isoformat(),
435 "reason": "ResourceAvailable",
436 "message": "Resource is available",
437 },
438 ]
439 else:
440 return [
441 {
442 "type": "Ready",
443 "status": "False",
444 "lastTransitionTime": datetime.now(timezone.utc).isoformat(),
445 "reason": "ResourceNotReady",
446 "message": "Resource is not ready",
447 },
448 {
449 "type": "Available",
450 "status": "False",
451 "lastTransitionTime": datetime.now(timezone.utc).isoformat(),
452 "reason": "ResourceUnavailable",
453 "message": "Resource is unavailable",
454 },
455 ]