1use crate::error::{OciError, Result};
6
7#[derive(Debug, Clone)]
9pub struct OciConfig {
10 pub user_id: String,
12
13 pub tenancy_id: String,
15
16 pub region: String,
18
19 pub fingerprint: String,
21
22 pub private_key: String,
24
25 pub compartment_id: Option<String>,
27}
28
29impl OciConfig {
30 pub fn from_env() -> Result<Self> {
57 use crate::auth::config_loader::ConfigLoader;
58 use crate::auth::key_loader::KeyLoader;
59 use std::env;
60
61 let partial_config = if let Ok(config_value) = env::var("OCI_CONFIG") {
63 Some(ConfigLoader::load_partial_from_env_var(&config_value)?)
64 } else {
65 None
66 };
67
68 let user_id = env::var("OCI_USER_ID")
70 .ok()
71 .or_else(|| partial_config.as_ref().and_then(|c| c.user_id.clone()))
72 .ok_or_else(|| {
73 OciError::EnvError(
74 "OCI_USER_ID must be set (either directly or via OCI_CONFIG)".to_string(),
75 )
76 })?;
77
78 let tenancy_id = env::var("OCI_TENANCY_ID")
79 .ok()
80 .or_else(|| partial_config.as_ref().and_then(|c| c.tenancy_id.clone()))
81 .ok_or_else(|| {
82 OciError::EnvError(
83 "OCI_TENANCY_ID must be set (either directly or via OCI_CONFIG)".to_string(),
84 )
85 })?;
86
87 let region = env::var("OCI_REGION")
88 .ok()
89 .or_else(|| partial_config.as_ref().and_then(|c| c.region.clone()))
90 .ok_or_else(|| {
91 OciError::EnvError(
92 "OCI_REGION must be set (either directly or via OCI_CONFIG)".to_string(),
93 )
94 })?;
95
96 let fingerprint = env::var("OCI_FINGERPRINT")
97 .ok()
98 .or_else(|| partial_config.as_ref().and_then(|c| c.fingerprint.clone()))
99 .ok_or_else(|| {
100 OciError::EnvError(
101 "OCI_FINGERPRINT must be set (either directly or via OCI_CONFIG)".to_string(),
102 )
103 })?;
104
105 let private_key = if let Ok(key_input) = env::var("OCI_PRIVATE_KEY") {
108 KeyLoader::load(&key_input)?
110 } else if let Ok(config_value) = env::var("OCI_CONFIG") {
111 let full_config = ConfigLoader::load_from_env_var(&config_value, None)?;
113 full_config.private_key
114 } else {
115 return Err(OciError::EnvError(
116 "OCI_PRIVATE_KEY must be set (or key_file must be in OCI_CONFIG)".to_string(),
117 ));
118 };
119
120 let compartment_id = env::var("OCI_COMPARTMENT_ID").ok();
122
123 Ok(Self {
124 user_id,
125 tenancy_id,
126 region,
127 fingerprint,
128 private_key,
129 compartment_id,
130 })
131 }
132
133 pub fn region(&self) -> &str {
135 &self.region
136 }
137
138 pub fn builder() -> OciConfigBuilder {
140 OciConfigBuilder::default()
141 }
142}
143
144#[derive(Default)]
146pub struct OciConfigBuilder {
147 user_id: Option<String>,
148 tenancy_id: Option<String>,
149 region: Option<String>,
150 fingerprint: Option<String>,
151 private_key: Option<String>,
152 compartment_id: Option<String>,
153}
154
155impl OciConfigBuilder {
156 pub fn config(mut self, path: impl AsRef<std::path::Path>) -> Result<Self> {
174 use crate::auth::config_loader::ConfigLoader;
175
176 let loaded = ConfigLoader::load_from_file(path.as_ref(), Some("DEFAULT"))?;
177
178 self.user_id = Some(loaded.user_id);
180 self.tenancy_id = Some(loaded.tenancy_id);
181 self.region = Some(loaded.region);
182 self.fingerprint = Some(loaded.fingerprint);
183 self.private_key = Some(loaded.private_key);
184 Ok(self)
187 }
188
189 pub fn user_id(mut self, user_id: impl Into<String>) -> Self {
190 self.user_id = Some(user_id.into());
191 self
192 }
193
194 pub fn tenancy_id(mut self, tenancy_id: impl Into<String>) -> Self {
195 self.tenancy_id = Some(tenancy_id.into());
196 self
197 }
198
199 pub fn region(mut self, region: impl Into<String>) -> Self {
200 self.region = Some(region.into());
201 self
202 }
203
204 pub fn fingerprint(mut self, fingerprint: impl Into<String>) -> Self {
205 self.fingerprint = Some(fingerprint.into());
206 self
207 }
208
209 pub fn private_key(mut self, private_key: impl Into<String>) -> Result<Self> {
230 use crate::auth::key_loader::KeyLoader;
231
232 let key_input = private_key.into();
233 let loaded_key = KeyLoader::load(&key_input)?;
234 self.private_key = Some(loaded_key);
235
236 Ok(self)
237 }
238
239 pub fn compartment_id(mut self, compartment_id: impl Into<String>) -> Self {
240 self.compartment_id = Some(compartment_id.into());
241 self
242 }
243
244 pub fn build(self) -> Result<OciConfig> {
245 Ok(OciConfig {
246 user_id: self
247 .user_id
248 .ok_or_else(|| OciError::ConfigError("user_id is not set".to_string()))?,
249 tenancy_id: self
250 .tenancy_id
251 .ok_or_else(|| OciError::ConfigError("tenancy_id is not set".to_string()))?,
252 region: self
253 .region
254 .ok_or_else(|| OciError::ConfigError("region is not set".to_string()))?,
255 fingerprint: self
256 .fingerprint
257 .ok_or_else(|| OciError::ConfigError("fingerprint is not set".to_string()))?,
258 private_key: self
259 .private_key
260 .ok_or_else(|| OciError::ConfigError("private_key is not set".to_string()))?,
261 compartment_id: self.compartment_id,
262 })
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_builder_all_fields() {
272 let user_id = "ocid1.user.test";
273 let tenancy_id = "ocid1.tenancy.test";
274 let region = "ap-seoul-1";
275 let fingerprint = "aa:bb:cc:dd";
276 let config = OciConfig::builder()
277 .user_id(user_id)
278 .tenancy_id(tenancy_id)
279 .region(region)
280 .fingerprint(fingerprint)
281 .private_key("-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----")
282 .unwrap()
283 .build()
284 .unwrap();
285
286 assert_eq!(config.user_id, user_id);
287 assert_eq!(config.tenancy_id, tenancy_id);
288 assert_eq!(config.region, region);
289 assert_eq!(config.fingerprint, fingerprint);
290 assert!(config.private_key.contains("BEGIN RSA PRIVATE KEY"));
291 }
292
293 #[test]
294 fn test_builder_missing_user_id() {
295 let result = OciConfig::builder()
296 .tenancy_id("ocid1.tenancy.test")
297 .region("ap-seoul-1")
298 .fingerprint("aa:bb:cc:dd")
299 .private_key("-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----")
300 .unwrap()
301 .build();
302
303 assert!(result.is_err());
304 match result.unwrap_err() {
305 OciError::ConfigError(msg) => assert!(msg.contains("user_id")),
306 _ => panic!("Expected ConfigError"),
307 }
308 }
309
310 #[test]
311 fn test_builder_missing_tenancy_id() {
312 let result = OciConfig::builder()
313 .user_id("ocid1.user.test")
314 .region("ap-seoul-1")
315 .fingerprint("aa:bb:cc:dd")
316 .private_key("-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----")
317 .unwrap()
318 .build();
319
320 assert!(result.is_err());
321 match result.unwrap_err() {
322 OciError::ConfigError(msg) => assert!(msg.contains("tenancy_id")),
323 _ => panic!("Expected ConfigError"),
324 }
325 }
326
327 #[test]
328 fn test_builder_missing_region() {
329 let result = OciConfig::builder()
330 .user_id("ocid1.user.test")
331 .tenancy_id("ocid1.tenancy.test")
332 .fingerprint("aa:bb:cc:dd")
333 .private_key("-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----")
334 .unwrap()
335 .build();
336
337 assert!(result.is_err());
338 match result.unwrap_err() {
339 OciError::ConfigError(msg) => assert!(msg.contains("region")),
340 _ => panic!("Expected ConfigError"),
341 }
342 }
343
344 #[test]
345 fn test_builder_missing_fingerprint() {
346 let result = OciConfig::builder()
347 .user_id("ocid1.user.test")
348 .tenancy_id("ocid1.tenancy.test")
349 .region("ap-seoul-1")
350 .private_key("-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----")
351 .unwrap()
352 .build();
353
354 assert!(result.is_err());
355 match result.unwrap_err() {
356 OciError::ConfigError(msg) => assert!(msg.contains("fingerprint")),
357 _ => panic!("Expected ConfigError"),
358 }
359 }
360
361 #[test]
362 fn test_builder_missing_private_key() {
363 let result = OciConfig::builder()
364 .user_id("ocid1.user.test")
365 .tenancy_id("ocid1.tenancy.test")
366 .region("ap-seoul-1")
367 .fingerprint("aa:bb:cc:dd")
368 .build();
369
370 assert!(result.is_err());
371 match result.unwrap_err() {
372 OciError::ConfigError(msg) => assert!(msg.contains("private_key")),
373 _ => panic!("Expected ConfigError"),
374 }
375 }
376
377 #[test]
378 fn test_from_env_missing_user_id() {
379 unsafe {
380 std::env::remove_var("OCI_CONFIG");
381 std::env::remove_var("OCI_USER_ID");
382 std::env::remove_var("OCI_TENANCY_ID");
383 std::env::remove_var("OCI_REGION");
384 std::env::remove_var("OCI_FINGERPRINT");
385 std::env::remove_var("OCI_PRIVATE_KEY");
386 }
387
388 let result = OciConfig::from_env();
389 assert!(result.is_err());
390 if let Err(e) = result {
391 match e {
392 OciError::EnvError(msg) => {
393 assert!(msg.contains("OCI_USER_ID") || msg.contains("OCI_CONFIG"));
394 }
395 _ => panic!("Expected EnvError, got: {:?}", e),
396 }
397 }
398 }
399
400 #[test]
401 fn test_env_override_with_oci_config() {
402 unsafe {
403 std::env::remove_var("OCI_CONFIG");
405 std::env::remove_var("OCI_USER_ID");
406 std::env::remove_var("OCI_TENANCY_ID");
407 std::env::remove_var("OCI_REGION");
408 std::env::remove_var("OCI_FINGERPRINT");
409 std::env::remove_var("OCI_PRIVATE_KEY");
410 }
411
412 unsafe {
413 std::env::set_var("OCI_USER_ID", "ocid1.user.from_env");
415 std::env::set_var("OCI_REGION", "ap-seoul-1");
416 std::env::set_var(
417 "OCI_PRIVATE_KEY",
418 "-----BEGIN PRIVATE KEY-----\ntest_key\n-----END PRIVATE KEY-----",
419 );
420
421 std::env::set_var(
423 "OCI_CONFIG",
424 r#"
425[DEFAULT]
426user=ocid1.user.from_config
427tenancy=ocid1.tenancy.from_config
428region=us-phoenix-1
429fingerprint=aa:bb:cc:dd:ee:ff
430"#,
431 );
432 }
433
434 let config = OciConfig::from_env().expect("Failed to load config");
435
436 assert_eq!(config.user_id, "ocid1.user.from_env");
438 assert_eq!(config.region, "ap-seoul-1");
439 assert_eq!(
440 config.private_key,
441 "-----BEGIN PRIVATE KEY-----\ntest_key\n-----END PRIVATE KEY-----"
442 );
443
444 assert_eq!(config.tenancy_id, "ocid1.tenancy.from_config");
446 assert_eq!(config.fingerprint, "aa:bb:cc:dd:ee:ff");
447
448 unsafe {
449 std::env::remove_var("OCI_CONFIG");
450 std::env::remove_var("OCI_USER_ID");
451 std::env::remove_var("OCI_REGION");
452 std::env::remove_var("OCI_PRIVATE_KEY");
453 }
454 }
455
456 #[test]
457 fn test_oci_private_key_not_in_config() {
458 unsafe {
459 std::env::set_var(
461 "OCI_CONFIG",
462 r#"
463[DEFAULT]
464user=ocid1.user.test
465tenancy=ocid1.tenancy.test
466region=us-phoenix-1
467fingerprint=aa:bb:cc:dd:ee:ff
468private_key=this_should_be_ignored
469"#,
470 );
471
472 std::env::set_var(
474 "OCI_PRIVATE_KEY",
475 "-----BEGIN PRIVATE KEY-----\ntest_key\n-----END PRIVATE KEY-----",
476 );
477 }
478
479 let config = OciConfig::from_env().expect("Failed to load config");
480
481 assert_eq!(
483 config.private_key,
484 "-----BEGIN PRIVATE KEY-----\ntest_key\n-----END PRIVATE KEY-----"
485 );
486
487 unsafe {
488 std::env::remove_var("OCI_CONFIG");
489 std::env::remove_var("OCI_PRIVATE_KEY");
490 }
491 }
492}