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::models::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::docker::DockerError;
98    use crate::{
99        client::Client,
100        docker::{
101            CommandOutput, DockerInspectContainer, RunCommandInContainer,
102            RunCommandInContainerError,
103        },
104        test_utils::{
105            create_container_inspect_response_no_auth, create_container_inspect_response_with_auth,
106        },
107    };
108    use bollard::{
109        models::{
110            ContainerConfig, ContainerInspectResponse, ContainerState, ContainerStateStatusEnum,
111        },
112        query_parameters::InspectContainerOptions,
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, DockerError>;
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(|_, _| Err(DockerError::NotFound));
209
210        let client = Client::new(mock_docker);
211        let container_id_or_name = "nonexistent-deployment".to_string();
212
213        // Act
214        let result = client.get_connection_string(container_id_or_name).await;
215
216        // Assert
217        assert!(result.is_err());
218        assert!(matches!(
219            result.unwrap_err(),
220            GetConnectionStringError::GetDeployment(_)
221        ));
222    }
223
224    #[tokio::test]
225    async fn test_get_connection_string_missing_port_binding() {
226        // Arrange
227        let mut mock_docker = MockDocker::new();
228        let container_inspect_response = ContainerInspectResponse {
229            id: Some("test_container_id".to_string()),
230            name: Some("/test-deployment".to_string()),
231            config: Some(ContainerConfig {
232                labels: Some(hashmap! {
233                    "mongodb-atlas-local".to_string() => "container".to_string(),
234                    "version".to_string() => "7.0.0".to_string(),
235                    "mongodb-type".to_string() => "community".to_string(),
236                }),
237                env: Some(vec!["TOOL=ATLASCLI".to_string()]),
238                ..Default::default()
239            }),
240            state: Some(ContainerState {
241                status: Some(ContainerStateStatusEnum::RUNNING),
242                ..Default::default()
243            }),
244            network_settings: Some(bollard::models::NetworkSettings {
245                ports: Some(hashmap! {}), // No port mappings
246                ..Default::default()
247            }),
248            ..Default::default()
249        };
250
251        // Mock call to get_deployment
252        mock_docker
253            .expect_inspect_container()
254            .with(
255                mockall::predicate::eq("test-deployment"),
256                mockall::predicate::eq(None::<InspectContainerOptions>),
257            )
258            .times(1)
259            .returning(move |_, _| Ok(container_inspect_response.clone()));
260
261        let client = Client::new(mock_docker);
262        let container_id_or_name = "test-deployment".to_string();
263
264        // Act
265        let result = client.get_connection_string(container_id_or_name).await;
266
267        // Assert
268        assert!(result.is_err());
269        assert!(matches!(
270            result.unwrap_err(),
271            GetConnectionStringError::MissingPortBinding
272        ));
273    }
274
275    #[tokio::test]
276    async fn test_get_connection_string_verify_success() {
277        // Arrange
278        let mut mock_docker = MockDocker::new();
279
280        // Mock call to get_deployment
281        mock_docker
282            .expect_inspect_container()
283            .with(
284                mockall::predicate::eq("test-deployment"),
285                mockall::predicate::eq(None::<InspectContainerOptions>),
286            )
287            .times(1)
288            .returning(move |_, _| Ok(create_container_inspect_response_with_auth(27017)));
289
290        let client = Client::new(mock_docker);
291
292        let container_id_or_name = "test-deployment".to_string();
293
294        // Act
295        let result = client.get_connection_string(container_id_or_name).await;
296
297        // Assert
298        assert!(result.is_ok());
299        assert_eq!(
300            result.unwrap(),
301            "mongodb://testuser:testpass@127.0.0.1:27017/?directConnection=true"
302        );
303    }
304}