flyboat 2.0.0

Container environment manager for development
Documentation
use crate::cli::{BuildArgs, RunArgs};
use crate::config::GlobalConfig;
use crate::default_container_name;
use crate::docker::Engine;
use crate::docker::build::image_name;
use crate::docker::run::{MountSpec, RunCommand, container_name};
use crate::environment::{Environment, EnvironmentManager};
use crate::template::{ProcessContext, process_templates};
use crate::{EnvFeaturizedName, Error, Result};
use colored::Colorize;
use std::env;
use std::path::Path;
use std::process::Child;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};

static INTERRUPTED: AtomicBool = AtomicBool::new(false);

pub fn execute_run(args: &RunArgs, verbose: bool) -> Result<()> {
    let manager = EnvironmentManager::new()?;
    let paths = manager.paths();

    // Load global config for defaults
    let global_config = GlobalConfig::load(&paths.global_config())?;

    // Parse environment name with features (name+features)
    let env_feat_name = EnvFeaturizedName::parse(&args.env)?;

    // Find environment
    let env = manager.search_result(&env_feat_name.name)?;

    // Clone config and apply features
    let mut config = env.config.clone();
    if !env_feat_name.features.is_empty() {
        eprintln!("Taking base configuration '{}'", env_feat_name.name.green());
        for feature in &env_feat_name.features {
            eprintln!("  - Applying feature '{}'", feature.cyan());
        }
        config.apply_features(&env_feat_name.features)?;
    }

    if verbose {
        eprintln!("Running environment: {}", env.name.green().bold());
    }

    // Determine engine
    let engine = match &args.engine {
        Some(e) => e.parse::<Engine>()?,
        None => global_config.get_container_engine()?,
    };

    // Determine architecture
    let arch = args.arch.as_deref().or(global_config.arch.as_deref());

    // Security check: prevent running from dangerous directories
    let cwd = env::current_dir()?;
    check_safe_directory(&cwd)?;

    // Validate ports early (before any output)
    let validated_ports = validate_ports(&args.port)?;

    // Build using execute_build
    let build_args = BuildArgs {
        env: args.env.clone(),
        arch: args.arch.clone(),
        engine: args.engine.clone(),
        rebuild: args.rebuild,
        dry_run: args.dry_run,
    };
    super::build::execute_build(&build_args, verbose)?;

    // Process templates (run context) - Build templates handled by execute_build
    if !args.dry_run && !config.templates.is_empty() {
        if verbose {
            eprintln!("{} Processing templates (run)...", "→".blue());
        }
        process_templates(&env.path, &config.templates, ProcessContext::Run, verbose)?;
    }

    // Prepare mounts
    let mut mounts = vec![];

    // Default mount path when working_dir is not specified
    let mount_path = config
        .settings
        .working_dir
        .clone()
        .unwrap_or_else(|| "/project".to_string());

    // Mount current directory based on disk_access mode
    match config.settings.disk_access.as_str() {
        "mount" => {
            mounts.push(MountSpec {
                host_path: cwd.display().to_string(),
                container_path: mount_path.clone(),
                readonly: false,
            });
        }
        "readonly" => {
            mounts.push(MountSpec {
                host_path: cwd.display().to_string(),
                container_path: mount_path.clone(),
                readonly: true,
            });
        }
        "tmpfs" => unimplemented!("tmpfs has not been implemented yet."),
        "none" => {}
        other => {
            eprintln!(
                "{}: Unknown disk_access '{}', defaulting to 'none'",
                "Warning".yellow(),
                other
            );
        }
    }

    // Add custom mounts from dev_env.yaml
    for m in &config.mounts {
        mounts.push(MountSpec {
            host_path: m.host_path.clone(),
            container_path: m.container_path.clone(),
            readonly: m.read_only,
        });
    }

    // Create artifacts folder if enabled
    if config.settings.artifacts_folder {
        let artifacts_path = cwd.join("artifacts");
        if !artifacts_path.exists() {
            std::fs::create_dir_all(&artifacts_path)?;
        }
        mounts.push(MountSpec {
            host_path: artifacts_path.display().to_string(),
            container_path: "/artifacts".to_string(),
            readonly: false,
        });
    }

    // Determine network
    let network = if args.network != "bridge" {
        args.network.clone()
    } else {
        config.settings.network.clone()
    };

    // Generate names
    let img_name = image_name(&env.name, arch);
    let name_postfix = args.name.clone().unwrap_or_else(default_container_name);
    let cont_name = container_name(&env.name, arch, &name_postfix);

    // Build run command
    let run_cmd = RunCommand {
        engine,
        image_name: img_name.clone(),
        container_name: cont_name.clone(),
        working_dir: config.settings.working_dir.clone(),
        network,
        ports: validated_ports,
        mounts,
        custom_args: config.custom_args.clone(),
        entrypoint: args.entrypoint.clone(),
        interactive: true,
        remove_on_exit: true,
    };

    if args.dry_run {
        println!("  Run:   {}", run_cmd);
        return Ok(());
    }

    println!(
        "{} container {}",
        "Starting".blue(),
        cont_name.green().bold()
    );

    if verbose {
        eprintln!("  Command: {}", run_cmd);
    }

    // Set up signal handling
    setup_signal_handler();

    // Execute run with signal handling
    let mut child = crate::docker::execute_command_spawn(&engine, run_cmd.args(), &env.path)?;

    // Wait for container with interrupt handling
    let status = wait_with_interrupt(&mut child, engine, &cont_name, env)?;

    if !status.success() {
        crate::docker::handle_docker_exit_codes(status.code())?
    }

    Ok(())
}

/// Check that we're not running from a dangerous directory
fn check_safe_directory(path: &Path) -> Result<()> {
    let path_str = path.display().to_string();

    let dangerous_paths = ["/", "/root", "/home"];

    for dangerous in &dangerous_paths {
        if path_str == *dangerous {
            return Err(Error::SecurityViolation(format!(
                "Cannot run flyboat from '{}'. Please cd to a project directory.",
                dangerous
            )));
        }
    }

    // Also check if it's the home directory
    if let Ok(home) = env::var("HOME")
        && path_str == home
    {
        return Err(Error::SecurityViolation(
            "Cannot run flyboat from home directory. Please cd to a project directory.".to_string(),
        ));
    }

    Ok(())
}

/// Validate port specifications
/// Accepts: "8080" or "8080:80" (host:container)
fn validate_ports(ports: &[String]) -> Result<Vec<String>> {
    let mut validated = Vec::new();

    for port in ports {
        let parts: Vec<&str> = port.split(':').collect();

        match parts.len() {
            1 => {
                // Single port: "8080"
                validate_port_number(parts[0], port)?;
                validated.push(port.clone());
            }
            2 => {
                // Port mapping: "8080:80"
                validate_port_number(parts[0], port)?;
                validate_port_number(parts[1], port)?;
                validated.push(port.clone());
            }
            _ => {
                return Err(Error::InvalidPort(format!(
                    "'{}' - use PORT or HOST_PORT:CONTAINER_PORT format",
                    port
                )));
            }
        }
    }

    Ok(validated)
}

/// Validate a single port number
fn validate_port_number(port_str: &str, original: &str) -> Result<()> {
    let port: u16 = port_str.parse().map_err(|_| {
        Error::InvalidPort(format!(
            "'{}' - '{}' is not a valid port number",
            original, port_str
        ))
    })?;

    if port == 0 {
        return Err(Error::InvalidPort(format!(
            "'{}' - port must be between 1 and 65535",
            original
        )));
    }

    Ok(())
}

/// Set up signal handler for Ctrl+C
fn setup_signal_handler() {
    // Reset interrupted flag
    INTERRUPTED.store(false, Ordering::SeqCst);

    // Note: We rely on the terminal passing SIGINT to the child process
    // The container will receive the signal and handle cleanup
}

/// Wait for child process with interrupt handling
fn wait_with_interrupt(
    child: &mut Child,
    engine: Engine,
    container_name: &str,
    env: Rc<Environment>,
) -> Result<std::process::ExitStatus> {
    // Wait for the child process
    let status = child.wait().map_err(|e| {
        Error::ContainerCommandFailed(format!("Failed to wait for container: {}", e))
    })?;

    // If we were interrupted but container is still running, stop it
    if INTERRUPTED.load(Ordering::SeqCst) {
        let _status = crate::docker::execute_command_status(
            &engine,
            ["stop", "-t", "2", container_name],
            &env.path,
        )?;
    }

    Ok(status)
}