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: arm64-build:
timeout-minutes: 90 timeout-minutes: 90
# runs-on: ubuntu-24.04-arm runs-on: ubuntu-24.04-arm
runs-on: ubuntu-latest # runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
packages: write packages: write
@ -218,8 +218,8 @@ jobs:
- name: Maximize build space - name: Maximize build space
uses: ublue-os/remove-unwanted-software@cc0becac701cf642c8f0a6613bbdaf5dc36b259e # v9 uses: ublue-os/remove-unwanted-software@cc0becac701cf642c8f0a6613bbdaf5dc36b259e # v9
- name: Set up QEMU # - name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 # uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 - 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" comlexr = "1"
indexmap = { version = "2", features = ["serde"] } indexmap = { version = "2", features = ["serde"] }
indicatif = { version = "0.18", features = ["improved_unicode", "rayon"] } indicatif = { version = "0.18", features = ["improved_unicode", "rayon"] }
lazy-regex = "3"
log = "0.4" log = "0.4"
miette = "7" miette = "7"
nix = { version = "0.29" } nix = { version = "0.29" }
oci-distribution = { version = "0.11", default-features = false } oci-distribution = { version = "0.11", default-features = false }
pretty_assertions = "1"
regex = "1"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
rstest = "0.18" rstest = "0.18"
semver = "1" semver = "1"
@ -78,7 +81,6 @@ jsonschema = "0.30"
open = "5" open = "5"
os_info = "3" os_info = "3"
rayon = "1" rayon = "1"
regex = "1"
requestty = { version = "0.5", features = ["macros", "termion"] } requestty = { version = "0.5", features = ["macros", "termion"] }
shadow-rs = { version = "1", default-features = false } shadow-rs = { version = "1", default-features = false }
thiserror = "2" thiserror = "2"
@ -94,6 +96,7 @@ indicatif.workspace = true
log.workspace = true log.workspace = true
miette = { workspace = true, features = ["fancy"] } miette = { workspace = true, features = ["fancy"] }
oci-distribution.workspace = true oci-distribution.workspace = true
regex.workspace = true
reqwest.workspace = true reqwest.workspace = true
semver.workspace = true semver.workspace = true
serde.workspace = true serde.workspace = true
@ -109,6 +112,11 @@ users.workspace = true
# Top level features # Top level features
default = [] default = []
v0_10_0 = [
"bootc"
]
bootc = ["blue-build-process-management/bootc"]
[dev-dependencies] [dev-dependencies]
rusty-hook = "0.11" rusty-hook = "0.11"

View file

@ -43,8 +43,8 @@ build-full:
switch: switch:
FROM +test-base FROM +test-base
RUN mkdir -p /etc/bluebuild && touch $BB_TEST_LOCAL_IMAGE RUN --no-cache bluebuild -v switch --boot-driver rpm-ostree recipes/recipe.yml
RUN --no-cache bluebuild -v switch recipes/recipe.yml RUN --no-cache bluebuild -v switch --boot-driver bootc recipes/recipe.yml
validate: validate:
FROM +test-base FROM +test-base
@ -92,7 +92,7 @@ init:
legacy-base: legacy-base:
FROM ../+blue-build-cli --RELEASE=false 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 ENV CLICOLOR_FORCE=1
COPY ./mock-scripts/ /usr/bin/ 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 ../+INSTALL --OUT_DIR="/usr/bin/" --BUILD_TARGET="x86_64-unknown-linux-musl" --TAGGED="true"
DO +GEN_KEYPAIR DO +GEN_KEYPAIR
ENV USER=root
test-base: test-base:
FROM ../+blue-build-cli --RELEASE=false FROM ../+blue-build-cli --RELEASE=false
RUN git config --global user.email "you@example.com" && \ RUN git config --global user.email "you@example.com" && \
git config --global user.name "Your Name" 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 ENV CLICOLOR_FORCE=1
ARG MOCK="true" ARG MOCK="true"
@ -121,6 +122,7 @@ test-base:
COPY ./test-repo /test COPY ./test-repo /test
DO +GEN_KEYPAIR DO +GEN_KEYPAIR
ENV USER=root
GEN_KEYPAIR: GEN_KEYPAIR:
FUNCTION 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 "Exporting image to a tarball (JK JUST A MOCK!)"
echo "${tarpath}" echo "${tarpath}"
touch $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 else
echo 'Running podman' echo 'Running podman'
fi fi

View file

@ -3,11 +3,11 @@
set -euo pipefail set -euo pipefail
if [ "$1" = "rebase" ]; then 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" echo "Rebased to local image $BB_TEST_LOCAL_IMAGE"
else else
echo "Failed to rebase" echo "Failed to rebase"
exit 1 exit 1
fi fi
elif [ "$1" = "upgrade" ]; then elif [ "$1" = "upgrade" ]; then
echo "Performing upgrade for $BB_TEST_LOCAL_IMAGE" 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 # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-arm64 name: cli/test-arm64
description: This is my personal OS image. description: This is my personal OS image.
base-image: quay.io/fedora/fedora-silverblue base-image: quay.io/fedora/fedora-bootc
image-version: latest image-version: latest
stages: stages:
- from-file: stages.yml - from-file: stages.yml

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-buildah name: cli/test-buildah
description: This is my personal OS image. 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 image-version: latest
stages: stages:
- from-file: stages.yml - from-file: stages.yml

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-docker-external name: cli/test-docker-external
description: This is my personal OS image. 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 image-version: latest
stages: stages:
- from-file: stages.yml - from-file: stages.yml

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test name: cli/test
description: This is my personal OS image. 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 image-version: 40
stages: stages:
- from-file: invalid-stages.yml - from-file: invalid-stages.yml

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-invalid-module name: cli/test-invalid-module
description: This is my personal OS image. 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 image-version: 40
stages: stages:
- from-file: stages.yml - from-file: stages.yml

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-invalid-stage name: cli/test-invalid-stage
description: This is my personal OS image. 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 image-version: 40
stages: stages:
- name: ubuntu-test - name: ubuntu-test

View file

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

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-podman name: cli/test-podman
description: This is my personal OS image. 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 image-version: latest
stages: stages:
- from-file: stages.yml - from-file: stages.yml

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-rechunk name: cli/test-rechunk
description: This is my personal OS image. 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 image-version: latest
stages: stages:
- from-file: stages.yml - from-file: stages.yml

View file

@ -2,7 +2,7 @@
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test name: cli/test
description: This is my personal OS image. 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 image-version: latest
stages: stages:
- from-file: stages.yml - from-file: stages.yml

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
use clap::ValueEnum; use clap::ValueEnum;
pub use boot::*;
pub use build::*; pub use build::*;
pub use ci::*; pub use ci::*;
pub use inspect::*; pub use inspect::*;
@ -7,6 +8,7 @@ pub use rechunk::*;
pub use run::*; pub use run::*;
pub use signing::*; pub use signing::*;
mod boot;
mod build; mod build;
mod ci; mod ci;
mod inspect; 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 blue_build_utils::secret::Secret;
use bon::Builder; use bon::Builder;
@ -9,16 +9,14 @@ use crate::drivers::types::{ImageRef, Platform};
use super::CompressionType; use super::CompressionType;
/// Options for building /// Options for building
#[derive(Debug, Clone, Builder)] #[derive(Debug, Clone, Copy, Builder)]
pub struct BuildOpts<'scope> { pub struct BuildOpts<'scope> {
#[builder(into)] pub image: &'scope ImageRef<'scope>,
pub image: ImageRef<'scope>,
#[builder(default)] #[builder(default)]
pub squash: bool, pub squash: bool,
#[builder(into)] pub containerfile: &'scope Path,
pub containerfile: Cow<'scope, Path>,
pub platform: Option<Platform>, pub platform: Option<Platform>,
@ -27,18 +25,14 @@ pub struct BuildOpts<'scope> {
#[builder(default)] #[builder(default)]
pub privileged: bool, pub privileged: bool,
#[builder(into)]
pub cache_from: Option<&'scope Reference>, pub cache_from: Option<&'scope Reference>,
#[builder(into)]
pub cache_to: Option<&'scope Reference>, pub cache_to: Option<&'scope Reference>,
#[builder(default)] #[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 struct TagOpts<'scope> {
pub src_image: &'scope Reference, pub src_image: &'scope Reference,
pub dest_image: &'scope Reference, pub dest_image: &'scope Reference,
@ -47,7 +41,7 @@ pub struct TagOpts<'scope> {
pub privileged: bool, pub privileged: bool,
} }
#[derive(Debug, Clone, Builder)] #[derive(Debug, Clone, Copy, Builder)]
pub struct PushOpts<'scope> { pub struct PushOpts<'scope> {
pub image: &'scope Reference, pub image: &'scope Reference,
pub compression_type: Option<CompressionType>, pub compression_type: Option<CompressionType>,
@ -56,7 +50,7 @@ pub struct PushOpts<'scope> {
pub privileged: bool, pub privileged: bool,
} }
#[derive(Debug, Clone, Builder)] #[derive(Debug, Clone, Copy, Builder)]
pub struct PruneOpts { pub struct PruneOpts {
pub all: bool, pub all: bool,
pub volumes: bool, pub volumes: bool,
@ -64,19 +58,17 @@ pub struct PruneOpts {
/// Options for building, tagging, and pusing images. /// Options for building, tagging, and pusing images.
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Builder)] #[derive(Debug, Clone, Copy, Builder)]
pub struct BuildTagPushOpts<'scope> { pub struct BuildTagPushOpts<'scope> {
/// The base image name. /// The base image name.
#[builder(into)] pub image: &'scope ImageRef<'scope>,
pub image: ImageRef<'scope>,
/// The path to the Containerfile to build. /// The path to the Containerfile to build.
#[builder(into)] pub containerfile: &'scope Path,
pub containerfile: Cow<'scope, Path>,
/// The list of tags for the image being built. /// The list of tags for the image being built.
#[builder(default, into)] #[builder(default)]
pub tags: Vec<Cow<'scope, str>>, pub tags: &'scope [String],
/// Enable pushing the image. /// Enable pushing the image.
#[builder(default)] #[builder(default)]
@ -115,5 +107,5 @@ pub struct BuildTagPushOpts<'scope> {
/// Secrets to mount /// Secrets to mount
#[builder(default)] #[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 bon::Builder;
use oci_distribution::Reference; use oci_distribution::Reference;
use crate::drivers::types::Platform; use crate::drivers::types::Platform;
#[derive(Debug, Clone, Builder)] #[derive(Debug, Clone, Copy, Builder)]
pub struct GenerateTagsOpts<'scope> { pub struct GenerateTagsOpts<'scope> {
pub oci_ref: &'scope Reference, pub oci_ref: &'scope Reference,
#[builder(into)] #[builder(into)]
pub alt_tags: Option<Vec<Cow<'scope, str>>>, pub alt_tags: Option<&'scope [String]>,
pub platform: Option<Platform>, pub platform: Option<Platform>,
} }
#[derive(Debug, Clone, Builder)] #[derive(Debug, Clone, Copy, Builder)]
pub struct GenerateImageNameOpts<'scope> { pub struct GenerateImageNameOpts<'scope> {
#[builder(into)] pub name: &'scope str,
pub name: Cow<'scope, str>, pub registry: Option<&'scope str>,
pub registry_namespace: Option<&'scope str>,
#[builder(into)]
pub registry: Option<Cow<'scope, str>>,
#[builder(into)]
pub registry_namespace: Option<Cow<'scope, str>>,
} }

View file

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

View file

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

View file

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

View file

@ -1,13 +1,14 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
ops::Not,
path::Path, path::Path,
process::{Command, ExitStatus}, process::{Command, ExitStatus},
time::Duration, time::Duration,
}; };
use blue_build_utils::{ use blue_build_utils::{
constants::SUDO_ASKPASS, credentials::Credentials, has_env_var, running_as_root, constants::USER, credentials::Credentials, get_env_var, secret::SecretArgs, semver::Version,
secret::SecretArgs, semver::Version, sudo_cmd,
}; };
use cached::proc_macro::cached; use cached::proc_macro::cached;
use colored::Colorize; use colored::Colorize;
@ -21,7 +22,10 @@ use tempfile::TempDir;
use super::{ use super::{
ContainerMountDriver, RechunkDriver, ContainerMountDriver, RechunkDriver,
opts::{CreateContainerOpts, RemoveContainerOpts, RemoveImageOpts}, opts::{
ContainerOpts, CreateContainerOpts, PruneOpts, RemoveContainerOpts, RemoveImageOpts,
VolumeOpts,
},
types::{ContainerId, MountId}, types::{ContainerId, MountId},
}; };
use crate::{ use crate::{
@ -108,6 +112,42 @@ struct PodmanVersionJson {
#[derive(Debug)] #[derive(Debug)]
pub struct PodmanDriver; 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 { impl DriverVersion for PodmanDriver {
// First podman version to use buildah v1.24 // First podman version to use buildah v1.24
// https://github.com/containers/podman/blob/main/RELEASE_NOTES.md#400 // https://github.com/containers/podman/blob/main/RELEASE_NOTES.md#400
@ -134,29 +174,17 @@ impl DriverVersion for PodmanDriver {
} }
impl BuildDriver for PodmanDriver { impl BuildDriver for PodmanDriver {
fn build(opts: &BuildOpts) -> Result<()> { fn build(opts: BuildOpts) -> Result<()> {
trace!("PodmanDriver::build({opts:#?})"); trace!("PodmanDriver::build({opts:#?})");
let temp_dir = TempDir::new() let temp_dir = TempDir::new()
.into_diagnostic() .into_diagnostic()
.wrap_err("Failed to create temporary directory for secrets")?; .wrap_err("Failed to create temporary directory for secrets")?;
let use_sudo = opts.privileged && !running_as_root(); let command = sudo_cmd!(
let command = cmd!( prompt = SUDO_PROMPT,
if use_sudo { sudo_check = opts.privileged,
"sudo" "podman",
} else {
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => [
"--preserve-env",
"podman",
],
"build", "build",
if let Some(platform) = opts.platform => [ if let Some(platform) = opts.platform => [
"--platform", "--platform",
@ -182,7 +210,7 @@ impl BuildDriver for PodmanDriver {
if opts.host_network => "--net=host", if opts.host_network => "--net=host",
format!("--layers={}", !opts.squash), format!("--layers={}", !opts.squash),
"-f", "-f",
&*opts.containerfile, opts.containerfile,
"-t", "-t",
opts.image.to_string(), opts.image.to_string(),
for opts.secrets.args(&temp_dir)?, for opts.secrets.args(&temp_dir)?,
@ -203,24 +231,15 @@ impl BuildDriver for PodmanDriver {
Ok(()) Ok(())
} }
fn tag(opts: &TagOpts) -> Result<()> { fn tag(opts: TagOpts) -> Result<()> {
trace!("PodmanDriver::tag({opts:#?})"); trace!("PodmanDriver::tag({opts:#?})");
let dest_image_str = opts.dest_image.to_string(); let dest_image_str = opts.dest_image.to_string();
let use_sudo = opts.privileged && !running_as_root(); let mut command = sudo_cmd!(
let mut command = cmd!( prompt = SUDO_PROMPT,
if use_sudo { sudo_check = opts.privileged,
"sudo" "podman",
} else {
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
"tag", "tag",
opts.src_image.to_string(), opts.src_image.to_string(),
&dest_image_str &dest_image_str
@ -237,24 +256,15 @@ impl BuildDriver for PodmanDriver {
Ok(()) Ok(())
} }
fn push(opts: &PushOpts) -> Result<()> { fn push(opts: PushOpts) -> Result<()> {
trace!("PodmanDriver::push({opts:#?})"); trace!("PodmanDriver::push({opts:#?})");
let image_str = opts.image.to_string(); let image_str = opts.image.to_string();
let use_sudo = opts.privileged && !running_as_root(); let command = sudo_cmd!(
let command = cmd!( prompt = SUDO_PROMPT,
if use_sudo { sudo_check = opts.privileged,
"sudo" "podman",
} else {
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
"push", "push",
format!( format!(
"--compression-format={}", "--compression-format={}",
@ -312,7 +322,7 @@ impl BuildDriver for PodmanDriver {
Ok(()) Ok(())
} }
fn prune(opts: &super::opts::PruneOpts) -> Result<()> { fn prune(opts: PruneOpts) -> Result<()> {
trace!("PodmanDriver::prune({opts:?})"); trace!("PodmanDriver::prune({opts:?})");
let status = { let status = {
@ -339,7 +349,7 @@ impl BuildDriver for PodmanDriver {
} }
impl InspectDriver for PodmanDriver { impl InspectDriver for PodmanDriver {
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> { fn get_metadata(opts: GetMetadataOpts) -> Result<ImageMetadata> {
get_metadata_cache(opts) get_metadata_cache(opts)
} }
} }
@ -350,7 +360,7 @@ impl InspectDriver for PodmanDriver {
convert = r#"{ format!("{}-{:?}", opts.image, opts.platform)}"#, convert = r#"{ format!("{}-{:?}", opts.image, opts.platform)}"#,
sync_writes = "by_key" 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:#?})"); trace!("PodmanDriver::get_metadata({opts:#?})");
let image_str = opts.image.to_string(); let image_str = opts.image.to_string();
@ -409,21 +419,12 @@ fn get_metadata_cache(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
} }
impl ContainerMountDriver for PodmanDriver { impl ContainerMountDriver for PodmanDriver {
fn mount_container(opts: &super::opts::ContainerOpts) -> Result<MountId> { fn mount_container(opts: ContainerOpts) -> Result<MountId> {
let use_sudo = opts.privileged && !running_as_root();
let output = { let output = {
let c = cmd!( let c = sudo_cmd!(
if use_sudo { prompt = SUDO_PROMPT,
"sudo" sudo_check = opts.privileged,
} else { "podman",
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
"mount", "mount",
opts.container_id, opts.container_id,
); );
@ -442,21 +443,12 @@ impl ContainerMountDriver for PodmanDriver {
)) ))
} }
fn unmount_container(opts: &super::opts::ContainerOpts) -> Result<()> { fn unmount_container(opts: ContainerOpts) -> Result<()> {
let use_sudo = opts.privileged && !running_as_root();
let output = { let output = {
let c = cmd!( let c = sudo_cmd!(
if use_sudo { prompt = SUDO_PROMPT,
"sudo" sudo_check = opts.privileged,
} else { "podman",
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
"unmount", "unmount",
opts.container_id opts.container_id
); );
@ -473,24 +465,15 @@ impl ContainerMountDriver for PodmanDriver {
Ok(()) Ok(())
} }
fn remove_volume(opts: &super::opts::VolumeOpts) -> Result<()> { fn remove_volume(opts: VolumeOpts) -> Result<()> {
let use_sudo = opts.privileged && !running_as_root();
let output = { let output = {
let c = cmd!( let c = sudo_cmd!(
if use_sudo { prompt = SUDO_PROMPT,
"sudo" sudo_check = opts.privileged,
} else { "podman",
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
"volume", "volume",
"rm", "rm",
&*opts.volume_id opts.volume_id
); );
trace!("{c:?}"); trace!("{c:?}");
c c
@ -509,7 +492,7 @@ impl ContainerMountDriver for PodmanDriver {
impl RechunkDriver for PodmanDriver {} impl RechunkDriver for PodmanDriver {}
impl RunDriver for PodmanDriver { impl RunDriver for PodmanDriver {
fn run(opts: &RunOpts) -> Result<ExitStatus> { fn run(opts: RunOpts) -> Result<ExitStatus> {
trace!("PodmanDriver::run({opts:#?})"); trace!("PodmanDriver::run({opts:#?})");
let cid_path = TempDir::new().into_diagnostic()?; let cid_path = TempDir::new().into_diagnostic()?;
@ -520,7 +503,7 @@ impl RunDriver for PodmanDriver {
add_cid(&cid); add_cid(&cid);
let status = podman_run(opts, &cid_file) let status = podman_run(opts, &cid_file)
.build_status(&*opts.image, "Running container") .build_status(opts.image, "Running container")
.into_diagnostic()?; .into_diagnostic()?;
remove_cid(&cid); remove_cid(&cid);
@ -528,7 +511,7 @@ impl RunDriver for PodmanDriver {
Ok(status) 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:#?})"); trace!("PodmanDriver::run_output({opts:#?})");
let cid_path = TempDir::new().into_diagnostic()?; let cid_path = TempDir::new().into_diagnostic()?;
@ -545,23 +528,14 @@ impl RunDriver for PodmanDriver {
Ok(output) Ok(output)
} }
fn create_container(opts: &CreateContainerOpts) -> Result<ContainerId> { fn create_container(opts: CreateContainerOpts) -> Result<ContainerId> {
trace!("PodmanDriver::create_container({opts:?})"); trace!("PodmanDriver::create_container({opts:?})");
let use_sudo = opts.privileged && !running_as_root();
let output = { let output = {
let c = cmd!( let c = sudo_cmd!(
if use_sudo { prompt = SUDO_PROMPT,
"sudo" sudo_check = opts.privileged,
} else { "podman",
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
"create", "create",
opts.image.to_string(), opts.image.to_string(),
"bash" "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:?})"); trace!("PodmanDriver::remove_container({opts:?})");
let use_sudo = opts.privileged && !running_as_root();
let output = { let output = {
let c = cmd!( let c = sudo_cmd!(
if use_sudo { prompt = SUDO_PROMPT,
"sudo" sudo_check = opts.privileged,
} else { "podman",
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
"rm", "rm",
opts.container_id, opts.container_id,
); );
@ -614,23 +579,14 @@ impl RunDriver for PodmanDriver {
Ok(()) Ok(())
} }
fn remove_image(opts: &RemoveImageOpts) -> Result<()> { fn remove_image(opts: RemoveImageOpts) -> Result<()> {
trace!("PodmanDriver::remove_image({opts:?})"); trace!("PodmanDriver::remove_image({opts:?})");
let use_sudo = opts.privileged && !running_as_root();
let output = { let output = {
let c = cmd!( let c = sudo_cmd!(
if use_sudo { prompt = SUDO_PROMPT,
"sudo" sudo_check = opts.privileged,
} else { "podman",
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
"rmi", "rmi",
opts.image.to_string() opts.image.to_string()
); );
@ -656,20 +612,11 @@ impl RunDriver for PodmanDriver {
trace!("PodmanDriver::list_images({privileged})"); trace!("PodmanDriver::list_images({privileged})");
let use_sudo = privileged && !running_as_root();
let output = { let output = {
let c = cmd!( let c = sudo_cmd!(
if use_sudo { prompt = SUDO_PROMPT,
"sudo" sudo_check = privileged,
} else { "podman",
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
"images", "images",
"--format", "--format",
"json" "json"
@ -698,20 +645,11 @@ impl RunDriver for PodmanDriver {
} }
} }
fn podman_run(opts: &RunOpts, cid_file: &Path) -> Command { fn podman_run(opts: RunOpts, cid_file: &Path) -> Command {
let use_sudo = opts.privileged && !running_as_root(); let command = sudo_cmd!(
let command = cmd!( prompt = SUDO_PROMPT,
if use_sudo { sudo_check = opts.privileged,
"sudo" "podman",
} else {
"podman"
},
if use_sudo && has_env_var(SUDO_ASKPASS) => [
"-A",
"-p",
SUDO_PROMPT,
],
if use_sudo => "podman",
"run", "run",
format!("--cidfile={}", cid_file.display()), format!("--cidfile={}", cid_file.display()),
if opts.privileged => [ if opts.privileged => [
@ -729,7 +667,7 @@ fn podman_run(opts: &RunOpts, cid_file: &Path) -> Command {
"--env", "--env",
format!("{key}={value}"), format!("{key}={value}"),
], ],
&*opts.image, opts.image,
for arg in opts.args.iter() => &**arg, for arg in opts.args.iter() => &**arg,
); );
trace!("{command:?}"); 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; pub struct SigstoreDriver;
impl SigningDriver for 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 path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
let priv_key_path = path.join(COSIGN_PRIV_PATH); let priv_key_path = path.join(COSIGN_PRIV_PATH);
let pub_key_path = path.join(COSIGN_PUB_PATH); let pub_key_path = path.join(COSIGN_PUB_PATH);
@ -70,7 +70,7 @@ impl SigningDriver for SigstoreDriver {
Ok(()) Ok(())
} }
fn check_signing_files(opts: &CheckKeyPairOpts) -> miette::Result<()> { fn check_signing_files(opts: CheckKeyPairOpts) -> miette::Result<()> {
trace!("SigstoreDriver::check_signing_files({opts:?})"); trace!("SigstoreDriver::check_signing_files({opts:?})");
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir); 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:?})"); trace!("SigstoreDriver::sign({opts:?})");
if opts.image.digest().is_none() { if opts.image.digest().is_none() {
@ -176,7 +176,7 @@ impl SigningDriver for SigstoreDriver {
Ok(()) Ok(())
} }
fn verify(opts: &VerifyOpts) -> miette::Result<()> { fn verify(opts: VerifyOpts) -> miette::Result<()> {
let mut client = ClientBuilder::default().build().into_diagnostic()?; let mut client = ClientBuilder::default().build().into_diagnostic()?;
let image_digest: OciReference = opts.image.to_string().parse().into_diagnostic()?; let image_digest: OciReference = opts.image.to_string().parse().into_diagnostic()?;
@ -253,9 +253,10 @@ mod test {
fn generate_key_pair() { fn generate_key_pair() {
let tempdir = TempDir::new().unwrap(); let tempdir = TempDir::new().unwrap();
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build(); SigstoreDriver::generate_key_pair(
GenerateKeyPairOpts::builder().dir(tempdir.path()).build(),
SigstoreDriver::generate_key_pair(&gen_opts).unwrap(); )
.unwrap();
eprintln!( eprintln!(
"Private key:\n{}", "Private key:\n{}",
@ -266,27 +267,27 @@ mod test {
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap() fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
); );
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build(); SigstoreDriver::check_signing_files(
CheckKeyPairOpts::builder().dir(tempdir.path()).build(),
SigstoreDriver::check_signing_files(&check_opts).unwrap(); )
.unwrap();
} }
#[test] #[test]
fn check_key_pairs() { fn check_key_pairs() {
let path = Path::new("../test-files/keys"); let path = Path::new("../test-files/keys");
let opts = CheckKeyPairOpts::builder().dir(path).build(); SigstoreDriver::check_signing_files(CheckKeyPairOpts::builder().dir(path).build()).unwrap();
SigstoreDriver::check_signing_files(&opts).unwrap();
} }
#[test] #[test]
fn compatibility() { fn compatibility() {
let tempdir = TempDir::new().unwrap(); let tempdir = TempDir::new().unwrap();
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build(); SigstoreDriver::generate_key_pair(
GenerateKeyPairOpts::builder().dir(tempdir.path()).build(),
SigstoreDriver::generate_key_pair(&gen_opts).unwrap(); )
.unwrap();
eprintln!( eprintln!(
"Private key:\n{}", "Private key:\n{}",
@ -297,8 +298,7 @@ mod test {
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap() fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
); );
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build(); CosignDriver::check_signing_files(CheckKeyPairOpts::builder().dir(tempdir.path()).build())
.unwrap();
CosignDriver::check_signing_files(&check_opts).unwrap();
} }
} }

View file

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

View file

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

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

View file

@ -44,6 +44,35 @@ color_string() {
fi 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 # Parse OS version and export it
export OS_VERSION="$(awk -F= '/^VERSION_ID=/ {gsub(/"/, "", $2); print $2}' /usr/lib/os-release)" export OS_VERSION="$(awk -F= '/^VERSION_ID=/ {gsub(/"/, "", $2); print $2}' /usr/lib/os-release)"
export OS_ARCH="$(uname -m)" export OS_ARCH="$(uname -m)"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,29 +1,19 @@
use std::{ use std::path::PathBuf;
path::{Path, PathBuf},
time::Duration,
};
use blue_build_process_management::{ use blue_build_process_management::drivers::{
drivers::{Driver, DriverArgs}, BootDriver, BuildDriver, CiDriver, Driver, DriverArgs, PodmanDriver, RunDriver,
logging::CommandLogging, opts::{BuildOpts, GenerateImageNameOpts, RemoveImageOpts, SwitchOpts},
types::ImageRef,
}; };
use blue_build_recipe::Recipe; use blue_build_recipe::Recipe;
use blue_build_utils::{ use blue_build_utils::constants::BB_SKIP_VALIDATION;
constants::{
ARCHIVE_SUFFIX, BB_SKIP_VALIDATION, LOCAL_BUILD, OCI_ARCHIVE, OSTREE_UNVERIFIED_IMAGE,
SUDO_ASKPASS,
},
has_env_var, running_as_root,
};
use bon::Builder; use bon::Builder;
use clap::Args; use clap::Args;
use comlexr::cmd; use log::trace;
use indicatif::ProgressBar;
use log::{debug, trace};
use miette::{IntoDiagnostic, Result, bail}; use miette::{IntoDiagnostic, Result, bail};
use tempfile::TempDir; use tempfile::TempDir;
use crate::{commands::build::BuildCommand, rpm_ostree_status::RpmOstreeStatus}; use crate::commands::generate::GenerateCommand;
use super::BlueBuildCommand; use super::BlueBuildCommand;
@ -60,238 +50,59 @@ impl BlueBuildCommand for SwitchCommand {
Driver::init(self.drivers); Driver::init(self.drivers);
let status = RpmOstreeStatus::try_new()?; let status = Driver::status()?;
trace!("{status:?}");
if status.transaction_in_progress() { if status.transaction_in_progress() {
bail!("There is a transaction in progress. Please cancel it using `rpm-ostree cancel`"); 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 { let tempdir = if let Some(ref dir) = self.tempdir {
TempDir::new_in(dir).into_diagnostic()? TempDir::new_in(dir).into_diagnostic()?
} else { } else {
TempDir::new().into_diagnostic()? TempDir::new().into_diagnostic()?
}; };
trace!("{tempdir:?}"); let containerfile = tempdir
.path()
.join(blue_build_utils::generate_containerfile_path(&self.recipe)?);
BuildCommand::builder() GenerateCommand::builder()
.recipe([self.recipe.clone()]) .output(&containerfile)
.archive(tempdir.path()) .recipe(&self.recipe)
.maybe_tempdir(self.tempdir.clone())
.skip_validation(self.skip_validation)
.build() .build()
.try_run()?; .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)?; if status
let image_file_name = format!( .booted_image()
"{}.{ARCHIVE_SUFFIX}", .is_some_and(|booted| booted == image_name)
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)
{ {
let command = cmd!("rpm-ostree", "upgrade", if self.reboot => "--reboot"); Driver::upgrade(
SwitchOpts::builder()
trace!("{command:?}"); .image(&image_name)
command .reboot(self.reboot)
.build(),
)
} else { } else {
let image_ref = format!( Driver::switch(
"{OSTREE_UNVERIFIED_IMAGE}:{OCI_ARCHIVE}:{path}", SwitchOpts::builder()
path = archive_path.display() .image(&image_name)
); .reboot(self.reboot)
.build(),
let command = cmd!( )
"rpm-ostree",
"rebase",
&image_ref,
if self.reboot => "--reboot",
);
trace!("{command:?}");
command
} }
.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); shadow_rs::shadow!(shadow);
pub mod commands; 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"] } askama = { version = "0.14", features = ["serde_json"] }
blue-build-recipe = { version = "=0.9.22", path = "../recipe" } blue-build-recipe = { version = "=0.9.22", path = "../recipe" }
blue-build-utils = { version = "=0.9.22", path = "../utils" } blue-build-utils = { version = "=0.9.22", path = "../utils" }
oci-distribution.workspace = true
chrono.workspace = true chrono.workspace = true
log.workspace = true log.workspace = true

View file

@ -9,28 +9,29 @@ use bon::Builder;
use chrono::Utc; use chrono::Utc;
use colored::control::ShouldColorize; use colored::control::ShouldColorize;
use log::{debug, error, trace, warn}; use log::{debug, error, trace, warn};
use oci_distribution::Reference;
use uuid::Uuid; use uuid::Uuid;
pub use askama::Template; pub use askama::Template;
#[derive(Debug, Clone, Template, Builder)] #[derive(Debug, Clone, Template, Builder)]
#[template(path = "Containerfile.j2", escape = "none", whitespace = "minimize")] #[template(path = "Containerfile.j2", escape = "none", whitespace = "minimize")]
#[builder(on(Cow<'_, str>, into))]
pub struct ContainerFileTemplate<'a> { pub struct ContainerFileTemplate<'a> {
#[builder(into)] #[builder(into)]
recipe: &'a Recipe<'a>, recipe: &'a Recipe<'a>,
recipe_path: &'a Path,
#[builder(into)]
recipe_path: Cow<'a, Path>,
#[builder(into)] #[builder(into)]
build_id: Uuid, build_id: Uuid,
os_version: u64, os_version: u64,
registry: Cow<'a, str>, registry: &'a str,
build_scripts_image: Cow<'a, str>, build_scripts_image: &'a Reference,
repo: Cow<'a, str>, repo: &'a str,
base_digest: Cow<'a, str>, base_digest: &'a str,
nushell_version: Option<&'a MaybeVersion>, nushell_version: Option<&'a MaybeVersion>,
#[builder(default)]
build_features: &'a [String],
} }
impl ContainerFileTemplate<'_> { impl ContainerFileTemplate<'_> {
@ -47,6 +48,15 @@ impl ContainerFileTemplate<'_> {
Some(MaybeVersion::Version(version)) => version.to_string(), 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)] #[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 RECIPE={{ recipe_path.display() }}
ARG IMAGE_REGISTRY={{ registry }} ARG IMAGE_REGISTRY={{ registry }}
ARG BB_BUILD_FEATURES="{{ get_features() }}"
{%- if self::config_dir_exists() && !self::files_dir_exists() %} {%- if self::config_dir_exists() && !self::files_dir_exists() %}
ARG CONFIG_DIRECTORY="/tmp/config" 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 OSTREE_UNVERIFIED_IMAGE: &str = "ostree-unverified-image";
pub const SKOPEO_IMAGE: &str = "quay.io/skopeo/stable:latest"; 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 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_SHELL: &str = "<unknown shell>";
pub const UNKNOWN_VERSION: &str = "<unknown version>"; pub const UNKNOWN_VERSION: &str = "<unknown version>";
pub const UNKNOWN_TERMINAL: &str = "<unknown terminal>"; pub const UNKNOWN_TERMINAL: &str = "<unknown terminal>";
@ -110,3 +111,4 @@ pub const STAGE_SCHEMA: &str = concat!(JSON_SCHEMA, "/stage-v1.json");
// Messages // Messages
pub const BUG_REPORT_WARNING_MESSAGE: &str = pub const BUG_REPORT_WARNING_MESSAGE: &str =
"Please copy the above report and open an issue manually."; "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::{ use std::{
collections::HashSet,
fs, fs,
hash::{DefaultHasher, Hash, Hasher}, hash::{DefaultHasher, Hash, Hasher},
ops::Not, 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)] #[allow(private_bounds)]
pub trait SecretArgs: private::Private { pub trait SecretArgs: private::Private {
@ -138,7 +137,7 @@ pub trait SecretArgs: private::Private {
fn ssh(&self) -> bool; 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>> { fn args(&self, temp_dir: &TempDir) -> Result<Vec<String>> {
Ok(self Ok(self
.iter() .iter()
@ -173,7 +172,7 @@ impl<H: std::hash::BuildHasher> SecretArgs for HashSet<&Secret, H> {
} }
fn ssh(&self) -> bool { fn ssh(&self) -> bool {
self.contains(&Secret::Ssh) self.contains(&&Secret::Ssh)
} }
} }