feat(experimental): Build multiple recipes in parallel (#182)

The `build` subcommand can now take in any number of recipe files and
will build them all in parallel. Along with this new ability, I've added
a way to easily distinguish which part of the build log belongs to which
recipe. Check out the `docker_build` action of this PR for an example.


![gif](https://gitlab.com/wunker-bunker/wunker-os/-/raw/main/bluebuild.gif)

## Tasks

- [x] Make build log follow same pattern as normal logs to keep things
consistent
- [x] Update color ranges based on @xynydev 's feedback
- [x] Deal with ANSI control characters in log output
- [x] Add [`indicatif`](https://crates.io/crates/indicatif) to make logs
look nicer
- [x] Add ability to print logs to a file
This commit is contained in:
Gerald Pinder 2024-06-07 17:52:26 -04:00 committed by GitHub
parent 18e48a34a4
commit 4ca98c1c2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1449 additions and 500 deletions

View file

@ -1,17 +1,15 @@
use blue_build::commands::{BlueBuildArgs, BlueBuildCommand, CommandArgs};
use blue_build_utils::logging;
use blue_build_utils::logging::Logger;
use clap::Parser;
use log::LevelFilter;
fn main() {
let args = BlueBuildArgs::parse();
let log_level = args.verbosity.log_level_filter();
env_logger::builder()
Logger::new()
.filter_level(args.verbosity.log_level_filter())
.filter_module("hyper::proto", LevelFilter::Info)
.format(logging::format_log(log_level))
.filter_modules([("hyper::proto", LevelFilter::Info)])
.log_out_dir(args.log_out.clone())
.init();
log::trace!("Parsed arguments: {args:#?}");

View file

@ -1,3 +1,5 @@
use std::path::PathBuf;
use log::error;
use clap::{command, crate_authors, Args, Parser, Subcommand};
@ -50,6 +52,10 @@ pub struct BlueBuildArgs {
#[command(subcommand)]
pub command: CommandArgs,
/// The directory to output build logs.
#[arg(long)]
pub log_out: Option<PathBuf>,
#[clap(flatten)]
pub verbosity: Verbosity<InfoLevel>,
}

View file

@ -6,13 +6,16 @@ use std::{
use anyhow::{bail, Context, Result};
use blue_build_recipe::Recipe;
use blue_build_utils::constants::{
ARCHIVE_SUFFIX, BB_PASSWORD, BB_REGISTRY, BB_REGISTRY_NAMESPACE, BB_USERNAME, BUILD_ID_LABEL,
CI_DEFAULT_BRANCH, CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_PROJECT_URL, CI_REGISTRY,
CI_SERVER_HOST, CI_SERVER_PROTOCOL, CONFIG_PATH, CONTAINER_FILE, COSIGN_PRIVATE_KEY,
COSIGN_PRIV_PATH, COSIGN_PUB_PATH, GITHUB_REPOSITORY_OWNER, GITHUB_TOKEN,
GITHUB_TOKEN_ISSUER_URL, GITHUB_WORKFLOW_REF, GITIGNORE_PATH, LABELED_ERROR_MESSAGE,
NO_LABEL_ERROR_MESSAGE, RECIPE_FILE, RECIPE_PATH, SIGSTORE_ID_TOKEN,
use blue_build_utils::{
constants::{
ARCHIVE_SUFFIX, BB_PASSWORD, BB_REGISTRY, BB_REGISTRY_NAMESPACE, BB_USERNAME,
BUILD_ID_LABEL, CI_DEFAULT_BRANCH, CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_PROJECT_URL,
CI_REGISTRY, CI_SERVER_HOST, CI_SERVER_PROTOCOL, CONFIG_PATH, CONTAINER_FILE,
COSIGN_PRIVATE_KEY, COSIGN_PRIV_PATH, COSIGN_PUB_PATH, GITHUB_REPOSITORY_OWNER,
GITHUB_TOKEN, GITHUB_TOKEN_ISSUER_URL, GITHUB_WORKFLOW_REF, GITIGNORE_PATH,
LABELED_ERROR_MESSAGE, NO_LABEL_ERROR_MESSAGE, RECIPE_FILE, RECIPE_PATH, SIGSTORE_ID_TOKEN,
},
generate_containerfile_path,
};
use clap::Args;
use colored::Colorize;
@ -35,6 +38,13 @@ use super::{BlueBuildCommand, DriverArgs};
pub struct BuildCommand {
/// The recipe file to build an image
#[arg()]
#[cfg(feature = "multi-recipe")]
#[builder(default, setter(into, strip_option))]
recipe: Option<Vec<PathBuf>>,
/// The recipe file to build an image
#[arg()]
#[cfg(not(feature = "multi-recipe"))]
#[builder(default, setter(into, strip_option))]
recipe: Option<PathBuf>,
@ -126,6 +136,8 @@ impl BlueBuildCommand for BuildCommand {
.build()
.init()?;
self.update_gitignore()?;
if self.push && self.archive.is_some() {
bail!("You cannot use '--archive' and '--push' at the same time");
}
@ -133,96 +145,129 @@ impl BlueBuildCommand for BuildCommand {
if self.push {
blue_build_utils::check_command_exists("cosign")?;
self.check_cosign_files()?;
Self::login()?;
}
Self::login()?;
// Check if the Containerfile exists
// - If doesn't => *Build*
// - If it does:
// - check entry in .gitignore
// -> If it is => *Build*
// -> If isn't:
// - check if it has the BlueBuild tag (LABEL)
// -> If it does => *Ask* to add to .gitignore and remove from git
// -> If it doesn't => *Ask* to continue and override the file
let container_file_path = Path::new(CONTAINER_FILE);
if !self.force && container_file_path.exists() {
let gitignore = fs::read_to_string(GITIGNORE_PATH)
.context(format!("Failed to read {GITIGNORE_PATH}"))?;
let is_ignored = gitignore
.lines()
.any(|line: &str| line.contains(CONTAINER_FILE));
if !is_ignored {
let containerfile = fs::read_to_string(container_file_path)
.context(format!("Failed to read {}", container_file_path.display()))?;
let has_label = containerfile.lines().any(|line| {
let label = format!("LABEL {BUILD_ID_LABEL}");
line.to_string().trim().starts_with(&label)
});
let question = requestty::Question::confirm("build")
.message(
if has_label {
LABELED_ERROR_MESSAGE
} else {
NO_LABEL_ERROR_MESSAGE
}
.bright_yellow()
.to_string(),
)
.default(true)
.build();
if let Ok(answer) = requestty::prompt_one(question) {
if answer.as_bool().unwrap_or(false) {
blue_build_utils::append_to_file(
&GITIGNORE_PATH,
&format!("/{CONTAINER_FILE}"),
)?;
}
#[cfg(feature = "multi-recipe")]
{
use rayon::prelude::*;
let recipe_paths = self.recipe.clone().map_or_else(|| {
let legacy_path = Path::new(CONFIG_PATH);
let recipe_path = Path::new(RECIPE_PATH);
if recipe_path.exists() && recipe_path.is_dir() {
vec![recipe_path.join(RECIPE_FILE)]
} else {
warn!("Use of {CONFIG_PATH} for recipes is deprecated, please move your recipe files into {RECIPE_PATH}");
vec![legacy_path.join(RECIPE_FILE)]
}
}
},
|recipes| {
let mut same = std::collections::HashSet::new();
recipes.into_iter().filter(|recipe| same.insert(recipe.clone())).collect()
});
recipe_paths.par_iter().try_for_each(|recipe| {
GenerateCommand::builder()
.output(generate_containerfile_path(recipe)?)
.recipe(recipe)
.drivers(DriverArgs::builder().squash(self.drivers.squash).build())
.build()
.try_run()
})?;
self.start(&recipe_paths)
}
let recipe_path = self.recipe.clone().unwrap_or_else(|| {
let legacy_path = Path::new(CONFIG_PATH);
let recipe_path = Path::new(RECIPE_PATH);
if recipe_path.exists() && recipe_path.is_dir() {
recipe_path.join(RECIPE_FILE)
} else {
warn!("Use of {CONFIG_PATH} for recipes is deprecated, please move your recipe files into {RECIPE_PATH}");
legacy_path.join(RECIPE_FILE)
}
});
#[cfg(not(feature = "multi-recipe"))]
{
let recipe_path = self.recipe.clone().unwrap_or_else(|| {
let legacy_path = Path::new(CONFIG_PATH);
let recipe_path = Path::new(RECIPE_PATH);
if recipe_path.exists() && recipe_path.is_dir() {
recipe_path.join(RECIPE_FILE)
} else {
warn!("Use of {CONFIG_PATH} for recipes is deprecated, please move your recipe files into {RECIPE_PATH}");
legacy_path.join(RECIPE_FILE)
}
});
GenerateCommand::builder()
.recipe(&recipe_path)
.output(PathBuf::from("Containerfile"))
.build()
.try_run()?;
GenerateCommand::builder()
.output(generate_containerfile_path(&recipe_path)?)
.recipe(&recipe_path)
.drivers(DriverArgs::builder().squash(self.drivers.squash).build())
.build()
.try_run()?;
info!("Building image for recipe at {}", recipe_path.display());
self.start(&recipe_path)
self.start(&recipe_path)
}
}
}
impl BuildCommand {
fn start(&self, recipe_path: &Path) -> Result<()> {
#[cfg(feature = "multi-recipe")]
fn start(&self, recipe_paths: &[PathBuf]) -> Result<()> {
use rayon::prelude::*;
trace!("BuildCommand::build_image()");
let recipe = Recipe::parse(&recipe_path)?;
recipe_paths
.par_iter()
.try_for_each(|recipe_path| -> Result<()> {
let recipe = Recipe::parse(recipe_path)?;
let os_version = Driver::get_os_version(&recipe)?;
let containerfile = generate_containerfile_path(recipe_path)?;
let tags = recipe.generate_tags(os_version);
let image_name = self.generate_full_image_name(&recipe)?;
let opts = if let Some(archive_dir) = self.archive.as_ref() {
BuildTagPushOpts::builder()
.containerfile(&containerfile)
.archive_path(format!(
"{}/{}.{ARCHIVE_SUFFIX}",
archive_dir.to_string_lossy().trim_end_matches('/'),
recipe.name.to_lowercase().replace('/', "_"),
))
.squash(self.drivers.squash)
.build()
} else {
BuildTagPushOpts::builder()
.image(&image_name)
.containerfile(&containerfile)
.tags(tags.iter().map(String::as_str).collect::<Vec<_>>())
.push(self.push)
.no_retry_push(self.no_retry_push)
.retry_count(self.retry_count)
.compression(self.compression_format)
.squash(self.drivers.squash)
.build()
};
Driver::get_build_driver().build_tag_push(&opts)?;
if self.push && !self.no_sign {
sign_images(&image_name, tags.first().map(String::as_str))?;
}
Ok(())
})?;
info!("Build complete!");
Ok(())
}
#[cfg(not(feature = "multi-recipe"))]
fn start(&self, recipe_path: &Path) -> Result<()> {
trace!("BuildCommand::start()");
let recipe = Recipe::parse(recipe_path)?;
let os_version = Driver::get_os_version(&recipe)?;
let containerfile = generate_containerfile_path(recipe_path)?;
let tags = recipe.generate_tags(os_version);
let image_name = self.generate_full_image_name(&recipe)?;
let opts = if let Some(archive_dir) = self.archive.as_ref() {
BuildTagPushOpts::builder()
.containerfile(&containerfile)
.archive_path(format!(
"{}/{}.{ARCHIVE_SUFFIX}",
archive_dir.to_string_lossy().trim_end_matches('/'),
@ -233,6 +278,7 @@ impl BuildCommand {
} else {
BuildTagPushOpts::builder()
.image(&image_name)
.containerfile(&containerfile)
.tags(tags.iter().map(String::as_str).collect::<Vec<_>>())
.push(self.push)
.no_retry_push(self.no_retry_push)
@ -249,7 +295,6 @@ impl BuildCommand {
}
info!("Build complete!");
Ok(())
}
@ -348,6 +393,75 @@ impl BuildCommand {
Ok(image_name)
}
fn update_gitignore(&self) -> Result<()> {
// Check if the Containerfile exists
// - If doesn't => *Build*
// - If it does:
// - check entry in .gitignore
// -> If it is => *Build*
// -> If isn't:
// - check if it has the BlueBuild tag (LABEL)
// -> If it does => *Ask* to add to .gitignore and remove from git
// -> If it doesn't => *Ask* to continue and override the file
let container_file_path = Path::new(CONTAINER_FILE);
let label = format!("LABEL {BUILD_ID_LABEL}");
if !self.force && container_file_path.exists() {
let to_ignore_lines = [format!("/{CONTAINER_FILE}"), format!("/{CONTAINER_FILE}.*")];
let gitignore = fs::read_to_string(GITIGNORE_PATH)
.context(format!("Failed to read {GITIGNORE_PATH}"))?;
let mut edited_gitignore = gitignore.clone();
to_ignore_lines
.iter()
.filter(|to_ignore| {
!gitignore
.lines()
.any(|line| line.trim() == to_ignore.trim())
})
.try_for_each(|to_ignore| -> Result<()> {
let containerfile = fs::read_to_string(container_file_path)
.context(format!("Failed to read {}", container_file_path.display()))?;
let has_label = containerfile
.lines()
.any(|line| line.to_string().trim().starts_with(&label));
let question = requestty::Question::confirm("build")
.message(
if has_label {
LABELED_ERROR_MESSAGE
} else {
NO_LABEL_ERROR_MESSAGE
}
.bright_yellow()
.to_string(),
)
.default(true)
.build();
if let Ok(answer) = requestty::prompt_one(question) {
if answer.as_bool().unwrap_or(false) {
if !edited_gitignore.ends_with('\n') {
edited_gitignore.push('\n');
}
edited_gitignore.push_str(to_ignore);
edited_gitignore.push('\n');
}
}
Ok(())
})?;
if edited_gitignore != gitignore {
fs::write(GITIGNORE_PATH, edited_gitignore.as_str())?;
}
}
Ok(())
}
/// Checks the cosign private/public key pair to ensure they match.
///
/// # Errors

View file

@ -56,8 +56,15 @@ impl BlueBuildCommand for UpgradeCommand {
let recipe = Recipe::parse(&self.common.recipe)?;
let mut build = BuildCommand::builder()
.recipe(self.common.recipe.clone())
let build = BuildCommand::builder();
#[cfg(feature = "multi-recipe")]
let build = build.recipe(vec![self.common.recipe.clone()]);
#[cfg(not(feature = "multi-recipe"))]
let build = build.recipe(self.common.recipe.clone());
let mut build = build
.archive(LOCAL_BUILD)
.drivers(self.common.drivers)
.force(self.common.force)
@ -108,8 +115,15 @@ impl BlueBuildCommand for RebaseCommand {
let recipe = Recipe::parse(&self.common.recipe)?;
let mut build = BuildCommand::builder()
.recipe(self.common.recipe.clone())
let build = BuildCommand::builder();
#[cfg(feature = "multi-recipe")]
let build = build.recipe(vec![self.common.recipe.clone()]);
#[cfg(not(feature = "multi-recipe"))]
let build = build.recipe(self.common.recipe.clone());
let mut build = build
.archive(LOCAL_BUILD)
.drivers(self.common.drivers)
.force(self.common.force)

View file

@ -1,15 +1,18 @@
use std::{
path::{Path, PathBuf},
process::Command,
time::Duration,
};
use anyhow::{bail, Result};
use blue_build_recipe::Recipe;
use blue_build_utils::constants::{
ARCHIVE_SUFFIX, LOCAL_BUILD, OCI_ARCHIVE, OSTREE_UNVERIFIED_IMAGE,
use blue_build_utils::{
constants::{ARCHIVE_SUFFIX, LOCAL_BUILD, OCI_ARCHIVE, OSTREE_UNVERIFIED_IMAGE},
logging::CommandLogging,
};
use clap::Args;
use colored::Colorize;
use indicatif::ProgressBar;
use log::{debug, trace, warn};
use tempdir::TempDir;
use typed_builder::TypedBuilder;
@ -65,7 +68,7 @@ impl BlueBuildCommand for SwitchCommand {
trace!("{tempdir:?}");
BuildCommand::builder()
.recipe(self.recipe.clone())
.recipe([self.recipe.clone()])
.archive(tempdir.path())
.force(self.force)
.build()
@ -124,6 +127,7 @@ impl SwitchCommand {
"{OSTREE_UNVERIFIED_IMAGE}:{OCI_ARCHIVE}:{path}",
path = archive_path.display()
);
let mut command = Command::new("rpm-ostree");
command.arg("rebase").arg(&image_ref);
@ -137,7 +141,10 @@ impl SwitchCommand {
);
command
}
.status()?;
.status_image_ref_progress(
format!("{}", archive_path.display()),
"Switching to new image",
)?;
if !status.success() {
bail!("Failed to switch to new image!");
@ -152,9 +159,15 @@ impl SwitchCommand {
to.display()
);
let progress = ProgressBar::new_spinner();
progress.enable_steady_tick(Duration::from_millis(100));
progress.set_message(format!("Moving image archive to {}...", to.display()));
trace!("sudo mv {} {}", from.display(), to.display());
let status = Command::new("sudo").arg("mv").args([from, to]).status()?;
progress.finish_and_clear();
if !status.success() {
bail!(
"Failed to move archive from {from} to {to}",
@ -193,12 +206,18 @@ impl SwitchCommand {
if !files.is_empty() {
let files = files.join(" ");
let progress = ProgressBar::new_spinner();
progress.enable_steady_tick(Duration::from_millis(100));
progress.set_message("Removing old image archive files...");
trace!("sudo rm -f {files}");
let status = Command::new("sudo")
.args(["rm", "-f"])
.arg(files)
.status()?;
progress.finish_and_clear();
if !status.success() {
bail!("Failed to clean out archives in {LOCAL_BUILD}");
}

View file

@ -165,6 +165,7 @@ pub trait BuildDriver: Sync + Send {
let build_opts = BuildOpts::builder()
.image(&full_image)
.containerfile(opts.containerfile.as_ref())
.squash(opts.squash)
.build();

View file

@ -1,6 +1,7 @@
use std::process::Command;
use anyhow::{bail, Result};
use blue_build_utils::logging::CommandLogging;
use log::{error, info, trace};
use semver::Version;
use serde::Deserialize;
@ -47,17 +48,21 @@ impl BuildDriver for BuildahDriver {
trace!("BuildahDriver::build({opts:#?})");
trace!(
"buildah build --pull=true --layers={} -t {}",
"buildah build --pull=true --layers={} -f {} -t {}",
!opts.squash,
opts.containerfile.display(),
opts.image,
);
let status = Command::new("buildah")
let mut command = Command::new("buildah");
command
.arg("build")
.arg("--pull=true")
.arg(format!("--layers={}", !opts.squash))
.arg("-f")
.arg(opts.containerfile.as_ref())
.arg("-t")
.arg(opts.image.as_ref())
.status()?;
.arg(opts.image.as_ref());
let status = command.status_image_ref_progress(&opts.image, "Building Image")?;
if status.success() {
info!("Successfully built {}", opts.image);
@ -89,14 +94,15 @@ impl BuildDriver for BuildahDriver {
trace!("BuildahDriver::push({opts:#?})");
trace!("buildah push {}", opts.image);
let status = Command::new("buildah")
let mut command = Command::new("buildah");
command
.arg("push")
.arg(format!(
"--compression-format={}",
opts.compression_type.unwrap_or_default()
))
.arg(opts.image.as_ref())
.status()?;
.arg(opts.image.as_ref());
let status = command.status_image_ref_progress(&opts.image, "Pushing Image")?;
if status.success() {
info!("Successfully pushed {}!", opts.image);

View file

@ -1,13 +1,11 @@
use std::{
env,
process::{Command, Stdio},
sync::Mutex,
};
use std::{env, process::Command, sync::Mutex, time::Duration};
use anyhow::{anyhow, bail, Result};
use blue_build_utils::constants::{
BB_BUILDKIT_CACHE_GHA, CONTAINER_FILE, DOCKER_HOST, SKOPEO_IMAGE,
use blue_build_utils::{
constants::{BB_BUILDKIT_CACHE_GHA, CONTAINER_FILE, DOCKER_HOST, SKOPEO_IMAGE},
logging::{CommandLogging, Logger},
};
use indicatif::{ProgressBar, ProgressStyle};
use log::{info, trace, warn};
use once_cell::sync::Lazy;
use semver::Version;
@ -76,12 +74,12 @@ impl DockerDriver {
.arg("--name=bluebuild")
.output()?;
if create_out.status.success() {
*lock = true;
} else {
if !create_out.status.success() {
bail!("{}", String::from_utf8_lossy(&create_out.stderr));
}
}
*lock = true;
drop(lock);
Ok(())
}
@ -119,7 +117,7 @@ impl BuildDriver for DockerDriver {
.arg("-t")
.arg(opts.image.as_ref())
.arg("-f")
.arg(CONTAINER_FILE)
.arg(opts.containerfile.as_ref())
.arg(".")
.status()?;
@ -211,12 +209,15 @@ impl BuildDriver for DockerDriver {
command.arg("--builder=bluebuild");
}
trace!("build --progress=plain --pull -f {CONTAINER_FILE}",);
trace!(
"build --progress=plain --pull -f {}",
opts.containerfile.display()
);
command
.arg("build")
.arg("--pull")
.arg("-f")
.arg(CONTAINER_FILE);
.arg(opts.containerfile.as_ref());
// https://github.com/moby/buildkit?tab=readme-ov-file#github-actions-cache-experimental
if env::var(BB_BUILDKIT_CACHE_GHA).map_or_else(|_| false, |e| e == "true") {
@ -228,18 +229,25 @@ impl BuildDriver for DockerDriver {
.arg("type=gha");
}
let mut final_image = String::new();
match (opts.image.as_ref(), opts.archive_path.as_ref()) {
(Some(image), None) => {
if opts.tags.is_empty() {
final_image.push_str(image);
trace!("-t {image}");
command.arg("-t").arg(image.as_ref());
} else {
for tag in opts.tags.as_ref() {
final_image
.push_str(format!("{image}:{}", opts.tags.first().unwrap_or(&"")).as_str());
opts.tags.iter().for_each(|tag| {
let full_image = format!("{image}:{tag}");
trace!("-t {full_image}");
command.arg("-t").arg(full_image);
}
});
}
if opts.push {
@ -254,6 +262,8 @@ impl BuildDriver for DockerDriver {
}
}
(None, Some(archive_path)) => {
final_image.push_str(archive_path);
trace!("--output type=oci,dest={archive_path}");
command
.arg("--output")
@ -266,14 +276,17 @@ impl BuildDriver for DockerDriver {
trace!(".");
command.arg(".");
if command.status()?.success() {
if command
.status_image_ref_progress(&final_image, "Building Image")?
.success()
{
if opts.push {
info!("Successfully built and pushed image");
info!("Successfully built and pushed image {}", final_image);
} else {
info!("Successfully built image");
info!("Successfully built image {}", final_image);
}
} else {
bail!("Failed to build image");
bail!("Failed to build image {}", final_image);
}
Ok(())
}
@ -288,6 +301,13 @@ impl InspectDriver for DockerDriver {
|tag| format!("docker://{}:{tag}", opts.image),
);
let progress = Logger::multi_progress().add(
ProgressBar::new_spinner()
.with_style(ProgressStyle::default_spinner())
.with_message(format!("Inspecting metadata for {url}")),
);
progress.enable_steady_tick(Duration::from_millis(100));
trace!("docker run {SKOPEO_IMAGE} inspect {url}");
let output = Command::new("docker")
.arg("run")
@ -295,9 +315,11 @@ impl InspectDriver for DockerDriver {
.arg(SKOPEO_IMAGE)
.arg("inspect")
.arg(&url)
.stderr(Stdio::inherit())
.output()?;
progress.finish();
Logger::multi_progress().remove(&progress);
if output.status.success() {
info!("Successfully inspected image {url}!");
} else {

View file

@ -1,4 +1,4 @@
use std::borrow::Cow;
use std::{borrow::Cow, path::Path};
use typed_builder::TypedBuilder;
@ -12,6 +12,9 @@ pub struct BuildOpts<'a> {
#[builder(default)]
pub squash: bool,
#[builder(setter(into))]
pub containerfile: Cow<'a, Path>,
}
#[derive(Debug, Clone, TypedBuilder)]
@ -33,6 +36,7 @@ pub struct PushOpts<'a> {
}
/// Options for building, tagging, and pusing images.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, TypedBuilder)]
pub struct BuildTagPushOpts<'a> {
/// The base image name.
@ -47,6 +51,10 @@ pub struct BuildTagPushOpts<'a> {
#[builder(default, setter(into, strip_option))]
pub archive_path: Option<Cow<'a, str>>,
/// The path to the Containerfile to build.
#[builder(setter(into))]
pub containerfile: Cow<'a, Path>,
/// The list of tags for the image being built.
#[builder(default, setter(into))]
pub tags: Cow<'a, [&'a str]>,
@ -65,9 +73,11 @@ pub struct BuildTagPushOpts<'a> {
#[builder(default = 1)]
pub retry_count: u8,
/// The compression type to use when pushing.
#[builder(default)]
pub compression: CompressionType,
/// Run all steps in a single layer.
#[builder(default)]
pub squash: bool,
}

View file

@ -1,7 +1,11 @@
use std::process::{Command, Stdio};
use std::{process::Command, time::Duration};
use anyhow::{bail, Result};
use blue_build_utils::constants::SKOPEO_IMAGE;
use blue_build_utils::{
constants::SKOPEO_IMAGE,
logging::{CommandLogging, Logger},
};
use indicatif::{ProgressBar, ProgressStyle};
use log::{debug, error, info, trace};
use semver::Version;
use serde::Deserialize;
@ -57,18 +61,22 @@ impl BuildDriver for PodmanDriver {
trace!("PodmanDriver::build({opts:#?})");
trace!(
"podman build --pull=true --layers={} . -t {}",
"podman build --pull=true --layers={} -f {} -t {} .",
!opts.squash,
opts.containerfile.display(),
opts.image,
);
let status = Command::new("podman")
let mut command = Command::new("podman");
command
.arg("build")
.arg("--pull=true")
.arg(format!("--layers={}", !opts.squash))
.arg(".")
.arg("-f")
.arg(opts.containerfile.as_ref())
.arg("-t")
.arg(opts.image.as_ref())
.status()?;
.arg(".");
let status = command.status_image_ref_progress(&opts.image, "Building Image")?;
if status.success() {
info!("Successfully built {}", opts.image);
@ -100,14 +108,15 @@ impl BuildDriver for PodmanDriver {
trace!("PodmanDriver::push({opts:#?})");
trace!("podman push {}", opts.image);
let status = Command::new("podman")
let mut command = Command::new("podman");
command
.arg("push")
.arg(format!(
"--compression-format={}",
opts.compression_type.unwrap_or_default()
))
.arg(opts.image.as_ref())
.status()?;
.arg(opts.image.as_ref());
let status = command.status_image_ref_progress(&opts.image, "Pushing Image")?;
if status.success() {
info!("Successfully pushed {}!", opts.image);
@ -154,6 +163,13 @@ impl InspectDriver for PodmanDriver {
|tag| format!("docker://{}:{tag}", opts.image),
);
let progress = Logger::multi_progress().add(
ProgressBar::new_spinner()
.with_style(ProgressStyle::default_spinner())
.with_message(format!("Inspecting metadata for {url}")),
);
progress.enable_steady_tick(Duration::from_millis(100));
trace!("podman run {SKOPEO_IMAGE} inspect {url}");
let output = Command::new("podman")
.arg("run")
@ -161,9 +177,11 @@ impl InspectDriver for PodmanDriver {
.arg(SKOPEO_IMAGE)
.arg("inspect")
.arg(&url)
.stderr(Stdio::inherit())
.output()?;
progress.finish();
Logger::multi_progress().remove(&progress);
if output.status.success() {
debug!("Successfully inspected image {url}!");
} else {

View file

@ -1,6 +1,11 @@
use std::process::{Command, Stdio};
use std::{
process::{Command, Stdio},
time::Duration,
};
use anyhow::{bail, Result};
use blue_build_utils::logging::Logger;
use indicatif::{ProgressBar, ProgressStyle};
use log::{debug, trace};
use crate::image_metadata::ImageMetadata;
@ -19,6 +24,13 @@ impl InspectDriver for SkopeoDriver {
|tag| format!("docker://{}:{tag}", opts.image),
);
let progress = Logger::multi_progress().add(
ProgressBar::new_spinner()
.with_style(ProgressStyle::default_spinner())
.with_message(format!("Inspecting metadata for {url}")),
);
progress.enable_steady_tick(Duration::from_millis(100));
trace!("skopeo inspect {url}");
let output = Command::new("skopeo")
.arg("inspect")
@ -26,6 +38,9 @@ impl InspectDriver for SkopeoDriver {
.stderr(Stdio::inherit())
.output()?;
progress.finish();
Logger::multi_progress().remove(&progress);
if output.status.success() {
debug!("Successfully inspected image {url}!");
} else {