1313//!
1414//! # Configuration Sources
1515//!
16- //! ## Configuration Files
17- //! The configuration system searches for TOML files in the following order:
18- //! - Explicit path provided via CLI argument
16+ //! ## Configuration Files (Optional)
17+ //! Configuration files are completely optional. The system searches for TOML files in the following order:
18+ //! - Explicit path provided via CLI argument (required to exist if specified)
1919//! - `keylimectl.toml` (current directory)
2020//! - `keylimectl.conf` (current directory)
2121//! - `/etc/keylime/tenant.conf` (system-wide)
2424//! - `~/.keylimectl.toml` (user-specific)
2525//! - `$XDG_CONFIG_HOME/keylime/tenant.conf` (XDG standard)
2626//!
27+ //! If no configuration files are found, keylimectl will work perfectly with defaults and environment variables.
28+ //!
2729//! ## Environment Variables
2830//! Environment variables use the prefix `KEYLIME_` with double underscores as separators:
2931//! - `KEYLIME_VERIFIER__IP=192.168.1.100`
@@ -208,6 +210,7 @@ pub struct TlsConfig {
208210 /// Client key password
209211 pub client_key_password : Option < String > ,
210212 /// Trusted CA certificates
213+ #[ serde( default ) ]
211214 pub trusted_ca : Vec < String > ,
212215 /// Verify server certificates
213216 pub verify_server_cert : bool ,
@@ -273,51 +276,69 @@ impl Config {
273276 ///
274277 /// Loads configuration with the following precedence (highest to lowest):
275278 /// 1. Environment variables (KEYLIME_*)
276- /// 2. Configuration files (TOML format)
279+ /// 2. Configuration files (TOML format) - **OPTIONAL**
277280 /// 3. Default values
278281 ///
282+ /// Configuration files are completely optional. If no configuration files are found,
283+ /// the system will use default values combined with any environment variables.
284+ /// This allows keylimectl to work out-of-the-box without requiring any configuration.
285+ ///
279286 /// # Arguments
280287 ///
281288 /// * `config_path` - Optional explicit path to configuration file.
282- /// If None, searches standard locations.
289+ /// If None, searches standard locations. If Some() but file doesn't exist, returns error.
283290 ///
284291 /// # Returns
285292 ///
286- /// Returns the merged configuration or a ConfigError if loading fails.
293+ /// Returns the merged configuration. Will not fail if no config files are found when
294+ /// using automatic discovery (config_path = None).
287295 ///
288296 /// # Examples
289297 ///
290298 /// ```rust
291299 /// use keylimectl::config::Config;
292300 ///
293- /// // Load from standard locations
301+ /// // Works with no config files - uses defaults + env vars
294302 /// let config = Config::load(None)?;
295303 ///
296- /// // Load from specific file
304+ /// // Load from specific file (errors if file doesn't exist)
297305 /// let config = Config::load(Some("/path/to/config.toml"))?;
298306 /// # Ok::<(), config::ConfigError>(())
299307 /// ```
300308 ///
301309 /// # Errors
302310 ///
303311 /// Returns ConfigError if:
312+ /// - Explicit configuration file path provided but file doesn't exist
304313 /// - Configuration file has invalid syntax
305314 /// - Environment variables have invalid values
306- /// - Required configuration is missing
307315 pub fn load ( config_path : Option < & str > ) -> Result < Self , ConfigError > {
308316 let mut builder = config:: Config :: builder ( )
309317 . add_source ( config:: Config :: try_from ( & Config :: default ( ) ) ?) ;
310318
311319 // Add configuration file sources
312320 let config_paths = Self :: get_config_paths ( config_path) ;
321+ let mut config_file_found = false ;
322+
313323 for path in config_paths {
314324 if path. exists ( ) {
325+ config_file_found = true ;
326+ log:: debug!( "Loading config from: {}" , path. display( ) ) ;
315327 builder = builder. add_source (
316328 File :: from ( path) . format ( FileFormat :: Toml ) . required ( false ) ,
317329 ) ;
318330 }
319331 }
320332
333+ // If an explicit config path was provided but the file doesn't exist, that's an error
334+ if let Some ( explicit_path) = config_path {
335+ if !PathBuf :: from ( explicit_path) . exists ( ) {
336+ return Err ( ConfigError :: Message ( format ! (
337+ "Specified configuration file not found: {explicit_path}"
338+ ) ) ) ;
339+ }
340+ }
341+
321342 // Add environment variables
322343 builder = builder. add_source (
323344 Environment :: with_prefix ( "KEYLIME" )
@@ -326,7 +347,18 @@ impl Config {
326347 . try_parsing ( true ) ,
327348 ) ;
328349
329- builder. build ( ) ?. try_deserialize ( )
350+ let config = builder. build ( ) ?. try_deserialize ( ) ?;
351+
352+ // Log information about configuration sources used
353+ if config_file_found {
354+ log:: debug!(
355+ "Configuration loaded successfully with config files"
356+ ) ;
357+ } else {
358+ log:: info!( "No configuration files found, using defaults and environment variables" ) ;
359+ }
360+
361+ Ok ( config)
330362 }
331363
332364 /// Apply command-line argument overrides
@@ -956,25 +988,37 @@ retry_interval = 2.0
956988 #[ test]
957989 fn test_load_config_no_files ( ) {
958990 // Test loading config when no config files exist
959- // This should succeed with defaults
991+ // This should always succeed with defaults since config files are optional
960992 let result = Config :: load ( None ) ;
961993
962- // This may fail due to config crate serialization issues with empty Vec
963- // If it fails, that's actually expected behavior - let's test that
994+ // Should always succeed now that config files are optional
964995 match result {
965996 Ok ( config) => {
966997 assert_eq ! ( config. verifier. ip, "127.0.0.1" ) ; // Default value
998+ assert_eq ! ( config. verifier. port, 8881 ) ; // Default value
999+ assert_eq ! ( config. registrar. ip, "127.0.0.1" ) ; // Default value
1000+ assert_eq ! ( config. registrar. port, 8891 ) ; // Default value
9671001 }
968- Err ( _) => {
969- // This is acceptable - the config crate may have issues with
970- // serializing/deserializing empty Vecs or other edge cases
971- // The important thing is that Config::default() works
972- let default_config = Config :: default ( ) ;
973- assert_eq ! ( default_config. verifier. ip, "127.0.0.1" ) ;
1002+ Err ( e) => {
1003+ panic ! ( "Config loading should succeed with no files, but got error: {e:?}" ) ;
9741004 }
9751005 }
9761006 }
9771007
1008+ #[ test]
1009+ fn test_load_config_explicit_file_not_found ( ) {
1010+ // Test that explicit config file paths are still required to exist
1011+ let result = Config :: load ( Some ( "/nonexistent/path/config.toml" ) ) ;
1012+
1013+ assert ! (
1014+ result. is_err( ) ,
1015+ "Should error when explicit config file doesn't exist"
1016+ ) ;
1017+ let error_msg = result. unwrap_err ( ) . to_string ( ) ;
1018+ assert ! ( error_msg. contains( "Specified configuration file not found" ) ) ;
1019+ assert ! ( error_msg. contains( "/nonexistent/path/config.toml" ) ) ;
1020+ }
1021+
9781022 #[ test]
9791023 fn test_get_config_paths_explicit ( ) {
9801024 let paths = Config :: get_config_paths ( Some ( "/custom/path.toml" ) ) ;
0 commit comments