use crate::auth::key_loader::KeyLoader;
use crate::error::{Error, Result};
use ini::{Ini, Properties};
use std::path::Path;
#[derive(Debug)]
pub struct LoadedConfig {
pub user_id: String,
pub tenancy_id: String,
pub region: String,
pub fingerprint: String,
pub private_key: String,
}
pub struct ConfigLoader;
impl ConfigLoader {
pub fn load_from_env_var(config_value: &str, profile: Option<&str>) -> Result<LoadedConfig> {
let path = Path::new(config_value);
if path.exists() {
Self::load_from_file(path, profile)
} else {
Self::load_from_ini_content(config_value, profile)
}
}
pub fn load_from_ini_content(ini_content: &str, profile: Option<&str>) -> Result<LoadedConfig> {
let profile_name = profile.unwrap_or("DEFAULT");
let ini = Ini::load_from_str(ini_content)
.map_err(|e| Error::IniError(format!("Failed to parse INI content: {e}")))?;
let section = ini.section(Some(profile_name)).ok_or_else(|| {
Error::ConfigError(format!(
"Profile '{profile_name}' not found in INI content"
))
})?;
Self::build_config_from_section(section)
}
pub fn load_from_file(path: &Path, profile: Option<&str>) -> Result<LoadedConfig> {
let profile_name = profile.unwrap_or("DEFAULT");
let ini = Ini::load_from_file(path)
.map_err(|e| Error::IniError(format!("Failed to load INI file: {e}")))?;
let section = ini.section(Some(profile_name)).ok_or_else(|| {
Error::ConfigError(format!("Profile '{profile_name}' not found"))
})?;
Self::build_config_from_section(section)
}
fn build_config_from_section(section: &Properties) -> Result<LoadedConfig> {
let user_id = section
.get("user")
.ok_or_else(|| Error::ConfigError("user field not found in config".to_string()))?
.to_string();
let tenancy_id = section
.get("tenancy")
.ok_or_else(|| Error::ConfigError("tenancy field not found in config".to_string()))?
.to_string();
let region = section
.get("region")
.ok_or_else(|| Error::ConfigError("region field not found in config".to_string()))?
.to_string();
let fingerprint = section
.get("fingerprint")
.ok_or_else(|| {
Error::ConfigError("fingerprint field not found in config".to_string())
})?
.to_string();
let key_file = section.get("key_file").ok_or_else(|| {
Error::ConfigError("key_file field not found in config".to_string())
})?;
let key_path = if key_file.starts_with("~/") {
let home = std::env::var("HOME").map_err(|_| {
Error::EnvError("Cannot find HOME environment variable".to_string())
})?;
key_file.replacen("~", &home, 1)
} else {
key_file.to_string()
};
let private_key = KeyLoader::load(&key_path)?;
Ok(LoadedConfig {
user_id,
tenancy_id,
region,
fingerprint,
private_key,
})
}
pub(crate) fn load_partial_from_env_var(config_value: &str) -> Result<PartialOciConfig> {
let ini = if std::path::Path::new(config_value).exists() {
Ini::load_from_file(config_value)
.map_err(|e| Error::ConfigError(format!("Failed to load config file: {e}")))?
} else {
Ini::load_from_str(config_value)
.map_err(|e| Error::ConfigError(format!("Failed to parse INI content: {e}")))?
};
let profile_name = "DEFAULT";
let section = ini.section(Some(profile_name)).ok_or_else(|| {
Error::ConfigError(format!("Profile '{profile_name}' not found"))
})?;
Ok(PartialOciConfig {
user_id: section.get("user").map(|s| s.to_string()),
tenancy_id: section.get("tenancy").map(|s| s.to_string()),
region: section.get("region").map(|s| s.to_string()),
fingerprint: section.get("fingerprint").map(|s| s.to_string()),
})
}
}
#[derive(Debug, Default)]
pub(crate) struct PartialOciConfig {
pub user_id: Option<String>,
pub tenancy_id: Option<String>,
pub region: Option<String>,
pub fingerprint: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_load_from_file_success() {
let mut key_file = NamedTempFile::new().unwrap();
let key_content = "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----\n";
key_file.write_all(key_content.as_bytes()).unwrap();
let mut ini_file = NamedTempFile::new().unwrap();
let ini_content = format!(
r#"
[DEFAULT]
user=ocid1.user.test
tenancy=ocid1.tenancy.test
region=ap-seoul-1
fingerprint=aa:bb:cc:dd:ee:ff
key_file={}
"#,
key_file.path().to_str().unwrap()
);
ini_file.write_all(ini_content.as_bytes()).unwrap();
let result = ConfigLoader::load_from_file(ini_file.path(), None);
assert!(result.is_ok());
let config = result.unwrap();
assert_eq!(config.user_id, "ocid1.user.test");
assert_eq!(config.tenancy_id, "ocid1.tenancy.test");
assert_eq!(config.region, "ap-seoul-1");
assert_eq!(config.fingerprint, "aa:bb:cc:dd:ee:ff");
assert!(config.private_key.contains("BEGIN RSA PRIVATE KEY"));
}
#[test]
fn test_load_from_file_missing_field() {
let mut ini_file = NamedTempFile::new().unwrap();
let ini_content = r#"
[DEFAULT]
user=ocid1.user.test
tenancy=ocid1.tenancy.test
region=ap-seoul-1
"#;
ini_file.write_all(ini_content.as_bytes()).unwrap();
let result = ConfigLoader::load_from_file(ini_file.path(), None);
assert!(result.is_err());
}
#[test]
fn test_load_from_file_profile_not_found() {
let mut ini_file = NamedTempFile::new().unwrap();
let ini_content = r#"
[DEFAULT]
user=ocid1.user.test
"#;
ini_file.write_all(ini_content.as_bytes()).unwrap();
let result = ConfigLoader::load_from_file(ini_file.path(), Some("NONEXISTENT"));
assert!(result.is_err());
match result.unwrap_err() {
Error::ConfigError(msg) => assert!(msg.contains("NONEXISTENT")),
_ => panic!("Expected ConfigError"),
}
}
}