Skip to main content

atlas_local/models/
create_deployment_options.rs

1use bollard::{
2    query_parameters::CreateContainerOptions,
3    secret::{ContainerCreateBody, HostConfig, PortBinding},
4};
5use maplit::hashmap;
6use rand::RngExt;
7use std::{time::Duration, vec};
8
9use crate::models::{
10    CreationSource, ENV_VAR_DO_NOT_TRACK, ENV_VAR_MONGODB_INITDB_DATABASE,
11    ENV_VAR_MONGODB_INITDB_ROOT_PASSWORD, ENV_VAR_MONGODB_INITDB_ROOT_PASSWORD_FILE,
12    ENV_VAR_MONGODB_INITDB_ROOT_USERNAME, ENV_VAR_MONGODB_INITDB_ROOT_USERNAME_FILE,
13    ENV_VAR_MONGODB_LOAD_SAMPLE_DATA, ENV_VAR_MONGOT_LOG_FILE, ENV_VAR_RUNNER_LOG_FILE,
14    ENV_VAR_TELEMETRY_BASE_URL, ENV_VAR_TOOL, ENV_VAR_VOYAGE_API_KEY, ImageTag,
15    LOCAL_DEPLOYMENT_LABEL_KEY, LOCAL_DEPLOYMENT_LABEL_VALUE,
16};
17use crate::models::{MongoDBPortBinding, deployment::LOCAL_SEED_LOCATION};
18pub const ATLAS_LOCAL_IMAGE: &str = "quay.io/mongodb/mongodb-atlas-local";
19
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21#[derive(Clone, Debug, Default, PartialEq, Eq)]
22pub struct CreateDeploymentOptions {
23    // Identifiers
24    pub name: Option<String>,
25
26    // Image details
27    pub image: Option<String>,
28    pub skip_pull_image: Option<bool>,
29    pub image_tag: Option<ImageTag>,
30
31    // Creation Options
32    pub wait_until_healthy: Option<bool>,
33    pub wait_until_healthy_timeout: Option<Duration>,
34    pub creation_source: Option<CreationSource>,
35
36    // Initial database configuration
37    pub local_seed_location: Option<String>,
38    pub mongodb_initdb_database: Option<String>,
39    pub mongodb_initdb_root_password_file: Option<String>,
40    pub mongodb_initdb_root_password: Option<String>,
41    pub mongodb_initdb_root_username_file: Option<String>,
42    pub mongodb_initdb_root_username: Option<String>,
43    pub voyage_api_key: Option<String>,
44    pub load_sample_data: Option<bool>,
45
46    // Logging
47    pub mongot_log_file: Option<String>,
48    pub runner_log_file: Option<String>,
49
50    // Telemetry
51    pub do_not_track: Option<bool>,
52    pub telemetry_base_url: Option<String>,
53
54    // Port configuration
55    pub mongodb_port_binding: Option<MongoDBPortBinding>,
56}
57
58impl From<&CreateDeploymentOptions> for CreateContainerOptions {
59    fn from(deployment_options: &CreateDeploymentOptions) -> Self {
60        let name = deployment_options
61            .name
62            .clone()
63            .unwrap_or_else(|| format!("local{}", rand::rng().random_range(0..10000)));
64
65        CreateContainerOptions {
66            name: Some(name),
67            ..Default::default()
68        }
69    }
70}
71
72impl From<&CreateDeploymentOptions> for ContainerCreateBody {
73    fn from(deployment_options: &CreateDeploymentOptions) -> Self {
74        // Get the port bindings if available, otherwise default to binding a random avaiable port on 127.0.0.1
75        let port_binding = deployment_options
76            .mongodb_port_binding
77            .as_ref()
78            .map(PortBinding::from)
79            .unwrap_or_else(|| PortBinding {
80                host_ip: Some("127.0.0.1".to_string()),
81                host_port: None,
82            });
83
84        let port_bindings_map = Some(hashmap! {
85            "27017/tcp".to_string() => Some(vec![port_binding])
86        });
87
88        // Set up volume bindings if a local seed location is provided
89        let volume_bindings_map =
90            deployment_options
91                .local_seed_location
92                .as_ref()
93                .map(|local_seed_location| {
94                    vec![format!("{local_seed_location}:{LOCAL_SEED_LOCATION}:rw")]
95                });
96
97        // Set environment variables if they are provided in the deployment options
98        let mut env_vars = [
99            (
100                ENV_VAR_RUNNER_LOG_FILE,
101                deployment_options.runner_log_file.as_ref(),
102            ),
103            (
104                ENV_VAR_MONGODB_INITDB_ROOT_USERNAME,
105                deployment_options.mongodb_initdb_root_username.as_ref(),
106            ),
107            (
108                ENV_VAR_MONGODB_INITDB_ROOT_USERNAME_FILE,
109                deployment_options
110                    .mongodb_initdb_root_username_file
111                    .as_ref(),
112            ),
113            (
114                ENV_VAR_MONGODB_INITDB_ROOT_PASSWORD,
115                deployment_options.mongodb_initdb_root_password.as_ref(),
116            ),
117            (
118                ENV_VAR_MONGODB_INITDB_ROOT_PASSWORD_FILE,
119                deployment_options
120                    .mongodb_initdb_root_password_file
121                    .as_ref(),
122            ),
123            (
124                ENV_VAR_MONGODB_INITDB_DATABASE,
125                deployment_options.mongodb_initdb_database.as_ref(),
126            ),
127            (
128                ENV_VAR_MONGODB_LOAD_SAMPLE_DATA,
129                deployment_options
130                    .load_sample_data
131                    .as_ref()
132                    .map(|b| b.to_string())
133                    .as_ref(),
134            ),
135            (
136                ENV_VAR_VOYAGE_API_KEY,
137                deployment_options.voyage_api_key.as_ref(),
138            ),
139            (
140                ENV_VAR_MONGOT_LOG_FILE,
141                deployment_options.mongot_log_file.as_ref(),
142            ),
143            (
144                ENV_VAR_DO_NOT_TRACK,
145                deployment_options
146                    .do_not_track
147                    .as_ref()
148                    .map(|b| b.to_string())
149                    .as_ref(),
150            ),
151            (
152                ENV_VAR_TELEMETRY_BASE_URL,
153                deployment_options.telemetry_base_url.as_ref(),
154            ),
155        ]
156        .into_iter()
157        .filter_map(|(env_key, value_opt)| {
158            value_opt.map(|env_value| format!("{env_key}={env_value}"))
159        })
160        .collect::<Vec<String>>();
161
162        if let Some(source) = deployment_options.creation_source.as_ref() {
163            env_vars.push(format!("{ENV_VAR_TOOL}={source}"));
164        }
165
166        // Only set env if we have any to set, otherwise leave it as None
167        let env = if env_vars.is_empty() {
168            None
169        } else {
170            Some(env_vars)
171        };
172
173        // Get the image and tag
174        let image_string = deployment_options
175            .image
176            .clone()
177            .unwrap_or(ATLAS_LOCAL_IMAGE.to_string());
178
179        let tag = deployment_options
180            .image_tag
181            .as_ref()
182            .map(ToString::to_string)
183            .unwrap_or_else(|| "latest".to_string());
184
185        let image = Some(format!("{image_string}:{tag}"));
186
187        // Get labels
188        let labels = Some(hashmap! {
189            LOCAL_DEPLOYMENT_LABEL_KEY.to_string() => LOCAL_DEPLOYMENT_LABEL_VALUE.to_string(),
190        });
191
192        ContainerCreateBody {
193            image,
194            labels,
195            env,
196            host_config: Some(HostConfig {
197                port_bindings: port_bindings_map,
198                binds: volume_bindings_map,
199                ..Default::default()
200            }),
201            ..Default::default()
202        }
203    }
204}
205
206#[cfg(test)]
207mod tests {
208
209    use crate::models::BindingType;
210
211    use super::*;
212
213    #[test]
214    fn test_into_container_create_body_full() {
215        // Create a full CreateDeploymentOptions with all fields set
216        let create_deployment_options = CreateDeploymentOptions {
217            name: Some("deployment_name".to_string()),
218            image: Some(ATLAS_LOCAL_IMAGE.to_string()),
219            skip_pull_image: Some(false),
220            image_tag: Some(ImageTag::Latest),
221            wait_until_healthy: Some(true),
222            wait_until_healthy_timeout: Some(Duration::from_secs(60)),
223            creation_source: Some(CreationSource::Container),
224            local_seed_location: Some("/host/seed-data".to_string()),
225            mongodb_initdb_database: Some("testdb".to_string()),
226            mongodb_initdb_root_password_file: Some("/run/secrets/password".to_string()),
227            mongodb_initdb_root_password: Some("password123".to_string()),
228            mongodb_initdb_root_username_file: Some("/run/secrets/username".to_string()),
229            mongodb_initdb_root_username: Some("admin".to_string()),
230            voyage_api_key: Some("voyage-api-key".to_string()),
231            load_sample_data: Some(true),
232            mongot_log_file: Some("/tmp/mongot.log".to_string()),
233            runner_log_file: Some("/tmp/runner.log".to_string()),
234            do_not_track: Some(false),
235            telemetry_base_url: Some("https://telemetry.example.com".to_string()),
236            mongodb_port_binding: Some(MongoDBPortBinding::new(Some(50000), BindingType::Loopback)),
237        };
238
239        // Convert to ContainerCreateBody
240        let container_create_body: ContainerCreateBody =
241            ContainerCreateBody::from(&create_deployment_options);
242
243        // Assert all fields are set correctly
244        assert_eq!(
245            container_create_body.image,
246            Some("quay.io/mongodb/mongodb-atlas-local:latest".to_string())
247        );
248        assert_eq!(
249            container_create_body
250                .labels
251                .unwrap()
252                .get(LOCAL_DEPLOYMENT_LABEL_KEY),
253            Some(&LOCAL_DEPLOYMENT_LABEL_VALUE.to_string())
254        );
255
256        // Check Creation Options
257        assert_eq!(create_deployment_options.wait_until_healthy, Some(true));
258        assert_eq!(
259            create_deployment_options.creation_source,
260            Some(CreationSource::Container)
261        );
262
263        // Check environment variables
264        let env_vars = container_create_body.env.unwrap();
265        assert!(env_vars.contains(&format!("{}=CONTAINER", ENV_VAR_TOOL)));
266        assert!(env_vars.contains(&format!("{}=/tmp/runner.log", ENV_VAR_RUNNER_LOG_FILE)));
267        assert!(env_vars.contains(&format!("{}=admin", ENV_VAR_MONGODB_INITDB_ROOT_USERNAME)));
268        assert!(env_vars.contains(&format!(
269            "{}=/run/secrets/username",
270            ENV_VAR_MONGODB_INITDB_ROOT_USERNAME_FILE
271        )));
272        assert!(env_vars.contains(&format!(
273            "{}=password123",
274            ENV_VAR_MONGODB_INITDB_ROOT_PASSWORD
275        )));
276        assert!(env_vars.contains(&format!(
277            "{}=/run/secrets/password",
278            ENV_VAR_MONGODB_INITDB_ROOT_PASSWORD_FILE
279        )));
280        assert!(env_vars.contains(&format!("{}=testdb", ENV_VAR_MONGODB_INITDB_DATABASE)));
281        assert!(env_vars.contains(&format!("{}=true", ENV_VAR_MONGODB_LOAD_SAMPLE_DATA)));
282        assert!(env_vars.contains(&format!("{}=/tmp/mongot.log", ENV_VAR_MONGOT_LOG_FILE)));
283        assert!(env_vars.contains(&format!("{}=false", ENV_VAR_DO_NOT_TRACK)));
284        assert!(env_vars.contains(&format!(
285            "{}=https://telemetry.example.com",
286            ENV_VAR_TELEMETRY_BASE_URL
287        )));
288        assert!(env_vars.contains(&format!("{}=voyage-api-key", ENV_VAR_VOYAGE_API_KEY)));
289        assert_eq!(env_vars.len(), 12);
290
291        let host_config = container_create_body.host_config.unwrap();
292        let port_bindings = host_config.port_bindings.unwrap();
293        let port_binding = port_bindings
294            .get("27017/tcp")
295            .unwrap()
296            .as_ref()
297            .unwrap()
298            .first()
299            .unwrap();
300        assert_eq!(port_binding.host_ip, Some("127.0.0.1".to_string()));
301        assert_eq!(port_binding.host_port, Some("50000".to_string()));
302
303        let volumn_binds = host_config.binds.unwrap();
304        assert_eq!(volumn_binds.len(), 1);
305        assert_eq!(
306            volumn_binds[0],
307            format!("/host/seed-data:{}:rw", LOCAL_SEED_LOCATION)
308        );
309    }
310
311    #[test]
312    fn test_into_container_create_body_minimal() {
313        // Create a minimal CreateDeploymentOptions with only required fields set through defaults
314        let create_deployment_options = CreateDeploymentOptions::default();
315
316        // Convert to ContainerCreateBody
317        let container_create_body: ContainerCreateBody =
318            ContainerCreateBody::from(&create_deployment_options);
319
320        // Assert default fields are set correctly and optional fields are None
321        assert_eq!(
322            container_create_body.image,
323            Some(format!("{ATLAS_LOCAL_IMAGE}:latest"))
324        );
325
326        assert!(container_create_body.env.is_none());
327
328        let host_config = container_create_body.host_config.unwrap();
329        let port_bindings = host_config.port_bindings.unwrap();
330        let port_binding = port_bindings
331            .get("27017/tcp")
332            .unwrap()
333            .as_ref()
334            .unwrap()
335            .first()
336            .unwrap();
337
338        assert_eq!(port_binding.host_ip, Some("127.0.0.1".to_string()));
339        assert!(port_binding.host_port.is_none());
340
341        assert_eq!(
342            container_create_body
343                .labels
344                .unwrap()
345                .get(LOCAL_DEPLOYMENT_LABEL_KEY),
346            Some(&LOCAL_DEPLOYMENT_LABEL_VALUE.to_string())
347        );
348        assert!(container_create_body.exposed_ports.is_none());
349    }
350
351    #[test]
352    fn test_into_create_container_options_minimal() {
353        // Create a minimal CreateDeploymentOptions with only name set
354        let create_deployment_options = CreateDeploymentOptions {
355            name: Some("deployment_name".to_string()),
356            ..Default::default()
357        };
358
359        let create_container_options: CreateContainerOptions =
360            CreateContainerOptions::from(&create_deployment_options);
361
362        // Assert the name is set correctly
363        assert_eq!(
364            create_container_options.name,
365            Some("deployment_name".to_string())
366        );
367    }
368
369    #[test]
370    fn test_into_create_container_options_default() {
371        // Create a default CreateDeploymentOptions
372        let options: CreateDeploymentOptions = CreateDeploymentOptions::default();
373        let create_container_options: CreateContainerOptions =
374            CreateContainerOptions::from(&options);
375
376        // Name should start with "local" followed by random numbers
377        assert!(create_container_options.name.unwrap().starts_with("local"));
378    }
379
380    #[test]
381    fn test_create_deployment_options_default() {
382        let options = CreateDeploymentOptions::default();
383
384        // All fields should be None by default
385        assert!(options.name.is_none());
386        assert!(options.image.is_none());
387        assert!(options.image_tag.is_none());
388        assert!(options.wait_until_healthy.is_none());
389        assert!(options.wait_until_healthy_timeout.is_none());
390        assert!(options.creation_source.is_none());
391        assert!(options.local_seed_location.is_none());
392        assert!(options.mongodb_initdb_database.is_none());
393        assert!(options.mongodb_initdb_root_password_file.is_none());
394        assert!(options.mongodb_initdb_root_password.is_none());
395        assert!(options.mongodb_initdb_root_username_file.is_none());
396        assert!(options.mongodb_initdb_root_username.is_none());
397        assert!(options.voyage_api_key.is_none());
398        assert!(options.load_sample_data.is_none());
399        assert!(options.mongot_log_file.is_none());
400        assert!(options.runner_log_file.is_none());
401        assert!(options.do_not_track.is_none());
402        assert!(options.telemetry_base_url.is_none());
403        assert!(options.mongodb_port_binding.is_none());
404    }
405
406    #[test]
407    fn test_into_container_create_body_preview_tag() {
408        let create_deployment_options = CreateDeploymentOptions {
409            image_tag: Some(ImageTag::Preview),
410            ..Default::default()
411        };
412
413        let container_create_body: ContainerCreateBody =
414            ContainerCreateBody::from(&create_deployment_options);
415
416        assert_eq!(
417            container_create_body.image,
418            Some(format!("{ATLAS_LOCAL_IMAGE}:preview"))
419        );
420    }
421}