Skip to main content

atlas_local/client/create_deployment/
mod.rs

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    /// Creates a local Atlas deployment.
55    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        // Spawn the deployment creation in a background task.
63        // Errors from `create_deployment_inner` are forwarded to the receiver via the progress channel.
64        // This code cannot panic: the crate denies unwrap/expect/panic usage (see lib.rs),
65        // and any errors from `create_deployment_inner` are captured in the `Result` and sent
66        // to the receiver through `progress.finalize_deployment()`.
67        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            // Forward the result (success or error) to the receiver via the channel.
75            // The caller can await the returned `CreateDeploymentProgress` to receive this result.
76            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        // Pull the image for Atlas Local if requested
88        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        // Create the container with the correct configuration
115        let create_container_options: CreateContainerOptions = (&deployment_options).into();
116        let create_container_config: ContainerCreateBody = (&deployment_options).into();
117
118        // Get the cluster name
119        // It is safe to unwrap because CreateContainerOptions::from will generate a random name if none is provided
120        #[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        // Start the Atlas Local container
141        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        // Default to waiting for the deployment to be healthy
151        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        // Return the deployment details
170        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        // Arrange
365        let mut mock_docker = MockDocker::new();
366        let options = CreateDeploymentOptions {
367            name: Some("test-deployment".to_string()),
368            ..Default::default()
369        };
370
371        // Set up expectations
372        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        // Act
412        let result = client.create_deployment(options).await;
413
414        // Assert
415        assert!(result.is_ok());
416    }
417
418    #[tokio::test]
419    async fn test_create_deployment_pulls_preview_tag_when_image_tag_preview() {
420        // Arrange
421        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        // Set up expectations - pull_image should be called with preview tag
429        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        // Act
469        let result = client.create_deployment(options).await;
470
471        // Assert
472        assert!(result.is_ok());
473    }
474
475    #[tokio::test]
476    async fn test_create_deployment_pull_image_error() {
477        // Arrange
478        let mut mock_docker = MockDocker::new();
479        let options = CreateDeploymentOptions {
480            name: Some("test-deployment".to_string()),
481            ..Default::default()
482        };
483
484        // Set up expectations
485        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        // Act
495        let result = client.create_deployment(options).await;
496
497        // Assert
498        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        // Arrange
508        let mut mock_docker = MockDocker::new();
509        let options = CreateDeploymentOptions {
510            name: Some("test-deployment".to_string()),
511            ..Default::default()
512        };
513
514        // Set up expectations
515        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        // Act
533        let result = client.create_deployment(options).await;
534
535        // Assert
536        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        // Arrange
548        let mut mock_docker = MockDocker::new();
549        let options = CreateDeploymentOptions {
550            name: Some("test-deployment".to_string()),
551            ..Default::default()
552        };
553
554        // Set up expectations
555        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        // Act
573        let result = client.create_deployment(options).await;
574
575        // Assert
576        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        // Arrange
586        let mut mock_docker = MockDocker::new();
587        let options = CreateDeploymentOptions {
588            name: Some("test-deployment".to_string()),
589            ..Default::default()
590        };
591
592        // Set up expectations
593        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        // Act
621        let result = client.create_deployment(options).await;
622
623        // Assert
624        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        // Arrange
634        let mut mock_docker = MockDocker::new();
635        let options = CreateDeploymentOptions {
636            name: Some("test-deployment".to_string()),
637            ..Default::default()
638        };
639
640        // Set up expectations
641        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        // Act
681        let result = client.create_deployment(options).await;
682
683        // Assert
684        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        // Arrange
696        let mut mock_docker = MockDocker::new();
697        let options = CreateDeploymentOptions {
698            name: Some("test-deployment".to_string()),
699            ..Default::default()
700        };
701
702        // Set up expectations
703        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        // Act
752        let result = client.create_deployment(options).await;
753
754        // Assert
755        assert!(result.is_ok());
756    }
757
758    #[tokio::test]
759    async fn test_wait_for_healthy_deployment_disabled() {
760        // Arrange
761        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        // Set up expectations
769        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        // Act
809        let result = client.create_deployment(options).await;
810
811        // Assert
812        assert!(result.is_ok());
813    }
814
815    #[tokio::test]
816    async fn test_create_deployment_timeout() {
817        // Arrange
818        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        // Set up expectations
827        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 inspect_container to always return STARTING status, which will cause timeout
856        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        // Act
867        let result = client.create_deployment(options).await;
868
869        // Assert
870        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        // Arrange
884        let mut mock_docker = MockDocker::new();
885        let options = CreateDeploymentOptions {
886            name: Some("test-deployment".to_string()),
887            ..Default::default()
888        };
889
890        // Set up expectations
891        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        // Act
931        let result = client.create_deployment(options).await;
932
933        // Assert
934        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        // Arrange
950        let mut mock_docker = MockDocker::new();
951        let options = CreateDeploymentOptions {
952            name: Some("test-deployment".to_string()),
953            ..Default::default()
954        };
955
956        // Set up expectations
957        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        // Act
997        let result = client.create_deployment(options).await;
998
999        // Assert
1000        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        // Arrange
1016        let mut mock_docker = MockDocker::new();
1017        let options = CreateDeploymentOptions {
1018            name: Some("test-deployment".to_string()),
1019            ..Default::default()
1020        };
1021
1022        // Set up expectations
1023        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        // Act
1063        let result = client.create_deployment(options).await;
1064
1065        // Assert
1066        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}