feat: Squash builds (#155)
### Buildah/Podman support Buildah and podman can make heavy use of the squash feature. Something that I've noticed when trying to build from inside of a container, requiring intermediate layers with mounts causes build times to skyrocket. Build times are much faster when using the `--squash` functionality (seen as `--layers=false`). Here are the following results from my personal build using both squash and non-squash functionality. #### Squash upgrade: ``` $> rpm-ostree upgrade Pulling manifest: ostree-image-signed:docker://registry.gitlab.com/wunker-bunker/wunker-os/jp-laptop Importing: ostree-image-signed:docker://registry.gitlab.com/wunker-bunker/wunker-os/jp-laptop (digest: sha256:60f743ba322041918d302e7e7f10438c59502e19343c294064bacb676c8eb7b7) ostree chunk layers already present: 65 custom layers already present: 3 custom layers needed: 1 (814.0 MB) ``` All changes appear to show as a single custom layer. Any small change even at the end of the build appears to require completely downloading the new layer (squash only squashes additional layers on top of the base layer). This makes sense as layers cannot currently be downloaded by diff. #### Non-squash upgrade: ``` $> rpm-ostree upgrade Pulling manifest: ostree-image-signed:docker://registry.gitlab.com/wunker-bunker/wunker-os/jp-desktop-gaming:latest Importing: ostree-image-signed:docker://registry.gitlab.com/wunker-bunker/wunker-os/jp-desktop-gaming:latest (digest: sha256:0658b51febfcbaa1722961b7a6d2b197d3823a6228e330f45dd1e1aaefd145c5) ostree chunk layers already present: 65 custom layers already present: 4 custom layers needed: 15 (942.4 MB) ``` As expected, there are more layers when not squashing and the size is slightly bigger. Most likely due to there being extra information stored in the layers that is subsequently removed. ### Docker support Docker is apparently [no longer supporting](https://github.com/docker/buildx/issues/1287) the use of the `--squash` arg. The use of squash will not be available for the docker driver in this case.
This commit is contained in:
parent
e9c96e204d
commit
6e3a193e92
11 changed files with 269 additions and 101 deletions
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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()?;
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
};
|
||||
|
||||
|
|
@ -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}"));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
@ -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.
|
||||
///
|
||||
|
|
@ -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
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)]
|
||||
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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))?;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::{
|
|||
|
||||
use anyhow::{bail, Result};
|
||||
use blue_build_utils::constants::{BB_BUILDKIT_CACHE_GHA, CONTAINER_FILE, SKOPEO_IMAGE};
|
||||
use log::{info, trace};
|
||||
use log::{info, trace, warn};
|
||||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ use crate::image_metadata::ImageMetadata;
|
|||
|
||||
use super::{
|
||||
credentials,
|
||||
opts::{BuildTagPushOpts, CompressionType},
|
||||
opts::{BuildOpts, BuildTagPushOpts, GetMetadataOpts, PushOpts, TagOpts},
|
||||
BuildDriver, DriverVersion, InspectDriver,
|
||||
};
|
||||
|
||||
|
|
@ -51,57 +51,62 @@ impl DriverVersion for DockerDriver {
|
|||
}
|
||||
|
||||
impl BuildDriver for DockerDriver {
|
||||
fn build(&self, image: &str) -> Result<()> {
|
||||
trace!("DockerDriver::build({image})");
|
||||
fn build(&self, opts: &BuildOpts) -> Result<()> {
|
||||
trace!("DockerDriver::build({opts:#?})");
|
||||
|
||||
trace!("docker build -t {image} -f {CONTAINER_FILE} .");
|
||||
if opts.squash {
|
||||
warn!("Squash is deprecated for docker so this build will not squash");
|
||||
}
|
||||
|
||||
trace!("docker build -t {} -f {CONTAINER_FILE} .", opts.image);
|
||||
let status = Command::new("docker")
|
||||
.arg("build")
|
||||
.arg("-t")
|
||||
.arg(image)
|
||||
.arg(opts.image.as_ref())
|
||||
.arg("-f")
|
||||
.arg(CONTAINER_FILE)
|
||||
.arg(".")
|
||||
.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<()> {
|
||||
trace!("DockerDriver::tag({src_image}, {image_name}, {tag})");
|
||||
fn tag(&self, opts: &TagOpts) -> Result<()> {
|
||||
trace!("DockerDriver::tag({opts:#?})");
|
||||
|
||||
let dest_image = format!("{image_name}:{tag}");
|
||||
|
||||
trace!("docker tag {src_image} {dest_image}");
|
||||
trace!("docker tag {} {}", opts.src_image, opts.dest_image);
|
||||
let status = Command::new("docker")
|
||||
.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, _: CompressionType) -> Result<()> {
|
||||
trace!("DockerDriver::push({image})");
|
||||
fn push(&self, opts: &PushOpts) -> Result<()> {
|
||||
trace!("DockerDriver::push({opts:#?})");
|
||||
|
||||
trace!("docker push {image}");
|
||||
let status = Command::new("docker").arg("push").arg(image).status()?;
|
||||
trace!("docker push {}", opts.image);
|
||||
let status = Command::new("docker")
|
||||
.arg("push")
|
||||
.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(())
|
||||
}
|
||||
|
|
@ -132,6 +137,10 @@ impl BuildDriver for DockerDriver {
|
|||
fn build_tag_push(&self, opts: &BuildTagPushOpts) -> Result<()> {
|
||||
trace!("DockerDriver::build_tag_push({opts:#?})");
|
||||
|
||||
if opts.squash {
|
||||
warn!("Squash is deprecated for docker so this build will not squash");
|
||||
}
|
||||
|
||||
let mut command = Command::new("docker");
|
||||
|
||||
trace!("docker buildx build -f {CONTAINER_FILE}");
|
||||
|
|
@ -172,8 +181,8 @@ impl BuildDriver for DockerDriver {
|
|||
opts.compression
|
||||
));
|
||||
} else {
|
||||
trace!("--builder default");
|
||||
command.arg("--builder").arg("default");
|
||||
trace!("--load");
|
||||
command.arg("--load");
|
||||
}
|
||||
}
|
||||
(None, Some(archive_path)) => {
|
||||
|
|
@ -203,14 +212,18 @@ impl BuildDriver for DockerDriver {
|
|||
}
|
||||
|
||||
impl InspectDriver for DockerDriver {
|
||||
fn get_metadata(&self, image_name: &str, tag: &str) -> Result<ImageMetadata> {
|
||||
trace!("DockerDriver::get_labels({image_name}, {tag})");
|
||||
fn get_metadata(&self, opts: &GetMetadataOpts) -> Result<ImageMetadata> {
|
||||
trace!("DockerDriver::get_labels({opts:#?})");
|
||||
|
||||
let url = format!("docker://{image_name}:{tag}");
|
||||
let url = opts.tag.as_ref().map_or_else(
|
||||
|| format!("docker://{}", opts.image),
|
||||
|tag| format!("docker://{}:{tag}", opts.image),
|
||||
);
|
||||
|
||||
trace!("docker run {SKOPEO_IMAGE} inspect {url}");
|
||||
let output = Command::new("docker")
|
||||
.arg("run")
|
||||
.arg("--rm")
|
||||
.arg(SKOPEO_IMAGE)
|
||||
.arg("inspect")
|
||||
.arg(&url)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,23 @@
|
|||
mod build;
|
||||
use clap::ValueEnum;
|
||||
|
||||
pub use build::*;
|
||||
pub use inspect::*;
|
||||
|
||||
mod build;
|
||||
mod inspect;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default, ValueEnum)]
|
||||
pub enum CompressionType {
|
||||
#[default]
|
||||
Gzip,
|
||||
Zstd,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CompressionType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::Zstd => "zstd",
|
||||
Self::Gzip => "gzip",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,35 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use clap::ValueEnum;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default, ValueEnum)]
|
||||
pub enum CompressionType {
|
||||
#[default]
|
||||
Gzip,
|
||||
Zstd,
|
||||
use super::CompressionType;
|
||||
|
||||
/// Options for building
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct BuildOpts<'a> {
|
||||
#[builder(setter(into))]
|
||||
pub image: Cow<'a, str>,
|
||||
|
||||
#[builder(default)]
|
||||
pub squash: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CompressionType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::Zstd => "zstd",
|
||||
Self::Gzip => "gzip",
|
||||
})
|
||||
}
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct TagOpts<'a> {
|
||||
#[builder(setter(into))]
|
||||
pub src_image: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
pub dest_image: Cow<'a, str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct PushOpts<'a> {
|
||||
#[builder(setter(into))]
|
||||
pub image: Cow<'a, str>,
|
||||
|
||||
#[builder(default, setter(strip_option))]
|
||||
pub compression_type: Option<CompressionType>,
|
||||
}
|
||||
|
||||
/// Options for building, tagging, and pusing images.
|
||||
|
|
@ -54,4 +67,7 @@ pub struct BuildTagPushOpts<'a> {
|
|||
|
||||
#[builder(default)]
|
||||
pub compression: CompressionType,
|
||||
|
||||
#[builder(default)]
|
||||
pub squash: bool,
|
||||
}
|
||||
|
|
|
|||
12
src/drivers/opts/inspect.rs
Normal file
12
src/drivers/opts/inspect.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct GetMetadataOpts<'a> {
|
||||
#[builder(setter(into))]
|
||||
pub image: Cow<'a, str>,
|
||||
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub tag: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
|
@ -8,7 +8,11 @@ use serde::Deserialize;
|
|||
|
||||
use crate::image_metadata::ImageMetadata;
|
||||
|
||||
use super::{credentials, opts::CompressionType, BuildDriver, DriverVersion, InspectDriver};
|
||||
use super::{
|
||||
credentials,
|
||||
opts::{BuildOpts, GetMetadataOpts, PushOpts, TagOpts},
|
||||
BuildDriver, DriverVersion, InspectDriver,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PodmanVersionJsonClient {
|
||||
|
|
@ -31,6 +35,9 @@ impl DriverVersion for PodmanDriver {
|
|||
const VERSION_REQ: &'static str = ">=4";
|
||||
|
||||
fn version() -> Result<Version> {
|
||||
trace!("PodmanDriver::version()");
|
||||
|
||||
trace!("podman version -f json");
|
||||
let output = Command::new("podman")
|
||||
.arg("version")
|
||||
.arg("-f")
|
||||
|
|
@ -38,64 +45,80 @@ impl DriverVersion for PodmanDriver {
|
|||
.output()?;
|
||||
|
||||
let version_json: PodmanVersionJson = serde_json::from_slice(&output.stdout)?;
|
||||
trace!("{version_json:#?}");
|
||||
|
||||
Ok(version_json.client.version)
|
||||
}
|
||||
}
|
||||
|
||||
impl BuildDriver for PodmanDriver {
|
||||
fn build(&self, image: &str) -> Result<()> {
|
||||
trace!("podman build . -t {image}");
|
||||
fn build(&self, opts: &BuildOpts) -> Result<()> {
|
||||
trace!("PodmanDriver::build({opts:#?})");
|
||||
|
||||
trace!(
|
||||
"podman build --pull=true --layers={} . -t {}",
|
||||
!opts.squash,
|
||||
opts.image,
|
||||
);
|
||||
let status = Command::new("podman")
|
||||
.arg("build")
|
||||
.arg("--pull=true")
|
||||
.arg(format!("--layers={}", !opts.squash))
|
||||
.arg(".")
|
||||
.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}");
|
||||
fn tag(&self, opts: &TagOpts) -> Result<()> {
|
||||
trace!("PodmanDriver::tag({opts:#?})");
|
||||
|
||||
trace!("podman tag {src_image} {dest_image}");
|
||||
trace!("podman tag {} {}", opts.src_image, opts.dest_image);
|
||||
let status = Command::new("podman")
|
||||
.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!("podman push {image}");
|
||||
fn push(&self, opts: &PushOpts) -> Result<()> {
|
||||
trace!("PodmanDriver::push({opts:#?})");
|
||||
|
||||
trace!("podman push {}", opts.image);
|
||||
let status = Command::new("podman")
|
||||
.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!("PodmanDriver::login()");
|
||||
|
||||
let (registry, username, password) =
|
||||
credentials::get().map(|c| (&c.registry, &c.username, &c.password))?;
|
||||
|
||||
|
|
@ -118,12 +141,18 @@ impl BuildDriver for PodmanDriver {
|
|||
}
|
||||
|
||||
impl InspectDriver for PodmanDriver {
|
||||
fn get_metadata(&self, image_name: &str, tag: &str) -> Result<ImageMetadata> {
|
||||
let url = format!("docker://{image_name}:{tag}");
|
||||
fn get_metadata(&self, opts: &GetMetadataOpts) -> Result<ImageMetadata> {
|
||||
trace!("PodmanDriver::get_metadata({opts:#?})");
|
||||
|
||||
let url = opts.tag.as_ref().map_or_else(
|
||||
|| format!("docker://{}", opts.image),
|
||||
|tag| format!("docker://{}:{tag}", opts.image),
|
||||
);
|
||||
|
||||
trace!("podman run {SKOPEO_IMAGE} inspect {url}");
|
||||
let output = Command::new("podman")
|
||||
.arg("run")
|
||||
.arg("--rm")
|
||||
.arg(SKOPEO_IMAGE)
|
||||
.arg("inspect")
|
||||
.arg(&url)
|
||||
|
|
@ -133,7 +162,7 @@ impl InspectDriver for PodmanDriver {
|
|||
if output.status.success() {
|
||||
debug!("Successfully inspected image {url}!");
|
||||
} else {
|
||||
bail!("Failed to inspect image {url}")
|
||||
bail!("Failed to inspect image {url}");
|
||||
}
|
||||
Ok(serde_json::from_slice(&output.stdout)?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,19 @@ use log::{debug, trace};
|
|||
|
||||
use crate::image_metadata::ImageMetadata;
|
||||
|
||||
use super::InspectDriver;
|
||||
use super::{opts::GetMetadataOpts, InspectDriver};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SkopeoDriver;
|
||||
|
||||
impl InspectDriver for SkopeoDriver {
|
||||
fn get_metadata(&self, image_name: &str, tag: &str) -> Result<ImageMetadata> {
|
||||
let url = format!("docker://{image_name}:{tag}");
|
||||
fn get_metadata(&self, opts: &GetMetadataOpts) -> Result<ImageMetadata> {
|
||||
trace!("SkopeoDriver::get_metadata({opts:#?})");
|
||||
|
||||
let url = opts.tag.as_ref().map_or_else(
|
||||
|| format!("docker://{}", opts.image),
|
||||
|tag| format!("docker://{}:{tag}", opts.image),
|
||||
);
|
||||
|
||||
trace!("skopeo inspect {url}");
|
||||
let output = Command::new("skopeo")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue