oci_api/auth/
config_loader.rs1use crate::auth::config::{OciConfig, OciConfigBuilder};
6use crate::auth::key_loader::KeyLoader;
7use crate::error::{OciError, Result};
8use ini::{Ini, Properties};
9use std::path::Path;
10
11pub struct ConfigLoader;
13
14impl ConfigLoader {
15 pub fn load_from_env_var(config_value: &str, profile: Option<&str>) -> Result<OciConfig> {
23 let path = Path::new(config_value);
25 if path.exists() {
26 Self::load_from_file(path, profile)
27 } else {
28 Self::load_from_ini_content(config_value, profile)
30 }
31 }
32
33 pub fn load_from_ini_content(ini_content: &str, profile: Option<&str>) -> Result<OciConfig> {
39 let profile_name = profile.unwrap_or("DEFAULT");
40
41 let ini = Ini::load_from_str(ini_content)
43 .map_err(|e| OciError::IniError(format!("Failed to parse INI content: {}", e)))?;
44
45 let section = ini.section(Some(profile_name)).ok_or_else(|| {
47 OciError::ConfigError(format!(
48 "Profile '{}' not found in INI content",
49 profile_name
50 ))
51 })?;
52
53 Self::build_config_from_section(section)
55 }
56
57 pub fn load_from_file(path: &Path, profile: Option<&str>) -> Result<OciConfig> {
63 let profile_name = profile.unwrap_or("DEFAULT");
64
65 let ini = Ini::load_from_file(path)
67 .map_err(|e| OciError::IniError(format!("Failed to load INI file: {}", e)))?;
68
69 let section = ini.section(Some(profile_name)).ok_or_else(|| {
71 OciError::ConfigError(format!("Profile '{}' not found", profile_name))
72 })?;
73
74 Self::build_config_from_section(section)
76 }
77
78 fn build_config_from_section(section: &Properties) -> Result<OciConfig> {
80 let user_id = section
82 .get("user")
83 .ok_or_else(|| OciError::ConfigError("user field not found in config".to_string()))?
84 .to_string();
85
86 let tenancy_id = section
87 .get("tenancy")
88 .ok_or_else(|| OciError::ConfigError("tenancy field not found in config".to_string()))?
89 .to_string();
90
91 let region = section
92 .get("region")
93 .ok_or_else(|| OciError::ConfigError("region field not found in config".to_string()))?
94 .to_string();
95
96 let fingerprint = section
97 .get("fingerprint")
98 .ok_or_else(|| {
99 OciError::ConfigError("fingerprint field not found in config".to_string())
100 })?
101 .to_string();
102
103 let key_file = section.get("key_file").ok_or_else(|| {
106 OciError::ConfigError("key_file field not found in config".to_string())
107 })?;
108
109 let key_path = if key_file.starts_with("~/") {
113 let home = std::env::var("HOME").map_err(|_| {
114 OciError::EnvError("Cannot find HOME environment variable".to_string())
115 })?;
116 key_file.replacen("~", &home, 1)
117 } else {
118 key_file.to_string()
119 };
120
121 let private_key = KeyLoader::load(&key_path)?;
122
123 OciConfigBuilder::default()
125 .user_id(user_id)
126 .tenancy_id(tenancy_id)
127 .region(region)
128 .fingerprint(fingerprint)
129 .private_key(private_key)?
130 .build()
131 }
132
133 pub(crate) fn load_partial_from_env_var(config_value: &str) -> Result<PartialOciConfig> {
137 let ini = if std::path::Path::new(config_value).exists() {
138 Ini::load_from_file(config_value)
140 .map_err(|e| OciError::ConfigError(format!("Failed to load config file: {}", e)))?
141 } else {
142 Ini::load_from_str(config_value)
144 .map_err(|e| OciError::ConfigError(format!("Failed to parse INI content: {}", e)))?
145 };
146
147 let profile_name = "DEFAULT";
148 let section = ini.section(Some(profile_name)).ok_or_else(|| {
149 OciError::ConfigError(format!("Profile '{}' not found", profile_name))
150 })?;
151
152 Ok(PartialOciConfig {
154 user_id: section.get("user").map(|s| s.to_string()),
155 tenancy_id: section.get("tenancy").map(|s| s.to_string()),
156 region: section.get("region").map(|s| s.to_string()),
157 fingerprint: section.get("fingerprint").map(|s| s.to_string()),
158 })
159 }
160}
161
162#[derive(Debug, Default)]
165pub(crate) struct PartialOciConfig {
166 pub user_id: Option<String>,
167 pub tenancy_id: Option<String>,
168 pub region: Option<String>,
169 pub fingerprint: Option<String>,
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use std::io::Write;
176 use tempfile::NamedTempFile;
177
178 #[test]
179 fn test_load_from_file_success() {
180 let mut key_file = NamedTempFile::new().unwrap();
182 let key_content = "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----\n";
183 key_file.write_all(key_content.as_bytes()).unwrap();
184
185 let mut ini_file = NamedTempFile::new().unwrap();
186 let ini_content = format!(
187 r#"
188[DEFAULT]
189user=ocid1.user.test
190tenancy=ocid1.tenancy.test
191region=ap-seoul-1
192fingerprint=aa:bb:cc:dd:ee:ff
193key_file={}
194"#,
195 key_file.path().to_str().unwrap()
196 );
197 ini_file.write_all(ini_content.as_bytes()).unwrap();
198
199 let result = ConfigLoader::load_from_file(ini_file.path(), None);
200 assert!(result.is_ok());
201
202 let config = result.unwrap();
203 assert_eq!(config.user_id, "ocid1.user.test");
204 assert_eq!(config.tenancy_id, "ocid1.tenancy.test");
205 assert_eq!(config.region, "ap-seoul-1");
206 assert_eq!(config.fingerprint, "aa:bb:cc:dd:ee:ff");
207 assert!(config.private_key.contains("BEGIN RSA PRIVATE KEY"));
208 }
209
210 #[test]
211 fn test_load_from_file_missing_field() {
212 let mut ini_file = NamedTempFile::new().unwrap();
213 let ini_content = r#"
214[DEFAULT]
215user=ocid1.user.test
216tenancy=ocid1.tenancy.test
217region=ap-seoul-1
218"#;
219 ini_file.write_all(ini_content.as_bytes()).unwrap();
220
221 let result = ConfigLoader::load_from_file(ini_file.path(), None);
222 assert!(result.is_err());
223 }
224
225 #[test]
226 fn test_load_from_file_profile_not_found() {
227 let mut ini_file = NamedTempFile::new().unwrap();
228 let ini_content = r#"
229[DEFAULT]
230user=ocid1.user.test
231"#;
232 ini_file.write_all(ini_content.as_bytes()).unwrap();
233
234 let result = ConfigLoader::load_from_file(ini_file.path(), Some("NONEXISTENT"));
235 assert!(result.is_err());
236 match result.unwrap_err() {
237 OciError::ConfigError(msg) => assert!(msg.contains("NONEXISTENT")),
238 _ => panic!("Expected ConfigError"),
239 }
240 }
241}