1use bollard::{
2 models::ContainerCreateBody,
3 query_parameters::{CreateContainerOptions, StartContainerOptions},
4};
5use tokio::sync::oneshot;
6
7use crate::{
8 GetDeploymentError,
9 client::Client,
10 docker::{
11 DockerCreateContainer, DockerError, DockerInspectContainer, DockerPullImage,
12 DockerStartContainer,
13 },
14 models::{ATLAS_LOCAL_IMAGE, CreateDeploymentOptions, Deployment, WatchOptions},
15};
16
17use super::{PullImageError, WatchDeploymentError};
18
19mod progress;
20
21pub use progress::{CreateDeploymentProgress, CreateDeploymentStepOutcome};
22use progress::{CreateDeploymentProgressSender, create_progress_pairs};
23
24#[derive(Debug, thiserror::Error)]
25pub enum CreateDeploymentError {
26 #[error("Failed to create container: {0}")]
27 CreateContainer(DockerError),
28 #[error(transparent)]
29 PullImage(#[from] PullImageError),
30 #[error("Container already exists: {0}")]
31 ContainerAlreadyExists(String),
32 #[error("Failed to check status of started container: {0}")]
33 ContainerInspect(DockerError),
34 #[error("Created Deployment {0} is not healthy")]
35 UnhealthyDeployment(String),
36 #[error("Unable to get details for Deployment: {0}")]
37 GetDeploymentError(GetDeploymentError),
38 #[error("Error when waiting for deployment to become healthy: {0}")]
39 WatchDeployment(#[from] WatchDeploymentError),
40 #[error("Error when receiving deployment: {0}")]
41 ReceiveDeployment(#[from] oneshot::error::RecvError),
42 #[error(
43 "Image must not include a tag. Use the `image_tag` field to specify a tag. Got: \"{0}\""
44 )]
45 InvalidImage(String),
46}
47
48impl<
49 D: DockerPullImage
50 + DockerCreateContainer
51 + DockerStartContainer
52 + DockerInspectContainer
53 + Send
54 + Sync
55 + 'static,
56> Client<D>
57{
58 pub fn create_deployment(
60 &self,
61 deployment_options: CreateDeploymentOptions,
62 ) -> CreateDeploymentProgress {
63 let (sender, receiver) = create_progress_pairs();
64 let client = self.clone();
65
66 tokio::spawn(async move {
72 let mut progress: CreateDeploymentProgressSender = sender;
73
74 let result = client
75 .create_deployment_inner(deployment_options, &mut progress)
76 .await;
77
78 progress.finalize_deployment(result).await;
81 });
82
83 receiver
84 }
85
86 async fn create_deployment_inner(
87 &self,
88 deployment_options: CreateDeploymentOptions,
89 progress: &mut CreateDeploymentProgressSender,
90 ) -> Result<Deployment, CreateDeploymentError> {
91 if let Some(image) = &deployment_options.image
92 && image.contains(':')
93 {
94 return Err(CreateDeploymentError::InvalidImage(image.clone()));
95 }
96
97 let will_pull_image = !deployment_options.skip_pull_image.unwrap_or(false);
99 if will_pull_image {
100 let tag = deployment_options
101 .image_tag
102 .as_ref()
103 .map(ToString::to_string)
104 .unwrap_or_else(|| "latest".to_string());
105
106 self.pull_image(
107 deployment_options
108 .image
109 .as_ref()
110 .unwrap_or(&ATLAS_LOCAL_IMAGE.to_string()),
111 tag.as_str(),
112 )
113 .await?;
114 }
115
116 progress
117 .set_pull_image_finished(if will_pull_image {
118 CreateDeploymentStepOutcome::Success
119 } else {
120 CreateDeploymentStepOutcome::Skipped
121 })
122 .await;
123
124 let create_container_options: CreateContainerOptions = (&deployment_options).into();
126 let create_container_config: ContainerCreateBody = (&deployment_options).into();
127
128 #[allow(clippy::expect_used)]
131 let cluster_name = create_container_options
132 .name
133 .clone()
134 .expect("Container name to be set by CreateContainerOptions::from");
135
136 self.docker
137 .create_container(Some(create_container_options), create_container_config)
138 .await
139 .map_err(|err| match err {
140 DockerError::Conflict => {
141 CreateDeploymentError::ContainerAlreadyExists(cluster_name.to_string())
142 }
143 _ => CreateDeploymentError::CreateContainer(err),
144 })?;
145
146 progress
147 .set_create_container_finished(CreateDeploymentStepOutcome::Success)
148 .await;
149
150 self.docker
152 .start_container(&cluster_name.to_string(), None::<StartContainerOptions>)
153 .await
154 .map_err(CreateDeploymentError::CreateContainer)?;
155
156 progress
157 .set_start_container_finished(CreateDeploymentStepOutcome::Success)
158 .await;
159
160 let will_wait_for_healthy = deployment_options.wait_until_healthy.unwrap_or(true);
162 if will_wait_for_healthy {
163 let watch_options = WatchOptions {
164 timeout_duration: deployment_options.wait_until_healthy_timeout,
165 allow_unhealthy_initial_state: false,
166 };
167 self.wait_for_healthy_deployment(&cluster_name, watch_options)
168 .await?;
169 }
170
171 progress
172 .set_wait_for_healthy_deployment_finished(if will_wait_for_healthy {
173 CreateDeploymentStepOutcome::Success
174 } else {
175 CreateDeploymentStepOutcome::Skipped
176 })
177 .await;
178
179 self.get_deployment(&cluster_name)
181 .await
182 .map_err(CreateDeploymentError::GetDeploymentError)
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use crate::client::WatchDeploymentError;
190 use crate::docker::DockerError;
191 use crate::models::{ContainerHealthStatus, ImageTag};
192 use bollard::{
193 models::{
194 ContainerConfig, ContainerCreateResponse, ContainerInspectResponse, ContainerState,
195 ContainerStateStatusEnum, HealthStatusEnum,
196 },
197 query_parameters::InspectContainerOptions,
198 };
199 use maplit::hashmap;
200 use mockall::mock;
201 use pretty_assertions::assert_eq;
202 use tokio::time;
203
204 mock! {
205 Docker {}
206
207 impl DockerPullImage for Docker {
208 async fn pull_image(&self, image: &str, tag: &str) -> Result<(), DockerError>;
209 }
210
211 impl DockerCreateContainer for Docker {
212 async fn create_container(
213 &self,
214 options: Option<CreateContainerOptions>,
215 config: ContainerCreateBody,
216 ) -> Result<ContainerCreateResponse, DockerError>;
217 }
218
219 impl DockerStartContainer for Docker {
220 async fn start_container(
221 &self,
222 container_id: &str,
223 options: Option<StartContainerOptions>,
224 ) -> Result<(), DockerError>;
225 }
226
227 impl DockerInspectContainer for Docker {
228 async fn inspect_container(
229 &self,
230 container_id: &str,
231 options: Option<InspectContainerOptions>,
232 ) -> Result<ContainerInspectResponse, DockerError>;
233 }
234 }
235
236 fn create_test_container_inspect_response() -> ContainerInspectResponse {
237 ContainerInspectResponse {
238 id: Some("test_container_id".to_string()),
239 name: Some("/test-deployment".to_string()),
240 config: Some(ContainerConfig {
241 labels: Some(hashmap! {
242 "mongodb-atlas-local".to_string() => "container".to_string(),
243 "version".to_string() => "8.0.0".to_string(),
244 "mongodb-type".to_string() => "community".to_string(),
245 }),
246 env: Some(vec!["TOOL=ATLASCLI".to_string()]),
247 ..Default::default()
248 }),
249 state: Some(ContainerState {
250 status: Some(ContainerStateStatusEnum::RUNNING),
251 health: Some(bollard::models::Health {
252 status: Some(HealthStatusEnum::HEALTHY),
253 ..Default::default()
254 }),
255 ..Default::default()
256 }),
257 ..Default::default()
258 }
259 }
260
261 fn create_test_container_inspect_response_unhealthy() -> ContainerInspectResponse {
262 ContainerInspectResponse {
263 id: Some("test_container_id".to_string()),
264 name: Some("/test-deployment".to_string()),
265 config: Some(ContainerConfig {
266 labels: Some(hashmap! {
267 "mongodb-atlas-local".to_string() => "container".to_string(),
268 "version".to_string() => "8.0.0".to_string(),
269 "mongodb-type".to_string() => "community".to_string(),
270 }),
271 env: Some(vec!["TOOL=ATLASCLI".to_string()]),
272 ..Default::default()
273 }),
274 state: Some(ContainerState {
275 health: Some(bollard::models::Health {
276 status: Some(HealthStatusEnum::UNHEALTHY),
277 ..Default::default()
278 }),
279 ..Default::default()
280 }),
281 ..Default::default()
282 }
283 }
284
285 fn create_test_container_inspect_response_starting() -> ContainerInspectResponse {
286 ContainerInspectResponse {
287 id: Some("test_container_id".to_string()),
288 name: Some("/test-deployment".to_string()),
289 config: Some(ContainerConfig {
290 labels: Some(hashmap! {
291 "mongodb-atlas-local".to_string() => "container".to_string(),
292 "version".to_string() => "8.0.0".to_string(),
293 "mongodb-type".to_string() => "community".to_string(),
294 }),
295 env: Some(vec!["TOOL=ATLASCLI".to_string()]),
296 ..Default::default()
297 }),
298 state: Some(ContainerState {
299 health: Some(bollard::models::Health {
300 status: Some(HealthStatusEnum::STARTING),
301 ..Default::default()
302 }),
303 ..Default::default()
304 }),
305 ..Default::default()
306 }
307 }
308
309 fn create_test_container_inspect_response_no_state() -> ContainerInspectResponse {
310 ContainerInspectResponse {
311 id: Some("test_container_id".to_string()),
312 name: Some("/test-deployment".to_string()),
313 config: Some(ContainerConfig {
314 labels: Some(hashmap! {
315 "mongodb-atlas-local".to_string() => "container".to_string(),
316 "version".to_string() => "8.0.0".to_string(),
317 "mongodb-type".to_string() => "community".to_string(),
318 }),
319 env: Some(vec!["TOOL=ATLASCLI".to_string()]),
320 ..Default::default()
321 }),
322 state: None,
323 ..Default::default()
324 }
325 }
326
327 fn create_test_container_inspect_response_no_health() -> ContainerInspectResponse {
328 ContainerInspectResponse {
329 id: Some("test_container_id".to_string()),
330 name: Some("/test-deployment".to_string()),
331 config: Some(ContainerConfig {
332 labels: Some(hashmap! {
333 "mongodb-atlas-local".to_string() => "container".to_string(),
334 "version".to_string() => "8.0.0".to_string(),
335 "mongodb-type".to_string() => "community".to_string(),
336 }),
337 env: Some(vec!["TOOL=ATLASCLI".to_string()]),
338 ..Default::default()
339 }),
340 state: Some(ContainerState {
341 health: None,
342 ..Default::default()
343 }),
344 ..Default::default()
345 }
346 }
347
348 fn create_test_container_inspect_response_no_health_status() -> ContainerInspectResponse {
349 ContainerInspectResponse {
350 id: Some("test_container_id".to_string()),
351 name: Some("/test-deployment".to_string()),
352 config: Some(ContainerConfig {
353 labels: Some(hashmap! {
354 "mongodb-atlas-local".to_string() => "container".to_string(),
355 "version".to_string() => "8.0.0".to_string(),
356 "mongodb-type".to_string() => "community".to_string(),
357 }),
358 env: Some(vec!["TOOL=ATLASCLI".to_string()]),
359 ..Default::default()
360 }),
361 state: Some(ContainerState {
362 health: Some(bollard::models::Health {
363 status: None,
364 ..Default::default()
365 }),
366 ..Default::default()
367 }),
368 ..Default::default()
369 }
370 }
371
372 #[tokio::test]
373 async fn test_create_deployment() {
374 let mut mock_docker = MockDocker::new();
376 let options = CreateDeploymentOptions {
377 name: Some("test-deployment".to_string()),
378 ..Default::default()
379 };
380
381 mock_docker
383 .expect_pull_image()
384 .with(
385 mockall::predicate::eq(ATLAS_LOCAL_IMAGE),
386 mockall::predicate::eq("latest"),
387 )
388 .times(1)
389 .returning(|_, _| Ok(()));
390
391 mock_docker
392 .expect_create_container()
393 .times(1)
394 .returning(|_, _| {
395 Ok(ContainerCreateResponse {
396 id: "container_id".to_string(),
397 warnings: vec![],
398 })
399 });
400
401 mock_docker
402 .expect_start_container()
403 .with(
404 mockall::predicate::eq("test-deployment"),
405 mockall::predicate::eq(None::<StartContainerOptions>),
406 )
407 .times(1)
408 .returning(|_, _| Ok(()));
409
410 mock_docker
411 .expect_inspect_container()
412 .with(
413 mockall::predicate::eq("test-deployment"),
414 mockall::predicate::eq(None::<InspectContainerOptions>),
415 )
416 .times(2)
417 .returning(|_, _| Ok(create_test_container_inspect_response()));
418
419 let client = Client::new(mock_docker);
420
421 let result = client.create_deployment(options).await;
423
424 assert!(result.is_ok());
426 }
427
428 #[tokio::test]
429 async fn test_create_deployment_pulls_preview_tag_when_image_tag_preview() {
430 let mut mock_docker = MockDocker::new();
432 let options = CreateDeploymentOptions {
433 name: Some("test-deployment".to_string()),
434 image_tag: Some(ImageTag::Preview),
435 ..Default::default()
436 };
437
438 mock_docker
440 .expect_pull_image()
441 .with(
442 mockall::predicate::eq(ATLAS_LOCAL_IMAGE),
443 mockall::predicate::eq("preview"),
444 )
445 .times(1)
446 .returning(|_, _| Ok(()));
447
448 mock_docker
449 .expect_create_container()
450 .times(1)
451 .returning(|_, _| {
452 Ok(ContainerCreateResponse {
453 id: "container_id".to_string(),
454 warnings: vec![],
455 })
456 });
457
458 mock_docker
459 .expect_start_container()
460 .with(
461 mockall::predicate::eq("test-deployment"),
462 mockall::predicate::eq(None::<StartContainerOptions>),
463 )
464 .times(1)
465 .returning(|_, _| Ok(()));
466
467 mock_docker
468 .expect_inspect_container()
469 .with(
470 mockall::predicate::eq("test-deployment"),
471 mockall::predicate::eq(None::<InspectContainerOptions>),
472 )
473 .times(2)
474 .returning(|_, _| Ok(create_test_container_inspect_response()));
475
476 let client = Client::new(mock_docker);
477
478 let result = client.create_deployment(options).await;
480
481 assert!(result.is_ok());
483 }
484
485 #[tokio::test]
486 async fn test_create_deployment_pull_image_error() {
487 let mut mock_docker = MockDocker::new();
489 let options = CreateDeploymentOptions {
490 name: Some("test-deployment".to_string()),
491 ..Default::default()
492 };
493
494 mock_docker
496 .expect_pull_image()
497 .times(1)
498 .returning(|_, _| Err(DockerError::ServerError));
499
500 let client = Client::new(mock_docker);
501
502 let result = client.create_deployment(options).await;
504
505 assert!(result.is_err());
507 assert!(matches!(
508 result.unwrap_err(),
509 CreateDeploymentError::PullImage(_)
510 ));
511 }
512
513 #[tokio::test]
514 async fn test_create_deployment_container_already_exists() {
515 let mut mock_docker = MockDocker::new();
517 let options = CreateDeploymentOptions {
518 name: Some("test-deployment".to_string()),
519 ..Default::default()
520 };
521
522 mock_docker
524 .expect_pull_image()
525 .times(1)
526 .returning(|_, _| Ok(()));
527
528 mock_docker
529 .expect_create_container()
530 .times(1)
531 .returning(|_, _| Err(DockerError::Conflict));
532
533 let client = Client::new(mock_docker);
534
535 let result = client.create_deployment(options).await;
537
538 assert!(result.is_err());
540 match result.unwrap_err() {
541 CreateDeploymentError::ContainerAlreadyExists(name) => {
542 assert_eq!(name, "test-deployment");
543 }
544 _ => panic!("Expected ContainerAlreadyExists error"),
545 }
546 }
547
548 #[tokio::test]
549 async fn test_create_deployment_create_container_error() {
550 let mut mock_docker = MockDocker::new();
552 let options = CreateDeploymentOptions {
553 name: Some("test-deployment".to_string()),
554 ..Default::default()
555 };
556
557 mock_docker
559 .expect_pull_image()
560 .times(1)
561 .returning(|_, _| Ok(()));
562
563 mock_docker
564 .expect_create_container()
565 .times(1)
566 .returning(|_, _| Err(DockerError::ServerError));
567
568 let client = Client::new(mock_docker);
569
570 let result = client.create_deployment(options).await;
572
573 assert!(result.is_err());
575 assert!(matches!(
576 result.unwrap_err(),
577 CreateDeploymentError::CreateContainer(_)
578 ));
579 }
580
581 #[tokio::test]
582 async fn test_create_deployment_start_container_error() {
583 let mut mock_docker = MockDocker::new();
585 let options = CreateDeploymentOptions {
586 name: Some("test-deployment".to_string()),
587 ..Default::default()
588 };
589
590 mock_docker
592 .expect_pull_image()
593 .times(1)
594 .returning(|_, _| Ok(()));
595
596 mock_docker
597 .expect_create_container()
598 .times(1)
599 .returning(|_, _| {
600 Ok(ContainerCreateResponse {
601 id: "container_id".to_string(),
602 warnings: vec![],
603 })
604 });
605
606 mock_docker
607 .expect_start_container()
608 .times(1)
609 .returning(|_, _| Err(DockerError::ServerError));
610
611 let client = Client::new(mock_docker);
612
613 let result = client.create_deployment(options).await;
615
616 assert!(result.is_err());
618 assert!(matches!(
619 result.unwrap_err(),
620 CreateDeploymentError::CreateContainer(_)
621 ));
622 }
623
624 #[tokio::test]
625 async fn test_create_deployment_wait_for_healthy_deployment_unhealthy() {
626 let mut mock_docker = MockDocker::new();
628 let options = CreateDeploymentOptions {
629 name: Some("test-deployment".to_string()),
630 ..Default::default()
631 };
632
633 mock_docker
635 .expect_pull_image()
636 .with(
637 mockall::predicate::eq(ATLAS_LOCAL_IMAGE),
638 mockall::predicate::eq("latest"),
639 )
640 .times(1)
641 .returning(|_, _| Ok(()));
642
643 mock_docker
644 .expect_create_container()
645 .times(1)
646 .returning(|_, _| {
647 Ok(ContainerCreateResponse {
648 id: "container_id".to_string(),
649 warnings: vec![],
650 })
651 });
652
653 mock_docker
654 .expect_start_container()
655 .with(
656 mockall::predicate::eq("test-deployment"),
657 mockall::predicate::eq(None::<StartContainerOptions>),
658 )
659 .times(1)
660 .returning(|_, _| Ok(()));
661
662 mock_docker
663 .expect_inspect_container()
664 .with(
665 mockall::predicate::eq("test-deployment"),
666 mockall::predicate::eq(None::<InspectContainerOptions>),
667 )
668 .times(1)
669 .returning(|_, _| Ok(create_test_container_inspect_response_unhealthy()));
670
671 let client = Client::new(mock_docker);
672
673 let result = client.create_deployment(options).await;
675
676 assert!(result.is_err());
678 assert!(matches!(
679 result.unwrap_err(),
680 CreateDeploymentError::WatchDeployment(
681 WatchDeploymentError::UnhealthyDeployment { .. }
682 )
683 ));
684 }
685
686 #[tokio::test]
687 async fn test_wait_for_healthy_deployment_retries() {
688 let mut mock_docker = MockDocker::new();
690 let options = CreateDeploymentOptions {
691 name: Some("test-deployment".to_string()),
692 ..Default::default()
693 };
694
695 mock_docker
697 .expect_pull_image()
698 .with(
699 mockall::predicate::eq(ATLAS_LOCAL_IMAGE),
700 mockall::predicate::eq("latest"),
701 )
702 .times(1)
703 .returning(|_, _| Ok(()));
704
705 mock_docker
706 .expect_create_container()
707 .times(1)
708 .returning(|_, _| {
709 Ok(ContainerCreateResponse {
710 id: "container_id".to_string(),
711 warnings: vec![],
712 })
713 });
714
715 mock_docker
716 .expect_start_container()
717 .with(
718 mockall::predicate::eq("test-deployment"),
719 mockall::predicate::eq(None::<StartContainerOptions>),
720 )
721 .times(1)
722 .returning(|_, _| Ok(()));
723
724 mock_docker
725 .expect_inspect_container()
726 .with(
727 mockall::predicate::eq("test-deployment"),
728 mockall::predicate::eq(None::<InspectContainerOptions>),
729 )
730 .times(1)
731 .returning(|_, _| Ok(create_test_container_inspect_response_starting()));
732
733 mock_docker
734 .expect_inspect_container()
735 .with(
736 mockall::predicate::eq("test-deployment"),
737 mockall::predicate::eq(None::<InspectContainerOptions>),
738 )
739 .times(2)
740 .returning(|_, _| Ok(create_test_container_inspect_response()));
741
742 let client = Client::new(mock_docker);
743
744 let result = client.create_deployment(options).await;
746
747 assert!(result.is_ok());
749 }
750
751 #[tokio::test]
752 async fn test_wait_for_healthy_deployment_disabled() {
753 let mut mock_docker = MockDocker::new();
755 let options = CreateDeploymentOptions {
756 name: Some("test-deployment".to_string()),
757 wait_until_healthy: Some(false),
758 ..Default::default()
759 };
760
761 mock_docker
763 .expect_pull_image()
764 .with(
765 mockall::predicate::eq(ATLAS_LOCAL_IMAGE),
766 mockall::predicate::eq("latest"),
767 )
768 .times(1)
769 .returning(|_, _| Ok(()));
770
771 mock_docker
772 .expect_create_container()
773 .times(1)
774 .returning(|_, _| {
775 Ok(ContainerCreateResponse {
776 id: "container_id".to_string(),
777 warnings: vec![],
778 })
779 });
780
781 mock_docker
782 .expect_start_container()
783 .with(
784 mockall::predicate::eq("test-deployment"),
785 mockall::predicate::eq(None::<StartContainerOptions>),
786 )
787 .times(1)
788 .returning(|_, _| Ok(()));
789
790 mock_docker
791 .expect_inspect_container()
792 .with(
793 mockall::predicate::eq("test-deployment"),
794 mockall::predicate::eq(None::<InspectContainerOptions>),
795 )
796 .times(1)
797 .returning(|_, _| Ok(create_test_container_inspect_response()));
798
799 let client = Client::new(mock_docker);
800
801 let result = client.create_deployment(options).await;
803
804 assert!(result.is_ok());
806 }
807
808 #[tokio::test]
809 async fn test_create_deployment_timeout() {
810 let mut mock_docker = MockDocker::new();
812 let options = CreateDeploymentOptions {
813 name: Some("test-deployment".to_string()),
814 wait_until_healthy: Some(true),
815 wait_until_healthy_timeout: Some(time::Duration::from_millis(1)),
816 ..Default::default()
817 };
818
819 mock_docker
821 .expect_pull_image()
822 .with(
823 mockall::predicate::eq(ATLAS_LOCAL_IMAGE),
824 mockall::predicate::eq("latest"),
825 )
826 .times(1)
827 .returning(|_, _| Ok(()));
828
829 mock_docker
830 .expect_create_container()
831 .times(1)
832 .returning(|_, _| {
833 Ok(ContainerCreateResponse {
834 id: "container_id".to_string(),
835 warnings: vec![],
836 })
837 });
838
839 mock_docker
840 .expect_start_container()
841 .with(
842 mockall::predicate::eq("test-deployment"),
843 mockall::predicate::eq(None::<StartContainerOptions>),
844 )
845 .times(1)
846 .returning(|_, _| Ok(()));
847
848 mock_docker
850 .expect_inspect_container()
851 .with(
852 mockall::predicate::eq("test-deployment"),
853 mockall::predicate::eq(None::<InspectContainerOptions>),
854 )
855 .returning(|_, _| Ok(create_test_container_inspect_response_starting()));
856
857 let client = Client::new(mock_docker);
858
859 let result = client.create_deployment(options).await;
861
862 assert!(result.is_err());
864 match result.unwrap_err() {
865 CreateDeploymentError::WatchDeployment(WatchDeploymentError::Timeout {
866 deployment_name,
867 }) => {
868 assert_eq!(deployment_name, "test-deployment");
869 }
870 _ => panic!("Expected WatchDeployment Timeout error"),
871 }
872 }
873
874 #[tokio::test]
875 async fn test_wait_for_healthy_deployment_no_state() {
876 let mut mock_docker = MockDocker::new();
878 let options = CreateDeploymentOptions {
879 name: Some("test-deployment".to_string()),
880 ..Default::default()
881 };
882
883 mock_docker
885 .expect_pull_image()
886 .with(
887 mockall::predicate::eq(ATLAS_LOCAL_IMAGE),
888 mockall::predicate::eq("latest"),
889 )
890 .times(1)
891 .returning(|_, _| Ok(()));
892
893 mock_docker
894 .expect_create_container()
895 .times(1)
896 .returning(|_, _| {
897 Ok(ContainerCreateResponse {
898 id: "container_id".to_string(),
899 warnings: vec![],
900 })
901 });
902
903 mock_docker
904 .expect_start_container()
905 .with(
906 mockall::predicate::eq("test-deployment"),
907 mockall::predicate::eq(None::<StartContainerOptions>),
908 )
909 .times(1)
910 .returning(|_, _| Ok(()));
911
912 mock_docker
913 .expect_inspect_container()
914 .with(
915 mockall::predicate::eq("test-deployment"),
916 mockall::predicate::eq(None::<InspectContainerOptions>),
917 )
918 .times(1)
919 .returning(|_, _| Ok(create_test_container_inspect_response_no_state()));
920
921 let client = Client::new(mock_docker);
922
923 let result = client.create_deployment(options).await;
925
926 assert!(result.is_err());
928 match result.unwrap_err() {
929 CreateDeploymentError::WatchDeployment(WatchDeploymentError::UnhealthyDeployment {
930 deployment_name,
931 status,
932 }) => {
933 assert_eq!(deployment_name, "test-deployment");
934 assert_eq!(status, ContainerHealthStatus::None);
935 }
936 _ => panic!("Expected WatchDeployment error"),
937 }
938 }
939
940 #[tokio::test]
941 async fn test_wait_for_healthy_deployment_no_health() {
942 let mut mock_docker = MockDocker::new();
944 let options = CreateDeploymentOptions {
945 name: Some("test-deployment".to_string()),
946 ..Default::default()
947 };
948
949 mock_docker
951 .expect_pull_image()
952 .with(
953 mockall::predicate::eq(ATLAS_LOCAL_IMAGE),
954 mockall::predicate::eq("latest"),
955 )
956 .times(1)
957 .returning(|_, _| Ok(()));
958
959 mock_docker
960 .expect_create_container()
961 .times(1)
962 .returning(|_, _| {
963 Ok(ContainerCreateResponse {
964 id: "container_id".to_string(),
965 warnings: vec![],
966 })
967 });
968
969 mock_docker
970 .expect_start_container()
971 .with(
972 mockall::predicate::eq("test-deployment"),
973 mockall::predicate::eq(None::<StartContainerOptions>),
974 )
975 .times(1)
976 .returning(|_, _| Ok(()));
977
978 mock_docker
979 .expect_inspect_container()
980 .with(
981 mockall::predicate::eq("test-deployment"),
982 mockall::predicate::eq(None::<InspectContainerOptions>),
983 )
984 .times(1)
985 .returning(|_, _| Ok(create_test_container_inspect_response_no_health()));
986
987 let client = Client::new(mock_docker);
988
989 let result = client.create_deployment(options).await;
991
992 assert!(result.is_err());
994 match result.unwrap_err() {
995 CreateDeploymentError::WatchDeployment(WatchDeploymentError::UnhealthyDeployment {
996 deployment_name,
997 status,
998 }) => {
999 assert_eq!(deployment_name, "test-deployment");
1000 assert_eq!(status, ContainerHealthStatus::None);
1001 }
1002 _ => panic!("Expected WatchDeployment error"),
1003 }
1004 }
1005
1006 #[tokio::test]
1007 async fn test_create_deployment_rejects_image_with_tag() {
1008 let mock_docker = MockDocker::new();
1009 let options = CreateDeploymentOptions {
1010 image: Some("mongo:6.0".to_string()),
1011 ..Default::default()
1012 };
1013
1014 let client = Client::new(mock_docker);
1015 let result = client.create_deployment(options).await;
1016
1017 assert!(matches!(
1018 result.unwrap_err(),
1019 CreateDeploymentError::InvalidImage(img) if img == "mongo:6.0"
1020 ));
1021 }
1022
1023 #[tokio::test]
1024 async fn test_create_deployment_rejects_image_with_latest_tag() {
1025 let mock_docker = MockDocker::new();
1026 let options = CreateDeploymentOptions {
1027 image: Some("quay.io/mongodb/mongodb-atlas-local:latest".to_string()),
1028 ..Default::default()
1029 };
1030
1031 let client = Client::new(mock_docker);
1032 let result = client.create_deployment(options).await;
1033
1034 assert!(matches!(
1035 result.unwrap_err(),
1036 CreateDeploymentError::InvalidImage(img) if img == "quay.io/mongodb/mongodb-atlas-local:latest"
1037 ));
1038 }
1039
1040 #[tokio::test]
1041 async fn test_create_deployment_accepts_image_without_tag() {
1042 let mut mock_docker = MockDocker::new();
1043 let options = CreateDeploymentOptions {
1044 name: Some("test-deployment".to_string()),
1045 image: Some("quay.io/mongodb/mongodb-atlas-local".to_string()),
1046 ..Default::default()
1047 };
1048
1049 mock_docker
1050 .expect_pull_image()
1051 .with(
1052 mockall::predicate::eq("quay.io/mongodb/mongodb-atlas-local"),
1053 mockall::predicate::eq("latest"),
1054 )
1055 .times(1)
1056 .returning(|_, _| Ok(()));
1057
1058 mock_docker
1059 .expect_create_container()
1060 .times(1)
1061 .returning(|_, _| {
1062 Ok(ContainerCreateResponse {
1063 id: "container_id".to_string(),
1064 warnings: vec![],
1065 })
1066 });
1067
1068 mock_docker
1069 .expect_start_container()
1070 .with(
1071 mockall::predicate::eq("test-deployment"),
1072 mockall::predicate::eq(None::<StartContainerOptions>),
1073 )
1074 .times(1)
1075 .returning(|_, _| Ok(()));
1076
1077 mock_docker
1078 .expect_inspect_container()
1079 .with(
1080 mockall::predicate::eq("test-deployment"),
1081 mockall::predicate::eq(None::<InspectContainerOptions>),
1082 )
1083 .times(2)
1084 .returning(|_, _| Ok(create_test_container_inspect_response()));
1085
1086 let client = Client::new(mock_docker);
1087 let result = client.create_deployment(options).await;
1088
1089 assert!(result.is_ok());
1090 }
1091
1092 #[tokio::test]
1093 async fn test_wait_for_healthy_deployment_no_health_status() {
1094 let mut mock_docker = MockDocker::new();
1096 let options = CreateDeploymentOptions {
1097 name: Some("test-deployment".to_string()),
1098 ..Default::default()
1099 };
1100
1101 mock_docker
1103 .expect_pull_image()
1104 .with(
1105 mockall::predicate::eq(ATLAS_LOCAL_IMAGE),
1106 mockall::predicate::eq("latest"),
1107 )
1108 .times(1)
1109 .returning(|_, _| Ok(()));
1110
1111 mock_docker
1112 .expect_create_container()
1113 .times(1)
1114 .returning(|_, _| {
1115 Ok(ContainerCreateResponse {
1116 id: "container_id".to_string(),
1117 warnings: vec![],
1118 })
1119 });
1120
1121 mock_docker
1122 .expect_start_container()
1123 .with(
1124 mockall::predicate::eq("test-deployment"),
1125 mockall::predicate::eq(None::<StartContainerOptions>),
1126 )
1127 .times(1)
1128 .returning(|_, _| Ok(()));
1129
1130 mock_docker
1131 .expect_inspect_container()
1132 .with(
1133 mockall::predicate::eq("test-deployment"),
1134 mockall::predicate::eq(None::<InspectContainerOptions>),
1135 )
1136 .times(1)
1137 .returning(|_, _| Ok(create_test_container_inspect_response_no_health_status()));
1138
1139 let client = Client::new(mock_docker);
1140
1141 let result = client.create_deployment(options).await;
1143
1144 assert!(result.is_err());
1146 match result.unwrap_err() {
1147 CreateDeploymentError::WatchDeployment(WatchDeploymentError::UnhealthyDeployment {
1148 deployment_name,
1149 status,
1150 }) => {
1151 assert_eq!(deployment_name, "test-deployment");
1152 assert_eq!(status, ContainerHealthStatus::None);
1153 }
1154 _ => panic!("Expected WatchDeployment error"),
1155 }
1156 }
1157}