Skip to main content

sal_virt/nerdctl/
container_operations.rs

1// File: /root/code/git.threefold.info/herocode/sal/src/virt/nerdctl/container_operations.rs
2
3use super::container_types::{Container, ContainerStatus, ResourceUsage};
4use crate::nerdctl::{execute_nerdctl_command, NerdctlError};
5use sal_process::CommandResult;
6use serde_json;
7
8impl Container {
9    /// Start the container and verify it's running
10    /// If the container hasn't been created yet, it will be created automatically.
11    ///
12    /// # Returns
13    ///
14    /// * `Result<CommandResult, NerdctlError>` - Command result or error with detailed information
15    pub fn start(&self) -> Result<CommandResult, NerdctlError> {
16        // If container_id is None, we need to create the container first
17        let container = if self.container_id.is_none() {
18            // Check if we have an image specified
19            if self.image.is_none() {
20                return Err(NerdctlError::Other(
21                    "No image specified for container creation".to_string(),
22                ));
23            }
24
25            // Clone self and create the container
26            println!("Container not created yet. Creating container from image...");
27
28            // First, try to pull the image if it doesn't exist locally
29            let image = self.image.as_ref().unwrap();
30            match execute_nerdctl_command(&["image", "inspect", image]) {
31                Err(_) => {
32                    println!("Image '{}' not found locally. Pulling image...", image);
33                    if let Err(e) = execute_nerdctl_command(&["pull", image]) {
34                        return Err(NerdctlError::CommandFailed(format!(
35                            "Failed to pull image '{}': {}",
36                            image, e
37                        )));
38                    }
39                    println!("Image '{}' pulled successfully.", image);
40                }
41                Ok(_) => {
42                    println!("Image '{}' found locally.", image);
43                }
44            }
45
46            // Now create the container
47            match self.clone().build() {
48                Ok(built) => built,
49                Err(e) => {
50                    return Err(NerdctlError::CommandFailed(format!(
51                        "Failed to create container from image '{}': {}",
52                        image, e
53                    )));
54                }
55            }
56        } else {
57            // Container already has an ID, use it as is
58            self.clone()
59        };
60
61        if let Some(container_id) = &container.container_id {
62            // First, try to start the container
63            let start_result = execute_nerdctl_command(&["start", container_id]);
64
65            // If the start command failed, return the error with details
66            if let Err(err) = &start_result {
67                return Err(NerdctlError::CommandFailed(format!(
68                    "Failed to start container {}: {}",
69                    container_id, err
70                )));
71            }
72
73            // Verify the container is actually running
74            match container.verify_running() {
75                Ok(true) => start_result,
76                Ok(false) => {
77                    // Container started but isn't running - get detailed information
78                    let mut error_message =
79                        format!("Container {} started but is not running.", container_id);
80
81                    // Get container status
82                    if let Ok(status) = container.status() {
83                        error_message.push_str(&format!(
84                            "\nStatus: {}, State: {}, Health: {}",
85                            status.status,
86                            status.state,
87                            status.health_status.unwrap_or_else(|| "N/A".to_string())
88                        ));
89                    }
90
91                    // Get container logs
92                    if let Ok(logs) = execute_nerdctl_command(&["logs", container_id]) {
93                        if !logs.stdout.trim().is_empty() {
94                            error_message.push_str(&format!(
95                                "\nContainer logs (stdout):\n{}",
96                                logs.stdout.trim()
97                            ));
98                        }
99                        if !logs.stderr.trim().is_empty() {
100                            error_message.push_str(&format!(
101                                "\nContainer logs (stderr):\n{}",
102                                logs.stderr.trim()
103                            ));
104                        }
105                    }
106
107                    // Get container exit code if available
108                    if let Ok(inspect_result) = execute_nerdctl_command(&[
109                        "inspect",
110                        "--format",
111                        "{{.State.ExitCode}}",
112                        container_id,
113                    ]) {
114                        let exit_code = inspect_result.stdout.trim();
115                        if !exit_code.is_empty() && exit_code != "0" {
116                            error_message
117                                .push_str(&format!("\nContainer exit code: {}", exit_code));
118                        }
119                    }
120
121                    Err(NerdctlError::CommandFailed(error_message))
122                }
123                Err(err) => {
124                    // Failed to verify if container is running
125                    Err(NerdctlError::CommandFailed(format!(
126                        "Container {} may have started, but verification failed: {}",
127                        container_id, err
128                    )))
129                }
130            }
131        } else {
132            Err(NerdctlError::Other(
133                "Failed to create container. No container ID available.".to_string(),
134            ))
135        }
136    }
137
138    /// Verify if the container is running
139    ///
140    /// # Returns
141    ///
142    /// * `Result<bool, NerdctlError>` - True if running, false if not running, error if verification failed
143    fn verify_running(&self) -> Result<bool, NerdctlError> {
144        if let Some(container_id) = &self.container_id {
145            // Use inspect to check if the container is running
146            let inspect_result = execute_nerdctl_command(&[
147                "inspect",
148                "--format",
149                "{{.State.Running}}",
150                container_id,
151            ]);
152
153            match inspect_result {
154                Ok(result) => {
155                    let running = result.stdout.trim().to_lowercase() == "true";
156                    Ok(running)
157                }
158                Err(err) => Err(err),
159            }
160        } else {
161            Err(NerdctlError::Other("No container ID available".to_string()))
162        }
163    }
164
165    /// Stop the container
166    ///
167    /// # Returns
168    ///
169    /// * `Result<CommandResult, NerdctlError>` - Command result or error
170    pub fn stop(&self) -> Result<CommandResult, NerdctlError> {
171        if let Some(container_id) = &self.container_id {
172            execute_nerdctl_command(&["stop", container_id])
173        } else {
174            Err(NerdctlError::Other("No container ID available".to_string()))
175        }
176    }
177
178    /// Remove the container
179    ///
180    /// # Returns
181    ///
182    /// * `Result<CommandResult, NerdctlError>` - Command result or error
183    pub fn remove(&self) -> Result<CommandResult, NerdctlError> {
184        if let Some(container_id) = &self.container_id {
185            execute_nerdctl_command(&["rm", container_id])
186        } else {
187            Err(NerdctlError::Other("No container ID available".to_string()))
188        }
189    }
190
191    /// Execute a command in the container
192    ///
193    /// # Arguments
194    ///
195    /// * `command` - The command to run
196    ///
197    /// # Returns
198    ///
199    /// * `Result<CommandResult, NerdctlError>` - Command result or error
200    pub fn exec(&self, command: &str) -> Result<CommandResult, NerdctlError> {
201        if let Some(container_id) = &self.container_id {
202            execute_nerdctl_command(&["exec", container_id, "sh", "-c", command])
203        } else {
204            Err(NerdctlError::Other("No container ID available".to_string()))
205        }
206    }
207
208    /// Copy files between container and local filesystem
209    ///
210    /// # Arguments
211    ///
212    /// * `source` - Source path (can be container:path or local path)
213    /// * `dest` - Destination path (can be container:path or local path)
214    ///
215    /// # Returns
216    ///
217    /// * `Result<CommandResult, NerdctlError>` - Command result or error
218    pub fn copy(&self, source: &str, dest: &str) -> Result<CommandResult, NerdctlError> {
219        if self.container_id.is_some() {
220            execute_nerdctl_command(&["cp", source, dest])
221        } else {
222            Err(NerdctlError::Other("No container ID available".to_string()))
223        }
224    }
225
226    /// Export the container to a tarball
227    ///
228    /// # Arguments
229    ///
230    /// * `path` - Path to save the tarball
231    ///
232    /// # Returns
233    ///
234    /// * `Result<CommandResult, NerdctlError>` - Command result or error
235    pub fn export(&self, path: &str) -> Result<CommandResult, NerdctlError> {
236        if let Some(container_id) = &self.container_id {
237            execute_nerdctl_command(&["export", "-o", path, container_id])
238        } else {
239            Err(NerdctlError::Other("No container ID available".to_string()))
240        }
241    }
242
243    /// Commit the container to an image
244    ///
245    /// # Arguments
246    ///
247    /// * `image_name` - Name for the new image
248    ///
249    /// # Returns
250    ///
251    /// * `Result<CommandResult, NerdctlError>` - Command result or error
252    pub fn commit(&self, image_name: &str) -> Result<CommandResult, NerdctlError> {
253        if let Some(container_id) = &self.container_id {
254            execute_nerdctl_command(&["commit", container_id, image_name])
255        } else {
256            Err(NerdctlError::Other("No container ID available".to_string()))
257        }
258    }
259
260    /// Get container status
261    ///
262    /// # Returns
263    ///
264    /// * `Result<ContainerStatus, NerdctlError>` - Container status or error
265    pub fn status(&self) -> Result<ContainerStatus, NerdctlError> {
266        if let Some(container_id) = &self.container_id {
267            let result = execute_nerdctl_command(&["inspect", container_id])?;
268
269            // Parse the JSON output
270            match serde_json::from_str::<serde_json::Value>(&result.stdout) {
271                Ok(json) => {
272                    if let Some(container_json) = json.as_array().and_then(|arr| arr.first()) {
273                        let state = container_json
274                            .get("State")
275                            .and_then(|state| state.get("Status"))
276                            .and_then(|status| status.as_str())
277                            .unwrap_or("unknown")
278                            .to_string();
279
280                        let status = container_json
281                            .get("State")
282                            .and_then(|state| state.get("Running"))
283                            .and_then(|running| {
284                                if running.as_bool().unwrap_or(false) {
285                                    Some("running")
286                                } else {
287                                    Some("stopped")
288                                }
289                            })
290                            .unwrap_or("unknown")
291                            .to_string();
292
293                        let created = container_json
294                            .get("Created")
295                            .and_then(|created| created.as_str())
296                            .unwrap_or("unknown")
297                            .to_string();
298
299                        let started = container_json
300                            .get("State")
301                            .and_then(|state| state.get("StartedAt"))
302                            .and_then(|started| started.as_str())
303                            .unwrap_or("unknown")
304                            .to_string();
305
306                        // Get health status if available
307                        let health_status = container_json
308                            .get("State")
309                            .and_then(|state| state.get("Health"))
310                            .and_then(|health| health.get("Status"))
311                            .and_then(|status| status.as_str())
312                            .map(|s| s.to_string());
313
314                        // Get health check output if available
315                        let health_output = container_json
316                            .get("State")
317                            .and_then(|state| state.get("Health"))
318                            .and_then(|health| health.get("Log"))
319                            .and_then(|log| log.as_array())
320                            .and_then(|log_array| log_array.last())
321                            .and_then(|last_log| last_log.get("Output"))
322                            .and_then(|output| output.as_str())
323                            .map(|s| s.to_string());
324
325                        Ok(ContainerStatus {
326                            state,
327                            status,
328                            created,
329                            started,
330                            health_status,
331                            health_output,
332                        })
333                    } else {
334                        Err(NerdctlError::JsonParseError(
335                            "Invalid container inspect JSON".to_string(),
336                        ))
337                    }
338                }
339                Err(e) => Err(NerdctlError::JsonParseError(format!(
340                    "Failed to parse container inspect JSON: {}",
341                    e
342                ))),
343            }
344        } else {
345            Err(NerdctlError::Other("No container ID available".to_string()))
346        }
347    }
348
349    /// Get the health status of the container
350    ///
351    /// # Returns
352    ///
353    /// * `Result<String, NerdctlError>` - Health status or error
354    pub fn health_status(&self) -> Result<String, NerdctlError> {
355        if let Some(container_id) = &self.container_id {
356            let result = execute_nerdctl_command(&[
357                "inspect",
358                "--format",
359                "{{.State.Health.Status}}",
360                container_id,
361            ])?;
362            Ok(result.stdout.trim().to_string())
363        } else {
364            Err(NerdctlError::Other("No container ID available".to_string()))
365        }
366    }
367
368    /// Get container logs
369    ///
370    /// # Returns
371    ///
372    /// * `Result<CommandResult, NerdctlError>` - Command result or error
373    pub fn logs(&self) -> Result<CommandResult, NerdctlError> {
374        if let Some(container_id) = &self.container_id {
375            execute_nerdctl_command(&["logs", container_id])
376        } else {
377            Err(NerdctlError::Other("No container ID available".to_string()))
378        }
379    }
380
381    /// Get container resource usage
382    ///
383    /// # Returns
384    ///
385    /// * `Result<ResourceUsage, NerdctlError>` - Resource usage or error
386    pub fn resources(&self) -> Result<ResourceUsage, NerdctlError> {
387        if let Some(container_id) = &self.container_id {
388            let result = execute_nerdctl_command(&["stats", "--no-stream", container_id])?;
389
390            // Parse the output
391            let lines: Vec<&str> = result.stdout.lines().collect();
392            if lines.len() >= 2 {
393                let headers = lines[0];
394                let values = lines[1];
395
396                let headers_vec: Vec<&str> = headers.split_whitespace().collect();
397                let values_vec: Vec<&str> = values.split_whitespace().collect();
398
399                // Find indices for each metric
400                let cpu_index = headers_vec
401                    .iter()
402                    .position(|&h| h.contains("CPU"))
403                    .unwrap_or(0);
404                let mem_index = headers_vec
405                    .iter()
406                    .position(|&h| h.contains("MEM"))
407                    .unwrap_or(0);
408                let mem_perc_index = headers_vec
409                    .iter()
410                    .position(|&h| h.contains("MEM%"))
411                    .unwrap_or(0);
412                let net_in_index = headers_vec
413                    .iter()
414                    .position(|&h| h.contains("NET"))
415                    .unwrap_or(0);
416                let net_out_index = if net_in_index > 0 {
417                    net_in_index + 1
418                } else {
419                    0
420                };
421                let block_in_index = headers_vec
422                    .iter()
423                    .position(|&h| h.contains("BLOCK"))
424                    .unwrap_or(0);
425                let block_out_index = if block_in_index > 0 {
426                    block_in_index + 1
427                } else {
428                    0
429                };
430                let pids_index = headers_vec
431                    .iter()
432                    .position(|&h| h.contains("PIDS"))
433                    .unwrap_or(0);
434
435                let cpu_usage = if cpu_index < values_vec.len() {
436                    values_vec[cpu_index].to_string()
437                } else {
438                    "unknown".to_string()
439                };
440
441                let memory_usage = if mem_index < values_vec.len() {
442                    values_vec[mem_index].to_string()
443                } else {
444                    "unknown".to_string()
445                };
446
447                let memory_limit = if mem_index + 1 < values_vec.len() {
448                    values_vec[mem_index + 1].to_string()
449                } else {
450                    "unknown".to_string()
451                };
452
453                let memory_percentage = if mem_perc_index < values_vec.len() {
454                    values_vec[mem_perc_index].to_string()
455                } else {
456                    "unknown".to_string()
457                };
458
459                let network_input = if net_in_index < values_vec.len() {
460                    values_vec[net_in_index].to_string()
461                } else {
462                    "unknown".to_string()
463                };
464
465                let network_output = if net_out_index < values_vec.len() {
466                    values_vec[net_out_index].to_string()
467                } else {
468                    "unknown".to_string()
469                };
470
471                let block_input = if block_in_index < values_vec.len() {
472                    values_vec[block_in_index].to_string()
473                } else {
474                    "unknown".to_string()
475                };
476
477                let block_output = if block_out_index < values_vec.len() {
478                    values_vec[block_out_index].to_string()
479                } else {
480                    "unknown".to_string()
481                };
482
483                let pids = if pids_index < values_vec.len() {
484                    values_vec[pids_index].to_string()
485                } else {
486                    "unknown".to_string()
487                };
488
489                Ok(ResourceUsage {
490                    cpu_usage,
491                    memory_usage,
492                    memory_limit,
493                    memory_percentage,
494                    network_input,
495                    network_output,
496                    block_input,
497                    block_output,
498                    pids,
499                })
500            } else {
501                Err(NerdctlError::ConversionError(
502                    "Failed to parse stats output".to_string(),
503                ))
504            }
505        } else {
506            Err(NerdctlError::Other("No container ID available".to_string()))
507        }
508    }
509}