1use bollard::secret::ContainerInspectResponse;
2use semver::Version;
3
4use crate::models::{
5 CreationSource, EnvironmentVariables, GetLocalDeploymentLabelsError,
6 GetMongoDBPortBindingError, GetStateError, LocalDeploymentLabels, MongoDBPortBinding,
7 MongodbType, State,
8};
9
10pub const LOCAL_SEED_LOCATION: &str = "/docker-entrypoint-initdb.d";
11
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct Deployment {
15 pub container_id: String,
17 pub name: Option<String>,
18
19 pub state: State,
21 pub port_bindings: Option<MongoDBPortBinding>,
22
23 pub mongodb_type: MongodbType,
25 pub mongodb_version: Version,
26
27 pub creation_source: Option<CreationSource>,
29
30 pub local_seed_location: Option<String>,
32 pub mongodb_initdb_database: Option<String>,
33 pub mongodb_initdb_root_password_file: Option<String>,
34 pub mongodb_initdb_root_password: Option<String>,
35 pub mongodb_initdb_root_username_file: Option<String>,
36 pub mongodb_initdb_root_username: Option<String>,
37 pub mongodb_load_sample_data: Option<bool>,
38 pub voyage_api_key: Option<String>,
39
40 pub mongot_log_file: Option<String>,
42 pub runner_log_file: Option<String>,
43
44 pub do_not_track: bool,
46 pub telemetry_base_url: Option<String>,
47}
48
49#[derive(Debug, thiserror::Error, PartialEq)]
50pub enum IntoDeploymentError {
51 #[error("Container ID is missing")]
52 MissingContainerID,
53 #[error(transparent)]
54 LocalDeploymentLabels(#[from] GetLocalDeploymentLabelsError),
55 #[error(transparent)]
56 MongoDBPortBinding(#[from] GetMongoDBPortBindingError),
57 #[error(transparent)]
58 State(#[from] GetStateError),
59}
60
61impl TryFrom<ContainerInspectResponse> for Deployment {
62 type Error = IntoDeploymentError;
63
64 fn try_from(value: ContainerInspectResponse) -> Result<Self, Self::Error> {
65 let container_id = value
67 .id
68 .as_ref()
69 .ok_or(IntoDeploymentError::MissingContainerID)?
70 .clone();
71
72 let name = value
75 .name
76 .as_ref()
77 .and_then(|n| n.strip_prefix('/'))
78 .map(|n| n.to_string());
79
80 let container_labels = LocalDeploymentLabels::try_from(&value)?;
82 let container_environment_variables = EnvironmentVariables::from(&value);
83 let local_seed_location = extract_local_seed_location(&value);
84 let port_bindings = MongoDBPortBinding::try_from(&value)?;
85 let state = State::try_from(&value)?;
86
87 let LocalDeploymentLabels {
89 mongodb_version,
90 mongodb_type,
91 } = container_labels;
92
93 let EnvironmentVariables {
94 tool,
95 runner_log_file,
96 mongodb_initdb_root_username,
97 mongodb_initdb_root_username_file,
98 mongodb_initdb_root_password,
99 mongodb_initdb_root_password_file,
100 mongodb_initdb_database,
101 mongodb_load_sample_data,
102 mongot_log_file,
103 do_not_track,
104 telemetry_base_url,
105 voyage_api_key,
106 } = container_environment_variables;
107
108 Ok(Self {
109 name,
111 container_id,
112
113 state,
115 port_bindings,
116
117 mongodb_type,
119 mongodb_version,
120
121 creation_source: tool,
123
124 local_seed_location,
126 mongodb_initdb_database,
127 mongodb_initdb_root_password_file,
128 mongodb_initdb_root_password,
129 mongodb_initdb_root_username_file,
130 mongodb_initdb_root_username,
131 mongodb_load_sample_data: mongodb_load_sample_data.map(is_seeding_true),
132 voyage_api_key,
133
134 mongot_log_file,
136 runner_log_file,
137
138 do_not_track: do_not_track.is_some(),
141 telemetry_base_url,
142 })
143 }
144}
145
146fn extract_local_seed_location(
147 container_inspect_response: &ContainerInspectResponse,
148) -> Option<String> {
149 let mount = container_inspect_response
151 .mounts
152 .as_ref()?
153 .iter()
154 .find(|m| m.destination.as_deref() == Some(LOCAL_SEED_LOCATION))?;
155
156 mount.source.clone()
158}
159
160fn is_seeding_true(value: impl AsRef<str>) -> bool {
162 let value = value.as_ref();
163 if let Ok(value) = value.to_ascii_lowercase().parse::<bool>() {
165 return value;
166 }
167
168 if let Ok(value) = value.parse::<i32>() {
170 return value > 0;
171 }
172
173 false
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use bollard::secret::{
180 ContainerConfig, ContainerState, ContainerStateStatusEnum, MountPoint, NetworkSettings,
181 PortBinding,
182 };
183 use std::collections::HashMap;
184
185 #[test]
186 fn test_into_deployment() {
187 let mut labels = HashMap::new();
189 labels.insert("mongodb-atlas-local".to_string(), "container".to_string());
190 labels.insert("version".to_string(), "8.0.0".to_string());
191 labels.insert("mongodb-type".to_string(), "community".to_string());
192
193 let env_vars = vec![
195 "TOOL=ATLASCLI".to_string(),
196 "MONGODB_INITDB_ROOT_USERNAME=admin".to_string(),
197 "MONGODB_INITDB_ROOT_USERNAME_FILE=/run/secrets/username".to_string(),
198 "MONGODB_INITDB_ROOT_PASSWORD=password123".to_string(),
199 "MONGODB_INITDB_ROOT_PASSWORD_FILE=/run/secrets/password".to_string(),
200 "MONGODB_INITDB_DATABASE=testdb".to_string(),
201 "RUNNER_LOG_FILE=/tmp/runner.log".to_string(),
202 "MONGOT_LOG_FILE=/tmp/mongot.log".to_string(),
203 "TELEMETRY_BASE_URL=https://telemetry.example.com".to_string(),
204 "MONGODB_LOAD_SAMPLE_DATA=true".to_string(),
205 "VOYAGE_API_KEY=voyage-api-key".to_string(),
206 ];
207
208 let mount = MountPoint {
210 destination: Some("/docker-entrypoint-initdb.d".to_string()),
211 source: Some("/host/seed-data".to_string()),
212 ..Default::default()
213 };
214
215 let container_state = ContainerState {
217 status: Some(ContainerStateStatusEnum::RUNNING),
218 ..Default::default()
219 };
220
221 let port_binding = PortBinding {
223 host_ip: Some("127.0.0.1".to_string()),
224 host_port: Some("27017".to_string()),
225 };
226 let mut port_map = HashMap::new();
227 port_map.insert("27017/tcp".to_string(), Some(vec![port_binding]));
228 let network_settings = NetworkSettings {
229 ports: Some(port_map),
230 ..Default::default()
231 };
232
233 let container_inspect_response = ContainerInspectResponse {
234 id: Some("container_id".to_string()),
235 name: Some("/test-deployment".to_string()),
236 config: Some(ContainerConfig {
237 env: Some(env_vars),
238 labels: Some(labels),
239 ..Default::default()
240 }),
241 mounts: Some(vec![mount]),
242 state: Some(container_state),
243 network_settings: Some(network_settings),
244 ..Default::default()
245 };
246
247 let deployment = Deployment::try_from(container_inspect_response).unwrap();
248
249 assert_eq!(deployment.container_id, "container_id");
251 assert_eq!(deployment.name, Some("test-deployment".to_string()));
252 assert_eq!(deployment.state, State::Running);
253 assert!(deployment.port_bindings.is_some());
254 let port_binding = deployment.port_bindings.unwrap();
255 assert_eq!(port_binding.port, Some(27017));
256 assert_eq!(
257 port_binding.binding_type,
258 crate::models::BindingType::Loopback
259 );
260 assert_eq!(deployment.creation_source, Some(CreationSource::AtlasCLI));
261 assert_eq!(deployment.mongodb_type, MongodbType::Community);
262 assert_eq!(deployment.mongodb_version, Version::new(8, 0, 0));
263 assert_eq!(
264 deployment.local_seed_location,
265 Some("/host/seed-data".to_string())
266 );
267 assert_eq!(
268 deployment.mongodb_initdb_database,
269 Some("testdb".to_string())
270 );
271 assert_eq!(
272 deployment.mongodb_initdb_root_username,
273 Some("admin".to_string())
274 );
275 assert_eq!(
276 deployment.mongodb_initdb_root_username_file,
277 Some("/run/secrets/username".to_string())
278 );
279 assert_eq!(
280 deployment.mongodb_initdb_root_password,
281 Some("password123".to_string())
282 );
283 assert_eq!(
284 deployment.mongodb_initdb_root_password_file,
285 Some("/run/secrets/password".to_string())
286 );
287 assert_eq!(
288 deployment.runner_log_file,
289 Some("/tmp/runner.log".to_string())
290 );
291 assert_eq!(
292 deployment.mongot_log_file,
293 Some("/tmp/mongot.log".to_string())
294 );
295 assert_eq!(deployment.do_not_track, false);
296 assert_eq!(
297 deployment.telemetry_base_url,
298 Some("https://telemetry.example.com".to_string())
299 );
300 assert_eq!(deployment.mongodb_load_sample_data, Some(true));
301 assert_eq!(
302 deployment.voyage_api_key,
303 Some("voyage-api-key".to_string())
304 );
305 }
306
307 #[test]
308 fn test_extract_local_seed_location_no_mounts() {
309 let container_inspect_response = ContainerInspectResponse {
310 mounts: None,
311 ..Default::default()
312 };
313
314 let result = extract_local_seed_location(&container_inspect_response);
315 assert_eq!(result, None);
316 }
317
318 #[test]
319 fn test_extract_local_seed_location_empty_mounts() {
320 let container_inspect_response = ContainerInspectResponse {
321 mounts: Some(vec![]),
322 ..Default::default()
323 };
324
325 let result = extract_local_seed_location(&container_inspect_response);
326 assert_eq!(result, None);
327 }
328
329 #[test]
330 fn test_extract_local_seed_location_no_matching_mount() {
331 let mount1 = MountPoint {
332 destination: Some("/var/log".to_string()),
333 source: Some("/host/logs".to_string()),
334 ..Default::default()
335 };
336 let mount2 = MountPoint {
337 destination: Some("/app/data".to_string()),
338 source: Some("/host/data".to_string()),
339 ..Default::default()
340 };
341
342 let container_inspect_response = ContainerInspectResponse {
343 mounts: Some(vec![mount1, mount2]),
344 ..Default::default()
345 };
346
347 let result = extract_local_seed_location(&container_inspect_response);
348 assert_eq!(result, None);
349 }
350
351 #[test]
352 fn test_extract_local_seed_location_matching_mount() {
353 let mount = MountPoint {
354 destination: Some(LOCAL_SEED_LOCATION.to_string()),
355 source: Some("/host/seed-data".to_string()),
356 ..Default::default()
357 };
358
359 let container_inspect_response = ContainerInspectResponse {
360 mounts: Some(vec![mount]),
361 ..Default::default()
362 };
363
364 let result = extract_local_seed_location(&container_inspect_response);
365 assert_eq!(result, Some("/host/seed-data".to_string()));
366 }
367
368 #[test]
369 fn test_is_seeding_true() {
370 assert!(is_seeding_true("true"));
372 assert!(is_seeding_true("True"));
373 assert!(is_seeding_true("TRUE"));
374 assert!(is_seeding_true("1"));
375 assert!(is_seeding_true("2"));
376
377 assert!(!is_seeding_true("false"));
379 assert!(!is_seeding_true("0"));
380 assert!(!is_seeding_true("something else"));
381 }
382}