refactor: Create SigningDriver and CiDriver (#197)

This also includes a new `login` command. The signing and CI logic is now using the Driver trait system along with a new experimental sigstore signing driver. New static macros have also been created to make implementation management easier for `Command` usage and `Driver` trait implementation calls.

---------

Co-authored-by: xyny <60004820+xynydev@users.noreply.github.com>
This commit is contained in:
Gerald Pinder 2024-08-12 23:52:07 -04:00 committed by GitHub
parent 3ecb0d3d93
commit 8ce83ba7ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
63 changed files with 6468 additions and 2083 deletions

View file

@ -1,34 +0,0 @@
# Add the contents of this file to `config.toml` to enable "fast build" configuration. Please read the notes below.
# NOTE: For maximum performance, build using a nightly compiler
# If you are using rust stable, remove the "-Zshare-generics=y" below.
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = [
"-Clink-arg=-fuse-ld=lld", # Use LLD Linker
# "-Zshare-generics=y", # (Nightly) Make the current crate share its generic instantiations
]
# NOTE: you must install [Mach-O LLD Port](https://lld.llvm.org/MachO/index.html) on mac. you can easily do this by installing llvm which includes lld with the "brew" package manager:
# `brew install llvm`
[target.x86_64-apple-darwin]
rustflags = [
"-Clink-arg=-fuse-ld=/usr/local/opt/llvm/bin/ld64.lld", # Use LLD Linker
# "-Zshare-generics=y", # (Nightly) Make the current crate share its generic instantiations
]
[target.aarch64-apple-darwin]
rustflags = [
"-Clink-arg=-fuse-ld=/opt/homebrew/opt/llvm/bin/ld64.lld", # Use LLD Linker
# "-Zshare-generics=y", # (Nightly) Make the current crate share its generic instantiations
]
[target.x86_64-pc-windows-msvc]
linker = "rust-lld.exe" # Use LLD Linker
# rustflags = ["-Zshare-generics=n"]
# Optional: Uncommenting the following improves compile times, but reduces the amount of debug info to 'line number tables only'
# In most cases the gains are negligible, but if you are on macos and have slow compile times you should see significant gains.
#[profile.dev]
#debug = 1

View file

@ -13,6 +13,42 @@ env:
RUST_LOG_STYLE: always
jobs:
test:
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- uses: earthly/actions-setup@v1
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Run test
id: build
run: |
earthly --ci +test
lint:
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- uses: earthly/actions-setup@v1
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Run lint
id: build
run: |
earthly --ci +lint
arm64-prebuild:
timeout-minutes: 60
runs-on: ubuntu-latest
@ -220,7 +256,7 @@ jobs:
cd integration-tests/test-repo
bluebuild template -vv | tee Containerfile
grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1
bluebuild build --push -vv recipes/recipe.yml recipes/recipe-39.yml
bluebuild build --retry-push -B docker -I docker -S sigstore --push -vv recipes/recipe.yml recipes/recipe-39.yml
docker-build-external-login:
timeout-minutes: 60
@ -275,7 +311,7 @@ jobs:
cd integration-tests/test-repo
bluebuild template -vv | tee Containerfile
grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1
bluebuild build --push -vv recipes/recipe.yml recipes/recipe-39.yml
bluebuild build --retry-push -S sigstore --push -vv recipes/recipe.yml recipes/recipe-39.yml
podman-build:
timeout-minutes: 60
@ -327,10 +363,10 @@ jobs:
cd integration-tests/test-repo
bluebuild template -vv | tee Containerfile
grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1
bluebuild build -B podman --push -vv recipes/recipe.yml recipes/recipe-39.yml
bluebuild build --retry-push -B podman -I podman -S sigstore --push -vv recipes/recipe.yml recipes/recipe-39.yml
buildah-build:
timeout-minutes: 60
timeout-minutes: 15
runs-on: ubuntu-latest
permissions:
contents: read
@ -379,4 +415,4 @@ jobs:
cd integration-tests/test-repo
bluebuild template -vv | tee Containerfile
grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1
bluebuild build -B buildah --push -vv recipes/recipe.yml recipes/recipe-39.yml
bluebuild build --retry-push -B buildah -I podman -S sigstore --squash --push -vv recipes/recipe.yml recipes/recipe-39.yml

View file

@ -16,6 +16,42 @@ env:
RUST_LOG_STYLE: always
jobs:
test:
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- uses: earthly/actions-setup@v1
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Run build
id: build
run: |
earthly --ci +test
lint:
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- uses: earthly/actions-setup@v1
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Run build
id: build
run: |
earthly --ci +test
arm64-prebuild:
timeout-minutes: 60
runs-on: ubuntu-latest
@ -218,7 +254,7 @@ jobs:
cd integration-tests/test-repo
bluebuild template -vv | tee Containerfile
grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1
bluebuild build --push -vv recipes/recipe.yml recipes/recipe-39.yml
bluebuild build --retry-push -B docker -I docker -S sigstore --push -vv recipes/recipe.yml recipes/recipe-39.yml
docker-build-external-login:
timeout-minutes: 60
@ -273,7 +309,7 @@ jobs:
cd integration-tests/test-repo
bluebuild template -vv | tee Containerfile
grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1
bluebuild build --push -vv recipes/recipe.yml recipes/recipe-39.yml
bluebuild build --retry-push -S sigstore --push -vv recipes/recipe.yml recipes/recipe-39.yml
podman-build:
timeout-minutes: 60
@ -325,7 +361,7 @@ jobs:
cd integration-tests/test-repo
bluebuild template -vv | tee Containerfile
grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1
bluebuild build -B podman --push -vv recipes/recipe.yml recipes/recipe-39.yml
bluebuild build --retry-push -B podman -I podman -S sigstore --push -vv recipes/recipe.yml recipes/recipe-39.yml
buildah-build:
timeout-minutes: 60
@ -377,4 +413,4 @@ jobs:
cd integration-tests/test-repo
bluebuild template -vv | tee Containerfile
grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1
bluebuild build -B buildah --push -vv recipes/recipe.yml recipes/recipe-39.yml
bluebuild build --retry-push -B buildah -I podman -S sigstore --squash --push -vv recipes/recipe.yml recipes/recipe-39.yml

1
.gitignore vendored
View file

@ -5,6 +5,7 @@ result*
.direnv/
cosign.key
!test-files/keys/cosign.key
# Local testing for bluebuild recipe files
/config/*

View file

@ -1,5 +1,5 @@
[hooks]
pre-commit = "cargo fmt --check && cargo test && cargo test --all-features && cargo clippy -- -D warnings && cargo clippy --all-features -- -D warnings"
pre-push = "cargo fmt --check && cargo test --workspace && cargo test --workspace --all-features && cargo clippy -- -D warnings && cargo clippy --all-features -- -D warnings"
[logging]
verbose = true

3036
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
[workspace]
members = ["utils", "recipe", "template"]
members = ["utils", "recipe", "template", "process"]
[workspace.package]
description = "A CLI tool built for creating Containerfile templates for ostree based atomic distros"
@ -13,30 +13,29 @@ version = "0.8.12"
chrono = "0.4"
clap = "4"
colored = "2"
format_serde_error = "0.3"
indexmap = { version = "2", features = ["serde"] }
indicatif = { version = "0.17", features = ["improved_unicode"] }
indicatif-log-bridge = "0.2"
log = "0.4"
miette = "7"
once_cell = "1"
rstest = "0.18"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
tempdir = "0.3"
typed-builder = "0.18"
users = "0.11"
uuid = { version = "1", features = ["v4"] }
[workspace.lints.rust]
unsafe_code = "forbid"
[workspace.lints.clippy]
correctness = "warn"
suspicious = "warn"
perf = "warn"
style = "warn"
nursery = "warn"
pedantic = "warn"
correctness = "deny"
suspicious = "deny"
perf = "deny"
style = "deny"
nursery = "deny"
pedantic = "deny"
module_name_repetitions = { level = "allow", priority = 1 }
[package]
@ -59,34 +58,28 @@ pre-release-replacements = [
blue-build-recipe = { version = "=0.8.12", path = "./recipe" }
blue-build-template = { version = "=0.8.12", path = "./template" }
blue-build-utils = { version = "=0.8.12", path = "./utils" }
blue-build-process-management = { version = "=0.8.12", path = "./process" }
clap-verbosity-flag = "2"
clap_complete = "4"
clap_complete_nushell = "4"
fuzzy-matcher = "0.3"
lenient_semver = "0.4"
open = "5"
os_info = "3"
rayon = { version = "1.10.0", optional = true }
requestty = { version = "0.5", features = ["macros", "termion"] }
semver = { version = "1", features = ["serde"] }
shadow-rs = "0.26"
urlencoding = "2"
users = "0.11"
chrono.workspace = true
clap = { workspace = true, features = ["derive", "cargo", "unicode", "env"] }
colored.workspace = true
indexmap.workspace = true
indicatif.workspace = true
log.workspace = true
miette = { workspace = true, features = ["fancy"] }
once_cell.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
tempdir.workspace = true
typed-builder.workspace = true
uuid.workspace = true
users.workspace = true
[features]
default = []
@ -94,6 +87,8 @@ stages = ["blue-build-recipe/stages"]
copy = ["blue-build-recipe/copy"]
multi-recipe = ["rayon", "indicatif/rayon"]
switch = []
sigstore = ["blue-build-process-management/sigstore"]
login = []
[dev-dependencies]
rusty-hook = "0.11"
@ -107,5 +102,10 @@ workspace = true
[profile.release]
lto = true
codegen-units = 1
strip = true
strip = "none"
debug = false
panic = "abort"
[patch.crates-io]
# sigstore = { path = "../../sigstore-rs/" }
sigstore = { git = "https://github.com/gmpinder/sigstore-rs.git", rev = "3a804bff" }

View file

@ -16,7 +16,6 @@ build:
WAIT
BUILD --platform=linux/amd64 --platform=linux/arm64 +build-scripts
END
BUILD +run-checks
BUILD --platform=linux/amd64 --platform=linux/arm64 +build-images
run-checks:
@ -34,15 +33,19 @@ prebuild:
lint:
FROM +common
DO rust+CARGO --args="clippy -- -D warnings"
DO rust+CARGO --args="clippy --all-features -- -D warnings"
DO rust+CARGO --args="clippy --no-default-features -- -D warnings"
RUN cargo fmt --check
DO rust+CARGO --args="clippy"
DO rust+CARGO --args="clippy --all-features"
DO rust+CARGO --args="clippy --no-default-features"
test:
FROM +common
DO rust+CARGO --args="test -- --show-output"
DO rust+CARGO --args="test --all-features -- --show-output"
DO rust+CARGO --args="test --no-default-features -- --show-output"
COPY --dir test-files/ integration-tests/ /app
COPY +cosign/cosign /usr/bin/cosign
DO rust+CARGO --args="test --workspace -- --show-output"
DO rust+CARGO --args="test --workspace --all-features -- --show-output"
DO rust+CARGO --args="test --workspace --no-default-features -- --show-output"
install:
FROM +common
@ -64,7 +67,7 @@ common:
FROM --platform=native ghcr.io/blue-build/earthly-lib/cargo-builder
WORKDIR /app
COPY --keep-ts --dir src/ template/ recipe/ utils/ /app
COPY --keep-ts --dir src/ template/ recipe/ utils/ process/ /app
COPY --keep-ts Cargo.* /app
COPY --keep-ts *.md /app
COPY --keep-ts LICENSE /app

View file

@ -1,6 +1,8 @@
VERSION 0.8
PROJECT blue-build/cli
IMPORT github.com/earthly/lib/utils/dind AS dind
all:
BUILD +test-image
BUILD +test-legacy-image
@ -31,37 +33,51 @@ build-template:
template-containerfile:
FROM +test-base
RUN bluebuild -vv generate recipes/recipe.yml | tee Containerfile
RUN bluebuild -v generate recipes/recipe.yml | tee Containerfile
SAVE ARTIFACT /test
template-legacy-containerfile:
FROM +legacy-base
RUN bluebuild -vv template config/recipe.yml | tee Containerfile
RUN bluebuild -v template config/recipe.yml | tee Containerfile
SAVE ARTIFACT /test
build:
FROM +test-base
RUN bluebuild -vv build recipes/recipe.yml
RUN bluebuild -v build recipes/recipe.yml
build-full:
FROM +test-base --MOCK="false"
DO dind+INSTALL
ENV BB_USERNAME=gmpinder
ENV BB_REGISTRY=ghcr.io
ENV BB_REGISTRY_NAMESPACE=blue-build
WITH DOCKER
RUN --secret BB_PASSWORD=github/registry bluebuild build --push -S sigstore -vv recipes/recipe.yml
END
rebase:
FROM +legacy-base
RUN bluebuild -vv rebase config/recipe.yml
RUN bluebuild -v rebase config/recipe.yml
upgrade:
FROM +legacy-base
RUN mkdir -p /etc/bluebuild && touch $BB_TEST_LOCAL_IMAGE
RUN bluebuild -vv upgrade config/recipe.yml
RUN bluebuild -v upgrade config/recipe.yml
switch:
FROM +test-base
RUN mkdir -p /etc/bluebuild && touch $BB_TEST_LOCAL_IMAGE
RUN bluebuild -vv switch recipes/recipe.yml
RUN bluebuild -v switch recipes/recipe.yml
legacy-base:
FROM ../+blue-build-cli-alpine
@ -84,7 +100,10 @@ test-base:
ENV BB_TEST_LOCAL_IMAGE=/etc/bluebuild/cli_test.tar.gz
ENV CLICOLOR_FORCE=1
COPY ./mock-scripts/ /usr/bin/
ARG MOCK="true"
IF [ "$MOCK" = "true" ]
COPY ./mock-scripts/ /usr/bin/
END
WORKDIR /test
COPY ./test-repo /test
@ -94,7 +113,9 @@ test-base:
GEN_KEYPAIR:
FUNCTION
# Setup a cosign key pair
RUN echo -n "\n\n" | cosign generate-key-pair
ENV COSIGN_PASSWORD=""
ENV COSIGN_YES="true"
RUN cosign generate-key-pair
ENV COSIGN_PRIVATE_KEY=$(cat cosign.key)
RUN rm cosign.key

View file

@ -35,11 +35,11 @@ test-all-features:
# Run clippy
lint:
cargo clippy -- -D warnings
cargo clippy
# Run clippy for all features
lint-all-features:
cargo clippy --all-features -- -D warnings
cargo clippy --all-features
# Watch the files and run cargo check on changes
watch:
@ -63,14 +63,17 @@ watch-test-all-features:
# Run lint anytime a file is changed
watch-lint:
cargo watch -c -x 'clippy -- -D warnings'
cargo watch -c -x 'clippy'
# Run all feature lint anytime a file is changed
watch-lint-all-features:
cargo watch -c -x 'clippy --all-features -- -D warnings'
cargo watch -c -x 'clippy --all-features'
# Installs cargo tools that help with development
tools:
rustup toolchain install stable
rustup override set stable
rustup component add --toolchain stable rust-analyzer clippy rustfmt
cargo install cargo-watch
# Run cargo release and push the tag separately

50
process/Cargo.toml Normal file
View file

@ -0,0 +1,50 @@
[package]
name = "blue-build-process-management"
description.workspace = true
edition.workspace = true
repository.workspace = true
license.workspace = true
categories.workspace = true
version.workspace = true
[lib]
path = "process.rs"
[dependencies]
anyhow = "1"
blue-build-recipe = { version = "=0.8.12", path = "../recipe" }
blue-build-utils = { version = "=0.8.12", path = "../utils" }
expect-exit = "0.5"
indicatif-log-bridge = "0.2"
lenient_semver = "0.4"
log4rs = { version = "1", features = ["background_rotation"] }
nu-ansi-term = { version = "0.50", features = ["gnu_legacy"] }
nix = { version = "0.29", features = ["signal"] }
once_cell = "1"
os_pipe = { version = "1", features = ["io_safety"] }
rand = "0.8"
semver = { version = "1", features = ["serde"] }
signal-hook = { version = "0.3", features = ["extended-siginfo"] }
sigstore = { version = "0.9", features = ["full-rustls-tls", "cached-client", "sigstore-trust-root", "sign"], default-features = false }
tokio = { version = "1.39.2", features = ["rt", "rt-multi-thread"], optional = true }
zeroize = { version = "1", features = ["aarch64", "derive", "serde"] }
chrono.workspace = true
clap = { workspace = true, features = ["derive", "env"] }
colored.workspace = true
indicatif.workspace = true
indexmap.workspace = true
log.workspace = true
miette.workspace = true
serde.workspace = true
serde_json.workspace = true
tempdir.workspace = true
typed-builder.workspace = true
users.workspace = true
uuid.workspace = true
[lints]
workspace = true
[features]
sigstore = ["dep:tokio"]

408
process/drivers.rs Normal file
View file

@ -0,0 +1,408 @@
//! This module is responsible for managing various strategies
//! to perform actions throughout the program. This hides all
//! the implementation details from the command logic and allows
//! for caching certain long execution tasks like inspecting the
//! labels for an image.
use std::{
collections::{hash_map::Entry, HashMap},
fmt::Debug,
process::{ExitStatus, Output},
sync::{Mutex, RwLock},
};
use blue_build_recipe::Recipe;
use blue_build_utils::constants::IMAGE_VERSION_LABEL;
use clap::Args;
use log::{debug, info, trace};
use miette::{miette, Result};
use once_cell::sync::Lazy;
#[cfg(feature = "sigstore")]
use sigstore_driver::SigstoreDriver;
use typed_builder::TypedBuilder;
use uuid::Uuid;
use self::{
buildah_driver::BuildahDriver,
cosign_driver::CosignDriver,
docker_driver::DockerDriver,
github_driver::GithubDriver,
gitlab_driver::GitlabDriver,
image_metadata::ImageMetadata,
local_driver::LocalDriver,
opts::{
BuildOpts, BuildTagPushOpts, CheckKeyPairOpts, GenerateKeyPairOpts, GetMetadataOpts,
PushOpts, RunOpts, SignOpts, TagOpts, VerifyOpts,
},
podman_driver::PodmanDriver,
skopeo_driver::SkopeoDriver,
types::{
BuildDriverType, CiDriverType, DetermineDriver, InspectDriverType, RunDriverType,
SigningDriverType,
},
};
pub use traits::*;
mod buildah_driver;
mod cosign_driver;
mod docker_driver;
mod functions;
mod github_driver;
mod gitlab_driver;
pub mod image_metadata;
mod local_driver;
pub mod opts;
mod podman_driver;
#[cfg(feature = "sigstore")]
mod sigstore_driver;
mod skopeo_driver;
mod traits;
pub mod types;
static INIT: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
static SELECTED_BUILD_DRIVER: Lazy<RwLock<Option<BuildDriverType>>> =
Lazy::new(|| RwLock::new(None));
static SELECTED_INSPECT_DRIVER: Lazy<RwLock<Option<InspectDriverType>>> =
Lazy::new(|| RwLock::new(None));
static SELECTED_RUN_DRIVER: Lazy<RwLock<Option<RunDriverType>>> = Lazy::new(|| RwLock::new(None));
static SELECTED_SIGNING_DRIVER: Lazy<RwLock<Option<SigningDriverType>>> =
Lazy::new(|| RwLock::new(None));
static SELECTED_CI_DRIVER: Lazy<RwLock<Option<CiDriverType>>> = Lazy::new(|| RwLock::new(None));
/// UUID used to mark the current builds
static BUILD_ID: Lazy<Uuid> = Lazy::new(Uuid::new_v4);
/// The cached os versions
static OS_VERSION: Lazy<Mutex<HashMap<String, u64>>> = Lazy::new(|| Mutex::new(HashMap::new()));
/// Args for selecting the various drivers to use for runtime.
///
/// If the args are left uninitialized, the program will determine
/// the best one available.
#[derive(Default, Clone, Copy, Debug, TypedBuilder, Args)]
pub struct DriverArgs {
/// Select which driver to use to build
/// your image.
#[builder(default)]
#[arg(short = 'B', long)]
build_driver: Option<BuildDriverType>,
/// Select which driver to use to inspect
/// images.
#[builder(default)]
#[arg(short = 'I', long)]
inspect_driver: Option<InspectDriverType>,
/// Select which driver to use to sign
/// images.
#[builder(default)]
#[arg(short = 'S', long)]
signing_driver: Option<SigningDriverType>,
/// Select which driver to use to run
/// containers.
#[builder(default)]
#[arg(short = 'R', long)]
run_driver: Option<RunDriverType>,
}
macro_rules! impl_driver_type {
($cache:ident) => {{
let lock = $cache.read().expect("Should read");
lock.expect("Driver should have initialized build driver")
}};
}
macro_rules! impl_driver_init {
(@) => { };
($init:ident; $($tail:tt)*) => {
{
let mut initialized = $init.lock().expect("Must lock INIT");
if !*initialized {
impl_driver_init!(@ $($tail)*);
*initialized = true;
}
}
};
(@ default => $cache:ident; $($tail:tt)*) => {
{
let mut driver = $cache.write().expect("Should lock");
impl_driver_init!(@ $($tail)*);
*driver = Some(driver.determine_driver());
::log::trace!("Driver set {driver:?}");
drop(driver);
}
};
(@ $driver:expr => $cache:ident; $($tail:tt)*) => {
{
let mut driver = $cache.write().expect("Should lock");
impl_driver_init!(@ $($tail)*);
*driver = Some($driver.determine_driver());
::log::trace!("Driver set {driver:?}");
drop(driver);
}
};
}
pub struct Driver;
impl Driver {
/// Initializes the Strategy with user provided credentials.
///
/// If you want to take advantage of a user's credentials,
/// you will want to run init before trying to use any of
/// the strategies.
///
/// # Panics
/// Will panic if it is unable to initialize drivers.
pub fn init(mut args: DriverArgs) {
trace!("Driver::init()");
impl_driver_init! {
INIT;
args.build_driver => SELECTED_BUILD_DRIVER;
args.inspect_driver => SELECTED_INSPECT_DRIVER;
args.run_driver => SELECTED_RUN_DRIVER;
args.signing_driver => SELECTED_SIGNING_DRIVER;
default => SELECTED_CI_DRIVER;
}
}
/// Gets the current build's UUID
#[must_use]
pub fn get_build_id() -> Uuid {
trace!("Driver::get_build_id()");
*BUILD_ID
}
/// Retrieve the `os_version` for an image.
///
/// This gets cached for faster resolution if it's required
/// in another part of the program.
///
/// # Errors
/// Will error if the image doesn't have OS version info
/// or we are unable to lock a mutex.
///
/// # Panics
/// Panics if the mutex fails to lock.
pub fn get_os_version(recipe: &Recipe) -> Result<u64> {
#[cfg(test)]
{
use miette::IntoDiagnostic;
if std::env::var(crate::test::BB_UNIT_TEST_MOCK_GET_OS_VERSION).is_ok() {
return crate::test::create_test_recipe()
.image_version
.parse()
.into_diagnostic();
}
}
trace!("Driver::get_os_version({recipe:#?})");
let image = format!("{}:{}", &recipe.base_image, &recipe.image_version);
let mut os_version_lock = OS_VERSION.lock().expect("Should lock");
let entry = os_version_lock.get(&image);
let os_version = match entry {
None => {
info!("Retrieving OS version from {image}. This might take a bit");
let inspect_opts = GetMetadataOpts::builder()
.image(&*recipe.base_image)
.tag(&*recipe.image_version)
.build();
let inspection = Self::get_metadata(&inspect_opts)?;
let os_version = inspection.get_version().ok_or_else(|| {
miette!(
help = format!("Please check with the image author about using '{IMAGE_VERSION_LABEL}' to report the os version."),
"Unable to get the OS version from the labels"
)
})?;
trace!("os_version: {os_version}");
os_version
}
Some(os_version) => {
debug!("Found cached {os_version} for {image}");
*os_version
}
};
if let Entry::Vacant(entry) = os_version_lock.entry(image.clone()) {
trace!("Caching version {os_version} for {image}");
entry.insert(os_version);
}
drop(os_version_lock);
Ok(os_version)
}
fn get_build_driver() -> BuildDriverType {
impl_driver_type!(SELECTED_BUILD_DRIVER)
}
fn get_inspect_driver() -> InspectDriverType {
impl_driver_type!(SELECTED_INSPECT_DRIVER)
}
fn get_signing_driver() -> SigningDriverType {
impl_driver_type!(SELECTED_SIGNING_DRIVER)
}
fn get_run_driver() -> RunDriverType {
impl_driver_type!(SELECTED_RUN_DRIVER)
}
fn get_ci_driver() -> CiDriverType {
impl_driver_type!(SELECTED_CI_DRIVER)
}
}
macro_rules! impl_build_driver {
($func:ident($($args:expr),*)) => {
match Self::get_build_driver() {
BuildDriverType::Buildah => BuildahDriver::$func($($args,)*),
BuildDriverType::Podman => PodmanDriver::$func($($args,)*),
BuildDriverType::Docker => DockerDriver::$func($($args,)*),
}
};
}
impl BuildDriver for Driver {
fn build(opts: &BuildOpts) -> Result<()> {
impl_build_driver!(build(opts))
}
fn tag(opts: &TagOpts) -> Result<()> {
impl_build_driver!(tag(opts))
}
fn push(opts: &PushOpts) -> Result<()> {
impl_build_driver!(push(opts))
}
fn login() -> Result<()> {
impl_build_driver!(login())
}
fn build_tag_push(opts: &BuildTagPushOpts) -> Result<()> {
impl_build_driver!(build_tag_push(opts))
}
}
macro_rules! impl_signing_driver {
($func:ident($($args:expr),*)) => {
match Self::get_signing_driver() {
SigningDriverType::Cosign => CosignDriver::$func($($args,)*),
#[cfg(feature = "sigstore")]
SigningDriverType::Sigstore => SigstoreDriver::$func($($args,)*),
}
};
}
impl SigningDriver for Driver {
fn generate_key_pair(opts: &GenerateKeyPairOpts) -> Result<()> {
impl_signing_driver!(generate_key_pair(opts))
}
fn check_signing_files(opts: &CheckKeyPairOpts) -> Result<()> {
impl_signing_driver!(check_signing_files(opts))
}
fn sign(opts: &SignOpts) -> Result<()> {
impl_signing_driver!(sign(opts))
}
fn verify(opts: &VerifyOpts) -> Result<()> {
impl_signing_driver!(verify(opts))
}
fn signing_login() -> Result<()> {
impl_signing_driver!(signing_login())
}
}
macro_rules! impl_inspect_driver {
($func:ident($($args:expr),*)) => {
match Self::get_inspect_driver() {
InspectDriverType::Skopeo => SkopeoDriver::$func($($args,)*),
InspectDriverType::Podman => PodmanDriver::$func($($args,)*),
InspectDriverType::Docker => DockerDriver::$func($($args,)*),
}
};
}
impl InspectDriver for Driver {
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
impl_inspect_driver!(get_metadata(opts))
}
}
macro_rules! impl_run_driver {
($func:ident($($args:expr),*)) => {
match Self::get_run_driver() {
RunDriverType::Docker => DockerDriver::$func($($args,)*),
RunDriverType::Podman => PodmanDriver::$func($($args,)*),
}
};
}
impl RunDriver for Driver {
fn run(opts: &RunOpts) -> std::io::Result<ExitStatus> {
impl_run_driver!(run(opts))
}
fn run_output(opts: &RunOpts) -> std::io::Result<Output> {
impl_run_driver!(run_output(opts))
}
}
macro_rules! impl_ci_driver {
($func:ident($($args:expr),*)) => {
match Self::get_ci_driver() {
CiDriverType::Local => LocalDriver::$func($($args)*),
CiDriverType::Gitlab => GitlabDriver::$func($($args)*),
CiDriverType::Github => GithubDriver::$func($($args)*),
}
};
}
impl CiDriver for Driver {
fn on_default_branch() -> bool {
impl_ci_driver!(on_default_branch())
}
fn keyless_cert_identity() -> Result<String> {
impl_ci_driver!(keyless_cert_identity())
}
fn oidc_provider() -> Result<String> {
impl_ci_driver!(oidc_provider())
}
fn generate_tags(recipe: &Recipe) -> Result<Vec<String>> {
impl_ci_driver!(generate_tags(recipe))
}
fn get_repo_url() -> Result<String> {
impl_ci_driver!(get_repo_url())
}
fn get_registry() -> Result<String> {
impl_ci_driver!(get_registry())
}
fn generate_image_name(recipe: &Recipe) -> Result<String> {
impl_ci_driver!(generate_image_name(recipe))
}
}

View file

@ -1,12 +1,12 @@
use std::process::Command;
use std::{io::Write, process::Stdio};
use blue_build_utils::logging::CommandLogging;
use log::{error, info, trace};
use miette::{bail, IntoDiagnostic, Result};
use blue_build_utils::{cmd, credentials::Credentials};
use log::{debug, error, info, trace};
use miette::{bail, miette, IntoDiagnostic, Result};
use semver::Version;
use serde::Deserialize;
use crate::credentials::{self, Credentials};
use crate::logging::CommandLogging;
use super::{
opts::{BuildOpts, PushOpts, TagOpts},
@ -30,9 +30,7 @@ impl DriverVersion for BuildahDriver {
trace!("BuildahDriver::version()");
trace!("buildah version --json");
let output = Command::new("buildah")
.arg("version")
.arg("--json")
let output = cmd!("buildah", "version", "--json")
.output()
.into_diagnostic()?;
@ -46,24 +44,21 @@ impl DriverVersion for BuildahDriver {
}
impl BuildDriver for BuildahDriver {
fn build(&self, opts: &BuildOpts) -> Result<()> {
fn build(opts: &BuildOpts) -> Result<()> {
trace!("BuildahDriver::build({opts:#?})");
trace!(
"buildah build --pull=true --layers={} -f {} -t {}",
!opts.squash,
opts.containerfile.display(),
opts.image,
let command = cmd!(
"buildah",
"build",
"--pull=true",
format!("--layers={}", !opts.squash),
"-f",
&*opts.containerfile,
"-t",
&*opts.image,
);
let mut command = Command::new("buildah");
command
.arg("build")
.arg("--pull=true")
.arg(format!("--layers={}", !opts.squash))
.arg("-f")
.arg(opts.containerfile.as_ref())
.arg("-t")
.arg(opts.image.as_ref());
trace!("{command:?}");
let status = command
.status_image_ref_progress(&opts.image, "Building Image")
.into_diagnostic()?;
@ -76,18 +71,13 @@ impl BuildDriver for BuildahDriver {
Ok(())
}
fn tag(&self, opts: &TagOpts) -> Result<()> {
fn tag(opts: &TagOpts) -> Result<()> {
trace!("BuildahDriver::tag({opts:#?})");
trace!("buildah tag {} {}", opts.src_image, opts.dest_image);
let status = Command::new("buildah")
.arg("tag")
.arg(opts.src_image.as_ref())
.arg(opts.dest_image.as_ref())
.status()
.into_diagnostic()?;
let mut command = cmd!("buildah", "tag", &*opts.src_image, &*opts.dest_image,);
if status.success() {
trace!("{command:?}");
if command.status().into_diagnostic()?.success() {
info!("Successfully tagged {}!", opts.dest_image);
} else {
bail!("Failed to tag image {}", opts.dest_image);
@ -95,18 +85,20 @@ impl BuildDriver for BuildahDriver {
Ok(())
}
fn push(&self, opts: &PushOpts) -> Result<()> {
fn push(opts: &PushOpts) -> Result<()> {
trace!("BuildahDriver::push({opts:#?})");
trace!("buildah push {}", opts.image);
let mut command = Command::new("buildah");
command
.arg("push")
.arg(format!(
let command = cmd!(
"buildah",
"push",
format!(
"--compression-format={}",
opts.compression_type.unwrap_or_default()
))
.arg(opts.image.as_ref());
),
&*opts.image,
);
trace!("{command:?}");
let status = command
.status_image_ref_progress(&opts.image, "Pushing Image")
.into_diagnostic()?;
@ -119,30 +111,47 @@ impl BuildDriver for BuildahDriver {
Ok(())
}
fn login(&self) -> Result<()> {
fn login() -> Result<()> {
trace!("BuildahDriver::login()");
if let Some(Credentials {
registry,
username,
password,
}) = credentials::get()
}) = Credentials::get()
{
trace!("buildah login -u {username} -p [MASKED] {registry}");
let output = Command::new("buildah")
.arg("login")
.arg("-u")
.arg(username)
.arg("-p")
.arg(password)
.arg(registry)
.output()
.into_diagnostic()?;
let mut command = cmd!(
"buildah",
"login",
"-u",
username,
"--password-stdin",
registry
);
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
trace!("{command:?}");
let mut child = command.spawn().into_diagnostic()?;
write!(
child
.stdin
.as_mut()
.ok_or_else(|| miette!("Unable to open pipe to stdin"))?,
"{password}"
)
.into_diagnostic()?;
let output = child.wait_with_output().into_diagnostic()?;
if !output.status.success() {
let err_out = String::from_utf8_lossy(&output.stderr);
bail!("Failed to login for buildah: {err_out}");
bail!("Failed to login for buildah:\n{}", err_out.trim());
}
debug!("Logged into {registry}");
}
Ok(())
}

View file

@ -0,0 +1,241 @@
use std::{fmt::Debug, fs, io::Write, path::Path, process::Stdio};
use blue_build_utils::{
cmd,
constants::{COSIGN_PASSWORD, COSIGN_PUB_PATH, COSIGN_YES},
credentials::Credentials,
};
use log::{debug, trace};
use miette::{bail, miette, Context, IntoDiagnostic, Result};
use crate::drivers::opts::VerifyType;
use super::{
functions::get_private_key,
opts::{CheckKeyPairOpts, GenerateKeyPairOpts, SignOpts, VerifyOpts},
SigningDriver,
};
#[derive(Debug)]
pub struct CosignDriver;
impl SigningDriver for CosignDriver {
fn generate_key_pair(opts: &GenerateKeyPairOpts) -> Result<()> {
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
let mut command = cmd!(
"cosign",
"generate-key-pair",
COSIGN_PASSWORD => "",
COSIGN_YES => "true",
);
command.current_dir(path);
let status = command.status().into_diagnostic()?;
if !status.success() {
bail!("Failed to generate cosign key-pair!");
}
Ok(())
}
fn check_signing_files(opts: &CheckKeyPairOpts) -> Result<()> {
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
let priv_key = get_private_key(path)?;
let mut command = cmd!(
"cosign",
"public-key",
format!("--key={priv_key}"),
COSIGN_PASSWORD => "",
COSIGN_YES => "true",
);
trace!("{command:?}");
let output = command.output().into_diagnostic()?;
if !output.status.success() {
bail!(
"Failed to run cosign public-key: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let calculated_pub_key = String::from_utf8(output.stdout).into_diagnostic()?;
let found_pub_key = fs::read_to_string(path.join(COSIGN_PUB_PATH))
.into_diagnostic()
.with_context(|| format!("Failed to read {COSIGN_PUB_PATH}"))?;
trace!("calculated_pub_key={calculated_pub_key},found_pub_key={found_pub_key}");
if calculated_pub_key.trim() == found_pub_key.trim() {
debug!("Cosign files match, continuing build");
Ok(())
} else {
bail!("Public key '{COSIGN_PUB_PATH}' does not match private key")
}
}
fn signing_login() -> Result<()> {
trace!("CosignDriver::signing_login()");
if let Some(Credentials {
registry,
username,
password,
}) = Credentials::get()
{
let mut command = cmd!(
"cosign",
"login",
"-u",
username,
"--password-stdin",
registry,
stdin = Stdio::piped(),
stdout = Stdio::piped(),
stderr = Stdio::piped(),
);
trace!("{command:?}");
let mut child = command.spawn().into_diagnostic()?;
write!(
child
.stdin
.as_mut()
.ok_or_else(|| miette!("Unable to open pipe to stdin"))?,
"{password}"
)
.into_diagnostic()?;
let output = child.wait_with_output().into_diagnostic()?;
if !output.status.success() {
let err_out = String::from_utf8_lossy(&output.stderr);
bail!("Failed to login for cosign:\n{}", err_out.trim());
}
debug!("Logged into {registry}");
}
Ok(())
}
fn sign(opts: &SignOpts) -> Result<()> {
let image_digest: &str = opts.image.as_ref();
let mut command = cmd!(
"cosign",
"sign",
if let Some(ref key) = opts.key => format!("--key={key}"),
"--recursive",
image_digest,
COSIGN_PASSWORD => "",
COSIGN_YES => "true",
);
trace!("{command:?}");
if !command.status().into_diagnostic()?.success() {
bail!("Failed to sign {image_digest}");
}
Ok(())
}
fn verify(opts: &VerifyOpts) -> Result<()> {
let image_name_tag: &str = opts.image.as_ref();
let mut command = cmd!(
"cosign",
"verify",
|c| {
match &opts.verify_type {
VerifyType::File(path) => cmd!(c, format!("--key={}", path.display())),
VerifyType::Keyless { issuer, identity } => cmd!(
c,
"--certificate-identity-regexp",
identity as &str,
"--certificate-oidc-issuer",
issuer as &str,
),
};
},
image_name_tag
);
trace!("{command:?}");
if !command.status().into_diagnostic()?.success() {
bail!("Failed to verify {image_name_tag}");
}
Ok(())
}
}
#[cfg(test)]
mod test {
use std::{fs, path::Path};
use blue_build_utils::constants::{COSIGN_PRIV_PATH, COSIGN_PUB_PATH};
use tempdir::TempDir;
use crate::drivers::{
opts::{CheckKeyPairOpts, GenerateKeyPairOpts},
SigningDriver,
};
use super::CosignDriver;
#[test]
fn generate_key_pair() {
let tempdir = TempDir::new("keypair").unwrap();
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
CosignDriver::generate_key_pair(&gen_opts).unwrap();
eprintln!(
"Private key:\n{}",
fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
);
eprintln!(
"Public key:\n{}",
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
);
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
CosignDriver::check_signing_files(&check_opts).unwrap();
}
#[test]
fn check_key_pairs() {
let path = Path::new("../test-files/keys");
let opts = CheckKeyPairOpts::builder().dir(path).build();
CosignDriver::check_signing_files(&opts).unwrap();
}
#[test]
#[cfg(feature = "sigstore")]
fn compatibility() {
use crate::drivers::sigstore_driver::SigstoreDriver;
let tempdir = TempDir::new("keypair").unwrap();
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
CosignDriver::generate_key_pair(&gen_opts).unwrap();
eprintln!(
"Private key:\n{}",
fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
);
eprintln!(
"Public key:\n{}",
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
);
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
SigstoreDriver::check_signing_files(&check_opts).unwrap();
}
}

View file

@ -1,18 +1,20 @@
use std::{
env,
io::Write,
path::Path,
process::{Command, ExitStatus},
process::{Command, ExitStatus, Stdio},
sync::Mutex,
time::Duration,
};
use blue_build_utils::{
cmd,
constants::{BB_BUILDKIT_CACHE_GHA, CONTAINER_FILE, DOCKER_HOST, SKOPEO_IMAGE},
logging::{CommandLogging, Logger},
signal_handler::{add_cid, remove_cid, ContainerId},
credentials::Credentials,
string_vec,
};
use indicatif::{ProgressBar, ProgressStyle};
use log::{info, trace, warn};
use log::{debug, info, trace, warn};
use miette::{bail, IntoDiagnostic, Result};
use once_cell::sync::Lazy;
use semver::Version;
@ -20,11 +22,12 @@ use serde::Deserialize;
use tempdir::TempDir;
use crate::{
credentials::Credentials, drivers::types::RunDriverType, image_metadata::ImageMetadata,
drivers::image_metadata::ImageMetadata,
logging::{CommandLogging, Logger},
signal_handler::{add_cid, remove_cid, ContainerId, ContainerRuntime},
};
use super::{
credentials,
opts::{BuildOpts, BuildTagPushOpts, GetMetadataOpts, PushOpts, RunOpts, TagOpts},
BuildDriver, DriverVersion, InspectDriver, RunDriver,
};
@ -58,10 +61,7 @@ impl DockerDriver {
}
trace!("docker buildx ls --format={}", "{{.Name}}");
let ls_out = Command::new("docker")
.arg("buildx")
.arg("ls")
.arg("--format={{.Name}}")
let ls_out = cmd!("docker", "buildx", "ls", "--format={{.Name}}")
.output()
.into_diagnostic()?;
@ -75,14 +75,16 @@ impl DockerDriver {
if !ls_out.lines().any(|line| line == "bluebuild") {
trace!("docker buildx create --bootstrap --driver=docker-container --name=bluebuild");
let create_out = Command::new("docker")
.arg("buildx")
.arg("create")
.arg("--bootstrap")
.arg("--driver=docker-container")
.arg("--name=bluebuild")
.output()
.into_diagnostic()?;
let create_out = cmd!(
"docker",
"buildx",
"create",
"--bootstrap",
"--driver=docker-container",
"--name=bluebuild",
)
.output()
.into_diagnostic()?;
if !create_out.status.success() {
bail!("{}", String::from_utf8_lossy(&create_out.stderr));
@ -101,10 +103,7 @@ impl DriverVersion for DockerDriver {
const VERSION_REQ: &'static str = ">=23";
fn version() -> Result<Version> {
let output = Command::new("docker")
.arg("version")
.arg("-f")
.arg("json")
let output = cmd!("docker", "version", "-f", "json")
.output()
.into_diagnostic()?;
@ -116,7 +115,7 @@ impl DriverVersion for DockerDriver {
}
impl BuildDriver for DockerDriver {
fn build(&self, opts: &BuildOpts) -> Result<()> {
fn build(opts: &BuildOpts) -> Result<()> {
trace!("DockerDriver::build({opts:#?})");
if opts.squash {
@ -124,15 +123,17 @@ impl BuildDriver for DockerDriver {
}
trace!("docker build -t {} -f {CONTAINER_FILE} .", opts.image);
let status = Command::new("docker")
.arg("build")
.arg("-t")
.arg(opts.image.as_ref())
.arg("-f")
.arg(opts.containerfile.as_ref())
.arg(".")
.status()
.into_diagnostic()?;
let status = cmd!(
"docker",
"build",
"-t",
&*opts.image,
"-f",
&*opts.containerfile,
".",
)
.status()
.into_diagnostic()?;
if status.success() {
info!("Successfully built {}", opts.image);
@ -142,14 +143,11 @@ impl BuildDriver for DockerDriver {
Ok(())
}
fn tag(&self, opts: &TagOpts) -> Result<()> {
fn tag(opts: &TagOpts) -> Result<()> {
trace!("DockerDriver::tag({opts:#?})");
trace!("docker tag {} {}", opts.src_image, opts.dest_image);
let status = Command::new("docker")
.arg("tag")
.arg(opts.src_image.as_ref())
.arg(opts.dest_image.as_ref())
let status = cmd!("docker", "tag", &*opts.src_image, &*opts.dest_image,)
.status()
.into_diagnostic()?;
@ -161,13 +159,11 @@ impl BuildDriver for DockerDriver {
Ok(())
}
fn push(&self, opts: &PushOpts) -> Result<()> {
fn push(opts: &PushOpts) -> Result<()> {
trace!("DockerDriver::push({opts:#?})");
trace!("docker push {}", opts.image);
let status = Command::new("docker")
.arg("push")
.arg(opts.image.as_ref())
let status = cmd!("docker", "push", &*opts.image)
.status()
.into_diagnostic()?;
@ -179,119 +175,117 @@ impl BuildDriver for DockerDriver {
Ok(())
}
fn login(&self) -> Result<()> {
fn login() -> Result<()> {
trace!("DockerDriver::login()");
if let Some(Credentials {
registry,
username,
password,
}) = credentials::get()
}) = Credentials::get()
{
trace!("docker login -u {username} -p [MASKED] {registry}");
let output = Command::new("docker")
.arg("login")
.arg("-u")
.arg(username)
.arg("-p")
.arg(password)
.arg(registry)
.output()
.into_diagnostic()?;
let mut command = cmd!(
"docker",
"login",
"-u",
username,
"--password-stdin",
registry,
stdin = Stdio::piped(),
stdout = Stdio::piped(),
stderr = Stdio::piped(),
);
trace!("{command:?}");
let mut child = command.spawn().into_diagnostic()?;
write!(child.stdin.as_mut().unwrap(), "{password}").into_diagnostic()?;
let output = child.wait_with_output().into_diagnostic()?;
if !output.status.success() {
let err_out = String::from_utf8_lossy(&output.stderr);
bail!("Failed to login for docker: {err_out}");
bail!("Failed to login for docker:\n{}", err_out.trim());
}
debug!("Logged into {registry}");
}
Ok(())
}
fn build_tag_push(&self, opts: &BuildTagPushOpts) -> Result<()> {
fn build_tag_push(opts: &BuildTagPushOpts) -> Result<()> {
trace!("DockerDriver::build_tag_push({opts:#?})");
if opts.squash {
warn!("Squash is deprecated for docker so this build will not squash");
}
trace!("docker buildx");
let mut command = Command::new("docker");
command.arg("buildx");
if !env::var(DOCKER_HOST).is_ok_and(|dh| !dh.is_empty()) {
Self::setup()?;
trace!("--builder=bluebuild");
command.arg("--builder=bluebuild");
}
trace!(
"build --progress=plain --pull -f {}",
opts.containerfile.display()
let mut command = cmd!(
"docker",
"buildx",
|command|? {
if !env::var(DOCKER_HOST).is_ok_and(|dh| !dh.is_empty()) {
Self::setup()?;
cmd!(command, "--builder=bluebuild");
}
},
"build",
"--pull",
"-f",
&*opts.containerfile,
// https://github.com/moby/buildkit?tab=readme-ov-file#github-actions-cache-experimental
if env::var(BB_BUILDKIT_CACHE_GHA)
.map_or_else(|_| false, |e| e == "true") => [
"--cache-from",
"type=gha",
"--cache-to",
"type=gha",
],
);
command
.arg("build")
.arg("--pull")
.arg("-f")
.arg(opts.containerfile.as_ref());
// https://github.com/moby/buildkit?tab=readme-ov-file#github-actions-cache-experimental
if env::var(BB_BUILDKIT_CACHE_GHA).map_or_else(|_| false, |e| e == "true") {
trace!("--cache-from type=gha --cache-to type=gha");
command
.arg("--cache-from")
.arg("type=gha")
.arg("--cache-to")
.arg("type=gha");
}
let mut final_image = String::new();
match (opts.image.as_ref(), opts.archive_path.as_ref()) {
match (opts.image.as_deref(), opts.archive_path.as_deref()) {
(Some(image), None) => {
if opts.tags.is_empty() {
final_image.push_str(image);
trace!("-t {image}");
command.arg("-t").arg(image.as_ref());
cmd!(command, "-t", image);
} else {
final_image
.push_str(format!("{image}:{}", opts.tags.first().unwrap_or(&"")).as_str());
final_image.push_str(
format!("{image}:{}", opts.tags.first().map_or("", String::as_str))
.as_str(),
);
opts.tags.iter().for_each(|tag| {
let full_image = format!("{image}:{tag}");
trace!("-t {full_image}");
command.arg("-t").arg(full_image);
cmd!(command, "-t", format!("{image}:{tag}"));
});
}
if opts.push {
trace!("--output type=image,name={image},push=true,compression={},oci-mediatypes=true", opts.compression);
command.arg("--output").arg(format!(
"type=image,name={image},push=true,compression={},oci-mediatypes=true",
opts.compression
));
cmd!(
command,
"--output",
format!(
"type=image,name={image},push=true,compression={},oci-mediatypes=true",
opts.compression
)
);
} else {
trace!("--load");
command.arg("--load");
cmd!(command, "--load");
}
}
(None, Some(archive_path)) => {
final_image.push_str(archive_path);
trace!("--output type=oci,dest={archive_path}");
command
.arg("--output")
.arg(format!("type=oci,dest={archive_path}"));
cmd!(command, "--output", format!("type=oci,dest={archive_path}"));
}
(Some(_), Some(_)) => bail!("Cannot use both image and archive path"),
(None, None) => bail!("Need either the image or archive path set"),
}
trace!(".");
command.arg(".");
cmd!(command, ".");
trace!("{command:?}");
if command
.status_image_ref_progress(&final_image, "Building Image")
.into_diagnostic()?
@ -310,7 +304,7 @@ impl BuildDriver for DockerDriver {
}
impl InspectDriver for DockerDriver {
fn get_metadata(&self, opts: &GetMetadataOpts) -> Result<ImageMetadata> {
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
trace!("DockerDriver::get_labels({opts:#?})");
let url = opts.tag.as_ref().map_or_else(
@ -325,14 +319,14 @@ impl InspectDriver for DockerDriver {
);
progress.enable_steady_tick(Duration::from_millis(100));
let output = self
.run_output(
&RunOpts::builder()
.image(SKOPEO_IMAGE)
.args(&["inspect".to_string(), url.clone()])
.build(),
)
.into_diagnostic()?;
let output = Self::run_output(
&RunOpts::builder()
.image(SKOPEO_IMAGE)
.args(string_vec!["inspect", url.clone()])
.remove(true)
.build(),
)
.into_diagnostic()?;
progress.finish();
Logger::multi_progress().remove(&progress);
@ -348,29 +342,25 @@ impl InspectDriver for DockerDriver {
}
impl RunDriver for DockerDriver {
fn run(&self, opts: &RunOpts) -> std::io::Result<ExitStatus> {
trace!("DockerDriver::run({opts:#?})");
fn run(opts: &RunOpts) -> std::io::Result<ExitStatus> {
let cid_path = TempDir::new("docker")?;
let cid_file = cid_path.path().join("cid");
let cid = ContainerId::new(&cid_file, RunDriverType::Docker, false);
let cid = ContainerId::new(&cid_file, ContainerRuntime::Docker, false);
add_cid(&cid);
let status = docker_run(opts, &cid_file)
.status_image_ref_progress(opts.image.as_ref(), "Running container")?;
.status_image_ref_progress(&*opts.image, "Running container")?;
remove_cid(&cid);
Ok(status)
}
fn run_output(&self, opts: &RunOpts) -> std::io::Result<std::process::Output> {
trace!("DockerDriver::run({opts:#?})");
fn run_output(opts: &RunOpts) -> std::io::Result<std::process::Output> {
let cid_path = TempDir::new("docker")?;
let cid_file = cid_path.path().join("cid");
let cid = ContainerId::new(&cid_file, RunDriverType::Docker, false);
let cid = ContainerId::new(&cid_file, ContainerRuntime::Docker, false);
add_cid(&cid);
@ -383,41 +373,29 @@ impl RunDriver for DockerDriver {
}
fn docker_run(opts: &RunOpts, cid_file: &Path) -> Command {
let mut command = Command::new("docker");
command
.arg("run")
.arg(format!("--cidfile={}", cid_file.display()));
if opts.privileged {
command.arg("--privileged");
}
if opts.remove {
command.arg("--rm");
}
if opts.pull {
command.arg("--pull=always");
}
opts.volumes.iter().for_each(|volume| {
command.arg("--volume");
command.arg(format!(
"{}:{}",
volume.path_or_vol_name, volume.container_path,
));
});
opts.env_vars.iter().for_each(|env| {
command.arg("--env");
command.arg(format!("{}={}", env.key, env.value));
});
command.arg(opts.image.as_ref());
command.args(opts.args.iter());
trace!("{command:?}");
command
cmd!(
"docker",
"run",
format!("--cidfile={}", cid_file.display()),
if opts.privileged => "--privileged",
if opts.remove => "--rm",
if opts.pull => "--pull=always",
for volume in opts.volumes => [
"--volume",
format!("{}:{}", volume.path_or_vol_name, volume.container_path),
],
for env in opts.env_vars => [
"--env",
format!("{}={}", env.key, env.value),
],
|command| {
match (opts.uid, opts.gid) {
(Some(uid), None) => cmd!(command, "-u", format!("{uid}")),
(Some(uid), Some(gid)) => cmd!(command, "-u", format!("{}:{}", uid, gid)),
_ => {}
}
},
&*opts.image,
for opts.args,
)
}

View file

@ -0,0 +1,52 @@
use std::{env, path::Path};
use blue_build_utils::{
constants::{BB_PRIVATE_KEY, COSIGN_PRIVATE_KEY, COSIGN_PRIV_PATH, COSIGN_PUB_PATH},
string,
};
use miette::{bail, Result};
use super::opts::PrivateKey;
pub(super) fn get_private_key<P>(path: P) -> Result<PrivateKey>
where
P: AsRef<Path>,
{
let path = path.as_ref();
Ok(
match (
path.join(COSIGN_PUB_PATH).exists(),
env::var(BB_PRIVATE_KEY).ok(),
env::var(COSIGN_PRIVATE_KEY).ok(),
path.join(COSIGN_PRIV_PATH),
) {
(true, Some(private_key), _, _) if !private_key.is_empty() => {
PrivateKey::Env(string!(BB_PRIVATE_KEY))
}
(true, _, Some(cosign_priv_key), _) if !cosign_priv_key.is_empty() => {
PrivateKey::Env(string!(COSIGN_PRIVATE_KEY))
}
(true, _, _, cosign_priv_key_path) if cosign_priv_key_path.exists() => {
PrivateKey::Path(cosign_priv_key_path)
}
_ => {
bail!(
help = format!(
"{}{}{}{}{}{}",
format_args!("Make sure you have a `{COSIGN_PUB_PATH}`\n"),
format_args!(
"in the root of your repo and have either {COSIGN_PRIVATE_KEY}\n"
),
format_args!("set in your env variables or a `{COSIGN_PRIV_PATH}`\n"),
"file in the root of your repo.\n\n",
"See https://blue-build.org/how-to/cosign/ for more information.\n\n",
"If you don't want to sign your image, use the `--no-sign` flag.",
),
"{}",
"Unable to find private/public key pair",
)
}
},
)
}

View file

@ -0,0 +1,272 @@
use blue_build_utils::{
constants::{
GITHUB_EVENT_NAME, GITHUB_REF_NAME, GITHUB_SHA, GITHUB_TOKEN_ISSUER_URL,
GITHUB_WORKFLOW_REF, PR_EVENT_NUMBER,
},
get_env_var,
};
use event::Event;
use log::trace;
use super::{CiDriver, Driver};
mod event;
pub struct GithubDriver;
impl CiDriver for GithubDriver {
fn on_default_branch() -> bool {
Event::try_new().map_or_else(
|_| false,
|event| match (event.commit_ref, event.head) {
(Some(commit_ref), _) => {
commit_ref.trim_start_matches("refs/heads/") == event.repository.default_branch
}
(_, Some(head)) => event.repository.default_branch == head.commit_ref,
_ => false,
},
)
}
fn keyless_cert_identity() -> miette::Result<String> {
get_env_var(GITHUB_WORKFLOW_REF)
}
fn oidc_provider() -> miette::Result<String> {
Ok(GITHUB_TOKEN_ISSUER_URL.to_string())
}
fn generate_tags(recipe: &blue_build_recipe::Recipe) -> miette::Result<Vec<String>> {
let mut tags: Vec<String> = Vec::new();
let os_version = Driver::get_os_version(recipe)?;
let github_event_name = get_env_var(GITHUB_EVENT_NAME)?;
if github_event_name == "pull_request" {
trace!("Running in a PR");
let github_event_number = get_env_var(PR_EVENT_NUMBER)?;
tags.push(format!("pr-{github_event_number}-{os_version}"));
} else if Self::on_default_branch() {
tags.push(os_version.to_string());
let timestamp = blue_build_utils::get_tag_timestamp();
tags.push(format!("{timestamp}-{os_version}"));
if let Some(ref alt_tags) = recipe.alt_tags {
tags.extend(alt_tags.iter().map(ToString::to_string));
} else {
tags.push("latest".into());
tags.push(timestamp);
}
} else {
let github_ref_name = get_env_var(GITHUB_REF_NAME)?;
tags.push(format!("br-{github_ref_name}-{os_version}"));
}
let mut short_sha = get_env_var(GITHUB_SHA)?;
short_sha.truncate(7);
tags.push(format!("{short_sha}-{os_version}"));
Ok(tags)
}
fn get_repo_url() -> miette::Result<String> {
Ok(Event::try_new()?.repository.html_url)
}
fn get_registry() -> miette::Result<String> {
Ok(format!(
"ghcr.io/{}",
Event::try_new()?.repository.owner.login
))
}
}
#[cfg(test)]
mod test {
use std::env;
use blue_build_utils::constants::{
GITHUB_EVENT_NAME, GITHUB_EVENT_PATH, GITHUB_REF_NAME, GITHUB_SHA, PR_EVENT_NUMBER,
};
use crate::{
drivers::CiDriver,
test::{create_test_recipe, BB_UNIT_TEST_MOCK_GET_OS_VERSION, ENV_LOCK},
};
use super::GithubDriver;
fn setup_default_branch() {
setup();
env::set_var(
GITHUB_EVENT_PATH,
"../test-files/github-events/default-branch.json",
);
env::set_var(GITHUB_REF_NAME, "main");
}
fn setup_pr_branch() {
setup();
env::set_var(
GITHUB_EVENT_PATH,
"../test-files/github-events/pr-branch.json",
);
env::set_var(GITHUB_EVENT_NAME, "pull_request");
env::set_var(GITHUB_REF_NAME, "test");
env::set_var(PR_EVENT_NUMBER, "12");
}
fn setup_branch() {
setup();
env::set_var(GITHUB_EVENT_PATH, "../test-files/github-events/branch.json");
env::set_var(GITHUB_REF_NAME, "test");
}
fn setup() {
env::set_var(GITHUB_EVENT_NAME, "push");
env::set_var(GITHUB_SHA, "1234567890");
env::set_var(BB_UNIT_TEST_MOCK_GET_OS_VERSION, "");
}
fn teardown() {
env::remove_var(GITHUB_EVENT_NAME);
env::remove_var(GITHUB_EVENT_PATH);
env::remove_var(GITHUB_REF_NAME);
env::remove_var(PR_EVENT_NUMBER);
env::remove_var(GITHUB_SHA);
env::remove_var(BB_UNIT_TEST_MOCK_GET_OS_VERSION);
}
#[test]
fn get_registry() {
let _env = ENV_LOCK.lock().unwrap();
setup_default_branch();
let registry = GithubDriver::get_registry().unwrap();
assert_eq!(registry, "ghcr.io/test-owner");
teardown();
}
#[test]
fn on_default_branch_true() {
let _env = ENV_LOCK.lock().unwrap();
setup_default_branch();
assert!(GithubDriver::on_default_branch());
teardown();
}
#[test]
fn on_default_branch_false() {
let _env = ENV_LOCK.lock().unwrap();
setup_pr_branch();
assert!(!GithubDriver::on_default_branch());
teardown();
}
#[test]
fn get_repo_url() {
let _env = ENV_LOCK.lock().unwrap();
setup_branch();
let url = GithubDriver::get_repo_url().unwrap();
assert_eq!(url, "https://example.com/");
teardown();
}
#[test]
fn generate_tags_default_branch() {
let _env = ENV_LOCK.lock().unwrap();
let timestamp = blue_build_utils::get_tag_timestamp();
setup_default_branch();
let mut tags = GithubDriver::generate_tags(&create_test_recipe()).unwrap();
tags.sort();
let mut expected_tags = vec![
format!("{timestamp}-40"),
"latest".to_string(),
timestamp,
"1234567-40".to_string(),
"40".to_string(),
];
expected_tags.sort();
assert_eq!(tags, expected_tags);
teardown();
}
#[test]
fn generate_tags_default_branch_alt_tags() {
let _env = ENV_LOCK.lock().unwrap();
let timestamp = blue_build_utils::get_tag_timestamp();
setup_default_branch();
let mut recipe = create_test_recipe();
recipe.alt_tags = Some(vec!["test-tag1".into(), "test-tag2".into()]);
let mut tags = GithubDriver::generate_tags(&recipe).unwrap();
tags.sort();
let mut expected_tags = vec![
format!("{timestamp}-40"),
"1234567-40".to_string(),
"40".to_string(),
];
expected_tags.extend(recipe.alt_tags.unwrap().iter().map(ToString::to_string));
expected_tags.sort();
assert_eq!(tags, expected_tags);
teardown();
}
#[test]
fn generate_tags_pr_branch() {
let _env = ENV_LOCK.lock().unwrap();
setup_pr_branch();
let mut tags = GithubDriver::generate_tags(&create_test_recipe()).unwrap();
tags.sort();
let mut expected_tags = vec!["pr-12-40".to_string(), "1234567-40".to_string()];
expected_tags.sort();
assert_eq!(tags, expected_tags);
teardown();
}
#[test]
fn generate_tags_branch() {
let _env = ENV_LOCK.lock().unwrap();
setup_branch();
let mut tags = GithubDriver::generate_tags(&create_test_recipe()).unwrap();
tags.sort();
let mut expected_tags = vec!["1234567-40".to_string(), "br-test-40".to_string()];
expected_tags.sort();
assert_eq!(tags, expected_tags);
teardown();
}
}

View file

@ -0,0 +1,44 @@
use std::{fs, path::PathBuf};
use blue_build_utils::{constants::GITHUB_EVENT_PATH, get_env_var};
use miette::{IntoDiagnostic, Result};
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
pub(super) struct Event {
pub repository: EventRepository,
// pub base: Option<EventRefInfo>,
pub head: Option<EventRefInfo>,
#[serde(alias = "ref")]
pub commit_ref: Option<String>,
}
impl Event {
pub fn try_new() -> Result<Self> {
get_env_var(GITHUB_EVENT_PATH)
.map(PathBuf::from)
.and_then(|event_path| {
serde_json::from_str::<Self>(&fs::read_to_string(event_path).into_diagnostic()?)
.into_diagnostic()
})
}
}
#[derive(Debug, Deserialize, Clone)]
pub(super) struct EventRepository {
pub default_branch: String,
pub owner: EventRepositoryOwner,
pub html_url: String,
}
#[derive(Debug, Deserialize, Clone)]
pub(super) struct EventRepositoryOwner {
pub login: String,
}
#[derive(Debug, Deserialize, Clone)]
pub(super) struct EventRefInfo {
#[serde(alias = "ref")]
pub commit_ref: String,
}

View file

@ -0,0 +1,293 @@
use std::env;
use blue_build_utils::{
constants::{
CI_COMMIT_REF_NAME, CI_COMMIT_SHORT_SHA, CI_DEFAULT_BRANCH, CI_MERGE_REQUEST_IID,
CI_PIPELINE_SOURCE, CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_PROJECT_URL, CI_REGISTRY,
CI_SERVER_HOST, CI_SERVER_PROTOCOL,
},
get_env_var,
};
use log::{debug, trace};
use crate::drivers::Driver;
use super::CiDriver;
pub struct GitlabDriver;
impl CiDriver for GitlabDriver {
fn on_default_branch() -> bool {
env::var(CI_DEFAULT_BRANCH).is_ok_and(|default_branch| {
env::var(CI_COMMIT_REF_NAME).is_ok_and(|branch| default_branch == branch)
})
}
fn keyless_cert_identity() -> miette::Result<String> {
Ok(format!(
"{}//.gitlab-ci.yml@refs/heads/{}",
get_env_var(CI_PROJECT_URL)?,
get_env_var(CI_DEFAULT_BRANCH)?,
))
}
fn oidc_provider() -> miette::Result<String> {
Ok(format!(
"{}://{}",
get_env_var(CI_SERVER_PROTOCOL)?,
get_env_var(CI_SERVER_HOST)?,
))
}
fn generate_tags(recipe: &blue_build_recipe::Recipe) -> miette::Result<Vec<String>> {
let mut tags: Vec<String> = Vec::new();
let os_version = Driver::get_os_version(recipe)?;
if Self::on_default_branch() {
debug!("Running on the default branch");
tags.push(os_version.to_string());
let timestamp = blue_build_utils::get_tag_timestamp();
tags.push(format!("{timestamp}-{os_version}"));
if let Some(ref alt_tags) = recipe.alt_tags {
tags.extend(alt_tags.iter().map(ToString::to_string));
} else {
tags.push("latest".into());
tags.push(timestamp);
}
} else if let Ok(mr_iid) = env::var(CI_MERGE_REQUEST_IID) {
trace!("{CI_MERGE_REQUEST_IID}={mr_iid}");
let pipeline_source = get_env_var(CI_PIPELINE_SOURCE)?;
trace!("{CI_PIPELINE_SOURCE}={pipeline_source}");
if pipeline_source == "merge_request_event" {
debug!("Running in a MR");
tags.push(format!("mr-{mr_iid}-{os_version}"));
}
} else {
let commit_branch = get_env_var(CI_COMMIT_REF_NAME)?;
trace!("{CI_COMMIT_REF_NAME}={commit_branch}");
debug!("Running on branch {commit_branch}");
tags.push(format!("br-{commit_branch}-{os_version}"));
}
let commit_sha = get_env_var(CI_COMMIT_SHORT_SHA)?;
trace!("{CI_COMMIT_SHORT_SHA}={commit_sha}");
tags.push(format!("{commit_sha}-{os_version}"));
Ok(tags)
}
fn get_repo_url() -> miette::Result<String> {
Ok(format!(
"{}://{}/{}/{}",
get_env_var(CI_SERVER_PROTOCOL)?,
get_env_var(CI_SERVER_HOST)?,
get_env_var(CI_PROJECT_NAMESPACE)?,
get_env_var(CI_PROJECT_NAME)?,
))
}
fn get_registry() -> miette::Result<String> {
Ok(format!(
"{}/{}/{}",
get_env_var(CI_REGISTRY)?,
get_env_var(CI_PROJECT_NAMESPACE)?,
get_env_var(CI_PROJECT_NAME)?,
)
.to_lowercase())
}
}
#[cfg(test)]
mod test {
use std::env;
use blue_build_utils::constants::{
CI_COMMIT_REF_NAME, CI_COMMIT_SHORT_SHA, CI_DEFAULT_BRANCH, CI_MERGE_REQUEST_IID,
CI_PIPELINE_SOURCE, CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_REGISTRY, CI_SERVER_HOST,
CI_SERVER_PROTOCOL,
};
use crate::{
drivers::CiDriver,
test::{create_test_recipe, BB_UNIT_TEST_MOCK_GET_OS_VERSION, ENV_LOCK},
};
use super::GitlabDriver;
fn setup_default_branch() {
setup();
env::set_var(CI_COMMIT_REF_NAME, "main");
}
fn setup_mr_branch() {
setup();
env::set_var(CI_MERGE_REQUEST_IID, "12");
env::set_var(CI_PIPELINE_SOURCE, "merge_request_event");
env::set_var(CI_COMMIT_REF_NAME, "test");
}
fn setup_branch() {
setup();
env::set_var(CI_COMMIT_REF_NAME, "test");
}
fn setup() {
env::set_var(CI_DEFAULT_BRANCH, "main");
env::set_var(CI_COMMIT_SHORT_SHA, "1234567");
env::set_var(CI_REGISTRY, "registry.example.com");
env::set_var(CI_PROJECT_NAMESPACE, "test-project");
env::set_var(CI_PROJECT_NAME, "test");
env::set_var(CI_SERVER_PROTOCOL, "https");
env::set_var(CI_SERVER_HOST, "gitlab.example.com");
env::set_var(BB_UNIT_TEST_MOCK_GET_OS_VERSION, "");
}
fn teardown() {
env::remove_var(CI_COMMIT_REF_NAME);
env::remove_var(CI_MERGE_REQUEST_IID);
env::remove_var(CI_PIPELINE_SOURCE);
env::remove_var(CI_DEFAULT_BRANCH);
env::remove_var(CI_COMMIT_SHORT_SHA);
env::remove_var(CI_REGISTRY);
env::remove_var(CI_PROJECT_NAMESPACE);
env::remove_var(CI_PROJECT_NAME);
env::remove_var(CI_SERVER_PROTOCOL);
env::remove_var(CI_SERVER_HOST);
env::remove_var(BB_UNIT_TEST_MOCK_GET_OS_VERSION);
}
#[test]
fn get_registry() {
let _env = ENV_LOCK.lock().unwrap();
setup();
let registry = GitlabDriver::get_registry().unwrap();
assert_eq!(registry, "registry.example.com/test-project/test");
teardown();
}
#[test]
fn on_default_branch_true() {
let _env = ENV_LOCK.lock().unwrap();
setup_default_branch();
assert!(GitlabDriver::on_default_branch());
teardown();
}
#[test]
fn on_default_branch_false() {
let _env = ENV_LOCK.lock().unwrap();
setup_branch();
assert!(!GitlabDriver::on_default_branch());
teardown();
}
#[test]
fn get_repo_url() {
let _env = ENV_LOCK.lock().unwrap();
setup();
let url = GitlabDriver::get_repo_url().unwrap();
assert_eq!(url, "https://gitlab.example.com/test-project/test");
teardown();
}
#[test]
fn generate_tags_default_branch() {
let _env = ENV_LOCK.lock().unwrap();
let timestamp = blue_build_utils::get_tag_timestamp();
setup_default_branch();
let mut tags = GitlabDriver::generate_tags(&create_test_recipe()).unwrap();
tags.sort();
let mut expected_tags = vec![
format!("{timestamp}-40"),
"latest".to_string(),
timestamp,
"1234567-40".to_string(),
"40".to_string(),
];
expected_tags.sort();
assert_eq!(tags, expected_tags);
teardown();
}
#[test]
fn generate_tags_default_branch_alt_tags() {
let _env = ENV_LOCK.lock().unwrap();
let timestamp = blue_build_utils::get_tag_timestamp();
setup_default_branch();
let mut recipe = create_test_recipe();
recipe.alt_tags = Some(vec!["test-tag1".into(), "test-tag2".into()]);
let mut tags = GitlabDriver::generate_tags(&recipe).unwrap();
tags.sort();
let mut expected_tags = vec![
format!("{timestamp}-40"),
"1234567-40".to_string(),
"40".to_string(),
];
expected_tags.extend(recipe.alt_tags.unwrap().iter().map(ToString::to_string));
expected_tags.sort();
assert_eq!(tags, expected_tags);
teardown();
}
#[test]
fn generate_tags_mr_branch() {
let _env = ENV_LOCK.lock().unwrap();
setup_mr_branch();
let mut tags = GitlabDriver::generate_tags(&create_test_recipe()).unwrap();
tags.sort();
let mut expected_tags = vec!["mr-12-40".to_string(), "1234567-40".to_string()];
expected_tags.sort();
assert_eq!(tags, expected_tags);
teardown();
}
#[test]
fn generate_tags_branch() {
let _env = ENV_LOCK.lock().unwrap();
setup_branch();
let mut tags = GitlabDriver::generate_tags(&create_test_recipe()).unwrap();
tags.sort();
let mut expected_tags = vec!["1234567-40".to_string(), "br-test-40".to_string()];
expected_tags.sort();
assert_eq!(tags, expected_tags);
teardown();
}
}

View file

@ -0,0 +1,43 @@
use log::trace;
use miette::bail;
use super::{CiDriver, Driver};
pub struct LocalDriver;
impl CiDriver for LocalDriver {
fn on_default_branch() -> bool {
trace!("LocalDriver::on_default_branch()");
false
}
fn keyless_cert_identity() -> miette::Result<String> {
trace!("LocalDriver::keyless_cert_identity()");
bail!("Keyless not supported");
}
fn oidc_provider() -> miette::Result<String> {
trace!("LocalDriver::oidc_provider()");
bail!("Keyless not supported");
}
fn generate_tags(recipe: &blue_build_recipe::Recipe) -> miette::Result<Vec<String>> {
trace!("LocalDriver::generate_tags({recipe:?})");
Ok(vec![format!("local-{}", Driver::get_os_version(recipe)?)])
}
fn generate_image_name(recipe: &blue_build_recipe::Recipe) -> miette::Result<String> {
trace!("LocalDriver::generate_image_name({recipe:?})");
Ok(recipe.name.trim().to_lowercase())
}
fn get_repo_url() -> miette::Result<String> {
trace!("LocalDriver::get_repo_url()");
Ok(String::new())
}
fn get_registry() -> miette::Result<String> {
trace!("LocalDriver::get_registry()");
Ok(String::new())
}
}

View file

@ -3,10 +3,12 @@ use clap::ValueEnum;
pub use build::*;
pub use inspect::*;
pub use run::*;
pub use signing::*;
mod build;
mod inspect;
mod run;
mod signing;
#[derive(Debug, Copy, Clone, Default, ValueEnum)]
pub enum CompressionType {

View file

@ -57,7 +57,7 @@ pub struct BuildTagPushOpts<'a> {
/// The list of tags for the image being built.
#[builder(default, setter(into))]
pub tags: Cow<'a, [&'a str]>,
pub tags: Cow<'a, [String]>,
/// Enable pushing the image.
#[builder(default)]

View file

@ -0,0 +1,82 @@
use std::borrow::Cow;
use typed_builder::TypedBuilder;
#[derive(Debug, Clone, TypedBuilder)]
pub struct RunOpts<'scope> {
#[builder(setter(into))]
pub image: Cow<'scope, str>,
#[builder(default, setter(into))]
pub args: Cow<'scope, [String]>,
#[builder(default, setter(into))]
pub env_vars: Cow<'scope, [RunOptsEnv<'scope>]>,
#[builder(default, setter(into))]
pub volumes: Cow<'scope, [RunOptsVolume<'scope>]>,
#[builder(default, setter(strip_option))]
pub uid: Option<u32>,
#[builder(default, setter(strip_option))]
pub gid: Option<u32>,
#[builder(default, setter(into))]
pub workdir: Cow<'scope, str>,
#[builder(default)]
pub privileged: bool,
#[builder(default)]
pub pull: bool,
#[builder(default)]
pub remove: bool,
}
#[derive(Debug, Clone, TypedBuilder)]
pub struct RunOptsVolume<'scope> {
#[builder(setter(into))]
pub path_or_vol_name: Cow<'scope, str>,
#[builder(setter(into))]
pub container_path: Cow<'scope, str>,
}
#[macro_export]
macro_rules! run_volumes {
($($host:expr => $container:expr),+ $(,)?) => {
{
[
$($crate::drivers::opts::RunOptsVolume::builder()
.path_or_vol_name($host)
.container_path($container)
.build(),)*
]
}
};
}
#[derive(Debug, Clone, TypedBuilder)]
pub struct RunOptsEnv<'scope> {
#[builder(setter(into))]
pub key: Cow<'scope, str>,
#[builder(setter(into))]
pub value: Cow<'scope, str>,
}
#[macro_export]
macro_rules! run_envs {
($($key:expr => $value:expr),+ $(,)?) => {
{
[
$($crate::drivers::opts::RunOptsEnv::builder()
.key($key)
.value($value)
.build(),)*
]
}
};
}

View file

@ -0,0 +1,115 @@
use std::{
borrow::Cow,
env, fs,
path::{Path, PathBuf},
};
use miette::{IntoDiagnostic, Result};
use typed_builder::TypedBuilder;
use zeroize::{Zeroize, Zeroizing};
pub enum PrivateKey {
Env(String),
Path(PathBuf),
}
pub trait PrivateKeyContents<T>
where
T: Zeroize,
{
/// Gets's the contents of the `PrivateKey`.
///
/// # Errors
/// Will error if the file or the environment couldn't be read.
fn contents(&self) -> Result<Zeroizing<T>>;
}
impl PrivateKeyContents<Vec<u8>> for PrivateKey {
fn contents(&self) -> Result<Zeroizing<Vec<u8>>> {
let key: Zeroizing<String> = self.contents()?;
Ok(Zeroizing::new(key.as_bytes().to_vec()))
}
}
impl PrivateKeyContents<String> for PrivateKey {
fn contents(&self) -> Result<Zeroizing<String>> {
Ok(Zeroizing::new(match *self {
Self::Env(ref env) => env::var(env).into_diagnostic()?,
Self::Path(ref path) => fs::read_to_string(path).into_diagnostic()?,
}))
}
}
impl std::fmt::Display for PrivateKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(
match *self {
Self::Env(ref env) => format!("env://{env}"),
Self::Path(ref path) => format!("{}", path.display()),
}
.as_str(),
)
}
}
#[derive(Debug, Clone, TypedBuilder)]
pub struct GenerateKeyPairOpts<'scope> {
#[builder(setter(into, strip_option))]
pub dir: Option<Cow<'scope, Path>>,
}
#[derive(Debug, Clone, TypedBuilder)]
pub struct CheckKeyPairOpts<'scope> {
#[builder(setter(into, strip_option))]
pub dir: Option<Cow<'scope, Path>>,
}
#[derive(Debug, Clone, TypedBuilder)]
pub struct SignOpts<'scope> {
#[builder(setter(into))]
pub image: Cow<'scope, str>,
#[builder(default, setter(into, strip_option))]
pub key: Option<Cow<'scope, str>>,
#[builder(default, setter(into, strip_option))]
pub dir: Option<Cow<'scope, Path>>,
}
#[derive(Debug, Clone)]
pub enum VerifyType<'scope> {
File(Cow<'scope, Path>),
Keyless {
issuer: Cow<'scope, str>,
identity: Cow<'scope, str>,
},
}
#[derive(Debug, Clone, TypedBuilder)]
pub struct VerifyOpts<'scope> {
#[builder(setter(into))]
pub image: Cow<'scope, str>,
pub verify_type: VerifyType<'scope>,
}
#[derive(Debug, Clone, TypedBuilder)]
pub struct SignVerifyOpts<'scope> {
#[builder(setter(into))]
pub image: Cow<'scope, str>,
#[builder(default, setter(into, strip_option))]
pub tag: Option<Cow<'scope, str>>,
#[builder(default, setter(into, strip_option))]
pub dir: Option<Cow<'scope, Path>>,
/// Enable retry logic for pushing.
#[builder(default)]
pub retry_push: bool,
/// Number of times to retry pushing.
///
/// Defaults to 1.
#[builder(default = 1)]
pub retry_count: u8,
}

View file

@ -1,28 +1,26 @@
use std::{
io::Write,
path::Path,
process::{Command, ExitStatus},
process::{Command, ExitStatus, Stdio},
time::Duration,
};
use blue_build_utils::{
constants::SKOPEO_IMAGE,
logging::{CommandLogging, Logger},
signal_handler::{add_cid, remove_cid, ContainerId},
};
use blue_build_utils::{cmd, constants::SKOPEO_IMAGE, credentials::Credentials, string_vec};
use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};
use log::{debug, error, info, trace, warn};
use miette::{bail, IntoDiagnostic, Result};
use miette::{bail, miette, IntoDiagnostic, Result};
use semver::Version;
use serde::Deserialize;
use tempdir::TempDir;
use crate::{
credentials::Credentials, drivers::types::RunDriverType, image_metadata::ImageMetadata,
drivers::image_metadata::ImageMetadata,
logging::{CommandLogging, Logger},
signal_handler::{add_cid, remove_cid, ContainerId, ContainerRuntime},
};
use super::{
credentials,
opts::{BuildOpts, GetMetadataOpts, PushOpts, RunOpts, TagOpts},
BuildDriver, DriverVersion, InspectDriver, RunDriver,
};
@ -51,10 +49,7 @@ impl DriverVersion for PodmanDriver {
trace!("PodmanDriver::version()");
trace!("podman version -f json");
let output = Command::new("podman")
.arg("version")
.arg("-f")
.arg("json")
let output = cmd!("podman", "version", "-f", "json")
.output()
.into_diagnostic()?;
@ -68,25 +63,22 @@ impl DriverVersion for PodmanDriver {
}
impl BuildDriver for PodmanDriver {
fn build(&self, opts: &BuildOpts) -> Result<()> {
fn build(opts: &BuildOpts) -> Result<()> {
trace!("PodmanDriver::build({opts:#?})");
trace!(
"podman build --pull=true --layers={} -f {} -t {} .",
!opts.squash,
opts.containerfile.display(),
opts.image,
let command = cmd!(
"podman",
"build",
"--pull=true",
format!("--layers={}", !opts.squash),
"-f",
&*opts.containerfile,
"-t",
&*opts.image,
".",
);
let mut command = Command::new("podman");
command
.arg("build")
.arg("--pull=true")
.arg(format!("--layers={}", !opts.squash))
.arg("-f")
.arg(opts.containerfile.as_ref())
.arg("-t")
.arg(opts.image.as_ref())
.arg(".");
trace!("{command:?}");
let status = command
.status_image_ref_progress(&opts.image, "Building Image")
.into_diagnostic()?;
@ -99,16 +91,13 @@ impl BuildDriver for PodmanDriver {
Ok(())
}
fn tag(&self, opts: &TagOpts) -> Result<()> {
fn tag(opts: &TagOpts) -> Result<()> {
trace!("PodmanDriver::tag({opts:#?})");
trace!("podman tag {} {}", opts.src_image, opts.dest_image);
let status = Command::new("podman")
.arg("tag")
.arg(opts.src_image.as_ref())
.arg(opts.dest_image.as_ref())
.status()
.into_diagnostic()?;
let mut command = cmd!("podman", "tag", &*opts.src_image, &*opts.dest_image,);
trace!("{command:?}");
let status = command.status().into_diagnostic()?;
if status.success() {
info!("Successfully tagged {}!", opts.dest_image);
@ -118,18 +107,20 @@ impl BuildDriver for PodmanDriver {
Ok(())
}
fn push(&self, opts: &PushOpts) -> Result<()> {
fn push(opts: &PushOpts) -> Result<()> {
trace!("PodmanDriver::push({opts:#?})");
trace!("podman push {}", opts.image);
let mut command = Command::new("podman");
command
.arg("push")
.arg(format!(
let command = cmd!(
"podman",
"push",
format!(
"--compression-format={}",
opts.compression_type.unwrap_or_default()
))
.arg(opts.image.as_ref());
),
&*opts.image,
);
trace!("{command:?}");
let status = command
.status_image_ref_progress(&opts.image, "Pushing Image")
.into_diagnostic()?;
@ -142,40 +133,57 @@ impl BuildDriver for PodmanDriver {
Ok(())
}
fn login(&self) -> Result<()> {
fn login() -> Result<()> {
trace!("PodmanDriver::login()");
if let Some(Credentials {
registry,
username,
password,
}) = credentials::get()
}) = Credentials::get()
{
trace!("podman login -u {username} -p [MASKED] {registry}");
let output = Command::new("podman")
.arg("login")
.arg("-u")
.arg(username)
.arg("-p")
.arg(password)
.arg(registry)
.output()
.into_diagnostic()?;
let mut command = cmd!(
"podman",
"login",
"-u",
username,
"--password-stdin",
registry
);
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
trace!("{command:?}");
let mut child = command.spawn().into_diagnostic()?;
write!(
child
.stdin
.as_mut()
.ok_or_else(|| miette!("Unable to open pipe to stdin"))?,
"{password}"
)
.into_diagnostic()?;
let output = child.wait_with_output().into_diagnostic()?;
if !output.status.success() {
let err_out = String::from_utf8_lossy(&output.stderr);
bail!("Failed to login for podman: {err_out}");
bail!("Failed to login for podman:\n{}", err_out.trim());
}
debug!("Logged into {registry}");
}
Ok(())
}
}
impl InspectDriver for PodmanDriver {
fn get_metadata(&self, opts: &GetMetadataOpts) -> Result<ImageMetadata> {
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
trace!("PodmanDriver::get_metadata({opts:#?})");
let url = opts.tag.as_ref().map_or_else(
let url = opts.tag.as_deref().map_or_else(
|| format!("docker://{}", opts.image),
|tag| format!("docker://{}:{tag}", opts.image),
);
@ -187,14 +195,14 @@ impl InspectDriver for PodmanDriver {
);
progress.enable_steady_tick(Duration::from_millis(100));
let output = self
.run_output(
&RunOpts::builder()
.image(SKOPEO_IMAGE)
.args(&["inspect".to_string(), url.clone()])
.build(),
)
.into_diagnostic()?;
let output = Self::run_output(
&RunOpts::builder()
.image(SKOPEO_IMAGE)
.args(string_vec!["inspect", url.clone()])
.remove(true)
.build(),
)
.into_diagnostic()?;
progress.finish();
Logger::multi_progress().remove(&progress);
@ -209,31 +217,31 @@ impl InspectDriver for PodmanDriver {
}
impl RunDriver for PodmanDriver {
fn run(&self, opts: &RunOpts) -> std::io::Result<ExitStatus> {
fn run(opts: &RunOpts) -> std::io::Result<ExitStatus> {
trace!("PodmanDriver::run({opts:#?})");
let cid_path = TempDir::new("podman")?;
let cid_file = cid_path.path().join("cid");
let cid = ContainerId::new(&cid_file, RunDriverType::Podman, opts.privileged);
let cid = ContainerId::new(&cid_file, ContainerRuntime::Podman, opts.privileged);
add_cid(&cid);
let status = podman_run(opts, &cid_file)
.status_image_ref_progress(opts.image.as_ref(), "Running container")?;
.status_image_ref_progress(&*opts.image, "Running container")?;
remove_cid(&cid);
Ok(status)
}
fn run_output(&self, opts: &RunOpts) -> std::io::Result<std::process::Output> {
fn run_output(opts: &RunOpts) -> std::io::Result<std::process::Output> {
trace!("PodmanDriver::run_output({opts:#?})");
let cid_path = TempDir::new("podman")?;
let cid_file = cid_path.path().join("cid");
let cid = ContainerId::new(&cid_file, RunDriverType::Podman, opts.privileged);
let cid = ContainerId::new(&cid_file, ContainerRuntime::Podman, opts.privileged);
add_cid(&cid);
@ -246,52 +254,31 @@ impl RunDriver for PodmanDriver {
}
fn podman_run(opts: &RunOpts, cid_file: &Path) -> Command {
let mut command = if opts.privileged {
warn!(
"Running 'podman' in privileged mode requires '{}'",
"sudo".bold().red()
);
Command::new("sudo")
} else {
Command::new("podman")
};
if opts.privileged {
command.arg("podman");
}
command
.arg("run")
.arg(format!("--cidfile={}", cid_file.display()));
if opts.privileged {
command.arg("--privileged");
}
if opts.remove {
command.arg("--rm");
}
if opts.pull {
command.arg("--pull=always");
}
opts.volumes.iter().for_each(|volume| {
command.arg("--volume");
command.arg(format!(
"{}:{}",
volume.path_or_vol_name, volume.container_path,
));
});
opts.env_vars.iter().for_each(|env| {
command.arg("--env");
command.arg(format!("{}={}", env.key, env.value));
});
command.arg(opts.image.as_ref());
command.args(opts.args.iter());
trace!("{command:?}");
command
cmd!(
if opts.privileged {
warn!(
"Running 'podman' in privileged mode requires '{}'",
"sudo".bold().red()
);
"sudo"
} else {
"podman"
},
if opts.privileged => "podman",
"run",
format!("--cidfile={}", cid_file.display()),
if opts.privileged => "--privileged",
if opts.remove => "--rm",
if opts.pull => "--pull=always",
for volume in opts.volumes => [
"--volume",
format!("{}:{}", volume.path_or_vol_name, volume.container_path),
],
for env in opts.env_vars => [
"--env",
format!("{}={}", env.key, env.value),
],
&*opts.image,
for opts.args,
)
}

View file

@ -0,0 +1,288 @@
use std::{fs, path::Path};
use crate::{
drivers::opts::{PrivateKeyContents, VerifyType},
RT,
};
use super::{
functions::get_private_key,
opts::{CheckKeyPairOpts, GenerateKeyPairOpts, SignOpts, VerifyOpts},
SigningDriver,
};
use blue_build_utils::{
constants::{COSIGN_PRIV_PATH, COSIGN_PUB_PATH},
credentials::Credentials,
};
use log::{debug, trace};
use miette::{bail, miette, Context, IntoDiagnostic};
use sigstore::{
cosign::{
constraint::PrivateKeySigner,
verification_constraint::{PublicKeyVerifier, VerificationConstraintVec},
ClientBuilder, Constraint, CosignCapabilities, SignatureLayer,
},
crypto::{signing_key::SigStoreKeyPair, SigningScheme},
errors::SigstoreVerifyConstraintsError,
registry::{Auth, OciReference},
};
use zeroize::Zeroizing;
pub struct SigstoreDriver;
impl SigningDriver for SigstoreDriver {
fn generate_key_pair(opts: &GenerateKeyPairOpts) -> miette::Result<()> {
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
let priv_key_path = path.join(COSIGN_PRIV_PATH);
let pub_key_path = path.join(COSIGN_PUB_PATH);
if priv_key_path.exists() {
bail!("Private key file already exists at {COSIGN_PRIV_PATH}");
} else if pub_key_path.exists() {
bail!("Public key file already exists at {COSIGN_PUB_PATH}");
}
let signer = SigningScheme::default()
.create_signer()
.into_diagnostic()
.context("Failed to create signer")?;
let keypair = signer
.to_sigstore_keypair()
.into_diagnostic()
.context("Failed to create key pair")?;
let priv_key = keypair
.private_key_to_encrypted_pem(b"")
.into_diagnostic()
.context("Failed to create encrypted private key")?;
let pub_key = keypair.public_key_to_pem().into_diagnostic()?;
fs::write(priv_key_path, priv_key)
.into_diagnostic()
.with_context(|| format!("Failed to write {COSIGN_PRIV_PATH}"))?;
fs::write(pub_key_path, pub_key)
.into_diagnostic()
.with_context(|| format!("Failed to write {COSIGN_PUB_PATH}"))?;
Ok(())
}
fn check_signing_files(opts: &CheckKeyPairOpts) -> miette::Result<()> {
trace!("SigstoreDriver::check_signing_files({opts:?})");
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
let pub_path = path.join(COSIGN_PUB_PATH);
let pub_key = fs::read_to_string(&pub_path)
.into_diagnostic()
.with_context(|| format!("Failed to open public key file {}", pub_path.display()))?;
debug!("Retrieved public key from {COSIGN_PUB_PATH}");
trace!("{pub_key}");
let key: Zeroizing<String> = get_private_key(path)
.context("Failed to get private key")?
.contents()?;
debug!("Retrieved private key");
let keypair = SigStoreKeyPair::from_encrypted_pem(key.as_bytes(), b"")
.into_diagnostic()
.context("Failed to generate key pair from private key")?;
let gen_pub = keypair
.public_key_to_pem()
.into_diagnostic()
.context("Failed to generate public key from private key")?;
debug!("Generated public key from private key");
trace!("{gen_pub}");
if pub_key.trim() == gen_pub.trim() {
debug!("Public and private key matches");
Ok(())
} else {
bail!("Private and public keys do not match.")
}
}
fn sign(opts: &SignOpts) -> miette::Result<()> {
trace!("SigstoreDriver::sign({opts:?})");
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
let mut client = ClientBuilder::default().build().into_diagnostic()?;
let image_digest: &str = opts.image.as_ref();
let image_digest: OciReference = image_digest.parse().into_diagnostic()?;
trace!("{image_digest:?}");
let signing_scheme = SigningScheme::default();
let key: Zeroizing<Vec<u8>> = get_private_key(path)?.contents()?;
debug!("Retrieved private key");
let signer = PrivateKeySigner::new_with_signer(
SigStoreKeyPair::from_encrypted_pem(&key, b"")
.into_diagnostic()?
.to_sigstore_signer(&signing_scheme)
.into_diagnostic()?,
);
debug!("Created signer");
let Credentials {
registry: _,
username,
password,
} = Credentials::get().ok_or_else(|| miette!("Credentials are required for signing"))?;
let auth = Auth::Basic(username.clone(), password.clone());
debug!("Credentials retrieved");
let (cosign_signature_image, source_image_digest) = RT
.block_on(client.triangulate(&image_digest, &auth))
.into_diagnostic()
.with_context(|| format!("Failed to triangulate image {image_digest}"))?;
debug!("Triangulating image");
trace!("{cosign_signature_image}, {source_image_digest}");
let mut signature_layer =
SignatureLayer::new_unsigned(&image_digest, &source_image_digest).into_diagnostic()?;
signer
.add_constraint(&mut signature_layer)
.into_diagnostic()?;
debug!("Created signing layer");
debug!("Pushing signature");
RT.block_on(client.push_signature(
None,
&auth,
&cosign_signature_image,
vec![signature_layer],
))
.into_diagnostic()
.with_context(|| {
format!("Failed to push signature {cosign_signature_image} for image {image_digest}")
})?;
debug!("Successfully pushed signature");
Ok(())
}
fn verify(opts: &VerifyOpts) -> miette::Result<()> {
let mut client = ClientBuilder::default().build().into_diagnostic()?;
let image_digest: &str = opts.image.as_ref();
let image_digest: OciReference = image_digest.parse().into_diagnostic()?;
trace!("{image_digest:?}");
let signing_scheme = SigningScheme::default();
let pub_key = fs::read_to_string(match &opts.verify_type {
VerifyType::File(path) => path,
VerifyType::Keyless { .. } => {
todo!("Keyless currently not supported for sigstore driver")
}
})
.into_diagnostic()
.with_context(|| format!("Failed to open public key file {COSIGN_PUB_PATH}"))?;
debug!("Retrieved public key from {COSIGN_PUB_PATH}");
trace!("{pub_key}");
let verifier =
PublicKeyVerifier::new(pub_key.as_bytes(), &signing_scheme).into_diagnostic()?;
let verification_constraints: VerificationConstraintVec = vec![Box::new(verifier)];
let auth = Auth::Anonymous;
let (cosign_signature_image, source_image_digest) = RT
.block_on(client.triangulate(&image_digest, &auth))
.into_diagnostic()
.with_context(|| format!("Failed to triangulate image {image_digest}"))?;
debug!("Triangulating image");
trace!("{cosign_signature_image}, {source_image_digest}");
let trusted_layers = RT
.block_on(client.trusted_signature_layers(
&auth,
&source_image_digest,
&cosign_signature_image,
))
.into_diagnostic()?;
sigstore::cosign::verify_constraints(&trusted_layers, verification_constraints.iter())
.map_err(
|SigstoreVerifyConstraintsError {
unsatisfied_constraints,
}| {
miette!("Failed to verify for constraints: {unsatisfied_constraints:?}")
},
)
}
fn signing_login() -> miette::Result<()> {
Ok(())
}
}
#[cfg(test)]
mod test {
use std::{fs, path::Path};
use blue_build_utils::constants::{COSIGN_PRIV_PATH, COSIGN_PUB_PATH};
use tempdir::TempDir;
use crate::drivers::{
cosign_driver::CosignDriver,
opts::{CheckKeyPairOpts, GenerateKeyPairOpts},
SigningDriver,
};
use super::SigstoreDriver;
#[test]
fn generate_key_pair() {
let tempdir = TempDir::new("keypair").unwrap();
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
SigstoreDriver::generate_key_pair(&gen_opts).unwrap();
eprintln!(
"Private key:\n{}",
fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
);
eprintln!(
"Public key:\n{}",
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
);
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
SigstoreDriver::check_signing_files(&check_opts).unwrap();
}
#[test]
fn check_key_pairs() {
let path = Path::new("../test-files/keys");
let opts = CheckKeyPairOpts::builder().dir(path).build();
SigstoreDriver::check_signing_files(&opts).unwrap();
}
#[test]
fn compatibility() {
let tempdir = TempDir::new("keypair").unwrap();
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
SigstoreDriver::generate_key_pair(&gen_opts).unwrap();
eprintln!(
"Private key:\n{}",
fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
);
eprintln!(
"Public key:\n{}",
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
);
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
CosignDriver::check_signing_files(&check_opts).unwrap();
}
}

View file

@ -1,22 +1,19 @@
use std::{
process::{Command, Stdio},
time::Duration,
};
use std::{process::Stdio, time::Duration};
use blue_build_utils::logging::Logger;
use blue_build_utils::cmd;
use indicatif::{ProgressBar, ProgressStyle};
use log::{debug, trace};
use miette::{bail, IntoDiagnostic, Result};
use crate::image_metadata::ImageMetadata;
use crate::logging::Logger;
use super::{opts::GetMetadataOpts, InspectDriver};
use super::{image_metadata::ImageMetadata, opts::GetMetadataOpts, InspectDriver};
#[derive(Debug)]
pub struct SkopeoDriver;
impl InspectDriver for SkopeoDriver {
fn get_metadata(&self, opts: &GetMetadataOpts) -> Result<ImageMetadata> {
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
trace!("SkopeoDriver::get_metadata({opts:#?})");
let url = opts.tag.as_ref().map_or_else(
@ -32,9 +29,7 @@ impl InspectDriver for SkopeoDriver {
progress.enable_steady_tick(Duration::from_millis(100));
trace!("skopeo inspect {url}");
let output = Command::new("skopeo")
.arg("inspect")
.arg(&url)
let output = cmd!("skopeo", "inspect", &url)
.stderr(Stdio::inherit())
.output()
.into_diagnostic()?;

335
process/drivers/traits.rs Normal file
View file

@ -0,0 +1,335 @@
use std::{
path::PathBuf,
process::{ExitStatus, Output},
};
use blue_build_recipe::Recipe;
use blue_build_utils::{constants::COSIGN_PUB_PATH, retry};
use log::{debug, info, trace};
use miette::{bail, miette, Result};
use semver::{Version, VersionReq};
use crate::drivers::{functions::get_private_key, types::CiDriverType, Driver};
use super::{
image_metadata::ImageMetadata,
opts::{
BuildOpts, BuildTagPushOpts, CheckKeyPairOpts, GenerateKeyPairOpts, GetMetadataOpts,
PushOpts, RunOpts, SignOpts, SignVerifyOpts, TagOpts, VerifyOpts, VerifyType,
},
};
/// Trait for retrieving version of a driver.
pub trait DriverVersion {
/// The version req string slice that follows
/// the semver standard <https://semver.org/>.
const VERSION_REQ: &'static str;
/// Returns the version of the driver.
///
/// # Errors
/// Will error if it can't retrieve the version.
fn version() -> Result<Version>;
#[must_use]
fn is_supported_version() -> bool {
Self::version().is_ok_and(|version| {
VersionReq::parse(Self::VERSION_REQ).is_ok_and(|req| req.matches(&version))
})
}
}
/// Allows agnostic building, tagging
/// pushing, and login.
pub trait BuildDriver {
/// Runs the build logic for the driver.
///
/// # Errors
/// Will error if the build fails.
fn build(opts: &BuildOpts) -> Result<()>;
/// Runs the tag logic for the driver.
///
/// # Errors
/// Will error if the tagging fails.
fn tag(opts: &TagOpts) -> Result<()>;
/// Runs the push logic for the driver
///
/// # Errors
/// Will error if the push fails.
fn push(opts: &PushOpts) -> Result<()>;
/// Runs the login logic for the driver.
///
/// # Errors
/// Will error if login fails.
fn login() -> Result<()>;
/// Runs the logic for building, tagging, and pushing an image.
///
/// # Errors
/// Will error if building, tagging, or pusing fails.
fn build_tag_push(opts: &BuildTagPushOpts) -> Result<()> {
trace!("BuildDriver::build_tag_push({opts:#?})");
let full_image = match (opts.archive_path.as_ref(), opts.image.as_ref()) {
(Some(archive_path), None) => {
format!("oci-archive:{archive_path}")
}
(None, Some(image)) => opts
.tags
.first()
.map_or_else(|| image.to_string(), |tag| format!("{image}:{tag}")),
(Some(_), Some(_)) => bail!("Cannot use both image and archive path"),
(None, None) => bail!("Need either the image or archive path set"),
};
let build_opts = BuildOpts::builder()
.image(&full_image)
.containerfile(opts.containerfile.as_ref())
.squash(opts.squash)
.build();
info!("Building image {full_image}");
Self::build(&build_opts)?;
if !opts.tags.is_empty() && opts.archive_path.is_none() {
let image = opts
.image
.as_ref()
.ok_or_else(|| miette!("Image is required in order to tag"))?;
debug!("Tagging all images");
for tag in opts.tags.as_ref() {
debug!("Tagging {} with {tag}", &full_image);
let tag_opts = TagOpts::builder()
.src_image(&full_image)
.dest_image(format!("{image}:{tag}"))
.build();
Self::tag(&tag_opts)?;
if opts.push {
let retry_count = if opts.retry_push { opts.retry_count } else { 0 };
debug!("Pushing all images");
// Push images with retries (1s delay between retries)
blue_build_utils::retry(retry_count, 5, || {
let tag_image = format!("{image}:{tag}");
debug!("Pushing image {tag_image}");
let push_opts = PushOpts::builder()
.image(&tag_image)
.compression_type(opts.compression)
.build();
Self::push(&push_opts)
})?;
}
}
}
Ok(())
}
}
/// Allows agnostic inspection of images.
pub trait InspectDriver {
/// Gets the metadata on an image tag.
///
/// # Errors
/// Will error if it is unable to get the labels.
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata>;
}
pub trait RunDriver: Sync + Send {
/// Run a container to perform an action.
///
/// # Errors
/// Will error if there is an issue running the container.
fn run(opts: &RunOpts) -> std::io::Result<ExitStatus>;
/// Run a container to perform an action and capturing output.
///
/// # Errors
/// Will error if there is an issue running the container.
fn run_output(opts: &RunOpts) -> std::io::Result<Output>;
}
pub trait SigningDriver {
/// Generate a new private/public key pair.
///
/// # Errors
/// Will error if a key-pair couldn't be generated.
fn generate_key_pair(opts: &GenerateKeyPairOpts) -> Result<()>;
/// Checks the signing key files to ensure
/// they match.
///
/// # Errors
/// Will error if the files cannot be verified.
fn check_signing_files(opts: &CheckKeyPairOpts) -> Result<()>;
/// Signs the image digest.
///
/// # Errors
/// Will error if signing fails.
fn sign(opts: &SignOpts) -> Result<()>;
/// Verifies the image.
///
/// The image can be verified either with `VerifyType::File` containing
/// the public key contents, or with `VerifyType::Keyless` containing
/// information about the `issuer` and `identity`.
///
/// # Errors
/// Will error if the image fails to be verified.
fn verify(opts: &VerifyOpts) -> Result<()>;
/// Sign an image given the image name and tag.
///
/// # Errors
/// Will error if the image fails to be signed.
fn sign_and_verify(opts: &SignVerifyOpts) -> Result<()> {
trace!("sign_and_verify({opts:?})");
let path = opts
.dir
.as_ref()
.map_or_else(|| PathBuf::from("."), |d| d.to_path_buf());
let image_name: &str = opts.image.as_ref();
let inspect_opts = GetMetadataOpts::builder().image(image_name);
let inspect_opts = if let Some(ref tag) = opts.tag {
inspect_opts.tag(tag.as_ref() as &str).build()
} else {
inspect_opts.build()
};
let image_digest = Driver::get_metadata(&inspect_opts)?.digest;
let image_name_tag = opts
.tag
.as_ref()
.map_or_else(|| image_name.to_owned(), |t| format!("{image_name}:{t}"));
let image_digest = format!("{image_name}@{image_digest}");
let (sign_opts, verify_opts) = match (Driver::get_ci_driver(), get_private_key(&path)) {
// Cosign public/private key pair
(_, Ok(priv_key)) => (
SignOpts::builder()
.image(&image_digest)
.dir(&path)
.key(priv_key.to_string())
.build(),
VerifyOpts::builder()
.image(&image_name_tag)
.verify_type(VerifyType::File(path.join(COSIGN_PUB_PATH).into()))
.build(),
),
// Gitlab keyless
(CiDriverType::Github | CiDriverType::Gitlab, _) => (
SignOpts::builder().dir(&path).image(&image_digest).build(),
VerifyOpts::builder()
.image(&image_name_tag)
.verify_type(VerifyType::Keyless {
issuer: Driver::oidc_provider()?.into(),
identity: Driver::keyless_cert_identity()?.into(),
})
.build(),
),
_ => bail!("Failed to get information for signing the image"),
};
let retry_count = if opts.retry_push { opts.retry_count } else { 0 };
retry(retry_count, 5, || {
Self::sign(&sign_opts)?;
Self::verify(&verify_opts)
})?;
Ok(())
}
/// Runs the login logic for the signing driver.
///
/// # Errors
/// Will error if login fails.
fn signing_login() -> Result<()>;
}
/// Allows agnostic retrieval of CI-based information.
pub trait CiDriver {
/// Determines if we're on the main branch of
/// a repository.
fn on_default_branch() -> bool;
/// Retrieve the certificate identity for
/// keyless signing.
///
/// # Errors
/// Will error if the environment variables aren't set.
fn keyless_cert_identity() -> Result<String>;
/// Retrieve the OIDC Provider for keyless signing.
///
/// # Errors
/// Will error if the environment variables aren't set.
fn oidc_provider() -> Result<String>;
/// Generate a list of tags based on the OS version.
///
/// ## CI
/// The tags are generated based on the CI system that
/// is detected. The general format for the default branch is:
/// - `${os_version}`
/// - `${timestamp}-${os_version}`
///
/// On a branch:
/// - `br-${branch_name}-${os_version}`
///
/// In a PR(GitHub)/MR(GitLab)
/// - `pr-${pr_event_number}-${os_version}`/`mr-${mr_iid}-${os_version}`
///
/// In all above cases the short git sha is also added:
/// - `${commit_sha}-${os_version}`
///
/// When `alt_tags` are not present, the following tags are added:
/// - `latest`
/// - `${timestamp}`
///
/// ## Locally
/// When ran locally, only a local tag is created:
/// - `local-${os_version}`
///
/// # Errors
/// Will error if the environment variables aren't set.
fn generate_tags(recipe: &Recipe) -> Result<Vec<String>>;
/// Generates the image name based on CI.
///
/// # Errors
/// Will error if the environment variables aren't set.
fn generate_image_name(recipe: &Recipe) -> Result<String> {
Ok(format!(
"{}/{}",
Self::get_registry()?,
recipe.name.trim().to_lowercase()
))
}
/// Get the URL for the repository.
///
/// # Errors
/// Will error if the environment variables aren't set.
fn get_repo_url() -> Result<String>;
/// Get the registry ref for the image.
///
/// # Errors
/// Will error if the environment variables aren't set.
fn get_registry() -> Result<String>;
}

169
process/drivers/types.rs Normal file
View file

@ -0,0 +1,169 @@
use std::env;
use blue_build_utils::constants::{GITHUB_ACTIONS, GITLAB_CI};
use clap::ValueEnum;
use log::trace;
use crate::drivers::{
buildah_driver::BuildahDriver, docker_driver::DockerDriver, podman_driver::PodmanDriver,
DriverVersion,
};
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() => {
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 {}, ", 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,
#[cfg(feature = "sigstore")]
Sigstore,
}
impl DetermineDriver<SigningDriverType> for Option<SigningDriverType> {
fn determine_driver(&mut self) -> SigningDriverType {
trace!("SigningDriverType::determine_signing_driver()");
#[cfg(feature = "sigstore")]
{
*self.get_or_insert(
blue_build_utils::check_command_exists("cosign")
.map_or(SigningDriverType::Sigstore, |()| SigningDriverType::Cosign),
)
}
#[cfg(not(feature = "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 (env::var(GITLAB_CI).ok(), env::var(GITHUB_ACTIONS).ok()) {
(Some(_gitlab_ci), None) => CiDriverType::Gitlab,
(None, Some(_github_actions)) => CiDriverType::Github,
_ => CiDriverType::Local,
},
)
}
}

57
process/process.rs Normal file
View file

@ -0,0 +1,57 @@
//! This module is responsible for managing processes spawned
//! by this tool. It contains drivers for running, building, inspecting, and signing
//! images that interface with tools like docker or podman.
#[cfg(feature = "sigstore")]
use once_cell::sync::Lazy;
#[cfg(feature = "sigstore")]
use tokio::runtime::Runtime;
pub mod drivers;
pub mod logging;
pub mod signal_handler;
#[cfg(feature = "sigstore")]
pub(crate) static RT: Lazy<Runtime> = Lazy::new(|| {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
});
#[cfg(test)]
pub(crate) mod test {
use std::sync::Mutex;
use blue_build_recipe::{Module, ModuleExt, Recipe};
use indexmap::IndexMap;
use once_cell::sync::Lazy;
pub const BB_UNIT_TEST_MOCK_GET_OS_VERSION: &str = "BB_UNIT_TEST_MOCK_GET_OS_VERSION";
/// This mutex is used for tests that require the reading of
/// environment variables. Env vars are an inheritly unsafe
/// as they can be changed and affect other threads functionality.
///
/// For tests that require setting env vars, they need to lock this
/// mutex before making changes to the env. Any changes made to the env
/// MUST be undone in the same test before dropping the lock. Failure to
/// do so will cause unpredictable behavior with other tests.
pub static ENV_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
pub fn create_test_recipe() -> Recipe<'static> {
Recipe::builder()
.name("test")
.description("This is a test")
.base_image("base-image")
.image_version("40")
.modules_ext(
ModuleExt::builder()
.modules(vec![Module::builder().build()])
.build(),
)
.stages_ext(None)
.extra(IndexMap::new())
.build()
}
}

View file

@ -1,11 +1,11 @@
use std::{
fs,
path::PathBuf,
process::{self, Command},
sync::{atomic::AtomicBool, Arc, Mutex},
thread,
};
use blue_build_utils::cmd;
use log::{debug, error, trace, warn};
use nix::{
libc::{SIGABRT, SIGCONT, SIGHUP, SIGTSTP},
@ -26,21 +26,34 @@ use crate::logging::Logger;
pub struct ContainerId {
cid_path: PathBuf,
requires_sudo: bool,
crt: String,
container_runtime: ContainerRuntime,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContainerRuntime {
Podman,
Docker,
}
impl std::fmt::Display for ContainerRuntime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match *self {
Self::Podman => "podman",
Self::Docker => "docker",
})
}
}
impl ContainerId {
pub fn new<P, S>(cid_path: P, container_runtime: S, requires_sudo: bool) -> Self
pub fn new<P>(cid_path: P, container_runtime: ContainerRuntime, requires_sudo: bool) -> Self
where
P: Into<PathBuf>,
S: Into<String>,
{
let cid_path = cid_path.into();
let crt = container_runtime.into();
Self {
cid_path,
requires_sudo,
crt,
container_runtime,
}
}
}
@ -97,13 +110,9 @@ where
debug!("Killing container {id}");
let status = if cid.requires_sudo {
Command::new("sudo")
.arg(&cid.crt)
.arg("stop")
.arg(id)
.status()
cmd!("sudo", cid.container_runtime.to_string(), "stop", id).status()
} else {
Command::new(&cid.crt).arg("stop").arg(id).status()
cmd!(cid.container_runtime.to_string(), "stop", id).status()
};
if let Err(e) = status {
@ -113,7 +122,7 @@ where
});
drop(cid_list);
process::exit(1);
expect_exit::exit_unwind(1);
}
SIGTSTP => {
if has_terminal {

View file

@ -11,7 +11,6 @@ license.workspace = true
[dependencies]
blue-build-utils = { version = "=0.8.12", path = "../utils" }
chrono.workspace = true
colored.workspace = true
log.workspace = true
miette.workspace = true

View file

@ -1,12 +1,7 @@
use std::{borrow::Cow, env, fs, path::Path};
use std::{borrow::Cow, fs, path::Path};
use blue_build_utils::constants::{
CI_COMMIT_REF_NAME, CI_COMMIT_SHORT_SHA, CI_DEFAULT_BRANCH, CI_MERGE_REQUEST_IID,
CI_PIPELINE_SOURCE, GITHUB_EVENT_NAME, GITHUB_REF_NAME, GITHUB_SHA, PR_EVENT_NUMBER,
};
use chrono::Local;
use indexmap::IndexMap;
use log::{debug, trace, warn};
use log::{debug, trace};
use miette::{Context, IntoDiagnostic, Result};
use serde::{Deserialize, Serialize};
use serde_yaml::Value;
@ -83,116 +78,6 @@ pub struct Recipe<'a> {
}
impl<'a> Recipe<'a> {
/// Generate a list of tags based on the OS version.
///
/// ## CI
/// The tags are generated based on the CI system that
/// is detected. The general format for the default branch is:
/// - `${os_version}`
/// - `${timestamp}-${os_version}`
///
/// On a branch:
/// - `br-${branch_name}-${os_version}`
///
/// In a PR(GitHub)/MR(GitLab)
/// - `pr-${pr_event_number}-${os_version}`/`mr-${mr_iid}-${os_version}`
///
/// In all above cases the short git sha is also added:
/// - `${commit_sha}-${os_version}`
///
/// When `alt_tags` are not present, the following tags are added:
/// - `latest`
/// - `${timestamp}`
///
/// ## Locally
/// When ran locally, only a local tag is created:
/// - `local-${os_version}`
#[must_use]
pub fn generate_tags(&self, os_version: u64) -> Vec<String> {
trace!("Recipe::generate_tags()");
trace!("Generating image tags for {}", &self.name);
let mut tags: Vec<String> = Vec::new();
let timestamp = Local::now().format("%Y%m%d").to_string();
if let (Ok(commit_branch), Ok(default_branch), Ok(commit_sha), Ok(pipeline_source)) = (
env::var(CI_COMMIT_REF_NAME),
env::var(CI_DEFAULT_BRANCH),
env::var(CI_COMMIT_SHORT_SHA),
env::var(CI_PIPELINE_SOURCE),
) {
trace!("CI_COMMIT_REF_NAME={commit_branch}, CI_DEFAULT_BRANCH={default_branch},CI_COMMIT_SHORT_SHA={commit_sha}, CI_PIPELINE_SOURCE={pipeline_source}");
warn!("Detected running in Gitlab, pulling information from CI variables");
if let Ok(mr_iid) = env::var(CI_MERGE_REQUEST_IID) {
trace!("CI_MERGE_REQUEST_IID={mr_iid}");
if pipeline_source == "merge_request_event" {
debug!("Running in a MR");
tags.push(format!("mr-{mr_iid}-{os_version}"));
}
}
if default_branch == commit_branch {
debug!("Running on the default branch");
tags.push(os_version.to_string());
tags.push(format!("{timestamp}-{os_version}"));
if let Some(alt_tags) = self.alt_tags.as_ref() {
tags.extend(alt_tags.iter().map(ToString::to_string));
} else {
tags.push("latest".into());
tags.push(timestamp);
}
} else {
debug!("Running on branch {commit_branch}");
tags.push(format!("br-{commit_branch}-{os_version}"));
}
tags.push(format!("{commit_sha}-{os_version}"));
} else if let (
Ok(github_event_name),
Ok(github_event_number),
Ok(github_sha),
Ok(github_ref_name),
) = (
env::var(GITHUB_EVENT_NAME),
env::var(PR_EVENT_NUMBER),
env::var(GITHUB_SHA),
env::var(GITHUB_REF_NAME),
) {
trace!("GITHUB_EVENT_NAME={github_event_name},PR_EVENT_NUMBER={github_event_number},GITHUB_SHA={github_sha},GITHUB_REF_NAME={github_ref_name}");
warn!("Detected running in Github, pulling information from GITHUB variables");
let mut short_sha = github_sha;
short_sha.truncate(7);
if github_event_name == "pull_request" {
debug!("Running in a PR");
tags.push(format!("pr-{github_event_number}-{os_version}"));
} else if github_ref_name == "live" || github_ref_name == "main" {
tags.push(os_version.to_string());
tags.push(format!("{timestamp}-{os_version}"));
if let Some(alt_tags) = self.alt_tags.as_ref() {
tags.extend(alt_tags.iter().map(ToString::to_string));
} else {
tags.push("latest".into());
tags.push(timestamp);
}
} else {
tags.push(format!("br-{github_ref_name}-{os_version}"));
}
tags.push(format!("{short_sha}-{os_version}"));
} else {
warn!("Running locally");
tags.push(format!("local-{os_version}"));
}
debug!("Finished generating tags!");
debug!("Tags: {tags:#?}");
tags.into_iter().map(|t| t.replace('/', "_")).collect()
}
/// Parse a recipe file
///
/// # Errors

View file

@ -1,5 +1,5 @@
use blue_build::commands::{BlueBuildArgs, BlueBuildCommand, CommandArgs};
use blue_build_utils::{logging::Logger, signal_handler};
use blue_build_process_management::{logging::Logger, signal_handler};
use clap::Parser;
use log::LevelFilter;
@ -8,7 +8,11 @@ fn main() {
Logger::new()
.filter_level(args.verbosity.log_level_filter())
.filter_modules([("hyper::proto", LevelFilter::Info)])
.filter_modules([
("hyper::proto", LevelFilter::Off),
("hyper_util", LevelFilter::Off),
("oci_distribution", LevelFilter::Off),
])
.log_out_dir(args.log_out.clone())
.init();
log::trace!("Parsed arguments: {args:#?}");
@ -32,6 +36,9 @@ fn main() {
#[cfg(not(feature = "switch"))]
CommandArgs::Upgrade(mut command) => command.run(),
#[cfg(feature = "login")]
CommandArgs::Login(mut command) => command.run(),
CommandArgs::BugReport(mut command) => command.run(),
CommandArgs::Completions(mut command) => command.run(),

View file

@ -2,19 +2,17 @@ use std::path::PathBuf;
use log::error;
use clap::{command, crate_authors, Args, Parser, Subcommand};
use clap::{command, crate_authors, Parser, Subcommand};
use clap_verbosity_flag::{InfoLevel, Verbosity};
use typed_builder::TypedBuilder;
use crate::{
drivers::types::{BuildDriverType, InspectDriverType},
shadow,
};
use crate::shadow;
pub mod bug_report;
pub mod build;
pub mod completions;
pub mod generate;
#[cfg(feature = "login")]
pub mod login;
// #[cfg(feature = "init")]
// pub mod init;
#[cfg(not(feature = "switch"))]
@ -107,6 +105,10 @@ pub enum CommandArgs {
#[cfg(feature = "switch")]
Switch(switch::SwitchCommand),
/// Login to all services used for building.
#[cfg(feature = "login")]
Login(login::LoginCommand),
// /// Initialize a new Ublue Starting Point repo
// #[cfg(feature = "init")]
// Init(init::InitCommand),
@ -120,21 +122,6 @@ pub enum CommandArgs {
Completions(completions::CompletionsCommand),
}
#[derive(Default, Clone, Copy, Debug, TypedBuilder, Args)]
pub struct DriverArgs {
/// Select which driver to use to build
/// your image.
#[builder(default)]
#[arg(short = 'B', long)]
build_driver: Option<BuildDriverType>,
/// Select which driver to use to inspect
/// images.
#[builder(default)]
#[arg(short = 'I', long)]
inspect_driver: Option<InspectDriverType>,
}
#[cfg(test)]
mod test {
use clap::CommandFactory;

View file

@ -1,20 +1,19 @@
use std::{
env, fs,
fs,
path::{Path, PathBuf},
process::Command,
};
use blue_build_process_management::drivers::{
opts::{BuildTagPushOpts, CheckKeyPairOpts, CompressionType},
BuildDriver, CiDriver, Driver, DriverArgs, SigningDriver,
};
use blue_build_recipe::Recipe;
use blue_build_utils::{
constants::{
ARCHIVE_SUFFIX, BB_PASSWORD, BB_REGISTRY, BB_REGISTRY_NAMESPACE, BB_USERNAME,
BUILD_ID_LABEL, CI_DEFAULT_BRANCH, CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_PROJECT_URL,
CI_REGISTRY, CI_SERVER_HOST, CI_SERVER_PROTOCOL, CONFIG_PATH, CONTAINER_FILE,
COSIGN_PRIVATE_KEY, COSIGN_PRIV_PATH, COSIGN_PUB_PATH, GITHUB_REPOSITORY_OWNER,
GITHUB_TOKEN, GITHUB_TOKEN_ISSUER_URL, GITHUB_WORKFLOW_REF, GITIGNORE_PATH,
LABELED_ERROR_MESSAGE, NO_LABEL_ERROR_MESSAGE, RECIPE_FILE, RECIPE_PATH, SIGSTORE_ID_TOKEN,
ARCHIVE_SUFFIX, BB_REGISTRY_NAMESPACE, BUILD_ID_LABEL, CONFIG_PATH, CONTAINER_FILE,
GITIGNORE_PATH, LABELED_ERROR_MESSAGE, NO_LABEL_ERROR_MESSAGE, RECIPE_FILE, RECIPE_PATH,
},
generate_containerfile_path,
credentials::{Credentials, CredentialsArgs},
};
use clap::Args;
use colored::Colorize;
@ -22,16 +21,9 @@ use log::{debug, info, trace, warn};
use miette::{bail, Context, IntoDiagnostic, Result};
use typed_builder::TypedBuilder;
use crate::{
commands::generate::GenerateCommand,
credentials::{self, Credentials},
drivers::{
opts::{BuildTagPushOpts, CompressionType, GetMetadataOpts},
Driver,
},
};
use crate::commands::generate::GenerateCommand;
use super::{BlueBuildCommand, DriverArgs};
use super::BlueBuildCommand;
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Args, TypedBuilder)]
@ -88,11 +80,6 @@ pub struct BuildCommand {
#[builder(default, setter(into, strip_option))]
archive: Option<PathBuf>,
/// The registry's domain name.
#[arg(long, env = BB_REGISTRY)]
#[builder(default, setter(into, strip_option))]
registry: Option<String>,
/// The url path to your base
/// project images.
#[arg(long, env = BB_REGISTRY_NAMESPACE)]
@ -100,18 +87,6 @@ pub struct BuildCommand {
#[arg(visible_alias("registry-path"))]
registry_namespace: Option<String>,
/// The username to login to the
/// container registry.
#[arg(short = 'U', long, env = BB_USERNAME, hide_env_values = true)]
#[builder(default, setter(into, strip_option))]
username: Option<String>,
/// The password to login to the
/// container registry.
#[arg(short = 'P', long, env = BB_PASSWORD, hide_env_values = true)]
#[builder(default, setter(into, strip_option))]
password: Option<String>,
/// Do not sign the image on push.
#[arg(long)]
#[builder(default)]
@ -128,6 +103,10 @@ pub struct BuildCommand {
#[builder(default)]
squash: bool,
#[clap(flatten)]
#[builder(default)]
credentials: CredentialsArgs,
#[clap(flatten)]
#[builder(default)]
drivers: DriverArgs,
@ -138,14 +117,9 @@ impl BlueBuildCommand for BuildCommand {
fn try_run(&mut self) -> Result<()> {
trace!("BuildCommand::try_run()");
Driver::builder()
.username(self.username.as_ref())
.password(self.password.as_ref())
.registry(self.registry.as_ref())
.build_driver(self.drivers.build_driver)
.inspect_driver(self.drivers.inspect_driver)
.build()
.init();
Driver::init(self.drivers);
Credentials::init(self.credentials.clone());
self.update_gitignore()?;
@ -155,8 +129,9 @@ impl BlueBuildCommand for BuildCommand {
if self.push {
blue_build_utils::check_command_exists("cosign")?;
self.check_cosign_files()?;
Self::login()?;
Driver::check_signing_files(&CheckKeyPairOpts::builder().dir(Path::new(".")).build())?;
Driver::login()?;
Driver::signing_login()?;
}
#[cfg(feature = "multi-recipe")]
@ -180,7 +155,11 @@ impl BlueBuildCommand for BuildCommand {
recipe_paths.par_iter().try_for_each(|recipe| {
GenerateCommand::builder()
.output(generate_containerfile_path(recipe)?)
.output(if recipe_paths.len() > 1 {
blue_build_utils::generate_containerfile_path(recipe)?
} else {
PathBuf::from(CONTAINER_FILE)
})
.recipe(recipe)
.drivers(self.drivers)
.build()
@ -204,7 +183,7 @@ impl BlueBuildCommand for BuildCommand {
});
GenerateCommand::builder()
.output(generate_containerfile_path(&recipe_path)?)
.output(CONTAINER_FILE)
.recipe(&recipe_path)
.drivers(self.drivers)
.build()
@ -218,16 +197,21 @@ impl BlueBuildCommand for BuildCommand {
impl BuildCommand {
#[cfg(feature = "multi-recipe")]
fn start(&self, recipe_paths: &[PathBuf]) -> Result<()> {
use blue_build_process_management::drivers::opts::SignVerifyOpts;
use rayon::prelude::*;
trace!("BuildCommand::build_image()");
recipe_paths
.par_iter()
.try_for_each(|recipe_path| -> Result<()> {
let recipe = Recipe::parse(recipe_path)?;
let os_version = Driver::get_os_version(&recipe)?;
let containerfile = generate_containerfile_path(recipe_path)?;
let tags = recipe.generate_tags(os_version);
let containerfile = if recipe_paths.len() > 1 {
blue_build_utils::generate_containerfile_path(recipe_path)?
} else {
PathBuf::from(CONTAINER_FILE)
};
let tags = Driver::generate_tags(&recipe)?;
let image_name = self.generate_full_image_name(&recipe)?;
let opts = if let Some(archive_dir) = self.archive.as_ref() {
@ -244,7 +228,7 @@ impl BuildCommand {
BuildTagPushOpts::builder()
.image(&image_name)
.containerfile(&containerfile)
.tags(tags.iter().map(String::as_str).collect::<Vec<_>>())
.tags(&tags)
.push(self.push)
.retry_push(self.retry_push)
.retry_count(self.retry_count)
@ -253,10 +237,19 @@ impl BuildCommand {
.build()
};
Driver::get_build_driver().build_tag_push(&opts)?;
Driver::build_tag_push(&opts)?;
if self.push && !self.no_sign {
sign_images(&image_name, tags.first().map(String::as_str))?;
let opts = SignVerifyOpts::builder()
.image(&image_name)
.retry_push(self.retry_push)
.retry_count(self.retry_count);
let opts = if let Some(tag) = tags.first() {
opts.tag(tag).build()
} else {
opts.build()
};
Driver::sign_and_verify(&opts)?;
}
Ok(())
@ -268,12 +261,13 @@ impl BuildCommand {
#[cfg(not(feature = "multi-recipe"))]
fn start(&self, recipe_path: &Path) -> Result<()> {
use blue_build_process_management::drivers::opts::SignVerifyOpts;
trace!("BuildCommand::start()");
let recipe = Recipe::parse(recipe_path)?;
let os_version = Driver::get_os_version(&recipe)?;
let containerfile = generate_containerfile_path(recipe_path)?;
let tags = recipe.generate_tags(os_version);
let containerfile = PathBuf::from(CONTAINER_FILE);
let tags = Driver::generate_tags(&recipe)?;
let image_name = self.generate_full_image_name(&recipe)?;
let opts = if let Some(archive_dir) = self.archive.as_ref() {
@ -290,7 +284,7 @@ impl BuildCommand {
BuildTagPushOpts::builder()
.image(&image_name)
.containerfile(&containerfile)
.tags(tags.iter().map(String::as_str).collect::<Vec<_>>())
.tags(&tags)
.push(self.push)
.retry_push(self.retry_push)
.retry_count(self.retry_count)
@ -299,105 +293,45 @@ impl BuildCommand {
.build()
};
Driver::get_build_driver().build_tag_push(&opts)?;
Driver::build_tag_push(&opts)?;
if self.push && !self.no_sign {
sign_images(&image_name, tags.first().map(String::as_str))?;
let opts = SignVerifyOpts::builder()
.image(&image_name)
.retry_push(self.retry_push)
.retry_count(self.retry_count);
let opts = if let Some(tag) = tags.first() {
opts.tag(tag).build()
} else {
opts.build()
};
Driver::sign_and_verify(&opts)?;
}
info!("Build complete!");
Ok(())
}
fn login() -> Result<()> {
trace!("BuildCommand::login()");
info!("Attempting to login to the registry");
if let Some(Credentials {
registry,
username,
password,
}) = credentials::get()
{
info!("Logging into the registry, {registry}");
Driver::get_build_driver().login()?;
trace!("cosign login -u {username} -p [MASKED] {registry}");
let login_output = Command::new("cosign")
.arg("login")
.arg("-u")
.arg(username)
.arg("-p")
.arg(password)
.arg(registry)
.output()
.into_diagnostic()?;
if !login_output.status.success() {
let err_output = String::from_utf8_lossy(&login_output.stderr);
bail!("Failed to login for cosign: {err_output}");
}
info!("Login success at {registry}");
}
Ok(())
}
/// # Errors
///
/// Will return `Err` if the image name cannot be generated.
pub fn generate_full_image_name(&self, recipe: &Recipe) -> Result<String> {
fn generate_full_image_name(&self, recipe: &Recipe) -> Result<String> {
trace!("BuildCommand::generate_full_image_name({recipe:#?})");
info!("Generating full image name");
let image_name = match (
env::var(CI_REGISTRY).ok().map(|s| s.to_lowercase()),
env::var(CI_PROJECT_NAMESPACE)
.ok()
.map(|s| s.to_lowercase()),
env::var(CI_PROJECT_NAME).ok().map(|s| s.to_lowercase()),
env::var(GITHUB_REPOSITORY_OWNER)
.ok()
.map(|s| s.to_lowercase()),
self.registry.as_ref().map(|s| s.to_lowercase()),
let image_name = if let (Some(registry), Some(registry_path)) = (
self.credentials.registry.as_ref().map(|r| r.to_lowercase()),
self.registry_namespace.as_ref().map(|s| s.to_lowercase()),
) {
(_, _, _, _, Some(registry), Some(registry_path)) => {
trace!("registry={registry}, registry_path={registry_path}");
format!(
"{}/{}/{}",
registry.trim().trim_matches('/'),
registry_path.trim().trim_matches('/'),
recipe.name.trim(),
)
}
(
Some(ci_registry),
Some(ci_project_namespace),
Some(ci_project_name),
None,
None,
None,
) => {
trace!("CI_REGISTRY={ci_registry}, CI_PROJECT_NAMESPACE={ci_project_namespace}, CI_PROJECT_NAME={ci_project_name}");
warn!("Generating Gitlab Registry image");
format!(
"{ci_registry}/{ci_project_namespace}/{ci_project_name}/{}",
recipe.name.trim().to_lowercase()
)
}
(None, None, None, Some(github_repository_owner), None, None) => {
trace!("GITHUB_REPOSITORY_OWNER={github_repository_owner}");
warn!("Generating Github Registry image");
format!("ghcr.io/{github_repository_owner}/{}", &recipe.name)
}
_ => {
trace!("Nothing to indicate an image name with a registry");
if self.push {
bail!("Need '--registry' and '--registry-namespace' in order to push image");
}
recipe.name.trim().to_lowercase()
}
trace!("registry={registry}, registry_path={registry_path}");
format!(
"{}/{}/{}",
registry.trim().trim_matches('/'),
registry_path.trim().trim_matches('/'),
recipe.name.trim(),
)
} else {
Driver::generate_image_name(recipe)?
};
debug!("Using image name '{image_name}'");
@ -478,296 +412,4 @@ impl BuildCommand {
Ok(())
}
/// Checks the cosign private/public key pair to ensure they match.
///
/// # Errors
/// Will error if it's unable to verify key pairs.
fn check_cosign_files(&self) -> Result<()> {
trace!("check_for_cosign_files()");
if self.no_sign {
Ok(())
} else {
env::set_var("COSIGN_PASSWORD", "");
env::set_var("COSIGN_YES", "true");
match (
env::var(COSIGN_PRIVATE_KEY).ok(),
Path::new(COSIGN_PRIV_PATH),
) {
(Some(cosign_priv_key), _)
if !cosign_priv_key.is_empty() && Path::new(COSIGN_PUB_PATH).exists() =>
{
trace!("cosign public-key --key env://COSIGN_PRIVATE_KEY");
let output = Command::new("cosign")
.arg("public-key")
.arg("--key=env://COSIGN_PRIVATE_KEY")
.output()
.into_diagnostic()?;
if !output.status.success() {
bail!(
"Failed to run cosign public-key: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let calculated_pub_key = String::from_utf8(output.stdout).into_diagnostic()?;
let found_pub_key = fs::read_to_string(COSIGN_PUB_PATH)
.into_diagnostic()
.with_context(|| format!("Failed to read {COSIGN_PUB_PATH}"))?;
trace!("calculated_pub_key={calculated_pub_key},found_pub_key={found_pub_key}");
if calculated_pub_key.trim() == found_pub_key.trim() {
debug!("Cosign files match, continuing build");
Ok(())
} else {
bail!("Public key '{COSIGN_PUB_PATH}' does not match private key")
}
}
(None, cosign_priv_key_path) if cosign_priv_key_path.exists() => {
trace!("cosign public-key --key {COSIGN_PRIV_PATH}");
let output = Command::new("cosign")
.arg("public-key")
.arg(format!("--key={COSIGN_PRIV_PATH}"))
.output()
.into_diagnostic()?;
if !output.status.success() {
bail!(
"Failed to run cosign public-key: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let calculated_pub_key = String::from_utf8(output.stdout).into_diagnostic()?;
let found_pub_key = fs::read_to_string(COSIGN_PUB_PATH)
.into_diagnostic()
.with_context(|| format!("Failed to read {COSIGN_PUB_PATH}"))?;
trace!("calculated_pub_key={calculated_pub_key},found_pub_key={found_pub_key}");
if calculated_pub_key.trim() == found_pub_key.trim() {
debug!("Cosign files match, continuing build");
Ok(())
} else {
bail!("Public key '{COSIGN_PUB_PATH}' does not match private key")
}
}
_ => {
bail!("Unable to find private/public key pair.\n\nMake sure you have a `{COSIGN_PUB_PATH}` in the root of your repo and have either {COSIGN_PRIVATE_KEY} set in your env variables or a `{COSIGN_PRIV_PATH}` file in the root of your repo.\n\nSee https://blue-build.org/how-to/cosign/ for more information.\n\nIf you don't want to sign your image, use the `--no-sign` flag.");
}
}
}
}
}
// ======================================================== //
// ========================= Helpers ====================== //
// ======================================================== //
#[allow(clippy::too_many_lines)]
fn sign_images(image_name: &str, tag: Option<&str>) -> Result<()> {
trace!("BuildCommand::sign_images({image_name}, {tag:?})");
env::set_var("COSIGN_PASSWORD", "");
env::set_var("COSIGN_YES", "true");
let inspect_opts = GetMetadataOpts::builder().image(image_name);
let inspect_opts = if let Some(tag) = tag {
inspect_opts.tag(tag).build()
} else {
inspect_opts.build()
};
let image_digest = Driver::get_inspection_driver()
.get_metadata(&inspect_opts)?
.digest;
let image_name_digest = format!("{image_name}@{image_digest}");
let image_name_tag = tag.map_or_else(|| image_name.to_owned(), |t| format!("{image_name}:{t}"));
match (
// GitLab specific vars
env::var(CI_DEFAULT_BRANCH),
env::var(CI_PROJECT_URL),
env::var(CI_SERVER_PROTOCOL),
env::var(CI_SERVER_HOST),
env::var(SIGSTORE_ID_TOKEN),
// GitHub specific vars
env::var(GITHUB_TOKEN),
env::var(GITHUB_WORKFLOW_REF),
// Cosign public/private key pair
env::var(COSIGN_PRIVATE_KEY),
Path::new(COSIGN_PRIV_PATH),
) {
// Cosign public/private key pair
(_, _, _, _, _, _, _, Ok(cosign_private_key), _)
if !cosign_private_key.is_empty() && Path::new(COSIGN_PUB_PATH).exists() =>
{
sign_priv_public_pair_env(&image_name_digest, &image_name_tag)?;
}
(_, _, _, _, _, _, _, _, cosign_priv_key_path) if cosign_priv_key_path.exists() => {
sign_priv_public_pair_file(&image_name_digest, &image_name_tag)?;
}
// Gitlab keyless
(
Ok(ci_default_branch),
Ok(ci_project_url),
Ok(ci_server_protocol),
Ok(ci_server_host),
Ok(_),
_,
_,
_,
_,
) => {
trace!("CI_PROJECT_URL={ci_project_url}, CI_DEFAULT_BRANCH={ci_default_branch}, CI_SERVER_PROTOCOL={ci_server_protocol}, CI_SERVER_HOST={ci_server_host}");
info!("Signing image: {image_name_digest}");
trace!("cosign sign {image_name_digest}");
if Command::new("cosign")
.arg("sign")
.arg("--recursive")
.arg(&image_name_digest)
.status()
.into_diagnostic()?
.success()
{
info!("Successfully signed image!");
} else {
bail!("Failed to sign image: {image_name_digest}");
}
let cert_ident =
format!("{ci_project_url}//.gitlab-ci.yml@refs/heads/{ci_default_branch}");
let cert_oidc = format!("{ci_server_protocol}://{ci_server_host}");
trace!("cosign verify --certificate-identity {cert_ident} --certificate-oidc-issuer {cert_oidc} {image_name_tag}");
if !Command::new("cosign")
.arg("verify")
.arg("--certificate-identity")
.arg(&cert_ident)
.arg("--certificate-oidc-issuer")
.arg(&cert_oidc)
.arg(&image_name_tag)
.status()
.into_diagnostic()?
.success()
{
bail!("Failed to verify image!");
}
}
// GitHub keyless
(_, _, _, _, _, Ok(_), Ok(github_worflow_ref), _, _) => {
trace!("GITHUB_WORKFLOW_REF={github_worflow_ref}");
info!("Signing image {image_name_digest}");
trace!("cosign sign {image_name_digest}");
if Command::new("cosign")
.arg("sign")
.arg("--recursive")
.arg(&image_name_digest)
.status()
.into_diagnostic()?
.success()
{
info!("Successfully signed image!");
} else {
bail!("Failed to sign image: {image_name_digest}");
}
trace!("cosign verify --certificate-identity-regexp {github_worflow_ref} --certificate-oidc-issuer {GITHUB_TOKEN_ISSUER_URL} {image_name_tag}");
if !Command::new("cosign")
.arg("verify")
.arg("--certificate-identity-regexp")
.arg(&github_worflow_ref)
.arg("--certificate-oidc-issuer")
.arg(GITHUB_TOKEN_ISSUER_URL)
.arg(&image_name_tag)
.status()
.into_diagnostic()?
.success()
{
bail!("Failed to verify image!");
}
}
_ => warn!("Not running in CI with cosign variables, not signing"),
}
Ok(())
}
fn sign_priv_public_pair_env(image_digest: &str, image_name_tag: &str) -> Result<()> {
info!("Signing image: {image_digest}");
trace!("cosign sign --key=env://{COSIGN_PRIVATE_KEY} {image_digest}");
if Command::new("cosign")
.arg("sign")
.arg("--key=env://COSIGN_PRIVATE_KEY")
.arg("--recursive")
.arg(image_digest)
.status()
.into_diagnostic()?
.success()
{
info!("Successfully signed image!");
} else {
bail!("Failed to sign image: {image_digest}");
}
trace!("cosign verify --key {COSIGN_PUB_PATH} {image_name_tag}");
if !Command::new("cosign")
.arg("verify")
.arg(format!("--key={COSIGN_PUB_PATH}"))
.arg(image_name_tag)
.status()
.into_diagnostic()?
.success()
{
bail!("Failed to verify image!");
}
Ok(())
}
fn sign_priv_public_pair_file(image_digest: &str, image_name_tag: &str) -> Result<()> {
info!("Signing image: {image_digest}");
trace!("cosign sign --key={COSIGN_PRIV_PATH} {image_digest}");
if Command::new("cosign")
.arg("sign")
.arg(format!("--key={COSIGN_PRIV_PATH}"))
.arg("--recursive")
.arg(image_digest)
.status()
.into_diagnostic()?
.success()
{
info!("Successfully signed image!");
} else {
bail!("Failed to sign image: {image_digest}");
}
trace!("cosign verify --key {COSIGN_PUB_PATH} {image_name_tag}");
if !Command::new("cosign")
.arg("verify")
.arg(format!("--key={COSIGN_PUB_PATH}"))
.arg(image_name_tag)
.status()
.into_diagnostic()?
.success()
{
bail!("Failed to verify image!");
}
Ok(())
}

View file

@ -3,13 +3,11 @@ use std::{
path::{Path, PathBuf},
};
use blue_build_process_management::drivers::{CiDriver, Driver, DriverArgs};
use blue_build_recipe::Recipe;
use blue_build_template::{ContainerFileTemplate, Template};
use blue_build_utils::{
constants::{
CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_REGISTRY, CONFIG_PATH, GITHUB_REPOSITORY_OWNER,
RECIPE_FILE, RECIPE_PATH,
},
constants::{CONFIG_PATH, RECIPE_FILE, RECIPE_PATH},
syntax_highlighting::{self, DefaultThemes},
};
use clap::{crate_version, Args};
@ -17,9 +15,9 @@ use log::{debug, info, trace, warn};
use miette::{IntoDiagnostic, Result};
use typed_builder::TypedBuilder;
use crate::{drivers::Driver, shadow};
use crate::shadow;
use super::{BlueBuildCommand, DriverArgs};
use super::BlueBuildCommand;
#[derive(Debug, Clone, Args, TypedBuilder)]
pub struct GenerateCommand {
@ -73,11 +71,7 @@ pub struct GenerateCommand {
impl BlueBuildCommand for GenerateCommand {
fn try_run(&mut self) -> Result<()> {
Driver::builder()
.build_driver(self.drivers.build_driver)
.inspect_driver(self.drivers.inspect_driver)
.build()
.init();
Driver::init(self.drivers);
self.template_file()
}
@ -119,7 +113,8 @@ impl GenerateCommand {
.build_id(Driver::get_build_id())
.recipe(&recipe_de)
.recipe_path(recipe_path.as_path())
.registry(self.get_registry())
.registry(Driver::get_registry()?)
.repo(Driver::get_repo_url()?)
.exports_tag({
#[allow(clippy::const_is_empty)]
if shadow::COMMIT_HASH.is_empty() {
@ -146,37 +141,6 @@ impl GenerateCommand {
Ok(())
}
fn get_registry(&self) -> String {
match (
self.registry.as_ref(),
self.registry_namespace.as_ref(),
Self::get_github_repo_owner(),
Self::get_gitlab_registry_path(),
) {
(Some(r), Some(rn), _, _) => format!("{r}/{rn}"),
(Some(r), None, _, _) => r.to_string(),
(None, None, Some(gh_repo_owner), None) => format!("ghcr.io/{gh_repo_owner}"),
(None, None, None, Some(gl_reg_path)) => gl_reg_path,
_ => "localhost".to_string(),
}
}
fn get_github_repo_owner() -> Option<String> {
Some(env::var(GITHUB_REPOSITORY_OWNER).ok()?.to_lowercase())
}
fn get_gitlab_registry_path() -> Option<String> {
Some(
format!(
"{}/{}/{}",
env::var(CI_REGISTRY).ok()?,
env::var(CI_PROJECT_NAMESPACE).ok()?,
env::var(CI_PROJECT_NAME).ok()?,
)
.to_lowercase(),
)
}
}
// ======================================================== //

View file

@ -1,11 +1,14 @@
use std::{
fs,
path::{Path, PathBuf},
process::Command,
};
use blue_build_process_management::drivers::DriverArgs;
use blue_build_recipe::Recipe;
use blue_build_utils::constants::{ARCHIVE_SUFFIX, LOCAL_BUILD};
use blue_build_utils::{
cmd,
constants::{ARCHIVE_SUFFIX, LOCAL_BUILD},
};
use clap::Args;
use log::{debug, info, trace};
use miette::{bail, IntoDiagnostic, Result};
@ -14,7 +17,7 @@ use users::{Users, UsersCache};
use crate::commands::build::BuildCommand;
use super::{BlueBuildCommand, DriverArgs};
use super::BlueBuildCommand;
#[derive(Default, Clone, Debug, TypedBuilder, Args)]
pub struct LocalCommonArgs {
@ -81,19 +84,14 @@ impl BlueBuildCommand for UpgradeCommand {
info!("Upgrading image {image_name} and rebooting");
trace!("rpm-ostree upgrade --reboot");
Command::new("rpm-ostree")
.arg("upgrade")
.arg("--reboot")
cmd!("rpm-ostree", "upgrade", "--reboot")
.status()
.into_diagnostic()?
} else {
info!("Upgrading image {image_name}");
trace!("rpm-ostree upgrade");
Command::new("rpm-ostree")
.arg("upgrade")
.status()
.into_diagnostic()?
cmd!("rpm-ostree", "upgrade").status().into_diagnostic()?
};
if status.success() {
@ -146,19 +144,14 @@ impl BlueBuildCommand for RebaseCommand {
info!("Rebasing image {image_name} and rebooting");
trace!("rpm-ostree rebase --reboot {rebase_url}");
Command::new("rpm-ostree")
.arg("rebase")
.arg("--reboot")
.arg(rebase_url)
cmd!("rpm-ostree", "rebase", "--reboot", rebase_url)
.status()
.into_diagnostic()?
} else {
info!("Rebasing image {image_name}");
trace!("rpm-ostree rebase {rebase_url}");
Command::new("rpm-ostree")
.arg("rebase")
.arg(rebase_url)
cmd!("rpm-ostree", "rebase", rebase_url)
.status()
.into_diagnostic()?
};

105
src/commands/login.rs Normal file
View file

@ -0,0 +1,105 @@
use std::io::{self, Read};
use blue_build_process_management::drivers::{BuildDriver, Driver, DriverArgs, SigningDriver};
use blue_build_utils::credentials::{Credentials, CredentialsArgs};
use clap::Args;
use miette::{bail, IntoDiagnostic, Result};
use requestty::questions;
use typed_builder::TypedBuilder;
use super::BlueBuildCommand;
#[derive(Debug, Clone, Args, TypedBuilder)]
pub struct LoginCommand {
/// The server to login to.
server: String,
/// The password to login with.
///
/// Cannont be used with `--password-stdin`.
#[arg(group = "pass", long, short)]
password: Option<String>,
/// Read password from stdin,
///
/// Cannot be used with `--password/-p`
#[arg(group = "pass", long)]
password_stdin: bool,
/// The username to login with
#[arg(long, short)]
username: Option<String>,
#[clap(flatten)]
#[builder(default)]
drivers: DriverArgs,
}
impl BlueBuildCommand for LoginCommand {
fn try_run(&mut self) -> miette::Result<()> {
Driver::init(self.drivers);
Credentials::init(
CredentialsArgs::builder()
.registry(&self.server)
.username(self.get_username()?)
.password(self.get_password()?)
.build(),
);
Driver::login()?;
Driver::signing_login()?;
Ok(())
}
}
impl LoginCommand {
fn get_username(&self) -> Result<String> {
Ok(if let Some(ref username) = self.username {
username.clone()
} else if !self.password_stdin {
let questions = questions! [ inline
Input {
name: "username",
},
];
requestty::prompt(questions)
.into_diagnostic()?
.get("username")
.unwrap()
.as_string()
.unwrap()
.to_string()
} else {
bail!("Cannot prompt for username when using `--password-stdin`");
})
}
fn get_password(&self) -> Result<String> {
Ok(if let Some(ref password) = self.password {
password.clone()
} else if self.password_stdin {
let mut password = String::new();
io::stdin()
.read_to_string(&mut password)
.into_diagnostic()?;
password
} else {
let questions = questions! [ inline
Password {
name: "password",
}
];
requestty::prompt(questions)
.into_diagnostic()?
.get("password")
.unwrap()
.as_string()
.unwrap()
.to_string()
})
}
}

View file

@ -1,13 +1,16 @@
use std::{
path::{Path, PathBuf},
process::Command,
time::Duration,
};
use blue_build_process_management::{
drivers::{Driver, DriverArgs},
logging::CommandLogging,
};
use blue_build_recipe::Recipe;
use blue_build_utils::{
cmd,
constants::{ARCHIVE_SUFFIX, LOCAL_BUILD, OCI_ARCHIVE, OSTREE_UNVERIFIED_IMAGE},
logging::CommandLogging,
};
use clap::Args;
use colored::Colorize;
@ -17,9 +20,9 @@ use miette::{bail, IntoDiagnostic, Result};
use tempdir::TempDir;
use typed_builder::TypedBuilder;
use crate::{commands::build::BuildCommand, drivers::Driver, rpm_ostree_status::RpmOstreeStatus};
use crate::{commands::build::BuildCommand, rpm_ostree_status::RpmOstreeStatus};
use super::{BlueBuildCommand, DriverArgs};
use super::BlueBuildCommand;
#[derive(Default, Clone, Debug, TypedBuilder, Args)]
pub struct SwitchCommand {
@ -51,11 +54,7 @@ impl BlueBuildCommand for SwitchCommand {
fn try_run(&mut self) -> Result<()> {
trace!("SwitchCommand::try_run()");
Driver::builder()
.build_driver(self.drivers.build_driver)
.inspect_driver(self.drivers.inspect_driver)
.build()
.init();
Driver::init(self.drivers);
let status = RpmOstreeStatus::try_new()?;
trace!("{status:?}");
@ -110,17 +109,13 @@ impl SwitchCommand {
let status = if status.is_booted_on_archive(archive_path)
|| status.is_staged_on_archive(archive_path)
{
let mut command = Command::new("rpm-ostree");
command.arg("upgrade");
let mut command = cmd!("rpm-ostree", "upgrade");
if self.reboot {
command.arg("--reboot");
cmd!(command, "--reboot");
}
trace!(
"rpm-ostree upgrade {}",
self.reboot.then_some("--reboot").unwrap_or_default()
);
trace!("{command:?}");
command
} else {
let image_ref = format!(
@ -128,17 +123,13 @@ impl SwitchCommand {
path = archive_path.display()
);
let mut command = Command::new("rpm-ostree");
command.arg("rebase").arg(&image_ref);
let mut command = cmd!("rpm-ostree", "rebase", &image_ref);
if self.reboot {
command.arg("--reboot");
cmd!(command, "--reboot");
}
trace!(
"rpm-ostree rebase{} {image_ref}",
self.reboot.then_some(" --reboot").unwrap_or_default()
);
trace!("{command:?}");
command
}
.status_image_ref_progress(
@ -165,11 +156,7 @@ impl SwitchCommand {
progress.set_message(format!("Moving image archive to {}...", to.display()));
trace!("sudo mv {} {}", from.display(), to.display());
let status = Command::new("sudo")
.arg("mv")
.args([from, to])
.status()
.into_diagnostic()?;
let status = cmd!("sudo", "mv", from, to).status().into_diagnostic()?;
progress.finish_and_clear();
@ -194,8 +181,7 @@ impl SwitchCommand {
trace!("sudo ls {LOCAL_BUILD}");
let output = String::from_utf8(
Command::new("sudo")
.args(["ls", LOCAL_BUILD])
cmd!("sudo", "ls", LOCAL_BUILD)
.output()
.into_diagnostic()?
.stdout,
@ -218,11 +204,7 @@ impl SwitchCommand {
progress.set_message("Removing old image archive files...");
trace!("sudo rm -f {files}");
let status = Command::new("sudo")
.args(["rm", "-f"])
.arg(files)
.status()
.into_diagnostic()?;
let status = cmd!("sudo", "rm", "-f", files).status().into_diagnostic()?;
progress.finish_and_clear();
@ -236,8 +218,7 @@ impl SwitchCommand {
local_build_path.display()
);
let status = Command::new("sudo")
.args(["mkdir", "-p", LOCAL_BUILD])
let status = cmd!("sudo", "mkdir", "-p", LOCAL_BUILD)
.status()
.into_diagnostic()?;

View file

@ -1,129 +0,0 @@
use std::{env, sync::Mutex};
use blue_build_utils::constants::{
CI_REGISTRY, CI_REGISTRY_PASSWORD, CI_REGISTRY_USER, GITHUB_ACTIONS, GITHUB_ACTOR, GITHUB_TOKEN,
};
use log::trace;
use once_cell::sync::Lazy;
use typed_builder::TypedBuilder;
/// Stored user creds.
///
/// This is a special handoff static ref that is consumed
/// by the `ENV_CREDENTIALS` static ref. This can be set
/// at the beginning of a command for future calls for
/// creds to source from.
static USER_CREDS: Mutex<UserCreds> = Mutex::new(UserCreds {
username: None,
password: None,
registry: None,
});
/// The credentials for logging into image registries.
#[derive(Debug, Default, Clone, TypedBuilder)]
pub struct Credentials {
pub registry: String,
pub username: String,
pub password: String,
}
struct UserCreds {
pub username: Option<String>,
pub password: Option<String>,
pub registry: Option<String>,
}
/// Stores the global env credentials.
///
/// This on load will determine the credentials based off of
/// `USER_CREDS` and env vars from CI systems. Once this is called
/// the value is stored and cannot change.
///
/// If you have user
/// provided credentials, make sure you update `USER_CREDS`
/// before trying to access this reference.
static ENV_CREDENTIALS: Lazy<Option<Credentials>> = Lazy::new(|| {
let (username, password, registry) = {
USER_CREDS.lock().map_or((None, None, None), |creds| {
(
creds.username.as_ref().map(std::borrow::ToOwned::to_owned),
creds.password.as_ref().map(std::borrow::ToOwned::to_owned),
creds.registry.as_ref().map(std::borrow::ToOwned::to_owned),
)
})
};
let registry = match (
registry,
env::var(CI_REGISTRY).ok(),
env::var(GITHUB_ACTIONS).ok(),
) {
(Some(registry), _, _) if !registry.is_empty() => registry,
(None, Some(ci_registry), None) if !ci_registry.is_empty() => ci_registry,
(None, None, Some(_)) => "ghcr.io".to_string(),
_ => return None,
};
trace!("Registry: {registry}");
let username = match (
username,
env::var(CI_REGISTRY_USER).ok(),
env::var(GITHUB_ACTOR).ok(),
) {
(Some(username), _, _) if !username.is_empty() => username,
(None, Some(ci_registry_user), None) if !ci_registry_user.is_empty() => ci_registry_user,
(None, None, Some(github_actor)) if !github_actor.is_empty() => github_actor,
_ => return None,
};
trace!("Username: {username}");
let password = match (
password,
env::var(CI_REGISTRY_PASSWORD).ok(),
env::var(GITHUB_TOKEN).ok(),
) {
(Some(password), _, _) if !password.is_empty() => password,
(None, Some(ci_registry_password), None) if !ci_registry_password.is_empty() => {
ci_registry_password
}
(None, None, Some(registry_token)) if !registry_token.is_empty() => registry_token,
_ => return None,
};
Some(
Credentials::builder()
.registry(registry)
.username(username)
.password(password)
.build(),
)
});
/// Set the users credentials for
/// the current set of actions.
///
/// Be sure to call this before trying to use
/// any strategy that requires credentials as
/// the environment credentials are lazy allocated.
///
/// # Panics
/// Will panic if it can't lock the mutex.
pub fn set_user_creds(
username: Option<&String>,
password: Option<&String>,
registry: Option<&String>,
) {
trace!("credentials::set({username:?}, password, {registry:?})");
let mut creds_lock = USER_CREDS.lock().expect("Must lock USER_CREDS");
creds_lock.username = username.map(ToOwned::to_owned);
creds_lock.password = password.map(ToOwned::to_owned);
creds_lock.registry = registry.map(ToOwned::to_owned);
drop(creds_lock);
let _ = ENV_CREDENTIALS.as_ref();
}
/// Get the credentials for the current set of actions.
pub fn get() -> Option<&'static Credentials> {
trace!("credentials::get()");
ENV_CREDENTIALS.as_ref()
}

View file

@ -1,474 +0,0 @@
//! This module is responsible for managing various strategies
//! to perform actions throughout the program. This hides all
//! the implementation details from the command logic and allows
//! for caching certain long execution tasks like inspecting the
//! labels for an image.
use std::{
collections::{hash_map::Entry, HashMap},
process::{ExitStatus, Output},
sync::{Arc, Mutex},
};
use blue_build_recipe::Recipe;
use blue_build_utils::constants::IMAGE_VERSION_LABEL;
use log::{debug, info, trace};
use miette::{bail, miette, Result};
use once_cell::sync::Lazy;
use semver::{Version, VersionReq};
use typed_builder::TypedBuilder;
use uuid::Uuid;
use crate::{credentials, image_metadata::ImageMetadata};
use self::{
buildah_driver::BuildahDriver,
docker_driver::DockerDriver,
opts::{BuildOpts, BuildTagPushOpts, GetMetadataOpts, PushOpts, RunOpts, TagOpts},
podman_driver::PodmanDriver,
skopeo_driver::SkopeoDriver,
types::{BuildDriverType, InspectDriverType, RunDriverType},
};
mod buildah_driver;
mod docker_driver;
pub mod opts;
mod podman_driver;
mod skopeo_driver;
pub mod types;
static INIT: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
static SELECTED_BUILD_DRIVER: Lazy<Mutex<Option<BuildDriverType>>> = Lazy::new(|| Mutex::new(None));
static SELECTED_INSPECT_DRIVER: Lazy<Mutex<Option<InspectDriverType>>> =
Lazy::new(|| Mutex::new(None));
static SELECTED_RUN_DRIVER: Lazy<Mutex<Option<RunDriverType>>> = Lazy::new(|| Mutex::new(None));
/// Stores the build driver.
///
/// This will, on load, find the best way to build in the
/// current environment. Once that strategy is determined,
/// it will be available for any part of the program to call
/// on to perform builds.
///
/// # Panics
///
/// This will cause a panic if a build strategy could
/// not be determined.
static BUILD_DRIVER: Lazy<Arc<dyn BuildDriver>> = Lazy::new(|| {
let driver = SELECTED_BUILD_DRIVER.lock().unwrap();
driver.map_or_else(
|| panic!("Driver needs to be initialized"),
|driver| -> Arc<dyn BuildDriver> {
match driver {
BuildDriverType::Buildah => Arc::new(BuildahDriver),
BuildDriverType::Podman => Arc::new(PodmanDriver),
BuildDriverType::Docker => Arc::new(DockerDriver),
}
},
)
});
/// Stores the inspection driver.
///
/// This will, on load, find the best way to inspect images in the
/// current environment. Once that strategy is determined,
/// it will be available for any part of the program to call
/// on to perform inspections.
///
/// # Panics
///
/// This will cause a panic if a inspect strategy could
/// not be determined.
static INSPECT_DRIVER: Lazy<Arc<dyn InspectDriver>> = Lazy::new(|| {
let driver = SELECTED_INSPECT_DRIVER.lock().unwrap();
driver.map_or_else(
|| panic!("Driver needs to be initialized"),
|driver| -> Arc<dyn InspectDriver> {
match driver {
InspectDriverType::Skopeo => Arc::new(SkopeoDriver),
InspectDriverType::Podman => Arc::new(PodmanDriver),
InspectDriverType::Docker => Arc::new(DockerDriver),
}
},
)
});
/// Stores the run driver.
///
/// This will, on load, find the best way to run containers in the
/// current environment. Once that strategy is determined,
/// it will be available for any part of the program to call
/// on to perform inspections.
///
/// # Panics
///
/// This will cause a panic if a run strategy could
/// not be determined.
static RUN_DRIVER: Lazy<Arc<dyn RunDriver>> = Lazy::new(|| {
let driver = SELECTED_RUN_DRIVER.lock().unwrap();
driver.map_or_else(
|| panic!("Driver needs to be initialized"),
|driver| -> Arc<dyn RunDriver> {
match driver {
RunDriverType::Podman => Arc::new(PodmanDriver),
RunDriverType::Docker => Arc::new(DockerDriver),
}
},
)
});
/// UUID used to mark the current builds
static BUILD_ID: Lazy<Uuid> = Lazy::new(Uuid::new_v4);
/// The cached os versions
static OS_VERSION: Lazy<Mutex<HashMap<String, u64>>> = Lazy::new(|| Mutex::new(HashMap::new()));
/// Trait for retrieving version of a driver.
pub trait DriverVersion {
/// The version req string slice that follows
/// the semver standard <https://semver.org/>.
const VERSION_REQ: &'static str;
/// Returns the version of the driver.
///
/// # Errors
/// Will error if it can't retrieve the version.
fn version() -> Result<Version>;
#[must_use]
fn is_supported_version() -> bool {
Self::version().is_ok_and(|version| {
VersionReq::parse(Self::VERSION_REQ).is_ok_and(|req| req.matches(&version))
})
}
}
/// Allows agnostic building, tagging
/// pushing, and login.
pub trait BuildDriver: Sync + Send {
/// Runs the build logic for the strategy.
///
/// # Errors
/// Will error if the build fails.
fn build(&self, opts: &BuildOpts) -> Result<()>;
/// Runs the tag logic for the strategy.
///
/// # Errors
/// Will error if the tagging fails.
fn tag(&self, opts: &TagOpts) -> Result<()>;
/// Runs the push logic for the strategy
///
/// # Errors
/// Will error if the push fails.
fn push(&self, opts: &PushOpts) -> Result<()>;
/// Runs the login logic for the strategy.
///
/// # Errors
/// Will error if login fails.
fn login(&self) -> Result<()>;
/// Runs the logic for building, tagging, and pushing an image.
///
/// # Errors
/// Will error if building, tagging, or pusing fails.
fn build_tag_push(&self, opts: &BuildTagPushOpts) -> Result<()> {
trace!("BuildDriver::build_tag_push({opts:#?})");
let full_image = match (opts.archive_path.as_ref(), opts.image.as_ref()) {
(Some(archive_path), None) => {
format!("oci-archive:{archive_path}")
}
(None, Some(image)) => opts
.tags
.first()
.map_or_else(|| image.to_string(), |tag| format!("{image}:{tag}")),
(Some(_), Some(_)) => bail!("Cannot use both image and archive path"),
(None, None) => bail!("Need either the image or archive path set"),
};
let build_opts = BuildOpts::builder()
.image(&full_image)
.containerfile(opts.containerfile.as_ref())
.squash(opts.squash)
.build();
info!("Building image {full_image}");
self.build(&build_opts)?;
if !opts.tags.is_empty() && opts.archive_path.is_none() {
let image = opts
.image
.as_ref()
.ok_or_else(|| miette!("Image is required in order to tag"))?;
debug!("Tagging all images");
for tag in opts.tags.as_ref() {
debug!("Tagging {} with {tag}", &full_image);
let tag_opts = TagOpts::builder()
.src_image(&full_image)
.dest_image(format!("{image}:{tag}"))
.build();
self.tag(&tag_opts)?;
if opts.push {
let retry_count = if opts.retry_push { opts.retry_count } else { 0 };
debug!("Pushing all images");
// Push images with retries (1s delay between retries)
blue_build_utils::retry(retry_count, 10, || {
let tag_image = format!("{image}:{tag}");
debug!("Pushing image {tag_image}");
let push_opts = PushOpts::builder()
.image(&tag_image)
.compression_type(opts.compression)
.build();
self.push(&push_opts)
})?;
}
}
}
Ok(())
}
}
pub trait RunDriver: Sync + Send {
/// Run a container to perform an action.
///
/// # Errors
/// Will error if there is an issue running the container.
fn run(&self, opts: &RunOpts) -> std::io::Result<ExitStatus>;
/// Run a container to perform an action and capturing output.
///
/// # Errors
/// Will error if there is an issue running the container.
fn run_output(&self, opts: &RunOpts) -> std::io::Result<Output>;
}
/// Allows agnostic inspection of images.
pub trait InspectDriver: Sync + Send {
/// Gets the metadata on an image tag.
///
/// # Errors
/// Will error if it is unable to get the labels.
fn get_metadata(&self, opts: &GetMetadataOpts) -> Result<ImageMetadata>;
}
#[derive(Debug, TypedBuilder)]
pub struct Driver<'a> {
#[builder(default)]
username: Option<&'a String>,
#[builder(default)]
password: Option<&'a String>,
#[builder(default)]
registry: Option<&'a String>,
#[builder(default)]
build_driver: Option<BuildDriverType>,
#[builder(default)]
inspect_driver: Option<InspectDriverType>,
#[builder(default)]
run_driver: Option<RunDriverType>,
}
impl Driver<'_> {
/// Initializes the Strategy with user provided credentials.
///
/// If you want to take advantage of a user's credentials,
/// you will want to run init before trying to use any of
/// the strategies.
///
/// # Panics
/// Will panic if it is unable to initialize drivers.
pub fn init(self) {
trace!("Driver::init()");
let mut initialized = INIT.lock().expect("Must lock INIT");
if !*initialized {
credentials::set_user_creds(self.username, self.password, self.registry);
let mut build_driver = SELECTED_BUILD_DRIVER.lock().expect("Must lock BuildDriver");
let mut inspect_driver = SELECTED_INSPECT_DRIVER
.lock()
.expect("Must lock InspectDriver");
let mut run_driver = SELECTED_RUN_DRIVER.lock().expect("Must lock RunDriver");
*build_driver = Some(
self.build_driver
.map_or_else(Self::determine_build_driver, |driver| driver),
);
trace!("Build driver set to {:?}", *build_driver);
drop(build_driver);
let _ = Self::get_build_driver();
*inspect_driver = Some(
self.inspect_driver
.map_or_else(Self::determine_inspect_driver, |driver| driver),
);
trace!("Inspect driver set to {:?}", *inspect_driver);
drop(inspect_driver);
let _ = Self::get_inspection_driver();
*run_driver = Some(
self.run_driver
.map_or_else(Self::determine_run_driver, |driver| driver),
);
drop(run_driver);
let _ = Self::get_run_driver();
*initialized = true;
}
}
/// Gets the current build's UUID
#[must_use]
pub fn get_build_id() -> Uuid {
trace!("Driver::get_build_id()");
*BUILD_ID
}
/// Gets the current run's build strategy
pub fn get_build_driver() -> Arc<dyn BuildDriver> {
trace!("Driver::get_build_driver()");
BUILD_DRIVER.clone()
}
/// Gets the current run's inspectioin strategy
pub fn get_inspection_driver() -> Arc<dyn InspectDriver> {
trace!("Driver::get_inspection_driver()");
INSPECT_DRIVER.clone()
}
pub fn get_run_driver() -> Arc<dyn RunDriver> {
trace!("Driver::get_run_driver()");
RUN_DRIVER.clone()
}
/// Retrieve the `os_version` for an image.
///
/// This gets cached for faster resolution if it's required
/// in another part of the program.
///
/// # Errors
/// Will error if the image doesn't have OS version info
/// or we are unable to lock a mutex.
///
/// # Panics
/// Panics if the mutex fails to lock.
pub fn get_os_version(recipe: &Recipe) -> Result<u64> {
trace!("Driver::get_os_version({recipe:#?})");
let image = format!("{}:{}", &recipe.base_image, &recipe.image_version);
let mut os_version_lock = OS_VERSION.lock().expect("Should lock");
let entry = os_version_lock.get(&image);
let os_version = match entry {
None => {
info!("Retrieving OS version from {image}. This might take a bit");
let inspect_opts = GetMetadataOpts::builder()
.image(recipe.base_image.as_ref())
.tag(recipe.image_version.as_ref())
.build();
let inspection = INSPECT_DRIVER.get_metadata(&inspect_opts)?;
let os_version = inspection.get_version().ok_or_else(|| {
miette!(
help = format!("Please check with the image author about using '{IMAGE_VERSION_LABEL}' to report the os version."),
"Unable to get the OS version from the labels"
)
})?;
trace!("os_version: {os_version}");
os_version
}
Some(os_version) => {
debug!("Found cached {os_version} for {image}");
*os_version
}
};
if let Entry::Vacant(entry) = os_version_lock.entry(image.clone()) {
trace!("Caching version {os_version} for {image}");
entry.insert(os_version);
}
drop(os_version_lock);
Ok(os_version)
}
fn determine_inspect_driver() -> InspectDriverType {
trace!("Driver::determine_inspect_driver()");
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"),
}
}
fn determine_build_driver() -> BuildDriverType {
trace!("Driver::determine_build_driver()");
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() => {
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, need either docker version {}, podman version {}, or buildah version {} to continue",
DockerDriver::VERSION_REQ,
PodmanDriver::VERSION_REQ,
BuildahDriver::VERSION_REQ,
),
}
}
fn determine_run_driver() -> RunDriverType {
trace!("Driver::determine_run_driver()");
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
),
),
}
}
}

View file

@ -1,45 +0,0 @@
use std::borrow::Cow;
use typed_builder::TypedBuilder;
#[derive(Debug, Clone, TypedBuilder)]
pub struct RunOpts<'a> {
#[builder(default, setter(into))]
pub image: Cow<'a, str>,
#[builder(default, setter(into))]
pub args: Cow<'a, [String]>,
#[builder(default, setter(into))]
pub env_vars: Cow<'a, [RunOptsEnv<'a>]>,
#[builder(default, setter(into))]
pub volumes: Cow<'a, [RunOptsVolume<'a>]>,
#[builder(default)]
pub privileged: bool,
#[builder(default)]
pub pull: bool,
#[builder(default)]
pub remove: bool,
}
#[derive(Debug, Clone, TypedBuilder)]
pub struct RunOptsVolume<'a> {
#[builder(setter(into))]
pub path_or_vol_name: Cow<'a, str>,
#[builder(setter(into))]
pub container_path: Cow<'a, str>,
}
#[derive(Debug, Clone, TypedBuilder)]
pub struct RunOptsEnv<'a> {
#[builder(setter(into))]
pub key: Cow<'a, str>,
#[builder(setter(into))]
pub value: Cow<'a, str>,
}

View file

@ -1,30 +0,0 @@
use clap::ValueEnum;
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum InspectDriverType {
Skopeo,
Podman,
Docker,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum BuildDriverType {
Buildah,
Podman,
Docker,
}
#[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(),
}
}
}

View file

@ -5,7 +5,4 @@
shadow_rs::shadow!(shadow);
pub mod commands;
pub mod credentials;
pub mod drivers;
pub mod image_metadata;
pub mod rpm_ostree_status;

View file

@ -1,5 +1,6 @@
use std::{borrow::Cow, path::Path, process::Command};
use std::{borrow::Cow, path::Path};
use blue_build_utils::cmd;
use log::trace;
use miette::{bail, IntoDiagnostic, Result};
use serde::Deserialize;
@ -27,8 +28,7 @@ impl<'a> RpmOstreeStatus<'a> {
blue_build_utils::check_command_exists("rpm-ostree")?;
trace!("rpm-ostree status --json");
let output = Command::new("rpm-ostree")
.args(["status", "--json"])
let output = cmd!("rpm-ostree", "status", "--json")
.output()
.into_diagnostic()?;

View file

@ -14,9 +14,6 @@ blue-build-recipe = { version = "=0.8.12", path = "../recipe" }
blue-build-utils = { version = "=0.8.12", path = "../utils" }
log.workspace = true
serde.workspace = true
serde_yaml.workspace = true
serde_json.workspace = true
typed-builder.workspace = true
uuid.workspace = true

View file

@ -1,10 +1,8 @@
use std::{borrow::Cow, env, fs, path::Path, process};
use std::{borrow::Cow, fs, path::Path, process};
use blue_build_recipe::Recipe;
use blue_build_utils::constants::{
CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_SERVER_HOST, CI_SERVER_PROTOCOL, CONFIG_PATH,
CONTAINERFILES_PATH, CONTAINER_FILE, COSIGN_PUB_PATH, FILES_PATH, GITHUB_RESPOSITORY,
GITHUB_SERVER_URL,
CONFIG_PATH, CONTAINERFILES_PATH, CONTAINER_FILE, COSIGN_PUB_PATH, FILES_PATH,
};
use log::{debug, error, trace, warn};
use typed_builder::TypedBuilder;
@ -30,6 +28,9 @@ pub struct ContainerFileTemplate<'a> {
#[builder(setter(into))]
exports_tag: Cow<'a, str>,
#[builder(setter(into))]
repo: Cow<'a, str>,
}
#[derive(Debug, Clone, Template, TypedBuilder)]
@ -78,6 +79,19 @@ pub struct GithubIssueTemplate<'a> {
terminal_version: Cow<'a, str>,
}
#[derive(Debug, Clone, Template, TypedBuilder)]
#[template(path = "init/README.j2", escape = "md")]
pub struct InitReadmeTemplate<'a> {
#[builder(setter(into))]
repo_name: Cow<'a, str>,
#[builder(setter(into))]
registry: Cow<'a, str>,
#[builder(setter(into))]
image_name: Cow<'a, str>,
}
fn has_cosign_file() -> bool {
trace!("has_cosign_file()");
std::env::current_dir()
@ -110,38 +124,6 @@ fn print_containerfile(containerfile: &str) -> String {
file
}
fn get_repo_url() -> Option<String> {
Some(
match (
// GitHub vars
env::var(GITHUB_SERVER_URL),
env::var(GITHUB_RESPOSITORY),
// GitLab vars
env::var(CI_SERVER_PROTOCOL),
env::var(CI_SERVER_HOST),
env::var(CI_PROJECT_NAMESPACE),
env::var(CI_PROJECT_NAME),
) {
(Ok(github_server), Ok(github_repo), _, _, _, _) => {
format!("{github_server}/{github_repo}")
}
(
_,
_,
Ok(ci_server_protocol),
Ok(ci_server_host),
Ok(ci_project_namespace),
Ok(ci_project_name),
) => {
format!(
"{ci_server_protocol}://{ci_server_host}/{ci_project_namespace}/{ci_project_name}"
)
}
_ => return None,
},
)
}
fn modules_exists() -> bool {
let mod_path = Path::new("modules");
mod_path.exists() && mod_path.is_dir()

View file

@ -38,7 +38,5 @@ RUN rm -fr /tmp/* /var/* && ostree container commit
LABEL {{ blue_build_utils::constants::BUILD_ID_LABEL }}="{{ build_id }}"
LABEL org.opencontainers.image.title="{{ recipe.name }}"
LABEL org.opencontainers.image.description="{{ recipe.description }}"
{%- if let Some(repo) = self::get_repo_url() %}
LABEL org.opencontainers.image.source="{{ repo }}"
{%- endif %}
LABEL io.artifacthub.package.readme-url=https://raw.githubusercontent.com/blue-build/cli/main/README.md

View file

@ -0,0 +1,45 @@
# {{ repo_name }} Image Repo
See the [BlueBuild docs](https://blue-build.org/how-to/setup/) for quick setup instructions for setting up your own repository based on this template.
After setup, it is recommended you update this README to describe your custom image.
## Installation
> **Warning**
> [This is an experimental feature](https://www.fedoraproject.org/wiki/Changes/OstreeNativeContainerStable), try at your own discretion.
To rebase an existing atomic Fedora installation to the latest build:
- First rebase to the unsigned image, to get the proper signing keys and policies installed:
```
rpm-ostree rebase ostree-unverified-registry:{{ registry }}/{{ repo_name }}/{{ image_name }}:latest
```
- Reboot to complete the rebase:
```
systemctl reboot
```
- Then rebase to the signed image, like so:
```
rpm-ostree rebase ostree-image-signed:docker://{{ registry }}/{{ repo_name }}/{{ image_name }}:latest
```
- Reboot again to complete the installation
```
systemctl reboot
```
The `latest` tag will automatically point to the latest build. That build will still always use the Fedora version specified in `recipe.yml`, so you won't get accidentally updated to the next major version.
## ISO
If build on Fedora Atomic, you can generate an offline ISO with the instructions available [here](https://blue-build.org/learn/universal-blue/#fresh-install-from-an-iso). These ISOs cannot unfortunately be distributed on GitHub for free due to large sizes, so for public projects something else has to be used for hosting.
## Verification
These images are signed with [Sigstore](https://www.sigstore.dev/)'s [cosign](https://github.com/sigstore/cosign). You can verify the signature by downloading the `cosign.pub` file from this repo and running the following command:
```bash
cosign verify --key cosign.pub {{ registry }}/{{ repo_name }}/{{ image_name }}
```
Cloned from https://github.com/blue-build/template

View file

@ -0,0 +1,10 @@
{
"ref": "refs/heads/test-branch",
"repository": {
"default_branch": "main",
"owner": {
"login": "test-owner"
},
"html_url": "https://example.com/"
}
}

View file

@ -0,0 +1,10 @@
{
"ref": "refs/heads/main",
"repository": {
"default_branch": "main",
"owner": {
"login": "test-owner"
},
"html_url": "https://example.com/"
}
}

View file

@ -0,0 +1,15 @@
{
"head": {
"ref": "test-branch"
},
"base": {
"ref": "main"
},
"repository": {
"default_branch": "main",
"owner": {
"login": "test-owner"
},
"html_url": "https://example.com/"
}
}

View file

@ -0,0 +1,11 @@
-----BEGIN ENCRYPTED SIGSTORE PRIVATE KEY-----
eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjo2NTUzNiwiciI6
OCwicCI6MX0sInNhbHQiOiIvNjdKOVZ3WThhNnJhdk9DQUxmTzFQM05HRDRYc2s2
L005aE5iYVhDNytBPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94
Iiwibm9uY2UiOiIvaHQ1MjlSNlhnbkFGbVV6L3U0anlRVE1lb200VDZNVCJ9LCJj
aXBoZXJ0ZXh0IjoiWkpZWWsyR1FhWmdKdEh6UzBKdFVuTWhTblFXc25HcEQzYTVC
MjN3ZVlLb2REbzJkeFVOZXhFSURwODhGUkMzalVSTTRiNTZFSEVjblZVWmFETDNj
Z2ZrTjdNZWVvMThWWVN2Wm13STdYaFJaczExOUc2eWlmaThIcVpGYmdJM21Rd052
MEVEcDFEekw0d2ZJWjBweVAreEEvM2xOeTlteWZSZDZSM1JoR0h5SWt6NVF4eHJ2
WjB1VHZHVExOcmdLSHVzL3NTbis1WktsL1E9PSJ9
-----END ENCRYPTED SIGSTORE PRIVATE KEY-----

View file

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEq8TdgrRtcWVq6MXuB2uznS14EOQ9
Ol41BztsDr0Qd8BGfYM6lOkZ+/NLteBFZ9gQsgVhVrjrSifcHmMAUOZYwg==
-----END PUBLIC KEY-----

View file

@ -9,38 +9,30 @@ license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1"
atty = "0.2"
base64 = "0.22.1"
blake2 = "0.10.6"
base64 = "0.22"
blake2 = "0.10"
directories = "5"
rand = "0.8.5"
log4rs = { version = "1.3.0", features = ["background_rotation"] }
nix = { version = "0.29.0", features = ["signal"] }
nu-ansi-term = { version = "0.50.0", features = ["gnu_legacy"] }
os_pipe = { version = "1", features = ["io_safety"] }
docker_credential = "1"
format_serde_error = "0.3"
process_control = { version = "4", features = ["crossbeam-channel"] }
signal-hook = { version = "0.3.17", features = ["extended-siginfo"] }
syntect = "5"
which = "6"
chrono.workspace = true
clap = { workspace = true, features = ["derive"] }
colored.workspace = true
format_serde_error.workspace = true
indicatif.workspace = true
indicatif-log-bridge.workspace = true
clap = { workspace = true, features = ["derive", "env"] }
log.workspace = true
miette.workspace = true
once_cell.workspace = true
tempdir.workspace = true
serde.workspace = true
serde_yaml.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
typed-builder.workspace = true
[build-dependencies]
syntect = "5.2.0"
syntect = "5"
[dev-dependencies]
rstest.workspace = true
[lints]
workspace = true

View file

@ -19,6 +19,7 @@ pub const IMAGE_VERSION_LABEL: &str = "org.opencontainers.image.version";
// BlueBuild vars
pub const BB_BUILDKIT_CACHE_GHA: &str = "BB_BUILDKIT_CACHE_GHA";
pub const BB_PASSWORD: &str = "BB_PASSWORD";
pub const BB_PRIVATE_KEY: &str = "BB_PRIVATE_KEY";
pub const BB_REGISTRY: &str = "BB_REGISTRY";
pub const BB_REGISTRY_NAMESPACE: &str = "BB_REGISTRY_NAMESPACE";
pub const BB_USERNAME: &str = "BB_USERNAME";
@ -27,7 +28,9 @@ pub const BB_USERNAME: &str = "BB_USERNAME";
pub const DOCKER_HOST: &str = "DOCKER_HOST";
// Cosign vars
pub const COSIGN_PASSWORD: &str = "COSIGN_PASSWORD";
pub const COSIGN_PRIVATE_KEY: &str = "COSIGN_PRIVATE_KEY";
pub const COSIGN_YES: &str = "COSIGN_YES";
pub const GITHUB_TOKEN_ISSUER_URL: &str = "https://token.actions.githubusercontent.com";
pub const SIGSTORE_ID_TOKEN: &str = "SIGSTORE_ID_TOKEN";
@ -35,6 +38,7 @@ pub const SIGSTORE_ID_TOKEN: &str = "SIGSTORE_ID_TOKEN";
pub const GITHUB_ACTIONS: &str = "GITHUB_ACTIONS";
pub const GITHUB_ACTOR: &str = "GITHUB_ACTOR";
pub const GITHUB_EVENT_NAME: &str = "GITHUB_EVENT_NAME";
pub const GITHUB_EVENT_PATH: &str = "GITHUB_EVENT_PATH";
pub const GITHUB_REF_NAME: &str = "GITHUB_REF_NAME";
pub const GITHUB_RESPOSITORY: &str = "GITHUB_REPOSITORY";
pub const GITHUB_REPOSITORY_OWNER: &str = "GITHUB_REPOSITORY_OWNER";
@ -58,6 +62,7 @@ pub const CI_SERVER_PROTOCOL: &str = "CI_SERVER_PROTOCOL";
pub const CI_REGISTRY: &str = "CI_REGISTRY";
pub const CI_REGISTRY_PASSWORD: &str = "CI_REGISTRY_PASSWORD";
pub const CI_REGISTRY_USER: &str = "CI_REGISTRY_USER";
pub const GITLAB_CI: &str = "GITLAB_CI";
// Terminal vars
pub const TERM_PROGRAM: &str = "TERM_PROGRAM";
@ -67,10 +72,12 @@ pub const LC_TERMINAL_VERSION: &str = "LC_TERMINAL_VERSION";
pub const XDG_RUNTIME_DIR: &str = "XDG_RUNTIME_DIR";
// Misc
pub const COSIGN_IMAGE: &str = "gcr.io/projectsigstore/cosign:latest";
pub const OCI_ARCHIVE: &str = "oci-archive";
pub const OSTREE_IMAGE_SIGNED: &str = "ostree-image-signed";
pub const OSTREE_UNVERIFIED_IMAGE: &str = "ostree-unverified-image";
pub const SKOPEO_IMAGE: &str = "quay.io/skopeo/stable:latest";
pub const TEMPLATE_REPO_URL: &str = "https://github.com/blue-build/template.git";
pub const UNKNOWN_SHELL: &str = "<unknown shell>";
pub const UNKNOWN_VERSION: &str = "<unknown version>";
pub const UNKNOWN_TERMINAL: &str = "<unknown terminal>";

177
utils/src/credentials.rs Normal file
View file

@ -0,0 +1,177 @@
use std::{
env,
sync::{LazyLock, Mutex},
};
use clap::Args;
use docker_credential::DockerCredential;
use log::trace;
use typed_builder::TypedBuilder;
use crate::{
constants::{
BB_PASSWORD, BB_REGISTRY, BB_USERNAME, CI_REGISTRY, CI_REGISTRY_PASSWORD, CI_REGISTRY_USER,
GITHUB_ACTIONS, GITHUB_ACTOR, GITHUB_TOKEN,
},
string,
};
static INIT: LazyLock<Mutex<bool>> = LazyLock::new(|| Mutex::new(false));
/// Stored user creds.
///
/// This is a special handoff static ref that is consumed
/// by the `ENV_CREDENTIALS` static ref. This can be set
/// at the beginning of a command for future calls for
/// creds to source from.
static INIT_CREDS: Mutex<CredentialsArgs> = Mutex::new(CredentialsArgs {
username: None,
password: None,
registry: None,
});
/// Stores the global env credentials.
///
/// This on load will determine the credentials based off of
/// `USER_CREDS` and env vars from CI systems. Once this is called
/// the value is stored and cannot change.
///
/// If you have user
/// provided credentials, make sure you update `USER_CREDS`
/// before trying to access this reference.
static ENV_CREDENTIALS: LazyLock<Option<Credentials>> = LazyLock::new(|| {
let (username, password, registry) = {
INIT_CREDS.lock().map_or((None, None, None), |mut creds| {
(
creds.username.take(),
creds.password.take(),
creds.registry.take(),
)
})
};
let registry = match (
registry,
env::var(CI_REGISTRY).ok(),
env::var(GITHUB_ACTIONS).ok(),
) {
(Some(registry), _, _) if !registry.is_empty() => registry,
(None, Some(ci_registry), None) if !ci_registry.is_empty() => ci_registry,
(None, None, Some(_)) => string!("ghcr.io"),
_ => return None,
};
trace!("Registry: {registry:?}");
let docker_creds = docker_credential::get_credential(&registry).ok();
let podman_creds = docker_credential::get_podman_credential(&registry).ok();
let username = match (
username,
env::var(CI_REGISTRY_USER).ok(),
env::var(GITHUB_ACTOR).ok(),
&docker_creds,
&podman_creds,
) {
(Some(username), _, _, _, _) if !username.is_empty() => username,
(_, _, _, Some(DockerCredential::UsernamePassword(username, _)), _)
| (_, _, _, _, Some(DockerCredential::UsernamePassword(username, _)))
if !username.is_empty() =>
{
username.clone()
}
(None, Some(ci_registry_user), None, _, _) if !ci_registry_user.is_empty() => {
ci_registry_user
}
(None, None, Some(github_actor), _, _) if !github_actor.is_empty() => github_actor,
_ => return None,
};
trace!("Username: {username:?}");
let password = match (
password,
env::var(CI_REGISTRY_PASSWORD).ok(),
env::var(GITHUB_TOKEN).ok(),
&docker_creds,
&podman_creds,
) {
(Some(password), _, _, _, _) if !password.is_empty() => password,
(_, _, _, Some(DockerCredential::UsernamePassword(_, password)), _)
| (_, _, _, _, Some(DockerCredential::UsernamePassword(_, password)))
if !password.is_empty() =>
{
password.clone()
}
(None, Some(ci_registry_password), None, _, _) if !ci_registry_password.is_empty() => {
ci_registry_password
}
(None, None, Some(registry_token), _, _) if !registry_token.is_empty() => registry_token,
_ => return None,
};
Some(
Credentials::builder()
.registry(registry)
.username(username)
.password(password)
.build(),
)
});
/// The credentials for logging into image registries.
#[derive(Debug, Default, Clone, TypedBuilder)]
pub struct Credentials {
pub registry: String,
pub username: String,
pub password: String,
}
impl Credentials {
/// Set the users credentials for
/// the current set of actions.
///
/// Be sure to call this before trying to use
/// any strategy that requires credentials as
/// the environment credentials are lazy allocated.
///
/// # Panics
/// Will panic if it can't lock the mutex.
pub fn init(args: CredentialsArgs) {
trace!("Credentials::init()");
let mut initialized = INIT.lock().expect("Must lock INIT");
if !*initialized {
let mut creds_lock = INIT_CREDS.lock().expect("Must lock USER_CREDS");
*creds_lock = args;
drop(creds_lock);
let _ = ENV_CREDENTIALS.as_ref();
*initialized = true;
}
}
/// Get the credentials for the current set of actions.
pub fn get() -> Option<&'static Self> {
trace!("credentials::get()");
ENV_CREDENTIALS.as_ref()
}
}
#[derive(Debug, Default, Clone, TypedBuilder, Args)]
pub struct CredentialsArgs {
/// The registry's domain name.
#[arg(long, env = BB_REGISTRY)]
#[builder(default, setter(into, strip_option))]
pub registry: Option<String>,
/// The username to login to the
/// container registry.
#[arg(short = 'U', long, env = BB_USERNAME, hide_env_values = true)]
#[builder(default, setter(into, strip_option))]
pub username: Option<String>,
/// The password to login to the
/// container registry.
#[arg(short = 'P', long, env = BB_PASSWORD, hide_env_values = true)]
#[builder(default, setter(into, strip_option))]
pub password: Option<String>,
}

View file

@ -1,13 +1,12 @@
pub mod command_output;
pub mod constants;
pub mod logging;
pub mod signal_handler;
pub mod credentials;
mod macros;
pub mod syntax_highlighting;
use std::{
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
process::Command,
thread,
time::Duration,
};
@ -17,9 +16,10 @@ use blake2::{
digest::{Update, VariableOutput},
Blake2bVar,
};
use chrono::Local;
use format_serde_error::SerdeError;
use log::trace;
use miette::{miette, IntoDiagnostic, Result};
use miette::{miette, Context, IntoDiagnostic, Result};
use crate::constants::CONTAINER_FILE;
@ -33,8 +33,7 @@ pub fn check_command_exists(command: &str) -> Result<()> {
trace!("check_command_exists({command})");
trace!("which {command}");
if Command::new("which")
.arg(command)
if cmd!("which", command)
.output()
.into_diagnostic()?
.status
@ -109,3 +108,18 @@ pub fn generate_containerfile_path<T: AsRef<Path>>(path: T) -> Result<PathBuf> {
BASE64_URL_SAFE_NO_PAD.encode(buf)
)))
}
#[must_use]
pub fn get_tag_timestamp() -> String {
Local::now().format("%Y%m%d").to_string()
}
/// Get's the env var wrapping it with a miette error
///
/// # Errors
/// Will error if the env var doesn't exist.
pub fn get_env_var(key: &str) -> Result<String> {
std::env::var(key)
.into_diagnostic()
.with_context(|| format!("Failed to get {key}'"))
}

150
utils/src/macros.rs Normal file
View file

@ -0,0 +1,150 @@
/// Creates or modifies a `std::process::Command` adding args.
///
/// # Examples
/// ```
/// use blue_build_utils::cmd;
///
/// const NAME: &str = "Bob";
/// let mut command = cmd!("echo", "Hello world!");
/// cmd!(command, "This is Joe.", format!("And this is {NAME}"));
/// command.status().unwrap();
/// ```
#[macro_export]
macro_rules! cmd {
($command:expr) => {
::std::process::Command::new($command)
};
($command:ident, $($tail:tt)*) => {
cmd!(@ $command, $($tail)*)
};
($command:expr, $($tail:tt)*) => {
{
let mut c = cmd!($command);
cmd!(@ c, $($tail)*);
c
}
};
(@ $command:ident $(,)?) => { };
(@ $command:ident, for $for_expr:expr $(, $($tail:tt)*)?) => {
{
for arg in $for_expr.iter() {
cmd!($command, arg);
}
$(cmd!(@ $command, $($tail)*);)*
}
};
(@ $command:ident, for $iter:ident in $for_expr:expr => [ $($arg:expr),* $(,)? ] $(, $($tail:tt)*)?) => {
{
for $iter in $for_expr.iter() {
$(cmd!(@ $command, $arg);)*
}
$(cmd!(@ $command, $($tail)*);)*
}
};
(@ $command:ident, for $iter:ident in $for_expr:expr => $arg:expr $(, $($tail:tt)*)?) => {
{
for $iter in $for_expr.iter() {
cmd!(@ $command, $arg);
}
$(cmd!(@ $command, $($tail)*);)*
}
};
(@ $command:ident, if let $let_pat:pat = $if_expr:expr => [ $($arg:expr),* $(,)? ] $(, $($tail:tt)*)?) => {
{
if let $let_pat = $if_expr {
$(cmd!(@ $command, $arg);)*
}
$(cmd!(@ $command, $($tail)*);)*
}
};
(@ $command:ident, if let $let_pat:pat = $if_expr:expr => $arg:expr $(, $($tail:tt)*)?) => {
{
if let $let_pat = $if_expr {
cmd!(@ $command, $arg);
}
$(cmd!(@ $command, $($tail)*);)*
}
};
(@ $command:ident, if $if_expr:expr => [ $($arg:expr),* $(,)?] $(, $($tail:tt)*)?) => {
{
if $if_expr {
$(cmd!(@ $command, $arg);)*
}
$(cmd!(@ $command, $($tail)*);)*
}
};
(@ $command:ident, if $if_expr:expr => $arg:expr $(, $($tail:tt)*)?) => {
{
if $if_expr {
cmd!(@ $command, $arg);
}
$(cmd!(@ $command, $($tail)*);)*
}
};
(@ $command:ident, |$cmd_ref:ident|? $op:block $(, $($tail:tt)*)?) => {
{
let op_fn = |$cmd_ref: &mut ::std::process::Command| -> Result<()> {
$op
Ok(())
};
op_fn(&mut $command)?;
$(cmd!(@ $command, $($tail)*);)*
}
};
(@ $command:ident, |$cmd_ref:ident| $op:block $(, $($tail:tt)*)?) => {
{
let op_fn = |$cmd_ref: &mut ::std::process::Command| $op;
op_fn(&mut $command);
$(cmd!(@ $command, $($tail)*);)*
}
};
(@ $command:ident, $key:expr => $value:expr $(, $($tail:tt)*)?) => {
{
$command.env($key, $value);
$(cmd!(@ $command, $($tail)*);)*
}
};
(@ $command:ident, stdin = $pipe:expr $(, $($tail:tt)*)?) => {
{
$command.stdin($pipe);
$(cmd!(@ $command, $($tail)*);)*
}
};
(@ $command:ident, stdout = $pipe:expr $(, $($tail:tt)*)?) => {
{
$command.stdout($pipe);
$(cmd!(@ $command, $($tail)*);)*
}
};
(@ $command:ident, stderr = $pipe:expr $(, $($tail:tt)*)?) => {
{
$command.stderr($pipe);
$(cmd!(@ $command, $($tail)*);)*
}
};
(@ $command:ident, $arg:expr $(, $($tail:tt)*)?) => {
{
$command.arg($arg);
$(cmd!(@ $command, $($tail)*);)*
}
};
}
#[macro_export]
macro_rules! string {
($str:expr) => {
String::from($str)
};
}
#[macro_export]
macro_rules! string_vec {
($($string:expr),+ $(,)?) => {
{
use $crate::string;
vec![
$(string!($string),)*
]
}
};
}