Skip to main content

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