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!(
57 "Profile '{profile_name}' not found in INI content"
58 ))
59 })?;
60
61 Self::build_config_from_section(section)
63 }
64
65 pub fn load_from_file(path: &Path, profile: Option<&str>) -> Result<LoadedConfig> {
71 let profile_name = profile.unwrap_or("DEFAULT");
72
73 let ini = Ini::load_from_file(path)
75 .map_err(|e| Error::IniError(format!("Failed to load INI file: {e}")))?;
76
77 let section = ini.section(Some(profile_name)).ok_or_else(|| {
79 Error::ConfigError(format!("Profile '{profile_name}' not found"))
80 })?;
81
82 Self::build_config_from_section(section)
84 }
85
86 fn build_config_from_section(section: &Properties) -> Result<LoadedConfig> {
88 let user_id = section
90 .get("user")
91 .ok_or_else(|| Error::ConfigError("user field not found in config".to_string()))?
92 .to_string();
93
94 let tenancy_id = section
95 .get("tenancy")
96 .ok_or_else(|| Error::ConfigError("tenancy field not found in config".to_string()))?
97 .to_string();
98
99 let region = section
100 .get("region")
101 .ok_or_else(|| Error::ConfigError("region field not found in config".to_string()))?
102 .to_string();
103
104 let fingerprint = section
105 .get("fingerprint")
106 .ok_or_else(|| {
107 Error::ConfigError("fingerprint field not found in config".to_string())
108 })?
109 .to_string();
110
111 let key_file = section.get("key_file").ok_or_else(|| {
114 Error::ConfigError("key_file field not found in config".to_string())
115 })?;
116
117 let key_path = if key_file.starts_with("~/") {
121 let home = std::env::var("HOME").map_err(|_| {
122 Error::EnvError("Cannot find HOME environment variable".to_string())
123 })?;
124 key_file.replacen("~", &home, 1)
125 } else {
126 key_file.to_string()
127 };
128
129 let private_key = KeyLoader::load(&key_path)?;
130
131 Ok(LoadedConfig {
132 user_id,
133 tenancy_id,
134 region,
135 fingerprint,
136 private_key,
137 })
138 }
139
140 pub(crate) fn load_partial_from_env_var(config_value: &str) -> Result<PartialOciConfig> {
144 let ini = if std::path::Path::new(config_value).exists() {
145 Ini::load_from_file(config_value)
147 .map_err(|e| Error::ConfigError(format!("Failed to load config file: {e}")))?
148 } else {
149 Ini::load_from_str(config_value)
151 .map_err(|e| Error::ConfigError(format!("Failed to parse INI content: {e}")))?
152 };
153
154 let profile_name = "DEFAULT";
155 let section = ini.section(Some(profile_name)).ok_or_else(|| {
156 Error::ConfigError(format!("Profile '{profile_name}' not found"))
157 })?;
158
159 Ok(PartialOciConfig {
161 user_id: section.get("user").map(|s| s.to_string()),
162 tenancy_id: section.get("tenancy").map(|s| s.to_string()),
163 region: section.get("region").map(|s| s.to_string()),
164 fingerprint: section.get("fingerprint").map(|s| s.to_string()),
165 })
166 }
167}
168
169#[derive(Debug, Default)]
172pub(crate) struct PartialOciConfig {
173 pub user_id: Option<String>,
174 pub tenancy_id: Option<String>,
175 pub region: Option<String>,
176 pub fingerprint: Option<String>,
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use std::io::Write;
183 use tempfile::NamedTempFile;
184
185 #[test]
186 fn test_load_from_file_success() {
187 let mut key_file = NamedTempFile::new().unwrap();
189 let key_content = "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----\n";
190 key_file.write_all(key_content.as_bytes()).unwrap();
191
192 let mut ini_file = NamedTempFile::new().unwrap();
193 let ini_content = format!(
194 r#"
195[DEFAULT]
196user=ocid1.user.test
197tenancy=ocid1.tenancy.test
198region=ap-seoul-1
199fingerprint=aa:bb:cc:dd:ee:ff
200key_file={}
201"#,
202 key_file.path().to_str().unwrap()
203 );
204 ini_file.write_all(ini_content.as_bytes()).unwrap();
205
206 let result = ConfigLoader::load_from_file(ini_file.path(), None);
207 assert!(result.is_ok());
208
209 let config = result.unwrap();
210 assert_eq!(config.user_id, "ocid1.user.test");
211 assert_eq!(config.tenancy_id, "ocid1.tenancy.test");
212 assert_eq!(config.region, "ap-seoul-1");
213 assert_eq!(config.fingerprint, "aa:bb:cc:dd:ee:ff");
214 assert!(config.private_key.contains("BEGIN RSA PRIVATE KEY"));
215 }
216
217 #[test]
218 fn test_load_from_file_missing_field() {
219 let mut ini_file = NamedTempFile::new().unwrap();
220 let ini_content = r#"
221[DEFAULT]
222user=ocid1.user.test
223tenancy=ocid1.tenancy.test
224region=ap-seoul-1
225"#;
226 ini_file.write_all(ini_content.as_bytes()).unwrap();
227
228 let result = ConfigLoader::load_from_file(ini_file.path(), None);
229 assert!(result.is_err());
230 }
231
232 #[test]
233 fn test_load_from_file_profile_not_found() {
234 let mut ini_file = NamedTempFile::new().unwrap();
235 let ini_content = r#"
236[DEFAULT]
237user=ocid1.user.test
238"#;
239 ini_file.write_all(ini_content.as_bytes()).unwrap();
240
241 let result = ConfigLoader::load_from_file(ini_file.path(), Some("NONEXISTENT"));
242 assert!(result.is_err());
243 match result.unwrap_err() {
244 Error::ConfigError(msg) => assert!(msg.contains("NONEXISTENT")),
245 _ => panic!("Expected ConfigError"),
246 }
247 }
248}