22
33import os
44from pathlib import Path
5+ from http import HTTPStatus
56from typing import Optional , Self
67
78import kubernetes .client
3031)
3132
3233
33- class ClusterIDUnavailableError (Exception ):
34- """Cluster ID is not available."""
34+ class K8sAuthenticationError (Exception ):
35+ """Base exception for Kubernetes authentication errors."""
36+
37+
38+ class K8sAPIConnectionError (K8sAuthenticationError ):
39+ """Cannot connect to Kubernetes API server.
40+
41+ Indicates transient failures that may be resolved by retrying.
42+ Maps to HTTP 503 Service Unavailable.
43+ """
44+
45+
46+ class K8sConfigurationError (K8sAuthenticationError ):
47+ """Kubernetes cluster configuration issue.
48+
49+ Indicates persistent configuration problems requiring admin intervention.
50+ Maps to HTTP 500 Internal Server Error.
51+ """
52+
53+
54+ class ClusterVersionNotFoundError (K8sConfigurationError ):
55+ """ClusterVersion resource not found in OpenShift cluster.
56+
57+ Raised when the ClusterVersion custom resource does not exist (HTTP 404).
58+ """
59+
60+
61+ class ClusterVersionPermissionError (K8sConfigurationError ):
62+ """No permission to access ClusterVersion resource.
63+
64+ Raised when RBAC denies access to the ClusterVersion resource (HTTP 403).
65+ """
66+
67+
68+ class InvalidClusterVersionError (K8sConfigurationError ):
69+ """ClusterVersion resource has invalid structure or missing required fields.
70+
71+ Raised when the ClusterVersion exists but is missing spec.clusterID or has wrong type.
72+ """
3573
3674
3775class K8sClientSingleton :
@@ -156,9 +194,12 @@ def _get_cluster_id(cls) -> str:
156194 str: The cluster's `clusterID`.
157195
158196 Raises:
159- ClusterIDUnavailableError: If the cluster ID cannot be obtained due
160- to missing keys, an API error, or any unexpected error.
197+ K8sAPIConnectionError: If the Kubernetes API is unreachable or returns 5xx errors.
198+ ClusterVersionNotFoundError: If the ClusterVersion resource does not exist (404).
199+ ClusterVersionPermissionError: If access to ClusterVersion is denied (403).
200+ InvalidClusterVersionError: If ClusterVersion has invalid structure or missing fields.
161201 """
202+ version_data : Optional [object ] = None
162203 try :
163204 custom_objects_api = cls .get_custom_objects_api ()
164205 version_data = custom_objects_api .get_cluster_custom_object (
@@ -167,22 +208,47 @@ def _get_cluster_id(cls) -> str:
167208 cluster_id = version_data ["spec" ]["clusterID" ]
168209 cls ._cluster_id = cluster_id
169210 return cluster_id
211+ except ApiException as e :
212+ # Handle specific HTTP status codes from Kubernetes API
213+ if e .status == HTTPStatus .NOT_FOUND :
214+ logger .error (
215+ "ClusterVersion resource 'version' not found in cluster: %s" ,
216+ e .reason ,
217+ )
218+ raise ClusterVersionNotFoundError (
219+ "ClusterVersion 'version' resource not found in OpenShift cluster"
220+ ) from e
221+ if e .status == HTTPStatus .FORBIDDEN :
222+ logger .error (
223+ "Permission denied to access ClusterVersion resource: %s" , e .reason
224+ )
225+ raise ClusterVersionPermissionError (
226+ "Insufficient permissions to read ClusterVersion resource"
227+ ) from e
228+ # Treat all other ApiException as connection/server errors
229+ logger .error (
230+ "Kubernetes API error while fetching ClusterVersion (status %s): %s" ,
231+ e .status ,
232+ e .reason ,
233+ )
234+ raise K8sAPIConnectionError (
235+ f"Failed to connect to Kubernetes API: { e .reason } (status { e .status } )"
236+ ) from e
170237 except KeyError as e :
171238 logger .error (
172- "Failed to get cluster_id from cluster, missing keys in version object"
239+ "ClusterVersion missing required field 'spec.clusterID': %s" , e
173240 )
174- raise ClusterIDUnavailableError ("Failed to get cluster ID" ) from e
241+ raise InvalidClusterVersionError (
242+ f"ClusterVersion missing required field: { e } "
243+ ) from e
175244 except TypeError as e :
176245 logger .error (
177- "Failed to get cluster_id, version object is: %s" , version_data
246+ "ClusterVersion has invalid structure, version_data type: %s" ,
247+ type (version_data ).__name__ if version_data is not None else "unknown" ,
178248 )
179- raise ClusterIDUnavailableError ("Failed to get cluster ID" ) from e
180- except ApiException as e :
181- logger .error ("API exception during ClusterInfo: %s" , e )
182- raise ClusterIDUnavailableError ("Failed to get cluster ID" ) from e
183- except Exception as e :
184- logger .error ("Unexpected error during getting cluster ID: %s" , e )
185- raise ClusterIDUnavailableError ("Failed to get cluster ID" ) from e
249+ raise InvalidClusterVersionError (
250+ f"ClusterVersion has invalid type or structure: { e } "
251+ ) from e
186252
187253 @classmethod
188254 def get_cluster_id (cls ) -> str :
@@ -199,7 +265,10 @@ def get_cluster_id(cls) -> str:
199265 str: The cluster identifier.
200266
201267 Raises:
202- ClusterIDUnavailableError: If running in-cluster and fetching the cluster ID fails.
268+ K8sAPIConnectionError: If the Kubernetes API is unreachable.
269+ ClusterVersionNotFoundError: If the ClusterVersion resource does not exist.
270+ ClusterVersionPermissionError: If access to ClusterVersion is denied.
271+ InvalidClusterVersionError: If ClusterVersion has invalid structure.
203272 """
204273 if cls ._instance is None :
205274 cls ()
@@ -310,11 +379,24 @@ async def __call__(self, request: Request) -> tuple[str, str, bool, str]:
310379 if user_info .user .username == "kube:admin" :
311380 try :
312381 user_info .user .uid = K8sClientSingleton .get_cluster_id ()
313- except ClusterIDUnavailableError as e :
314- logger .error ("Failed to get cluster ID: %s" , e )
382+ except K8sAPIConnectionError as e :
383+ # Kubernetes API is unreachable - return 503
384+ logger .error ("Cannot connect to Kubernetes API: %s" , e )
385+ response = ServiceUnavailableResponse (
386+ backend_name = "Kubernetes API" ,
387+ cause = str (e ),
388+ )
389+ raise HTTPException (** response .model_dump ()) from e
390+ except (
391+ ClusterVersionNotFoundError ,
392+ ClusterVersionPermissionError ,
393+ InvalidClusterVersionError ,
394+ ) as e :
395+ # Cluster misconfiguration - return 500
396+ logger .error ("Cluster configuration error: %s" , e )
315397 response = InternalServerErrorResponse (
316398 response = "Internal server error" ,
317- cause = "Unable to retrieve cluster ID" ,
399+ cause = str ( e ) ,
318400 )
319401 raise HTTPException (** response .model_dump ()) from e
320402
0 commit comments