1use crate::{
2 Client,
3 client::{get_deployment::GetDeploymentError, get_mongodb_secret::get_mongodb_secret},
4 docker::{DockerInspectContainer, RunCommandInContainer, RunCommandInContainerError},
5};
6
7#[derive(Debug, thiserror::Error)]
8pub enum GetDeploymentIdError {
9 #[error("Failed to get deployment: {0}")]
10 GetDeployment(#[from] GetDeploymentError),
11 #[error("Failed to get MongoDB username: {0}")]
12 GetMongodbUsername(RunCommandInContainerError),
13 #[error("Failed to get MongoDB password: {0}")]
14 GetMongodbPassword(RunCommandInContainerError),
15 #[error("Failed to run mongosh command: {0}")]
16 RunMongoshCommand(RunCommandInContainerError),
17 #[error("Deployment ID is empty")]
18 DeploymentIdEmpty,
19}
20
21impl<D: DockerInspectContainer + RunCommandInContainer> Client<D> {
22 pub async fn get_deployment_id(
24 &self,
25 cluster_id_or_name: &str,
26 ) -> Result<String, GetDeploymentIdError> {
27 let deployment = self.get_deployment(cluster_id_or_name).await?;
28
29 let mongodb_root_username = get_mongodb_secret(
31 self.docker.as_ref(),
32 &deployment,
33 |d| d.mongodb_initdb_root_username.as_deref(),
34 |d| d.mongodb_initdb_root_username_file.as_deref(),
35 )
36 .await
37 .map_err(GetDeploymentIdError::GetMongodbUsername)?;
38
39 let mongodb_root_password = get_mongodb_secret(
41 self.docker.as_ref(),
42 &deployment,
43 |d| d.mongodb_initdb_root_password.as_deref(),
44 |d| d.mongodb_initdb_root_password_file.as_deref(),
45 )
46 .await
47 .map_err(GetDeploymentIdError::GetMongodbPassword)?;
48
49 let mut mongosh_command = vec![
51 "mongosh".to_string(),
52 "mongodb://127.0.0.1:27017/?directConnection=true".to_string(),
53 ];
54 if let Some(username) = mongodb_root_username {
55 mongosh_command.push(format!("--username={}", username));
56 }
57 if let Some(password) = mongodb_root_password {
58 mongosh_command.push(format!("--password={}", password));
59 }
60
61 mongosh_command.push("--eval".to_string());
62 mongosh_command.push("db.getSiblingDB('admin').atlascli.findOne()?.uuid".to_string());
63 mongosh_command.push("--quiet".to_string());
64
65 let command_output = self
67 .docker
68 .run_command_in_container(&deployment.container_id, mongosh_command)
69 .await
70 .map_err(GetDeploymentIdError::RunMongoshCommand)?;
71
72 match command_output.stdout.into_iter().next() {
73 Some(line) if line.is_empty() => Err(GetDeploymentIdError::DeploymentIdEmpty),
74 Some(line) => Ok(line),
75 None => Err(GetDeploymentIdError::DeploymentIdEmpty),
76 }
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use crate::{client::get_deployment::GetDeploymentError, docker::CommandOutput};
84 use bollard::{
85 errors::Error as BollardError,
86 query_parameters::InspectContainerOptions,
87 secret::{
88 ContainerConfig, ContainerInspectResponse, ContainerState, ContainerStateStatusEnum,
89 },
90 };
91 use maplit::hashmap;
92 use mockall::{mock, predicate::eq};
93
94 mock! {
95 Docker {}
96
97 impl DockerInspectContainer for Docker {
98 async fn inspect_container(
99 &self,
100 container_id: &str,
101 options: Option<InspectContainerOptions>,
102 ) -> Result<ContainerInspectResponse, BollardError>;
103 }
104
105 impl RunCommandInContainer for Docker {
106 async fn run_command_in_container(
107 &self,
108 container_id: &str,
109 command: Vec<String>,
110 ) -> Result<CommandOutput, RunCommandInContainerError>;
111 }
112 }
113
114 fn create_test_container_inspect_response() -> ContainerInspectResponse {
116 ContainerInspectResponse {
117 id: Some("test_container_id".to_string()),
118 name: Some("/test-deployment".to_string()),
119 config: Some(ContainerConfig {
120 labels: Some(hashmap! {
121 "mongodb-atlas-local".to_string() => "container".to_string(),
122 "version".to_string() => "8.0.0".to_string(),
123 "mongodb-type".to_string() => "community".to_string(),
124 }),
125 env: Some(vec!["TOOL=ATLASCLI".to_string()]),
126 ..Default::default()
127 }),
128 state: Some(ContainerState {
129 status: Some(ContainerStateStatusEnum::RUNNING),
130 ..Default::default()
131 }),
132 ..Default::default()
133 }
134 }
135
136 #[tokio::test]
137 async fn test_get_deployment_id_happy_path_no_auth() {
138 let mut mock_docker = MockDocker::new();
140 let container_inspect_response = create_test_container_inspect_response();
141
142 mock_docker
144 .expect_inspect_container()
145 .with(eq("test-deployment"), eq(None::<InspectContainerOptions>))
146 .times(1)
147 .returning(move |_, _| Ok(container_inspect_response.clone()));
148
149 mock_docker
151 .expect_run_command_in_container()
152 .with(
153 eq("test_container_id"),
154 eq(vec![
155 "mongosh".to_string(),
156 "mongodb://127.0.0.1:27017/?directConnection=true".to_string(),
157 "--eval".to_string(),
158 "db.getSiblingDB('admin').atlascli.findOne()?.uuid".to_string(),
159 "--quiet".to_string(),
160 ]),
161 )
162 .times(1)
163 .returning(|_, _| {
164 Ok(CommandOutput {
165 stdout: vec!["deployment-uuid-123".to_string()],
166 stderr: vec![],
167 })
168 });
169
170 let client = Client::new(mock_docker);
171
172 let result = client.get_deployment_id("test-deployment").await;
174
175 assert!(result.is_ok());
177 assert_eq!(result.unwrap(), "deployment-uuid-123");
178 }
179
180 #[tokio::test]
181 async fn test_get_deployment_id_happy_path_env_auth() {
182 let mut mock_docker = MockDocker::new();
184 let mut container_inspect_response = create_test_container_inspect_response();
185
186 if let Some(config) = container_inspect_response.config.as_mut()
188 && let Some(env) = config.env.as_mut()
189 {
190 env.push("MONGODB_INITDB_ROOT_USERNAME=testuser".to_string());
191 env.push("MONGODB_INITDB_ROOT_PASSWORD=testpass".to_string());
192 }
193
194 mock_docker
196 .expect_inspect_container()
197 .with(eq("test-deployment"), eq(None::<InspectContainerOptions>))
198 .times(1)
199 .returning(move |_, _| Ok(container_inspect_response.clone()));
200
201 mock_docker
203 .expect_run_command_in_container()
204 .with(
205 eq("test_container_id"),
206 eq(vec![
207 "mongosh".to_string(),
208 "mongodb://127.0.0.1:27017/?directConnection=true".to_string(),
209 "--username=testuser".to_string(),
210 "--password=testpass".to_string(),
211 "--eval".to_string(),
212 "db.getSiblingDB('admin').atlascli.findOne()?.uuid".to_string(),
213 "--quiet".to_string(),
214 ]),
215 )
216 .times(1)
217 .returning(|_, _| {
218 Ok(CommandOutput {
219 stdout: vec!["deployment-uuid-456".to_string()],
220 stderr: vec![],
221 })
222 });
223
224 let client = Client::new(mock_docker);
225
226 let result = client.get_deployment_id("test-deployment").await;
228
229 assert!(result.is_ok());
231 assert_eq!(result.unwrap(), "deployment-uuid-456");
232 }
233
234 #[tokio::test]
235 async fn test_get_deployment_id_happy_path_file_auth() {
236 let mut mock_docker = MockDocker::new();
238 let mut container_inspect_response = create_test_container_inspect_response();
239
240 if let Some(config) = container_inspect_response.config.as_mut()
242 && let Some(env) = config.env.as_mut()
243 {
244 env.push("MONGODB_INITDB_ROOT_USERNAME_FILE=/run/secrets/username".to_string());
245 env.push("MONGODB_INITDB_ROOT_PASSWORD_FILE=/run/secrets/password".to_string());
246 }
247
248 mock_docker
250 .expect_inspect_container()
251 .with(eq("test-deployment"), eq(None::<InspectContainerOptions>))
252 .times(1)
253 .returning(move |_, _| Ok(container_inspect_response.clone()));
254
255 mock_docker
257 .expect_run_command_in_container()
258 .with(
259 eq("test_container_id"),
260 eq(vec!["cat".to_string(), "/run/secrets/username".to_string()]),
261 )
262 .times(1)
263 .returning(|_, _| {
264 Ok(CommandOutput {
265 stdout: vec!["fileuser".to_string()],
266 stderr: vec![],
267 })
268 });
269
270 mock_docker
272 .expect_run_command_in_container()
273 .with(
274 eq("test_container_id"),
275 eq(vec!["cat".to_string(), "/run/secrets/password".to_string()]),
276 )
277 .times(1)
278 .returning(|_, _| {
279 Ok(CommandOutput {
280 stdout: vec!["filepass".to_string()],
281 stderr: vec![],
282 })
283 });
284
285 mock_docker
287 .expect_run_command_in_container()
288 .with(
289 eq("test_container_id"),
290 eq(vec![
291 "mongosh".to_string(),
292 "mongodb://127.0.0.1:27017/?directConnection=true".to_string(),
293 "--username=fileuser".to_string(),
294 "--password=filepass".to_string(),
295 "--eval".to_string(),
296 "db.getSiblingDB('admin').atlascli.findOne()?.uuid".to_string(),
297 "--quiet".to_string(),
298 ]),
299 )
300 .times(1)
301 .returning(|_, _| {
302 Ok(CommandOutput {
303 stdout: vec!["deployment-uuid-789".to_string()],
304 stderr: vec![],
305 })
306 });
307
308 let client = Client::new(mock_docker);
309
310 let result = client.get_deployment_id("test-deployment").await;
312
313 assert!(result.is_ok());
315 assert_eq!(result.unwrap(), "deployment-uuid-789");
316 }
317
318 #[tokio::test]
319 async fn test_get_deployment_id_get_deployment_error() {
320 let mut mock_docker = MockDocker::new();
322
323 mock_docker
325 .expect_inspect_container()
326 .with(
327 eq("nonexistent-deployment"),
328 eq(None::<InspectContainerOptions>),
329 )
330 .times(1)
331 .returning(|_, _| {
332 Err(BollardError::DockerResponseServerError {
333 status_code: 404,
334 message: "No such container".to_string(),
335 })
336 });
337
338 let client = Client::new(mock_docker);
339
340 let result = client.get_deployment_id("nonexistent-deployment").await;
342
343 assert!(result.is_err());
345 match result.unwrap_err() {
346 GetDeploymentIdError::GetDeployment(GetDeploymentError::ContainerInspect(_)) => {
347 }
349 other => panic!("Expected GetDeployment error, got: {:?}", other),
350 }
351 }
352
353 #[tokio::test]
354 async fn test_get_deployment_id_get_mongodb_username_error() {
355 let mut mock_docker = MockDocker::new();
357 let mut container_inspect_response = create_test_container_inspect_response();
358
359 if let Some(config) = container_inspect_response.config.as_mut()
361 && let Some(env) = config.env.as_mut()
362 {
363 env.push("MONGODB_INITDB_ROOT_USERNAME_FILE=/run/secrets/username".to_string());
364 }
365
366 mock_docker
368 .expect_inspect_container()
369 .with(eq("test-deployment"), eq(None::<InspectContainerOptions>))
370 .times(1)
371 .returning(move |_, _| Ok(container_inspect_response.clone()));
372
373 mock_docker
375 .expect_run_command_in_container()
376 .with(
377 eq("test_container_id"),
378 eq(vec!["cat".to_string(), "/run/secrets/username".to_string()]),
379 )
380 .times(1)
381 .returning(|_, _| {
382 Err(RunCommandInContainerError::CreateExec(
383 BollardError::DockerResponseServerError {
384 status_code: 500,
385 message: "Failed to read file".to_string(),
386 },
387 ))
388 });
389
390 let client = Client::new(mock_docker);
391
392 let result = client.get_deployment_id("test-deployment").await;
394
395 assert!(result.is_err());
397 match result.unwrap_err() {
398 GetDeploymentIdError::GetMongodbUsername(_) => {
399 }
401 other => panic!("Expected GetMongodbUsername error, got: {:?}", other),
402 }
403 }
404
405 #[tokio::test]
406 async fn test_get_deployment_id_get_mongodb_password_error() {
407 let mut mock_docker = MockDocker::new();
409 let mut container_inspect_response = create_test_container_inspect_response();
410
411 if let Some(config) = container_inspect_response.config.as_mut()
413 && let Some(env) = config.env.as_mut()
414 {
415 env.push("MONGODB_INITDB_ROOT_PASSWORD_FILE=/run/secrets/password".to_string());
416 }
417
418 mock_docker
420 .expect_inspect_container()
421 .with(eq("test-deployment"), eq(None::<InspectContainerOptions>))
422 .times(1)
423 .returning(move |_, _| Ok(container_inspect_response.clone()));
424
425 mock_docker
427 .expect_run_command_in_container()
428 .with(
429 eq("test_container_id"),
430 eq(vec!["cat".to_string(), "/run/secrets/password".to_string()]),
431 )
432 .times(1)
433 .returning(|_, _| {
434 Err(RunCommandInContainerError::StartExec(
435 BollardError::DockerResponseServerError {
436 status_code: 500,
437 message: "Failed to start exec".to_string(),
438 },
439 ))
440 });
441
442 let client = Client::new(mock_docker);
443
444 let result = client.get_deployment_id("test-deployment").await;
446
447 assert!(result.is_err());
449 match result.unwrap_err() {
450 GetDeploymentIdError::GetMongodbPassword(_) => {
451 }
453 other => panic!("Expected GetMongodbPassword error, got: {:?}", other),
454 }
455 }
456
457 #[tokio::test]
458 async fn test_get_deployment_id_run_mongosh_command_error() {
459 let mut mock_docker = MockDocker::new();
461 let container_inspect_response = create_test_container_inspect_response();
462
463 mock_docker
465 .expect_inspect_container()
466 .with(eq("test-deployment"), eq(None::<InspectContainerOptions>))
467 .times(1)
468 .returning(move |_, _| Ok(container_inspect_response.clone()));
469
470 mock_docker
472 .expect_run_command_in_container()
473 .with(
474 eq("test_container_id"),
475 eq(vec![
476 "mongosh".to_string(),
477 "mongodb://127.0.0.1:27017/?directConnection=true".to_string(),
478 "--eval".to_string(),
479 "db.getSiblingDB('admin').atlascli.findOne()?.uuid".to_string(),
480 "--quiet".to_string(),
481 ]),
482 )
483 .times(1)
484 .returning(|_, _| Err(RunCommandInContainerError::GetOutput));
485
486 let client = Client::new(mock_docker);
487
488 let result = client.get_deployment_id("test-deployment").await;
490
491 assert!(result.is_err());
493 match result.unwrap_err() {
494 GetDeploymentIdError::RunMongoshCommand(_) => {
495 }
497 other => panic!("Expected RunMongoshCommand error, got: {:?}", other),
498 }
499 }
500
501 #[tokio::test]
502 async fn test_get_deployment_id_mixed_auth_env_username_file_password() {
503 let mut mock_docker = MockDocker::new();
505 let mut container_inspect_response = create_test_container_inspect_response();
506
507 if let Some(config) = container_inspect_response.config.as_mut()
509 && let Some(env) = config.env.as_mut()
510 {
511 env.push("MONGODB_INITDB_ROOT_USERNAME=envuser".to_string());
512 env.push("MONGODB_INITDB_ROOT_PASSWORD_FILE=/run/secrets/password".to_string());
513 }
514
515 mock_docker
517 .expect_inspect_container()
518 .with(eq("test-deployment"), eq(None::<InspectContainerOptions>))
519 .times(1)
520 .returning(move |_, _| Ok(container_inspect_response.clone()));
521
522 mock_docker
524 .expect_run_command_in_container()
525 .with(
526 eq("test_container_id"),
527 eq(vec!["cat".to_string(), "/run/secrets/password".to_string()]),
528 )
529 .times(1)
530 .returning(|_, _| {
531 Ok(CommandOutput {
532 stdout: vec!["filepass".to_string()],
533 stderr: vec![],
534 })
535 });
536
537 mock_docker
539 .expect_run_command_in_container()
540 .with(
541 eq("test_container_id"),
542 eq(vec![
543 "mongosh".to_string(),
544 "mongodb://127.0.0.1:27017/?directConnection=true".to_string(),
545 "--username=envuser".to_string(),
546 "--password=filepass".to_string(),
547 "--eval".to_string(),
548 "db.getSiblingDB('admin').atlascli.findOne()?.uuid".to_string(),
549 "--quiet".to_string(),
550 ]),
551 )
552 .times(1)
553 .returning(|_, _| {
554 Ok(CommandOutput {
555 stdout: vec!["deployment-uuid-mixed".to_string()],
556 stderr: vec![],
557 })
558 });
559
560 let client = Client::new(mock_docker);
561
562 let result = client.get_deployment_id("test-deployment").await;
564
565 assert!(result.is_ok());
567 assert_eq!(result.unwrap(), "deployment-uuid-mixed");
568 }
569
570 #[tokio::test]
571 async fn test_get_deployment_id_all_run_command_in_container_error_variants() {
572 let mut mock_docker = MockDocker::new();
576 let container_inspect_response = create_test_container_inspect_response();
577
578 mock_docker
579 .expect_inspect_container()
580 .with(eq("test-deployment"), eq(None::<InspectContainerOptions>))
581 .times(1)
582 .returning(move |_, _| Ok(container_inspect_response.clone()));
583
584 mock_docker
585 .expect_run_command_in_container()
586 .with(
587 eq("test_container_id"),
588 eq(vec![
589 "mongosh".to_string(),
590 "mongodb://127.0.0.1:27017/?directConnection=true".to_string(),
591 "--eval".to_string(),
592 "db.getSiblingDB('admin').atlascli.findOne()?.uuid".to_string(),
593 "--quiet".to_string(),
594 ]),
595 )
596 .times(1)
597 .returning(|_, _| {
598 Err(RunCommandInContainerError::GetOutputError(
599 BollardError::DockerResponseServerError {
600 status_code: 500,
601 message: "Failed to get output".to_string(),
602 },
603 ))
604 });
605
606 let client = Client::new(mock_docker);
607 let result = client.get_deployment_id("test-deployment").await;
608
609 assert!(result.is_err());
610 match result.unwrap_err() {
611 GetDeploymentIdError::RunMongoshCommand(
612 RunCommandInContainerError::GetOutputError(_),
613 ) => {
614 }
616 other => panic!(
617 "Expected RunMongoshCommand GetOutputError, got: {:?}",
618 other
619 ),
620 }
621 }
622
623 #[tokio::test]
624 async fn test_get_deployment_id_empty_stdout() {
625 let mut mock_docker = MockDocker::new();
627 let container_inspect_response = create_test_container_inspect_response();
628
629 mock_docker
630 .expect_inspect_container()
631 .with(eq("test-deployment"), eq(None::<InspectContainerOptions>))
632 .times(1)
633 .returning(move |_, _| Ok(container_inspect_response.clone()));
634
635 mock_docker
636 .expect_run_command_in_container()
637 .with(
638 eq("test_container_id"),
639 eq(vec![
640 "mongosh".to_string(),
641 "mongodb://127.0.0.1:27017/?directConnection=true".to_string(),
642 "--eval".to_string(),
643 "db.getSiblingDB('admin').atlascli.findOne()?.uuid".to_string(),
644 "--quiet".to_string(),
645 ]),
646 )
647 .times(1)
648 .returning(|_, _| {
649 Ok(CommandOutput {
650 stdout: vec![],
651 stderr: vec![],
652 })
653 });
654
655 let client = Client::new(mock_docker);
656 let result = client.get_deployment_id("test-deployment").await;
657
658 assert!(result.is_err());
659 }
660
661 #[tokio::test]
662 async fn test_get_deployment_id_username_only() {
663 let mut mock_docker = MockDocker::new();
665 let mut container_inspect_response = create_test_container_inspect_response();
666
667 if let Some(config) = container_inspect_response.config.as_mut()
669 && let Some(env) = config.env.as_mut()
670 {
671 env.push("MONGODB_INITDB_ROOT_USERNAME=onlyuser".to_string());
672 }
673
674 mock_docker
675 .expect_inspect_container()
676 .with(eq("test-deployment"), eq(None::<InspectContainerOptions>))
677 .times(1)
678 .returning(move |_, _| Ok(container_inspect_response.clone()));
679
680 mock_docker
681 .expect_run_command_in_container()
682 .with(
683 eq("test_container_id"),
684 eq(vec![
685 "mongosh".to_string(),
686 "mongodb://127.0.0.1:27017/?directConnection=true".to_string(),
687 "--username=onlyuser".to_string(),
688 "--eval".to_string(),
689 "db.getSiblingDB('admin').atlascli.findOne()?.uuid".to_string(),
690 "--quiet".to_string(),
691 ]),
692 )
693 .times(1)
694 .returning(|_, _| {
695 Ok(CommandOutput {
696 stdout: vec!["deployment-uuid-username-only".to_string()],
697 stderr: vec![],
698 })
699 });
700
701 let client = Client::new(mock_docker);
702 let result = client.get_deployment_id("test-deployment").await;
703
704 assert!(result.is_ok());
705 assert_eq!(result.unwrap(), "deployment-uuid-username-only");
706 }
707
708 #[tokio::test]
709 async fn test_get_deployment_id_password_only() {
710 let mut mock_docker = MockDocker::new();
712 let mut container_inspect_response = create_test_container_inspect_response();
713
714 if let Some(config) = container_inspect_response.config.as_mut()
716 && let Some(env) = config.env.as_mut()
717 {
718 env.push("MONGODB_INITDB_ROOT_PASSWORD=onlypass".to_string());
719 }
720
721 mock_docker
722 .expect_inspect_container()
723 .with(eq("test-deployment"), eq(None::<InspectContainerOptions>))
724 .times(1)
725 .returning(move |_, _| Ok(container_inspect_response.clone()));
726
727 mock_docker
728 .expect_run_command_in_container()
729 .with(
730 eq("test_container_id"),
731 eq(vec![
732 "mongosh".to_string(),
733 "mongodb://127.0.0.1:27017/?directConnection=true".to_string(),
734 "--password=onlypass".to_string(),
735 "--eval".to_string(),
736 "db.getSiblingDB('admin').atlascli.findOne()?.uuid".to_string(),
737 "--quiet".to_string(),
738 ]),
739 )
740 .times(1)
741 .returning(|_, _| {
742 Ok(CommandOutput {
743 stdout: vec!["deployment-uuid-password-only".to_string()],
744 stderr: vec![],
745 })
746 });
747
748 let client = Client::new(mock_docker);
749 let result = client.get_deployment_id("test-deployment").await;
750
751 assert!(result.is_ok());
752 assert_eq!(result.unwrap(), "deployment-uuid-password-only");
753 }
754}