Skip to main content

atlas_local/models/
deployment.rs

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    // Identifiers
16    pub container_id: String,
17    pub name: Option<String>,
18
19    // Docker specific
20    pub state: State,
21    pub port_bindings: Option<MongoDBPortBinding>,
22
23    // MongoDB details (MongoD)
24    pub mongodb_type: MongodbType,
25    pub mongodb_version: Version,
26
27    // Creation source
28    pub creation_source: Option<CreationSource>,
29
30    // Initial database configuration
31    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    // Logging
41    pub mongot_log_file: Option<String>,
42    pub runner_log_file: Option<String>,
43
44    // Telemetry
45    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        // Extract the container ID from the response
66        let container_id = value
67            .id
68            .as_ref()
69            .ok_or(IntoDeploymentError::MissingContainerID)?
70            .clone();
71
72        // Extract the container name as the deployment name
73        // Docker names have a leading slash, so we remove it
74        let name = value
75            .name
76            .as_ref()
77            .and_then(|n| n.strip_prefix('/'))
78            .map(|n| n.to_string());
79
80        // Get container labels, environment variables, and local seed location from the container inspect response
81        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        // Deconstruct the labels and environment variables
88        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            // Identifiers
110            name,
111            container_id,
112
113            // Docker specific
114            state,
115            port_bindings,
116
117            // MongoDB details (MongoD)
118            mongodb_type,
119            mongodb_version,
120
121            // Creation source
122            creation_source: tool,
123
124            // Initial database configuration
125            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            // Logging
135            mongot_log_file,
136            runner_log_file,
137
138            // Telemetry
139            // If the DO_NOT_TRACK environment variable is set, do not track is enabled
140            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    // Go through the mounts and find the one that has the local seed location (mounted at /docker-entrypoint-initdb.d)
150    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    // Return the source of the mount
157    mount.source.clone()
158}
159
160/// Determine if the value is a boolean or an integer that is larger than 0, and return true if it is
161fn is_seeding_true(value: impl AsRef<str>) -> bool {
162    let value = value.as_ref();
163    // Try to parse the value as a boolean, if it succeeds, return the value
164    if let Ok(value) = value.to_ascii_lowercase().parse::<bool>() {
165        return value;
166    }
167
168    // Try to parse the value as an integer, if it succeeds, return true if the value is larger than 0
169    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        // Create required labels for LocalDeploymentLabels
188        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        // Create environment variables
194        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        // Create a mount for local seed location
209        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        // Create state for the container
216        let container_state = ContainerState {
217            status: Some(ContainerStateStatusEnum::RUNNING),
218            ..Default::default()
219        };
220
221        // Create network settings with port bindings
222        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        // Test all the fields to ensure proper parsing
250        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        // True values
371        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        // False values
378        assert!(!is_seeding_true("false"));
379        assert!(!is_seeding_true("0"));
380        assert!(!is_seeding_true("something else"));
381    }
382}