Skip to main content

sal_virt/rhai/
nerdctl.rs

1//! Rhai wrappers for Nerdctl module functions
2//!
3//! This module provides Rhai wrappers for the functions in the Nerdctl module.
4
5use crate::nerdctl::{self, Container, Image, NerdctlError};
6use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
7use sal_process::CommandResult;
8
9// Helper functions for error conversion with improved context
10fn nerdctl_error_to_rhai_error<T>(
11    result: Result<T, NerdctlError>,
12) -> Result<T, Box<EvalAltResult>> {
13    result.map_err(|e| {
14        // Create a more detailed error message based on the error type
15        let error_message = match &e {
16            NerdctlError::CommandExecutionFailed(io_err) => {
17                format!("Failed to execute nerdctl command: {}. This may indicate nerdctl is not installed or not in PATH.", io_err)
18            },
19            NerdctlError::CommandFailed(msg) => {
20                format!("Nerdctl command failed: {}. Check container status and logs for more details.", msg)
21            },
22            NerdctlError::JsonParseError(msg) => {
23                format!("Failed to parse nerdctl JSON output: {}. This may indicate an incompatible nerdctl version.", msg)
24            },
25            NerdctlError::ConversionError(msg) => {
26                format!("Data conversion error: {}. This may indicate unexpected output format from nerdctl.", msg)
27            },
28            NerdctlError::Other(msg) => {
29                format!("Nerdctl error: {}. This is an unexpected error.", msg)
30            },
31        };
32        Box::new(EvalAltResult::ErrorRuntime(
33            error_message.into(),
34            rhai::Position::NONE
35        ))
36    })
37}
38
39//
40// Container Builder Pattern Implementation
41//
42
43/// Create a new Container
44pub fn container_new(name: &str) -> Result<Container, Box<EvalAltResult>> {
45    nerdctl_error_to_rhai_error(Container::new(name))
46}
47
48/// Create a Container from an image
49pub fn container_from_image(name: &str, image: &str) -> Result<Container, Box<EvalAltResult>> {
50    nerdctl_error_to_rhai_error(Container::from_image(name, image))
51}
52
53/// Reset the container configuration to defaults while keeping the name and image
54pub fn container_reset(container: Container) -> Container {
55    container.reset()
56}
57
58/// Add a port mapping to a Container
59pub fn container_with_port(container: Container, port: &str) -> Container {
60    container.with_port(port)
61}
62
63/// Add a volume mount to a Container
64pub fn container_with_volume(container: Container, volume: &str) -> Container {
65    container.with_volume(volume)
66}
67
68/// Add an environment variable to a Container
69pub fn container_with_env(container: Container, key: &str, value: &str) -> Container {
70    container.with_env(key, value)
71}
72
73/// Set the network for a Container
74pub fn container_with_network(container: Container, network: &str) -> Container {
75    container.with_network(network)
76}
77
78/// Add a network alias to a Container
79pub fn container_with_network_alias(container: Container, alias: &str) -> Container {
80    container.with_network_alias(alias)
81}
82
83/// Set CPU limit for a Container
84pub fn container_with_cpu_limit(container: Container, cpus: &str) -> Container {
85    container.with_cpu_limit(cpus)
86}
87
88/// Set memory limit for a Container
89pub fn container_with_memory_limit(container: Container, memory: &str) -> Container {
90    container.with_memory_limit(memory)
91}
92
93/// Set restart policy for a Container
94pub fn container_with_restart_policy(container: Container, policy: &str) -> Container {
95    container.with_restart_policy(policy)
96}
97
98/// Set health check for a Container
99pub fn container_with_health_check(container: Container, cmd: &str) -> Container {
100    container.with_health_check(cmd)
101}
102
103/// Add multiple port mappings to a Container
104pub fn container_with_ports(mut container: Container, ports: Array) -> Container {
105    for port in ports.iter() {
106        if port.is_string() {
107            let port_str = port.clone().cast::<String>();
108            container = container.with_port(&port_str);
109        }
110    }
111    container
112}
113
114/// Add multiple volume mounts to a Container
115pub fn container_with_volumes(mut container: Container, volumes: Array) -> Container {
116    for volume in volumes.iter() {
117        if volume.is_string() {
118            let volume_str = volume.clone().cast::<String>();
119            container = container.with_volume(&volume_str);
120        }
121    }
122    container
123}
124
125/// Add multiple environment variables to a Container
126pub fn container_with_envs(mut container: Container, env_map: Map) -> Container {
127    for (key, value) in env_map.iter() {
128        if value.is_string() {
129            let value_str = value.clone().cast::<String>();
130            container = container.with_env(&key, &value_str);
131        }
132    }
133    container
134}
135
136/// Add multiple network aliases to a Container
137pub fn container_with_network_aliases(mut container: Container, aliases: Array) -> Container {
138    for alias in aliases.iter() {
139        if alias.is_string() {
140            let alias_str = alias.clone().cast::<String>();
141            container = container.with_network_alias(&alias_str);
142        }
143    }
144    container
145}
146
147/// Set memory swap limit for a Container
148pub fn container_with_memory_swap_limit(container: Container, memory_swap: &str) -> Container {
149    container.with_memory_swap_limit(memory_swap)
150}
151
152/// Set CPU shares for a Container
153pub fn container_with_cpu_shares(container: Container, shares: &str) -> Container {
154    container.with_cpu_shares(shares)
155}
156
157/// Set health check with options for a Container
158pub fn container_with_health_check_options(
159    container: Container,
160    cmd: &str,
161    interval: Option<&str>,
162    timeout: Option<&str>,
163    retries: Option<i64>,
164    start_period: Option<&str>,
165) -> Container {
166    // Convert i64 to u32 for retries
167    let retries_u32 = retries.map(|r| r as u32);
168    container.with_health_check_options(cmd, interval, timeout, retries_u32, start_period)
169}
170
171/// Set snapshotter for a Container
172pub fn container_with_snapshotter(container: Container, snapshotter: &str) -> Container {
173    container.with_snapshotter(snapshotter)
174}
175
176/// Set detach mode for a Container
177pub fn container_with_detach(container: Container, detach: bool) -> Container {
178    container.with_detach(detach)
179}
180
181/// Build and run the Container
182///
183/// This function builds and runs the container using the configured options.
184/// It provides detailed error information if the build fails.
185pub fn container_build(container: Container) -> Result<Container, Box<EvalAltResult>> {
186    // Get container details for better error reporting
187    let container_name = container.name.clone();
188    let image = container
189        .image
190        .clone()
191        .unwrap_or_else(|| "none".to_string());
192    let ports = container.ports.clone();
193    let volumes = container.volumes.clone();
194    let env_vars = container.env_vars.clone();
195
196    // Try to build the container
197    let build_result = container.build();
198
199    // Handle the result with improved error context
200    match build_result {
201        Ok(built_container) => {
202            // Container built successfully
203            Ok(built_container)
204        }
205        Err(err) => {
206            // Add more context to the error
207            let enhanced_error = match err {
208                NerdctlError::CommandFailed(msg) => {
209                    // Provide more detailed error information
210                    let mut enhanced_msg = format!(
211                        "Failed to build container '{}' from image '{}': {}",
212                        container_name, image, msg
213                    );
214
215                    // Add information about configured options that might be relevant
216                    if !ports.is_empty() {
217                        enhanced_msg.push_str(&format!("\nConfigured ports: {:?}", ports));
218                    }
219
220                    if !volumes.is_empty() {
221                        enhanced_msg.push_str(&format!("\nConfigured volumes: {:?}", volumes));
222                    }
223
224                    if !env_vars.is_empty() {
225                        enhanced_msg.push_str(&format!(
226                            "\nConfigured environment variables: {:?}",
227                            env_vars
228                        ));
229                    }
230
231                    // Add suggestions for common issues
232                    if msg.contains("not found") || msg.contains("no such image") {
233                        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().");
234                    } else if msg.contains("port is already allocated") {
235                        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.");
236                    } else if msg.contains("permission denied") {
237                        enhanced_msg.push_str("\nSuggestion: Permission issues detected. Check if you have the necessary permissions to create containers or access the specified volumes.");
238                    }
239
240                    NerdctlError::CommandFailed(enhanced_msg)
241                }
242                _ => err,
243            };
244
245            nerdctl_error_to_rhai_error(Err(enhanced_error))
246        }
247    }
248}
249
250/// Start the Container and verify it's running
251///
252/// This function starts the container and verifies that it's actually running.
253/// It returns detailed error information if the container fails to start or
254/// if it starts but stops immediately.
255pub fn container_start(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> {
256    // Get container details for better error reporting
257    let container_name = container.name.clone();
258    let container_id = container
259        .container_id
260        .clone()
261        .unwrap_or_else(|| "unknown".to_string());
262
263    // Try to start the container
264    let start_result = container.start();
265
266    // Handle the result with improved error context
267    match start_result {
268        Ok(result) => {
269            // Container started successfully
270            Ok(result)
271        }
272        Err(err) => {
273            // Add more context to the error
274            let enhanced_error = match err {
275                NerdctlError::CommandFailed(msg) => {
276                    // Check if this is a "container already running" error, which is not really an error
277                    if msg.contains("already running") {
278                        return Ok(CommandResult {
279                            stdout: format!("Container {} is already running", container_name),
280                            stderr: "".to_string(),
281                            success: true,
282                            code: 0,
283                        });
284                    }
285
286                    // Try to get more information about why the container might have failed to start
287                    let mut enhanced_msg = format!(
288                        "Failed to start container '{}' (ID: {}): {}",
289                        container_name, container_id, msg
290                    );
291
292                    // Try to check if the image exists
293                    if let Some(image) = &container.image {
294                        enhanced_msg.push_str(&format!("\nContainer was using image: {}", image));
295                    }
296
297                    NerdctlError::CommandFailed(enhanced_msg)
298                }
299                _ => err,
300            };
301
302            nerdctl_error_to_rhai_error(Err(enhanced_error))
303        }
304    }
305}
306
307/// Stop the Container
308pub fn container_stop(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> {
309    nerdctl_error_to_rhai_error(container.stop())
310}
311
312/// Remove the Container
313pub fn container_remove(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> {
314    nerdctl_error_to_rhai_error(container.remove())
315}
316
317/// Execute a command in the Container
318pub fn container_exec(
319    container: &mut Container,
320    command: &str,
321) -> Result<CommandResult, Box<EvalAltResult>> {
322    nerdctl_error_to_rhai_error(container.exec(command))
323}
324
325/// Get container logs
326pub fn container_logs(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> {
327    // Get container details for better error reporting
328    let container_name = container.name.clone();
329    let container_id = container
330        .container_id
331        .clone()
332        .unwrap_or_else(|| "unknown".to_string());
333
334    // Use the nerdctl::logs function
335    let logs_result = nerdctl::logs(&container_id);
336
337    match logs_result {
338        Ok(result) => Ok(result),
339        Err(err) => {
340            // Add more context to the error
341            let enhanced_error = NerdctlError::CommandFailed(format!(
342                "Failed to get logs for container '{}' (ID: {}): {}",
343                container_name, container_id, err
344            ));
345
346            nerdctl_error_to_rhai_error(Err(enhanced_error))
347        }
348    }
349}
350
351/// Copy files between the Container and local filesystem
352pub fn container_copy(
353    container: &mut Container,
354    source: &str,
355    dest: &str,
356) -> Result<CommandResult, Box<EvalAltResult>> {
357    nerdctl_error_to_rhai_error(container.copy(source, dest))
358}
359
360/// Create a new Map with default run options
361pub fn new_run_options() -> Map {
362    let mut map = Map::new();
363    map.insert("name".into(), Dynamic::UNIT);
364    map.insert("detach".into(), Dynamic::from(true));
365    map.insert("ports".into(), Dynamic::from(Array::new()));
366    map.insert("snapshotter".into(), Dynamic::from("native"));
367    map
368}
369
370//
371// Container Function Wrappers
372//
373
374/// Wrapper for nerdctl::run
375///
376/// Run a container from an image.
377pub fn nerdctl_run(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
378    nerdctl_error_to_rhai_error(nerdctl::run(image, None, true, None, None))
379}
380
381/// Run a container with a name
382pub fn nerdctl_run_with_name(image: &str, name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
383    nerdctl_error_to_rhai_error(nerdctl::run(image, Some(name), true, None, None))
384}
385
386/// Run a container with a port mapping
387pub fn nerdctl_run_with_port(
388    image: &str,
389    name: &str,
390    port: &str,
391) -> Result<CommandResult, Box<EvalAltResult>> {
392    let ports = vec![port];
393    nerdctl_error_to_rhai_error(nerdctl::run(image, Some(name), true, Some(&ports), None))
394}
395
396/// Wrapper for nerdctl::exec
397///
398/// Execute a command in a container.
399pub fn nerdctl_exec(container: &str, command: &str) -> Result<CommandResult, Box<EvalAltResult>> {
400    nerdctl_error_to_rhai_error(nerdctl::exec(container, command))
401}
402
403/// Wrapper for nerdctl::copy
404///
405/// Copy files between container and local filesystem.
406pub fn nerdctl_copy(source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> {
407    nerdctl_error_to_rhai_error(nerdctl::copy(source, dest))
408}
409
410/// Wrapper for nerdctl::stop
411///
412/// Stop a container.
413pub fn nerdctl_stop(container: &str) -> Result<CommandResult, Box<EvalAltResult>> {
414    nerdctl_error_to_rhai_error(nerdctl::stop(container))
415}
416
417/// Wrapper for nerdctl::remove
418///
419/// Remove a container.
420pub fn nerdctl_remove(container: &str) -> Result<CommandResult, Box<EvalAltResult>> {
421    nerdctl_error_to_rhai_error(nerdctl::remove(container))
422}
423
424/// Wrapper for nerdctl::list
425///
426/// List containers.
427pub fn nerdctl_list(all: bool) -> Result<CommandResult, Box<EvalAltResult>> {
428    nerdctl_error_to_rhai_error(nerdctl::list(all))
429}
430
431/// Wrapper for nerdctl::logs
432///
433/// Get container logs.
434pub fn nerdctl_logs(container: &str) -> Result<CommandResult, Box<EvalAltResult>> {
435    nerdctl_error_to_rhai_error(nerdctl::logs(container))
436}
437
438//
439// Image Function Wrappers
440//
441
442/// Wrapper for nerdctl::images
443///
444/// List images in local storage.
445pub fn nerdctl_images() -> Result<CommandResult, Box<EvalAltResult>> {
446    nerdctl_error_to_rhai_error(nerdctl::images())
447}
448
449/// Wrapper for nerdctl::image_remove
450///
451/// Remove one or more images.
452pub fn nerdctl_image_remove(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
453    nerdctl_error_to_rhai_error(nerdctl::image_remove(image))
454}
455
456/// Wrapper for nerdctl::image_push
457///
458/// Push an image to a registry.
459pub fn nerdctl_image_push(
460    image: &str,
461    destination: &str,
462) -> Result<CommandResult, Box<EvalAltResult>> {
463    nerdctl_error_to_rhai_error(nerdctl::image_push(image, destination))
464}
465
466/// Wrapper for nerdctl::image_tag
467///
468/// Add an additional name to a local image.
469pub fn nerdctl_image_tag(image: &str, new_name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
470    nerdctl_error_to_rhai_error(nerdctl::image_tag(image, new_name))
471}
472
473/// Wrapper for nerdctl::image_pull
474///
475/// Pull an image from a registry.
476pub fn nerdctl_image_pull(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
477    nerdctl_error_to_rhai_error(nerdctl::image_pull(image))
478}
479
480/// Wrapper for nerdctl::image_commit
481///
482/// Commit a container to an image.
483pub fn nerdctl_image_commit(
484    container: &str,
485    image_name: &str,
486) -> Result<CommandResult, Box<EvalAltResult>> {
487    nerdctl_error_to_rhai_error(nerdctl::image_commit(container, image_name))
488}
489
490/// Wrapper for nerdctl::image_build
491///
492/// Build an image using a Dockerfile.
493pub fn nerdctl_image_build(
494    tag: &str,
495    context_path: &str,
496) -> Result<CommandResult, Box<EvalAltResult>> {
497    nerdctl_error_to_rhai_error(nerdctl::image_build(tag, context_path))
498}
499
500/// Register Nerdctl module functions with the Rhai engine
501///
502/// # Arguments
503///
504/// * `engine` - The Rhai engine to register the functions with
505///
506/// # Returns
507///
508/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
509pub fn register_nerdctl_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
510    // Register types
511    register_nerdctl_types(engine)?;
512
513    // Register Container constructor
514    engine.register_fn("nerdctl_container_new", container_new);
515    engine.register_fn("nerdctl_container_from_image", container_from_image);
516
517    // Register Container instance methods
518    engine.register_fn("reset", container_reset);
519    engine.register_fn("with_port", container_with_port);
520    engine.register_fn("with_volume", container_with_volume);
521    engine.register_fn("with_env", container_with_env);
522    engine.register_fn("with_network", container_with_network);
523    engine.register_fn("with_network_alias", container_with_network_alias);
524    engine.register_fn("with_cpu_limit", container_with_cpu_limit);
525    engine.register_fn("with_memory_limit", container_with_memory_limit);
526    engine.register_fn("with_restart_policy", container_with_restart_policy);
527    engine.register_fn("with_health_check", container_with_health_check);
528    engine.register_fn("with_ports", container_with_ports);
529    engine.register_fn("with_volumes", container_with_volumes);
530    engine.register_fn("with_envs", container_with_envs);
531    engine.register_fn("with_network_aliases", container_with_network_aliases);
532    engine.register_fn("with_memory_swap_limit", container_with_memory_swap_limit);
533    engine.register_fn("with_cpu_shares", container_with_cpu_shares);
534    engine.register_fn(
535        "with_health_check_options",
536        container_with_health_check_options,
537    );
538    engine.register_fn("with_snapshotter", container_with_snapshotter);
539    engine.register_fn("with_detach", container_with_detach);
540    engine.register_fn("build", container_build);
541    engine.register_fn("start", container_start);
542    engine.register_fn("stop", container_stop);
543    engine.register_fn("remove", container_remove);
544    engine.register_fn("exec", container_exec);
545    engine.register_fn("logs", container_logs);
546    engine.register_fn("copy", container_copy);
547
548    // Register legacy container functions (for backward compatibility)
549    engine.register_fn("nerdctl_run", nerdctl_run);
550    engine.register_fn("nerdctl_run_with_name", nerdctl_run_with_name);
551    engine.register_fn("nerdctl_run_with_port", nerdctl_run_with_port);
552    engine.register_fn("new_run_options", new_run_options);
553    engine.register_fn("nerdctl_exec", nerdctl_exec);
554    engine.register_fn("nerdctl_copy", nerdctl_copy);
555    engine.register_fn("nerdctl_stop", nerdctl_stop);
556    engine.register_fn("nerdctl_remove", nerdctl_remove);
557    engine.register_fn("nerdctl_list", nerdctl_list);
558    engine.register_fn("nerdctl_logs", nerdctl_logs);
559
560    // Register image functions
561    engine.register_fn("nerdctl_images", nerdctl_images);
562    engine.register_fn("nerdctl_image_remove", nerdctl_image_remove);
563    engine.register_fn("nerdctl_image_push", nerdctl_image_push);
564    engine.register_fn("nerdctl_image_tag", nerdctl_image_tag);
565    engine.register_fn("nerdctl_image_pull", nerdctl_image_pull);
566    engine.register_fn("nerdctl_image_commit", nerdctl_image_commit);
567    engine.register_fn("nerdctl_image_build", nerdctl_image_build);
568
569    Ok(())
570}
571
572/// Register Nerdctl module types with the Rhai engine
573fn register_nerdctl_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
574    // Register Container type
575    engine.register_type_with_name::<Container>("NerdctlContainer");
576
577    // Register getters for Container properties
578    engine.register_get("name", |container: &mut Container| container.name.clone());
579    engine.register_get(
580        "container_id",
581        |container: &mut Container| match &container.container_id {
582            Some(id) => id.clone(),
583            None => "".to_string(),
584        },
585    );
586    engine.register_get("image", |container: &mut Container| {
587        match &container.image {
588            Some(img) => img.clone(),
589            None => "".to_string(),
590        }
591    });
592    engine.register_get("ports", |container: &mut Container| {
593        let mut array = Array::new();
594        for port in &container.ports {
595            array.push(Dynamic::from(port.clone()));
596        }
597        array
598    });
599    engine.register_get("volumes", |container: &mut Container| {
600        let mut array = Array::new();
601        for volume in &container.volumes {
602            array.push(Dynamic::from(volume.clone()));
603        }
604        array
605    });
606    engine.register_get("detach", |container: &mut Container| container.detach);
607
608    // Register Image type and methods
609    engine.register_type_with_name::<Image>("NerdctlImage");
610
611    // Register getters for Image properties
612    engine.register_get("id", |img: &mut Image| img.id.clone());
613    engine.register_get("repository", |img: &mut Image| img.repository.clone());
614    engine.register_get("tag", |img: &mut Image| img.tag.clone());
615    engine.register_get("size", |img: &mut Image| img.size.clone());
616    engine.register_get("created", |img: &mut Image| img.created.clone());
617
618    Ok(())
619}