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

1"""Discovery functions for finding cluster resources and generated files.""" 

2 

3import ast 

4import os 

5from concurrent.futures import Future, ThreadPoolExecutor, as_completed 

6from pathlib import Path 

7from typing import Any 

8 

9from kubernetes.dynamic import DynamicClient 

10from simple_logger.logger import get_logger 

11 

12from class_generator.constants import END_OF_GENERATED_CODE 

13from class_generator.utils import ResourceScanner 

14from ocp_resources.resource import get_client 

15 

16LOGGER = get_logger(name=__name__) 

17 

18 

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. 

24 

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") 

28 

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__}") 

36 

37 discovered_resources: dict[str, list[dict[str, Any]]] = {} 

38 

39 try: 

40 # Use the underlying kubernetes client to get API resources 

41 k8s_client = client.client 

42 

43 # Create a thread pool for parallel discovery 

44 with ThreadPoolExecutor(max_workers=20) as executor: 

45 futures: list[Future] = [] 

46 

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 ) 

57 

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", [] 

71 

72 # Submit core API discovery 

73 futures.append(executor.submit(process_core_api)) 

74 

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 ) 

86 

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, [] 

100 

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 ) 

110 

111 if groups_response and "groups" in groups_response: 

112 for group in groups_response["groups"]: 

113 group_name = group.get("name", "") 

114 

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 

119 

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)) 

126 

127 except Exception as e: 

128 LOGGER.debug(f"Failed to get API groups: {e}") 

129 

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() 

138 

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() 

143 

144 for crd in crd_items: 

145 crd_group = crd.spec.group 

146 

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 

151 

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])) 

161 

162 except Exception as e: 

163 LOGGER.debug(f"Failed to discover CRDs: {e}") 

164 return results 

165 

166 # Submit CRD discovery 

167 crd_future = executor.submit(process_crds) 

168 

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}") 

184 

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}") 

199 

200 except Exception as e: 

201 LOGGER.error(f"Failed to discover cluster resources: {e}") 

202 raise 

203 

204 return discovered_resources 

205 

206 

207def discover_generated_resources() -> list[dict[str, Any]]: 

208 """ 

209 Discover all generated resource files in the ocp_resources directory. 

210 

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() 

221 

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() 

227 

228 has_user_code = END_OF_GENERATED_CODE in content and len(content.split(END_OF_GENERATED_CODE)[1].strip()) > 0 

229 

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 }) 

236 

237 return resources