diff --git a/Cargo.lock b/Cargo.lock index e805e2e7..9845d56f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,6 +304,7 @@ dependencies = [ "serde_yaml 0.9.34+deprecated", "signal-hook", "syntect", + "tempdir", "typed-builder", "which", ] diff --git a/Cargo.toml b/Cargo.toml index b908a3fa..285976eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ once_cell = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" +tempdir = "0.3" typed-builder = "0.18" uuid = { version = "1", features = ["v4"] } @@ -69,7 +70,6 @@ rayon = { version = "1.10.0", optional = true } requestty = { version = "0.5", features = ["macros", "termion"] } semver = { version = "1", features = ["serde"] } shadow-rs = "0.26" -tempdir = "0.3" urlencoding = "2" users = "0.11" @@ -85,6 +85,7 @@ once_cell.workspace = true serde.workspace = true serde_json.workspace = true serde_yaml.workspace = true +tempdir.workspace = true typed-builder.workspace = true uuid.workspace = true diff --git a/src/commands.rs b/src/commands.rs index bb99f520..2e77035b 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -123,17 +123,6 @@ pub enum CommandArgs { #[derive(Default, Clone, Copy, Debug, TypedBuilder, Args)] pub struct DriverArgs { - /// Runs all instructions inside one layer of the final image. - /// - /// WARN: This doesn't work with the - /// docker driver as it has been deprecated. - /// - /// NOTE: Squash has a performance benefit for - /// podman and buildah when running inside a container. - #[arg(short, long)] - #[builder(default)] - squash: bool, - /// Select which driver to use to build /// your image. #[builder(default)] @@ -146,3 +135,15 @@ pub struct DriverArgs { #[arg(short = 'I', long)] inspect_driver: Option, } + +#[cfg(test)] +mod test { + use clap::CommandFactory; + + use super::BlueBuildArgs; + + #[test] + fn test_cli() { + BlueBuildArgs::command().debug_assert(); + } +} diff --git a/src/commands/build.rs b/src/commands/build.rs index 183284b8..5a264130 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -117,6 +117,17 @@ pub struct BuildCommand { #[builder(default)] no_sign: bool, + /// Runs all instructions inside one layer of the final image. + /// + /// WARN: This doesn't work with the + /// docker driver as it has been deprecated. + /// + /// NOTE: Squash has a performance benefit for + /// podman and buildah when running inside a container. + #[arg(short, long)] + #[builder(default)] + squash: bool, + #[clap(flatten)] #[builder(default)] drivers: DriverArgs, @@ -134,7 +145,7 @@ impl BlueBuildCommand for BuildCommand { .build_driver(self.drivers.build_driver) .inspect_driver(self.drivers.inspect_driver) .build() - .init()?; + .init(); self.update_gitignore()?; @@ -171,7 +182,7 @@ impl BlueBuildCommand for BuildCommand { GenerateCommand::builder() .output(generate_containerfile_path(recipe)?) .recipe(recipe) - .drivers(DriverArgs::builder().squash(self.drivers.squash).build()) + .drivers(self.drivers) .build() .try_run() })?; @@ -195,7 +206,7 @@ impl BlueBuildCommand for BuildCommand { GenerateCommand::builder() .output(generate_containerfile_path(&recipe_path)?) .recipe(&recipe_path) - .drivers(DriverArgs::builder().squash(self.drivers.squash).build()) + .drivers(self.drivers) .build() .try_run()?; @@ -227,7 +238,7 @@ impl BuildCommand { archive_dir.to_string_lossy().trim_end_matches('/'), recipe.name.to_lowercase().replace('/', "_"), )) - .squash(self.drivers.squash) + .squash(self.squash) .build() } else { BuildTagPushOpts::builder() @@ -238,7 +249,7 @@ impl BuildCommand { .no_retry_push(self.no_retry_push) .retry_count(self.retry_count) .compression(self.compression_format) - .squash(self.drivers.squash) + .squash(self.squash) .build() }; @@ -273,7 +284,7 @@ impl BuildCommand { archive_dir.to_string_lossy().trim_end_matches('/'), recipe.name.to_lowercase().replace('/', "_"), )) - .squash(self.drivers.squash) + .squash(self.squash) .build() } else { BuildTagPushOpts::builder() @@ -284,7 +295,7 @@ impl BuildCommand { .no_retry_push(self.no_retry_push) .retry_count(self.retry_count) .compression(self.compression_format) - .squash(self.drivers.squash) + .squash(self.squash) .build() }; diff --git a/src/commands/generate.rs b/src/commands/generate.rs index 80a8a70a..8fed2870 100644 --- a/src/commands/generate.rs +++ b/src/commands/generate.rs @@ -77,7 +77,7 @@ impl BlueBuildCommand for GenerateCommand { .build_driver(self.drivers.build_driver) .inspect_driver(self.drivers.inspect_driver) .build() - .init()?; + .init(); self.template_file() } diff --git a/src/commands/switch.rs b/src/commands/switch.rs index 4d062dd2..79f4c766 100644 --- a/src/commands/switch.rs +++ b/src/commands/switch.rs @@ -55,7 +55,7 @@ impl BlueBuildCommand for SwitchCommand { .build_driver(self.drivers.build_driver) .inspect_driver(self.drivers.inspect_driver) .build() - .init()?; + .init(); let status = RpmOstreeStatus::try_new()?; trace!("{status:?}"); diff --git a/src/credentials.rs b/src/credentials.rs index 4357167c..0376c596 100644 --- a/src/credentials.rs +++ b/src/credentials.rs @@ -1,6 +1,5 @@ use std::{env, sync::Mutex}; -use anyhow::{anyhow, Result}; use blue_build_utils::constants::{ CI_REGISTRY, CI_REGISTRY_PASSWORD, CI_REGISTRY_USER, GITHUB_ACTIONS, GITHUB_ACTOR, GITHUB_TOKEN, }; @@ -107,29 +106,23 @@ static ENV_CREDENTIALS: Lazy> = Lazy::new(|| { /// any strategy that requires credentials as /// the environment credentials are lazy allocated. /// -/// # Errors -/// Will error if it can't lock the mutex. +/// # Panics +/// Will panic if it can't lock the mutex. pub fn set_user_creds( username: Option<&String>, password: Option<&String>, registry: Option<&String>, -) -> Result<()> { +) { trace!("credentials::set({username:?}, password, {registry:?})"); - let mut creds_lock = USER_CREDS - .lock() - .map_err(|e| anyhow!("Failed to set credentials: {e}"))?; + let mut creds_lock = USER_CREDS.lock().expect("Must lock USER_CREDS"); creds_lock.username = username.map(ToOwned::to_owned); creds_lock.password = password.map(ToOwned::to_owned); creds_lock.registry = registry.map(ToOwned::to_owned); drop(creds_lock); let _ = ENV_CREDENTIALS.as_ref(); - Ok(()) } /// Get the credentials for the current set of actions. -/// -/// # Errors -/// Will error if there aren't any credentials available. pub fn get() -> Option<&'static Credentials> { trace!("credentials::get()"); ENV_CREDENTIALS.as_ref() diff --git a/src/drivers.rs b/src/drivers.rs index 43ca5792..724214f8 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -6,6 +6,7 @@ use std::{ collections::{hash_map::Entry, HashMap}, + process::{ExitStatus, Output}, sync::{Arc, Mutex}, }; @@ -23,10 +24,10 @@ use crate::{credentials, image_metadata::ImageMetadata}; use self::{ buildah_driver::BuildahDriver, docker_driver::DockerDriver, - opts::{BuildOpts, BuildTagPushOpts, GetMetadataOpts, PushOpts, TagOpts}, + opts::{BuildOpts, BuildTagPushOpts, GetMetadataOpts, PushOpts, RunOpts, TagOpts}, podman_driver::PodmanDriver, skopeo_driver::SkopeoDriver, - types::{BuildDriverType, InspectDriverType}, + types::{BuildDriverType, InspectDriverType, RunDriverType}, }; mod buildah_driver; @@ -36,10 +37,11 @@ mod podman_driver; mod skopeo_driver; pub mod types; -static INIT: Lazy> = Lazy::new(|| Mutex::new(())); +static INIT: Lazy> = Lazy::new(|| Mutex::new(false)); static SELECTED_BUILD_DRIVER: Lazy>> = Lazy::new(|| Mutex::new(None)); static SELECTED_INSPECT_DRIVER: Lazy>> = Lazy::new(|| Mutex::new(None)); +static SELECTED_RUN_DRIVER: Lazy>> = Lazy::new(|| Mutex::new(None)); /// Stores the build driver. /// @@ -75,7 +77,7 @@ static BUILD_DRIVER: Lazy> = Lazy::new(|| { /// /// # Panics /// -/// This will cause a panic if a build strategy could +/// This will cause a panic if a inspect strategy could /// not be determined. static INSPECT_DRIVER: Lazy> = Lazy::new(|| { let driver = SELECTED_INSPECT_DRIVER.lock().unwrap(); @@ -91,6 +93,30 @@ static INSPECT_DRIVER: Lazy> = Lazy::new(|| { ) }); +/// Stores the run driver. +/// +/// This will, on load, find the best way to run containers in the +/// current environment. Once that strategy is determined, +/// it will be available for any part of the program to call +/// on to perform inspections. +/// +/// # Panics +/// +/// This will cause a panic if a run strategy could +/// not be determined. +static RUN_DRIVER: Lazy> = Lazy::new(|| { + let driver = SELECTED_RUN_DRIVER.lock().unwrap(); + driver.map_or_else( + || panic!("Driver needs to be initialized"), + |driver| -> Arc { + match driver { + RunDriverType::Podman => Arc::new(PodmanDriver), + RunDriverType::Docker => Arc::new(DockerDriver), + } + }, + ) +}); + /// UUID used to mark the current builds static BUILD_ID: Lazy = Lazy::new(Uuid::new_v4); @@ -218,6 +244,20 @@ pub trait BuildDriver: Sync + Send { } } +pub trait RunDriver: Sync + Send { + /// Run a container to perform an action. + /// + /// # Errors + /// Will error if there is an issue running the container. + fn run(&self, opts: &RunOpts) -> std::io::Result; + + /// Run a container to perform an action and capturing output. + /// + /// # Errors + /// Will error if there is an issue running the container. + fn run_output(&self, opts: &RunOpts) -> std::io::Result; +} + /// Allows agnostic inspection of images. pub trait InspectDriver: Sync + Send { /// Gets the metadata on an image tag. @@ -243,6 +283,9 @@ pub struct Driver<'a> { #[builder(default)] inspect_driver: Option, + + #[builder(default)] + run_driver: Option, } impl Driver<'_> { @@ -252,35 +295,46 @@ impl Driver<'_> { /// you will want to run init before trying to use any of /// the strategies. /// - /// # Errors - /// Will error if it is unable to set the user credentials. - pub fn init(self) -> Result<()> { + /// # Panics + /// Will panic if it is unable to initialize drivers. + pub fn init(self) { trace!("Driver::init()"); - let init = INIT.lock().map_err(|e| anyhow!("{e}"))?; - credentials::set_user_creds(self.username, self.password, self.registry)?; - - let mut build_driver = SELECTED_BUILD_DRIVER.lock().map_err(|e| anyhow!("{e}"))?; - let mut inspect_driver = SELECTED_INSPECT_DRIVER.lock().map_err(|e| anyhow!("{e}"))?; - - *build_driver = Some(match self.build_driver { - None => Self::determine_build_driver()?, - Some(driver) => driver, - }); - trace!("Build driver set to {:?}", *build_driver); - drop(build_driver); - let _ = Self::get_build_driver(); - - *inspect_driver = Some(match self.inspect_driver { - None => Self::determine_inspect_driver()?, - Some(driver) => driver, - }); - trace!("Inspect driver set to {:?}", *inspect_driver); - drop(inspect_driver); - let _ = Self::get_inspection_driver(); - - drop(init); - - Ok(()) + let mut initialized = INIT.lock().expect("Must lock INIT"); + + if !*initialized { + credentials::set_user_creds(self.username, self.password, self.registry); + + let mut build_driver = SELECTED_BUILD_DRIVER.lock().expect("Must lock BuildDriver"); + let mut inspect_driver = SELECTED_INSPECT_DRIVER + .lock() + .expect("Must lock InspectDriver"); + let mut run_driver = SELECTED_RUN_DRIVER.lock().expect("Must lock RunDriver"); + + *build_driver = Some( + self.build_driver + .map_or_else(Self::determine_build_driver, |driver| driver), + ); + trace!("Build driver set to {:?}", *build_driver); + drop(build_driver); + let _ = Self::get_build_driver(); + + *inspect_driver = Some( + self.inspect_driver + .map_or_else(Self::determine_inspect_driver, |driver| driver), + ); + trace!("Inspect driver set to {:?}", *inspect_driver); + drop(inspect_driver); + let _ = Self::get_inspection_driver(); + + *run_driver = Some( + self.run_driver + .map_or_else(Self::determine_run_driver, |driver| driver), + ); + drop(run_driver); + let _ = Self::get_run_driver(); + + *initialized = true; + } } /// Gets the current build's UUID @@ -302,6 +356,11 @@ impl Driver<'_> { INSPECT_DRIVER.clone() } + pub fn get_run_driver() -> Arc { + trace!("Driver::get_run_driver()"); + RUN_DRIVER.clone() + } + /// Retrieve the `os_version` for an image. /// /// This gets cached for faster resolution if it's required @@ -352,10 +411,10 @@ impl Driver<'_> { Ok(os_version) } - fn determine_inspect_driver() -> Result { + fn determine_inspect_driver() -> InspectDriverType { trace!("Driver::determine_inspect_driver()"); - Ok(match ( + match ( blue_build_utils::check_command_exists("skopeo"), blue_build_utils::check_command_exists("docker"), blue_build_utils::check_command_exists("podman"), @@ -363,14 +422,14 @@ impl Driver<'_> { (Ok(_skopeo), _, _) => InspectDriverType::Skopeo, (_, Ok(_docker), _) => InspectDriverType::Docker, (_, _, Ok(_podman)) => InspectDriverType::Podman, - _ => bail!("Could not determine inspection strategy. You need either skopeo, docker, or podman"), - }) + _ => panic!("Could not determine inspection strategy. You need either skopeo, docker, or podman"), + } } - fn determine_build_driver() -> Result { + fn determine_build_driver() -> BuildDriverType { trace!("Driver::determine_build_driver()"); - Ok(match ( + match ( blue_build_utils::check_command_exists("docker"), blue_build_utils::check_command_exists("podman"), blue_build_utils::check_command_exists("buildah"), @@ -384,12 +443,34 @@ impl Driver<'_> { (_, _, Ok(_buildah)) if BuildahDriver::is_supported_version() => { BuildDriverType::Buildah } - _ => bail!( + _ => panic!( "Could not determine strategy, need either docker version {}, podman version {}, or buildah version {} to continue", DockerDriver::VERSION_REQ, PodmanDriver::VERSION_REQ, BuildahDriver::VERSION_REQ, ), - }) + } + } + + fn determine_run_driver() -> RunDriverType { + trace!("Driver::determine_run_driver()"); + + match ( + blue_build_utils::check_command_exists("docker"), + blue_build_utils::check_command_exists("podman"), + ) { + (Ok(_docker), _) if DockerDriver::is_supported_version() => RunDriverType::Docker, + (_, Ok(_podman)) if PodmanDriver::is_supported_version() => RunDriverType::Podman, + _ => panic!( + "{}{}{}{}", + "Could not determine strategy, ", + format_args!("need either docker version {}, ", DockerDriver::VERSION_REQ), + format_args!("podman version {}, ", PodmanDriver::VERSION_REQ), + format_args!( + "or buildah version {} to continue", + BuildahDriver::VERSION_REQ + ), + ), + } } } diff --git a/src/drivers/docker_driver.rs b/src/drivers/docker_driver.rs index f9be10fb..4f13030c 100644 --- a/src/drivers/docker_driver.rs +++ b/src/drivers/docker_driver.rs @@ -1,22 +1,32 @@ -use std::{env, process::Command, sync::Mutex, time::Duration}; +use std::{ + env, + path::Path, + process::{Command, ExitStatus}, + sync::Mutex, + time::Duration, +}; use anyhow::{anyhow, bail, Result}; use blue_build_utils::{ constants::{BB_BUILDKIT_CACHE_GHA, CONTAINER_FILE, DOCKER_HOST, SKOPEO_IMAGE}, logging::{CommandLogging, Logger}, + signal_handler::{add_cid, remove_cid, ContainerId}, }; use indicatif::{ProgressBar, ProgressStyle}; use log::{info, trace, warn}; use once_cell::sync::Lazy; use semver::Version; use serde::Deserialize; +use tempdir::TempDir; -use crate::{credentials::Credentials, image_metadata::ImageMetadata}; +use crate::{ + credentials::Credentials, drivers::types::RunDriverType, image_metadata::ImageMetadata, +}; use super::{ credentials, - opts::{BuildOpts, BuildTagPushOpts, GetMetadataOpts, PushOpts, TagOpts}, - BuildDriver, DriverVersion, InspectDriver, + opts::{BuildOpts, BuildTagPushOpts, GetMetadataOpts, PushOpts, RunOpts, TagOpts}, + BuildDriver, DriverVersion, InspectDriver, RunDriver, }; #[derive(Debug, Deserialize)] @@ -308,14 +318,12 @@ impl InspectDriver for DockerDriver { ); progress.enable_steady_tick(Duration::from_millis(100)); - trace!("docker run {SKOPEO_IMAGE} inspect {url}"); - let output = Command::new("docker") - .arg("run") - .arg("--rm") - .arg(SKOPEO_IMAGE) - .arg("inspect") - .arg(&url) - .output()?; + let output = self.run_output( + &RunOpts::builder() + .image(SKOPEO_IMAGE) + .args(&["inspect".to_string(), url.clone()]) + .build(), + )?; progress.finish(); Logger::multi_progress().remove(&progress); @@ -329,3 +337,78 @@ impl InspectDriver for DockerDriver { Ok(serde_json::from_slice(&output.stdout)?) } } + +impl RunDriver for DockerDriver { + fn run(&self, opts: &RunOpts) -> std::io::Result { + trace!("DockerDriver::run({opts:#?})"); + + let cid_path = TempDir::new("docker")?; + let cid_file = cid_path.path().join("cid"); + let cid = ContainerId::new(&cid_file, RunDriverType::Docker, false); + + add_cid(&cid); + + let status = docker_run(opts, &cid_file) + .status_image_ref_progress(opts.image.as_ref(), "Running container")?; + + remove_cid(&cid); + + Ok(status) + } + + fn run_output(&self, opts: &RunOpts) -> std::io::Result { + trace!("DockerDriver::run({opts:#?})"); + + let cid_path = TempDir::new("docker")?; + let cid_file = cid_path.path().join("cid"); + let cid = ContainerId::new(&cid_file, RunDriverType::Docker, false); + + add_cid(&cid); + + let output = docker_run(opts, &cid_file).output()?; + + remove_cid(&cid); + + Ok(output) + } +} + +fn docker_run(opts: &RunOpts, cid_file: &Path) -> Command { + let mut command = Command::new("docker"); + + command + .arg("run") + .arg(format!("--cidfile={}", cid_file.display())); + + if opts.privileged { + command.arg("--privileged"); + } + + if opts.remove { + command.arg("--rm"); + } + + if opts.pull { + command.arg("--pull=always"); + } + + opts.volumes.iter().for_each(|volume| { + command.arg("--volume"); + command.arg(format!( + "{}:{}", + volume.path_or_vol_name, volume.container_path, + )); + }); + + opts.env_vars.iter().for_each(|env| { + command.arg("--env"); + command.arg(format!("{}={}", env.key, env.value)); + }); + + command.arg(opts.image.as_ref()); + + command.args(opts.args.iter()); + + trace!("{command:?}"); + command +} diff --git a/src/drivers/opts.rs b/src/drivers/opts.rs index a4d08976..97632c5d 100644 --- a/src/drivers/opts.rs +++ b/src/drivers/opts.rs @@ -2,9 +2,11 @@ use clap::ValueEnum; pub use build::*; pub use inspect::*; +pub use run::*; mod build; mod inspect; +mod run; #[derive(Debug, Copy, Clone, Default, ValueEnum)] pub enum CompressionType { diff --git a/src/drivers/opts/run.rs b/src/drivers/opts/run.rs new file mode 100644 index 00000000..d381336c --- /dev/null +++ b/src/drivers/opts/run.rs @@ -0,0 +1,45 @@ +use std::borrow::Cow; + +use typed_builder::TypedBuilder; + +#[derive(Debug, Clone, TypedBuilder)] +pub struct RunOpts<'a> { + #[builder(default, setter(into))] + pub image: Cow<'a, str>, + + #[builder(default, setter(into))] + pub args: Cow<'a, [String]>, + + #[builder(default, setter(into))] + pub env_vars: Cow<'a, [RunOptsEnv<'a>]>, + + #[builder(default, setter(into))] + pub volumes: Cow<'a, [RunOptsVolume<'a>]>, + + #[builder(default)] + pub privileged: bool, + + #[builder(default)] + pub pull: bool, + + #[builder(default)] + pub remove: bool, +} + +#[derive(Debug, Clone, TypedBuilder)] +pub struct RunOptsVolume<'a> { + #[builder(setter(into))] + pub path_or_vol_name: Cow<'a, str>, + + #[builder(setter(into))] + pub container_path: Cow<'a, str>, +} + +#[derive(Debug, Clone, TypedBuilder)] +pub struct RunOptsEnv<'a> { + #[builder(setter(into))] + pub key: Cow<'a, str>, + + #[builder(setter(into))] + pub value: Cow<'a, str>, +} diff --git a/src/drivers/podman_driver.rs b/src/drivers/podman_driver.rs index 49fd806a..cdd6039e 100644 --- a/src/drivers/podman_driver.rs +++ b/src/drivers/podman_driver.rs @@ -1,21 +1,30 @@ -use std::{process::Command, time::Duration}; +use std::{ + path::Path, + process::{Command, ExitStatus}, + time::Duration, +}; use anyhow::{bail, Result}; use blue_build_utils::{ constants::SKOPEO_IMAGE, logging::{CommandLogging, Logger}, + signal_handler::{add_cid, remove_cid, ContainerId}, }; +use colored::Colorize; use indicatif::{ProgressBar, ProgressStyle}; -use log::{debug, error, info, trace}; +use log::{debug, error, info, trace, warn}; use semver::Version; use serde::Deserialize; +use tempdir::TempDir; -use crate::{credentials::Credentials, image_metadata::ImageMetadata}; +use crate::{ + credentials::Credentials, drivers::types::RunDriverType, image_metadata::ImageMetadata, +}; use super::{ credentials, - opts::{BuildOpts, GetMetadataOpts, PushOpts, TagOpts}, - BuildDriver, DriverVersion, InspectDriver, + opts::{BuildOpts, GetMetadataOpts, PushOpts, RunOpts, TagOpts}, + BuildDriver, DriverVersion, InspectDriver, RunDriver, }; #[derive(Debug, Deserialize)] @@ -170,14 +179,12 @@ impl InspectDriver for PodmanDriver { ); progress.enable_steady_tick(Duration::from_millis(100)); - trace!("podman run {SKOPEO_IMAGE} inspect {url}"); - let output = Command::new("podman") - .arg("run") - .arg("--rm") - .arg(SKOPEO_IMAGE) - .arg("inspect") - .arg(&url) - .output()?; + let output = self.run_output( + &RunOpts::builder() + .image(SKOPEO_IMAGE) + .args(&["inspect".to_string(), url.clone()]) + .build(), + )?; progress.finish(); Logger::multi_progress().remove(&progress); @@ -190,3 +197,91 @@ impl InspectDriver for PodmanDriver { Ok(serde_json::from_slice(&output.stdout)?) } } + +impl RunDriver for PodmanDriver { + fn run(&self, opts: &RunOpts) -> std::io::Result { + trace!("PodmanDriver::run({opts:#?})"); + + let cid_path = TempDir::new("podman")?; + let cid_file = cid_path.path().join("cid"); + + let cid = ContainerId::new(&cid_file, RunDriverType::Podman, opts.privileged); + + add_cid(&cid); + + let status = podman_run(opts, &cid_file) + .status_image_ref_progress(opts.image.as_ref(), "Running container")?; + + remove_cid(&cid); + + Ok(status) + } + + fn run_output(&self, opts: &RunOpts) -> std::io::Result { + trace!("PodmanDriver::run_output({opts:#?})"); + + let cid_path = TempDir::new("podman")?; + let cid_file = cid_path.path().join("cid"); + + let cid = ContainerId::new(&cid_file, RunDriverType::Podman, opts.privileged); + + add_cid(&cid); + + let output = podman_run(opts, &cid_file).output()?; + + remove_cid(&cid); + + Ok(output) + } +} + +fn podman_run(opts: &RunOpts, cid_file: &Path) -> Command { + let mut command = if opts.privileged { + warn!( + "Running 'podman' in privileged mode requires '{}'", + "sudo".bold().red() + ); + Command::new("sudo") + } else { + Command::new("podman") + }; + + if opts.privileged { + command.arg("podman"); + } + + command + .arg("run") + .arg(format!("--cidfile={}", cid_file.display())); + + if opts.privileged { + command.arg("--privileged"); + } + + if opts.remove { + command.arg("--rm"); + } + + if opts.pull { + command.arg("--pull=always"); + } + + opts.volumes.iter().for_each(|volume| { + command.arg("--volume"); + command.arg(format!( + "{}:{}", + volume.path_or_vol_name, volume.container_path, + )); + }); + + opts.env_vars.iter().for_each(|env| { + command.arg("--env"); + command.arg(format!("{}={}", env.key, env.value)); + }); + + command.arg(opts.image.as_ref()); + command.args(opts.args.iter()); + + trace!("{command:?}"); + command +} diff --git a/src/drivers/types.rs b/src/drivers/types.rs index a1491653..25eee8c7 100644 --- a/src/drivers/types.rs +++ b/src/drivers/types.rs @@ -13,3 +13,18 @@ pub enum BuildDriverType { Podman, Docker, } + +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum RunDriverType { + Podman, + Docker, +} + +impl From for String { + fn from(value: RunDriverType) -> Self { + match value { + RunDriverType::Podman => "podman".to_string(), + RunDriverType::Docker => "docker".to_string(), + } + } +} diff --git a/utils/Cargo.toml b/utils/Cargo.toml index 57943600..a86e0b9b 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -32,6 +32,7 @@ indicatif.workspace = true indicatif-log-bridge.workspace = true log.workspace = true once_cell.workspace = true +tempdir.workspace = true serde.workspace = true serde_yaml.workspace = true serde_json.workspace = true diff --git a/utils/src/logging.rs b/utils/src/logging.rs index fd8dfca7..11ab13c4 100644 --- a/utils/src/logging.rs +++ b/utils/src/logging.rs @@ -3,7 +3,7 @@ use std::{ fs::OpenOptions, io::{BufRead, BufReader, Result, Write as IoWrite}, path::{Path, PathBuf}, - process::{Command, ExitStatus}, + process::{Command, ExitStatus, Stdio}, sync::{Arc, Mutex}, thread, time::Duration, @@ -199,7 +199,9 @@ impl CommandLogging for Command { let log_prefix = Arc::new(log_header(short_name)); let (reader, writer) = os_pipe::pipe()?; - self.stdout(writer.try_clone()?).stderr(writer); + self.stdout(writer.try_clone()?) + .stderr(writer) + .stdin(Stdio::piped()); let progress = Logger::multi_progress() .add(ProgressBar::new_spinner().with_message(format!("{} {name}", message.as_ref()))); diff --git a/utils/src/signal_handler.rs b/utils/src/signal_handler.rs index 65e2648b..4e6a218e 100644 --- a/utils/src/signal_handler.rs +++ b/utils/src/signal_handler.rs @@ -1,10 +1,12 @@ use std::{ - process, + fs, + path::PathBuf, + process::{self, Command}, sync::{atomic::AtomicBool, Arc, Mutex}, thread, }; -use log::{error, trace, warn}; +use log::{debug, error, trace, warn}; use nix::{ libc::{SIGABRT, SIGCONT, SIGHUP, SIGTSTP}, sys::signal::{kill, Signal}, @@ -20,7 +22,31 @@ use signal_hook::{ use crate::logging::Logger; +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ContainerId { + cid_path: PathBuf, + requires_sudo: bool, + crt: String, +} + +impl ContainerId { + pub fn new(cid_path: P, container_runtime: S, requires_sudo: bool) -> Self + where + P: Into, + S: Into, + { + let cid_path = cid_path.into(); + let crt = container_runtime.into(); + Self { + cid_path, + requires_sudo, + crt, + } + } +} + static PID_LIST: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(vec![]))); +static CID_LIST: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(vec![]))); /// Initialize Ctrl-C handler. This should be done at the start /// of a binary. @@ -57,10 +83,36 @@ where warn!("Received termination signal, cleaning up..."); trace!("{info:#?}"); - Logger::multi_progress().clear().unwrap(); + Logger::multi_progress() + .clear() + .expect("Should clear multi_progress"); send_signal_processes(termsig); + let cid_list = CID_LIST.clone(); + let cid_list = cid_list.lock().expect("Should lock mutex"); + cid_list.iter().for_each(|cid| { + if let Ok(id) = fs::read_to_string(&cid.cid_path) { + let id = id.trim(); + debug!("Killing container {id}"); + + let status = if cid.requires_sudo { + Command::new("sudo") + .arg(&cid.crt) + .arg("stop") + .arg(id) + .status() + } else { + Command::new(&cid.crt).arg("stop").arg(id).status() + }; + + if let Err(e) = status { + error!("Failed to kill container {id}: Error {e}"); + } + } + }); + drop(cid_list); + process::exit(1); } SIGTSTP => { @@ -86,8 +138,12 @@ where fn send_signal_processes(sig: i32) { let pid_list = PID_LIST.clone(); let pid_list = pid_list.lock().expect("Should lock mutex"); + pid_list.iter().for_each(|pid| { - if let Err(e) = kill(Pid::from_raw(*pid), Signal::try_from(sig).unwrap()) { + if let Err(e) = kill( + Pid::from_raw(*pid), + Signal::try_from(sig).expect("Should be valid signal"), + ) { error!("Failed to kill process {pid}: Error {e}"); } else { trace!("Killed process {pid}"); @@ -130,3 +186,28 @@ where } } } + +/// Add a cid to the list to kill when the program +/// recieves a kill signal. +/// +/// # Panics +/// Will panic if the mutex cannot be locked. +pub fn add_cid(cid: &ContainerId) { + let mut cid_list = CID_LIST.lock().expect("Should lock cid_list"); + + if !cid_list.contains(cid) { + cid_list.push(cid.clone()); + } +} + +/// Remove a cid from the list of pids to kill. +/// +/// # Panics +/// Will panic if the mutex cannot be locked. +pub fn remove_cid(cid: &ContainerId) { + let mut cid_list = CID_LIST.lock().expect("Should lock cid_list"); + + if let Some(index) = cid_list.iter().position(|val| *val == *cid) { + cid_list.swap_remove(index); + } +}