particle-os-cli/process/drivers/buildah_driver.rs
Gerald Pinder 3a0be4099a
feat: Add bootc support (#448)
Adds support for using `bootc` as the preferred method for booting from
a locally created image. This new method gets rid of the need to create
a tarball and move it to the correct place and instead it will make use
of `podman scp` which copies the image to the root `containers-storage`
and then has `rpm-ostree` and `bootc` boot from that store.

Closes #418 
Closes #200
2025-08-09 14:05:59 -04:00

216 lines
5.9 KiB
Rust

use std::{io::Write, process::Stdio};
use blue_build_utils::{credentials::Credentials, secret::SecretArgs, semver::Version};
use colored::Colorize;
use comlexr::cmd;
use log::{debug, error, info, trace};
use miette::{Context, IntoDiagnostic, Result, bail, miette};
use serde::Deserialize;
use tempfile::TempDir;
use crate::logging::CommandLogging;
use super::{
BuildDriver, DriverVersion,
opts::{BuildOpts, PruneOpts, PushOpts, TagOpts},
};
#[derive(Debug, Deserialize)]
struct BuildahVersionJson {
pub version: Version,
}
#[derive(Debug)]
pub struct BuildahDriver;
impl DriverVersion for BuildahDriver {
// The prune command wasn't present until 1.29
const VERSION_REQ: &'static str = ">=1.29";
fn version() -> Result<Version> {
trace!("BuildahDriver::version()");
let output = {
let c = cmd!("buildah", "version", "--json");
trace!("{c:?}");
c
}
.output()
.into_diagnostic()?;
let version_json: BuildahVersionJson = serde_json::from_slice(&output.stdout)
.inspect_err(|e| error!("{e}: {}", String::from_utf8_lossy(&output.stdout)))
.into_diagnostic()?;
trace!("{version_json:#?}");
Ok(version_json.version)
}
}
impl BuildDriver for BuildahDriver {
fn build(opts: BuildOpts) -> Result<()> {
trace!("BuildahDriver::build({opts:#?})");
let temp_dir = TempDir::new()
.into_diagnostic()
.wrap_err("Failed to create temporary directory for secrets")?;
let command = cmd!(
"buildah",
"build",
for opts.secrets.args(&temp_dir)?,
if opts.secrets.ssh() => "--ssh",
if let Some(platform) = opts.platform => [
"--platform",
platform.to_string(),
],
"--pull=true",
format!("--layers={}", !opts.squash),
if let Some(cache_from) = opts.cache_from.as_ref() => [
"--cache-from",
format!(
"{}/{}",
cache_from.registry(),
cache_from.repository()
),
],
if let Some(cache_to) = opts.cache_to.as_ref() => [
"--cache-to",
format!(
"{}/{}",
cache_to.registry(),
cache_to.repository()
),
],
"-f",
opts.containerfile,
"-t",
opts.image.to_string(),
);
trace!("{command:?}");
let status = command
.build_status(opts.image.to_string(), "Building Image")
.into_diagnostic()?;
if status.success() {
info!("Successfully built {}", opts.image);
} else {
bail!("Failed to build {}", opts.image);
}
Ok(())
}
fn tag(opts: TagOpts) -> Result<()> {
trace!("BuildahDriver::tag({opts:#?})");
let dest_image_str = opts.dest_image.to_string();
let mut command = cmd!(
"buildah",
"tag",
opts.src_image.to_string(),
&dest_image_str,
);
trace!("{command:?}");
if command.status().into_diagnostic()?.success() {
info!("Successfully tagged {}!", dest_image_str.bold().green());
} else {
bail!("Failed to tag image {}", dest_image_str.bold().red());
}
Ok(())
}
fn push(opts: PushOpts) -> Result<()> {
trace!("BuildahDriver::push({opts:#?})");
let image_str = opts.image.to_string();
let command = cmd!(
"buildah",
"push",
format!(
"--compression-format={}",
opts.compression_type.unwrap_or_default()
),
&image_str,
);
trace!("{command:?}");
let status = command
.build_status(&image_str, "Pushing Image")
.into_diagnostic()?;
if status.success() {
info!("Successfully pushed {}!", image_str.bold().green());
} else {
bail!("Failed to push image {}", image_str.bold().red());
}
Ok(())
}
fn login() -> Result<()> {
trace!("BuildahDriver::login()");
if let Some(Credentials {
registry,
username,
password,
}) = Credentials::get()
{
let mut command = cmd!(
"buildah",
"login",
"-u",
username,
"--password-stdin",
registry
);
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
trace!("{command:?}");
let mut child = command.spawn().into_diagnostic()?;
write!(
child
.stdin
.as_mut()
.ok_or_else(|| miette!("Unable to open pipe to stdin"))?,
"{password}"
)
.into_diagnostic()?;
let output = child.wait_with_output().into_diagnostic()?;
if !output.status.success() {
let err_out = String::from_utf8_lossy(&output.stderr);
bail!("Failed to login for buildah:\n{}", err_out.trim());
}
debug!("Logged into {registry}");
}
Ok(())
}
fn prune(opts: PruneOpts) -> Result<()> {
trace!("PodmanDriver::prune({opts:?})");
let status = cmd!(
"buildah",
"prune",
"--force",
if opts.all => "--all",
)
.message_status("buildah prune", "Pruning Buildah System")
.into_diagnostic()?;
if !status.success() {
bail!("Failed to prune buildah");
}
Ok(())
}
}