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