Coverage for fake_kubernetes_client/resource_registry.py: 51%
142 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"""FakeResourceRegistry implementation for fake Kubernetes client"""
3import logging
4from collections import defaultdict
5from typing import Any, DefaultDict, Union
7from fake_kubernetes_client.resource_field import FakeResourceField
8from ocp_resources.utils.schema_validator import SchemaValidator
10logger = logging.getLogger(__name__)
13class FakeResourceRegistry:
14 """Registry for resource definitions"""
16 def __init__(self) -> None:
17 # Store by kind to allow searching across API groups
18 self.resources: DefaultDict[str, list[dict[str, Any]]] = defaultdict(list)
19 self._resource_mappings_cache: Union[dict[str, Any], None] = None
20 self._builtin_resources: dict[tuple[str, str], dict[str, Any]] = {}
21 self._additional_resources: dict[str, list[dict[str, Any]]] = {}
22 self._load_resource_definitions()
23 self._register_additional_resources()
25 def _generate_plural_form(self, kind: str) -> str:
26 """Generate plural form of resource kind using standard Kubernetes rules"""
27 kind_lower = kind.lower()
28 if kind_lower.endswith("s"):
29 return kind_lower
30 elif kind_lower.endswith("y"):
31 return kind_lower[:-1] + "ies"
32 elif kind_lower.endswith(("sh", "ch", "x", "z")):
33 return kind_lower + "es"
34 else:
35 return kind_lower + "s"
37 def _get_resource_mappings(self) -> dict[str, Any]:
38 """Load and cache the resource mappings file using SchemaValidator"""
39 if self._resource_mappings_cache is None:
40 # Use SchemaValidator to load the mappings - single source of truth
41 if SchemaValidator.load_mappings_data():
42 self._resource_mappings_cache = SchemaValidator.get_mappings_data() or {}
43 logger.debug(f"Loaded {len(self._resource_mappings_cache)} resource mappings from SchemaValidator")
44 else:
45 logger.error("Failed to load resource mappings from SchemaValidator")
46 self._resource_mappings_cache = {}
48 return self._resource_mappings_cache
50 def _apply_known_corrections(self, kind: str, resource_def: dict[str, Any]) -> dict[str, Any]:
51 """Apply known corrections to resource definitions"""
52 # Known corrections for incorrect data in the JSON file
53 corrections = {
54 # Service is incorrectly marked as non-namespaced in the JSON
55 "Service": {"namespaced": True},
56 # Event is incorrectly marked as non-namespaced in the JSON
57 "Event": {"namespaced": True},
58 # Add other known corrections here as needed
59 }
61 if kind in corrections:
62 resource_def.update(corrections[kind])
64 return resource_def
66 def _load_resource_definitions(self) -> None:
67 """Load resource definitions from JSON file - single source of truth"""
68 mappings = self._get_resource_mappings()
69 if not mappings:
70 # If no mappings file found, registry will be empty
71 # This will cause proper errors when resources are requested
72 return
74 for kind_lower, resource_mappings in mappings.items():
75 if not isinstance(resource_mappings, list) or not resource_mappings:
76 continue
78 # Process all mappings for this kind (there might be multiple API groups)
79 for mapping in resource_mappings:
80 # Extract kubernetes metadata from x-kubernetes-group-version-kind
81 k8s_gvk = mapping.get("x-kubernetes-group-version-kind", [])
82 if not k8s_gvk or not isinstance(k8s_gvk, list) or not k8s_gvk:
83 continue
85 # Process each group-version-kind entry
86 for gvk in k8s_gvk:
87 schema_group = gvk.get("group", "")
88 schema_version = gvk.get("version")
89 schema_kind = gvk.get("kind", kind_lower.title())
91 # Skip if no version found in mappings
92 if not schema_version:
93 continue
95 # Build full API version
96 if schema_group:
97 full_api_version = f"{schema_group}/{schema_version}"
98 else:
99 full_api_version = schema_version
101 # Get namespace info from mappings
102 is_namespaced = mapping.get("namespaced")
103 if is_namespaced is None:
104 continue
106 # Generate plural form
107 plural = self._generate_plural_form(schema_kind)
109 resource_def = {
110 "kind": schema_kind,
111 "api_version": schema_version, # Just version part for KubeAPIVersion compatibility
112 "group": schema_group,
113 "version": schema_version,
114 "group_version": full_api_version, # Full group/version for storage operations
115 "plural": plural,
116 "singular": schema_kind.lower(),
117 "namespaced": is_namespaced,
118 "shortNames": [],
119 "categories": ["all"],
120 "schema_source": "mappings",
121 }
123 # Apply known corrections
124 resource_def = self._apply_known_corrections(schema_kind, resource_def)
126 # Store in both places for compatibility
127 self.resources[schema_kind].append(resource_def)
128 key = (full_api_version, schema_kind)
129 self._builtin_resources[key] = resource_def
131 def _register_additional_resources(self) -> None:
132 """Register additional resources that are not in OpenShift schema"""
133 # MTQ resources (not in OpenShift schema)
134 additional_resources = [
135 {
136 "kind": "MigrationToolkitQuota",
137 "api_version": "v1alpha1",
138 "group": "mtq.kubevirt.io",
139 "version": "v1alpha1",
140 "group_version": "mtq.kubevirt.io/v1alpha1",
141 "plural": "migrationtoolkitquotas",
142 "singular": "migrationtoolkitquota",
143 "namespaced": False,
144 "shortNames": ["mtq"],
145 "categories": ["all"],
146 "schema_source": "additional",
147 },
148 {
149 "kind": "MTQ",
150 "api_version": "v1alpha1",
151 "group": "mtq.kubevirt.io",
152 "version": "v1alpha1",
153 "group_version": "mtq.kubevirt.io/v1alpha1",
154 "plural": "mtqs",
155 "singular": "mtq",
156 "namespaced": False,
157 "shortNames": [],
158 "categories": ["all"],
159 "schema_source": "additional",
160 },
161 {
162 "kind": "Service",
163 "api_version": "v1",
164 "group": "serving.knative.dev",
165 "version": "v1",
166 "group_version": "serving.knative.dev/v1",
167 "plural": "services",
168 "singular": "service",
169 "namespaced": True,
170 "shortNames": ["ksvc"],
171 "categories": ["all"],
172 "schema_source": "additional",
173 },
174 {
175 "kind": "PodMetrics",
176 "api_version": "v1beta1",
177 "group": "metrics.k8s.io",
178 "version": "v1beta1",
179 "group_version": "metrics.k8s.io/v1beta1",
180 "plural": "podmetrics",
181 "singular": "podmetrics",
182 "namespaced": True,
183 "shortNames": [],
184 "categories": ["all"],
185 "schema_source": "additional",
186 },
187 {
188 "kind": "Image",
189 "api_version": "v1alpha1",
190 "group": "caching.internal.knative.dev",
191 "version": "v1alpha1",
192 "group_version": "caching.internal.knative.dev/v1alpha1",
193 "plural": "images",
194 "singular": "image",
195 "namespaced": True,
196 "shortNames": [],
197 "categories": ["all"],
198 "schema_source": "additional",
199 },
200 ]
202 for resource_def in additional_resources:
203 kind = str(resource_def["kind"])
204 group_version = str(resource_def["group_version"])
205 self.resources[kind].append(resource_def)
206 key = (group_version, kind)
207 self._builtin_resources[key] = resource_def
208 self._additional_resources.setdefault(kind, []).append(resource_def)
210 def register_resources(self, resources: Union[dict[str, Any], list[dict[str, Any]]]) -> None:
211 """
212 Register custom resources dynamically.
214 Args:
215 resources: Either a single resource definition dict or a list of resource definitions.
216 Each resource definition should contain:
217 - kind: Resource kind (required)
218 - api_version: API version without group (required)
219 - group: API group (optional, empty string for core resources)
220 - version: Same as api_version (required)
221 - group_version: Full group/version string (required)
222 - plural: Plural name (optional, will be generated if not provided)
223 - singular: Singular name (optional, defaults to lowercase kind)
224 - namespaced: Whether resource is namespaced (optional, defaults to True)
225 - shortNames: List of short names (optional)
226 - categories: List of categories (optional, defaults to ["all"])
228 Example:
229 client.registry.register_resources({
230 "kind": "MyCustomResource",
231 "api_version": "v1alpha1",
232 "group": "example.com",
233 "version": "v1alpha1",
234 "group_version": "example.com/v1alpha1",
235 "plural": "mycustomresources",
236 "singular": "mycustomresource",
237 "namespaced": True,
238 "shortNames": ["mcr"],
239 "categories": ["all"]
240 })
241 """
242 # Convert single resource to list for uniform processing
243 resource_list = [resources] if isinstance(resources, dict) else resources
245 for resource_def in resource_list:
246 # Validate required fields
247 if not isinstance(resource_def, dict):
248 raise ValueError(f"Resource definition must be a dictionary, got {type(resource_def)}")
250 kind = resource_def.get("kind", "")
251 if not kind:
252 raise ValueError("Resource definition must have 'kind' field")
254 api_version = resource_def.get("api_version", "")
255 if not api_version:
256 raise ValueError(f"Resource {kind} must have 'api_version' field")
258 # Build complete resource definition with defaults
259 group = resource_def.get("group", "")
260 version = resource_def.get("version", api_version)
262 # Generate group_version if not provided
263 if "group_version" not in resource_def:
264 group_version = f"{group}/{version}" if group else version
265 else:
266 group_version = resource_def["group_version"]
268 # Generate plural if not provided
269 plural = resource_def.get("plural", self._generate_plural_form(kind))
271 # Build complete resource definition
272 complete_def = {
273 "kind": kind,
274 "api_version": api_version,
275 "group": group,
276 "version": version,
277 "group_version": group_version,
278 "plural": plural,
279 "singular": resource_def.get("singular", kind.lower()),
280 "namespaced": resource_def.get("namespaced", True),
281 "shortNames": resource_def.get("shortNames", []),
282 "categories": resource_def.get("categories", ["all"]),
283 "schema_source": "user_defined",
284 }
286 # Register the resource
287 self.resources[kind].append(complete_def)
288 key = (str(group_version), str(kind))
289 self._builtin_resources[key] = complete_def
290 self._additional_resources.setdefault(kind, []).append(complete_def)
292 def search(
293 self,
294 kind: Union[str, None] = None,
295 group: Union[str, None] = None,
296 api_version: Union[str, None] = None,
297 **kwargs: Any,
298 ) -> list[FakeResourceField]:
299 """Search for resource definitions"""
300 results = []
302 # If searching by kind and group, look for that specific combination
303 if kind and group is not None:
304 definitions = self.resources.get(kind, [])
305 for definition in definitions:
306 if definition.get("group") == group:
307 results.append(FakeResourceField(data=definition))
308 else:
309 # General search through all resources
310 for resource_kind, definitions in self.resources.items():
311 # Filter by kind if specified
312 if kind and resource_kind != kind:
313 continue
315 for definition in definitions:
316 # Filter by group if specified
317 if group and definition.get("group") != group:
318 continue
319 # Filter by api_version if specified
320 if api_version and definition.get("group_version") != api_version:
321 continue
323 results.append(FakeResourceField(data=definition))
325 return results
327 def get_resource_definitions(self, kind: str) -> list[dict[str, Any]]:
328 """Get all resource definitions for a kind"""
329 return self.resources.get(kind, [])
331 def get_resource_definition(self, kind: str, api_version: str) -> Union[dict[str, Any], None]:
332 """Get specific resource definition by kind and API version"""
333 definitions = self.resources.get(kind, [])
335 # If api_version doesn't contain '/', it's a core resource (no group)
336 if "/" not in api_version:
337 # First, try to find a core resource (empty group) with this version
338 for definition in definitions:
339 if definition.get("group") == "" and definition["version"] == api_version:
340 return definition
342 # If not found, fall back to checking group_version for compatibility
343 for definition in definitions:
344 if definition.get("group_version") == api_version:
345 return definition
346 else:
347 # API version contains group, do exact match on group_version
348 for definition in definitions:
349 if definition.get("group_version") == api_version:
350 return definition
352 return None
354 def get_resource_definition_by_plural(self, plural: str, api_version: str) -> Union[dict[str, Any], None]:
355 """Get resource definition by plural name and API version"""
356 for kind, definitions in self.resources.items():
357 for definition in definitions:
358 if definition.get("plural") == plural and (
359 definition["api_version"] == api_version or definition.get("group_version") == api_version
360 ):
361 return definition
362 return None
364 def list_api_resources(self, api_version: str) -> FakeResourceField:
365 """List all resources for an API version"""
366 resources = []
367 for kind, definitions in self.resources.items():
368 for definition in definitions:
369 # Check both api_version and group_version
370 if definition["api_version"] == api_version or definition.get("group_version") == api_version:
371 resources.append({
372 "name": definition.get("plural", f"{kind.lower()}s"),
373 "singularName": definition.get("singular", kind.lower()),
374 "namespaced": definition.get("namespaced", True),
375 "kind": kind,
376 "verbs": ["create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"],
377 })
379 # Create APIResourceList response
380 response = {
381 "apiVersion": "v1",
382 "groupVersion": api_version,
383 "kind": "APIResourceList",
384 "resources": resources,
385 }
387 return FakeResourceField(data=response)