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();
let global_config = GlobalConfig::load(&paths.global_config())?;
let env_feat_name = EnvFeaturizedName::parse(&args.env)?;
let env = manager.search_result(&env_feat_name.name)?;
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());
}
let engine = match &args.engine {
Some(e) => e.parse::<Engine>()?,
None => global_config.get_container_engine()?,
};
let arch = args.arch.as_deref().or(global_config.arch.as_deref());
let cwd = env::current_dir()?;
check_safe_directory(&cwd)?;
let validated_ports = validate_ports(&args.port)?;
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)?;
if !args.dry_run && !config.templates.is_empty() {
if verbose {
eprintln!("{} Processing templates (run)...", "→".blue());
}
process_templates(&env.path, &config.templates, ProcessContext::Run, verbose)?;
}
let mut mounts = vec![];
let mount_path = config
.settings
.working_dir
.clone()
.unwrap_or_else(|| "/project".to_string());
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
);
}
}
for m in &config.mounts {
mounts.push(MountSpec {
host_path: m.host_path.clone(),
container_path: m.container_path.clone(),
readonly: m.read_only,
});
}
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,
});
}
let network = if args.network != "bridge" {
args.network.clone()
} else {
config.settings.network.clone()
};
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);
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);
}
setup_signal_handler();
let mut child = crate::docker::execute_command_spawn(&engine, run_cmd.args(), &env.path)?;
let status = wait_with_interrupt(&mut child, engine, &cont_name, env)?;
if !status.success() {
crate::docker::handle_docker_exit_codes(status.code())?
}
Ok(())
}
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
)));
}
}
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(())
}
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 => {
validate_port_number(parts[0], port)?;
validated.push(port.clone());
}
2 => {
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)
}
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(())
}
fn setup_signal_handler() {
INTERRUPTED.store(false, Ordering::SeqCst);
}
fn wait_with_interrupt(
child: &mut Child,
engine: Engine,
container_name: &str,
env: Rc<Environment>,
) -> Result<std::process::ExitStatus> {
let status = child.wait().map_err(|e| {
Error::ContainerCommandFailed(format!("Failed to wait for container: {}", e))
})?;
if INTERRUPTED.load(Ordering::SeqCst) {
let _status = crate::docker::execute_command_status(
&engine,
["stop", "-t", "2", container_name],
&env.path,
)?;
}
Ok(status)
}