Coverage for class_generator/core/discovery.py: 74%
120 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"""Discovery functions for finding cluster resources and generated files."""
3import ast
4import os
5from concurrent.futures import Future, ThreadPoolExecutor, as_completed
6from pathlib import Path
7from typing import Any
9from kubernetes.dynamic import DynamicClient
10from simple_logger.logger import get_logger
12from class_generator.constants import END_OF_GENERATED_CODE
13from class_generator.utils import ResourceScanner
14from ocp_resources.resource import get_client
16LOGGER = get_logger(name=__name__)
19def discover_cluster_resources(
20 client: DynamicClient | None = None, api_group_filter: str | None = None
21) -> dict[str, list[dict[str, Any]]]:
22 """
23 Discover all resources available in the cluster.
25 Args:
26 client: Kubernetes dynamic client. If None, will create one.
27 api_group_filter: Filter resources by API group (e.g., "apps", "route.openshift.io")
29 Returns:
30 Dictionary mapping API version to list of resources
31 """
32 if not client:
33 client = get_client()
34 if not isinstance(client, DynamicClient):
35 raise ValueError(f"Expected DynamicClient instance, got {type(client).__name__}")
37 discovered_resources: dict[str, list[dict[str, Any]]] = {}
39 try:
40 # Use the underlying kubernetes client to get API resources
41 k8s_client = client.client
43 # Create a thread pool for parallel discovery
44 with ThreadPoolExecutor(max_workers=20) as executor:
45 futures: list[Future] = []
47 # Function to process core API resources
48 def process_core_api() -> tuple[str, list[dict[str, Any]]]:
49 try:
50 response = k8s_client.call_api(
51 resource_path="/api/v1",
52 method="GET",
53 auth_settings=["BearerToken"],
54 response_type="object",
55 _return_http_data_only=True,
56 )
58 if response and "resources" in response:
59 resources = []
60 for r in response["resources"]:
61 if "/" not in r.get("name", ""): # Filter out subresources
62 resources.append({
63 "name": r.get("name"),
64 "kind": r.get("kind"),
65 "namespaced": r.get("namespaced", True),
66 })
67 return "v1", resources
68 except Exception as e:
69 LOGGER.debug(f"Failed to get core API resources: {e}")
70 return "v1", []
72 # Submit core API discovery
73 futures.append(executor.submit(process_core_api))
75 # Function to process a specific API group version
76 def process_api_group_version(group_version: str) -> tuple[str, list[dict[str, Any]]]:
77 try:
78 api_path = f"/apis/{group_version}"
79 resources_response = k8s_client.call_api(
80 resource_path=api_path,
81 method="GET",
82 auth_settings=["BearerToken"],
83 response_type="object",
84 _return_http_data_only=True,
85 )
87 if resources_response and "resources" in resources_response:
88 resources = []
89 for r in resources_response["resources"]:
90 if "/" not in r.get("name", ""): # Filter out subresources
91 resources.append({
92 "name": r.get("name"),
93 "kind": r.get("kind"),
94 "namespaced": r.get("namespaced", True),
95 })
96 return group_version, resources
97 except Exception as e:
98 LOGGER.debug(f"Failed to get resources for {group_version}: {e}")
99 return group_version, []
101 # Get all API groups
102 try:
103 groups_response = k8s_client.call_api(
104 resource_path="/apis",
105 method="GET",
106 auth_settings=["BearerToken"],
107 response_type="object",
108 _return_http_data_only=True,
109 )
111 if groups_response and "groups" in groups_response:
112 for group in groups_response["groups"]:
113 group_name = group.get("name", "")
115 # Apply filter if specified
116 if api_group_filter and group_name != api_group_filter:
117 if api_group_filter not in group_name:
118 continue
120 # Process each version in the group
121 for version in group.get("versions", []):
122 group_version = version.get("groupVersion", "")
123 if group_version:
124 # Submit API group version discovery to thread pool
125 futures.append(executor.submit(process_api_group_version, group_version))
127 except Exception as e:
128 LOGGER.debug(f"Failed to get API groups: {e}")
130 # Function to process CRDs
131 def process_crds() -> list[tuple[str, list[dict[str, Any]]]]:
132 results = []
133 try:
134 crd_resources = client.resources.get(
135 api_version="apiextensions.k8s.io/v1", kind="CustomResourceDefinition"
136 )
137 crds = crd_resources.get()
139 # Check if items is iterable
140 crd_items = crds.items if hasattr(crds, "items") else []
141 if callable(crd_items):
142 crd_items = crd_items()
144 for crd in crd_items:
145 crd_group = crd.spec.group
147 # Apply filter if specified
148 if api_group_filter and crd_group != api_group_filter:
149 if api_group_filter not in crd_group:
150 continue
152 for version in crd.spec.versions:
153 if version.served:
154 group_version = f"{crd_group}/{version.name}"
155 resource_info = {
156 "name": crd.spec.names.plural,
157 "kind": crd.spec.names.kind,
158 "namespaced": crd.spec.scope == "Namespaced",
159 }
160 results.append((group_version, [resource_info]))
162 except Exception as e:
163 LOGGER.debug(f"Failed to discover CRDs: {e}")
164 return results
166 # Submit CRD discovery
167 crd_future = executor.submit(process_crds)
169 # Collect results from all futures
170 for future in as_completed(futures):
171 try:
172 api_version, resources = future.result()
173 if resources:
174 if api_version in discovered_resources:
175 # Merge resources, avoiding duplicates
176 existing_kinds = {r["kind"] for r in discovered_resources[api_version]}
177 for resource in resources:
178 if resource["kind"] not in existing_kinds:
179 discovered_resources[api_version].append(resource)
180 else:
181 discovered_resources[api_version] = resources
182 except Exception as e:
183 LOGGER.debug(f"Failed to process discovery result: {e}")
185 # Process CRD results
186 try:
187 crd_results = crd_future.result()
188 for group_version, resources in crd_results:
189 if group_version in discovered_resources:
190 # Merge, avoiding duplicates
191 existing_kinds = {r["kind"] for r in discovered_resources[group_version]}
192 for resource in resources:
193 if resource["kind"] not in existing_kinds:
194 discovered_resources[group_version].append(resource)
195 else:
196 discovered_resources[group_version] = resources
197 except Exception as e:
198 LOGGER.debug(f"Failed to process CRD results: {e}")
200 except Exception as e:
201 LOGGER.error(f"Failed to discover cluster resources: {e}")
202 raise
204 return discovered_resources
207def discover_generated_resources() -> list[dict[str, Any]]:
208 """
209 Discover all generated resource files in the ocp_resources directory.
211 Returns:
212 List of dictionaries containing:
213 - path: Full path to the file
214 - kind: Resource class name
215 - filename: File name without extension
216 - has_user_code: Whether file contains user modifications
217 """
218 # Reuse existing ResourceScanner
219 scanner = ResourceScanner()
220 resource_infos = scanner.scan_resources()
222 resources = []
223 for info in resource_infos:
224 # Read file to check for user code
225 with open(info.file_path, "r") as f:
226 content = f.read()
228 has_user_code = END_OF_GENERATED_CODE in content and len(content.split(END_OF_GENERATED_CODE)[1].strip()) > 0
230 resources.append({
231 "path": info.file_path,
232 "kind": info.name,
233 "filename": Path(info.file_path).stem,
234 "has_user_code": has_user_code
235 })
237 return resources