Skip to main content

atlas_local/client/
list_deployments.rs

1use bollard::query_parameters::ListContainersOptionsBuilder;
2use maplit::hashmap;
3
4use crate::{
5    client::Client,
6    docker::{DockerInspectContainer, DockerListContainers},
7    models::{Deployment, LOCAL_DEPLOYMENT_LABEL_KEY, LOCAL_DEPLOYMENT_LABEL_VALUE},
8};
9
10use super::GetDeploymentError;
11
12impl<D: DockerListContainers + DockerInspectContainer> Client<D> {
13    /// Lists all local Atlas deployments.
14    pub async fn list_deployments(&self) -> Result<Vec<Deployment>, GetDeploymentError> {
15        // Build the list containers options which will filter for containers with the local deployment label
16        let list_container_options = ListContainersOptionsBuilder::default()
17            .all(true)
18            .filters(&hashmap! {
19                "label" => vec![format!("{}={}", LOCAL_DEPLOYMENT_LABEL_KEY, LOCAL_DEPLOYMENT_LABEL_VALUE)],
20            })
21            .build();
22
23        // Get all the containers using the list containers options
24        let container_summaries = self
25            .docker
26            .list_containers(Some(list_container_options))
27            .await?;
28
29        // Create the output vector used to return the deployments
30        let mut deployments = Vec::with_capacity(container_summaries.len());
31
32        // Iterate over the container summaries and get the deployment details for each container
33        for container_summary in container_summaries {
34            // Get the container ID from the container summary
35            // This should always be present, but it's cleaner to not use unwrap and skip if it's not present
36            if let Some(container_id) = container_summary.id {
37                // Get the deployment details for the container
38                let deployment = self.get_deployment(container_id.as_ref()).await?;
39                deployments.push(deployment);
40            }
41        }
42
43        Ok(deployments)
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use crate::models::{MongodbType, State};
51    use bollard::{
52        errors::Error as BollardError,
53        query_parameters::{InspectContainerOptions, ListContainersOptions},
54        secret::{
55            ContainerConfig, ContainerInspectResponse, ContainerState, ContainerStateStatusEnum,
56            ContainerSummary,
57        },
58    };
59    use mockall::mock;
60    use std::collections::HashMap;
61
62    mock! {
63        Docker {}
64
65        impl DockerListContainers for Docker {
66            async fn list_containers(
67                &self,
68                options: Option<ListContainersOptions>,
69            ) -> Result<Vec<ContainerSummary>, BollardError>;
70        }
71
72        impl DockerInspectContainer for Docker {
73            async fn inspect_container(
74                &self,
75                container_id: &str,
76                options: Option<InspectContainerOptions>,
77            ) -> Result<ContainerInspectResponse, BollardError>;
78        }
79    }
80
81    fn create_container_summary(id: &str, name: &str) -> ContainerSummary {
82        ContainerSummary {
83            id: Some(id.to_string()),
84            names: Some(vec![format!("/{}", name)]),
85            ..Default::default()
86        }
87    }
88
89    fn create_container_inspect_response(id: &str, name: &str) -> ContainerInspectResponse {
90        let mut labels = HashMap::new();
91        labels.insert("mongodb-atlas-local".to_string(), "container".to_string());
92        labels.insert("version".to_string(), "8.0.0".to_string());
93        labels.insert("mongodb-type".to_string(), "community".to_string());
94
95        let env_vars = vec!["TOOL=ATLASCLI".to_string()];
96
97        ContainerInspectResponse {
98            id: Some(id.to_string()),
99            name: Some(format!("/{}", name)),
100            config: Some(ContainerConfig {
101                labels: Some(labels),
102                env: Some(env_vars),
103                ..Default::default()
104            }),
105            state: Some(ContainerState {
106                status: Some(ContainerStateStatusEnum::RUNNING),
107                ..Default::default()
108            }),
109            ..Default::default()
110        }
111    }
112
113    #[tokio::test]
114    async fn test_list_deployments() {
115        // Arrange
116        let mut mock_docker = MockDocker::new();
117
118        let container_summaries = vec![
119            create_container_summary("container1", "deployment1"),
120            create_container_summary("container2", "deployment2"),
121        ];
122
123        let container_inspect_response1 =
124            create_container_inspect_response("container1", "deployment1");
125        let container_inspect_response2 =
126            create_container_inspect_response("container2", "deployment2");
127
128        // Set up expectations
129        mock_docker
130            .expect_list_containers()
131            .times(1)
132            .returning(move |_| Ok(container_summaries.clone()));
133
134        mock_docker
135            .expect_inspect_container()
136            .with(
137                mockall::predicate::eq("container1"),
138                mockall::predicate::eq(None::<InspectContainerOptions>),
139            )
140            .times(1)
141            .returning(move |_, _| Ok(container_inspect_response1.clone()));
142
143        mock_docker
144            .expect_inspect_container()
145            .with(
146                mockall::predicate::eq("container2"),
147                mockall::predicate::eq(None::<InspectContainerOptions>),
148            )
149            .times(1)
150            .returning(move |_, _| Ok(container_inspect_response2.clone()));
151
152        let client = Client::new(mock_docker);
153
154        // Act
155        let result = client.list_deployments().await;
156
157        // Assert
158        assert!(result.is_ok());
159        let deployments = result.unwrap();
160        assert_eq!(deployments.len(), 2);
161
162        assert_eq!(deployments[0].container_id, "container1");
163        assert_eq!(deployments[0].name, Some("deployment1".to_string()));
164        assert_eq!(deployments[0].state, State::Running);
165        assert_eq!(deployments[0].mongodb_type, MongodbType::Community);
166
167        assert_eq!(deployments[1].container_id, "container2");
168        assert_eq!(deployments[1].name, Some("deployment2".to_string()));
169        assert_eq!(deployments[1].state, State::Running);
170        assert_eq!(deployments[1].mongodb_type, MongodbType::Community);
171    }
172
173    #[tokio::test]
174    async fn test_list_deployments_empty() {
175        // Arrange
176        let mut mock_docker = MockDocker::new();
177
178        // Set up expectations
179        mock_docker
180            .expect_list_containers()
181            .times(1)
182            .returning(|_| Ok(vec![]));
183
184        let client = Client::new(mock_docker);
185
186        // Act
187        let result = client.list_deployments().await;
188
189        // Assert
190        assert!(result.is_ok());
191        let deployments = result.unwrap();
192        assert_eq!(deployments.len(), 0);
193    }
194
195    #[tokio::test]
196    async fn test_list_deployments_list_containers_error() {
197        // Arrange
198        let mut mock_docker = MockDocker::new();
199
200        // Set up expectations
201        mock_docker
202            .expect_list_containers()
203            .times(1)
204            .returning(|_| {
205                Err(BollardError::DockerResponseServerError {
206                    status_code: 500,
207                    message: "Internal Server Error".to_string(),
208                })
209            });
210
211        let client = Client::new(mock_docker);
212
213        // Act
214        let result = client.list_deployments().await;
215
216        // Assert
217        assert!(result.is_err());
218        assert!(matches!(
219            result.unwrap_err(),
220            GetDeploymentError::ContainerInspect(_)
221        ));
222    }
223
224    #[tokio::test]
225    async fn test_list_deployments_inspect_container_error() {
226        // Arrange
227        let mut mock_docker = MockDocker::new();
228
229        let container_summaries = vec![create_container_summary("container1", "deployment1")];
230
231        // Set up expectations
232        mock_docker
233            .expect_list_containers()
234            .times(1)
235            .returning(move |_| Ok(container_summaries.clone()));
236
237        mock_docker
238            .expect_inspect_container()
239            .with(
240                mockall::predicate::eq("container1"),
241                mockall::predicate::eq(None::<InspectContainerOptions>),
242            )
243            .times(1)
244            .returning(|_, _| {
245                Err(BollardError::DockerResponseServerError {
246                    status_code: 404,
247                    message: "No such container".to_string(),
248                })
249            });
250
251        let client = Client::new(mock_docker);
252
253        // Act
254        let result = client.list_deployments().await;
255
256        // Assert
257        assert!(result.is_err());
258        assert!(matches!(
259            result.unwrap_err(),
260            GetDeploymentError::ContainerInspect(_)
261        ));
262    }
263
264    #[tokio::test]
265    async fn test_list_deployments_skip_containers_without_id() {
266        // Arrange
267        let mut mock_docker = MockDocker::new();
268
269        let container_summaries = vec![
270            ContainerSummary {
271                id: None, // Container without ID should be skipped
272                ..Default::default()
273            },
274            create_container_summary("container2", "deployment2"),
275        ];
276
277        let container_inspect_response2 =
278            create_container_inspect_response("container2", "deployment2");
279
280        // Set up expectations
281        mock_docker
282            .expect_list_containers()
283            .times(1)
284            .returning(move |_| Ok(container_summaries.clone()));
285
286        mock_docker
287            .expect_inspect_container()
288            .with(
289                mockall::predicate::eq("container2"),
290                mockall::predicate::eq(None::<InspectContainerOptions>),
291            )
292            .times(1)
293            .returning(move |_, _| Ok(container_inspect_response2.clone()));
294
295        let client = Client::new(mock_docker);
296
297        // Act
298        let result = client.list_deployments().await;
299
300        // Assert
301        assert!(result.is_ok());
302        let deployments = result.unwrap();
303        assert_eq!(deployments.len(), 1); // Only one deployment should be returned
304        assert_eq!(deployments[0].container_id, "container2");
305        assert_eq!(deployments[0].name, Some("deployment2".to_string()));
306    }
307}