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
This commit is contained in:
Gerald Pinder 2025-08-09 14:05:59 -04:00 committed by GitHub
parent 2c525854c9
commit 3a0be4099a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 2991 additions and 1857 deletions

View file

@ -207,8 +207,8 @@ jobs:
arm64-build:
timeout-minutes: 90
# runs-on: ubuntu-24.04-arm
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
# runs-on: ubuntu-latest
permissions:
contents: read
packages: write
@ -218,8 +218,8 @@ jobs:
- name: Maximize build space
uses: ublue-os/remove-unwanted-software@cc0becac701cf642c8f0a6613bbdaf5dc36b259e # v9
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
# - name: Set up QEMU
# uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2

445
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -18,10 +18,13 @@ colored = "2"
comlexr = "1"
indexmap = { version = "2", features = ["serde"] }
indicatif = { version = "0.18", features = ["improved_unicode", "rayon"] }
lazy-regex = "3"
log = "0.4"
miette = "7"
nix = { version = "0.29" }
oci-distribution = { version = "0.11", default-features = false }
pretty_assertions = "1"
regex = "1"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
rstest = "0.18"
semver = "1"
@ -78,7 +81,6 @@ jsonschema = "0.30"
open = "5"
os_info = "3"
rayon = "1"
regex = "1"
requestty = { version = "0.5", features = ["macros", "termion"] }
shadow-rs = { version = "1", default-features = false }
thiserror = "2"
@ -94,6 +96,7 @@ indicatif.workspace = true
log.workspace = true
miette = { workspace = true, features = ["fancy"] }
oci-distribution.workspace = true
regex.workspace = true
reqwest.workspace = true
semver.workspace = true
serde.workspace = true
@ -109,6 +112,11 @@ users.workspace = true
# Top level features
default = []
v0_10_0 = [
"bootc"
]
bootc = ["blue-build-process-management/bootc"]
[dev-dependencies]
rusty-hook = "0.11"

View file

@ -43,8 +43,8 @@ build-full:
switch:
FROM +test-base
RUN mkdir -p /etc/bluebuild && touch $BB_TEST_LOCAL_IMAGE
RUN --no-cache bluebuild -v switch recipes/recipe.yml
RUN --no-cache bluebuild -v switch --boot-driver rpm-ostree recipes/recipe.yml
RUN --no-cache bluebuild -v switch --boot-driver bootc recipes/recipe.yml
validate:
FROM +test-base
@ -92,7 +92,7 @@ init:
legacy-base:
FROM ../+blue-build-cli --RELEASE=false
ENV BB_TEST_LOCAL_IMAGE=/etc/bluebuild/cli_test-legacy.tar.gz
ENV BB_TEST_LOCAL_IMAGE=localhost/cli/test:latest
ENV CLICOLOR_FORCE=1
COPY ./mock-scripts/ /usr/bin/
@ -103,13 +103,14 @@ legacy-base:
DO ../+INSTALL --OUT_DIR="/usr/bin/" --BUILD_TARGET="x86_64-unknown-linux-musl" --TAGGED="true"
DO +GEN_KEYPAIR
ENV USER=root
test-base:
FROM ../+blue-build-cli --RELEASE=false
RUN git config --global user.email "you@example.com" && \
git config --global user.name "Your Name"
ENV BB_TEST_LOCAL_IMAGE=/etc/bluebuild/cli_test.tar.gz
ENV BB_TEST_LOCAL_IMAGE=localhost/cli/test:latest
ENV CLICOLOR_FORCE=1
ARG MOCK="true"
@ -121,6 +122,7 @@ test-base:
COPY ./test-repo /test
DO +GEN_KEYPAIR
ENV USER=root
GEN_KEYPAIR:
FUNCTION

View file

@ -0,0 +1,41 @@
#!/bin/bash
set -euo pipefail
if [ "$1" = "switch" ]; then
if [[ "$2" == "--transport=containers-storage" && "$3" == "$BB_TEST_LOCAL_IMAGE" ]]; then
echo "Rebased to local image $BB_TEST_LOCAL_IMAGE"
else
echo "Failed to rebase"
exit 1
fi
elif [ "$1" = "upgrade" ]; then
echo "Performing upgrade for $BB_TEST_LOCAL_IMAGE"
elif [ "$1" = "status" ]; then
cat <<EOF
{
"status": {
"staged": null,
"booted": {
"image": {
"image": {
"image": "ghcr.io/blue-build/cli/test",
"transport": "registry"
}
}
},
"rollback": {
"image": {
"image": {
"image": "ghcr.io/blue-build/cli/test",
"transport": "registry"
}
}
}
}
}
EOF
else
echo "Arg $1 is not recognized"
exit 1
fi

View file

@ -13,6 +13,10 @@ main() {
echo "Exporting image to a tarball (JK JUST A MOCK!)"
echo "${tarpath}"
touch $tarpath
elif [[ "$1" == "image" && "$2" == "scp" ]]; then
echo "Copying image $3 to $4"
elif [[ "$1" == "rmi" && "$2" == "$BB_TEST_LOCAL_IMAGE" ]]; then
echo "Removing image $2"
else
echo 'Running podman'
fi

View file

@ -3,11 +3,11 @@
set -euo pipefail
if [ "$1" = "rebase" ]; then
if [ "$2" = "ostree-unverified-image:oci-archive:$BB_TEST_LOCAL_IMAGE" ]; then
if [ "$2" = "ostree-unverified-image:containers-storage:$BB_TEST_LOCAL_IMAGE" ]; then
echo "Rebased to local image $BB_TEST_LOCAL_IMAGE"
else
echo "Failed to rebase"
exit 1
echo "Failed to rebase"
exit 1
fi
elif [ "$1" = "upgrade" ]; then
echo "Performing upgrade for $BB_TEST_LOCAL_IMAGE"

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-arm64
description: This is my personal OS image.
base-image: quay.io/fedora/fedora-silverblue
base-image: quay.io/fedora/fedora-bootc
image-version: latest
stages:
- from-file: stages.yml

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-buildah
description: This is my personal OS image.
base-image: ghcr.io/ublue-os/silverblue-main
base-image: quay.io/fedora/fedora-bootc
image-version: latest
stages:
- from-file: stages.yml

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-docker-external
description: This is my personal OS image.
base-image: ghcr.io/ublue-os/silverblue-main
base-image: quay.io/fedora/fedora-bootc
image-version: latest
stages:
- from-file: stages.yml

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test
description: This is my personal OS image.
base-image: ghcr.io/ublue-os/silverblue-main
base-image: quay.io/fedora/fedora-bootc
image-version: 40
stages:
- from-file: invalid-stages.yml

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-invalid-module
description: This is my personal OS image.
base-image: ghcr.io/ublue-os/silverblue-main
base-image: quay.io/fedora/fedora-bootc
image-version: 40
stages:
- from-file: stages.yml

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-invalid-stage
description: This is my personal OS image.
base-image: ghcr.io/ublue-os/silverblue-main
base-image: quay.io/fedora/fedora-bootc
image-version: 40
stages:
- name: ubuntu-test

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-invalid
description: 10
base-image: ghcr.io/ublue-os/silverblue-main
base-image: quay.io/fedora/fedora-bootc
image-version:
- 40
- 39

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-podman
description: This is my personal OS image.
base-image: ghcr.io/ublue-os/silverblue-main
base-image: quay.io/fedora/fedora-bootc
image-version: latest
stages:
- from-file: stages.yml

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-rechunk
description: This is my personal OS image.
base-image: ghcr.io/ublue-os/silverblue-main
base-image: quay.io/fedora/fedora-bootc
image-version: latest
stages:
- from-file: stages.yml

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test
description: This is my personal OS image.
base-image: ghcr.io/ublue-os/silverblue-main
base-image: quay.io/fedora/fedora-bootc
image-version: latest
stages:
- from-file: stages.yml

View file

@ -28,6 +28,7 @@ colored.workspace = true
comlexr.workspace = true
indicatif.workspace = true
indexmap.workspace = true
lazy-regex.workspace = true
log.workspace = true
miette.workspace = true
nix = { workspace = true, features = ["signal"] }
@ -44,8 +45,12 @@ uuid.workspace = true
zeroize.workspace = true
[dev-dependencies]
pretty_assertions.workspace = true
rstest.workspace = true
blue-build-utils = { version = "=0.9.22", path = "../utils", features = ["test"] }
[lints]
workspace = true
[features]
bootc = []

View file

@ -10,7 +10,7 @@ use std::{
borrow::Borrow,
fmt::Debug,
process::{ExitStatus, Output},
sync::{Mutex, RwLock},
sync::{LazyLock, RwLock, atomic::AtomicBool},
time::Duration,
};
@ -24,12 +24,13 @@ use log::{info, trace, warn};
use miette::{Result, miette};
use oci_distribution::Reference;
use opts::{
BuildOpts, BuildTagPushOpts, CheckKeyPairOpts, CreateContainerOpts, GenerateImageNameOpts,
GenerateKeyPairOpts, GenerateTagsOpts, GetMetadataOpts, PushOpts, RemoveContainerOpts,
RemoveImageOpts, RunOpts, SignOpts, TagOpts, VerifyOpts,
BuildOpts, BuildTagPushOpts, CheckKeyPairOpts, ContainerOpts, CopyOciDirOpts,
CreateContainerOpts, GenerateImageNameOpts, GenerateKeyPairOpts, GenerateTagsOpts,
GetMetadataOpts, PruneOpts, PushOpts, RechunkOpts, RemoveContainerOpts, RemoveImageOpts,
RunOpts, SignOpts, SwitchOpts, TagOpts, VerifyOpts, VolumeOpts,
};
use types::{
BuildDriverType, CiDriverType, DetermineDriver, ImageMetadata, InspectDriverType, Platform,
BootDriverType, BuildDriverType, CiDriverType, ImageMetadata, InspectDriverType, Platform,
RunDriverType, SigningDriverType,
};
use uuid::Uuid;
@ -39,10 +40,15 @@ use crate::logging::Logger;
pub use self::{
buildah_driver::BuildahDriver, cosign_driver::CosignDriver, docker_driver::DockerDriver,
github_driver::GithubDriver, gitlab_driver::GitlabDriver, local_driver::LocalDriver,
podman_driver::PodmanDriver, sigstore_driver::SigstoreDriver, skopeo_driver::SkopeoDriver,
traits::*,
podman_driver::PodmanDriver, rpm_ostree_driver::RpmOstreeDriver,
sigstore_driver::SigstoreDriver, skopeo_driver::SkopeoDriver, traits::*,
};
#[cfg(feature = "bootc")]
pub use bootc_driver::BootcDriver;
#[cfg(feature = "bootc")]
mod bootc_driver;
mod buildah_driver;
mod cosign_driver;
mod docker_driver;
@ -52,22 +58,25 @@ mod gitlab_driver;
mod local_driver;
pub mod opts;
mod podman_driver;
mod rpm_ostree_driver;
mod sigstore_driver;
mod skopeo_driver;
mod traits;
pub mod types;
static INIT: std::sync::LazyLock<Mutex<bool>> = std::sync::LazyLock::new(|| Mutex::new(false));
static SELECTED_BUILD_DRIVER: std::sync::LazyLock<RwLock<Option<BuildDriverType>>> =
std::sync::LazyLock::new(|| RwLock::new(None));
static SELECTED_INSPECT_DRIVER: std::sync::LazyLock<RwLock<Option<InspectDriverType>>> =
std::sync::LazyLock::new(|| RwLock::new(None));
static SELECTED_RUN_DRIVER: std::sync::LazyLock<RwLock<Option<RunDriverType>>> =
std::sync::LazyLock::new(|| RwLock::new(None));
static SELECTED_SIGNING_DRIVER: std::sync::LazyLock<RwLock<Option<SigningDriverType>>> =
std::sync::LazyLock::new(|| RwLock::new(None));
static SELECTED_CI_DRIVER: std::sync::LazyLock<RwLock<Option<CiDriverType>>> =
std::sync::LazyLock::new(|| RwLock::new(None));
static INIT: AtomicBool = AtomicBool::new(false);
static SELECTED_BUILD_DRIVER: LazyLock<RwLock<Option<BuildDriverType>>> =
LazyLock::new(|| RwLock::new(None));
static SELECTED_INSPECT_DRIVER: LazyLock<RwLock<Option<InspectDriverType>>> =
LazyLock::new(|| RwLock::new(None));
static SELECTED_RUN_DRIVER: LazyLock<RwLock<Option<RunDriverType>>> =
LazyLock::new(|| RwLock::new(None));
static SELECTED_SIGNING_DRIVER: LazyLock<RwLock<Option<SigningDriverType>>> =
LazyLock::new(|| RwLock::new(None));
static SELECTED_CI_DRIVER: LazyLock<RwLock<Option<CiDriverType>>> =
LazyLock::new(|| RwLock::new(None));
static SELECTED_BOOT_DRIVER: LazyLock<RwLock<Option<BootDriverType>>> =
LazyLock::new(|| RwLock::new(None));
/// Args for selecting the various drivers to use for runtime.
///
@ -95,6 +104,9 @@ pub struct DriverArgs {
/// containers.
#[arg(short = 'R', long)]
run_driver: Option<RunDriverType>,
#[arg(short = 'T', long)]
boot_driver: Option<BootDriverType>,
}
macro_rules! impl_driver_type {
@ -108,12 +120,13 @@ macro_rules! impl_driver_init {
(@) => { };
($init:ident; $($tail:tt)*) => {
{
let mut initialized = $init.lock().expect("Must lock INIT");
if !*initialized {
if $init.compare_exchange(
false,
true,
std::sync::atomic::Ordering::AcqRel,
std::sync::atomic::Ordering::Acquire
).is_ok() {
impl_driver_init!(@ $($tail)*);
*initialized = true;
}
}
};
@ -162,6 +175,7 @@ impl Driver {
args.inspect_driver => SELECTED_INSPECT_DRIVER;
args.run_driver => SELECTED_RUN_DRIVER;
args.signing_driver => SELECTED_SIGNING_DRIVER;
args.boot_driver => SELECTED_BOOT_DRIVER;
default => SELECTED_CI_DRIVER;
}
}
@ -206,7 +220,7 @@ impl Driver {
info!("Retrieving OS version from {oci_ref}");
let os_version = Self::get_metadata(
&GetMetadataOpts::builder()
GetMetadataOpts::builder()
.image(oci_ref)
.maybe_platform(platform)
.build(),
@ -247,6 +261,10 @@ impl Driver {
pub fn get_ci_driver() -> CiDriverType {
impl_driver_type!(SELECTED_CI_DRIVER)
}
pub fn get_boot_driver() -> BootDriverType {
impl_driver_type!(SELECTED_BOOT_DRIVER)
}
}
#[cached(
@ -278,9 +296,9 @@ fn get_version_run_image(oci_ref: &Reference) -> Result<u64> {
};
let output = Driver::run_output(
&RunOpts::builder()
.image(oci_ref.to_string())
.args(bon::vec![
RunOpts::builder()
.image(&oci_ref.to_string())
.args(&bon::vec![
"/bin/bash",
"-c",
r#"awk -F= '/^VERSION_ID=/ {gsub(/"/, "", $2); print $2}' /usr/lib/os-release"#,
@ -291,7 +309,7 @@ fn get_version_run_image(oci_ref: &Reference) -> Result<u64> {
)?;
if should_remove {
Driver::remove_image(&RemoveImageOpts::builder().image(oci_ref).build())?;
Driver::remove_image(RemoveImageOpts::builder().image(oci_ref).build())?;
}
progress.finish_and_clear();
@ -314,15 +332,15 @@ macro_rules! impl_build_driver {
}
impl BuildDriver for Driver {
fn build(opts: &BuildOpts) -> Result<()> {
fn build(opts: BuildOpts) -> Result<()> {
impl_build_driver!(build(opts))
}
fn tag(opts: &TagOpts) -> Result<()> {
fn tag(opts: TagOpts) -> Result<()> {
impl_build_driver!(tag(opts))
}
fn push(opts: &PushOpts) -> Result<()> {
fn push(opts: PushOpts) -> Result<()> {
impl_build_driver!(push(opts))
}
@ -330,11 +348,11 @@ impl BuildDriver for Driver {
impl_build_driver!(login())
}
fn prune(opts: &opts::PruneOpts) -> Result<()> {
fn prune(opts: PruneOpts) -> Result<()> {
impl_build_driver!(prune(opts))
}
fn build_tag_push(opts: &BuildTagPushOpts) -> Result<Vec<String>> {
fn build_tag_push(opts: BuildTagPushOpts) -> Result<Vec<String>> {
impl_build_driver!(build_tag_push(opts))
}
}
@ -349,19 +367,19 @@ macro_rules! impl_signing_driver {
}
impl SigningDriver for Driver {
fn generate_key_pair(opts: &GenerateKeyPairOpts) -> Result<()> {
fn generate_key_pair(opts: GenerateKeyPairOpts) -> Result<()> {
impl_signing_driver!(generate_key_pair(opts))
}
fn check_signing_files(opts: &CheckKeyPairOpts) -> Result<()> {
fn check_signing_files(opts: CheckKeyPairOpts) -> Result<()> {
impl_signing_driver!(check_signing_files(opts))
}
fn sign(opts: &SignOpts) -> Result<()> {
fn sign(opts: SignOpts) -> Result<()> {
impl_signing_driver!(sign(opts))
}
fn verify(opts: &VerifyOpts) -> Result<()> {
fn verify(opts: VerifyOpts) -> Result<()> {
impl_signing_driver!(verify(opts))
}
@ -381,7 +399,7 @@ macro_rules! impl_inspect_driver {
}
impl InspectDriver for Driver {
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
fn get_metadata(opts: GetMetadataOpts) -> Result<ImageMetadata> {
impl_inspect_driver!(get_metadata(opts))
}
}
@ -396,23 +414,23 @@ macro_rules! impl_run_driver {
}
impl RunDriver for Driver {
fn run(opts: &RunOpts) -> Result<ExitStatus> {
fn run(opts: RunOpts) -> Result<ExitStatus> {
impl_run_driver!(run(opts))
}
fn run_output(opts: &RunOpts) -> Result<Output> {
fn run_output(opts: RunOpts) -> Result<Output> {
impl_run_driver!(run_output(opts))
}
fn create_container(opts: &CreateContainerOpts) -> Result<types::ContainerId> {
fn create_container(opts: CreateContainerOpts) -> Result<types::ContainerId> {
impl_run_driver!(create_container(opts))
}
fn remove_container(opts: &RemoveContainerOpts) -> Result<()> {
fn remove_container(opts: RemoveContainerOpts) -> Result<()> {
impl_run_driver!(remove_container(opts))
}
fn remove_image(opts: &RemoveImageOpts) -> Result<()> {
fn remove_image(opts: RemoveImageOpts) -> Result<()> {
impl_run_driver!(remove_image(opts))
}
@ -444,7 +462,7 @@ impl CiDriver for Driver {
impl_ci_driver!(oidc_provider())
}
fn generate_tags(opts: &GenerateTagsOpts) -> Result<Vec<String>> {
fn generate_tags(opts: GenerateTagsOpts) -> Result<Vec<String>> {
impl_ci_driver!(generate_tags(opts))
}
@ -469,27 +487,52 @@ impl CiDriver for Driver {
}
impl ContainerMountDriver for Driver {
fn mount_container(opts: &opts::ContainerOpts) -> Result<types::MountId> {
fn mount_container(opts: ContainerOpts) -> Result<types::MountId> {
PodmanDriver::mount_container(opts)
}
fn unmount_container(opts: &opts::ContainerOpts) -> Result<()> {
fn unmount_container(opts: ContainerOpts) -> Result<()> {
PodmanDriver::unmount_container(opts)
}
fn remove_volume(opts: &opts::VolumeOpts) -> Result<()> {
fn remove_volume(opts: VolumeOpts) -> Result<()> {
PodmanDriver::remove_volume(opts)
}
}
impl OciCopy for Driver {
fn copy_oci_dir(opts: &opts::CopyOciDirOpts) -> Result<()> {
fn copy_oci_dir(opts: CopyOciDirOpts) -> Result<()> {
SkopeoDriver::copy_oci_dir(opts)
}
}
impl RechunkDriver for Driver {
fn rechunk(opts: &opts::RechunkOpts) -> Result<Vec<String>> {
fn rechunk(opts: RechunkOpts) -> Result<Vec<String>> {
PodmanDriver::rechunk(opts)
}
}
macro_rules! impl_boot_driver {
($func:ident($($args:expr),*)) => {
match Self::get_boot_driver() {
#[cfg(feature = "bootc")]
BootDriverType::Bootc => BootcDriver::$func($($args,)*),
BootDriverType::RpmOstree => RpmOstreeDriver::$func($($args,)*),
BootDriverType::None => ::miette::bail!("Cannot perform boot operation when no boot driver exists."),
}
};
}
impl BootDriver for Driver {
fn status() -> Result<Box<dyn BootStatus>> {
impl_boot_driver!(status())
}
fn switch(opts: SwitchOpts) -> Result<()> {
impl_boot_driver!(switch(opts))
}
fn upgrade(opts: SwitchOpts) -> Result<()> {
impl_boot_driver!(upgrade(opts))
}
}

View file

@ -0,0 +1,90 @@
use std::ops::Not;
use blue_build_utils::sudo_cmd;
use log::trace;
use miette::{Context, IntoDiagnostic, Result, bail};
use crate::logging::CommandLogging;
use super::{BootDriver, BootStatus, opts::SwitchOpts};
mod status;
pub use status::*;
const SUDO_PROMPT: &str = "Password needed to run bootc";
pub struct BootcDriver;
impl BootDriver for BootcDriver {
fn status() -> Result<Box<dyn BootStatus>> {
let output = {
let c = sudo_cmd!(prompt = SUDO_PROMPT, "bootc", "status", "--format=json");
trace!("{c:?}");
c
}
.output()
.into_diagnostic()?;
if !output.status.success() {
bail!("Failed to get `bootc` status!");
}
trace!("{}", String::from_utf8_lossy(&output.stdout));
Ok(Box::new(
serde_json::from_slice::<BootcStatus>(&output.stdout)
.into_diagnostic()
.wrap_err_with(|| {
format!(
"Failed to deserialize bootc status:\n{}",
String::from_utf8_lossy(&output.stdout)
)
})?,
))
}
fn switch(opts: SwitchOpts) -> Result<()> {
let status = {
let c = sudo_cmd!(
prompt = SUDO_PROMPT,
"bootc",
"switch",
"--transport=containers-storage",
opts.image.to_string(),
);
trace!("{c:?}");
c
}
.build_status(
opts.image.to_string(),
format!("Switching to {}", opts.image),
)
.into_diagnostic()?;
if status.success().not() {
bail!("Failed to switch to {}", opts.image);
}
Ok(())
}
fn upgrade(opts: SwitchOpts) -> Result<()> {
let status = {
let c = sudo_cmd!(prompt = SUDO_PROMPT, "bootc", "upgrade");
trace!("{c:?}");
c
}
.build_status(
opts.image.to_string(),
format!("Switching to {}", opts.image),
)
.into_diagnostic()?;
if status.success().not() {
bail!("Failed to switch to {}", opts.image);
}
Ok(())
}
}

View file

@ -0,0 +1,85 @@
use std::{borrow::Cow, path::PathBuf};
use blue_build_utils::constants::OCI_ARCHIVE;
use log::warn;
use oci_distribution::Reference;
use serde::Deserialize;
use crate::drivers::{BootStatus, types::ImageRef};
#[derive(Deserialize, Debug, Clone)]
pub struct BootcStatus {
status: BootcStatusExt,
}
#[derive(Deserialize, Debug, Clone)]
struct BootcStatusExt {
staged: Option<BootcStatusImage>,
booted: BootcStatusImage,
}
#[derive(Deserialize, Debug, Clone)]
struct BootcStatusImage {
image: BootcStatusImageInfo,
}
#[derive(Deserialize, Debug, Clone)]
struct BootcStatusImageInfo {
image: BootcStatusImageInfoRef,
}
#[derive(Deserialize, Debug, Clone)]
struct BootcStatusImageInfoRef {
image: String,
transport: String,
}
impl BootStatus for BootcStatus {
fn transaction_in_progress(&self) -> bool {
// Any call to bootc when a transaction is in progress
// will cause the process to block effectively making
// this check useless since bootc will continue with
// the operation as soon as the current transaction is
// completed.
false
}
fn booted_image(&self) -> Option<ImageRef<'_>> {
match self.status.booted.image.image.transport.as_str() {
"registry" | "containers-storage" => Some(ImageRef::Remote(Cow::Owned(
Reference::try_from(self.status.booted.image.image.image.as_str())
.inspect_err(|e| {
warn!(
"Failed to parse image ref {}:\n{e}",
self.status.booted.image.image.image
);
})
.ok()?,
))),
transport if transport == OCI_ARCHIVE => Some(ImageRef::LocalTar(Cow::Owned(
PathBuf::from(&self.status.booted.image.image.image),
))),
_ => None,
}
}
fn staged_image(&self) -> Option<ImageRef<'_>> {
let staged = self.status.staged.as_ref()?;
match staged.image.image.transport.as_str() {
"registry" | "containers-storage" => Some(ImageRef::Remote(Cow::Owned(
Reference::try_from(staged.image.image.image.as_str())
.inspect_err(|e| {
warn!(
"Failed to parse image ref {}:\n{e}",
staged.image.image.image
);
})
.ok()?,
))),
transport if transport == OCI_ARCHIVE => Some(ImageRef::LocalTar(Cow::Owned(
PathBuf::from(&staged.image.image.image),
))),
_ => None,
}
}
}

View file

@ -12,7 +12,7 @@ use crate::logging::CommandLogging;
use super::{
BuildDriver, DriverVersion,
opts::{BuildOpts, PushOpts, TagOpts},
opts::{BuildOpts, PruneOpts, PushOpts, TagOpts},
};
#[derive(Debug, Deserialize)]
@ -48,7 +48,7 @@ impl DriverVersion for BuildahDriver {
}
impl BuildDriver for BuildahDriver {
fn build(opts: &BuildOpts) -> Result<()> {
fn build(opts: BuildOpts) -> Result<()> {
trace!("BuildahDriver::build({opts:#?})");
let temp_dir = TempDir::new()
@ -83,7 +83,7 @@ impl BuildDriver for BuildahDriver {
),
],
"-f",
&*opts.containerfile,
opts.containerfile,
"-t",
opts.image.to_string(),
);
@ -101,7 +101,7 @@ impl BuildDriver for BuildahDriver {
Ok(())
}
fn tag(opts: &TagOpts) -> Result<()> {
fn tag(opts: TagOpts) -> Result<()> {
trace!("BuildahDriver::tag({opts:#?})");
let dest_image_str = opts.dest_image.to_string();
@ -122,7 +122,7 @@ impl BuildDriver for BuildahDriver {
Ok(())
}
fn push(opts: &PushOpts) -> Result<()> {
fn push(opts: PushOpts) -> Result<()> {
trace!("BuildahDriver::push({opts:#?})");
let image_str = opts.image.to_string();
@ -195,7 +195,7 @@ impl BuildDriver for BuildahDriver {
Ok(())
}
fn prune(opts: &super::opts::PruneOpts) -> Result<()> {
fn prune(opts: PruneOpts) -> Result<()> {
trace!("PodmanDriver::prune({opts:?})");
let status = cmd!(

View file

@ -21,7 +21,7 @@ use super::{
pub struct CosignDriver;
impl SigningDriver for CosignDriver {
fn generate_key_pair(opts: &GenerateKeyPairOpts) -> Result<()> {
fn generate_key_pair(opts: GenerateKeyPairOpts) -> Result<()> {
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
let status = {
@ -47,7 +47,7 @@ impl SigningDriver for CosignDriver {
Ok(())
}
fn check_signing_files(opts: &CheckKeyPairOpts) -> Result<()> {
fn check_signing_files(opts: CheckKeyPairOpts) -> Result<()> {
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
let priv_key = get_private_key(path)?;
@ -124,7 +124,7 @@ impl SigningDriver for CosignDriver {
Ok(())
}
fn sign(opts: &SignOpts) -> Result<()> {
fn sign(opts: SignOpts) -> Result<()> {
if opts.image.digest().is_none() {
bail!(
"Image ref {} is not a digest ref",
@ -140,7 +140,7 @@ impl SigningDriver for CosignDriver {
};
"cosign",
"sign",
if let Some(ref key) = opts.key => format!("--key={key}"),
if let Some(key) = opts.key => format!("--key={key}"),
"--recursive",
opts.image.to_string(),
);
@ -157,7 +157,7 @@ impl SigningDriver for CosignDriver {
Ok(())
}
fn verify(opts: &VerifyOpts) -> Result<()> {
fn verify(opts: VerifyOpts) -> Result<()> {
let status = {
let c = cmd!(
"cosign",
@ -205,9 +205,8 @@ mod test {
fn generate_key_pair() {
let tempdir = TempDir::new().unwrap();
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
CosignDriver::generate_key_pair(&gen_opts).unwrap();
CosignDriver::generate_key_pair(GenerateKeyPairOpts::builder().dir(tempdir.path()).build())
.unwrap();
eprintln!(
"Private key:\n{}",
@ -218,18 +217,15 @@ mod test {
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
);
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
CosignDriver::check_signing_files(&check_opts).unwrap();
CosignDriver::check_signing_files(CheckKeyPairOpts::builder().dir(tempdir.path()).build())
.unwrap();
}
#[test]
fn check_key_pairs() {
let path = Path::new("../test-files/keys");
let opts = CheckKeyPairOpts::builder().dir(path).build();
CosignDriver::check_signing_files(&opts).unwrap();
CosignDriver::check_signing_files(CheckKeyPairOpts::builder().dir(path).build()).unwrap();
}
#[test]
@ -238,9 +234,8 @@ mod test {
let tempdir = TempDir::new().unwrap();
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
CosignDriver::generate_key_pair(&gen_opts).unwrap();
CosignDriver::generate_key_pair(GenerateKeyPairOpts::builder().dir(tempdir.path()).build())
.unwrap();
eprintln!(
"Private key:\n{}",
@ -251,8 +246,9 @@ mod test {
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
);
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
SigstoreDriver::check_signing_files(&check_opts).unwrap();
SigstoreDriver::check_signing_files(
CheckKeyPairOpts::builder().dir(tempdir.path()).build(),
)
.unwrap();
}
}

View file

@ -36,7 +36,7 @@ use crate::{
signal_handler::{ContainerRuntime, ContainerSignalId, add_cid, remove_cid},
};
use super::opts::{CreateContainerOpts, RemoveContainerOpts, RemoveImageOpts};
use super::opts::{CreateContainerOpts, PruneOpts, RemoveContainerOpts, RemoveImageOpts};
#[derive(Debug, Deserialize)]
struct VerisonJsonClient {
@ -192,7 +192,7 @@ impl DriverVersion for DockerDriver {
}
impl BuildDriver for DockerDriver {
fn build(opts: &BuildOpts) -> Result<()> {
fn build(opts: BuildOpts) -> Result<()> {
trace!("DockerDriver::build({opts:#?})");
let temp_dir = TempDir::new()
@ -232,7 +232,7 @@ impl BuildDriver for DockerDriver {
repository = cache_to.repository(),
),
],
&*opts.containerfile,
opts.containerfile,
".",
);
trace!("{c:?}");
@ -249,7 +249,7 @@ impl BuildDriver for DockerDriver {
Ok(())
}
fn tag(opts: &TagOpts) -> Result<()> {
fn tag(opts: TagOpts) -> Result<()> {
trace!("DockerDriver::tag({opts:#?})");
let dest_image_str = opts.dest_image.to_string();
@ -270,7 +270,7 @@ impl BuildDriver for DockerDriver {
Ok(())
}
fn push(opts: &PushOpts) -> Result<()> {
fn push(opts: PushOpts) -> Result<()> {
trace!("DockerDriver::push({opts:#?})");
let image_str = opts.image.to_string();
@ -328,7 +328,7 @@ impl BuildDriver for DockerDriver {
Ok(())
}
fn prune(opts: &super::opts::PruneOpts) -> Result<()> {
fn prune(opts: PruneOpts) -> Result<()> {
trace!("DockerDriver::prune({opts:?})");
let (system, buildx) = std::thread::scope(
@ -385,7 +385,7 @@ impl BuildDriver for DockerDriver {
Ok(())
}
fn build_tag_push(opts: &BuildTagPushOpts) -> Result<Vec<String>> {
fn build_tag_push(opts: BuildTagPushOpts) -> Result<Vec<String>> {
trace!("DockerDriver::build_tag_push({opts:#?})");
let temp_dir = TempDir::new()
@ -420,7 +420,7 @@ impl BuildDriver for DockerDriver {
}
fn build_tag_push_cmd(
opts: &BuildTagPushOpts<'_>,
opts: BuildTagPushOpts<'_>,
first_image: &str,
temp_dir: &TempDir,
) -> Result<Command> {
@ -461,7 +461,7 @@ fn build_tag_push_cmd(
platform.to_string(),
],
"-f",
&*opts.containerfile,
opts.containerfile,
if let Some(cache_from) = opts.cache_from.as_ref() => [
"--cache-from",
format!(
@ -479,7 +479,7 @@ fn build_tag_push_cmd(
Ok(c)
}
fn get_final_images(opts: &BuildTagPushOpts<'_>) -> Vec<String> {
fn get_final_images(opts: BuildTagPushOpts<'_>) -> Vec<String> {
match &opts.image {
ImageRef::Remote(image) => {
if opts.tags.is_empty() {
@ -495,11 +495,14 @@ fn get_final_images(opts: &BuildTagPushOpts<'_>) -> Vec<String> {
ImageRef::LocalTar(archive_path) => {
string_vec![archive_path.display().to_string()]
}
ImageRef::Other(other) => {
string_vec![&**other]
}
}
}
impl InspectDriver for DockerDriver {
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
fn get_metadata(opts: GetMetadataOpts) -> Result<ImageMetadata> {
get_metadata_cache(opts)
}
}
@ -510,7 +513,7 @@ impl InspectDriver for DockerDriver {
convert = r#"{ format!("{}-{:?}", opts.image, opts.platform)}"#,
sync_writes = "by_key"
)]
fn get_metadata_cache(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
fn get_metadata_cache(opts: GetMetadataOpts) -> Result<ImageMetadata> {
trace!("DockerDriver::get_metadata({opts:#?})");
let image_str = opts.image.to_string();
@ -547,7 +550,7 @@ fn get_metadata_cache(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
}
impl RunDriver for DockerDriver {
fn run(opts: &RunOpts) -> Result<ExitStatus> {
fn run(opts: RunOpts) -> Result<ExitStatus> {
trace!("DockerDriver::run({opts:#?})");
let cid_path = TempDir::new().into_diagnostic()?;
@ -557,7 +560,7 @@ impl RunDriver for DockerDriver {
add_cid(&cid);
let status = docker_run(opts, &cid_file)
.build_status(&*opts.image, "Running container")
.build_status(opts.image, "Running container")
.into_diagnostic()?;
remove_cid(&cid);
@ -565,7 +568,7 @@ impl RunDriver for DockerDriver {
Ok(status)
}
fn run_output(opts: &RunOpts) -> Result<std::process::Output> {
fn run_output(opts: RunOpts) -> Result<std::process::Output> {
trace!("DockerDriver::run({opts:#?})");
let cid_path = TempDir::new().into_diagnostic()?;
@ -581,7 +584,7 @@ impl RunDriver for DockerDriver {
Ok(output)
}
fn create_container(opts: &CreateContainerOpts) -> Result<super::types::ContainerId> {
fn create_container(opts: CreateContainerOpts) -> Result<super::types::ContainerId> {
trace!("DockerDriver::create_container({opts:?})");
let output = {
@ -601,7 +604,7 @@ impl RunDriver for DockerDriver {
))
}
fn remove_container(opts: &RemoveContainerOpts) -> Result<()> {
fn remove_container(opts: RemoveContainerOpts) -> Result<()> {
trace!("DockerDriver::remove_container({opts:?})");
let output = {
@ -619,7 +622,7 @@ impl RunDriver for DockerDriver {
Ok(())
}
fn remove_image(opts: &RemoveImageOpts) -> Result<()> {
fn remove_image(opts: RemoveImageOpts) -> Result<()> {
trace!("DockerDriver::remove_image({opts:?})");
let output = {
@ -675,7 +678,7 @@ impl RunDriver for DockerDriver {
}
}
fn docker_run(opts: &RunOpts, cid_file: &Path) -> Command {
fn docker_run(opts: RunOpts, cid_file: &Path) -> Command {
let command = cmd!(
"docker",
"run",
@ -693,7 +696,7 @@ fn docker_run(opts: &RunOpts, cid_file: &Path) -> Command {
"--env",
format!("{key}={value}"),
],
&*opts.image,
opts.image,
for arg in opts.args.iter() => &**arg,
);
trace!("{command:?}");

View file

@ -36,7 +36,7 @@ impl CiDriver for GithubDriver {
Ok(GITHUB_TOKEN_ISSUER_URL.to_string())
}
fn generate_tags(opts: &GenerateTagsOpts) -> miette::Result<Vec<String>> {
fn generate_tags(opts: GenerateTagsOpts) -> miette::Result<Vec<String>> {
const PR_EVENT: &str = "pull_request";
let timestamp = blue_build_utils::get_tag_timestamp();
let os_version = Driver::get_os_version()
@ -142,8 +142,6 @@ impl CiDriver for GithubDriver {
#[cfg(test)]
mod test {
use std::borrow::Cow;
use blue_build_utils::{
constants::{
GITHUB_EVENT_NAME, GITHUB_EVENT_PATH, GITHUB_REF_NAME, GITHUB_SHA, PR_EVENT_NUMBER,
@ -286,7 +284,7 @@ mod test {
)]
fn generate_tags(
#[case] setup: impl FnOnce(),
#[case] alt_tags: Option<Vec<Cow<'_, str>>>,
#[case] alt_tags: Option<Vec<String>>,
#[case] mut expected: Vec<String>,
) {
setup();
@ -294,9 +292,9 @@ mod test {
let oci_ref: Reference = "ghcr.io/ublue-os/silverblue-main".parse().unwrap();
let mut tags = GithubDriver::generate_tags(
&GenerateTagsOpts::builder()
GenerateTagsOpts::builder()
.oci_ref(&oci_ref)
.maybe_alt_tags(alt_tags)
.maybe_alt_tags(alt_tags.as_deref())
.platform(Platform::LinuxAmd64)
.build(),
)

View file

@ -45,7 +45,7 @@ impl CiDriver for GitlabDriver {
))
}
fn generate_tags(opts: &GenerateTagsOpts) -> miette::Result<Vec<String>> {
fn generate_tags(opts: GenerateTagsOpts) -> miette::Result<Vec<String>> {
const MR_EVENT: &str = "merge_request_event";
let os_version = Driver::get_os_version()
.oci_ref(opts.oci_ref)
@ -151,8 +151,6 @@ impl CiDriver for GitlabDriver {
#[cfg(test)]
mod test {
use std::borrow::Cow;
use blue_build_utils::{
constants::{
CI_COMMIT_REF_NAME, CI_COMMIT_SHORT_SHA, CI_DEFAULT_BRANCH, CI_MERGE_REQUEST_IID,
@ -293,7 +291,7 @@ mod test {
)]
fn generate_tags(
#[case] setup: impl FnOnce(),
#[case] alt_tags: Option<Vec<Cow<'_, str>>>,
#[case] alt_tags: Option<Vec<String>>,
#[case] mut expected: Vec<String>,
) {
setup();
@ -301,9 +299,9 @@ mod test {
let oci_ref: Reference = "ghcr.io/ublue-os/silverblue-main".parse().unwrap();
let mut tags = GitlabDriver::generate_tags(
&GenerateTagsOpts::builder()
GenerateTagsOpts::builder()
.oci_ref(&oci_ref)
.maybe_alt_tags(alt_tags)
.maybe_alt_tags(alt_tags.as_deref())
.platform(crate::drivers::types::Platform::LinuxAmd64)
.build(),
)

View file

@ -3,6 +3,7 @@ use std::path::PathBuf;
use blue_build_utils::string_vec;
use comlexr::cmd;
use log::trace;
use miette::bail;
use super::{CiDriver, Driver, opts::GenerateTagsOpts};
@ -15,14 +16,14 @@ impl CiDriver for LocalDriver {
}
fn keyless_cert_identity() -> miette::Result<String> {
unimplemented!()
bail!("Unimplemented for local")
}
fn oidc_provider() -> miette::Result<String> {
unimplemented!()
bail!("Unimplemented for local")
}
fn generate_tags(opts: &GenerateTagsOpts) -> miette::Result<Vec<String>> {
fn generate_tags(opts: GenerateTagsOpts) -> miette::Result<Vec<String>> {
trace!("LocalDriver::generate_tags({opts:?})");
let os_version = Driver::get_os_version()
.oci_ref(opts.oci_ref)

View file

@ -1,5 +1,6 @@
use clap::ValueEnum;
pub use boot::*;
pub use build::*;
pub use ci::*;
pub use inspect::*;
@ -7,6 +8,7 @@ pub use rechunk::*;
pub use run::*;
pub use signing::*;
mod boot;
mod build;
mod ci;
mod inspect;

View file

@ -0,0 +1,10 @@
use bon::Builder;
use oci_distribution::Reference;
#[derive(Debug, Clone, Copy, Builder)]
pub struct SwitchOpts<'scope> {
pub image: &'scope Reference,
#[builder(default)]
pub reboot: bool,
}

View file

@ -1,4 +1,4 @@
use std::{borrow::Cow, collections::HashSet, path::Path};
use std::path::Path;
use blue_build_utils::secret::Secret;
use bon::Builder;
@ -9,16 +9,14 @@ use crate::drivers::types::{ImageRef, Platform};
use super::CompressionType;
/// Options for building
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct BuildOpts<'scope> {
#[builder(into)]
pub image: ImageRef<'scope>,
pub image: &'scope ImageRef<'scope>,
#[builder(default)]
pub squash: bool,
#[builder(into)]
pub containerfile: Cow<'scope, Path>,
pub containerfile: &'scope Path,
pub platform: Option<Platform>,
@ -27,18 +25,14 @@ pub struct BuildOpts<'scope> {
#[builder(default)]
pub privileged: bool,
#[builder(into)]
pub cache_from: Option<&'scope Reference>,
#[builder(into)]
pub cache_to: Option<&'scope Reference>,
#[builder(default)]
pub secrets: HashSet<&'scope Secret>,
pub secrets: &'scope [&'scope Secret],
}
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct TagOpts<'scope> {
pub src_image: &'scope Reference,
pub dest_image: &'scope Reference,
@ -47,7 +41,7 @@ pub struct TagOpts<'scope> {
pub privileged: bool,
}
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct PushOpts<'scope> {
pub image: &'scope Reference,
pub compression_type: Option<CompressionType>,
@ -56,7 +50,7 @@ pub struct PushOpts<'scope> {
pub privileged: bool,
}
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct PruneOpts {
pub all: bool,
pub volumes: bool,
@ -64,19 +58,17 @@ pub struct PruneOpts {
/// Options for building, tagging, and pusing images.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct BuildTagPushOpts<'scope> {
/// The base image name.
#[builder(into)]
pub image: ImageRef<'scope>,
pub image: &'scope ImageRef<'scope>,
/// The path to the Containerfile to build.
#[builder(into)]
pub containerfile: Cow<'scope, Path>,
pub containerfile: &'scope Path,
/// The list of tags for the image being built.
#[builder(default, into)]
pub tags: Vec<Cow<'scope, str>>,
#[builder(default)]
pub tags: &'scope [String],
/// Enable pushing the image.
#[builder(default)]
@ -115,5 +107,5 @@ pub struct BuildTagPushOpts<'scope> {
/// Secrets to mount
#[builder(default)]
pub secrets: HashSet<&'scope Secret>,
pub secrets: &'scope [&'scope Secret],
}

View file

@ -1,28 +1,21 @@
use std::borrow::Cow;
use bon::Builder;
use oci_distribution::Reference;
use crate::drivers::types::Platform;
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct GenerateTagsOpts<'scope> {
pub oci_ref: &'scope Reference,
#[builder(into)]
pub alt_tags: Option<Vec<Cow<'scope, str>>>,
pub alt_tags: Option<&'scope [String]>,
pub platform: Option<Platform>,
}
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct GenerateImageNameOpts<'scope> {
#[builder(into)]
pub name: Cow<'scope, str>,
#[builder(into)]
pub registry: Option<Cow<'scope, str>>,
#[builder(into)]
pub registry_namespace: Option<Cow<'scope, str>>,
pub name: &'scope str,
pub registry: Option<&'scope str>,
pub registry_namespace: Option<&'scope str>,
}

View file

@ -3,7 +3,7 @@ use oci_distribution::Reference;
use crate::drivers::types::Platform;
#[derive(Debug, Clone, Builder, Hash)]
#[derive(Debug, Clone, Copy, Builder, Hash)]
#[builder(derive(Clone))]
pub struct GetMetadataOpts<'scope> {
#[builder(into)]

View file

@ -1,4 +1,4 @@
use std::{borrow::Cow, collections::HashSet, path::Path};
use std::path::Path;
use blue_build_utils::secret::Secret;
use bon::Builder;
@ -8,25 +8,22 @@ use crate::drivers::types::{ContainerId, OciDir, Platform};
use super::CompressionType;
#[derive(Debug, Clone, Builder)]
#[builder(on(Cow<'_, str>, into))]
#[derive(Debug, Clone, Copy, Builder)]
pub struct RechunkOpts<'scope> {
pub image: Cow<'scope, str>,
#[builder(into)]
pub containerfile: Cow<'scope, Path>,
pub image: &'scope str,
pub containerfile: &'scope Path,
pub platform: Option<Platform>,
pub version: Cow<'scope, str>,
pub name: Cow<'scope, str>,
pub description: Cow<'scope, str>,
pub base_digest: Cow<'scope, str>,
pub base_image: Cow<'scope, str>,
pub repo: Cow<'scope, str>,
pub version: &'scope str,
pub name: &'scope str,
pub description: &'scope str,
pub base_digest: &'scope str,
pub base_image: &'scope str,
pub repo: &'scope str,
/// The list of tags for the image being built.
#[builder(default, into)]
pub tags: Vec<Cow<'scope, str>>,
#[builder(default)]
pub tags: &'scope [String],
/// Enable pushing the image.
#[builder(default)]
@ -57,10 +54,10 @@ pub struct RechunkOpts<'scope> {
pub cache_to: Option<&'scope Reference>,
#[builder(default)]
pub secrets: HashSet<&'scope Secret>,
pub secrets: &'scope [&'scope Secret],
}
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct ContainerOpts<'scope> {
pub container_id: &'scope ContainerId,
@ -68,16 +65,15 @@ pub struct ContainerOpts<'scope> {
pub privileged: bool,
}
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct VolumeOpts<'scope> {
#[builder(into)]
pub volume_id: Cow<'scope, str>,
pub volume_id: &'scope str,
#[builder(default)]
pub privileged: bool,
}
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct CopyOciDirOpts<'scope> {
pub oci_dir: &'scope OciDir,
pub registry: &'scope Reference,

View file

@ -1,26 +1,21 @@
use std::borrow::Cow;
use bon::Builder;
use oci_distribution::Reference;
use crate::drivers::types::ContainerId;
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct RunOpts<'scope> {
#[builder(into)]
pub image: Cow<'scope, str>,
pub image: &'scope str,
#[builder(default, into)]
pub args: Vec<Cow<'scope, str>>,
#[builder(default)]
pub args: &'scope [String],
#[builder(default, into)]
pub env_vars: Vec<RunOptsEnv<'scope>>,
#[builder(default)]
pub env_vars: &'scope [RunOptsEnv<'scope>],
#[builder(default, into)]
pub volumes: Vec<RunOptsVolume<'scope>>,
#[builder(into)]
pub user: Option<Cow<'scope, str>>,
#[builder(default)]
pub volumes: &'scope [RunOptsVolume<'scope>],
pub user: Option<&'scope str>,
#[builder(default)]
pub privileged: bool,
@ -32,13 +27,10 @@ pub struct RunOpts<'scope> {
pub remove: bool,
}
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct RunOptsVolume<'scope> {
#[builder(into)]
pub path_or_vol_name: Cow<'scope, str>,
#[builder(into)]
pub container_path: Cow<'scope, str>,
pub path_or_vol_name: &'scope str,
pub container_path: &'scope str,
}
#[macro_export]
@ -55,13 +47,10 @@ macro_rules! run_volumes {
};
}
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct RunOptsEnv<'scope> {
#[builder(into)]
pub key: Cow<'scope, str>,
#[builder(into)]
pub value: Cow<'scope, str>,
pub key: &'scope str,
pub value: &'scope str,
}
#[macro_export]
@ -78,7 +67,7 @@ macro_rules! run_envs {
};
}
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct CreateContainerOpts<'scope> {
pub image: &'scope Reference,
@ -86,7 +75,7 @@ pub struct CreateContainerOpts<'scope> {
pub privileged: bool,
}
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct RemoveContainerOpts<'scope> {
pub container_id: &'scope ContainerId,
@ -94,7 +83,7 @@ pub struct RemoveContainerOpts<'scope> {
pub privileged: bool,
}
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct RemoveImageOpts<'scope> {
pub image: &'scope Reference,

View file

@ -1,5 +1,4 @@
use std::{
borrow::Cow,
fs,
path::{Path, PathBuf},
};
@ -12,6 +11,7 @@ use zeroize::{Zeroize, Zeroizing};
use crate::drivers::types::Platform;
#[derive(Debug)]
pub enum PrivateKey {
Env(String),
Path(PathBuf),
@ -56,53 +56,42 @@ impl PrivateKeyContents<String> for PrivateKey {
}
}
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct GenerateKeyPairOpts<'scope> {
#[builder(into)]
pub dir: Option<Cow<'scope, Path>>,
pub dir: Option<&'scope Path>,
}
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct CheckKeyPairOpts<'scope> {
#[builder(into)]
pub dir: Option<Cow<'scope, Path>>,
pub dir: Option<&'scope Path>,
}
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct SignOpts<'scope> {
#[builder(into)]
pub image: &'scope Reference,
#[builder(into)]
pub key: Option<Cow<'scope, str>>,
#[builder(into)]
pub dir: Option<Cow<'scope, Path>>,
pub key: Option<&'scope PrivateKey>,
pub dir: Option<&'scope Path>,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
pub enum VerifyType<'scope> {
File(Cow<'scope, Path>),
File(&'scope Path),
Keyless {
issuer: Cow<'scope, str>,
identity: Cow<'scope, str>,
issuer: &'scope str,
identity: &'scope str,
},
}
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct VerifyOpts<'scope> {
#[builder(into)]
pub image: &'scope Reference,
pub verify_type: VerifyType<'scope>,
}
#[derive(Debug, Clone, Builder)]
#[derive(Debug, Clone, Copy, Builder)]
pub struct SignVerifyOpts<'scope> {
#[builder(into)]
pub image: &'scope Reference,
#[builder(into)]
pub dir: Option<Cow<'scope, Path>>,
pub dir: Option<&'scope Path>,
/// Enable retry logic for pushing.
#[builder(default)]

View file

@ -1,13 +1,14 @@
use std::{
collections::HashMap,
ops::Not,
path::Path,
process::{Command, ExitStatus},
time::Duration,
};
use blue_build_utils::{
constants::SUDO_ASKPASS, credentials::Credentials, has_env_var, running_as_root,
secret::SecretArgs, semver::Version,
constants::USER, credentials::Credentials, get_env_var, secret::SecretArgs, semver::Version,
sudo_cmd,
};
use cached::proc_macro::cached;
use colored::Colorize;
@ -21,7 +22,10 @@ use tempfile::TempDir;
use super::{
ContainerMountDriver, RechunkDriver,
opts::{CreateContainerOpts, RemoveContainerOpts, RemoveImageOpts},
opts::{
ContainerOpts, CreateContainerOpts, PruneOpts, RemoveContainerOpts, RemoveImageOpts,
VolumeOpts,
},
types::{ContainerId, MountId},
};
use crate::{
@ -108,6 +112,42 @@ struct PodmanVersionJson {
#[derive(Debug)]
pub struct PodmanDriver;
impl PodmanDriver {
/// Copy an image from the user container
/// store to the root container store for
/// booting off of.
///
/// # Errors
/// Will error if the image can't be copied.
pub fn copy_image_to_root_store(image: &Reference) -> Result<()> {
let image = image.whole();
let status = {
let c = sudo_cmd!(
prompt = SUDO_PROMPT,
"podman",
"image",
"scp",
format!("{}@localhost::{image}", get_env_var(USER)?),
"root@localhost::"
);
trace!("{c:?}");
c
}
.build_status(&image, "Copying image to root container store")
// .status()
.into_diagnostic()?;
if status.success().not() {
bail!(
"Failed to copy image {} to root container store",
image.bold()
);
}
Ok(())
}
}
impl DriverVersion for PodmanDriver {
// First podman version to use buildah v1.24
// https://github.com/containers/podman/blob/main/RELEASE_NOTES.md#400
@ -134,29 +174,17 @@ impl DriverVersion for PodmanDriver {
}
impl BuildDriver for PodmanDriver {
fn build(opts: &BuildOpts) -> Result<()> {
fn build(opts: BuildOpts) -> Result<()> {
trace!("PodmanDriver::build({opts:#?})");
let temp_dir = TempDir::new()
.into_diagnostic()
.wrap_err("Failed to create temporary directory for secrets")?;
let use_sudo = opts.privileged && !running_as_root();
let command = cmd!(
if use_sudo {
"sudo"
} else {
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => [
"--preserve-env",
"podman",
],
let command = sudo_cmd!(
prompt = SUDO_PROMPT,
sudo_check = opts.privileged,
"podman",
"build",
if let Some(platform) = opts.platform => [
"--platform",
@ -182,7 +210,7 @@ impl BuildDriver for PodmanDriver {
if opts.host_network => "--net=host",
format!("--layers={}", !opts.squash),
"-f",
&*opts.containerfile,
opts.containerfile,
"-t",
opts.image.to_string(),
for opts.secrets.args(&temp_dir)?,
@ -203,24 +231,15 @@ impl BuildDriver for PodmanDriver {
Ok(())
}
fn tag(opts: &TagOpts) -> Result<()> {
fn tag(opts: TagOpts) -> Result<()> {
trace!("PodmanDriver::tag({opts:#?})");
let dest_image_str = opts.dest_image.to_string();
let use_sudo = opts.privileged && !running_as_root();
let mut command = cmd!(
if use_sudo {
"sudo"
} else {
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
let mut command = sudo_cmd!(
prompt = SUDO_PROMPT,
sudo_check = opts.privileged,
"podman",
"tag",
opts.src_image.to_string(),
&dest_image_str
@ -237,24 +256,15 @@ impl BuildDriver for PodmanDriver {
Ok(())
}
fn push(opts: &PushOpts) -> Result<()> {
fn push(opts: PushOpts) -> Result<()> {
trace!("PodmanDriver::push({opts:#?})");
let image_str = opts.image.to_string();
let use_sudo = opts.privileged && !running_as_root();
let command = cmd!(
if use_sudo {
"sudo"
} else {
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
let command = sudo_cmd!(
prompt = SUDO_PROMPT,
sudo_check = opts.privileged,
"podman",
"push",
format!(
"--compression-format={}",
@ -312,7 +322,7 @@ impl BuildDriver for PodmanDriver {
Ok(())
}
fn prune(opts: &super::opts::PruneOpts) -> Result<()> {
fn prune(opts: PruneOpts) -> Result<()> {
trace!("PodmanDriver::prune({opts:?})");
let status = {
@ -339,7 +349,7 @@ impl BuildDriver for PodmanDriver {
}
impl InspectDriver for PodmanDriver {
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
fn get_metadata(opts: GetMetadataOpts) -> Result<ImageMetadata> {
get_metadata_cache(opts)
}
}
@ -350,7 +360,7 @@ impl InspectDriver for PodmanDriver {
convert = r#"{ format!("{}-{:?}", opts.image, opts.platform)}"#,
sync_writes = "by_key"
)]
fn get_metadata_cache(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
fn get_metadata_cache(opts: GetMetadataOpts) -> Result<ImageMetadata> {
trace!("PodmanDriver::get_metadata({opts:#?})");
let image_str = opts.image.to_string();
@ -409,21 +419,12 @@ fn get_metadata_cache(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
}
impl ContainerMountDriver for PodmanDriver {
fn mount_container(opts: &super::opts::ContainerOpts) -> Result<MountId> {
let use_sudo = opts.privileged && !running_as_root();
fn mount_container(opts: ContainerOpts) -> Result<MountId> {
let output = {
let c = cmd!(
if use_sudo {
"sudo"
} else {
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
let c = sudo_cmd!(
prompt = SUDO_PROMPT,
sudo_check = opts.privileged,
"podman",
"mount",
opts.container_id,
);
@ -442,21 +443,12 @@ impl ContainerMountDriver for PodmanDriver {
))
}
fn unmount_container(opts: &super::opts::ContainerOpts) -> Result<()> {
let use_sudo = opts.privileged && !running_as_root();
fn unmount_container(opts: ContainerOpts) -> Result<()> {
let output = {
let c = cmd!(
if use_sudo {
"sudo"
} else {
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
let c = sudo_cmd!(
prompt = SUDO_PROMPT,
sudo_check = opts.privileged,
"podman",
"unmount",
opts.container_id
);
@ -473,24 +465,15 @@ impl ContainerMountDriver for PodmanDriver {
Ok(())
}
fn remove_volume(opts: &super::opts::VolumeOpts) -> Result<()> {
let use_sudo = opts.privileged && !running_as_root();
fn remove_volume(opts: VolumeOpts) -> Result<()> {
let output = {
let c = cmd!(
if use_sudo {
"sudo"
} else {
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
let c = sudo_cmd!(
prompt = SUDO_PROMPT,
sudo_check = opts.privileged,
"podman",
"volume",
"rm",
&*opts.volume_id
opts.volume_id
);
trace!("{c:?}");
c
@ -509,7 +492,7 @@ impl ContainerMountDriver for PodmanDriver {
impl RechunkDriver for PodmanDriver {}
impl RunDriver for PodmanDriver {
fn run(opts: &RunOpts) -> Result<ExitStatus> {
fn run(opts: RunOpts) -> Result<ExitStatus> {
trace!("PodmanDriver::run({opts:#?})");
let cid_path = TempDir::new().into_diagnostic()?;
@ -520,7 +503,7 @@ impl RunDriver for PodmanDriver {
add_cid(&cid);
let status = podman_run(opts, &cid_file)
.build_status(&*opts.image, "Running container")
.build_status(opts.image, "Running container")
.into_diagnostic()?;
remove_cid(&cid);
@ -528,7 +511,7 @@ impl RunDriver for PodmanDriver {
Ok(status)
}
fn run_output(opts: &RunOpts) -> Result<std::process::Output> {
fn run_output(opts: RunOpts) -> Result<std::process::Output> {
trace!("PodmanDriver::run_output({opts:#?})");
let cid_path = TempDir::new().into_diagnostic()?;
@ -545,23 +528,14 @@ impl RunDriver for PodmanDriver {
Ok(output)
}
fn create_container(opts: &CreateContainerOpts) -> Result<ContainerId> {
fn create_container(opts: CreateContainerOpts) -> Result<ContainerId> {
trace!("PodmanDriver::create_container({opts:?})");
let use_sudo = opts.privileged && !running_as_root();
let output = {
let c = cmd!(
if use_sudo {
"sudo"
} else {
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
let c = sudo_cmd!(
prompt = SUDO_PROMPT,
sudo_check = opts.privileged,
"podman",
"create",
opts.image.to_string(),
"bash"
@ -581,23 +555,14 @@ impl RunDriver for PodmanDriver {
))
}
fn remove_container(opts: &RemoveContainerOpts) -> Result<()> {
fn remove_container(opts: RemoveContainerOpts) -> Result<()> {
trace!("PodmanDriver::remove_container({opts:?})");
let use_sudo = opts.privileged && !running_as_root();
let output = {
let c = cmd!(
if use_sudo {
"sudo"
} else {
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
let c = sudo_cmd!(
prompt = SUDO_PROMPT,
sudo_check = opts.privileged,
"podman",
"rm",
opts.container_id,
);
@ -614,23 +579,14 @@ impl RunDriver for PodmanDriver {
Ok(())
}
fn remove_image(opts: &RemoveImageOpts) -> Result<()> {
fn remove_image(opts: RemoveImageOpts) -> Result<()> {
trace!("PodmanDriver::remove_image({opts:?})");
let use_sudo = opts.privileged && !running_as_root();
let output = {
let c = cmd!(
if use_sudo {
"sudo"
} else {
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
let c = sudo_cmd!(
prompt = SUDO_PROMPT,
sudo_check = opts.privileged,
"podman",
"rmi",
opts.image.to_string()
);
@ -656,20 +612,11 @@ impl RunDriver for PodmanDriver {
trace!("PodmanDriver::list_images({privileged})");
let use_sudo = privileged && !running_as_root();
let output = {
let c = cmd!(
if use_sudo {
"sudo"
} else {
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
let c = sudo_cmd!(
prompt = SUDO_PROMPT,
sudo_check = privileged,
"podman",
"images",
"--format",
"json"
@ -698,20 +645,11 @@ impl RunDriver for PodmanDriver {
}
}
fn podman_run(opts: &RunOpts, cid_file: &Path) -> Command {
let use_sudo = opts.privileged && !running_as_root();
let command = cmd!(
if use_sudo {
"sudo"
} else {
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
fn podman_run(opts: RunOpts, cid_file: &Path) -> Command {
let command = sudo_cmd!(
prompt = SUDO_PROMPT,
sudo_check = opts.privileged,
"podman",
"run",
format!("--cidfile={}", cid_file.display()),
if opts.privileged => [
@ -729,7 +667,7 @@ fn podman_run(opts: &RunOpts, cid_file: &Path) -> Command {
"--env",
format!("{key}={value}"),
],
&*opts.image,
opts.image,
for arg in opts.args.iter() => &**arg,
);
trace!("{command:?}");

View file

@ -0,0 +1,88 @@
use std::ops::Not;
use blue_build_utils::constants::OSTREE_UNVERIFIED_IMAGE;
use comlexr::cmd;
use log::trace;
use miette::{Context, IntoDiagnostic, bail};
use crate::logging::CommandLogging;
use super::{BootDriver, BootStatus, opts::SwitchOpts};
mod status;
pub use status::*;
pub struct RpmOstreeDriver;
impl BootDriver for RpmOstreeDriver {
fn status() -> miette::Result<Box<dyn BootStatus>> {
let output = {
let c = cmd!("rpm-ostree", "status", "--json");
trace!("{c:?}");
c
}
.output()
.into_diagnostic()?;
if !output.status.success() {
bail!("Failed to get `rpm-ostree` status!");
}
trace!("{}", String::from_utf8_lossy(&output.stdout));
Ok(Box::new(
serde_json::from_slice::<Status>(&output.stdout)
.into_diagnostic()
.wrap_err_with(|| {
format!(
"Failed to deserialize rpm-ostree status:\n{}",
String::from_utf8_lossy(&output.stdout)
)
})?,
))
}
fn switch(opts: SwitchOpts) -> miette::Result<()> {
let status = {
let c = cmd!(
"rpm-ostree",
"rebase",
format!("{OSTREE_UNVERIFIED_IMAGE}:containers-storage:{}", opts.image),
if opts.reboot => "--reboot",
);
trace!("{c:?}");
c
}
.build_status(format!("{}", opts.image), "Switching to new image")
.into_diagnostic()?;
if status.success().not() {
bail!("Failed to switch to image {}", opts.image);
}
Ok(())
}
fn upgrade(opts: SwitchOpts) -> miette::Result<()> {
let status = {
let c = cmd!(
"rpm-ostree",
"upgrade",
if opts.reboot => "--reboot",
);
trace!("{c:?}");
c
}
.build_status(format!("{}", opts.image), "Switching to new image")
.into_diagnostic()?;
if status.success().not() {
bail!("Failed to switch to image {}", opts.image);
}
Ok(())
}
}

View file

@ -0,0 +1,173 @@
use image_ref::DeploymentImageRef;
use serde::Deserialize;
use crate::drivers::{BootStatus, types::ImageRef};
mod image_ref;
#[derive(Debug, Clone, Deserialize)]
pub struct Status {
deployments: Vec<Deployment>,
transactions: Option<Vec<String>>,
}
impl BootStatus for Status {
/// Checks if there is a transaction in progress.
fn transaction_in_progress(&self) -> bool {
self.transactions.as_ref().is_some_and(|tr| !tr.is_empty())
}
/// Get the booted image's reference.
fn booted_image(&self) -> Option<ImageRef<'_>> {
(&self
.deployments
.iter()
.find(|deployment| deployment.booted)?
.container_image_reference)
.try_into()
.inspect_err(|e| {
log::warn!("{e}");
})
.ok()
}
/// Get the booted image's reference.
fn staged_image(&self) -> Option<ImageRef<'_>> {
(&self
.deployments
.iter()
.find(|deployment| deployment.staged)?
.container_image_reference)
.try_into()
.inspect_err(|e| {
log::warn!("{e}");
})
.ok()
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
struct Deployment {
container_image_reference: DeploymentImageRef,
booted: bool,
staged: bool,
}
#[cfg(test)]
mod test {
use blue_build_utils::constants::{
ARCHIVE_SUFFIX, LOCAL_BUILD, OCI_ARCHIVE, OSTREE_IMAGE_SIGNED, OSTREE_UNVERIFIED_IMAGE,
};
use crate::drivers::{BootStatus, types::ImageRef};
use super::{Deployment, Status};
fn create_image_status() -> Status {
Status {
deployments: vec![
Deployment {
container_image_reference: format!(
"{OSTREE_IMAGE_SIGNED}:docker://ghcr.io/blue-build/cli/test"
)
.try_into()
.unwrap(),
booted: true,
staged: false,
},
Deployment {
container_image_reference: format!(
"{OSTREE_IMAGE_SIGNED}:docker://ghcr.io/blue-build/cli/test:last"
)
.try_into()
.unwrap(),
booted: false,
staged: false,
},
],
transactions: None,
}
}
fn create_transaction_status() -> Status {
Status {
deployments: vec![
Deployment {
container_image_reference: format!(
"{OSTREE_IMAGE_SIGNED}:docker://ghcr.io/blue-build/cli/test"
)
.try_into()
.unwrap(),
booted: true,
staged: false,
},
Deployment {
container_image_reference: format!(
"{OSTREE_IMAGE_SIGNED}:docker://ghcr.io/blue-build/cli/test:last"
)
.try_into()
.unwrap(),
booted: false,
staged: false,
},
],
transactions: Some(bon::vec!["Upgrade", "/"]),
}
}
fn create_archive_staged_status() -> Status {
Status {
deployments: vec![
Deployment {
container_image_reference: format!(
"{OSTREE_UNVERIFIED_IMAGE}:{OCI_ARCHIVE}:{LOCAL_BUILD}/cli_test.{ARCHIVE_SUFFIX}"
).try_into().unwrap(),
booted: false,
staged: true,
},
Deployment {
container_image_reference: format!(
"{OSTREE_UNVERIFIED_IMAGE}:{OCI_ARCHIVE}:{LOCAL_BUILD}/cli_test.{ARCHIVE_SUFFIX}"
).try_into().unwrap(),
booted: true,
staged: false,
},
Deployment {
container_image_reference: format!(
"{OSTREE_IMAGE_SIGNED}:docker://ghcr.io/blue-build/cli/test:last"
).try_into().unwrap(),
booted: false,
staged: false,
},
],
transactions: None,
}
}
#[test]
fn test_booted_image() {
assert!(matches!(
create_image_status()
.booted_image()
.expect("Contains image"),
ImageRef::Remote(_)
));
}
#[test]
fn test_staged_image() {
assert!(matches!(
create_archive_staged_status()
.staged_image()
.expect("Contains image"),
ImageRef::LocalTar(_)
));
}
#[test]
fn test_transaction_in_progress() {
assert!(create_transaction_status().transaction_in_progress());
assert!(!create_image_status().transaction_in_progress());
}
}

View file

@ -0,0 +1,738 @@
use std::{ops::Not, path::PathBuf, str::FromStr};
use blue_build_utils::impl_de_fromstr;
use lazy_regex::{regex_if, regex_switch};
use miette::{IntoDiagnostic, bail};
use oci_distribution::Reference;
use crate::drivers::types::ImageRef;
impl_de_fromstr!(
DeploymentImageRef,
ImageTransport,
RefIndex,
DockerDaemon,
DigestAlgorithm,
StorageSpecifier,
);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeploymentImageRef {
UnverifiedImage(ImageTransport),
UnverifiedRegistry(Reference),
RemoteImage {
remote: String,
reference: Reference,
},
RemoteRegistry {
remote: String,
reference: Reference,
},
ImageSigned(ImageTransport),
}
impl<'a> TryFrom<&'a DeploymentImageRef> for ImageRef<'a> {
type Error = miette::Error;
fn try_from(value: &'a DeploymentImageRef) -> Result<Self, Self::Error> {
Ok(match value {
DeploymentImageRef::UnverifiedImage(
ImageTransport::Registry(reference)
| ImageTransport::Docker(reference)
| ImageTransport::DockerDaemon(DockerDaemon::Reference(reference))
| ImageTransport::ContainersStorage {
storage_specifier: _,
reference,
},
)
| DeploymentImageRef::ImageSigned(
ImageTransport::Registry(reference)
| ImageTransport::Docker(reference)
| ImageTransport::DockerDaemon(DockerDaemon::Reference(reference))
| ImageTransport::ContainersStorage {
storage_specifier: _,
reference,
},
) => Self::Remote(std::borrow::Cow::Borrowed(reference)),
DeploymentImageRef::UnverifiedRegistry(reference) => {
Self::Remote(std::borrow::Cow::Borrowed(reference))
}
DeploymentImageRef::UnverifiedImage(ImageTransport::OciArchive {
path,
reference: _,
})
| DeploymentImageRef::ImageSigned(ImageTransport::OciArchive { path, reference: _ }) => {
Self::LocalTar(std::borrow::Cow::Borrowed(path))
}
_ => bail!("Failed to convert {value} into an image ref"),
})
}
}
impl std::fmt::Display for DeploymentImageRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::UnverifiedImage(transport) => format!("ostree-unverified-image:{transport}"),
Self::UnverifiedRegistry(reference) =>
format!("ostree-unverified-registry:{reference}"),
Self::RemoteImage { remote, reference } =>
format!("ostree-remote-image:{remote}:registry:{reference}"),
Self::RemoteRegistry { remote, reference } =>
format!("ostree-remote-registry:{remote}:{reference}"),
Self::ImageSigned(transport) => format!("ostree-image-signed:{transport}"),
}
)
}
}
impl FromStr for DeploymentImageRef {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
regex_switch!(
s,
r"ostree-unverified-image:(?<reference>.*)" => {
Self::UnverifiedImage(reference.try_into()?)
}
r"ostree-unverified-registry:(?<reference>.*)" => {
Self::UnverifiedRegistry(reference.try_into().into_diagnostic()?)
}
r"ostree-remote-image:(?<remote>[^:]+):registry:(?<reference>.*)" => {
Self::RemoteImage {
remote: remote.into(),
reference: reference.try_into().into_diagnostic()?,
}
}
r"ostree-remote-registry:(?<remote>[^:]+):(?<reference>.*)" => {
Self::RemoteRegistry {
remote: remote.into(),
reference: reference.try_into().into_diagnostic()?,
}
}
r"ostree-image-signed:(?<transport>.*)" => {
Self::ImageSigned(transport.try_into()?)
}
)
.ok_or_else(|| miette::miette!("Failed to parse '{s}' as an image transport"))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImageTransport {
Registry(Reference),
Docker(Reference),
DockerArchive {
path: PathBuf,
ref_index: Option<RefIndex>,
},
DockerDaemon(DockerDaemon),
Dir(PathBuf),
Oci {
path: PathBuf,
ref_index: Option<RefIndex>,
},
OciArchive {
path: PathBuf,
reference: Option<Reference>,
},
ContainersStorage {
storage_specifier: Option<StorageSpecifier>,
reference: Reference,
},
Ostree {
reference: Reference,
repo_path: Option<PathBuf>,
},
Sif(PathBuf),
}
impl std::fmt::Display for ImageTransport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Registry(reference) => format!("registry:{reference}"),
Self::Docker(reference) => format!("docker://{reference}"),
Self::DockerArchive {
path,
ref_index: None,
} => format!("docker-archive:{}", path.display()),
Self::DockerArchive {
path,
ref_index: Some(ref_index),
} => format!("docker-archive:{}:{ref_index}", path.display()),
Self::DockerDaemon(daemon) => format!("docker-daemon:{daemon}"),
Self::Dir(path) => format!("dir:{}", path.display()),
Self::Oci {
path,
ref_index: None,
} => format!("oci:{}", path.display()),
Self::Oci {
path,
ref_index: Some(ref_index),
} => format!("oci:{}:{ref_index}", path.display()),
Self::OciArchive {
path,
reference: None,
} => format!("oci-archive:{}", path.display()),
Self::OciArchive {
path,
reference: Some(reference),
} => format!("oci-archive:{}:{reference}", path.display()),
Self::ContainersStorage {
storage_specifier: None,
reference,
} => format!("containers-storage:{reference}"),
Self::ContainersStorage {
storage_specifier: Some(storage_specifier),
reference,
} => format!("containers-storage:[{storage_specifier}]{reference}"),
Self::Ostree {
reference,
repo_path: None,
} => format!("ostree:{reference}"),
Self::Ostree {
reference,
repo_path: Some(repo_path),
} => format!("ostree:{reference}@{}", repo_path.display()),
Self::Sif(path) => format!("sif:{}", path.display()),
}
)
}
}
impl FromStr for ImageTransport {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
regex_switch!(
s,
r"registry:(?<reference>.*)" => {
Self::Registry(reference.try_into().into_diagnostic()?)
}
r"docker://(?<reference>.*)" => {
Self::Docker(reference.try_into().into_diagnostic()?)
}
r"docker-archive:(?<path>[^:]+)(?::(?<ref_index>.*))?" => {
let ref_index = if ref_index.is_empty().not() {
Some(ref_index.try_into()?)
} else {
None
};
Self::DockerArchive { path: path.into(), ref_index }
}
r"docker-daemon:(?<reference>.*)" => {
Self::DockerDaemon(reference.try_into()?)
}
r"dir:(?<path>.*)" => {
Self::Dir(path.into())
}
r"oci:(?<path>[^:]+)(?::(?<ref_index>.*))?" => {
let ref_index = if ref_index.is_empty().not() {
Some(ref_index.try_into()?)
} else {
None
};
Self::Oci { path: path.into(), ref_index }
}
r"oci-archive:(?<path>[^:]+)(?::(?<reference>.*))?" => {
let reference = if reference.is_empty().not() {
Some(reference.try_into().into_diagnostic()?)
} else {
None
};
Self::OciArchive { path: path.into(), reference }
}
r"containers-storage:(?:\[(?<storage_specifier>.*)\])?(?<reference>.*)" => {
let storage_specifier = if storage_specifier.is_empty().not() {
Some(storage_specifier.try_into()?)
} else {
None
};
Self::ContainersStorage { storage_specifier, reference: reference.parse().into_diagnostic()? }
}
r"ostree:(?<reference>[^@]+)(?:@(?<repo_path>.*))?" => {
let repo_path = if repo_path.is_empty().not() {
Some(repo_path.into())
} else {
None
};
Self::Ostree { reference: reference.parse().into_diagnostic()?, repo_path }
}
r"sif:(?<path>.*)" => {
Self::Sif(path.into())
}
)
.ok_or_else(|| miette::miette!("Failed to parse '{s}' as an image transport"))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RefIndex {
Reference(Reference),
Index(usize),
}
impl std::fmt::Display for RefIndex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Reference(reference) => format!("{reference}"),
Self::Index(index) => format!("{index}"),
}
)
}
}
impl FromStr for RefIndex {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match (Reference::try_from(s), s.parse::<usize>()) {
(_, Ok(index)) => Self::Index(index),
(Ok(reference), _) => Self::Reference(reference),
_ => bail!("Failed to parse '{s}' into a reference or index"),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DockerDaemon {
Reference(Reference),
Algo {
algo: DigestAlgorithm,
digest: String,
},
}
impl std::fmt::Display for DockerDaemon {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Reference(reference) => format!("{reference}"),
Self::Algo { algo, digest } => format!(
"{}:{digest}",
match algo {
DigestAlgorithm::Sha256 => "sha256",
DigestAlgorithm::Sha384 => "sha384",
DigestAlgorithm::Sha512 => "sha512",
}
),
}
)
}
}
impl FromStr for DockerDaemon {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(
match (
s.split_once(':').map(|(algo, digest)| {
(
DigestAlgorithm::try_from(algo),
regex_if!(r"[a-f0-9]+", digest, digest),
)
}),
Reference::try_from(s),
) {
(Some((Ok(algo), Some(digest))), _) => Self::Algo {
algo,
digest: digest.into(),
},
(_, Ok(reference)) => Self::Reference(reference),
_ => bail!("Failed to parse '{s}' as a docker daemon reference"),
},
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DigestAlgorithm {
Sha256,
Sha384,
Sha512,
}
impl FromStr for DigestAlgorithm {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"sha256" => Self::Sha256,
"sha384" => Self::Sha384,
"sha512" => Self::Sha512,
_ => bail!("Failed to parse '{s}' as a digest algorithm"),
})
}
}
impl std::fmt::Display for DigestAlgorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Sha256 => "sha256",
Self::Sha384 => "sha384",
Self::Sha512 => "sha512",
}
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StorageSpecifier {
driver: Option<String>,
root: PathBuf,
run_root: Option<PathBuf>,
options: Option<String>,
}
impl std::fmt::Display for StorageSpecifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{driver}{root}{run_root}{options}",
driver = self
.driver
.as_ref()
.map(|d| format!("{d}@"))
.unwrap_or_default(),
root = self.root.display(),
run_root = self
.run_root
.as_ref()
.map(|r| format!("+{}", r.display()))
.unwrap_or_default(),
options = self
.options
.as_ref()
.map(|o| format!(":{o}"))
.unwrap_or_default(),
)
}
}
impl FromStr for StorageSpecifier {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
regex_if!(
r"(?:(?<driver>[\w-]+)@)?(?<root>[\w\/-]+)(?:\+(?<run_root>[\w\/-]+))?(?:\:(?<options>[\w,=-]+))?",
s,
{
Self {
driver: driver.is_empty().not().then(|| driver.into()),
root: root.into(),
run_root: run_root.is_empty().not().then(|| run_root.into()),
options: options.is_empty().not().then(|| options.into()),
}
}
)
.ok_or_else(|| miette::miette!("Failed to parse storage specifier"))
}
}
#[cfg(test)]
mod test {
use oci_distribution::Reference;
use pretty_assertions::assert_eq;
use rstest::rstest;
use super::*;
macro_rules! test_parse {
($($test:ident {
typ: $typ:ty,
value: $val:literal,
variant: $var:pat$(,)?
}),* $(,)?) => {
$(
#[test]
fn $test() {
let transport = <$typ>::try_from($val).unwrap();
assert!(
matches!(
&transport,
$var
)
);
assert_eq!($val, transport.to_string().as_str());
}
)*
};
}
test_parse!(
parse_image_transport_registry {
typ: ImageTransport,
value: "registry:ghcr.io/ublue-os/main-kinoite:42",
variant: ImageTransport::Registry(_),
},
parse_image_transport_docker {
typ: ImageTransport,
value: "docker://ghcr.io/ublue-os/main-kinoite:42",
variant: ImageTransport::Docker(_),
},
parse_image_transport_docker_archive {
typ: ImageTransport,
value: "docker-archive:/test/path",
variant: ImageTransport::DockerArchive {
path: _,
ref_index: None,
}
},
parse_image_transport_docker_archive_index {
typ: ImageTransport,
value: "docker-archive:/test/path:42",
variant: ImageTransport::DockerArchive {
path: _,
ref_index: Some(RefIndex::Index(_)),
}
},
parse_image_transport_docker_archive_ref {
typ: ImageTransport,
value: "docker-archive:/test/path:ghcr.io/ublue-os/main-kinoite:42",
variant: ImageTransport::DockerArchive {
path: _,
ref_index: Some(RefIndex::Reference(_)),
}
},
parse_image_transport_docker_daemon_ref {
typ: ImageTransport,
value: "docker-daemon:ghcr.io/ublue-os/main-kinoite:42",
variant: ImageTransport::DockerDaemon(DockerDaemon::Reference(_)),
},
parse_image_transport_docker_daemon_digest {
typ: ImageTransport,
value: "docker-daemon:sha256:e6cbc801b77c4cfe164f08b6b29de7e588f6d98e8ac0c52c0de0a9ae45f717ab",
variant: ImageTransport::DockerDaemon(DockerDaemon::Algo {
algo: DigestAlgorithm::Sha256,
digest: _,
}),
},
parse_image_transport_dir {
typ: ImageTransport,
value: "dir:/test/path",
variant: ImageTransport::Dir(_),
},
parse_image_transport_oci {
typ: ImageTransport,
value: "oci:/test/path",
variant: ImageTransport::Oci {
path: _,
ref_index: None
}
},
parse_image_transport_oci_ref {
typ: ImageTransport,
value: "oci:/test/path:ghcr.io/ublue-os/main-kinoite:42",
variant: ImageTransport::Oci {
path: _,
ref_index: Some(RefIndex::Reference(_)),
}
},
parse_image_transport_oci_ref_index {
typ: ImageTransport,
value: "oci:/test/path:42",
variant: ImageTransport::Oci {
path: _,
ref_index: Some(RefIndex::Index(_))
}
},
parse_image_transport_oci_archive {
typ: ImageTransport,
value: "oci-archive:/test/path",
variant: ImageTransport::OciArchive {
path: _,
reference: None
}
},
parse_image_transport_oci_archive_ref {
typ: ImageTransport,
value: "oci-archive:/test/path:ghcr.io/ublue-os/main-kinoite:42",
variant: ImageTransport::OciArchive {
path: _,
reference: Some(_)
}
},
parse_image_transport_containers_storage {
typ: ImageTransport,
value: "containers-storage:ghcr.io/ublue-os/main-kinoite:42",
variant: ImageTransport::ContainersStorage {
storage_specifier: None,
reference: _
}
},
parse_image_transport_containers_storage_specifier {
typ: ImageTransport,
value: "containers-storage:[overlayfs@/test/path]ghcr.io/ublue-os/main-kinoite:42",
variant: ImageTransport::ContainersStorage {
storage_specifier: Some(StorageSpecifier {
driver: Some(_),
root: _,
run_root: None,
options: None
}),
reference: _
}
},
parse_image_transport_ostree {
typ: ImageTransport,
value: "ostree:ghcr.io/ublue-os/main-kinoite:42",
variant: ImageTransport::Ostree {
reference: _,
repo_path: None
}
},
parse_image_transport_ostree_repo_path {
typ: ImageTransport,
value: "ostree:ghcr.io/ublue-os/main-kinoite:42@/test/path",
variant: ImageTransport::Ostree {
reference: _,
repo_path: Some(_)
}
},
parse_image_transport_sif {
typ: ImageTransport,
value: "sif:/test/path",
variant: ImageTransport::Sif(_),
},
parse_deployment_image_ref_unverified_image {
typ: DeploymentImageRef,
value: "ostree-unverified-image:registry:ghcr.io/ublue-os/main-kinoite:42",
variant: DeploymentImageRef::UnverifiedImage(ImageTransport::Registry(_)),
},
parse_deployment_image_ref_unverified_registry {
typ: DeploymentImageRef,
value: "ostree-unverified-registry:ghcr.io/ublue-os/main-kinoite:42",
variant: DeploymentImageRef::UnverifiedRegistry(_),
},
parse_deployment_image_ref_remote_image {
typ: DeploymentImageRef,
value: "ostree-remote-image:origin:registry:ghcr.io/ublue-os/main-kinoite:42",
variant: DeploymentImageRef::RemoteImage {
remote: _,
reference: _
}
},
parse_deployment_image_ref_remote_registry {
typ: DeploymentImageRef,
value: "ostree-remote-registry:origin:ghcr.io/ublue-os/main-kinoite:42",
variant: DeploymentImageRef::RemoteRegistry {
remote: _,
reference: _
}
},
parse_deployment_image_ref_image_signed {
typ: DeploymentImageRef,
value: "ostree-image-signed:registry:ghcr.io/ublue-os/main-kinoite:42",
variant: DeploymentImageRef::ImageSigned(ImageTransport::Registry(_)),
}
);
#[rstest]
#[case(
"ghcr.io/ublue-os/main-kinoite:42",
Some("ghcr.io/ublue-os/main-kinoite:42".try_into().unwrap()),
None
)]
#[case(
"sha256:e6cbc801b77c4cfe164f08b6b29de7e588f6d98e8ac0c52c0de0a9ae45f717ab",
None,
Some((
"sha256",
"e6cbc801b77c4cfe164f08b6b29de7e588f6d98e8ac0c52c0de0a9ae45f717ab",
))
)]
fn parse_docker_daemon(
#[case] value: &str,
#[case] reference: Option<Reference>,
#[case] algo_digest: Option<(&str, &str)>,
) {
let expected = match (reference, algo_digest) {
(Some(reference), None) => DockerDaemon::Reference(reference),
(None, Some((algo, digest))) => DockerDaemon::Algo {
algo: algo.try_into().unwrap(),
digest: digest.into(),
},
_ => unreachable!(),
};
assert_eq!(DockerDaemon::try_from(value).unwrap(), expected);
assert_eq!(value, &expected.to_string());
}
#[rstest]
#[case("/test/path", None, "/test/path", None, None)]
#[case("overlayfs@/test/path", Some("overlayfs"), "/test/path", None, None)]
#[case(
"/test/path+/test/run/path",
None,
"/test/path",
Some("/test/run/path"),
None
)]
#[case(
"/test/path:param_1=test,param_2=anotherTest",
None,
"/test/path",
None,
Some("param_1=test,param_2=anotherTest")
)]
#[case(
"/test/path+/test/run/path:param_1=test,param_2=anotherTest",
None,
"/test/path",
Some("/test/run/path"),
Some("param_1=test,param_2=anotherTest")
)]
#[case(
"overlayfs@/test/path+/test/run/path",
Some("overlayfs"),
"/test/path",
Some("/test/run/path"),
None
)]
#[case(
"overlayfs@/test/path:param_1=test,param_2=anotherTest",
Some("overlayfs"),
"/test/path",
None,
Some("param_1=test,param_2=anotherTest")
)]
#[case(
"overlayfs@/test/path+/test/run/path:param_1=test,param_2=anotherTest",
Some("overlayfs"),
"/test/path",
Some("/test/run/path"),
Some("param_1=test,param_2=anotherTest")
)]
fn parse_storage_specifier(
#[case] value: &str,
#[case] driver: Option<&str>,
#[case] root: &str,
#[case] run_root: Option<&str>,
#[case] options: Option<&str>,
) {
let expected = StorageSpecifier {
driver: driver.map(Into::into),
root: root.into(),
run_root: run_root.map(Into::into),
options: options.map(Into::into),
};
assert_eq!(StorageSpecifier::try_from(value).unwrap(), expected);
assert_eq!(value, expected.to_string().as_str());
}
}

View file

@ -33,7 +33,7 @@ use zeroize::Zeroizing;
pub struct SigstoreDriver;
impl SigningDriver for SigstoreDriver {
fn generate_key_pair(opts: &GenerateKeyPairOpts) -> miette::Result<()> {
fn generate_key_pair(opts: GenerateKeyPairOpts) -> miette::Result<()> {
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
let priv_key_path = path.join(COSIGN_PRIV_PATH);
let pub_key_path = path.join(COSIGN_PUB_PATH);
@ -70,7 +70,7 @@ impl SigningDriver for SigstoreDriver {
Ok(())
}
fn check_signing_files(opts: &CheckKeyPairOpts) -> miette::Result<()> {
fn check_signing_files(opts: CheckKeyPairOpts) -> miette::Result<()> {
trace!("SigstoreDriver::check_signing_files({opts:?})");
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
@ -105,7 +105,7 @@ impl SigningDriver for SigstoreDriver {
}
}
fn sign(opts: &SignOpts) -> miette::Result<()> {
fn sign(opts: SignOpts) -> miette::Result<()> {
trace!("SigstoreDriver::sign({opts:?})");
if opts.image.digest().is_none() {
@ -176,7 +176,7 @@ impl SigningDriver for SigstoreDriver {
Ok(())
}
fn verify(opts: &VerifyOpts) -> miette::Result<()> {
fn verify(opts: VerifyOpts) -> miette::Result<()> {
let mut client = ClientBuilder::default().build().into_diagnostic()?;
let image_digest: OciReference = opts.image.to_string().parse().into_diagnostic()?;
@ -253,9 +253,10 @@ mod test {
fn generate_key_pair() {
let tempdir = TempDir::new().unwrap();
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
SigstoreDriver::generate_key_pair(&gen_opts).unwrap();
SigstoreDriver::generate_key_pair(
GenerateKeyPairOpts::builder().dir(tempdir.path()).build(),
)
.unwrap();
eprintln!(
"Private key:\n{}",
@ -266,27 +267,27 @@ mod test {
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
);
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
SigstoreDriver::check_signing_files(&check_opts).unwrap();
SigstoreDriver::check_signing_files(
CheckKeyPairOpts::builder().dir(tempdir.path()).build(),
)
.unwrap();
}
#[test]
fn check_key_pairs() {
let path = Path::new("../test-files/keys");
let opts = CheckKeyPairOpts::builder().dir(path).build();
SigstoreDriver::check_signing_files(&opts).unwrap();
SigstoreDriver::check_signing_files(CheckKeyPairOpts::builder().dir(path).build()).unwrap();
}
#[test]
fn compatibility() {
let tempdir = TempDir::new().unwrap();
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
SigstoreDriver::generate_key_pair(&gen_opts).unwrap();
SigstoreDriver::generate_key_pair(
GenerateKeyPairOpts::builder().dir(tempdir.path()).build(),
)
.unwrap();
eprintln!(
"Private key:\n{}",
@ -297,8 +298,7 @@ mod test {
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
);
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
CosignDriver::check_signing_files(&check_opts).unwrap();
CosignDriver::check_signing_files(CheckKeyPairOpts::builder().dir(tempdir.path()).build())
.unwrap();
}
}

View file

@ -9,13 +9,17 @@ use miette::{IntoDiagnostic, Result, bail};
use crate::{drivers::types::Platform, logging::Logger};
use super::{InspectDriver, opts::GetMetadataOpts, types::ImageMetadata};
use super::{
InspectDriver,
opts::{CopyOciDirOpts, GetMetadataOpts},
types::ImageMetadata,
};
#[derive(Debug)]
pub struct SkopeoDriver;
impl InspectDriver for SkopeoDriver {
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
fn get_metadata(opts: GetMetadataOpts) -> Result<ImageMetadata> {
get_metadata_cache(opts)
}
}
@ -26,7 +30,7 @@ impl InspectDriver for SkopeoDriver {
convert = r#"{ format!("{}-{:?}", opts.image, opts.platform)}"#,
sync_writes = "by_key"
)]
fn get_metadata_cache(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
fn get_metadata_cache(opts: GetMetadataOpts) -> Result<ImageMetadata> {
trace!("SkopeoDriver::get_metadata({opts:#?})");
let image_str = opts.image.to_string();
@ -68,7 +72,7 @@ fn get_metadata_cache(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
}
impl super::OciCopy for SkopeoDriver {
fn copy_oci_dir(opts: &super::opts::CopyOciDirOpts) -> Result<()> {
fn copy_oci_dir(opts: CopyOciDirOpts) -> Result<()> {
use crate::logging::CommandLogging;
let use_sudo = opts.privileged && !blue_build_utils::running_as_root();

View file

@ -17,22 +17,16 @@ use crate::drivers::{
};
use super::{
buildah_driver::BuildahDriver,
cosign_driver::CosignDriver,
docker_driver::DockerDriver,
github_driver::GithubDriver,
gitlab_driver::GitlabDriver,
local_driver::LocalDriver,
opts::{
BuildOpts, BuildTagPushOpts, CheckKeyPairOpts, CreateContainerOpts, GenerateImageNameOpts,
GenerateKeyPairOpts, GenerateTagsOpts, GetMetadataOpts, PushOpts, RechunkOpts,
RemoveContainerOpts, RemoveImageOpts, RunOpts, SignOpts, SignVerifyOpts, TagOpts,
VerifyOpts, VerifyType,
BuildOpts, BuildTagPushOpts, CheckKeyPairOpts, ContainerOpts, CopyOciDirOpts,
CreateContainerOpts, GenerateImageNameOpts, GenerateKeyPairOpts, GenerateTagsOpts,
GetMetadataOpts, PushOpts, RechunkOpts, RemoveContainerOpts, RemoveImageOpts, RunOpts,
SignOpts, SignVerifyOpts, SwitchOpts, TagOpts, VerifyOpts, VerifyType, VolumeOpts,
},
types::{
BootDriverType, BuildDriverType, ContainerId, ImageMetadata, InspectDriverType, MountId,
RunDriverType, SigningDriverType,
},
podman_driver::PodmanDriver,
sigstore_driver::SigstoreDriver,
skopeo_driver::SkopeoDriver,
types::{ContainerId, ImageMetadata, MountId},
};
trait PrivateDriver {}
@ -46,19 +40,37 @@ macro_rules! impl_private_driver {
}
impl_private_driver!(
Driver,
DockerDriver,
PodmanDriver,
BuildahDriver,
GithubDriver,
GitlabDriver,
LocalDriver,
CosignDriver,
SkopeoDriver,
CiDriverType,
SigstoreDriver,
super::Driver,
super::docker_driver::DockerDriver,
super::podman_driver::PodmanDriver,
super::buildah_driver::BuildahDriver,
super::github_driver::GithubDriver,
super::gitlab_driver::GitlabDriver,
super::local_driver::LocalDriver,
super::cosign_driver::CosignDriver,
super::skopeo_driver::SkopeoDriver,
super::sigstore_driver::SigstoreDriver,
super::rpm_ostree_driver::RpmOstreeDriver,
super::rpm_ostree_driver::Status,
Option<BuildDriverType>,
Option<RunDriverType>,
Option<InspectDriverType>,
Option<SigningDriverType>,
Option<CiDriverType>,
Option<BootDriverType>,
);
#[cfg(feature = "bootc")]
impl_private_driver!(
super::bootc_driver::BootcDriver,
super::bootc_driver::BootcStatus
);
#[allow(private_bounds)]
pub trait DetermineDriver<T>: PrivateDriver {
fn determine_driver(&mut self) -> T;
}
/// Trait for retrieving version of a driver.
#[allow(private_bounds)]
pub trait DriverVersion: PrivateDriver {
@ -88,19 +100,19 @@ pub trait BuildDriver: PrivateDriver {
///
/// # Errors
/// Will error if the build fails.
fn build(opts: &BuildOpts) -> Result<()>;
fn build(opts: BuildOpts) -> Result<()>;
/// Runs the tag logic for the driver.
///
/// # Errors
/// Will error if the tagging fails.
fn tag(opts: &TagOpts) -> Result<()>;
fn tag(opts: TagOpts) -> Result<()>;
/// Runs the push logic for the driver
///
/// # Errors
/// Will error if the push fails.
fn push(opts: &PushOpts) -> Result<()>;
fn push(opts: PushOpts) -> Result<()>;
/// Runs the login logic for the driver.
///
@ -112,27 +124,27 @@ pub trait BuildDriver: PrivateDriver {
///
/// # Errors
/// Will error if the driver fails to prune.
fn prune(opts: &super::opts::PruneOpts) -> Result<()>;
fn prune(opts: super::opts::PruneOpts) -> Result<()>;
/// Runs the logic for building, tagging, and pushing an image.
///
/// # Errors
/// Will error if building, tagging, or pusing fails.
fn build_tag_push(opts: &BuildTagPushOpts) -> Result<Vec<String>> {
fn build_tag_push(opts: BuildTagPushOpts) -> Result<Vec<String>> {
trace!("BuildDriver::build_tag_push({opts:#?})");
let build_opts = BuildOpts::builder()
.image(&opts.image)
.image(opts.image)
.containerfile(opts.containerfile.as_ref())
.maybe_platform(opts.platform)
.squash(opts.squash)
.maybe_cache_from(opts.cache_from)
.maybe_cache_to(opts.cache_to)
.secrets(opts.secrets.clone())
.secrets(opts.secrets)
.build();
info!("Building image {}", opts.image);
Self::build(&build_opts)?;
Self::build(build_opts)?;
let image_list: Vec<String> = match &opts.image {
ImageRef::Remote(image) if !opts.tags.is_empty() => {
@ -140,7 +152,7 @@ pub trait BuildDriver: PrivateDriver {
let mut image_list = Vec::with_capacity(opts.tags.len());
for tag in &opts.tags {
for tag in opts.tags {
debug!("Tagging {} with {tag}", &image);
let tagged_image = Reference::with_tag(
image.registry().into(),
@ -153,7 +165,7 @@ pub trait BuildDriver: PrivateDriver {
.dest_image(&tagged_image)
.build();
Self::tag(&tag_opts)?;
Self::tag(tag_opts)?;
image_list.push(tagged_image.to_string());
if opts.push {
@ -169,7 +181,7 @@ pub trait BuildDriver: PrivateDriver {
.compression_type(opts.compression)
.build();
Self::push(&push_opts)
Self::push(push_opts)
})?;
}
}
@ -177,7 +189,7 @@ pub trait BuildDriver: PrivateDriver {
image_list
}
_ => {
string_vec![&opts.image]
string_vec![opts.image]
}
};
@ -192,7 +204,7 @@ pub trait InspectDriver: PrivateDriver {
///
/// # Errors
/// Will error if it is unable to get the labels.
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata>;
fn get_metadata(opts: GetMetadataOpts) -> Result<ImageMetadata>;
}
/// Allows agnostic running of containers.
@ -202,31 +214,31 @@ pub trait RunDriver: PrivateDriver {
///
/// # Errors
/// Will error if there is an issue running the container.
fn run(opts: &RunOpts) -> Result<ExitStatus>;
fn run(opts: RunOpts) -> Result<ExitStatus>;
/// Run a container to perform an action and capturing output.
///
/// # Errors
/// Will error if there is an issue running the container.
fn run_output(opts: &RunOpts) -> Result<Output>;
fn run_output(opts: RunOpts) -> Result<Output>;
/// Creates container
///
/// # Errors
/// Will error if the container create command fails.
fn create_container(opts: &CreateContainerOpts) -> Result<ContainerId>;
fn create_container(opts: CreateContainerOpts) -> Result<ContainerId>;
/// Removes a container
///
/// # Errors
/// Will error if the container remove command fails.
fn remove_container(opts: &RemoveContainerOpts) -> Result<()>;
fn remove_container(opts: RemoveContainerOpts) -> Result<()>;
/// Removes an image
///
/// # Errors
/// Will error if the image remove command fails.
fn remove_image(opts: &RemoveImageOpts) -> Result<()>;
fn remove_image(opts: RemoveImageOpts) -> Result<()>;
/// List all images in the local image registry.
///
@ -241,23 +253,23 @@ pub(super) trait ContainerMountDriver: PrivateDriver {
///
/// # Errors
/// Will error if the container mount command fails.
fn mount_container(opts: &super::opts::ContainerOpts) -> Result<MountId>;
fn mount_container(opts: ContainerOpts) -> Result<MountId>;
/// Unmount the container
///
/// # Errors
/// Will error if the container unmount command fails.
fn unmount_container(opts: &super::opts::ContainerOpts) -> Result<()>;
fn unmount_container(opts: ContainerOpts) -> Result<()>;
/// Remove a volume
///
/// # Errors
/// Will error if the volume remove command fails.
fn remove_volume(opts: &super::opts::VolumeOpts) -> Result<()>;
fn remove_volume(opts: VolumeOpts) -> Result<()>;
}
pub(super) trait OciCopy {
fn copy_oci_dir(opts: &super::opts::CopyOciDirOpts) -> Result<()>;
fn copy_oci_dir(opts: CopyOciDirOpts) -> Result<()>;
}
#[allow(private_bounds)]
@ -268,7 +280,7 @@ pub trait RechunkDriver: RunDriver + BuildDriver + ContainerMountDriver {
///
/// # Errors
/// Will error if the rechunk process fails.
fn rechunk(opts: &RechunkOpts) -> Result<Vec<String>> {
fn rechunk(opts: RechunkOpts) -> Result<Vec<String>> {
let ostree_cache_id = &uuid::Uuid::new_v4().to_string();
let raw_image =
&Reference::try_from(format!("localhost/{ostree_cache_id}/raw-rechunk")).unwrap();
@ -283,25 +295,25 @@ pub trait RechunkDriver: RunDriver + BuildDriver + ContainerMountDriver {
Self::login()?;
Self::build(
&BuildOpts::builder()
.image(raw_image)
.containerfile(&*opts.containerfile)
BuildOpts::builder()
.image(&ImageRef::from(raw_image))
.containerfile(opts.containerfile)
.maybe_platform(opts.platform)
.privileged(true)
.squash(true)
.host_network(true)
.secrets(opts.secrets.clone())
.secrets(opts.secrets)
.build(),
)?;
let container = &Self::create_container(
&CreateContainerOpts::builder()
CreateContainerOpts::builder()
.image(raw_image)
.privileged(true)
.build(),
)?;
let mount = &Self::mount_container(
&super::opts::ContainerOpts::builder()
super::opts::ContainerOpts::builder()
.container_id(container)
.privileged(true)
.build(),
@ -324,7 +336,7 @@ pub trait RechunkDriver: RunDriver + BuildDriver + ContainerMountDriver {
if opts.push {
let oci_dir = &super::types::OciDir::try_from(temp_dir.path().join(ostree_cache_id))?;
for tag in &opts.tags {
for tag in opts.tags {
let tagged_image = Reference::with_tag(
full_image.registry().to_string(),
full_image.repository().to_string(),
@ -335,7 +347,7 @@ pub trait RechunkDriver: RunDriver + BuildDriver + ContainerMountDriver {
debug!("Pushing image {tagged_image}");
Driver::copy_oci_dir(
&super::opts::CopyOciDirOpts::builder()
super::opts::CopyOciDirOpts::builder()
.oci_dir(oci_dir)
.registry(&tagged_image)
.privileged(true)
@ -357,39 +369,39 @@ pub trait RechunkDriver: RunDriver + BuildDriver + ContainerMountDriver {
mount: &MountId,
container: &ContainerId,
raw_image: &Reference,
opts: &RechunkOpts<'_>,
opts: RechunkOpts<'_>,
) -> Result<(), miette::Error> {
let status = Self::run(
&RunOpts::builder()
RunOpts::builder()
.image(Self::RECHUNK_IMAGE)
.remove(true)
.user("0:0")
.privileged(true)
.volumes(crate::run_volumes! {
.volumes(&crate::run_volumes! {
mount => "/var/tree",
})
.env_vars(crate::run_envs! {
.env_vars(&crate::run_envs! {
"TREE" => "/var/tree",
})
.args(bon::vec!["/sources/rechunk/1_prune.sh"])
.args(&bon::vec!["/sources/rechunk/1_prune.sh"])
.build(),
)?;
if !status.success() {
Self::unmount_container(
&super::opts::ContainerOpts::builder()
super::opts::ContainerOpts::builder()
.container_id(container)
.privileged(true)
.build(),
)?;
Self::remove_container(
&RemoveContainerOpts::builder()
RemoveContainerOpts::builder()
.container_id(container)
.privileged(true)
.build(),
)?;
Self::remove_image(
&RemoveImageOpts::builder()
RemoveImageOpts::builder()
.image(raw_image)
.privileged(true)
.build(),
@ -409,40 +421,40 @@ pub trait RechunkDriver: RunDriver + BuildDriver + ContainerMountDriver {
ostree_cache_id: &str,
container: &ContainerId,
raw_image: &Reference,
opts: &RechunkOpts<'_>,
opts: RechunkOpts<'_>,
) -> Result<()> {
let status = Self::run(
&RunOpts::builder()
RunOpts::builder()
.image(Self::RECHUNK_IMAGE)
.remove(true)
.user("0:0")
.privileged(true)
.volumes(crate::run_volumes! {
.volumes(&crate::run_volumes! {
mount => "/var/tree",
ostree_cache_id => "/var/ostree",
})
.env_vars(crate::run_envs! {
.env_vars(&crate::run_envs! {
"TREE" => "/var/tree",
"REPO" => "/var/ostree/repo",
"RESET_TIMESTAMP" => "1",
})
.args(bon::vec!["/sources/rechunk/2_create.sh"])
.args(&bon::vec!["/sources/rechunk/2_create.sh"])
.build(),
)?;
Self::unmount_container(
&super::opts::ContainerOpts::builder()
super::opts::ContainerOpts::builder()
.container_id(container)
.privileged(true)
.build(),
)?;
Self::remove_container(
&RemoveContainerOpts::builder()
RemoveContainerOpts::builder()
.container_id(container)
.privileged(true)
.build(),
)?;
Self::remove_image(
&RemoveImageOpts::builder()
RemoveImageOpts::builder()
.image(raw_image)
.privileged(true)
.build(),
@ -463,45 +475,51 @@ pub trait RechunkDriver: RunDriver + BuildDriver + ContainerMountDriver {
ostree_cache_id: &str,
temp_dir_str: &str,
current_dir: &str,
opts: &RechunkOpts<'_>,
opts: RechunkOpts<'_>,
) -> Result<()> {
let out_ref = format!("oci:{ostree_cache_id}");
let labels = format!(
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
format_args!(
"{}={}",
blue_build_utils::constants::BUILD_ID_LABEL,
Driver::get_build_id()
),
format_args!("org.opencontainers.image.title={}", &opts.name),
format_args!("org.opencontainers.image.description={}", &opts.description),
format_args!("org.opencontainers.image.source={}", &opts.repo),
format_args!("org.opencontainers.image.base.digest={}", &opts.base_digest),
format_args!("org.opencontainers.image.base.name={}", &opts.base_image),
"org.opencontainers.image.created=<timestamp>",
"io.artifacthub.package.readme-url=https://raw.githubusercontent.com/blue-build/cli/main/README.md",
);
let status = Self::run(
&RunOpts::builder()
.image(Self::RECHUNK_IMAGE)
.remove(true)
.user("0:0")
.privileged(true)
.volumes(crate::run_volumes! {
ostree_cache_id => "/var/ostree",
temp_dir_str => "/workspace",
current_dir => "/var/git"
})
.env_vars(crate::run_envs! {
"REPO" => "/var/ostree/repo",
"PREV_REF" => &*opts.image,
"OUT_NAME" => ostree_cache_id,
"CLEAR_PLAN" => if opts.clear_plan { "true" } else { "" },
"VERSION" => format!("{}", opts.version),
"OUT_REF" => format!("oci:{ostree_cache_id}"),
"GIT_DIR" => "/var/git",
"LABELS" => format!(
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
format_args!("{}={}", blue_build_utils::constants::BUILD_ID_LABEL, Driver::get_build_id()),
format_args!("org.opencontainers.image.title={}", &opts.name),
format_args!("org.opencontainers.image.description={}", &opts.description),
format_args!("org.opencontainers.image.source={}", &opts.repo),
format_args!("org.opencontainers.image.base.digest={}", &opts.base_digest),
format_args!("org.opencontainers.image.base.name={}", &opts.base_image),
"org.opencontainers.image.created=<timestamp>",
"io.artifacthub.package.readme-url=https://raw.githubusercontent.com/blue-build/cli/main/README.md",
)
})
.args(bon::vec!["/sources/rechunk/3_chunk.sh"])
.build(),
RunOpts::builder()
.image(Self::RECHUNK_IMAGE)
.remove(true)
.user("0:0")
.privileged(true)
.volumes(&crate::run_volumes! {
ostree_cache_id => "/var/ostree",
temp_dir_str => "/workspace",
current_dir => "/var/git"
})
.env_vars(&crate::run_envs! {
"REPO" => "/var/ostree/repo",
"PREV_REF" => opts.image,
"OUT_NAME" => ostree_cache_id,
"CLEAR_PLAN" => if opts.clear_plan { "true" } else { "" },
"VERSION" => opts.version,
"OUT_REF" => &out_ref,
"GIT_DIR" => "/var/git",
"LABELS" => &labels,
})
.args(&bon::vec!["/sources/rechunk/3_chunk.sh"])
.build(),
)?;
Self::remove_volume(
&super::opts::VolumeOpts::builder()
super::opts::VolumeOpts::builder()
.volume_id(ostree_cache_id)
.privileged(true)
.build(),
@ -522,20 +540,20 @@ pub trait SigningDriver: PrivateDriver {
///
/// # Errors
/// Will error if a key-pair couldn't be generated.
fn generate_key_pair(opts: &GenerateKeyPairOpts) -> Result<()>;
fn generate_key_pair(opts: GenerateKeyPairOpts) -> Result<()>;
/// Checks the signing key files to ensure
/// they match.
///
/// # Errors
/// Will error if the files cannot be verified.
fn check_signing_files(opts: &CheckKeyPairOpts) -> Result<()>;
fn check_signing_files(opts: CheckKeyPairOpts) -> Result<()>;
/// Signs the image digest.
///
/// # Errors
/// Will error if signing fails.
fn sign(opts: &SignOpts) -> Result<()>;
fn sign(opts: SignOpts) -> Result<()>;
/// Verifies the image.
///
@ -545,22 +563,23 @@ pub trait SigningDriver: PrivateDriver {
///
/// # Errors
/// Will error if the image fails to be verified.
fn verify(opts: &VerifyOpts) -> Result<()>;
fn verify(opts: VerifyOpts) -> Result<()>;
/// Sign an image given the image name and tag.
///
/// # Errors
/// Will error if the image fails to be signed.
fn sign_and_verify(opts: &SignVerifyOpts) -> Result<()> {
fn sign_and_verify(opts: SignVerifyOpts) -> Result<()> {
trace!("sign_and_verify({opts:?})");
let path = opts
.dir
.as_ref()
.map_or_else(|| PathBuf::from("."), |d| d.to_path_buf());
let cosign_file_path = path.join(COSIGN_PUB_PATH);
let image_digest = Driver::get_metadata(
&GetMetadataOpts::builder()
GetMetadataOpts::builder()
.image(opts.image)
.maybe_platform(opts.platform)
.build(),
@ -573,39 +592,40 @@ pub trait SigningDriver: PrivateDriver {
)
.parse()
.into_diagnostic()?;
let issuer = Driver::oidc_provider();
let identity = Driver::keyless_cert_identity();
let priv_key = get_private_key(&path);
let (sign_opts, verify_opts) = match (Driver::get_ci_driver(), get_private_key(&path)) {
// Cosign public/private key pair
(_, Ok(priv_key)) => (
SignOpts::builder()
.image(&image_digest)
.dir(&path)
.key(priv_key.to_string())
.build(),
VerifyOpts::builder()
.image(opts.image)
.verify_type(VerifyType::File(path.join(COSIGN_PUB_PATH).into()))
.build(),
),
// Gitlab keyless
(CiDriverType::Github | CiDriverType::Gitlab, _) => (
SignOpts::builder().dir(&path).image(&image_digest).build(),
VerifyOpts::builder()
.image(opts.image)
.verify_type(VerifyType::Keyless {
issuer: Driver::oidc_provider()?.into(),
identity: Driver::keyless_cert_identity()?.into(),
})
.build(),
),
_ => bail!("Failed to get information for signing the image"),
};
let (sign_opts, verify_opts) =
match (Driver::get_ci_driver(), &priv_key, &issuer, &identity) {
// Cosign public/private key pair
(_, Ok(priv_key), _, _) => (
SignOpts::builder()
.image(&image_digest)
.dir(&path)
.key(priv_key)
.build(),
VerifyOpts::builder()
.image(opts.image)
.verify_type(VerifyType::File(&cosign_file_path))
.build(),
),
// Gitlab keyless
(CiDriverType::Github | CiDriverType::Gitlab, _, Ok(issuer), Ok(identity)) => (
SignOpts::builder().dir(&path).image(&image_digest).build(),
VerifyOpts::builder()
.image(opts.image)
.verify_type(VerifyType::Keyless { issuer, identity })
.build(),
),
_ => bail!("Failed to get information for signing the image"),
};
let retry_count = if opts.retry_push { opts.retry_count } else { 0 };
retry(retry_count, 5, || {
Self::sign(&sign_opts)?;
Self::verify(&verify_opts)
Self::sign(sign_opts)?;
Self::verify(verify_opts)
})?;
Ok(())
@ -665,7 +685,7 @@ pub trait CiDriver: PrivateDriver {
///
/// # Errors
/// Will error if the environment variables aren't set.
fn generate_tags(opts: &GenerateTagsOpts) -> Result<Vec<String>>;
fn generate_tags(opts: GenerateTagsOpts) -> Result<Vec<String>>;
/// Generates the image name based on CI.
///
@ -722,3 +742,36 @@ pub trait CiDriver: PrivateDriver {
fn default_ci_file_path() -> PathBuf;
}
#[allow(private_bounds)]
pub trait BootDriver: PrivateDriver {
/// Get the status of the current booted image.
///
/// # Errors
/// Will error if we fail to get the status.
fn status() -> Result<Box<dyn BootStatus>>;
/// Switch to a new image.
///
/// # Errors
/// Will error if we fail to switch to a new image.
fn switch(opts: SwitchOpts) -> Result<()>;
/// Upgrade an image.
///
/// # Errors
/// Will error if we fail to upgrade to a new image.
fn upgrade(opts: SwitchOpts) -> Result<()>;
}
#[allow(private_bounds)]
pub trait BootStatus: PrivateDriver {
/// Checks to see if there's a transaction in progress.
fn transaction_in_progress(&self) -> bool;
/// Gets the booted image.
fn booted_image(&self) -> Option<ImageRef<'_>>;
/// Gets the staged image.
fn staged_image(&self) -> Option<ImageRef<'_>>;
}

View file

@ -1,501 +1,9 @@
use std::{
borrow::Cow,
collections::HashMap,
path::{Path, PathBuf},
};
use blue_build_utils::{
constants::{GITHUB_ACTIONS, GITLAB_CI, IMAGE_VERSION_LABEL},
get_env_var,
semver::Version,
string,
};
use clap::ValueEnum;
use log::{trace, warn};
use oci_distribution::Reference;
use serde::Deserialize;
use serde_json::Value;
use crate::drivers::{
DriverVersion, buildah_driver::BuildahDriver, docker_driver::DockerDriver,
podman_driver::PodmanDriver,
};
mod private {
pub trait Private {}
}
pub(super) trait DetermineDriver<T> {
fn determine_driver(&mut self) -> T;
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum InspectDriverType {
Skopeo,
Podman,
Docker,
}
impl DetermineDriver<InspectDriverType> for Option<InspectDriverType> {
fn determine_driver(&mut self) -> InspectDriverType {
*self.get_or_insert(
match (
blue_build_utils::check_command_exists("skopeo"),
blue_build_utils::check_command_exists("docker"),
blue_build_utils::check_command_exists("podman"),
) {
(Ok(_skopeo), _, _) => InspectDriverType::Skopeo,
(_, Ok(_docker), _) => InspectDriverType::Docker,
(_, _, Ok(_podman)) => InspectDriverType::Podman,
_ => panic!(
"{}{}",
"Could not determine inspection strategy. ",
"You need either skopeo, docker, or podman",
),
},
)
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum BuildDriverType {
Buildah,
Podman,
Docker,
}
impl DetermineDriver<BuildDriverType> for Option<BuildDriverType> {
fn determine_driver(&mut self) -> BuildDriverType {
*self.get_or_insert(
match (
blue_build_utils::check_command_exists("docker"),
blue_build_utils::check_command_exists("podman"),
blue_build_utils::check_command_exists("buildah"),
) {
(Ok(_docker), _, _)
if DockerDriver::is_supported_version() && DockerDriver::has_buildx() =>
{
BuildDriverType::Docker
}
(_, Ok(_podman), _) if PodmanDriver::is_supported_version() => {
BuildDriverType::Podman
}
(_, _, Ok(_buildah)) if BuildahDriver::is_supported_version() => {
BuildDriverType::Buildah
}
_ => panic!(
"{}{}{}{}",
"Could not determine strategy, ",
format_args!(
"need either docker version {} with buildx, ",
DockerDriver::VERSION_REQ,
),
format_args!("podman version {}, ", PodmanDriver::VERSION_REQ,),
format_args!(
"or buildah version {} to continue",
BuildahDriver::VERSION_REQ,
),
),
},
)
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum SigningDriverType {
Cosign,
Sigstore,
}
impl DetermineDriver<SigningDriverType> for Option<SigningDriverType> {
fn determine_driver(&mut self) -> SigningDriverType {
trace!("SigningDriverType::determine_signing_driver()");
*self.get_or_insert(
blue_build_utils::check_command_exists("cosign")
.map_or(SigningDriverType::Sigstore, |()| SigningDriverType::Cosign),
)
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum RunDriverType {
Podman,
Docker,
}
impl From<RunDriverType> for String {
fn from(value: RunDriverType) -> Self {
match value {
RunDriverType::Podman => "podman".to_string(),
RunDriverType::Docker => "docker".to_string(),
}
}
}
impl DetermineDriver<RunDriverType> for Option<RunDriverType> {
fn determine_driver(&mut self) -> RunDriverType {
trace!("RunDriver::determine_driver()");
*self.get_or_insert(
match (
blue_build_utils::check_command_exists("docker"),
blue_build_utils::check_command_exists("podman"),
) {
(Ok(_docker), _) if DockerDriver::is_supported_version() => RunDriverType::Docker,
(_, Ok(_podman)) if PodmanDriver::is_supported_version() => RunDriverType::Podman,
_ => panic!(
"{}{}{}{}",
"Could not determine strategy, ",
format_args!("need either docker version {}, ", DockerDriver::VERSION_REQ),
format_args!("podman version {}, ", PodmanDriver::VERSION_REQ),
format_args!(
"or buildah version {} to continue",
BuildahDriver::VERSION_REQ
),
),
},
)
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum CiDriverType {
Local,
Gitlab,
Github,
}
impl DetermineDriver<CiDriverType> for Option<CiDriverType> {
fn determine_driver(&mut self) -> CiDriverType {
trace!("CiDriverType::determine_driver()");
*self.get_or_insert(
match (
get_env_var(GITLAB_CI).ok(),
get_env_var(GITHUB_ACTIONS).ok(),
) {
(Some(_gitlab_ci), None) => CiDriverType::Gitlab,
(None, Some(_github_actions)) => CiDriverType::Github,
_ => CiDriverType::Local,
},
)
}
}
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq, Hash)]
pub enum Platform {
#[value(name = "linux/amd64")]
LinuxAmd64,
#[value(name = "linux/amd64/v2")]
LinuxAmd64V2,
#[value(name = "linux/arm64")]
LinuxArm64,
#[value(name = "linux/arm")]
LinuxArm,
#[value(name = "linux/arm/v6")]
LinuxArmV6,
#[value(name = "linux/arm/v7")]
LinuxArmV7,
#[value(name = "linux/386")]
Linux386,
#[value(name = "linux/loong64")]
LinuxLoong64,
#[value(name = "linux/mips")]
LinuxMips,
#[value(name = "linux/mipsle")]
LinuxMipsle,
#[value(name = "linux/mips64")]
LinuxMips64,
#[value(name = "linux/mips64le")]
LinuxMips64le,
#[value(name = "linux/ppc64")]
LinuxPpc64,
#[value(name = "linux/ppc64le")]
LinuxPpc64le,
#[value(name = "linux/riscv64")]
LinuxRiscv64,
#[value(name = "linux/s390x")]
LinuxS390x,
}
impl Platform {
/// The architecture of the platform.
#[must_use]
pub const fn arch(&self) -> &str {
match *self {
Self::LinuxAmd64 | Self::LinuxAmd64V2 => "amd64",
Self::LinuxArm64 => "arm64",
Self::LinuxArm | Self::LinuxArmV6 | Self::LinuxArmV7 => "arm",
Self::Linux386 => "386",
Self::LinuxLoong64 => "loong64",
Self::LinuxMips => "mips",
Self::LinuxMipsle => "mipsle",
Self::LinuxMips64 => "mips64",
Self::LinuxMips64le => "mips64le",
Self::LinuxPpc64 => "ppc64",
Self::LinuxPpc64le => "ppc64le",
Self::LinuxRiscv64 => "riscv64",
Self::LinuxS390x => "s390x",
}
}
/// The variant of the platform.
#[must_use]
pub const fn variant(&self) -> Option<&str> {
match *self {
Self::LinuxAmd64V2 => Some("v2"),
Self::LinuxArmV6 => Some("v6"),
Self::LinuxArmV7 => Some("v7"),
_ => None,
}
}
}
impl std::fmt::Display for Platform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match *self {
Self::LinuxAmd64 => "linux/amd64",
Self::LinuxAmd64V2 => "linux/amd64/v2",
Self::LinuxArm64 => "linux/arm64",
Self::LinuxArm => "linux/arm",
Self::LinuxArmV6 => "linux/arm/v6",
Self::LinuxArmV7 => "linux/arm/v7",
Self::Linux386 => "linux/386",
Self::LinuxLoong64 => "linux/loong64",
Self::LinuxMips => "linux/mips",
Self::LinuxMipsle => "linux/mipsle",
Self::LinuxMips64 => "linux/mips64",
Self::LinuxMips64le => "linux/mips64le",
Self::LinuxPpc64 => "linux/ppc64",
Self::LinuxPpc64le => "linux/ppc64le",
Self::LinuxRiscv64 => "linux/riscv64",
Self::LinuxS390x => "linux/s390x",
}
)
}
}
impl private::Private for Option<Platform> {}
pub trait PlatformInfo: private::Private {
/// The string representation of the platform.
///
/// If `None`, then the native architecture will be used.
fn to_string(&self) -> String;
/// The string representation of the architecture.
///
/// If `None`, then the native architecture will be used.
fn arch(&self) -> &str;
}
impl PlatformInfo for Option<Platform> {
fn to_string(&self) -> String {
self.map_or_else(
|| match std::env::consts::ARCH {
"x86_64" => string!("linux/amd64"),
"aarch64" => string!("linux/arm64"),
arch => unimplemented!("Arch {arch} is unsupported"),
},
|platform| format!("{platform}"),
)
}
fn arch(&self) -> &str {
self.as_ref().map_or_else(
|| match std::env::consts::ARCH {
"x86_64" => "amd64",
"aarch64" => "arm64",
arch => unimplemented!("Arch {arch} is unsupported"),
},
Platform::arch,
)
}
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct ImageMetadata {
pub labels: HashMap<String, Value>,
pub digest: String,
}
impl ImageMetadata {
#[must_use]
pub fn get_version(&self) -> Option<u64> {
Some(
self.labels
.get(IMAGE_VERSION_LABEL)
.map(ToOwned::to_owned)
.and_then(|v| {
serde_json::from_value::<Version>(v)
.inspect_err(|e| warn!("Failed to parse version:\n{e}"))
.ok()
})?
.major,
)
}
}
#[derive(Debug, Clone)]
pub struct ContainerId(pub(super) String);
impl std::fmt::Display for ContainerId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.0)
}
}
impl AsRef<std::ffi::OsStr> for ContainerId {
fn as_ref(&self) -> &std::ffi::OsStr {
self.0.as_ref()
}
}
pub struct MountId(pub(super) String);
impl std::fmt::Display for MountId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.0)
}
}
impl AsRef<std::ffi::OsStr> for MountId {
fn as_ref(&self) -> &std::ffi::OsStr {
self.0.as_ref()
}
}
impl<'a> From<&'a MountId> for std::borrow::Cow<'a, str> {
fn from(value: &'a MountId) -> Self {
Self::Borrowed(&value.0)
}
}
#[derive(Debug, Clone)]
pub struct OciDir(String);
impl std::fmt::Display for OciDir {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.0)
}
}
impl AsRef<std::ffi::OsStr> for OciDir {
fn as_ref(&self) -> &std::ffi::OsStr {
self.0.as_ref()
}
}
impl TryFrom<std::path::PathBuf> for OciDir {
type Error = miette::Report;
fn try_from(value: std::path::PathBuf) -> Result<Self, Self::Error> {
if !value.is_dir() {
miette::bail!("OCI directory doesn't exist at {}", value.display());
}
Ok(Self(format!("oci:{}", value.display())))
}
}
/// An image ref that could reference
/// a remote registry or a local tarball.
#[derive(Debug, Clone)]
pub enum ImageRef<'scope> {
Remote(Cow<'scope, Reference>),
LocalTar(Cow<'scope, Path>),
}
impl ImageRef<'_> {
#[must_use]
pub fn remote_ref(&self) -> Option<&Reference> {
match self {
Self::Remote(remote) => Some(remote.as_ref()),
Self::LocalTar(_) => None,
}
}
}
impl<'scope> From<&'scope Self> for ImageRef<'scope> {
fn from(value: &'scope ImageRef) -> Self {
match value {
Self::Remote(remote) => Self::Remote(Cow::Borrowed(remote.as_ref())),
Self::LocalTar(path) => Self::LocalTar(Cow::Borrowed(path.as_ref())),
}
}
}
impl<'scope> From<&'scope Reference> for ImageRef<'scope> {
fn from(value: &'scope Reference) -> Self {
Self::Remote(Cow::Borrowed(value))
}
}
impl From<Reference> for ImageRef<'_> {
fn from(value: Reference) -> Self {
Self::Remote(Cow::Owned(value))
}
}
impl<'scope> From<&'scope Path> for ImageRef<'scope> {
fn from(value: &'scope Path) -> Self {
Self::LocalTar(Cow::Borrowed(value))
}
}
impl<'scope> From<&'scope PathBuf> for ImageRef<'scope> {
fn from(value: &'scope PathBuf) -> Self {
Self::from(value.as_path())
}
}
impl From<PathBuf> for ImageRef<'_> {
fn from(value: PathBuf) -> Self {
Self::LocalTar(Cow::Owned(value))
}
}
impl From<ImageRef<'_>> for String {
fn from(value: ImageRef<'_>) -> Self {
Self::from(&value)
}
}
impl From<&ImageRef<'_>> for String {
fn from(value: &ImageRef<'_>) -> Self {
format!("{value}")
}
}
impl std::fmt::Display for ImageRef<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Remote(remote) => remote.whole(),
Self::LocalTar(path) => format!("oci-archive:{}", path.display()),
}
)
}
}
mod container;
mod drivers;
mod metadata;
mod platform;
pub use container::*;
pub use drivers::*;
pub use metadata::*;
pub use platform::*;

View file

@ -0,0 +1,179 @@
use std::{
borrow::Cow,
ops::Deref,
path::{Path, PathBuf},
};
use oci_distribution::Reference;
#[derive(Debug, Clone)]
pub struct ContainerId(pub(crate) String);
impl Deref for ContainerId {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::fmt::Display for ContainerId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.0)
}
}
impl AsRef<std::ffi::OsStr> for ContainerId {
fn as_ref(&self) -> &std::ffi::OsStr {
self.0.as_ref()
}
}
pub struct MountId(pub(crate) String);
impl Deref for MountId {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::fmt::Display for MountId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.0)
}
}
impl AsRef<std::ffi::OsStr> for MountId {
fn as_ref(&self) -> &std::ffi::OsStr {
self.0.as_ref()
}
}
impl<'a> From<&'a MountId> for std::borrow::Cow<'a, str> {
fn from(value: &'a MountId) -> Self {
Self::Borrowed(&value.0)
}
}
#[derive(Debug, Clone)]
pub struct OciDir(String);
impl std::fmt::Display for OciDir {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.0)
}
}
impl AsRef<std::ffi::OsStr> for OciDir {
fn as_ref(&self) -> &std::ffi::OsStr {
self.0.as_ref()
}
}
impl TryFrom<std::path::PathBuf> for OciDir {
type Error = miette::Report;
fn try_from(value: std::path::PathBuf) -> Result<Self, Self::Error> {
if !value.is_dir() {
miette::bail!("OCI directory doesn't exist at {}", value.display());
}
Ok(Self(format!("oci:{}", value.display())))
}
}
/// An image ref that could reference
/// a remote registry or a local tarball.
#[derive(Debug, Clone)]
pub enum ImageRef<'scope> {
Remote(Cow<'scope, Reference>),
LocalTar(Cow<'scope, Path>),
Other(Cow<'scope, str>),
}
impl ImageRef<'_> {
#[must_use]
pub fn remote_ref(&self) -> Option<&Reference> {
match self {
Self::Remote(remote) => Some(remote.as_ref()),
_ => None,
}
}
}
impl<'scope> From<&'scope Self> for ImageRef<'scope> {
fn from(value: &'scope ImageRef) -> Self {
match value {
Self::Remote(remote) => Self::Remote(Cow::Borrowed(remote.as_ref())),
Self::LocalTar(path) => Self::LocalTar(Cow::Borrowed(path.as_ref())),
Self::Other(other) => Self::Other(Cow::Borrowed(other.as_ref())),
}
}
}
impl<'scope> From<&'scope Reference> for ImageRef<'scope> {
fn from(value: &'scope Reference) -> Self {
Self::Remote(Cow::Borrowed(value))
}
}
impl From<Reference> for ImageRef<'_> {
fn from(value: Reference) -> Self {
Self::Remote(Cow::Owned(value))
}
}
impl<'scope> From<&'scope Path> for ImageRef<'scope> {
fn from(value: &'scope Path) -> Self {
Self::LocalTar(Cow::Borrowed(value))
}
}
impl<'scope> From<&'scope PathBuf> for ImageRef<'scope> {
fn from(value: &'scope PathBuf) -> Self {
Self::from(value.as_path())
}
}
impl From<PathBuf> for ImageRef<'_> {
fn from(value: PathBuf) -> Self {
Self::LocalTar(Cow::Owned(value))
}
}
impl From<ImageRef<'_>> for String {
fn from(value: ImageRef<'_>) -> Self {
Self::from(&value)
}
}
impl From<&ImageRef<'_>> for String {
fn from(value: &ImageRef<'_>) -> Self {
format!("{value}")
}
}
impl std::fmt::Display for ImageRef<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Remote(remote) => remote.whole(),
Self::LocalTar(path) => format!("oci-archive:{}", path.display()),
Self::Other(other) => other.to_string(),
}
)
}
}
impl PartialEq<Reference> for ImageRef<'_> {
fn eq(&self, other: &Reference) -> bool {
match self {
Self::Remote(remote) => &**remote == other,
_ => false,
}
}
}

View file

@ -0,0 +1,191 @@
use blue_build_utils::{
constants::{GITHUB_ACTIONS, GITLAB_CI},
get_env_var,
};
use clap::ValueEnum;
use log::trace;
use crate::drivers::{
DetermineDriver, DriverVersion, buildah_driver::BuildahDriver, docker_driver::DockerDriver,
podman_driver::PodmanDriver,
};
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum InspectDriverType {
Skopeo,
Podman,
Docker,
}
impl DetermineDriver<InspectDriverType> for Option<InspectDriverType> {
fn determine_driver(&mut self) -> InspectDriverType {
*self.get_or_insert(
match (
blue_build_utils::check_command_exists("skopeo"),
blue_build_utils::check_command_exists("docker"),
blue_build_utils::check_command_exists("podman"),
) {
(Ok(_skopeo), _, _) => InspectDriverType::Skopeo,
(_, Ok(_docker), _) => InspectDriverType::Docker,
(_, _, Ok(_podman)) => InspectDriverType::Podman,
_ => panic!(
"{}{}",
"Could not determine inspection strategy. ",
"You need either skopeo, docker, or podman",
),
},
)
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum BuildDriverType {
Buildah,
Podman,
Docker,
}
impl DetermineDriver<BuildDriverType> for Option<BuildDriverType> {
fn determine_driver(&mut self) -> BuildDriverType {
*self.get_or_insert(
match (
blue_build_utils::check_command_exists("docker"),
blue_build_utils::check_command_exists("podman"),
blue_build_utils::check_command_exists("buildah"),
) {
(Ok(_docker), _, _)
if DockerDriver::is_supported_version() && DockerDriver::has_buildx() =>
{
BuildDriverType::Docker
}
(_, Ok(_podman), _) if PodmanDriver::is_supported_version() => {
BuildDriverType::Podman
}
(_, _, Ok(_buildah)) if BuildahDriver::is_supported_version() => {
BuildDriverType::Buildah
}
_ => panic!(
"{}{}{}{}",
"Could not determine strategy, ",
format_args!(
"need either docker version {} with buildx, ",
DockerDriver::VERSION_REQ,
),
format_args!("podman version {}, ", PodmanDriver::VERSION_REQ,),
format_args!(
"or buildah version {} to continue",
BuildahDriver::VERSION_REQ,
),
),
},
)
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum SigningDriverType {
Cosign,
Sigstore,
}
impl DetermineDriver<SigningDriverType> for Option<SigningDriverType> {
fn determine_driver(&mut self) -> SigningDriverType {
trace!("SigningDriverType::determine_signing_driver()");
*self.get_or_insert(
blue_build_utils::check_command_exists("cosign")
.map_or(SigningDriverType::Sigstore, |()| SigningDriverType::Cosign),
)
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum RunDriverType {
Podman,
Docker,
}
impl From<RunDriverType> for String {
fn from(value: RunDriverType) -> Self {
match value {
RunDriverType::Podman => "podman".to_string(),
RunDriverType::Docker => "docker".to_string(),
}
}
}
impl DetermineDriver<RunDriverType> for Option<RunDriverType> {
fn determine_driver(&mut self) -> RunDriverType {
trace!("RunDriver::determine_driver()");
*self.get_or_insert(
match (
blue_build_utils::check_command_exists("docker"),
blue_build_utils::check_command_exists("podman"),
) {
(Ok(_docker), _) if DockerDriver::is_supported_version() => RunDriverType::Docker,
(_, Ok(_podman)) if PodmanDriver::is_supported_version() => RunDriverType::Podman,
_ => panic!(
"{}{}{}{}",
"Could not determine strategy, ",
format_args!("need either docker version {}, ", DockerDriver::VERSION_REQ),
format_args!("podman version {}, ", PodmanDriver::VERSION_REQ),
format_args!(
"or buildah version {} to continue",
BuildahDriver::VERSION_REQ
),
),
},
)
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum CiDriverType {
Local,
Gitlab,
Github,
}
impl DetermineDriver<CiDriverType> for Option<CiDriverType> {
fn determine_driver(&mut self) -> CiDriverType {
trace!("CiDriverType::determine_driver()");
*self.get_or_insert(
match (
get_env_var(GITLAB_CI).ok(),
get_env_var(GITHUB_ACTIONS).ok(),
) {
(Some(_gitlab_ci), None) => CiDriverType::Gitlab,
(None, Some(_github_actions)) => CiDriverType::Github,
_ => CiDriverType::Local,
},
)
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum BootDriverType {
#[cfg(feature = "bootc")]
Bootc,
RpmOstree,
None,
}
impl DetermineDriver<BootDriverType> for Option<BootDriverType> {
fn determine_driver(&mut self) -> BootDriverType {
trace!("BootDriverType::determine_driver()");
*self.get_or_insert(
match (
blue_build_utils::check_command_exists("bootc"),
blue_build_utils::check_command_exists("rpm-ostree"),
) {
#[cfg(feature = "bootc")]
(Ok(_bootc), _) => BootDriverType::Bootc,
(_, Ok(_rpm_ostree)) => BootDriverType::RpmOstree,
_ => BootDriverType::None,
},
)
}
}

View file

@ -0,0 +1,30 @@
use std::collections::HashMap;
use blue_build_utils::{constants::IMAGE_VERSION_LABEL, semver::Version};
use log::warn;
use serde::Deserialize;
use serde_json::Value;
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct ImageMetadata {
pub labels: HashMap<String, Value>,
pub digest: String,
}
impl ImageMetadata {
#[must_use]
pub fn get_version(&self) -> Option<u64> {
Some(
self.labels
.get(IMAGE_VERSION_LABEL)
.map(ToOwned::to_owned)
.and_then(|v| {
serde_json::from_value::<Version>(v)
.inspect_err(|e| warn!("Failed to parse version:\n{e}"))
.ok()
})?
.major,
)
}
}

View file

@ -0,0 +1,155 @@
use blue_build_utils::string;
use clap::ValueEnum;
mod private {
pub trait Private {}
}
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq, Hash)]
pub enum Platform {
#[value(name = "linux/amd64")]
LinuxAmd64,
#[value(name = "linux/amd64/v2")]
LinuxAmd64V2,
#[value(name = "linux/arm64")]
LinuxArm64,
#[value(name = "linux/arm")]
LinuxArm,
#[value(name = "linux/arm/v6")]
LinuxArmV6,
#[value(name = "linux/arm/v7")]
LinuxArmV7,
#[value(name = "linux/386")]
Linux386,
#[value(name = "linux/loong64")]
LinuxLoong64,
#[value(name = "linux/mips")]
LinuxMips,
#[value(name = "linux/mipsle")]
LinuxMipsle,
#[value(name = "linux/mips64")]
LinuxMips64,
#[value(name = "linux/mips64le")]
LinuxMips64le,
#[value(name = "linux/ppc64")]
LinuxPpc64,
#[value(name = "linux/ppc64le")]
LinuxPpc64le,
#[value(name = "linux/riscv64")]
LinuxRiscv64,
#[value(name = "linux/s390x")]
LinuxS390x,
}
impl Platform {
/// The architecture of the platform.
#[must_use]
pub const fn arch(&self) -> &str {
match *self {
Self::LinuxAmd64 | Self::LinuxAmd64V2 => "amd64",
Self::LinuxArm64 => "arm64",
Self::LinuxArm | Self::LinuxArmV6 | Self::LinuxArmV7 => "arm",
Self::Linux386 => "386",
Self::LinuxLoong64 => "loong64",
Self::LinuxMips => "mips",
Self::LinuxMipsle => "mipsle",
Self::LinuxMips64 => "mips64",
Self::LinuxMips64le => "mips64le",
Self::LinuxPpc64 => "ppc64",
Self::LinuxPpc64le => "ppc64le",
Self::LinuxRiscv64 => "riscv64",
Self::LinuxS390x => "s390x",
}
}
/// The variant of the platform.
#[must_use]
pub const fn variant(&self) -> Option<&str> {
match *self {
Self::LinuxAmd64V2 => Some("v2"),
Self::LinuxArmV6 => Some("v6"),
Self::LinuxArmV7 => Some("v7"),
_ => None,
}
}
}
impl std::fmt::Display for Platform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match *self {
Self::LinuxAmd64 => "linux/amd64",
Self::LinuxAmd64V2 => "linux/amd64/v2",
Self::LinuxArm64 => "linux/arm64",
Self::LinuxArm => "linux/arm",
Self::LinuxArmV6 => "linux/arm/v6",
Self::LinuxArmV7 => "linux/arm/v7",
Self::Linux386 => "linux/386",
Self::LinuxLoong64 => "linux/loong64",
Self::LinuxMips => "linux/mips",
Self::LinuxMipsle => "linux/mipsle",
Self::LinuxMips64 => "linux/mips64",
Self::LinuxMips64le => "linux/mips64le",
Self::LinuxPpc64 => "linux/ppc64",
Self::LinuxPpc64le => "linux/ppc64le",
Self::LinuxRiscv64 => "linux/riscv64",
Self::LinuxS390x => "linux/s390x",
}
)
}
}
impl private::Private for Option<Platform> {}
pub trait PlatformInfo: private::Private {
/// The string representation of the platform.
///
/// If `None`, then the native architecture will be used.
fn to_string(&self) -> String;
/// The string representation of the architecture.
///
/// If `None`, then the native architecture will be used.
fn arch(&self) -> &str;
}
impl PlatformInfo for Option<Platform> {
fn to_string(&self) -> String {
self.map_or_else(
|| match std::env::consts::ARCH {
"x86_64" => string!("linux/amd64"),
"aarch64" => string!("linux/arm64"),
arch => unimplemented!("Arch {arch} is unsupported"),
},
|platform| format!("{platform}"),
)
}
fn arch(&self) -> &str {
self.as_ref().map_or_else(
|| match std::env::consts::ARCH {
"x86_64" => "amd64",
"aarch64" => "arm64",
arch => unimplemented!("Arch {arch} is unsupported"),
},
Platform::arch,
)
}
}

View file

@ -138,7 +138,7 @@ impl Recipe<'_> {
}
#[must_use]
pub fn get_secrets(&self) -> HashSet<&Secret> {
pub fn get_secrets(&self) -> Vec<&Secret> {
self.modules_ext
.modules
.iter()
@ -154,6 +154,8 @@ impl Recipe<'_> {
.filter_map(|module| Some(&module.required_fields.as_ref()?.secrets))
.flatten(),
)
.collect::<HashSet<_>>()
.into_iter()
.collect()
}
}

View file

@ -44,6 +44,35 @@ color_string() {
fi
}
feature_enabled() {
# Ensure the function is called with exactly one argument
if [ "$#" -ne 1 ]; then
echo "Usage: feature_enabled <feature_name>" >&2
return 1
fi
local feature="$1"
local -a features
# Split BB_BUILD_FEATURES by commas and read into an array
IFS=,
read -r -a features <<< "$BB_BUILD_FEATURES"
# Loop through the array and check for a match
for f in "${features[@]}"; do
# Trim leading and trailing whitespace
local trimmed_f="${f## }"
trimmed_f="${trimmed_f%% }"
if [[ "$trimmed_f" == "$feature" ]]; then
return 0
fi
done
# Feature not found
return 1
}
# Parse OS version and export it
export OS_VERSION="$(awk -F= '/^VERSION_ID=/ {gsub(/"/, "", $2); print $2}' /usr/lib/os-release)"
export OS_ARCH="$(uname -m)"

View file

@ -1,9 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
. /scripts/exports.sh
rm -rf /tmp/* /var/*
# if command -v bootc > /dev/null; then
# bootc container lint
# fi
if feature_enabled "bootc" && command -v bootc > /dev/null; then
bootc container lint
fi

View file

@ -7,20 +7,18 @@ use blue_build_process_management::{
BuildTagPushOpts, CheckKeyPairOpts, CompressionType, GenerateImageNameOpts,
GenerateTagsOpts, SignVerifyOpts,
},
types::Platform,
types::{ImageRef, Platform},
},
logging::{color_str, gen_random_ansi_color},
};
use blue_build_recipe::Recipe;
use blue_build_utils::{
constants::{
ARCHIVE_SUFFIX, BB_REGISTRY_NAMESPACE, BB_SKIP_VALIDATION, CONFIG_PATH, CONTAINER_FILE,
RECIPE_FILE, RECIPE_PATH,
ARCHIVE_SUFFIX, BB_REGISTRY_NAMESPACE, BB_SKIP_VALIDATION, CONFIG_PATH, RECIPE_FILE,
RECIPE_PATH,
},
cowstr,
credentials::{Credentials, CredentialsArgs},
string,
traits::CowCollecter,
};
use bon::Builder;
use clap::Args;
@ -163,7 +161,7 @@ impl BlueBuildCommand for BuildCommand {
if self.push {
blue_build_utils::check_command_exists("cosign")?;
Driver::check_signing_files(&CheckKeyPairOpts::builder().dir(Path::new(".")).build())?;
Driver::check_signing_files(CheckKeyPairOpts::builder().dir(Path::new(".")).build())?;
Driver::login()?;
Driver::signing_login()?;
}
@ -191,11 +189,11 @@ impl BlueBuildCommand for BuildCommand {
recipe_paths.par_iter().try_for_each(|recipe| {
GenerateCommand::builder()
.output(tempdir.path().join(if recipe_paths.len() > 1 {
blue_build_utils::generate_containerfile_path(recipe)?
} else {
PathBuf::from(CONTAINER_FILE)
}))
.output(
tempdir
.path()
.join(blue_build_utils::generate_containerfile_path(recipe)?),
)
.skip_validation(self.skip_validation)
.maybe_platform(self.platform)
.recipe(recipe)
@ -217,12 +215,10 @@ impl BuildCommand {
let images = recipe_paths
.par_iter()
.try_fold(Vec::new, |mut images, recipe_path| -> Result<Vec<String>> {
let containerfile = temp_dir.join(if recipe_paths.len() > 1 {
blue_build_utils::generate_containerfile_path(recipe_path)?
} else {
PathBuf::from(CONTAINER_FILE)
});
images.extend(self.build(recipe_path, &containerfile)?);
images.extend(self.build(
recipe_path,
&temp_dir.join(blue_build_utils::generate_containerfile_path(recipe_path)?),
)?);
Ok(images)
})
.try_reduce(Vec::new, |mut init, image_names| {
@ -245,9 +241,9 @@ impl BuildCommand {
fn build(&self, recipe_path: &Path, containerfile: &Path) -> Result<Vec<String>> {
let recipe = Recipe::parse(recipe_path)?;
let tags = Driver::generate_tags(
&GenerateTagsOpts::builder()
GenerateTagsOpts::builder()
.oci_ref(&recipe.base_image_ref()?)
.maybe_alt_tags(recipe.alt_tags.as_ref().map(CowCollecter::collect_cow_vec))
.maybe_alt_tags(recipe.alt_tags.as_deref())
.maybe_platform(self.platform)
.build(),
)?;
@ -276,45 +272,44 @@ impl BuildCommand {
&image_name,
cache_image.as_ref(),
)?
} else if let Some(archive_dir) = self.archive.as_ref() {
Driver::build_tag_push(
BuildTagPushOpts::builder()
.containerfile(containerfile)
.maybe_platform(self.platform)
.image(&ImageRef::from(PathBuf::from(format!(
"{}/{}.{ARCHIVE_SUFFIX}",
archive_dir.to_string_lossy().trim_end_matches('/'),
recipe.name.to_lowercase().replace('/', "_"),
))))
.squash(self.squash)
.maybe_cache_from(cache_image.as_ref())
.maybe_cache_to(cache_image.as_ref())
.secrets(&recipe.get_secrets())
.build(),
)?
} else {
Driver::build_tag_push(&self.archive.as_ref().map_or_else(
|| {
BuildTagPushOpts::builder()
.image(&image)
.containerfile(containerfile)
.maybe_platform(self.platform)
.tags(tags.collect_cow_vec())
.push(self.push)
.retry_push(self.retry_push)
.retry_count(self.retry_count)
.compression(self.compression_format)
.squash(self.squash)
.maybe_cache_from(cache_image.as_ref())
.maybe_cache_to(cache_image.as_ref())
.secrets(recipe.get_secrets())
.build()
},
|archive_dir| {
BuildTagPushOpts::builder()
.containerfile(containerfile)
.maybe_platform(self.platform)
.image(PathBuf::from(format!(
"{}/{}.{ARCHIVE_SUFFIX}",
archive_dir.to_string_lossy().trim_end_matches('/'),
recipe.name.to_lowercase().replace('/', "_"),
)))
.squash(self.squash)
.maybe_cache_from(cache_image.as_ref())
.maybe_cache_to(cache_image.as_ref())
.secrets(recipe.get_secrets())
.build()
},
))?
Driver::build_tag_push(
BuildTagPushOpts::builder()
.image(&ImageRef::from(&image))
.containerfile(containerfile)
.maybe_platform(self.platform)
.tags(&tags)
.push(self.push)
.retry_push(self.retry_push)
.retry_count(self.retry_count)
.compression(self.compression_format)
.squash(self.squash)
.maybe_cache_from(cache_image.as_ref())
.maybe_cache_to(cache_image.as_ref())
.secrets(&recipe.get_secrets())
.build(),
)?
};
if self.push && !self.no_sign {
Driver::sign_and_verify(
&SignVerifyOpts::builder()
SignVerifyOpts::builder()
.image(&image)
.retry_push(self.retry_push)
.retry_count(self.retry_count)
@ -342,13 +337,13 @@ impl BuildCommand {
.parse()
.into_diagnostic()?;
Driver::rechunk(
&RechunkOpts::builder()
RechunkOpts::builder()
.image(image_name)
.containerfile(containerfile)
.maybe_platform(self.platform)
.tags(tags.collect_cow_vec())
.tags(tags)
.push(self.push)
.version(format!(
.version(&format!(
"{version}.<date>",
version = Driver::get_os_version()
.oci_ref(&recipe.base_image_ref()?)
@ -359,23 +354,23 @@ impl BuildCommand {
.retry_count(self.retry_count)
.compression(self.compression_format)
.base_digest(
Driver::get_metadata(
&GetMetadataOpts::builder()
&Driver::get_metadata(
GetMetadataOpts::builder()
.image(&base_image)
.maybe_platform(self.platform)
.build(),
)?
.digest,
)
.repo(Driver::get_repo_url()?)
.name(&*recipe.name)
.description(&*recipe.description)
.base_image(format!("{}:{}", &recipe.base_image, &recipe.image_version))
.repo(&Driver::get_repo_url()?)
.name(&recipe.name)
.description(&recipe.description)
.base_image(&format!("{}:{}", &recipe.base_image, &recipe.image_version))
.maybe_tempdir(self.tempdir.as_deref())
.clear_plan(self.rechunk_clear_plan)
.maybe_cache_from(cache_image)
.maybe_cache_to(cache_image)
.secrets(recipe.get_secrets())
.secrets(&recipe.get_secrets())
.build(),
)
}
@ -384,8 +379,8 @@ impl BuildCommand {
let image_name = Driver::generate_image_name(
GenerateImageNameOpts::builder()
.name(recipe.name.trim())
.maybe_registry(self.credentials.registry.as_ref().map(|r| cowstr!(r)))
.maybe_registry_namespace(self.registry_namespace.as_ref().map(|r| cowstr!(r)))
.maybe_registry(self.credentials.registry.as_deref())
.maybe_registry_namespace(self.registry_namespace.as_deref())
.build(),
)?;

View file

@ -142,6 +142,19 @@ impl GenerateCommand {
let base_image: Reference = format!("{}:{}", &recipe.base_image, &recipe.image_version)
.parse()
.into_diagnostic()?;
let base_digest = &Driver::get_metadata(
GetMetadataOpts::builder()
.image(&base_image)
.maybe_platform(self.platform)
.build(),
)?
.digest;
let build_scripts_image = &determine_scripts_tag(self.platform)?;
let repo = &Driver::get_repo_url()?;
let build_features = &[
#[cfg(feature = "bootc")]
"bootc".into(),
];
let template = ContainerFileTemplate::builder()
.os_version(
@ -153,19 +166,12 @@ impl GenerateCommand {
.build_id(Driver::get_build_id())
.recipe(&recipe)
.recipe_path(recipe_path.as_path())
.registry(registry)
.repo(Driver::get_repo_url()?)
.build_scripts_image(determine_scripts_tag(self.platform)?.to_string())
.base_digest(
Driver::get_metadata(
&GetMetadataOpts::builder()
.image(&base_image)
.maybe_platform(self.platform)
.build(),
)?
.digest,
)
.registry(&registry)
.repo(repo)
.build_scripts_image(build_scripts_image)
.base_digest(base_digest)
.maybe_nushell_version(recipe.nushell_version.as_ref())
.build_features(build_features)
.build();
let output_str = template.render().into_diagnostic()?;
@ -197,7 +203,7 @@ fn determine_scripts_tag(platform: Option<Platform>) -> Result<Reference> {
.parse()
.into_diagnostic()
.and_then(|image| {
Driver::get_metadata(&opts.clone().image(&image).build())
Driver::get_metadata(opts.clone().image(&image).build())
.inspect_err(|e| trace!("{e:?}"))
.map(|_| image)
})
@ -205,7 +211,7 @@ fn determine_scripts_tag(platform: Option<Platform>) -> Result<Reference> {
let image: Reference = format!("{BUILD_SCRIPTS_IMAGE_REF}:{}", shadow::BRANCH)
.parse()
.into_diagnostic()?;
Driver::get_metadata(&opts.clone().image(&image).build())
Driver::get_metadata(opts.clone().image(&image).build())
.inspect_err(|e| trace!("{e:?}"))
.map(|_| image)
})
@ -213,7 +219,7 @@ fn determine_scripts_tag(platform: Option<Platform>) -> Result<Reference> {
let image: Reference = format!("{BUILD_SCRIPTS_IMAGE_REF}:v{}", crate_version!())
.parse()
.into_diagnostic()?;
Driver::get_metadata(&opts.image(&image).build())
Driver::get_metadata(opts.image(&image).build())
.inspect_err(|e| trace!("{e:?}"))
.map(|_| image)
})

View file

@ -7,7 +7,6 @@ use blue_build_recipe::Recipe;
use blue_build_utils::{
constants::{ARCHIVE_SUFFIX, BB_SKIP_VALIDATION},
string_vec,
traits::CowCollecter,
};
use bon::Builder;
use clap::{Args, Subcommand, ValueEnum};
@ -189,8 +188,10 @@ impl GenerateIsoCommand {
format!("SECURE_BOOT_KEY_URL={}", self.secure_boot_url),
format!("ENROLLMENT_PASSWORD={}", self.enrollment_password),
];
let image_out_dir = &image_out_dir.display().to_string();
let output_dir = &output_dir.display().to_string();
let mut vols = run_volumes![
output_dir.display().to_string() => "/build-container-installer/build",
output_dir => "/build-container-installer/build",
"dnf-cache" => "/cache/dnf/",
];
@ -239,8 +240,8 @@ impl GenerateIsoCommand {
.call()?,
),
]);
vols.extend(run_volumes![
image_out_dir.display().to_string() => "/img_src/",
vols.extend(&run_volumes![
image_out_dir => "/img_src/",
]);
}
}
@ -250,11 +251,11 @@ impl GenerateIsoCommand {
.image("ghcr.io/jasonn3/build-container-installer")
.privileged(true)
.remove(true)
.args(args.collect_cow_vec())
.volumes(vols)
.args(&args)
.volumes(&vols)
.build();
let status = Driver::run(&opts)?;
let status = Driver::run(opts)?;
if !status.success() {
bail!("Failed to create ISO");

View file

@ -518,8 +518,8 @@ impl InitCommand {
.with_context(|| format!("Failed to delete old public file {COSIGN_PUB_PATH}"))?;
Driver::generate_key_pair(
&GenerateKeyPairOpts::builder()
.maybe_dir(self.dir.as_ref())
GenerateKeyPairOpts::builder()
.maybe_dir(self.dir.as_deref())
.build(),
)
}

View file

@ -73,7 +73,7 @@ impl BlueBuildCommand for PruneCommand {
}
Driver::prune(
&PruneOpts::builder()
PruneOpts::builder()
.all(self.all)
.volumes(self.volumes)
.build(),

View file

@ -1,29 +1,19 @@
use std::{
path::{Path, PathBuf},
time::Duration,
};
use std::path::PathBuf;
use blue_build_process_management::{
drivers::{Driver, DriverArgs},
logging::CommandLogging,
use blue_build_process_management::drivers::{
BootDriver, BuildDriver, CiDriver, Driver, DriverArgs, PodmanDriver, RunDriver,
opts::{BuildOpts, GenerateImageNameOpts, RemoveImageOpts, SwitchOpts},
types::ImageRef,
};
use blue_build_recipe::Recipe;
use blue_build_utils::{
constants::{
ARCHIVE_SUFFIX, BB_SKIP_VALIDATION, LOCAL_BUILD, OCI_ARCHIVE, OSTREE_UNVERIFIED_IMAGE,
SUDO_ASKPASS,
},
has_env_var, running_as_root,
};
use blue_build_utils::constants::BB_SKIP_VALIDATION;
use bon::Builder;
use clap::Args;
use comlexr::cmd;
use indicatif::ProgressBar;
use log::{debug, trace};
use log::trace;
use miette::{IntoDiagnostic, Result, bail};
use tempfile::TempDir;
use crate::{commands::build::BuildCommand, rpm_ostree_status::RpmOstreeStatus};
use crate::commands::generate::GenerateCommand;
use super::BlueBuildCommand;
@ -60,238 +50,59 @@ impl BlueBuildCommand for SwitchCommand {
Driver::init(self.drivers);
let status = RpmOstreeStatus::try_new()?;
trace!("{status:?}");
let status = Driver::status()?;
if status.transaction_in_progress() {
bail!("There is a transaction in progress. Please cancel it using `rpm-ostree cancel`");
}
let recipe = Recipe::parse(&self.recipe)?;
let image_name = Driver::generate_image_name(
GenerateImageNameOpts::builder()
.name(recipe.name.trim())
.build(),
)?;
let tempdir = if let Some(ref dir) = self.tempdir {
TempDir::new_in(dir).into_diagnostic()?
} else {
TempDir::new().into_diagnostic()?
};
trace!("{tempdir:?}");
let containerfile = tempdir
.path()
.join(blue_build_utils::generate_containerfile_path(&self.recipe)?);
BuildCommand::builder()
.recipe([self.recipe.clone()])
.archive(tempdir.path())
.maybe_tempdir(self.tempdir.clone())
.skip_validation(self.skip_validation)
GenerateCommand::builder()
.output(&containerfile)
.recipe(&self.recipe)
.build()
.try_run()?;
PodmanDriver::build(
BuildOpts::builder()
.image(&ImageRef::from(&image_name))
.containerfile(&containerfile)
.secrets(&recipe.get_secrets())
.build(),
)?;
PodmanDriver::copy_image_to_root_store(&image_name)?;
PodmanDriver::remove_image(RemoveImageOpts::builder().image(&image_name).build())?;
let recipe = Recipe::parse(&self.recipe)?;
let image_file_name = format!(
"{}.{ARCHIVE_SUFFIX}",
recipe.name.to_lowercase().replace('/', "_")
);
let temp_file_path = tempdir.path().join(&image_file_name);
let archive_path = Path::new(LOCAL_BUILD).join(&image_file_name);
Self::clean_local_build_dir()?;
Self::move_archive(&temp_file_path, &archive_path)?;
// We drop the tempdir ahead of time so that the directory
// can be cleaned out.
drop(tempdir);
self.switch(&archive_path, &status)
}
}
impl SwitchCommand {
fn switch(&self, archive_path: &Path, status: &RpmOstreeStatus<'_>) -> Result<()> {
trace!(
"SwitchCommand::switch({}, {status:#?})",
archive_path.display()
);
let status = if status.is_booted_on_archive(archive_path)
|| status.is_staged_on_archive(archive_path)
if status
.booted_image()
.is_some_and(|booted| booted == image_name)
{
let command = cmd!("rpm-ostree", "upgrade", if self.reboot => "--reboot");
trace!("{command:?}");
command
Driver::upgrade(
SwitchOpts::builder()
.image(&image_name)
.reboot(self.reboot)
.build(),
)
} else {
let image_ref = format!(
"{OSTREE_UNVERIFIED_IMAGE}:{OCI_ARCHIVE}:{path}",
path = archive_path.display()
);
let command = cmd!(
"rpm-ostree",
"rebase",
&image_ref,
if self.reboot => "--reboot",
);
trace!("{command:?}");
command
Driver::switch(
SwitchOpts::builder()
.image(&image_name)
.reboot(self.reboot)
.build(),
)
}
.build_status(
format!("{}", archive_path.display()),
"Switching to new image",
)
.into_diagnostic()?;
if !status.success() {
bail!("Failed to switch to new image!");
}
Ok(())
}
fn move_archive(from: &Path, to: &Path) -> Result<()> {
trace!(
"SwitchCommand::move_archive({}, {})",
from.display(),
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()));
let status = {
let c = cmd!(
if running_as_root() {
"mv"
} else {
"sudo"
},
if !running_as_root() && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
format!("Password needed to move {from:?} to {to:?}"),
],
if !running_as_root() => "mv",
from,
to,
);
trace!("{c:?}");
c
}
.status()
.into_diagnostic()?;
progress.finish_and_clear();
if !status.success() {
bail!(
"Failed to move archive from {from} to {to}",
from = from.display(),
to = to.display()
);
}
Ok(())
}
fn clean_local_build_dir() -> Result<()> {
trace!("SwitchCommand::clean_local_build_dir()");
let local_build_path = Path::new(LOCAL_BUILD);
if local_build_path.exists() {
debug!("Cleaning out build dir {LOCAL_BUILD}");
let mut command = {
let c = cmd!(
if running_as_root() {
"ls"
} else {
"sudo"
},
if !running_as_root() && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
format!("Password required to list files in {LOCAL_BUILD}"),
],
if !running_as_root() => "ls",
LOCAL_BUILD
);
trace!("{c:?}");
c
};
let output =
String::from_utf8(command.output().into_diagnostic()?.stdout).into_diagnostic()?;
trace!("{output}");
let files = output
.lines()
.filter(|line| line.ends_with(ARCHIVE_SUFFIX))
.map(|file| local_build_path.join(file).display().to_string())
.collect::<Vec<_>>();
if !files.is_empty() {
let progress = ProgressBar::new_spinner();
progress.enable_steady_tick(Duration::from_millis(100));
progress.set_message("Removing old image archive files...");
let status = {
let c = cmd!(
if running_as_root() {
"rm"
} else {
"sudo"
},
if !running_as_root() && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
format!("Password required to remove files: {files:?}"),
],
if !running_as_root() => "rm",
"-f",
for files,
);
trace!("{c:?}");
c
}
.status()
.into_diagnostic()?;
progress.finish_and_clear();
if !status.success() {
bail!("Failed to clean out archives in {LOCAL_BUILD}");
}
}
} else {
debug!(
"Creating build output dir at {}",
local_build_path.display()
);
let status = {
let c = cmd!(
if running_as_root() {
"mkdir"
} else {
"sudo"
},
if !running_as_root() && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
format!("Password needed to create directory {local_build_path:?}"),
],
if !running_as_root() => "mkdir",
"-p",
local_build_path,
);
trace!("{c:?}");
c
}
.status()
.into_diagnostic()?;
if !status.success() {
bail!("Failed to create directory {LOCAL_BUILD}");
}
}
Ok(())
}
}

View file

@ -4,4 +4,3 @@
shadow_rs::shadow!(shadow);
pub mod commands;
pub mod rpm_ostree_status;

View file

@ -1,256 +0,0 @@
use std::{borrow::Cow, path::Path};
use comlexr::cmd;
use log::trace;
use miette::{IntoDiagnostic, Result, bail};
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct RpmOstreeStatus<'a> {
deployments: Cow<'a, [RpmOstreeDeployments<'a>]>,
transactions: Option<Cow<'a, [Cow<'a, str>]>>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
struct RpmOstreeDeployments<'a> {
container_image_reference: Cow<'a, str>,
booted: bool,
staged: bool,
}
impl RpmOstreeStatus<'_> {
/// Creates a status struct for `rpm-ostree`.
///
/// # Errors
/// Errors if the command fails or deserialization fails.
pub fn try_new() -> Result<Self> {
blue_build_utils::check_command_exists("rpm-ostree")?;
trace!("rpm-ostree status --json");
let output = cmd!("rpm-ostree", "status", "--json")
.output()
.into_diagnostic()?;
if !output.status.success() {
bail!("Failed to get `rpm-ostree` status!");
}
trace!("{}", String::from_utf8_lossy(&output.stdout));
serde_json::from_slice(&output.stdout).into_diagnostic()
}
/// Checks if there is a transaction in progress.
#[must_use]
pub fn transaction_in_progress(&self) -> bool {
self.transactions.as_ref().is_some_and(|tr| !tr.is_empty())
}
/// Get the booted image's reference.
#[must_use]
pub fn booted_image(&self) -> Option<String> {
Some(
self.deployments
.iter()
.find(|deployment| deployment.booted)?
.container_image_reference
.to_string(),
)
}
/// Get the booted image's reference.
#[must_use]
pub fn staged_image(&self) -> Option<String> {
Some(
self.deployments
.iter()
.find(|deployment| deployment.staged)?
.container_image_reference
.to_string(),
)
}
#[must_use]
pub fn is_booted_on_archive<P>(&self, archive_path: P) -> bool
where
P: AsRef<Path>,
{
self.booted_image().is_some_and(|deployment| {
deployment
.split(':')
.next_back()
.is_some_and(|boot_ref| Path::new(boot_ref) == archive_path.as_ref())
})
}
#[must_use]
pub fn is_staged_on_archive<P>(&self, archive_path: P) -> bool
where
P: AsRef<Path>,
{
self.staged_image().is_some_and(|deployment| {
deployment
.split(':')
.next_back()
.is_some_and(|boot_ref| Path::new(boot_ref) == archive_path.as_ref())
})
}
}
#[cfg(test)]
mod test {
use std::path::Path;
use blue_build_utils::constants::{
ARCHIVE_SUFFIX, LOCAL_BUILD, OCI_ARCHIVE, OSTREE_IMAGE_SIGNED, OSTREE_UNVERIFIED_IMAGE,
};
use super::{RpmOstreeDeployments, RpmOstreeStatus};
fn create_image_status<'a>() -> RpmOstreeStatus<'a> {
RpmOstreeStatus {
deployments: vec![
RpmOstreeDeployments {
container_image_reference: format!(
"{OSTREE_IMAGE_SIGNED}:docker://ghcr.io/blue-build/cli/test"
)
.into(),
booted: true,
staged: false,
},
RpmOstreeDeployments {
container_image_reference: format!(
"{OSTREE_IMAGE_SIGNED}:docker://ghcr.io/blue-build/cli/test:last"
)
.into(),
booted: false,
staged: false,
},
]
.into(),
transactions: None,
}
}
fn create_transaction_status<'a>() -> RpmOstreeStatus<'a> {
RpmOstreeStatus {
deployments: vec![
RpmOstreeDeployments {
container_image_reference: format!(
"{OSTREE_IMAGE_SIGNED}:docker://ghcr.io/blue-build/cli/test"
)
.into(),
booted: true,
staged: false,
},
RpmOstreeDeployments {
container_image_reference: format!(
"{OSTREE_IMAGE_SIGNED}:docker://ghcr.io/blue-build/cli/test:last"
)
.into(),
booted: false,
staged: false,
},
]
.into(),
transactions: Some(vec!["Upgrade".into(), "/".into()].into()),
}
}
fn create_archive_status<'a>() -> RpmOstreeStatus<'a> {
RpmOstreeStatus {
deployments: vec![
RpmOstreeDeployments {
container_image_reference:
format!("{OSTREE_UNVERIFIED_IMAGE}:{OCI_ARCHIVE}:{LOCAL_BUILD}/cli_test.{ARCHIVE_SUFFIX}").into(),
booted: true,
staged: false,
},
RpmOstreeDeployments {
container_image_reference:
format!("{OSTREE_IMAGE_SIGNED}:docker://ghcr.io/blue-build/cli/test:last").into(),
booted: false,
staged: false,
},
]
.into(),
transactions: None,
}
}
fn create_archive_staged_status<'a>() -> RpmOstreeStatus<'a> {
RpmOstreeStatus {
deployments: vec![
RpmOstreeDeployments {
container_image_reference:
format!("{OSTREE_UNVERIFIED_IMAGE}:{OCI_ARCHIVE}:{LOCAL_BUILD}/cli_test.{ARCHIVE_SUFFIX}").into(),
booted: false,
staged: true,
},
RpmOstreeDeployments {
container_image_reference:
format!("{OSTREE_UNVERIFIED_IMAGE}:{OCI_ARCHIVE}:{LOCAL_BUILD}/cli_test.{ARCHIVE_SUFFIX}").into(),
booted: true,
staged: false,
},
RpmOstreeDeployments {
container_image_reference:
format!("{OSTREE_IMAGE_SIGNED}:docker://ghcr.io/blue-build/cli/test:last").into(),
booted: false,
staged: false,
},
]
.into(),
transactions: None,
}
}
#[test]
fn test_booted_image() {
assert!(
create_image_status()
.booted_image()
.expect("Contains image")
.ends_with("cli/test")
);
}
#[test]
fn test_staged_image() {
assert!(
create_archive_staged_status()
.staged_image()
.expect("Contains image")
.ends_with(&format!("cli_test.{ARCHIVE_SUFFIX}"))
);
}
#[test]
fn test_transaction_in_progress() {
assert!(create_transaction_status().transaction_in_progress());
assert!(!create_image_status().transaction_in_progress());
}
#[test]
fn test_is_booted_archive() {
assert!(
!create_archive_status()
.is_booted_on_archive(Path::new(LOCAL_BUILD).join(format!("cli.{ARCHIVE_SUFFIX}")))
);
assert!(create_archive_status().is_booted_on_archive(
Path::new(LOCAL_BUILD).join(format!("cli_test.{ARCHIVE_SUFFIX}"))
));
}
#[test]
fn test_is_staged_archive() {
assert!(
!create_archive_staged_status()
.is_staged_on_archive(Path::new(LOCAL_BUILD).join(format!("cli.{ARCHIVE_SUFFIX}")))
);
assert!(create_archive_staged_status().is_staged_on_archive(
Path::new(LOCAL_BUILD).join(format!("cli_test.{ARCHIVE_SUFFIX}"))
));
}
}

View file

@ -12,6 +12,7 @@ license.workspace = true
askama = { version = "0.14", features = ["serde_json"] }
blue-build-recipe = { version = "=0.9.22", path = "../recipe" }
blue-build-utils = { version = "=0.9.22", path = "../utils" }
oci-distribution.workspace = true
chrono.workspace = true
log.workspace = true

View file

@ -9,28 +9,29 @@ use bon::Builder;
use chrono::Utc;
use colored::control::ShouldColorize;
use log::{debug, error, trace, warn};
use oci_distribution::Reference;
use uuid::Uuid;
pub use askama::Template;
#[derive(Debug, Clone, Template, Builder)]
#[template(path = "Containerfile.j2", escape = "none", whitespace = "minimize")]
#[builder(on(Cow<'_, str>, into))]
pub struct ContainerFileTemplate<'a> {
#[builder(into)]
recipe: &'a Recipe<'a>,
#[builder(into)]
recipe_path: Cow<'a, Path>,
recipe_path: &'a Path,
#[builder(into)]
build_id: Uuid,
os_version: u64,
registry: Cow<'a, str>,
build_scripts_image: Cow<'a, str>,
repo: Cow<'a, str>,
base_digest: Cow<'a, str>,
registry: &'a str,
build_scripts_image: &'a Reference,
repo: &'a str,
base_digest: &'a str,
nushell_version: Option<&'a MaybeVersion>,
#[builder(default)]
build_features: &'a [String],
}
impl ContainerFileTemplate<'_> {
@ -47,6 +48,15 @@ impl ContainerFileTemplate<'_> {
Some(MaybeVersion::Version(version)) => version.to_string(),
}
}
#[must_use]
fn get_features(&self) -> String {
self.build_features
.iter()
.map(|feat| feat.trim())
.collect::<Vec<_>>()
.join(",")
}
}
#[derive(Debug, Clone, Template, Builder)]

View file

@ -7,6 +7,7 @@ FROM {{ recipe.base_image }}@{{ base_digest }} AS {{ main_stage }}
ARG RECIPE={{ recipe_path.display() }}
ARG IMAGE_REGISTRY={{ registry }}
ARG BB_BUILD_FEATURES="{{ get_features() }}"
{%- if self::config_dir_exists() && !self::files_dir_exists() %}
ARG CONFIG_DIRECTORY="/tmp/config"

View file

@ -88,6 +88,7 @@ pub const OSTREE_IMAGE_SIGNED: &str = "ostree-image-signed";
pub const OSTREE_UNVERIFIED_IMAGE: &str = "ostree-unverified-image";
pub const SKOPEO_IMAGE: &str = "quay.io/skopeo/stable:latest";
pub const TEMPLATE_REPO_URL: &str = "https://github.com/blue-build/template.git";
pub const USER: &str = "USER";
pub const UNKNOWN_SHELL: &str = "<unknown shell>";
pub const UNKNOWN_VERSION: &str = "<unknown version>";
pub const UNKNOWN_TERMINAL: &str = "<unknown terminal>";
@ -110,3 +111,4 @@ pub const STAGE_SCHEMA: &str = concat!(JSON_SCHEMA, "/stage-v1.json");
// Messages
pub const BUG_REPORT_WARNING_MESSAGE: &str =
"Please copy the above report and open an issue manually.";
pub const SUDO_PROMPT: &str = "Bluebuild requires your password for sudo operation";

View file

@ -39,3 +39,155 @@ macro_rules! cowstr_vec {
}
};
}
#[macro_export]
macro_rules! impl_de_fromstr {
($($typ:ty),* $(,)?) => {
$(
impl TryFrom<&str> for $typ {
type Error = miette::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
impl TryFrom<&String> for $typ {
type Error = miette::Error;
fn try_from(value: &String) -> Result<Self, Self::Error> {
Self::try_from(value.as_str())
}
}
impl TryFrom<String> for $typ {
type Error = miette::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_from(value.as_str())
}
}
impl<'de> serde::de::Deserialize<'de> for $typ {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Self::try_from(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
}
}
)*
};
}
#[macro_export]
macro_rules! sudo_cmd {
(
prompt = $prompt:expr,
sudo_check = $sudo_check:expr,
$command:expr,
$($rest:tt)*
) => {
{
let _use_sudo = ($sudo_check) && !$crate::running_as_root();
::comlexr::cmd!(
if _use_sudo {
"sudo"
} else {
$command
},
if _use_sudo && $crate::has_env_var($crate::constants::SUDO_ASKPASS) => [
"-A",
"-p",
$prompt,
],
if _use_sudo => [
"--preserve-env",
$command,
],
$($rest)*
)
}
};
(
sudo_check = $sudo_check:expr,
$command:expr,
$($rest:tt)*
) => {
{
let _use_sudo = ($sudo_check) && !$crate::running_as_root();
::comlexr::cmd!(
if _use_sudo {
"sudo"
} else {
$command
},
if _use_sudo && $crate::has_env_var($crate::constants::SUDO_ASKPASS) => [
"-A",
"-p",
$crate::constants::SUDO_PROMPT,
],
if _use_sudo => [
"--preserve-env",
$command,
],
$($rest)*
)
}
};
(
prompt = $prompt:expr,
$command:expr,
$($rest:tt)*
) => {
{
let _use_sudo = !$crate::running_as_root();
::comlexr::cmd!(
if _use_sudo {
"sudo"
} else {
$command
},
if _use_sudo && $crate::has_env_var($crate::constants::SUDO_ASKPASS) => [
"-A",
"-p",
$prompt,
],
if _use_sudo => [
"--preserve-env",
$command,
],
$($rest)*
)
}
};
(
$command:expr,
$($rest:tt)*
) => {
{
let _use_sudo = !$crate::running_as_root();
::comlexr::cmd!(
if _use_sudo {
"sudo"
} else {
$command
},
if _use_sudo && $crate::has_env_var($crate::constants::SUDO_ASKPASS) => [
"-A",
"-p",
$crate::constants::SUDO_PROMPT,
],
if _use_sudo => [
"--preserve-env",
$command,
],
$($rest)*
)
}
};
}

View file

@ -1,5 +1,4 @@
use std::{
collections::HashSet,
fs,
hash::{DefaultHasher, Hash, Hasher},
ops::Not,
@ -121,7 +120,7 @@ impl SecretMounts for Vec<Secret> {
}
}
impl<H: std::hash::BuildHasher> private::Private for HashSet<&Secret, H> {}
impl private::Private for &[&Secret] {}
#[allow(private_bounds)]
pub trait SecretArgs: private::Private {
@ -138,7 +137,7 @@ pub trait SecretArgs: private::Private {
fn ssh(&self) -> bool;
}
impl<H: std::hash::BuildHasher> SecretArgs for HashSet<&Secret, H> {
impl SecretArgs for &[&Secret] {
fn args(&self, temp_dir: &TempDir) -> Result<Vec<String>> {
Ok(self
.iter()
@ -173,7 +172,7 @@ impl<H: std::hash::BuildHasher> SecretArgs for HashSet<&Secret, H> {
}
fn ssh(&self) -> bool {
self.contains(&Secret::Ssh)
self.contains(&&Secret::Ssh)
}
}