2424# Local imports
2525from pulp_utils import (
2626 DEFAULT_TIMEOUT , DEFAULT_TASK_TIMEOUT ,
27- create_session_with_retry , validate_file_path
27+ create_session_with_retry , validate_file_path ,
28+ sanitize_error_message , read_file_with_base64_decode
2829)
2930
3031# Optional imports with fallback
@@ -142,8 +143,8 @@ def _retrieve_token(self) -> None:
142143 self ._access_token = token ["access_token" ]
143144
144145 except requests .RequestException as e :
145- logging .error ("Failed to retrieve OAuth2 token: %s" , e )
146- logging .error ("Traceback: %s" , traceback .format_exc ())
146+ logging .error ("Failed to retrieve OAuth2 token: %s" , sanitize_error_message ( str ( e )) )
147+ logging .error ("Traceback: %s" , sanitize_error_message ( traceback .format_exc () ))
147148 raise
148149
149150 @property
@@ -265,8 +266,8 @@ def _chunked_get(self, url: str, params: Optional[Dict[str, Any]] = None,
265266 all_results .extend (chunk_data ['results' ])
266267
267268 except Exception as e :
268- logging .error ("Failed to process chunk %d: %s" , i , e )
269- logging .error ("Traceback: %s" , traceback .format_exc ())
269+ logging .error ("Failed to process chunk %d: %s" , i , sanitize_error_message ( str ( e )) )
270+ logging .error ("Traceback: %s" , sanitize_error_message ( traceback .format_exc () ))
270271 raise
271272
272273 # Create aggregated response
@@ -291,10 +292,64 @@ def create_from_config_file(cls, path: Optional[str] = None, domain: Optional[st
291292 """
292293 Create a Pulp client from a standard configuration file that is
293294 used by the `pulp` CLI tool.
295+
296+ Args:
297+ path: Path to the config file (default: ~/.config/pulp/cli.toml)
298+ domain: Optional domain override
299+ namespace: Optional namespace override
300+
301+ Returns:
302+ PulpClient instance
303+
304+ Raises:
305+ FileNotFoundError: If the config file doesn't exist
306+ ValueError: If the config file is malformed or missing required sections
294307 """
295308 config_path = Path (path or "~/.config/pulp/cli.toml" ).expanduser ()
296- with open (config_path , "rb" ) as fp :
297- config = tomllib .load (fp )
309+
310+ # Check if config file exists
311+ if not config_path .exists ():
312+ logging .error ("Pulp config file not found: %s" , config_path )
313+ raise FileNotFoundError (f"Pulp config file not found: { config_path } " )
314+
315+ try :
316+ # Read and decode base64 if encoded
317+ _ , decoded_content = read_file_with_base64_decode (str (config_path ))
318+ config = tomllib .loads (decoded_content .decode ('utf-8' ))
319+ except OSError as e :
320+ # File system errors (FileNotFoundError, PermissionError, etc.)
321+ logging .error ("Failed to read config file: %s" , config_path )
322+ logging .error ("Error: %s" , sanitize_error_message (str (e )))
323+ raise FileNotFoundError (f"Pulp config file not found or cannot be read: { config_path } " ) from e
324+ except (ValueError , KeyError ) as e :
325+ # TOML parsing errors (TOMLDecodeError is a subclass of ValueError in tomllib/tomli)
326+ error_msg = str (e )
327+ sanitized_error = sanitize_error_message (error_msg )
328+ error_type = type (e ).__name__
329+
330+ if "TOMLDecodeError" in error_type or "Expected '='" in error_msg :
331+ logging .error ("Failed to parse TOML config file: %s" , config_path )
332+ logging .error ("The config file appears to be malformed." )
333+ logging .error ("Error type: %s" , error_type )
334+ logging .error ("Error message: %s" , sanitized_error )
335+ logging .error ("Please check the TOML syntax in the config file." )
336+ logging .error ("Common issues:" )
337+ logging .error (" - Missing '=' after a key in a key/value pair" )
338+ logging .error (" - Incomplete key-value pairs" )
339+ logging .error (" - Trailing syntax errors at the end of the file" )
340+ logging .error (" - Invalid TOML structure" )
341+ raise ValueError (f"Malformed TOML config file: { sanitized_error } " ) from e
342+
343+ logging .error ("Failed to load Pulp client from config file: %s" , config_path )
344+ logging .error ("Error type: %s" , error_type )
345+ logging .error ("Error message: %s" , sanitized_error )
346+ raise ValueError (f"Failed to load config file: { sanitized_error } " ) from e
347+
348+ # Validate that config has required 'cli' section
349+ if "cli" not in config :
350+ logging .error ("Config file missing required 'cli' section: %s" , config_path )
351+ raise ValueError (f"Config file missing required 'cli' section: { config_path } " )
352+
298353 return cls (config ["cli" ], domain , namespace )
299354
300355 @property
@@ -415,26 +470,30 @@ def _check_response(self, response: Response, operation: str = "request") -> Non
415470 """Check if a response is successful, raise exception if not."""
416471 if not response .ok :
417472 logging .error ("Failed to %s: %s - %s" , operation , response .status_code ,
418- response .text )
473+ sanitize_error_message ( response .text ) )
419474
420475 # Enhanced error logging for server errors
421476 if response .status_code >= 500 :
422477 logging .error ("Server error details:" )
423478 logging .error (" Status Code: %s" , response .status_code )
424- logging .error (" Headers: %s" , dict (response .headers ))
479+ # Sanitize headers to prevent credential leakage
480+ headers_dict = dict (response .headers )
481+ logging .error (" Headers: %s" , sanitize_error_message (str (headers_dict )))
425482 logging .error (" URL: %s" , response .url )
426483 logging .error (" Request Method: %s" ,
427484 response .request .method if response .request else "Unknown" )
428485
429486 # Try to parse error details
430487 try :
431488 error_data = response .json ()
432- logging .error (" Error Data: %s" , error_data )
489+ logging .error (" Error Data: %s" , sanitize_error_message ( str ( error_data )) )
433490 except (ValueError , json .JSONDecodeError ):
434- logging .error (" Raw Response: %s" , response .text )
491+ logging .error (" Raw Response: %s" , sanitize_error_message ( response .text ) )
435492
493+ # Sanitize error message in exception
494+ sanitized_text = sanitize_error_message (response .text )
436495 raise requests .RequestException (
437- f"Failed to { operation } : { response .status_code } - { response . text } "
496+ f"Failed to { operation } : { response .status_code } - { sanitized_text } "
438497 )
439498
440499 def check_response (self , response : Response , operation : str = "request" ) -> None :
@@ -498,12 +557,12 @@ def upload_content(self, file_path: str, labels: Dict[str, str],
498557 return response .json ()["pulp_href" ]
499558
500559 except requests .RequestException as e :
501- logging .error ("Request failed for %s %s: %s" , file_type , file_path , e )
502- logging .error ("Traceback: %s" , traceback .format_exc ())
560+ logging .error ("Request failed for %s %s: %s" , file_type , file_path , sanitize_error_message ( str ( e )) )
561+ logging .error ("Traceback: %s" , sanitize_error_message ( traceback .format_exc () ))
503562 raise
504563 except Exception as e :
505- logging .error ("Unexpected error uploading %s %s: %s" , file_type , file_path , e )
506- logging .error ("Traceback: %s" , traceback .format_exc ())
564+ logging .error ("Unexpected error uploading %s %s: %s" , file_type , file_path , sanitize_error_message ( str ( e )) )
565+ logging .error ("Traceback: %s" , sanitize_error_message ( traceback .format_exc () ))
507566 raise
508567
509568 def create_file_content (self , repository : str , content_or_path : Union [str , Path ],
@@ -610,7 +669,7 @@ def wait_for_finished_task(self, task: str, timeout: int = DEFAULT_TASK_TIMEOUT)
610669 response = self ._get_task (task )
611670
612671 if not response .ok :
613- logging .error ("Error processing task %s: %s" , task , response .text )
672+ logging .error ("Error processing task %s: %s" , task , sanitize_error_message ( response .text ) )
614673 return response
615674
616675 task_state = response .json ().get ("state" )
@@ -708,9 +767,10 @@ def gather_content_data(self, build_id: str,
708767 logging .debug ("Content response JSON: %s" , resp_json )
709768 content_results = resp_json ["results" ]
710769 except Exception as e :
711- logging .error ("Failed to get content by build ID: %s" , e )
712- logging .error ("Response text: %s" , resp .text if 'resp' in locals () else "No response" )
713- logging .error ("Traceback: %s" , traceback .format_exc ())
770+ logging .error ("Failed to get content by build ID: %s" , sanitize_error_message (str (e )))
771+ resp_text = resp .text if 'resp' in locals () else "No response"
772+ logging .error ("Response text: %s" , sanitize_error_message (resp_text ))
773+ logging .error ("Traceback: %s" , sanitize_error_message (traceback .format_exc ()))
714774 raise
715775
716776 if not content_results :
0 commit comments