sal/src/virt/nerdctl/container_operations.rs
2025-04-05 07:40:32 +02:00

448 lines
18 KiB
Rust

// File: /root/code/git.ourworld.tf/herocode/sal/src/virt/nerdctl/container_operations.rs
use crate::process::CommandResult;
use crate::virt::nerdctl::{execute_nerdctl_command, NerdctlError};
use super::container_types::{Container, ContainerStatus, ResourceUsage};
use serde_json;
impl Container {
/// Start the container and verify it's running
/// If the container hasn't been created yet, it will be created automatically.
///
/// # Returns
///
/// * `Result<CommandResult, NerdctlError>` - Command result or error with detailed information
pub fn start(&self) -> Result<CommandResult, NerdctlError> {
// If container_id is None, we need to create the container first
let container = if self.container_id.is_none() {
// Check if we have an image specified
if self.image.is_none() {
return Err(NerdctlError::Other("No image specified for container creation".to_string()));
}
// Clone self and create the container
println!("Container not created yet. Creating container from image...");
// First, try to pull the image if it doesn't exist locally
let image = self.image.as_ref().unwrap();
match execute_nerdctl_command(&["image", "inspect", image]) {
Err(_) => {
println!("Image '{}' not found locally. Pulling image...", image);
if let Err(e) = execute_nerdctl_command(&["pull", image]) {
return Err(NerdctlError::CommandFailed(
format!("Failed to pull image '{}': {}", image, e)
));
}
println!("Image '{}' pulled successfully.", image);
},
Ok(_) => {
println!("Image '{}' found locally.", image);
}
}
// Now create the container
match self.clone().build() {
Ok(built) => built,
Err(e) => {
return Err(NerdctlError::CommandFailed(
format!("Failed to create container from image '{}': {}", image, e)
));
}
}
} else {
// Container already has an ID, use it as is
self.clone()
};
if let Some(container_id) = &container.container_id {
// First, try to start the container
let start_result = execute_nerdctl_command(&["start", container_id]);
// If the start command failed, return the error with details
if let Err(err) = &start_result {
return Err(NerdctlError::CommandFailed(
format!("Failed to start container {}: {}", container_id, err)
));
}
// Verify the container is actually running
match container.verify_running() {
Ok(true) => start_result,
Ok(false) => {
// Container started but isn't running - get detailed information
let mut error_message = format!("Container {} started but is not running.", container_id);
// Get container status
if let Ok(status) = container.status() {
error_message.push_str(&format!("\nStatus: {}, State: {}, Health: {}",
status.status,
status.state,
status.health_status.unwrap_or_else(|| "N/A".to_string())
));
}
// Get container logs
if let Ok(logs) = execute_nerdctl_command(&["logs", container_id]) {
if !logs.stdout.trim().is_empty() {
error_message.push_str(&format!("\nContainer logs (stdout):\n{}", logs.stdout.trim()));
}
if !logs.stderr.trim().is_empty() {
error_message.push_str(&format!("\nContainer logs (stderr):\n{}", logs.stderr.trim()));
}
}
// Get container exit code if available
if let Ok(inspect_result) = execute_nerdctl_command(&["inspect", "--format", "{{.State.ExitCode}}", container_id]) {
let exit_code = inspect_result.stdout.trim();
if !exit_code.is_empty() && exit_code != "0" {
error_message.push_str(&format!("\nContainer exit code: {}", exit_code));
}
}
Err(NerdctlError::CommandFailed(error_message))
},
Err(err) => {
// Failed to verify if container is running
Err(NerdctlError::CommandFailed(
format!("Container {} may have started, but verification failed: {}",
container_id, err
)
))
}
}
} else {
Err(NerdctlError::Other("Failed to create container. No container ID available.".to_string()))
}
}
/// Verify if the container is running
///
/// # Returns
///
/// * `Result<bool, NerdctlError>` - True if running, false if not running, error if verification failed
fn verify_running(&self) -> Result<bool, NerdctlError> {
if let Some(container_id) = &self.container_id {
// Use inspect to check if the container is running
let inspect_result = execute_nerdctl_command(&["inspect", "--format", "{{.State.Running}}", container_id]);
match inspect_result {
Ok(result) => {
let running = result.stdout.trim().to_lowercase() == "true";
Ok(running)
},
Err(err) => Err(err)
}
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Stop the container
///
/// # Returns
///
/// * `Result<CommandResult, NerdctlError>` - Command result or error
pub fn stop(&self) -> Result<CommandResult, NerdctlError> {
if let Some(container_id) = &self.container_id {
execute_nerdctl_command(&["stop", container_id])
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Remove the container
///
/// # Returns
///
/// * `Result<CommandResult, NerdctlError>` - Command result or error
pub fn remove(&self) -> Result<CommandResult, NerdctlError> {
if let Some(container_id) = &self.container_id {
execute_nerdctl_command(&["rm", container_id])
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Execute a command in the container
///
/// # Arguments
///
/// * `command` - The command to run
///
/// # Returns
///
/// * `Result<CommandResult, NerdctlError>` - Command result or error
pub fn exec(&self, command: &str) -> Result<CommandResult, NerdctlError> {
if let Some(container_id) = &self.container_id {
execute_nerdctl_command(&["exec", container_id, "sh", "-c", command])
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Copy files between container and local filesystem
///
/// # Arguments
///
/// * `source` - Source path (can be container:path or local path)
/// * `dest` - Destination path (can be container:path or local path)
///
/// # Returns
///
/// * `Result<CommandResult, NerdctlError>` - Command result or error
pub fn copy(&self, source: &str, dest: &str) -> Result<CommandResult, NerdctlError> {
if self.container_id.is_some() {
execute_nerdctl_command(&["cp", source, dest])
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Export the container to a tarball
///
/// # Arguments
///
/// * `path` - Path to save the tarball
///
/// # Returns
///
/// * `Result<CommandResult, NerdctlError>` - Command result or error
pub fn export(&self, path: &str) -> Result<CommandResult, NerdctlError> {
if let Some(container_id) = &self.container_id {
execute_nerdctl_command(&["export", "-o", path, container_id])
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Commit the container to an image
///
/// # Arguments
///
/// * `image_name` - Name for the new image
///
/// # Returns
///
/// * `Result<CommandResult, NerdctlError>` - Command result or error
pub fn commit(&self, image_name: &str) -> Result<CommandResult, NerdctlError> {
if let Some(container_id) = &self.container_id {
execute_nerdctl_command(&["commit", container_id, image_name])
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Get container status
///
/// # Returns
///
/// * `Result<ContainerStatus, NerdctlError>` - Container status or error
pub fn status(&self) -> Result<ContainerStatus, NerdctlError> {
if let Some(container_id) = &self.container_id {
let result = execute_nerdctl_command(&["inspect", container_id])?;
// Parse the JSON output
match serde_json::from_str::<serde_json::Value>(&result.stdout) {
Ok(json) => {
if let Some(container_json) = json.as_array().and_then(|arr| arr.first()) {
let state = container_json
.get("State")
.and_then(|state| state.get("Status"))
.and_then(|status| status.as_str())
.unwrap_or("unknown")
.to_string();
let status = container_json
.get("State")
.and_then(|state| state.get("Running"))
.and_then(|running| {
if running.as_bool().unwrap_or(false) {
Some("running")
} else {
Some("stopped")
}
})
.unwrap_or("unknown")
.to_string();
let created = container_json
.get("Created")
.and_then(|created| created.as_str())
.unwrap_or("unknown")
.to_string();
let started = container_json
.get("State")
.and_then(|state| state.get("StartedAt"))
.and_then(|started| started.as_str())
.unwrap_or("unknown")
.to_string();
// Get health status if available
let health_status = container_json
.get("State")
.and_then(|state| state.get("Health"))
.and_then(|health| health.get("Status"))
.and_then(|status| status.as_str())
.map(|s| s.to_string());
// Get health check output if available
let health_output = container_json
.get("State")
.and_then(|state| state.get("Health"))
.and_then(|health| health.get("Log"))
.and_then(|log| log.as_array())
.and_then(|log_array| log_array.last())
.and_then(|last_log| last_log.get("Output"))
.and_then(|output| output.as_str())
.map(|s| s.to_string());
Ok(ContainerStatus {
state,
status,
created,
started,
health_status,
health_output,
})
} else {
Err(NerdctlError::JsonParseError("Invalid container inspect JSON".to_string()))
}
},
Err(e) => {
Err(NerdctlError::JsonParseError(format!("Failed to parse container inspect JSON: {}", e)))
}
}
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Get the health status of the container
///
/// # Returns
///
/// * `Result<String, NerdctlError>` - Health status or error
pub fn health_status(&self) -> Result<String, NerdctlError> {
if let Some(container_id) = &self.container_id {
let result = execute_nerdctl_command(&["inspect", "--format", "{{.State.Health.Status}}", container_id])?;
Ok(result.stdout.trim().to_string())
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Get container logs
///
/// # Returns
///
/// * `Result<CommandResult, NerdctlError>` - Command result or error
pub fn logs(&self) -> Result<CommandResult, NerdctlError> {
if let Some(container_id) = &self.container_id {
execute_nerdctl_command(&["logs", container_id])
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Get container resource usage
///
/// # Returns
///
/// * `Result<ResourceUsage, NerdctlError>` - Resource usage or error
pub fn resources(&self) -> Result<ResourceUsage, NerdctlError> {
if let Some(container_id) = &self.container_id {
let result = execute_nerdctl_command(&["stats", "--no-stream", container_id])?;
// Parse the output
let lines: Vec<&str> = result.stdout.lines().collect();
if lines.len() >= 2 {
let headers = lines[0];
let values = lines[1];
let headers_vec: Vec<&str> = headers.split_whitespace().collect();
let values_vec: Vec<&str> = values.split_whitespace().collect();
// Find indices for each metric
let cpu_index = headers_vec.iter().position(|&h| h.contains("CPU")).unwrap_or(0);
let mem_index = headers_vec.iter().position(|&h| h.contains("MEM")).unwrap_or(0);
let mem_perc_index = headers_vec.iter().position(|&h| h.contains("MEM%")).unwrap_or(0);
let net_in_index = headers_vec.iter().position(|&h| h.contains("NET")).unwrap_or(0);
let net_out_index = if net_in_index > 0 { net_in_index + 1 } else { 0 };
let block_in_index = headers_vec.iter().position(|&h| h.contains("BLOCK")).unwrap_or(0);
let block_out_index = if block_in_index > 0 { block_in_index + 1 } else { 0 };
let pids_index = headers_vec.iter().position(|&h| h.contains("PIDS")).unwrap_or(0);
let cpu_usage = if cpu_index < values_vec.len() {
values_vec[cpu_index].to_string()
} else {
"unknown".to_string()
};
let memory_usage = if mem_index < values_vec.len() {
values_vec[mem_index].to_string()
} else {
"unknown".to_string()
};
let memory_limit = if mem_index + 1 < values_vec.len() {
values_vec[mem_index + 1].to_string()
} else {
"unknown".to_string()
};
let memory_percentage = if mem_perc_index < values_vec.len() {
values_vec[mem_perc_index].to_string()
} else {
"unknown".to_string()
};
let network_input = if net_in_index < values_vec.len() {
values_vec[net_in_index].to_string()
} else {
"unknown".to_string()
};
let network_output = if net_out_index < values_vec.len() {
values_vec[net_out_index].to_string()
} else {
"unknown".to_string()
};
let block_input = if block_in_index < values_vec.len() {
values_vec[block_in_index].to_string()
} else {
"unknown".to_string()
};
let block_output = if block_out_index < values_vec.len() {
values_vec[block_out_index].to_string()
} else {
"unknown".to_string()
};
let pids = if pids_index < values_vec.len() {
values_vec[pids_index].to_string()
} else {
"unknown".to_string()
};
Ok(ResourceUsage {
cpu_usage,
memory_usage,
memory_limit,
memory_percentage,
network_input,
network_output,
block_input,
block_output,
pids,
})
} else {
Err(NerdctlError::ConversionError("Failed to parse stats output".to_string()))
}
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
}