feat(podman-api): Clean up working container for SIGINT and SIGTERM (#14)
Co-authored-by: Hikaru (ひかる, ヒカル) <lecoqjacob@gmail.com>
This commit is contained in:
parent
4fde628f82
commit
98398788f7
7 changed files with 355 additions and 173 deletions
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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()?;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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})");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue