nightly(podman-api): Use podman-api crate for building images

This commit is contained in:
Gerald Pinder 2024-01-19 18:55:26 +00:00
parent 218cc9c7d3
commit 1b950b08dc
11 changed files with 3068 additions and 108 deletions

View file

@ -1,2 +1,2 @@
[language-server.rust-analyzer.config]
cargo.features = ["init"]
cargo.features = ["nightly"]

2798
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -7,8 +7,6 @@ repository = "https://gitlab.com/wunker-bunker/blue-build"
license = "Apache-2.0"
categories = ["command-line-utilities"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.75"
askama = { version = "0.12.1", features = ["serde-json"] }
@ -18,18 +16,23 @@ clap = { version = "4.4.4", features = ["derive"] }
clap-verbosity-flag = "2.1.1"
derive_builder = "0.12.0"
env_logger = "0.10.1"
futures-util = { version = "0.3.30", optional = true }
log = "0.4.20"
podman-api = { version = "0.10.0", optional = true }
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.107"
serde_yaml = "0.9.25"
sigstore = { version = "0.8.0", optional = true }
tokio = { version = "1.35.1", features = ["full"], optional = true }
typed-builder = "0.18.0"
users = "0.11.0"
[features]
default = ["build"]
nightly = ["build"]
default = []
nightly = ["builtin-podman"]
builtin-podman = ["podman-api", "tokio", "futures-util"]
tls = ["podman-api/tls", "builtin-podman"]
init = []
build = []
[dev-dependencies]
rusty-hook = "0.11.2"

View file

@ -22,14 +22,11 @@ default:
BUILD +blue-build-cli-alpine --NIGHTLY=$NIGHTLY
BUILD +installer --NIGHTLY=$NIGHTLY
BUILD +integration-test-template --NIGHTLY=$NIGHTLY
BUILD +integration-test-build --NIGHTLY=$NIGHTLY
nightly:
BUILD +default --NIGHTLY=true
integration-tests:
BUILD +integration-test-template --NIGHTLY=true --NIGHTLY=false
BUILD +integration-test-build --NIGHTLY=true --NIGHTLY=false
lint:
FROM +common
@ -97,11 +94,11 @@ blue-build-cli-alpine:
DO cargo+SAVE_IMAGE --IMAGE=$IMAGE --TAG=$TAG --LATEST=$LATEST --NIGHTLY=$NIGHTLY --ALPINE=true
installer:
FROM alpine
# FROM alpine
FROM mgoltzsche/podman:minimal
ARG NIGHTLY=false
BUILD +install --BUILD_TARGET="x86_64-unknown-linux-gnu" --NIGHTLY=$NIGHTLY
COPY (+install/bb --BUILD_TARGET="x86_64-unknown-linux-gnu") /out/bb
COPY install.sh /install.sh
@ -113,7 +110,8 @@ installer:
DO cargo+SAVE_IMAGE --IMAGE=$IMAGE --TAG=$TAG --LATEST=$LATEST --NIGHTLY=$NIGHTLY --INSTALLER=$INSTALLER
integration-test-template:
FROM DOCKERFILE -f +integration-test-template-containerfile/test/Containerfile +integration-test-template-containerfile/test/*
ARG NIGHTLY=false
FROM DOCKERFILE -f +integration-test-template-containerfile/test/Containerfile +integration-test-template-containerfile/test/* --NIGHTLY=$NIGHTLY
integration-test-template-containerfile:
ARG NIGHTLY=false
@ -126,13 +124,19 @@ integration-test-build:
ARG NIGHTLY=false
FROM +integration-test-base --NIGHTLY=$NIGHTLY
RUN --privileged bb -vv build config/recipe-jp-desktop.yml
RUN --entrypoint --privileged podman info && bb -vv build config/recipe-jp-desktop.yml
integration-test-base:
ARG NIGHTLY=false
FROM +blue-build-cli-alpine --NIGHTLY=$NIGHTLY
RUN echo "#!/bin/sh
echo 'Running podman'" > /usr/bin/podman
RUN echo "#!/bin/sh
echo 'Running buildah'" > /usr/bin/buildah
GIT CLONE https://gitlab.com/wunker-bunker/wunker-os.git /test
WORKDIR /test

View file

@ -1,7 +1,7 @@
<div align="center">
<center>
<figure>
<img src="logos/BlueBuild-banner.png" alt="BlueBuild Banner" height="300" />
<img src="https://gitlab.com/wunker-bunker/blue-build/-/raw/main/logos/BlueBuild-banner.png" alt="BlueBuild Banner" style="max-height: 300px;" />
<figcaption>Graphic Designer: Ian Price</figcaption>
</figure>
</center>

View file

@ -19,6 +19,9 @@ struct BlueBuildArgs {
#[derive(Debug, Subcommand)]
enum CommandArgs {
/// Build an image from a recipe
Build(build::BuildCommand),
/// Generate a Containerfile from a recipe
Template(template::TemplateCommand),
@ -28,10 +31,6 @@ enum CommandArgs {
#[cfg(feature = "init")]
New(init::NewCommand),
/// Build an image from a recipe
#[cfg(feature = "build")]
Build(build::BuildCommand),
}
fn main() {
@ -39,12 +38,15 @@ fn main() {
env_logger::builder()
.filter_level(args.verbosity.log_level_filter())
.filter_module("hyper::proto", log::LevelFilter::Info)
.write_style(WriteStyle::Always)
.init();
trace!("{args:#?}");
match args.command {
CommandArgs::Build(mut command) => command.run(),
CommandArgs::Template(command) => command.run(),
#[cfg(feature = "init")]
@ -52,8 +54,5 @@ fn main() {
#[cfg(feature = "init")]
CommandArgs::New(command) => command.run(),
#[cfg(feature = "build")]
CommandArgs::Build(command) => command.run(),
}
}

View file

@ -1,3 +1,6 @@
#[cfg(feature = "podman-api")]
mod build_strategy;
use std::{
env, fs,
path::{Path, PathBuf},
@ -10,6 +13,22 @@ use log::{debug, error, info, trace, warn};
use typed_builder::TypedBuilder;
use users::{Users, UsersCache};
#[cfg(feature = "podman-api")]
use podman_api::{
api::Image,
opts::{ImageBuildOpts, ImageListOpts, ImagePushOpts, RegistryAuth},
Podman,
};
#[cfg(feature = "podman-api")]
use build_strategy::BuildStrategy;
#[cfg(feature = "futures-util")]
use futures_util::StreamExt;
#[cfg(feature = "tokio")]
use tokio::runtime::Runtime;
use crate::{
ops,
template::{Recipe, TemplateCommand},
@ -47,37 +66,87 @@ pub struct BuildCommand {
/// The registry's domain name.
#[arg(long)]
#[builder(default, setter(into))]
#[builder(default, setter(into, strip_option))]
registry: Option<String>,
/// The url path to your base
/// project images.
#[arg(long)]
#[builder(default, setter(into))]
#[builder(default, setter(into, strip_option))]
registry_path: Option<String>,
/// The username to login to the
/// container registry.
#[arg(short = 'U', long)]
#[builder(default, setter(into))]
#[builder(default, setter(into, strip_option))]
username: Option<String>,
/// The password to login to the
/// container registry.
#[arg(short = 'P', long)]
#[builder(default, setter(into))]
#[builder(default, setter(into, strip_option))]
password: Option<String>,
/// The connection string used to connect
/// to a remote podman socket.
#[cfg(feature = "podman-api")]
#[arg(short, long)]
#[builder(default, setter(into, strip_option))]
connection: Option<String>,
/// The path to the `cert.pem`, `key.pem`,
/// and `ca.pem` files needed to connect to
/// a remote podman build socket.
#[cfg(feature = "tls")]
#[arg(long)]
#[builder(default, setter(into, strip_option))]
tls_path: Option<PathBuf>,
/// Whether to sign the image.
#[cfg(feature = "sigstore")]
#[arg(short, long)]
#[builder(default)]
sign: bool,
/// Path to the public key used to sign the image.
///
/// If the contents of the key are in an environment
/// variable, you can use `env://` to sepcify which
/// variable to read from.
///
/// For example:
///
/// bb build --public-key env://PUBLIC_KEY ...
#[cfg(feature = "sigstore")]
#[arg(long)]
#[builder(default, setter(into, strip_option))]
public_key: Option<String>,
/// Path to the private key used to sign the image.
///
/// If the contents of the key are in an environment
/// variable, you can use `env://` to sepcify which
/// variable to read from.
///
/// For example:
///
/// bb build --private-key env://PRIVATE_KEY ...
#[cfg(feature = "sigstore")]
#[arg(long)]
#[builder(default, setter(into, strip_option))]
private_key: Option<String>,
}
impl BuildCommand {
/// Runs the command and returns a result.
pub fn try_run(&self) -> Result<()> {
pub fn try_run(&mut self) -> Result<()> {
trace!("BuildCommand::try_run()");
if self.push && self.rebase {
bail!("You cannot use '--rebase' and '--push' at the same time");
}
#[cfg(not(feature = "podman-api"))]
if let Err(e1) = ops::check_command_exists("buildah") {
ops::check_command_exists("podman").map_err(|e2| {
anyhow!("Need either 'buildah' or 'podman' commands to proceed: {e1}, {e2}")
@ -109,11 +178,22 @@ impl BuildCommand {
.try_run()?;
info!("Building image for recipe at {}", self.recipe.display());
#[cfg(feature = "podman-api")]
match BuildStrategy::determine_strategy()? {
BuildStrategy::Socket(socket) => {
let rt = Runtime::new()?;
rt.block_on(self.build_image_podman_api(Podman::unix(socket)))
}
_ => self.build_image(),
}
#[cfg(not(feature = "podman-api"))]
self.build_image()
}
/// Runs the command and exits if there is an error.
pub fn run(&self) {
pub fn run(&mut self) {
trace!("BuildCommand::run()");
if let Err(e) = self.try_run() {
@ -122,6 +202,156 @@ impl BuildCommand {
}
}
#[cfg(feature = "podman-api")]
async fn build_image_podman_api(&self, client: Podman) -> Result<()> {
use podman_api::opts::ImageTagOpts;
trace!("BuildCommand::build_image({client:#?})");
let (registry, username, password) = if self.push {
let registry = match (
self.registry.as_ref(),
env::var("CI_REGISTRY").ok(),
env::var("GITHUB_ACTIONS").ok(),
) {
(Some(registry), _, _) => registry.to_owned(),
(None, Some(ci_registry), None) => ci_registry,
(None, None, Some(_)) => "ghcr.io".to_string(),
_ => bail!("Need '--registry' set in order to login"),
};
let username = match (
self.username.as_ref(),
env::var("CI_REGISTRY_USER").ok(),
env::var("GITHUB_ACTOR").ok(),
) {
(Some(username), _, _) => username.to_owned(),
(None, Some(ci_registry_user), None) => ci_registry_user,
(None, None, Some(github_actor)) => github_actor,
_ => bail!("Need '--username' set in order to login"),
};
let password = match (
self.password.as_ref(),
env::var("CI_REGISTRY_PASSWORD").ok(),
env::var("REGISTRY_TOKEN").ok(),
) {
(Some(password), _, _) => password.to_owned(),
(None, Some(ci_registry_password), None) => ci_registry_password,
(None, None, Some(registry_token)) => registry_token,
_ => bail!("Need '--password' set in order to login"),
};
(registry, username, password)
} else {
Default::default()
};
let recipe: Recipe = serde_yaml::from_str(fs::read_to_string(&self.recipe)?.as_str())?;
trace!("recipe: {recipe:#?}");
// Get values for image
let tags = recipe.generate_tags();
let image_name = self.generate_full_image_name(&recipe)?;
let first_image_name = if tags.is_empty() || self.rebase {
image_name.clone()
} else {
format!("{}:{}", &image_name, &tags[0])
};
debug!("Full tag is {first_image_name}");
// Get podman ready to build
let opts = ImageBuildOpts::builder(".")
.tag(&first_image_name)
.dockerfile("Containerfile")
.remove(true)
.layers(true)
.pull(true)
.build();
trace!("Build options: {opts:#?}");
match client.images().build(&opts) {
Ok(mut build_stream) => {
while let Some(chunk) = build_stream.next().await {
match chunk {
Ok(chunk) => debug!("{}", chunk.stream.trim()),
Err(e) => error!("{}", e),
}
}
}
Err(e) => error!("{}", e),
};
if self.push {
debug!("Pushing is enabled");
trace!("cosign login -u {username} -p [MASKED] {registry}");
if !Command::new("cosign")
.arg("login")
.arg("-u")
.arg(&username)
.arg("-p")
.arg(&password)
.arg(&registry)
.output()?
.status
.success()
{
bail!("Failed to login for cosign!");
}
info!("Cosign login success at {registry}");
let first_image = client.images().get(&first_image_name);
for tag in &tags {
let full_image_name = format!("{image_name}:{tag}");
first_image
.tag(&ImageTagOpts::builder().repo(&image_name).tag(tag).build())
.await?;
debug!("Tagged image {full_image_name}");
let new_image = client.images().get(&full_image_name);
info!("Pushing {full_image_name}");
match new_image
.push(
&ImagePushOpts::builder()
.tls_verify(true)
.auth(
RegistryAuth::builder()
.username(&username)
.password(&password)
.server_address(&registry)
.build(),
)
.build(),
)
.await
{
Ok(_) => info!("Pushed {full_image_name} successfully!"),
Err(e) => bail!("Failed to push image: {e}"),
}
}
self.sign_images(&image_name, &tags[0])?;
} else if self.rebase {
debug!("Rebasing onto locally built image {image_name}");
if Command::new("rpm-ostree")
.arg("rebase")
.arg(format!("ostree-unverified-image:{first_image_name}"))
.status()?
.success()
{
info!("Successfully rebased to {first_image_name}");
} else {
bail!("Failed to rebase to {first_image_name}");
}
}
Ok(())
}
fn build_image(&self) -> Result<()> {
trace!("BuildCommand::build_image()");
let recipe: Recipe = serde_yaml::from_str(fs::read_to_string(&self.recipe)?.as_str())?;
@ -208,8 +438,6 @@ impl BuildCommand {
bail!("Failed to login for buildah!");
}
info!("Buildah login success at {registry} for user {username}!");
trace!("cosign login -u {username} -p [MASKED] {registry}");
if !Command::new("cosign")
.arg("login")
@ -224,7 +452,7 @@ impl BuildCommand {
{
bail!("Failed to login for cosign!");
}
info!("Cosign login success at {registry} for user {username}!");
info!("Login success at {registry}");
Ok(())
}

View file

@ -0,0 +1,57 @@
use std::{
env,
path::{Path, PathBuf},
};
use anyhow::{bail, Result};
use log::trace;
use crate::ops;
#[cfg(feature = "podman-api")]
#[derive(Debug, Clone, Default)]
pub enum BuildStrategy {
#[default]
Uninitialized,
Socket(PathBuf),
Buildah,
Podman,
}
#[cfg(feature = "podman-api")]
impl BuildStrategy {
pub fn determine_strategy() -> Result<Self> {
trace!("BuildStrategy::determin_strategy()");
Ok(
match (
env::var("XDG_RUNTIME_DIR"),
PathBuf::from("/run/podman/podman.sock"),
PathBuf::from("/var/run/podman/podman.sock"),
PathBuf::from("/var/run/podman.sock"),
ops::check_command_exists("buildah"),
ops::check_command_exists("podman"),
) {
(Ok(xdg_runtime), _, _, _, _, _)
if Path::new(&format!("{xdg_runtime}/podman/podman.sock")).exists() =>
{
Self::Socket(PathBuf::from(format!("{xdg_runtime}/podman/podman.sock")))
}
(_, run_podman_podman_sock, _, _, _, _) if run_podman_podman_sock.exists() => {
Self::Socket(run_podman_podman_sock)
}
(_, _, var_run_podman_podman_sock, _, _, _)
if var_run_podman_podman_sock.exists() =>
{
Self::Socket(var_run_podman_podman_sock)
}
(_, _, _, var_run_podman_sock, _, _) if var_run_podman_sock.exists() => {
Self::Socket(var_run_podman_sock)
}
(_, _, _, _, Ok(_), _) => Self::Buildah,
(_, _, _, _, _, Ok(_)) => Self::Podman,
_ => bail!("Could not determine strategy"),
},
)
}
}

View file

@ -1,10 +1,15 @@
//! The root library for blue-build.
#![doc(
html_logo_url = "https://gitlab.com/wunker-bunker/blue-build/-/raw/main/logos/BlueBuild-logo.png"
)]
#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![allow(unused_imports)]
#[cfg(feature = "init")]
pub mod init;
#[cfg(feature = "build")]
pub mod build;
mod ops;
pub mod template;

View file

@ -1,11 +1,16 @@
use std::process::Command;
use std::{
env,
path::{Path, PathBuf},
process::Command,
};
use anyhow::{anyhow, Result};
use anyhow::{anyhow, bail, Result};
use clap::ValueEnum;
use log::{debug, trace};
pub fn check_command_exists(command: &str) -> Result<()> {
debug!("Checking if {command} exists");
trace!("check_command_exists({command})");
debug!("Checking if {command} exists");
trace!("which {command}");
match Command::new("which")

View file

@ -49,8 +49,8 @@ pub struct Recipe {
impl Recipe {
pub fn generate_tags(&self) -> Vec<String> {
debug!("Generating image tags for {}", &self.name);
trace!("Recipe::generate_tags()");
debug!("Generating image tags for {}", &self.name);
let mut tags: Vec<String> = Vec::new();
let image_version = &self.image_version;
@ -144,11 +144,12 @@ pub struct Module {
pub struct TemplateCommand {
/// The recipe file to create a template from
#[arg()]
#[builder(setter(into))]
recipe: PathBuf,
/// File to output to instead of STDOUT
#[arg(short, long)]
#[builder(default, setter(into))]
#[builder(default, setter(into, strip_option))]
output: Option<PathBuf>,
}