Coverage for fake_kubernetes_client/status_templates.py: 11%

103 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2025-07-29 12:31 +0300

1"""Status template methods for fake Kubernetes resources""" 

2 

3from datetime import datetime, timezone 

4from typing import Any, Union 

5 

6from fake_kubernetes_client.status_schema_parser import StatusSchemaParser 

7 

8 

9def _get_ready_status_config(body: dict[str, Any]) -> tuple[str, str, str]: 

10 """ 

11 Get ready status configuration from resource annotations or spec. 

12 

13 Returns: 

14 tuple: (status, reason, message) - status is "True" or "False" 

15 """ 

16 # Default to ready 

17 status = "True" 

18 reason = "ResourceReady" 

19 message = "Resource is ready" 

20 

21 # Check annotations for test configuration 

22 metadata = body.get("metadata", {}) 

23 annotations = metadata.get("annotations", {}) 

24 

25 # Allow configuration via annotation "fake-client.io/ready" 

26 if annotations.get("fake-client.io/ready", "").lower() == "false": 

27 status = "False" 

28 reason = "ResourceNotReady" 

29 message = "Resource is not ready" 

30 

31 # Also check for a more specific ready status in spec 

32 if "readyStatus" in body.get("spec", {}): 

33 if body["spec"]["readyStatus"]: 

34 status = "True" 

35 reason = "ResourceReady" 

36 message = "Resource is ready" 

37 else: 

38 status = "False" 

39 reason = "ResourceNotReady" 

40 message = "Resource is not ready" 

41 

42 return status, reason, message 

43 

44 

45def add_realistic_status(body: dict[str, Any], resource_mappings: Union[dict[str, Any], None] = None) -> None: 

46 """Add realistic status to resources that need it""" 

47 kind = body.get("kind", "") 

48 

49 # First check if we have a hardcoded template 

50 if kind == "Pod": 

51 status = get_pod_status_template(body=body) 

52 elif kind == "Deployment": 

53 status = get_deployment_status_template(body=body) 

54 elif kind == "Service": 

55 status = get_service_status_template(body=body) 

56 elif kind == "Namespace": 

57 status = get_namespace_status_template(body=body) 

58 else: 

59 # Try schema-based generation if mappings are available 

60 if resource_mappings: 

61 status = generate_dynamic_status(body=body, resource_mappings=resource_mappings) 

62 else: 

63 # Fallback to generic status 

64 status = get_generic_status_template(body=body) 

65 

66 if status: 

67 body["status"] = status 

68 

69 

70def generate_dynamic_status(body: dict[str, Any], resource_mappings: dict[str, Any]) -> dict[str, Any]: 

71 """Generate status dynamically based on resource schema""" 

72 kind = body.get("kind", "") 

73 api_version = body.get("apiVersion", "v1") 

74 

75 parser = StatusSchemaParser(resource_mappings=resource_mappings) 

76 status_schema = parser.get_status_schema_for_resource(kind=kind, api_version=api_version) 

77 

78 if status_schema: 

79 return parser.generate_status_from_schema(schema=status_schema, resource_body=body) 

80 else: 

81 # Fallback to generic status 

82 return get_generic_status_template(body=body) 

83 

84 

85def get_pod_status_template(body: dict[str, Any]) -> dict[str, Any]: 

86 """Get realistic Pod status""" 

87 container_name = "test-container" 

88 if "spec" in body and "containers" in body["spec"] and body["spec"]["containers"]: 

89 container_name = body["spec"]["containers"][0].get("name", "test-container") 

90 

91 # For Pods, we need more specific configuration 

92 # Check for pod-specific annotation first, fall back to general ready annotation 

93 metadata = body.get("metadata", {}) 

94 annotations = metadata.get("annotations", {}) 

95 

96 # Default to ready 

97 ready_status = "True" 

98 ready_reason = "ContainersReady" 

99 ready_message = "All containers are ready" 

100 container_ready = True 

101 container_started = True 

102 container_state = {"running": {"startedAt": datetime.now(timezone.utc).isoformat()}} 

103 

104 # Check pod-specific annotation first 

105 if "fake-client.io/pod-ready" in annotations: 

106 if annotations["fake-client.io/pod-ready"].lower() == "false": 

107 ready_status = "False" 

108 ready_reason = "ContainersNotReady" 

109 ready_message = f"containers with unready status: [{container_name}]" 

110 container_ready = False 

111 container_started = False 

112 container_state = {"waiting": {"reason": "ContainerCreating"}} 

113 # Fall back to general ready annotation 

114 elif annotations.get("fake-client.io/ready", "").lower() == "false": 

115 ready_status = "False" 

116 ready_reason = "ContainersNotReady" 

117 ready_message = f"containers with unready status: [{container_name}]" 

118 container_ready = False 

119 container_started = False 

120 container_state = {"waiting": {"reason": "ContainerCreating"}} 

121 

122 # Check spec.readyStatus 

123 if "readyStatus" in body.get("spec", {}): 

124 ready_status = "True" if body["spec"]["readyStatus"] else "False" 

125 if ready_status == "False": 

126 ready_reason = "ContainersNotReady" 

127 ready_message = f"containers with unready status: [{container_name}]" 

128 container_ready = False 

129 container_started = False 

130 container_state = {"waiting": {"reason": "ContainerCreating"}} 

131 else: 

132 ready_reason = "ContainersReady" 

133 ready_message = "All containers are ready" 

134 container_ready = True 

135 container_started = True 

136 container_state = {"running": {"startedAt": datetime.now(timezone.utc).isoformat()}} 

137 

138 return { 

139 "phase": "Running", 

140 "conditions": [ 

141 { 

142 "type": "Initialized", 

143 "status": "True", 

144 "lastProbeTime": None, 

145 "lastTransitionTime": datetime.now(timezone.utc).isoformat(), 

146 "reason": "PodCompleted", 

147 }, 

148 { 

149 "type": "Ready", 

150 "status": ready_status, # Now configurable, defaults to True 

151 "lastProbeTime": None, 

152 "lastTransitionTime": datetime.now(timezone.utc).isoformat(), 

153 "reason": ready_reason, 

154 "message": ready_message, 

155 }, 

156 { 

157 "type": "ContainersReady", 

158 "status": ready_status, # Should match Ready status 

159 "lastProbeTime": None, 

160 "lastTransitionTime": datetime.now(timezone.utc).isoformat(), 

161 "reason": ready_reason, 

162 "message": ready_message, 

163 }, 

164 { 

165 "type": "PodScheduled", 

166 "status": "True", 

167 "lastProbeTime": None, 

168 "lastTransitionTime": datetime.now(timezone.utc).isoformat(), 

169 "reason": "PodScheduled", 

170 }, 

171 ], 

172 "hostIP": "10.0.0.1", 

173 "podIP": "10.244.0.2", 

174 "podIPs": [{"ip": "10.244.0.2"}], 

175 "startTime": datetime.now(timezone.utc).isoformat(), 

176 "containerStatuses": [ 

177 { 

178 "name": container_name, 

179 "state": container_state, 

180 "lastState": {}, 

181 "ready": container_ready, 

182 "restartCount": 0, 

183 "image": "nginx:latest", 

184 "imageID": "docker://sha256:nginx", 

185 "containerID": "docker://1234567890abcdef" if container_ready else "", 

186 "started": container_started, 

187 } 

188 ], 

189 } 

190 

191 

192def get_deployment_status_template(body: dict[str, Any]) -> dict[str, Any]: 

193 """Get realistic Deployment status""" 

194 # Get ready status configuration 

195 ready_status, ready_reason, ready_message = _get_ready_status_config(body=body) 

196 

197 # Adjust deployment-specific values based on ready status 

198 if ready_status == "True": 

199 replicas = body.get("spec", {}).get("replicas", 1) 

200 return { 

201 "replicas": replicas, 

202 "updatedReplicas": replicas, 

203 "readyReplicas": replicas, 

204 "availableReplicas": replicas, 

205 "observedGeneration": 1, 

206 "conditions": [ 

207 { 

208 "type": "Available", 

209 "status": "True", 

210 "lastUpdateTime": datetime.now(timezone.utc).isoformat(), 

211 "lastTransitionTime": datetime.now(timezone.utc).isoformat(), 

212 "reason": "MinimumReplicasAvailable", 

213 "message": "Deployment has minimum availability.", 

214 }, 

215 { 

216 "type": "Progressing", 

217 "status": "True", 

218 "lastUpdateTime": datetime.now(timezone.utc).isoformat(), 

219 "lastTransitionTime": datetime.now(timezone.utc).isoformat(), 

220 "reason": "NewReplicaSetAvailable", 

221 "message": "ReplicaSet has successfully progressed.", 

222 }, 

223 ], 

224 } 

225 else: 

226 replicas = body.get("spec", {}).get("replicas", 1) 

227 return { 

228 "replicas": replicas, 

229 "updatedReplicas": 0, 

230 "readyReplicas": 0, 

231 "availableReplicas": 0, 

232 "unavailableReplicas": replicas, 

233 "observedGeneration": 1, 

234 "conditions": [ 

235 { 

236 "type": "Available", 

237 "status": "False", 

238 "lastUpdateTime": datetime.now(timezone.utc).isoformat(), 

239 "lastTransitionTime": datetime.now(timezone.utc).isoformat(), 

240 "reason": "MinimumReplicasUnavailable", 

241 "message": "Deployment does not have minimum availability.", 

242 }, 

243 { 

244 "type": "Progressing", 

245 "status": "False", 

246 "lastUpdateTime": datetime.now(timezone.utc).isoformat(), 

247 "lastTransitionTime": datetime.now(timezone.utc).isoformat(), 

248 "reason": "ProgressDeadlineExceeded", 

249 "message": "ReplicaSet has timed out progressing.", 

250 }, 

251 ], 

252 } 

253 

254 

255def get_service_status_template(body: dict[str, Any]) -> dict[str, Any]: 

256 """Get realistic Service status""" 

257 # Services don't typically have complex status 

258 return {"loadBalancer": {}} 

259 

260 

261def get_namespace_status_template(body: dict[str, Any]) -> dict[str, Any]: 

262 """Get realistic Namespace status""" 

263 # Check if namespace should be terminating based on ready status 

264 ready_status, _, _ = _get_ready_status_config(body=body) 

265 

266 if ready_status == "True": 

267 return { 

268 "phase": "Active", 

269 "conditions": [ 

270 { 

271 "type": "NamespaceDeletionDiscoveryFailure", 

272 "status": "False", 

273 "lastTransitionTime": datetime.now(timezone.utc).isoformat(), 

274 "reason": "ResourcesDiscovered", 

275 "message": "All resources successfully discovered", 

276 }, 

277 { 

278 "type": "NamespaceDeletionContentFailure", 

279 "status": "False", 

280 "lastTransitionTime": datetime.now(timezone.utc).isoformat(), 

281 "reason": "ContentDeleted", 

282 "message": "All content successfully deleted", 

283 }, 

284 ], 

285 } 

286 else: 

287 # Namespace not ready could mean it's terminating 

288 return { 

289 "phase": "Terminating", 

290 "conditions": [ 

291 { 

292 "type": "NamespaceDeletionDiscoveryFailure", 

293 "status": "True", 

294 "lastTransitionTime": datetime.now(timezone.utc).isoformat(), 

295 "reason": "DiscoveryFailed", 

296 "message": "Discovery failed for some resources", 

297 }, 

298 { 

299 "type": "NamespaceDeletionContentFailure", 

300 "status": "True", 

301 "lastTransitionTime": datetime.now(timezone.utc).isoformat(), 

302 "reason": "ContentDeletionFailed", 

303 "message": "Failed to delete all content", 

304 }, 

305 ], 

306 } 

307 

308 

309def get_generic_status_template(body: dict[str, Any]) -> dict[str, Any]: 

310 """Get generic status for unknown resource types""" 

311 # Use the general ready status configuration 

312 ready_status, ready_reason, ready_message = _get_ready_status_config(body=body) 

313 

314 return { 

315 "conditions": [ 

316 { 

317 "type": "Ready", 

318 "status": ready_status, 

319 "lastTransitionTime": datetime.now(timezone.utc).isoformat(), 

320 "reason": ready_reason, 

321 "message": ready_message, 

322 } 

323 ] 

324 }