Coverage for fake_kubernetes_client/resource_storage.py: 13%
119 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"""FakeResourceStorage implementation for fake Kubernetes client"""
3import copy
4from collections import defaultdict
5from typing import Any, DefaultDict, Union
8class FakeResourceStorage:
9 """In-memory storage for Kubernetes resources"""
11 def __init__(self) -> None:
12 # Storage structure: {api_version: {kind: {namespace: {name: resource}}}}
13 self.resources: DefaultDict[str, DefaultDict[str, DefaultDict[Union[str, None], dict[str, Any]]]] = defaultdict(
14 lambda: defaultdict(lambda: defaultdict(dict))
15 )
17 def store_resource(
18 self, kind: str, api_version: str, name: str, namespace: Union[str, None], resource: dict[str, Any]
19 ) -> None:
20 """Store a resource"""
21 self.resources[api_version][kind][namespace][name] = copy.deepcopy(resource)
23 def get_resource(
24 self, kind: str, api_version: str, name: str, namespace: Union[str, None]
25 ) -> Union[dict[str, Any], None]:
26 """Get a specific resource"""
27 api_resources = self.resources.get(api_version)
28 if not api_resources:
29 return None
30 kind_resources = api_resources.get(kind)
31 if not kind_resources:
32 return None
33 namespace_resources = kind_resources.get(namespace)
34 if not namespace_resources:
35 return None
36 resource = namespace_resources.get(name)
37 return copy.deepcopy(resource) if resource else None
39 def list_resources(
40 self,
41 kind: str,
42 api_version: str,
43 namespace: Union[str, None] = None,
44 label_selector: Union[str, None] = None,
45 field_selector: Union[str, None] = None,
46 ) -> list[dict[str, Any]]:
47 """List resources with optional filtering"""
48 resources: list[dict[str, Any]] = []
50 api_resources = self.resources.get(api_version)
51 if not api_resources:
52 return resources
54 kind_resources = api_resources.get(kind)
55 if not kind_resources:
56 return resources
58 if namespace is not None:
59 # List resources in specific namespace
60 namespace_resources = kind_resources.get(namespace, {})
61 resources = list(namespace_resources.values())
62 else:
63 # List resources across all namespaces
64 for ns_resources in kind_resources.values():
65 resources.extend(ns_resources.values())
67 # Apply label selector filter
68 if label_selector:
69 resources = self._filter_by_labels(resources, label_selector)
71 # Apply field selector filter
72 if field_selector:
73 resources = self._filter_by_fields(resources, field_selector)
75 return [copy.deepcopy(r) for r in resources]
77 def delete_resource(
78 self, kind: str, api_version: str, name: str, namespace: Union[str, None]
79 ) -> Union[dict[str, Any], None]:
80 """Delete a resource"""
81 resource = self.get_resource(kind, api_version, name, namespace)
82 if resource:
83 del self.resources[api_version][kind][namespace][name]
84 # Clean up empty structures
85 if not self.resources[api_version][kind][namespace]:
86 del self.resources[api_version][kind][namespace]
87 if not self.resources[api_version][kind]:
88 del self.resources[api_version][kind]
89 if not self.resources[api_version]:
90 del self.resources[api_version]
91 return resource
93 def _filter_by_labels(self, resources: list[dict[str, Any]], label_selector: str) -> list[dict[str, Any]]:
94 """Filter resources by label selector"""
95 filtered = []
96 for resource in resources:
97 labels = resource.get("metadata", {}).get("labels", {})
98 if self._matches_label_selector(labels, label_selector):
99 filtered.append(resource)
100 return filtered
102 def _matches_label_selector(self, labels: dict[str, str], selector: str) -> bool:
103 """Check if labels match selector (simplified implementation)"""
104 # Handle simple equality selectors (key=value)
105 parts = selector.split(",")
106 for part in parts:
107 if "=" in part:
108 key, value = part.split("=", 1)
109 if labels.get(key.strip()) != value.strip():
110 return False
111 elif "!=" in part:
112 key, value = part.split("!=", 1)
113 if labels.get(key.strip()) == value.strip():
114 return False
115 else:
116 # Just key presence check
117 if part.strip() not in labels:
118 return False
119 return True
121 def _filter_by_fields(self, resources: list[dict[str, Any]], field_selector: str) -> list[dict[str, Any]]:
122 """Filter resources by field selector"""
123 filtered = []
124 for resource in resources:
125 if self._matches_field_selector(resource, field_selector):
126 filtered.append(resource)
127 return filtered
129 def _matches_field_selector(self, resource: dict[str, Any], selector: str) -> bool:
130 """Check if resource matches field selector"""
131 # Handle simple field selectors (field.path=value or field.path==value)
132 parts = selector.split(",")
133 for part in parts:
134 if "==" in part:
135 # Handle double equals
136 field_path, selector_value = part.split("==", 1)
137 field_value = self._get_field_value(resource, field_path.strip())
138 if not self._compare_field_values(field_value, selector_value.strip()):
139 return False
140 elif "=" in part:
141 # Handle single equals
142 field_path, selector_value = part.split("=", 1)
143 field_value = self._get_field_value(resource, field_path.strip())
144 if not self._compare_field_values(field_value, selector_value.strip()):
145 return False
146 return True
148 def _compare_field_values(self, field_value: Any, selector_value: str) -> bool:
149 """
150 Compare field value with selector value using type-aware comparison.
152 Attempts to parse the selector value to match the field value's type.
153 """
154 # Handle missing fields - they don't match any selector
155 if field_value is NotImplemented:
156 return False
158 # Handle None/null values
159 if field_value is None:
160 return selector_value.lower() in ("none", "null", "")
162 # Handle boolean values
163 if isinstance(field_value, bool):
164 if selector_value.lower() == "true":
165 return field_value is True
166 elif selector_value.lower() == "false":
167 return field_value is False
168 else:
169 return False
171 # Handle numeric values
172 if isinstance(field_value, (int, float)):
173 try:
174 # Try to parse as number
175 if "." in selector_value:
176 return field_value == float(selector_value)
177 else:
178 return field_value == int(selector_value)
179 except ValueError:
180 # If parsing fails, fall back to string comparison
181 return str(field_value) == selector_value
183 # Handle string values (default case)
184 return str(field_value) == selector_value
186 def _get_field_value(self, obj: dict[str, Any], path: str) -> Any:
187 """Get value from nested dictionary using dot notation"""
188 current = obj
189 parts = path.split(".")
191 for i, part in enumerate(parts):
192 if isinstance(current, dict):
193 if part not in current:
194 # Return a sentinel value to indicate the field doesn't exist
195 return NotImplemented
196 current = current[part]
197 else:
198 return NotImplemented
200 return current