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 pub name: Option<String>,
25
26 pub image: Option<String>,
28 pub skip_pull_image: Option<bool>,
29 pub image_tag: Option<ImageTag>,
30
31 pub wait_until_healthy: Option<bool>,
33 pub wait_until_healthy_timeout: Option<Duration>,
34 pub creation_source: Option<CreationSource>,
35
36 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 pub mongot_log_file: Option<String>,
48 pub runner_log_file: Option<String>,
49
50 pub do_not_track: Option<bool>,
52 pub telemetry_base_url: Option<String>,
53
54 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 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 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 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 let env = if env_vars.is_empty() {
168 None
169 } else {
170 Some(env_vars)
171 };
172
173 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 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 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 let container_create_body: ContainerCreateBody =
241 ContainerCreateBody::from(&create_deployment_options);
242
243 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 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 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 let create_deployment_options = CreateDeploymentOptions::default();
315
316 let container_create_body: ContainerCreateBody =
318 ContainerCreateBody::from(&create_deployment_options);
319
320 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 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_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 let options: CreateDeploymentOptions = CreateDeploymentOptions::default();
373 let create_container_options: CreateContainerOptions =
374 CreateContainerOptions::from(&options);
375
376 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 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}