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.  ## 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:
parent
18e48a34a4
commit
4ca98c1c2a
24 changed files with 1449 additions and 500 deletions
|
|
@ -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:#?}");
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue