sal/virt/src/rhai/nerdctl.rs
2025-07-09 16:27:11 +02:00

706 lines
25 KiB
Rust

//! Rhai wrappers for Nerdctl module functions
//!
//! This module provides Rhai wrappers for the functions in the Nerdctl module.
use crate::nerdctl::{self, Container, HealthCheck, Image, NerdctlError};
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
use sal_process::CommandResult;
// Helper functions for error conversion with improved context
fn nerdctl_error_to_rhai_error<T>(
result: Result<T, NerdctlError>,
) -> Result<T, Box<EvalAltResult>> {
result.map_err(|e| {
// Create a more detailed error message based on the error type
let error_message = match &e {
NerdctlError::CommandExecutionFailed(io_err) => {
format!("Failed to execute nerdctl command: {}. This may indicate nerdctl is not installed or not in PATH.", io_err)
},
NerdctlError::CommandFailed(msg) => {
format!("Nerdctl command failed: {}. Check container status and logs for more details.", msg)
},
NerdctlError::JsonParseError(msg) => {
format!("Failed to parse nerdctl JSON output: {}. This may indicate an incompatible nerdctl version.", msg)
},
NerdctlError::ConversionError(msg) => {
format!("Data conversion error: {}. This may indicate unexpected output format from nerdctl.", msg)
},
NerdctlError::Other(msg) => {
format!("Nerdctl error: {}. This is an unexpected error.", msg)
},
};
Box::new(EvalAltResult::ErrorRuntime(
error_message.into(),
rhai::Position::NONE
))
})
}
//
// Container Builder Pattern Implementation
//
/// Create a new Container
pub fn container_new(name: &str) -> Result<Container, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(Container::new(name))
}
/// Create a Container from an image
pub fn container_from_image(name: &str, image: &str) -> Result<Container, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(Container::from_image(name, image))
}
/// Reset the container configuration to defaults while keeping the name and image
pub fn container_reset(container: Container) -> Container {
container.reset()
}
// TODO: remove?
/// Set health check with options for a Container
pub fn container_with_health_check_options(
container: Container,
cmd: &str,
interval: Option<&str>,
timeout: Option<&str>,
retries: Option<i64>,
start_period: Option<&str>,
) -> Container {
// Convert i64 to u32 for retries
let retries_u32 = retries.map(|r| r as u32);
container.with_health_check_options(cmd, interval, timeout, retries_u32, start_period)
}
/// Build and run the Container
///
/// This function builds and runs the container using the configured options.
/// It provides detailed error information if the build fails.
pub fn container_build(container: Container) -> Result<Container, Box<EvalAltResult>> {
// Get container details for better error reporting
let container_name = container.name.clone();
let image = container
.image
.clone()
.unwrap_or_else(|| "none".to_string());
let ports = container.ports.clone();
let volumes = container.volumes.clone();
let env_vars = container.env_vars.clone();
// Try to build the container
let build_result = container.build();
// Handle the result with improved error context
match build_result {
Ok(built_container) => {
// Container built successfully
Ok(built_container)
}
Err(err) => {
// Add more context to the error
let enhanced_error = match err {
NerdctlError::CommandFailed(msg) => {
// Provide more detailed error information
let mut enhanced_msg = format!(
"Failed to build container '{}' from image '{}': {}",
container_name, image, msg
);
// Add information about configured options that might be relevant
if !ports.is_empty() {
enhanced_msg.push_str(&format!("\nConfigured ports: {:?}", ports));
}
if !volumes.is_empty() {
enhanced_msg.push_str(&format!("\nConfigured volumes: {:?}", volumes));
}
if !env_vars.is_empty() {
enhanced_msg.push_str(&format!(
"\nConfigured environment variables: {:?}",
env_vars
));
}
// Add suggestions for common issues
if msg.contains("not found") || msg.contains("no such image") {
enhanced_msg.push_str("\nSuggestion: The specified image may not exist or may not be pulled yet. Try pulling the image first with nerdctl_image_pull().");
} else if msg.contains("port is already allocated") {
enhanced_msg.push_str("\nSuggestion: One of the specified ports is already in use. Try using a different port or stopping the container using that port.");
} else if msg.contains("permission denied") {
enhanced_msg.push_str("\nSuggestion: Permission issues detected. Check if you have the necessary permissions to create containers or access the specified volumes.");
}
NerdctlError::CommandFailed(enhanced_msg)
}
_ => err,
};
nerdctl_error_to_rhai_error(Err(enhanced_error))
}
}
}
/// Start the Container and verify it's running
///
/// This function starts the container and verifies that it's actually running.
/// It returns detailed error information if the container fails to start or
/// if it starts but stops immediately.
pub fn container_start(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> {
// Get container details for better error reporting
let container_name = container.name.clone();
let container_id = container
.container_id
.clone()
.unwrap_or_else(|| "unknown".to_string());
// Try to start the container
let start_result = container.start();
// Handle the result with improved error context
match start_result {
Ok(result) => {
// Container started successfully
Ok(result)
}
Err(err) => {
// Add more context to the error
let enhanced_error = match err {
NerdctlError::CommandFailed(msg) => {
// Check if this is a "container already running" error, which is not really an error
if msg.contains("already running") {
return Ok(CommandResult {
stdout: format!("Container {} is already running", container_name),
stderr: "".to_string(),
success: true,
code: 0,
});
}
// Try to get more information about why the container might have failed to start
let mut enhanced_msg = format!(
"Failed to start container '{}' (ID: {}): {}",
container_name, container_id, msg
);
// Try to check if the image exists
if let Some(image) = &container.image {
enhanced_msg.push_str(&format!("\nContainer was using image: {}", image));
}
NerdctlError::CommandFailed(enhanced_msg)
}
_ => err,
};
nerdctl_error_to_rhai_error(Err(enhanced_error))
}
}
}
/// Stop the Container
pub fn container_stop(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(container.stop())
}
/// Remove the Container
pub fn container_remove(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(container.remove())
}
/// Execute a command in the Container
pub fn container_exec(
container: &mut Container,
command: &str,
) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(container.exec(command))
}
/// Get container logs
pub fn container_logs(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> {
// Get container details for better error reporting
let container_name = container.name.clone();
let container_id = container
.container_id
.clone()
.unwrap_or_else(|| "unknown".to_string());
// Use the nerdctl::logs function
let logs_result = nerdctl::logs(&container_id);
match logs_result {
Ok(result) => Ok(result),
Err(err) => {
// Add more context to the error
let enhanced_error = NerdctlError::CommandFailed(format!(
"Failed to get logs for container '{}' (ID: {}): {}",
container_name, container_id, err
));
nerdctl_error_to_rhai_error(Err(enhanced_error))
}
}
}
/// Copy files between the Container and local filesystem
pub fn container_copy(
container: &mut Container,
source: &str,
dest: &str,
) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(container.copy(source, dest))
}
/// Create a new Map with default run options
pub fn new_run_options() -> Map {
let mut map = Map::new();
map.insert("name".into(), Dynamic::UNIT);
map.insert("detach".into(), Dynamic::from(true));
map.insert("ports".into(), Dynamic::from(Array::new()));
map.insert("snapshotter".into(), Dynamic::from("native"));
map
}
//
// Container Function Wrappers
//
/// Wrapper for nerdctl::run
///
/// Run a container from an image.
pub fn nerdctl_run(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(nerdctl::run(image, None, true, None, None))
}
/// Run a container with a name
pub fn nerdctl_run_with_name(image: &str, name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(nerdctl::run(image, Some(name), true, None, None))
}
/// Run a container with a port mapping
pub fn nerdctl_run_with_port(
image: &str,
name: &str,
port: &str,
) -> Result<CommandResult, Box<EvalAltResult>> {
let ports = vec![port];
nerdctl_error_to_rhai_error(nerdctl::run(image, Some(name), true, Some(&ports), None))
}
/// Wrapper for nerdctl::exec
///
/// Execute a command in a container.
pub fn nerdctl_exec(container: &str, command: &str) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(nerdctl::exec(container, command))
}
/// Wrapper for nerdctl::copy
///
/// Copy files between container and local filesystem.
pub fn nerdctl_copy(source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(nerdctl::copy(source, dest))
}
/// Wrapper for nerdctl::stop
///
/// Stop a container.
pub fn nerdctl_stop(container: &str) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(nerdctl::stop(container))
}
/// Wrapper for nerdctl::remove
///
/// Remove a container.
pub fn nerdctl_remove(container: &str) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(nerdctl::remove(container))
}
/// Wrapper for nerdctl::list
///
/// List containers.
pub fn nerdctl_list(all: bool) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(nerdctl::list(all))
}
/// Wrapper for nerdctl::logs
///
/// Get container logs.
pub fn nerdctl_logs(container: &str) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(nerdctl::logs(container))
}
//
// Image Function Wrappers
//
/// Wrapper for nerdctl::images
///
/// List images in local storage.
pub fn nerdctl_images() -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(nerdctl::images())
}
/// Wrapper for nerdctl::image_remove
///
/// Remove one or more images.
pub fn nerdctl_image_remove(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(nerdctl::image_remove(image))
}
/// Wrapper for nerdctl::image_push
///
/// Push an image to a registry.
pub fn nerdctl_image_push(
image: &str,
destination: &str,
) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(nerdctl::image_push(image, destination))
}
/// Wrapper for nerdctl::image_tag
///
/// Add an additional name to a local image.
pub fn nerdctl_image_tag(image: &str, new_name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(nerdctl::image_tag(image, new_name))
}
/// Wrapper for nerdctl::image_pull
///
/// Pull an image from a registry.
pub fn nerdctl_image_pull(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(nerdctl::image_pull(image))
}
/// Wrapper for nerdctl::image_commit
///
/// Commit a container to an image.
pub fn nerdctl_image_commit(
container: &str,
image_name: &str,
) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(nerdctl::image_commit(container, image_name))
}
/// Wrapper for nerdctl::image_build
///
/// Build an image using a Dockerfile.
pub fn nerdctl_image_build(
tag: &str,
context_path: &str,
) -> Result<CommandResult, Box<EvalAltResult>> {
nerdctl_error_to_rhai_error(nerdctl::image_build(tag, context_path))
}
/// Register Nerdctl module functions with the Rhai engine
///
/// # Arguments
///
/// * `engine` - The Rhai engine to register the functions with
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_nerdctl_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register types
register_nerdctl_types(engine)?;
// Register Container constructor
engine.register_fn("nerdctl_container_new", container_new);
engine.register_fn("nerdctl_container_from_image", container_from_image);
// TODO: check if this works!
// Register Container instance methods
engine.register_fn("reset", container_reset);
// TODO: these functions should be getters and setter like the buildah example
// engine.register_fn("with_image", container_with_image);
// engine.register_fn("with_port", container_with_port);
// engine.register_fn("with_volume", container_with_volume);
// engine.register_fn("with_env", container_with_env);
// engine.register_fn("with_network", container_with_network);
// engine.register_fn("with_network_alias", container_with_network_alias);
// engine.register_fn("with_cpu_limit", container_with_cpu_limit);
// engine.register_fn("with_memory_limit", container_with_memory_limit);
// engine.register_fn("with_restart_policy", container_with_restart_policy);
// engine.register_fn("with_health_check", container_with_health_check);
// engine.register_fn("with_ports", container_with_ports);
// engine.register_fn("with_volumes", container_with_volumes);
// engine.register_fn("with_envs", container_with_envs);
// engine.register_fn("with_network_aliases", container_with_network_aliases);
// engine.register_fn("with_memory_swap_limit", container_with_memory_swap_limit);
// engine.register_fn("with_cpu_shares", container_with_cpu_shares);
// engine.register_fn(
// "with_health_check_options",
// container_with_health_check_options,
// );
// engine.register_fn("with_snapshotter", container_with_snapshotter);
// engine.register_fn("with_detach", container_with_detach);
engine.register_fn("build", container_build);
engine.register_fn("start", container_start);
engine.register_fn("stop", container_stop);
engine.register_fn("remove", container_remove);
engine.register_fn("exec", container_exec);
engine.register_fn("logs", container_logs);
engine.register_fn("copy", container_copy);
// Register legacy container functions (for backward compatibility)
engine.register_fn("nerdctl_run", nerdctl_run);
engine.register_fn("nerdctl_run_with_name", nerdctl_run_with_name);
engine.register_fn("nerdctl_run_with_port", nerdctl_run_with_port);
engine.register_fn("new_run_options", new_run_options);
engine.register_fn("nerdctl_exec", nerdctl_exec);
engine.register_fn("nerdctl_copy", nerdctl_copy);
engine.register_fn("nerdctl_stop", nerdctl_stop);
engine.register_fn("nerdctl_remove", nerdctl_remove);
engine.register_fn("nerdctl_list", nerdctl_list);
engine.register_fn("nerdctl_logs", nerdctl_logs);
// Register image functions
engine.register_fn("nerdctl_images", nerdctl_images);
engine.register_fn("nerdctl_image_remove", nerdctl_image_remove);
engine.register_fn("nerdctl_image_push", nerdctl_image_push);
engine.register_fn("nerdctl_image_tag", nerdctl_image_tag);
engine.register_fn("nerdctl_image_pull", nerdctl_image_pull);
engine.register_fn("nerdctl_image_commit", nerdctl_image_commit);
engine.register_fn("nerdctl_image_build", nerdctl_image_build);
Ok(())
}
/// Register Nerdctl module types with the Rhai engine
fn register_nerdctl_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register Container type
engine.register_type_with_name::<Container>("NerdctlContainer");
engine.register_type_with_name::<HealthCheck>("NerdctlHealthCheck");
// Register getters & setters for HealthCheck properties
engine.register_get("cmd", |hc: &mut HealthCheck| hc.cmd.clone());
engine.register_set("cmd", |hc: &mut HealthCheck, cmd: &str| {
hc.cmd = cmd.to_string();
});
engine.register_get("interval", |hc: &mut HealthCheck| {
hc.interval.clone().unwrap_or_default()
});
engine.register_set("interval", |hc: &mut HealthCheck, interval: &str| {
hc.interval = Some(interval.to_string());
});
engine.register_get("timeout", |hc: &mut HealthCheck| {
hc.timeout.clone().unwrap_or_default()
});
engine.register_set("timeout", |hc: &mut HealthCheck, timeout: &str| {
hc.timeout = Some(timeout.to_string());
});
engine.register_get("retries", |hc: &mut HealthCheck| {
hc.retries.map_or(0, |r| r as i64)
});
engine.register_set("retries", |hc: &mut HealthCheck, retries: i64| {
hc.retries = Some(retries as u32);
});
engine.register_get("start_period", |hc: &mut HealthCheck| {
hc.start_period.clone().unwrap_or_default()
});
engine.register_set("start_period", |hc: &mut HealthCheck, start_period: &str| {
hc.start_period = Some(start_period.to_string());
});
// Register getters & setters for Container properties
// -- name
engine.register_get("name", |container: &mut Container| container.name.clone());
engine.register_set("image", |container: &mut Container, image: &str| {
container.image = Some(image.to_string());
});
// -- container_id
engine.register_get(
"container_id",
|container: &mut Container| match &container.container_id {
Some(id) => id.clone(),
None => "".to_string(),
},
);
engine.register_set("container_id", |container: &mut Container, container_id: &str| {
container.container_id = Some(container_id.to_string());
});
// -- image
engine.register_get("image", |container: &mut Container| {
match &container.image {
Some(img) => img.clone(),
None => "".to_string(),
}
});
engine.register_set("image", |container: &mut Container, image: &str| {
container.image = Some(image.to_string());
});
// -- config
engine.register_get("config", |container: &mut Container| {
container
.config
.iter()
.map(|(k, v)| (k.clone().into(), v.clone().into()))
.collect::<Map>()
});
engine.register_set("config", |container: &mut Container, config: Map| {
container.config = config
.into_iter()
.map(|(k, v)| (k.to_string(), v.into_string().unwrap_or_default()))
.collect();
});
// -- ports
engine.register_get("ports", |container: &mut Container| {
let mut array = Array::new();
for port in &container.ports {
array.push(Dynamic::from(port.clone()));
}
array
});
engine.register_set("ports", |container: &mut Container, ports: Array| {
container.ports = ports
.into_iter()
.map(|v| v.into_string().unwrap_or_default())
.collect();
});
// -- volumes
engine.register_get("volumes", |container: &mut Container| {
let mut array = Array::new();
for volume in &container.volumes {
array.push(Dynamic::from(volume.clone()));
}
array
});
engine.register_set("volumes", |container: &mut Container, volumes: Array| {
container.volumes = volumes
.into_iter()
.map(|v| v.into_string().unwrap_or_default())
.collect();
});
// -- env_vars
engine.register_get("env_vars", |container: &mut Container| {
container
.env_vars
.iter()
.map(|(k, v)| (k.clone().into(), v.clone().into()))
.collect::<Map>()
});
engine.register_set("env_vars", |container: &mut Container, env_vars: Map| {
container.env_vars = env_vars
.into_iter()
.map(|(k, v)| (k.to_string(), v.into_string().unwrap_or_default()))
.collect();
});
// -- network
engine.register_get("network", |container: &mut Container| {
container.network.clone().unwrap_or_default()
});
engine.register_set("network", |container: &mut Container, network: &str| {
container.network = Some(network.to_string());
});
// -- network_aliases
engine.register_get("network_aliases", |container: &mut Container| {
container
.network_aliases
.iter()
.map(|alias| Dynamic::from(alias.clone()))
.collect::<Array>()
});
engine.register_set(
"network_aliases",
|container: &mut Container, aliases: Array| {
container.network_aliases = aliases
.into_iter()
.map(|a| a.into_string().unwrap_or_default())
.collect();
},
);
// -- cpu_limit
engine.register_get("cpu_limit", |container: &mut Container| {
container.cpu_limit.clone().unwrap_or_default()
});
engine.register_set("cpu_limit", |container: &mut Container, limit: &str| {
container.cpu_limit = Some(limit.to_string());
});
// -- memory_limit
engine.register_get("memory_limit", |container: &mut Container| {
container.memory_limit.clone().unwrap_or_default()
});
engine.register_set("memory_limit", |container: &mut Container, limit: &str| {
container.memory_limit = Some(limit.to_string());
});
// -- memory_swap_limit
engine.register_get("memory_swap_limit", |container: &mut Container| {
container.memory_swap_limit.clone().unwrap_or_default()
});
engine.register_set(
"memory_swap_limit",
|container: &mut Container, limit: &str| {
container.memory_swap_limit = Some(limit.to_string());
},
);
// -- cpu_shares
engine.register_get("cpu_shares", |container: &mut Container| {
container.cpu_shares.clone().unwrap_or_default()
});
engine.register_set("cpu_shares", |container: &mut Container, shares: &str| {
container.cpu_shares = Some(shares.to_string());
});
// -- restart_policy
engine.register_get("restart_policy", |container: &mut Container| {
container.restart_policy.clone().unwrap_or_default()
});
engine.register_set(
"restart_policy",
|container: &mut Container, policy: &str| {
container.restart_policy = Some(policy.to_string());
},
);
// TODO: setters and getters for health_check
// -- health_check
// engine.register_get("health_check", |container: &mut Container| {
// container.health_check.clone()
// });
// engine.register_set(
// "health_check",
// |container: &mut Container, health_check: HealthCheck| {
// container.health_check = Some(health_check);
// },
// );
// -- detach
engine.register_get("detach", |container: &mut Container| container.detach);
engine.register_set("detach", |container: &mut Container, detach: bool| {
container.detach = detach;
});
// -- snapshotter
engine.register_get("snapshotter", |container: &mut Container| {
container.snapshotter.clone().unwrap_or_default()
});
engine.register_set("snapshotter", |container: &mut Container, snapshotter: &str| {
container.snapshotter = Some(snapshotter.to_string());
});
// Register Image type and methods
engine.register_type_with_name::<Image>("NerdctlImage");
// Register getters for Image properties
engine.register_get("id", |img: &mut Image| img.id.clone());
engine.register_get("repository", |img: &mut Image| img.repository.clone());
engine.register_get("tag", |img: &mut Image| img.tag.clone());
engine.register_get("size", |img: &mut Image| img.size.clone());
engine.register_get("created", |img: &mut Image| img.created.clone());
Ok(())
}