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