Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Squash builds #155

Merged
merged 11 commits into from
Apr 11, 2024
13 changes: 13 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,19 @@ pub enum CommandArgs {

#[derive(Default, Clone, Copy, Debug, TypedBuilder, Args)]
pub struct DriverArgs {
/// Puts the build in a `squash-stage` and
/// COPY's the results to the final stage
/// as one layer.
///
/// WARN: This doesn't work with the
/// docker driver as it has been deprecated.
///
/// NOTE: Squash has a performance benefit for
/// the newer versions of podman and buildah.
#[arg(short, long)]
#[builder(default)]
squash: bool,

/// Select which driver to use to build
/// your image.
#[builder(default)]
Expand Down
17 changes: 15 additions & 2 deletions src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ use crate::{
commands::template::TemplateCommand,
credentials,
drivers::{
opts::{BuildTagPushOpts, CompressionType},
opts::{BuildTagPushOpts, CompressionType, GetMetadataOpts},
Driver,
},
};

use super::{BlueBuildCommand, DriverArgs};

#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Args, TypedBuilder)]
pub struct BuildCommand {
/// The recipe file to build an image
Expand Down Expand Up @@ -181,6 +182,7 @@ impl BlueBuildCommand for BuildCommand {
TemplateCommand::builder()
.recipe(&recipe_path)
.output(PathBuf::from("Containerfile"))
.drivers(DriverArgs::builder().squash(self.drivers.squash).build())
.build()
.try_run()?;

Expand Down Expand Up @@ -215,6 +217,7 @@ impl BuildCommand {
archive_dir.to_string_lossy().trim_end_matches('/'),
recipe.name.to_lowercase().replace('/', "_"),
))
.squash(self.drivers.squash)
.build()
} else {
BuildTagPushOpts::builder()
Expand All @@ -224,6 +227,7 @@ impl BuildCommand {
.no_retry_push(self.no_retry_push)
.retry_count(self.retry_count)
.compression(self.compression_format)
.squash(self.drivers.squash)
.build()
};

Expand Down Expand Up @@ -339,14 +343,23 @@ impl BuildCommand {
// ========================= Helpers ====================== //
// ======================================================== //

#[allow(clippy::too_many_lines)]
fn sign_images(image_name: &str, tag: Option<&str>) -> Result<()> {
trace!("BuildCommand::sign_images({image_name}, {tag:?})");

env::set_var("COSIGN_PASSWORD", "");
env::set_var("COSIGN_YES", "true");

let inspect_opts = GetMetadataOpts::builder().image(image_name);

let inspect_opts = if let Some(tag) = tag {
inspect_opts.tag(tag).build()
} else {
inspect_opts.build()
};

let image_digest = Driver::get_inspection_driver()
.get_metadata(image_name, tag.map_or_else(|| "latest", |t| t))?
.get_metadata(&inspect_opts)?
.digest;
let image_name_digest = format!("{image_name}@{image_digest}");
let image_name_tag = tag.map_or_else(|| image_name.to_owned(), |t| format!("{image_name}:{t}"));
Expand Down
12 changes: 9 additions & 3 deletions src/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use anyhow::{anyhow, Result};
use blue_build_utils::constants::{
CI_REGISTRY, CI_REGISTRY_PASSWORD, CI_REGISTRY_USER, GITHUB_ACTIONS, GITHUB_ACTOR, GITHUB_TOKEN,
};
use log::trace;
use once_cell::sync::Lazy;
use typed_builder::TypedBuilder;

Expand Down Expand Up @@ -63,6 +64,7 @@ static ENV_CREDENTIALS: Lazy<Option<Credentials>> = Lazy::new(|| {
(None, None, Some(_)) => "ghcr.io".to_string(),
_ => return None,
};
trace!("Registry: {registry}");

let username = match (
username,
Expand All @@ -74,6 +76,7 @@ static ENV_CREDENTIALS: Lazy<Option<Credentials>> = Lazy::new(|| {
(None, None, Some(github_actor)) => github_actor,
_ => return None,
};
trace!("Username: {username}");

let password = match (
password,
Expand Down Expand Up @@ -109,13 +112,15 @@ pub fn set_user_creds(
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}"))?;
creds_lock.username = username.map(std::borrow::ToOwned::to_owned);
creds_lock.password = password.map(std::borrow::ToOwned::to_owned);
creds_lock.registry = registry.map(std::borrow::ToOwned::to_owned);
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(())
}

Expand All @@ -124,6 +129,7 @@ pub fn set_user_creds(
/// # Errors
/// Will error if there aren't any credentials available.
pub fn get() -> Result<&'static Credentials> {
trace!("credentials::get()");
ENV_CREDENTIALS
.as_ref()
.ok_or_else(|| anyhow!("No credentials available"))
Expand Down
38 changes: 28 additions & 10 deletions src/drivers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crate::{credentials, image_metadata::ImageMetadata};
use self::{
buildah_driver::BuildahDriver,
docker_driver::DockerDriver,
opts::{BuildTagPushOpts, CompressionType},
opts::{BuildOpts, BuildTagPushOpts, GetMetadataOpts, PushOpts, TagOpts},
podman_driver::PodmanDriver,
skopeo_driver::SkopeoDriver,
types::{BuildDriverType, InspectDriverType},
Expand Down Expand Up @@ -124,19 +124,19 @@ pub trait BuildDriver: Sync + Send {
///
/// # Errors
/// Will error if the build fails.
fn build(&self, image: &str) -> Result<()>;
fn build(&self, opts: &BuildOpts) -> Result<()>;

/// Runs the tag logic for the strategy.
///
/// # Errors
/// Will error if the tagging fails.
fn tag(&self, src_image: &str, image_name: &str, tag: &str) -> Result<()>;
fn tag(&self, opts: &TagOpts) -> Result<()>;

/// Runs the push logic for the strategy
///
/// # Errors
/// Will error if the push fails.
fn push(&self, image: &str, compression: CompressionType) -> Result<()>;
fn push(&self, opts: &PushOpts) -> Result<()>;

/// Runs the login logic for the strategy.
///
Expand All @@ -163,8 +163,13 @@ pub trait BuildDriver: Sync + Send {
(None, None) => bail!("Need either the image or archive path set"),
};

let build_opts = BuildOpts::builder()
.image(&full_image)
.squash(opts.squash)
.build();

info!("Building image {full_image}");
self.build(&full_image)?;
self.build(&build_opts)?;

if !opts.tags.is_empty() && opts.archive_path.is_none() {
let image = opts
Expand All @@ -176,7 +181,12 @@ pub trait BuildDriver: Sync + Send {
for tag in opts.tags.as_ref() {
debug!("Tagging {} with {tag}", &full_image);

self.tag(&full_image, image.as_ref(), tag)?;
let tag_opts = TagOpts::builder()
.src_image(&full_image)
.dest_image(format!("{image}:{tag}"))
.build();

self.tag(&tag_opts)?;

if opts.push {
let retry_count = if opts.no_retry_push {
Expand All @@ -192,7 +202,12 @@ pub trait BuildDriver: Sync + Send {

debug!("Pushing image {tag_image}");

self.push(&tag_image, opts.compression)
let push_opts = PushOpts::builder()
.image(&tag_image)
.compression_type(opts.compression)
.build();

self.push(&push_opts)
})?;
}
}
Expand All @@ -208,7 +223,7 @@ pub trait InspectDriver: Sync + Send {
///
/// # Errors
/// Will error if it is unable to get the labels.
fn get_metadata(&self, image_name: &str, tag: &str) -> Result<ImageMetadata>;
fn get_metadata(&self, opts: &GetMetadataOpts) -> Result<ImageMetadata>;
}

#[derive(Debug, TypedBuilder)]
Expand Down Expand Up @@ -307,8 +322,11 @@ impl Driver<'_> {
let os_version = match entry {
None => {
info!("Retrieving OS version from {image}. This might take a bit");
let inspection =
INSPECT_DRIVER.get_metadata(&recipe.base_image, &recipe.image_version)?;
let inspect_opts = GetMetadataOpts::builder()
.image(recipe.base_image.as_ref())
.tag(recipe.image_version.as_ref())
.build();
let inspection = INSPECT_DRIVER.get_metadata(&inspect_opts)?;

let os_version = inspection.get_version().ok_or_else(|| {
anyhow!(
Expand Down
61 changes: 42 additions & 19 deletions src/drivers/buildah_driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ use serde::Deserialize;

use crate::credentials;

use super::{opts::CompressionType, BuildDriver, DriverVersion};
use super::{
opts::{BuildOpts, PushOpts, TagOpts},
BuildDriver, DriverVersion,
};

#[derive(Debug, Deserialize)]
struct BuildahVersionJson {
Expand All @@ -23,68 +26,88 @@ impl DriverVersion for BuildahDriver {
const VERSION_REQ: &'static str = ">=1.24";

fn version() -> Result<Version> {
trace!("BuildahDriver::version()");

trace!("buildah version --json");
let output = Command::new("buildah")
.arg("version")
.arg("--json")
.output()?;

let version_json: BuildahVersionJson = serde_json::from_slice(&output.stdout)?;
trace!("{version_json:#?}");

Ok(version_json.version)
}
}

impl BuildDriver for BuildahDriver {
fn build(&self, image: &str) -> Result<()> {
trace!("buildah build -t {image}");
fn build(&self, opts: &BuildOpts) -> Result<()> {
trace!("BuildahDriver::build({opts:#?})");

trace!(
"buildah build --pull=true --layers={} -t {}",
!opts.squash,
opts.image,
);
let status = Command::new("buildah")
.arg("build")
.arg("--pull=true")
.arg(format!("--layers={}", !opts.squash))
.arg("-t")
.arg(image)
.arg(opts.image.as_ref())
.status()?;

if status.success() {
info!("Successfully built {image}");
info!("Successfully built {}", opts.image);
} else {
bail!("Failed to build {image}");
bail!("Failed to build {}", opts.image);
}
Ok(())
}

fn tag(&self, src_image: &str, image_name: &str, tag: &str) -> Result<()> {
let dest_image = format!("{image_name}:{tag}");
trace!("buildah tag {src_image} {dest_image}");
fn tag(&self, opts: &TagOpts) -> Result<()> {
trace!("BuildahDriver::tag({opts:#?})");

trace!("buildah tag {} {}", opts.src_image, opts.dest_image);
let status = Command::new("buildah")
.arg("tag")
.arg(src_image)
.arg(&dest_image)
.arg(opts.src_image.as_ref())
.arg(opts.dest_image.as_ref())
.status()?;

if status.success() {
info!("Successfully tagged {dest_image}!");
info!("Successfully tagged {}!", opts.dest_image);
} else {
bail!("Failed to tag image {dest_image}");
bail!("Failed to tag image {}", opts.dest_image);
}
Ok(())
}

fn push(&self, image: &str, compression: CompressionType) -> Result<()> {
trace!("buildah push {image}");
fn push(&self, opts: &PushOpts) -> Result<()> {
trace!("BuildahDriver::push({opts:#?})");

trace!("buildah push {}", opts.image);
let status = Command::new("buildah")
.arg("push")
.arg(format!("--compression-format={compression}"))
.arg(image)
.arg(format!(
"--compression-format={}",
opts.compression_type.unwrap_or_default()
))
.arg(opts.image.as_ref())
.status()?;

if status.success() {
info!("Successfully pushed {image}!");
info!("Successfully pushed {}!", opts.image);
} else {
bail!("Failed to push image {image}")
bail!("Failed to push image {}", opts.image);
}
Ok(())
}

fn login(&self) -> Result<()> {
trace!("BuildahDriver::login()");

let (registry, username, password) =
credentials::get().map(|c| (&c.registry, &c.username, &c.password))?;

Expand Down
Loading