feat(podman-api): Clean up working container for SIGINT and SIGTERM (#14)

Co-authored-by: Hikaru (ひかる, ヒカル) <lecoqjacob@gmail.com>
This commit is contained in:
Gerald Pinder 2024-02-14 16:04:47 -05:00 committed by GitHub
parent 4fde628f82
commit 98398788f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 355 additions and 173 deletions

View file

@ -11,6 +11,10 @@ use anyhow::{anyhow, bail, Result};
use clap::Args;
use log::{debug, info, trace, warn};
use typed_builder::TypedBuilder;
use uuid::Uuid;
#[cfg(feature = "builtin-podman")]
use std::sync::Arc;
#[cfg(feature = "podman-api")]
use podman_api::{
@ -21,8 +25,14 @@ use podman_api::{
#[cfg(feature = "podman-api")]
use build_strategy::BuildStrategy;
#[cfg(feature = "signal-hook-tokio")]
use signal_hook_tokio::Signals;
#[cfg(feature = "tokio")]
use tokio::runtime::Runtime;
use tokio::{
runtime::Runtime,
sync::oneshot::{self, Sender},
};
use crate::{
commands::template::TemplateCommand,
@ -141,6 +151,8 @@ impl BlueBuildCommand for BuildCommand {
fn try_run(&mut self) -> Result<()> {
trace!("BuildCommand::try_run()");
let build_id = Uuid::new_v4();
if self.push && self.archive.is_some() {
bail!("You cannot use '--archive' and '--push' at the same time");
}
@ -166,27 +178,39 @@ impl BlueBuildCommand for BuildCommand {
TemplateCommand::builder()
.recipe(&recipe_path)
.output(PathBuf::from("Containerfile"))
.build_id(build_id)
.build()
.try_run()?;
info!("Building image for recipe at {}", recipe_path.display());
#[cfg(feature = "podman-api")]
#[cfg(feature = "builtin-podman")]
match BuildStrategy::determine_strategy()? {
BuildStrategy::Socket(socket) => Runtime::new()?
.block_on(self.build_image_podman_api(Podman::unix(socket), &recipe_path)),
BuildStrategy::Socket(socket) => Runtime::new()?.block_on(self.build_image_podman_api(
Podman::unix(socket),
build_id,
&recipe_path,
)),
_ => self.build_image(&recipe_path),
}
#[cfg(not(feature = "podman-api"))]
#[cfg(not(feature = "builtin-podman"))]
self.build_image(&recipe_path)
}
}
impl BuildCommand {
#[cfg(feature = "podman-api")]
async fn build_image_podman_api(&self, client: Podman, recipe_path: &Path) -> Result<()> {
#[cfg(feature = "builtin-podman")]
async fn build_image_podman_api(
&self,
client: Podman,
build_id: Uuid,
recipe_path: &Path,
) -> Result<()> {
use futures_util::StreamExt;
use signal_hook::consts::{SIGINT, SIGQUIT, SIGTERM};
use crate::ops::BUILD_ID_LABEL;
trace!("BuildCommand::build_image({client:#?})");
@ -211,34 +235,52 @@ impl BuildCommand {
};
debug!("Full tag is {first_image_name}");
// Prepare for the signal trap
let client = Arc::new(client);
let signals = Signals::new([SIGTERM, SIGINT, SIGQUIT])?;
let handle = signals.handle();
let (kill_tx, mut kill_rx) = oneshot::channel::<()>();
let signals_task = tokio::spawn(handle_signals(signals, kill_tx, build_id, client.clone()));
// Get podman ready to build
let opts = ImageBuildOpts::builder(".")
.tag(&first_image_name)
.dockerfile("Containerfile")
.remove(true)
.layers(true)
.labels([(BUILD_ID_LABEL, build_id.to_string())])
.pull(true)
.build();
trace!("Build options: {opts:#?}");
info!("Building image {first_image_name}");
match client.images().build(&opts) {
Ok(mut build_stream) => {
while let Some(chunk) = build_stream.next().await {
match chunk {
Ok(chunk) => chunk
.stream
.trim()
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.for_each(|line| info!("{line}")),
Err(e) => bail!("{e}"),
Ok(mut build_stream) => loop {
tokio::select! {
Some(chunk) = build_stream.next() => {
match chunk {
Ok(chunk) => chunk
.stream
.trim()
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.for_each(|line| info!("{line}")),
Err(e) => bail!("{e}"),
}
},
_ = &mut kill_rx => {
break;
}
}
}
},
Err(e) => bail!("{e}"),
};
handle.close();
signals_task.await??;
if self.push {
debug!("Pushing is enabled");
@ -274,6 +316,7 @@ impl BuildCommand {
sign_images(&image_name, tags.first().map(String::as_str))?;
}
Ok(())
}
@ -765,6 +808,71 @@ fn check_cosign_files() -> Result<()> {
}
}
#[cfg(feature = "builtin-podman")]
async fn handle_signals(
mut signals: Signals,
kill: Sender<()>,
build_id: Uuid,
client: Arc<Podman>,
) -> Result<()> {
use std::process;
use futures_util::StreamExt;
use podman_api::opts::{
ContainerListOpts, ContainerPruneFilter, ContainerPruneOpts, ImagePruneFilter,
ImagePruneOpts,
};
use signal_hook::consts::{SIGHUP, SIGINT};
use tokio::time::{self, Duration};
use crate::ops::BUILD_ID_LABEL;
trace!("handle_signals(signals, {build_id}, {client:#?})");
while let Some(signal) = signals.next().await {
match signal {
SIGHUP => (),
SIGINT => {
kill.send(()).unwrap();
info!("Recieved SIGINT, cleaning up build...");
time::sleep(Duration::from_secs(1)).await;
let containers = client
.containers()
.list(&ContainerListOpts::builder().sync(true).all(true).build())
.await?;
trace!("{containers:#?}");
// Prune containers from this build
let container_prune_opts = ContainerPruneOpts::builder()
.filter([ContainerPruneFilter::LabelKeyVal(
BUILD_ID_LABEL.to_string(),
build_id.to_string(),
)])
.build();
client.containers().prune(&container_prune_opts).await?;
debug!("Pruned containers");
// Prune images from this build
let image_prune_opts = ImagePruneOpts::builder()
.filter([ImagePruneFilter::LabelKeyVal(
BUILD_ID_LABEL.to_string(),
build_id.to_string(),
)])
.build();
client.images().prune(&image_prune_opts).await?;
debug!("Pruned images");
process::exit(2);
}
_ => unreachable!(),
}
}
Ok(())
}
fn tag_images(tags: &[String], image_name: &str, full_image: &str) -> Result<()> {
debug!("Tagging all images");

View file

@ -4,11 +4,12 @@ use std::{
process,
};
use anyhow::Result;
use anyhow::{anyhow, Result};
use askama::Template;
use clap::Args;
use log::{debug, error, info, trace};
use typed_builder::TypedBuilder;
use uuid::Uuid;
use crate::{
constants::{self},
@ -21,8 +22,13 @@ use super::BlueBuildCommand;
#[template(path = "Containerfile")]
pub struct ContainerFileTemplate<'a> {
recipe: &'a Recipe<'a>,
#[builder(setter(into))]
recipe_path: &'a Path,
#[builder(setter(into))]
build_id: Uuid,
#[builder(default)]
export_script: ExportsTemplate,
}
@ -42,6 +48,10 @@ pub struct TemplateCommand {
#[arg(short, long)]
#[builder(default, setter(into, strip_option))]
output: Option<PathBuf>,
#[clap(skip)]
#[builder(default, setter(into, strip_option))]
build_id: Option<Uuid>,
}
impl BlueBuildCommand for TemplateCommand {
@ -54,6 +64,8 @@ impl BlueBuildCommand for TemplateCommand {
.display()
);
self.build_id.get_or_insert(Uuid::new_v4());
self.template_file()
}
}
@ -71,9 +83,14 @@ impl TemplateCommand {
let recipe_de = Recipe::parse(&recipe_path)?;
trace!("recipe_de: {recipe_de:#?}");
let build_id = self
.build_id
.ok_or_else(|| anyhow!("Build ID should have been generated by now"))?;
let template = ContainerFileTemplate::builder()
.build_id(build_id)
.recipe(&recipe_de)
.recipe_path(&recipe_path)
.recipe_path(recipe_path.as_path())
.build();
let output_str = template.render()?;

View file

@ -39,7 +39,7 @@ pub struct Recipe<'a> {
pub blue_build_tag: Option<Cow<'a, str>>,
#[serde(flatten)]
pub modules_ext: ModuleExt,
pub modules_ext: ModuleExt<'a>,
#[serde(flatten)]
#[builder(setter(into))]
@ -145,7 +145,7 @@ impl<'a> Recipe<'a> {
let mut recipe =
serde_yaml::from_str::<Recipe>(&file).map_err(ops::serde_yaml_err(&file))?;
recipe.modules_ext.modules = Module::get_modules(&recipe.modules_ext.modules);
recipe.modules_ext.modules = Module::get_modules(&recipe.modules_ext.modules).into();
Ok(recipe)
}
@ -203,12 +203,12 @@ impl<'a> Recipe<'a> {
}
#[derive(Default, Serialize, Clone, Deserialize, Debug, TypedBuilder)]
pub struct ModuleExt {
pub struct ModuleExt<'a> {
#[builder(default, setter(into))]
pub modules: Vec<Module>,
pub modules: Cow<'a, [Module<'a>]>,
}
impl ModuleExt {
impl ModuleExt<'_> {
/// # Parse a module file returning a [`ModuleExt`]
///
/// # Errors
@ -236,21 +236,21 @@ impl ModuleExt {
}
#[derive(Serialize, Deserialize, Debug, Clone, TypedBuilder)]
pub struct Module {
pub struct Module<'a> {
#[builder(default, setter(into, strip_option))]
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub module_type: Option<String>,
pub module_type: Option<Cow<'a, str>>,
#[builder(default, setter(into, strip_option))]
#[serde(rename = "from-file", skip_serializing_if = "Option::is_none")]
pub from_file: Option<String>,
pub from_file: Option<Cow<'a, str>>,
#[serde(flatten)]
#[builder(default, setter(into))]
pub config: IndexMap<String, Value>,
}
impl Module {
impl Module<'_> {
#[must_use]
pub fn get_modules(modules: &[Self]) -> Vec<Self> {
modules

View file

@ -5,6 +5,7 @@ use std::process::Command;
pub const LOCAL_BUILD: &str = "/etc/bluebuild";
pub const ARCHIVE_SUFFIX: &str = "tar.gz";
pub const BUILD_ID_LABEL: &str = "org.blue-build.build-id";
pub fn check_command_exists(command: &str) -> Result<()> {
trace!("check_command_exists({command})");