Skip to main content

atlas_local/client/
get_connection_string.rs

1use crate::{
2    client::get_mongodb_secret::get_mongodb_secret,
3    docker::{DockerInspectContainer, RunCommandInContainer, RunCommandInContainerError},
4    models::MongoDBPortBinding,
5};
6use bollard::secret::PortBinding;
7
8use super::GetDeploymentError;
9
10#[derive(Debug, thiserror::Error)]
11pub enum GetConnectionStringError {
12    #[error("Failed to get deployment: {0}")]
13    GetDeployment(#[from] GetDeploymentError),
14    #[error("Failed to get MongoDB username: {0}")]
15    GetMongodbUsername(RunCommandInContainerError),
16    #[error("Failed to get MongoDB password: {0}")]
17    GetMongodbPassword(RunCommandInContainerError),
18    #[error("Missing port binding information")]
19    MissingPortBinding,
20}
21
22impl<D: DockerInspectContainer + RunCommandInContainer> crate::client::Client<D> {
23    // Gets a local Atlas deployment's connection string.
24    pub async fn get_connection_string(
25        &self,
26        container_id_or_name: String,
27    ) -> Result<String, GetConnectionStringError> {
28        // Get deployment
29        let deployment = self.get_deployment(&container_id_or_name).await?;
30
31        // Extract port binding
32        let port = match &deployment.port_bindings {
33            Some(MongoDBPortBinding { port, .. }) => Some(*port),
34            _ => None,
35        };
36        let port = port
37            .flatten()
38            .ok_or(GetConnectionStringError::MissingPortBinding)?;
39
40        let hostname = PortBinding::from(
41            deployment
42                .port_bindings
43                .as_ref()
44                .ok_or(GetConnectionStringError::MissingPortBinding)?,
45        )
46        .host_ip
47        .ok_or(GetConnectionStringError::MissingPortBinding)?;
48
49        // Try to get the MongoDB root username
50        let mongodb_root_username = get_mongodb_secret(
51            self.docker.as_ref(),
52            &deployment,
53            |d| d.mongodb_initdb_root_username.as_deref(),
54            |d| d.mongodb_initdb_root_username_file.as_deref(),
55        )
56        .await
57        .map_err(GetConnectionStringError::GetMongodbUsername)?;
58
59        // Try to get the MongoDB root password
60        let mongodb_root_password = get_mongodb_secret(
61            self.docker.as_ref(),
62            &deployment,
63            |d| d.mongodb_initdb_root_password.as_deref(),
64            |d| d.mongodb_initdb_root_password_file.as_deref(),
65        )
66        .await
67        .map_err(GetConnectionStringError::GetMongodbPassword)?;
68
69        // Construct the connection string
70        let connection_string =
71            format_connection_string(hostname, mongodb_root_username, mongodb_root_password, port);
72
73        Ok(connection_string)
74    }
75}
76
77// format_connection_string creates a MongoDB connection string with format depending on presence of username/password.
78fn format_connection_string(
79    hostname: String,
80    username: Option<String>,
81    password: Option<String>,
82    port: u16,
83) -> String {
84    let auth_string = match (username, password) {
85        (Some(u), Some(p)) if !u.is_empty() && !p.is_empty() => {
86            format!("{u}:{p}@")
87        }
88        _ => "".to_string(),
89    };
90
91    format!("mongodb://{auth_string}{hostname}:{port}/?directConnection=true",)
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::{
98        client::Client,
99        docker::{
100            CommandOutput, DockerInspectContainer, RunCommandInContainer,
101            RunCommandInContainerError,
102        },
103        test_utils::{
104            create_container_inspect_response_no_auth, create_container_inspect_response_with_auth,
105        },
106    };
107    use bollard::{
108        errors::Error as BollardError,
109        query_parameters::InspectContainerOptions,
110        secret::{
111            ContainerConfig, ContainerInspectResponse, ContainerState, ContainerStateStatusEnum,
112        },
113    };
114    use maplit::hashmap;
115    use mockall::mock;
116
117    mock! {
118        Docker {}
119
120        impl DockerInspectContainer for Docker {
121            async fn inspect_container(
122                &self,
123                container_id: &str,
124                options: Option<InspectContainerOptions>,
125            ) -> Result<ContainerInspectResponse, BollardError>;
126        }
127
128        impl RunCommandInContainer for Docker {
129            async fn run_command_in_container(
130                &self,
131                container_id: &str,
132                command: Vec<String>,
133            ) -> Result<CommandOutput, RunCommandInContainerError>;
134        }
135    }
136
137    #[tokio::test]
138    async fn test_get_connection_string() {
139        // Arrange
140        let mut mock_docker = MockDocker::new();
141
142        // Mock call to get_deployment
143        mock_docker
144            .expect_inspect_container()
145            .with(
146                mockall::predicate::eq("test-deployment"),
147                mockall::predicate::eq(None::<InspectContainerOptions>),
148            )
149            .times(1)
150            .returning(move |_, _| Ok(create_container_inspect_response_with_auth(27017)));
151
152        let client = Client::new(mock_docker);
153        let container_id_or_name = "test-deployment".to_string();
154
155        // Act
156        let result = client.get_connection_string(container_id_or_name).await;
157
158        // Assert
159        assert!(result.is_ok());
160        assert_eq!(
161            result.unwrap(),
162            "mongodb://testuser:testpass@127.0.0.1:27017/?directConnection=true"
163        );
164    }
165
166    #[tokio::test]
167    async fn test_get_connection_string_no_auth() {
168        // Arrange
169        let mut mock_docker = MockDocker::new();
170
171        // Mock call to get_deployment
172        mock_docker
173            .expect_inspect_container()
174            .with(
175                mockall::predicate::eq("test-deployment"),
176                mockall::predicate::eq(None::<InspectContainerOptions>),
177            )
178            .times(1)
179            .returning(move |_, _| Ok(create_container_inspect_response_no_auth(27017)));
180
181        let client = Client::new(mock_docker);
182        let container_id_or_name = "test-deployment".to_string();
183
184        // Act
185        let result = client.get_connection_string(container_id_or_name).await;
186
187        // Assert
188        assert!(result.is_ok());
189        assert_eq!(
190            result.unwrap(),
191            "mongodb://127.0.0.1:27017/?directConnection=true"
192        );
193    }
194
195    #[tokio::test]
196    async fn test_get_connection_string_get_deployment_error() {
197        // Arrange
198        let mut mock_docker = MockDocker::new();
199
200        // Mock call to get_deployment
201        mock_docker
202            .expect_inspect_container()
203            .with(
204                mockall::predicate::eq("nonexistent-deployment"),
205                mockall::predicate::eq(None::<InspectContainerOptions>),
206            )
207            .times(1)
208            .returning(|_, _| {
209                Err(BollardError::DockerResponseServerError {
210                    status_code: 404,
211                    message: "No such container".to_string(),
212                })
213            });
214
215        let client = Client::new(mock_docker);
216        let container_id_or_name = "nonexistent-deployment".to_string();
217
218        // Act
219        let result = client.get_connection_string(container_id_or_name).await;
220
221        // Assert
222        assert!(result.is_err());
223        assert!(matches!(
224            result.unwrap_err(),
225            GetConnectionStringError::GetDeployment(_)
226        ));
227    }
228
229    #[tokio::test]
230    async fn test_get_connection_string_missing_port_binding() {
231        // Arrange
232        let mut mock_docker = MockDocker::new();
233        let container_inspect_response = ContainerInspectResponse {
234            id: Some("test_container_id".to_string()),
235            name: Some("/test-deployment".to_string()),
236            config: Some(ContainerConfig {
237                labels: Some(hashmap! {
238                    "mongodb-atlas-local".to_string() => "container".to_string(),
239                    "version".to_string() => "7.0.0".to_string(),
240                    "mongodb-type".to_string() => "community".to_string(),
241                }),
242                env: Some(vec!["TOOL=ATLASCLI".to_string()]),
243                ..Default::default()
244            }),
245            state: Some(ContainerState {
246                status: Some(ContainerStateStatusEnum::RUNNING),
247                ..Default::default()
248            }),
249            network_settings: Some(bollard::secret::NetworkSettings {
250                ports: Some(hashmap! {}), // No port mappings
251                ..Default::default()
252            }),
253            ..Default::default()
254        };
255
256        // Mock call to get_deployment
257        mock_docker
258            .expect_inspect_container()
259            .with(
260                mockall::predicate::eq("test-deployment"),
261                mockall::predicate::eq(None::<InspectContainerOptions>),
262            )
263            .times(1)
264            .returning(move |_, _| Ok(container_inspect_response.clone()));
265
266        let client = Client::new(mock_docker);
267        let container_id_or_name = "test-deployment".to_string();
268
269        // Act
270        let result = client.get_connection_string(container_id_or_name).await;
271
272        // Assert
273        assert!(result.is_err());
274        assert!(matches!(
275            result.unwrap_err(),
276            GetConnectionStringError::MissingPortBinding
277        ));
278    }
279
280    #[tokio::test]
281    async fn test_get_connection_string_verify_success() {
282        // Arrange
283        let mut mock_docker = MockDocker::new();
284
285        // Mock call to get_deployment
286        mock_docker
287            .expect_inspect_container()
288            .with(
289                mockall::predicate::eq("test-deployment"),
290                mockall::predicate::eq(None::<InspectContainerOptions>),
291            )
292            .times(1)
293            .returning(move |_, _| Ok(create_container_inspect_response_with_auth(27017)));
294
295        let client = Client::new(mock_docker);
296
297        let container_id_or_name = "test-deployment".to_string();
298
299        // Act
300        let result = client.get_connection_string(container_id_or_name).await;
301
302        // Assert
303        assert!(result.is_ok());
304        assert_eq!(
305            result.unwrap(),
306            "mongodb://testuser:testpass@127.0.0.1:27017/?directConnection=true"
307        );
308    }
309}