diff --git a/.helix/languages.toml b/.helix/languages.toml index 506224c3..94a225c2 100644 --- a/.helix/languages.toml +++ b/.helix/languages.toml @@ -1,5 +1,5 @@ [language-server.rust-analyzer.config] -cargo.features = [] +cargo.features = "all" [language-server.rust-analyzer.config.check] command = "clippy" diff --git a/Cargo.lock b/Cargo.lock index b2abbc61..2343badd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -200,9 +200,9 @@ dependencies = [ "clap_complete", "clap_complete_nushell", "colored", - "dunce", "env_logger", "fuzzy-matcher", + "indexmap 2.2.3", "lenient_semver", "log", "once_cell", @@ -215,6 +215,7 @@ dependencies = [ "serde_json", "serde_yaml 0.9.32", "shadow-rs", + "tempdir", "typed-builder", "urlencoding", "users", @@ -501,12 +502,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "dunce" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" - [[package]] name = "either" version = "1.10.0" @@ -612,6 +607,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1fd087255f739f4f1aeea69f11b72f8080e9c2e7645cd06955dad4a178a49e3" +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "fuzzy-matcher" version = "0.3.7" @@ -1174,6 +1175,43 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -1238,6 +1276,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "requestty" version = "0.5.0" @@ -1507,6 +1554,16 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand", + "remove_dir_all", +] + [[package]] name = "tempfile" version = "3.10.0" diff --git a/Cargo.toml b/Cargo.toml index c8b4eb16..ef00c8ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,17 +11,18 @@ version = "0.8.9" [workspace.dependencies] anyhow = "1" -chrono = "0.4.35" +chrono = "0.4" clap = { version = "4", features = ["derive", "cargo", "unicode"] } -colored = "2.1.0" +colored = "2" env_logger = "0.11" -format_serde_error = "0.3.0" +format_serde_error = "0.3" +indexmap = { version = "2", features = ["serde"] } log = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1" -serde_yaml = "0.9.30" -typed-builder = "0.18.1" -uuid = { version = "1.7.0", features = ["v4"] } +serde_yaml = "0.9" +typed-builder = "0.18" +uuid = { version = "1", features = ["v4"] } [workspace.lints.rust] unsafe_code = "forbid" @@ -59,15 +60,16 @@ clap-verbosity-flag = "2" clap_complete = "4" clap_complete_nushell = "4" fuzzy-matcher = "0.3" -lenient_semver = "0.4.2" -once_cell = "1.19.0" +lenient_semver = "0.4" +once_cell = "1" open = "5" -os_info = "3.7" # update os module config and tests when upgrading os_info +os_info = "3" requestty = { version = "0.5", features = ["macros", "termion"] } -semver = { version = "1.0.22", features = ["serde"] } +semver = { version = "1", features = ["serde"] } shadow-rs = "0.26" -urlencoding = "2.1.3" -users = "0.11.0" +tempdir = "0.3" +urlencoding = "2" +users = "0.11" # Workspace dependencies anyhow.workspace = true @@ -75,6 +77,7 @@ chrono.workspace = true clap.workspace = true colored.workspace = true env_logger.workspace = true +indexmap.workspace = true log.workspace = true serde.workspace = true serde_json.workspace = true @@ -86,13 +89,13 @@ uuid.workspace = true default = [] stages = ["blue-build-recipe/stages"] copy = ["blue-build-recipe/copy"] +switch = [] [dev-dependencies] -rusty-hook = "0.11.2" +rusty-hook = "0.11" [build-dependencies] shadow-rs = "0.26" -dunce = "1.0.4" [lints] workspace = true diff --git a/README.md b/README.md index 31c1a7ad..4dc1fdfa 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,18 @@ sudo bluebuild upgrade recipes/recipe.yml The `--reboot` argument can be used with this command as well. +##### Switch + +> NOTE: This is an unstable feature and can only be used when installing from the `main` image or with the `switch` feature flag when compiling. + +With the switch command, you can build and boot an image locally using an `oci-archive` tarball. The `switch` command can be run as a normal user and will only ask for `sudo` permissions when moving the archive into `/etc/bluebuild`. + +```bash +bluebuild switch recipes/recipe.yml +``` + +You can initiate an immediate restart by adding the `--reboot/-r` option. + #### CI Builds ##### GitHub diff --git a/integration-tests/Earthfile b/integration-tests/Earthfile index bf6c7feb..06ecdde7 100644 --- a/integration-tests/Earthfile +++ b/integration-tests/Earthfile @@ -8,6 +8,7 @@ all: BUILD +build BUILD +rebase BUILD +upgrade + BUILD +switch test-image: FROM +build-template --src=template-containerfile @@ -45,7 +46,7 @@ build-template: template-containerfile: FROM +test-base - RUN bluebuild -vv template recipes/recipe.yml | tee Containerfile + RUN bluebuild -vv generate recipes/recipe.yml | tee Containerfile SAVE ARTIFACT /test @@ -57,13 +58,13 @@ template-legacy-containerfile: template-secureblue: FROM +secureblue-base - RUN bluebuild -vv template -o Containerfile recipes/general/recipe-silverblue-nvidia.yml + RUN bluebuild -vv generate -o Containerfile recipes/general/recipe-silverblue-nvidia.yml SAVE ARTIFACT /test template-secureblue-ucore: FROM +secureblue-base - RUN bluebuild -vv template -o Containerfile recipes/server/recipe-server-main.yml + RUN bluebuild -vv generate -o Containerfile recipes/server/recipe-server-main.yml SAVE ARTIFACT /test @@ -73,15 +74,21 @@ build: RUN bluebuild -vv build recipes/recipe.yml rebase: - FROM +test-base + FROM +legacy-base - RUN bluebuild -vv rebase recipes/recipe.yml + RUN bluebuild -vv 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 + +switch: FROM +test-base RUN mkdir -p /etc/bluebuild && touch $BB_TEST_LOCAL_IMAGE - RUN bluebuild -vv upgrade recipes/recipe.yml + RUN bluebuild -vv switch recipes/recipe.yml secureblue-base: FROM +test-base @@ -93,7 +100,8 @@ secureblue-base: legacy-base: FROM ../+blue-build-cli-alpine - ENV BB_TEST_LOCAL_IMAGE=/etc/bluebuild/cli_test.tar.gz + RUN apk update --no-cache && apk add bash grep jq sudo coreutils + ENV BB_TEST_LOCAL_IMAGE=/etc/bluebuild/cli_test-legacy.tar.gz ENV CLICOLOR_FORCE=1 COPY ./mock-scripts/ /usr/bin/ @@ -107,6 +115,7 @@ legacy-base: test-base: FROM ../+blue-build-cli-alpine + RUN apk update --no-cache && apk add bash grep jq sudo coreutils ENV BB_TEST_LOCAL_IMAGE=/etc/bluebuild/cli_test.tar.gz ENV CLICOLOR_FORCE=1 diff --git a/integration-tests/mock-scripts/buildah b/integration-tests/mock-scripts/buildah index cf1b83ed..5b26d730 100755 --- a/integration-tests/mock-scripts/buildah +++ b/integration-tests/mock-scripts/buildah @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash print_version_json() { local version="1.24.0" @@ -8,6 +8,11 @@ print_version_json() { main() { if [[ "$1" == "version" && "$2" == "--json" ]]; then print_version_json + elif [[ "$1" == "build" && "$6" == *"cli_test.tar.gz" ]]; then + tarpath=$(echo "$6" | awk -F ':' '{print $2}') + echo "Exporting image to a tarball (JK JUST A MOCK!)" + echo "${tarpath}" + touch $tarpath else echo 'Running buildah' fi diff --git a/integration-tests/mock-scripts/podman b/integration-tests/mock-scripts/podman index 211e2e16..679e8aee 100755 --- a/integration-tests/mock-scripts/podman +++ b/integration-tests/mock-scripts/podman @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash print_version_json() { local version="4.0.0" @@ -8,6 +8,11 @@ print_version_json() { main() { if [[ "$1" == "version" && "$2" == "-f" && "$3" == "json" ]]; then print_version_json + elif [[ "$1" == "build" && "$6" == *"cli_test.tar.gz" ]]; then + tarpath=$(echo "$6" | awk -F ':' '{print $2}') + echo "Exporting image to a tarball (JK JUST A MOCK!)" + echo "${tarpath}" + touch $tarpath else echo 'Running podman' fi diff --git a/integration-tests/mock-scripts/rpm-ostree b/integration-tests/mock-scripts/rpm-ostree index 7e8ac949..d397e02c 100755 --- a/integration-tests/mock-scripts/rpm-ostree +++ b/integration-tests/mock-scripts/rpm-ostree @@ -1,9 +1,7 @@ -#!/bin/sh +#!/bin/bash set -euo pipefail -echo 'Running rpm-ostree' - if [ "$1" = "rebase" ]; then if [ "$2" = "ostree-unverified-image:oci-archive:$BB_TEST_LOCAL_IMAGE" ]; then echo "Rebased to local image $BB_TEST_LOCAL_IMAGE" @@ -13,6 +11,23 @@ if [ "$1" = "rebase" ]; then fi elif [ "$1" = "upgrade" ]; then echo "Performing upgrade for $BB_TEST_LOCAL_IMAGE" +elif [ "$1" = "status" ]; then + cat < command.run(), + #[cfg(feature = "init")] CommandArgs::New(mut command) => command.run(), + CommandArgs::Build(mut command) => command.run(), + + CommandArgs::Generate(mut command) => command.run(), + + #[cfg(feature = "switch")] + CommandArgs::Switch(mut command) => command.run(), + + #[cfg(not(feature = "switch"))] CommandArgs::Rebase(mut command) => command.run(), + + #[cfg(not(feature = "switch"))] CommandArgs::Upgrade(mut command) => command.run(), - CommandArgs::Template(mut command) => command.run(), + CommandArgs::BugReport(mut command) => command.run(), + CommandArgs::Completions(mut command) => command.run(), } } diff --git a/src/commands.rs b/src/commands.rs index 673ec9f5..8bb0dea3 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -12,10 +12,13 @@ use crate::{ pub mod bug_report; pub mod build; pub mod completions; +pub mod generate; #[cfg(feature = "init")] pub mod init; +#[cfg(not(feature = "switch"))] pub mod local; -pub mod template; +#[cfg(feature = "switch")] +pub mod switch; pub trait BlueBuildCommand { /// Runs the command and returns a result @@ -57,7 +60,8 @@ pub enum CommandArgs { Build(build::BuildCommand), /// Generate a Containerfile from a recipe - Template(template::TemplateCommand), + #[clap(visible_alias = "template")] + Generate(generate::GenerateCommand), /// Upgrade your current OS with the /// local image saved at `/etc/bluebuild/`. @@ -69,6 +73,7 @@ pub enum CommandArgs { /// NOTE: This can only be used if you have `rpm-ostree` /// installed. This image will not be signed. #[command(visible_alias("update"))] + #[cfg(not(feature = "switch"))] Upgrade(local::UpgradeCommand), /// Rebase your current OS onto the image @@ -80,8 +85,21 @@ pub enum CommandArgs { /// /// NOTE: This can only be used if you have `rpm-ostree` /// installed. This image will not be signed. + #[cfg(not(feature = "switch"))] Rebase(local::RebaseCommand), + /// Switch your current OS onto the image + /// being built. + /// + /// This will create a tarball of your image at + /// `/etc/bluebuild/` and invoke `rpm-ostree` to + /// rebase/upgrade onto the image using `oci-archive`. + /// + /// NOTE: This can only be used if you have `rpm-ostree` + /// installed. This image will not be signed. + #[cfg(feature = "switch")] + Switch(switch::SwitchCommand), + /// Initialize a new Ublue Starting Point repo #[cfg(feature = "init")] Init(init::InitCommand), diff --git a/src/commands/build.rs b/src/commands/build.rs index 28b20ac4..53383a09 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -19,7 +19,7 @@ use log::{debug, info, trace, warn}; use typed_builder::TypedBuilder; use crate::{ - commands::template::TemplateCommand, + commands::generate::GenerateCommand, credentials, drivers::{ opts::{BuildTagPushOpts, CompressionType, GetMetadataOpts}, @@ -120,6 +120,15 @@ impl BlueBuildCommand for BuildCommand { .build() .init()?; + if self.push && self.archive.is_some() { + bail!("You cannot use '--archive' and '--push' at the same time"); + } + + if self.push { + blue_build_utils::check_command_exists("cosign")?; + check_cosign_files()?; + } + // Check if the Containerfile exists // - If doesn't => *Build* // - If it does: @@ -172,10 +181,6 @@ impl BlueBuildCommand for BuildCommand { } } - if self.push && self.archive.is_some() { - bail!("You cannot use '--archive' and '--push' at the same time"); - } - let recipe_path = self.recipe.clone().unwrap_or_else(|| { let legacy_path = Path::new(CONFIG_PATH); let recipe_path = Path::new(RECIPE_PATH); @@ -187,18 +192,12 @@ impl BlueBuildCommand for BuildCommand { } }); - TemplateCommand::builder() + GenerateCommand::builder() .recipe(&recipe_path) .output(PathBuf::from("Containerfile")) - .drivers(DriverArgs::builder().squash(self.drivers.squash).build()) .build() .try_run()?; - if self.push { - blue_build_utils::check_command_exists("cosign")?; - check_cosign_files()?; - } - info!("Building image for recipe at {}", recipe_path.display()); self.start(&recipe_path) diff --git a/src/commands/template.rs b/src/commands/generate.rs similarity index 98% rename from src/commands/template.rs rename to src/commands/generate.rs index f3f71ac9..cea56bda 100644 --- a/src/commands/template.rs +++ b/src/commands/generate.rs @@ -22,7 +22,7 @@ use crate::{drivers::Driver, shadow}; use super::{BlueBuildCommand, DriverArgs}; #[derive(Debug, Clone, Args, TypedBuilder)] -pub struct TemplateCommand { +pub struct GenerateCommand { /// The recipe file to create a template from #[arg()] #[builder(default, setter(into, strip_option))] @@ -71,7 +71,7 @@ pub struct TemplateCommand { drivers: DriverArgs, } -impl BlueBuildCommand for TemplateCommand { +impl BlueBuildCommand for GenerateCommand { fn try_run(&mut self) -> Result<()> { Driver::builder() .build_driver(self.drivers.build_driver) @@ -83,7 +83,7 @@ impl BlueBuildCommand for TemplateCommand { } } -impl TemplateCommand { +impl GenerateCommand { fn template_file(&self) -> Result<()> { trace!("TemplateCommand::template_file()"); diff --git a/src/commands/switch.rs b/src/commands/switch.rs new file mode 100644 index 00000000..d2cfdc02 --- /dev/null +++ b/src/commands/switch.rs @@ -0,0 +1,223 @@ +use std::{ + path::{Path, PathBuf}, + process::Command, +}; + +use anyhow::{bail, Result}; +use blue_build_recipe::Recipe; +use blue_build_utils::constants::{ + ARCHIVE_SUFFIX, LOCAL_BUILD, OCI_ARCHIVE, OSTREE_UNVERIFIED_IMAGE, +}; +use clap::Args; +use colored::Colorize; +use log::{debug, trace, warn}; +use tempdir::TempDir; +use typed_builder::TypedBuilder; + +use crate::{commands::build::BuildCommand, drivers::Driver, rpm_ostree_status::RpmOstreeStatus}; + +use super::{BlueBuildCommand, DriverArgs}; + +#[derive(Default, Clone, Debug, TypedBuilder, Args)] +pub struct SwitchCommand { + /// The recipe file to build an image. + #[arg()] + recipe: PathBuf, + + /// Reboot your system after + /// the update is complete. + #[arg(short, long)] + #[builder(default)] + reboot: bool, + + /// Allow `bluebuild` to overwrite an existing + /// Containerfile without confirmation. + /// + /// This is not needed if the Containerfile is in + /// .gitignore or has already been built by `bluebuild`. + #[arg(short, long)] + #[builder(default)] + force: bool, + + #[clap(flatten)] + #[builder(default)] + drivers: DriverArgs, +} + +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()?; + + let status = RpmOstreeStatus::try_new()?; + trace!("{status:?}"); + + if status.transaction_in_progress() { + bail!("There is a transaction in progress. Please cancel it using `rpm-ostree cancel`"); + } + + let tempdir = TempDir::new("oci-archive")?; + trace!("{tempdir:?}"); + + BuildCommand::builder() + .recipe(self.recipe.clone()) + .archive(tempdir.path()) + .force(self.force) + .build() + .try_run()?; + + let recipe = Recipe::parse(&self.recipe)?; + let image_file_name = format!( + "{}.{ARCHIVE_SUFFIX}", + recipe.name.to_lowercase().replace('/', "_") + ); + let temp_file_path = tempdir.path().join(&image_file_name); + let archive_path = Path::new(LOCAL_BUILD).join(&image_file_name); + + warn!( + "{notice}: {} {sudo} {}", + "The next few steps will require".yellow(), + "You may have to supply your password".yellow(), + notice = "NOTICE".bright_red().bold(), + sudo = "`sudo`.".italic().bright_red().bold(), + ); + Self::sudo_clean_local_build_dir()?; + Self::sudo_move_archive(&temp_file_path, &archive_path)?; + + // We drop the tempdir ahead of time so that the directory + // can be cleaned out. + drop(tempdir); + + self.switch(&archive_path, &status) + } +} + +impl SwitchCommand { + fn switch(&self, archive_path: &Path, status: &RpmOstreeStatus<'_>) -> Result<()> { + trace!( + "SwitchCommand::switch({}, {status:#?})", + archive_path.display() + ); + + let status = if status.is_booted_on_archive(archive_path) + || status.is_staged_on_archive(archive_path) + { + let mut command = Command::new("rpm-ostree"); + command.arg("upgrade"); + + if self.reboot { + command.arg("--reboot"); + } + + trace!( + "rpm-ostree upgrade {}", + self.reboot.then_some("--reboot").unwrap_or_default() + ); + command + } else { + let image_ref = format!( + "{OSTREE_UNVERIFIED_IMAGE}:{OCI_ARCHIVE}:{path}", + path = archive_path.display() + ); + let mut command = Command::new("rpm-ostree"); + command.arg("rebase").arg(&image_ref); + + if self.reboot { + command.arg("--reboot"); + } + + trace!( + "rpm-ostree rebase{} {image_ref}", + self.reboot.then_some(" --reboot").unwrap_or_default() + ); + command + } + .status()?; + + if !status.success() { + bail!("Failed to switch to new image!"); + } + Ok(()) + } + + fn sudo_move_archive(from: &Path, to: &Path) -> Result<()> { + trace!( + "SwitchCommand::sudo_move_archive({}, {})", + from.display(), + to.display() + ); + + trace!("sudo mv {} {}", from.display(), to.display()); + let status = Command::new("sudo").arg("mv").args([from, to]).status()?; + + if !status.success() { + bail!( + "Failed to move archive from {from} to {to}", + from = from.display(), + to = to.display() + ); + } + + Ok(()) + } + + fn sudo_clean_local_build_dir() -> Result<()> { + trace!("SwitchCommand::clean_local_build_dir()"); + + let local_build_path = Path::new(LOCAL_BUILD); + + if local_build_path.exists() { + debug!("Cleaning out build dir {LOCAL_BUILD}"); + + trace!("sudo ls {LOCAL_BUILD}"); + let output = String::from_utf8( + Command::new("sudo") + .args(["ls", LOCAL_BUILD]) + .output()? + .stdout, + )?; + + trace!("{output}"); + + let files = output + .lines() + .filter(|line| line.ends_with(ARCHIVE_SUFFIX)) + .map(|file| local_build_path.join(file).display().to_string()) + .collect::>(); + + if !files.is_empty() { + let files = files.join(" "); + + trace!("sudo rm -f {files}"); + let status = Command::new("sudo") + .args(["rm", "-f"]) + .arg(files) + .status()?; + + if !status.success() { + bail!("Failed to clean out archives in {LOCAL_BUILD}"); + } + } + } else { + debug!( + "Creating build output dir at {}", + local_build_path.display() + ); + + let status = Command::new("sudo") + .args(["mkdir", "-p", LOCAL_BUILD]) + .status()?; + + if !status.success() { + bail!("Failed to create directory {LOCAL_BUILD}"); + } + } + + Ok(()) + } +} diff --git a/src/drivers/buildah_driver.rs b/src/drivers/buildah_driver.rs index 599d3804..0850ddb1 100644 --- a/src/drivers/buildah_driver.rs +++ b/src/drivers/buildah_driver.rs @@ -1,7 +1,7 @@ use std::process::Command; use anyhow::{bail, Result}; -use log::{info, trace}; +use log::{error, info, trace}; use semver::Version; use serde::Deserialize; @@ -34,7 +34,8 @@ impl DriverVersion for BuildahDriver { .arg("--json") .output()?; - let version_json: BuildahVersionJson = serde_json::from_slice(&output.stdout)?; + let version_json: BuildahVersionJson = serde_json::from_slice(&output.stdout) + .inspect_err(|e| error!("{e}: {}", String::from_utf8_lossy(&output.stdout)))?; trace!("{version_json:#?}"); Ok(version_json.version) diff --git a/src/drivers/docker_driver.rs b/src/drivers/docker_driver.rs index 8bc4507e..8b1520a4 100644 --- a/src/drivers/docker_driver.rs +++ b/src/drivers/docker_driver.rs @@ -210,7 +210,6 @@ impl BuildDriver for DockerDriver { trace!("build --progress=plain --pull -f {CONTAINER_FILE}",); command .arg("build") - .arg("--progress=plain") .arg("--pull") .arg("-f") .arg(CONTAINER_FILE); diff --git a/src/drivers/podman_driver.rs b/src/drivers/podman_driver.rs index 4368ce90..220199ef 100644 --- a/src/drivers/podman_driver.rs +++ b/src/drivers/podman_driver.rs @@ -2,7 +2,7 @@ use std::process::{Command, Stdio}; use anyhow::{bail, Result}; use blue_build_utils::constants::SKOPEO_IMAGE; -use log::{debug, info, trace}; +use log::{debug, error, info, trace}; use semver::Version; use serde::Deserialize; @@ -44,7 +44,8 @@ impl DriverVersion for PodmanDriver { .arg("json") .output()?; - let version_json: PodmanVersionJson = serde_json::from_slice(&output.stdout)?; + let version_json: PodmanVersionJson = serde_json::from_slice(&output.stdout) + .inspect_err(|e| error!("{e}: {}", String::from_utf8_lossy(&output.stdout)))?; trace!("{version_json:#?}"); Ok(version_json.client.version) diff --git a/src/lib.rs b/src/lib.rs index ad7db7f3..f5f546f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,3 +8,4 @@ pub mod commands; pub mod credentials; pub mod drivers; pub mod image_metadata; +pub mod rpm_ostree_status; diff --git a/src/rpm_ostree_status.rs b/src/rpm_ostree_status.rs new file mode 100644 index 00000000..a07489dd --- /dev/null +++ b/src/rpm_ostree_status.rs @@ -0,0 +1,247 @@ +use std::{borrow::Cow, path::Path, process::Command}; + +use anyhow::{bail, Result}; +use log::trace; +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct RpmOstreeStatus<'a> { + deployments: Cow<'a, [RpmOstreeDeployments<'a>]>, + transactions: Option]>>, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] +struct RpmOstreeDeployments<'a> { + container_image_reference: Cow<'a, str>, + booted: bool, + staged: bool, +} + +impl<'a> RpmOstreeStatus<'a> { + /// Creates a status struct for `rpm-ostree`. + /// + /// # Errors + /// Errors if the command fails or deserialization fails. + pub fn try_new() -> Result { + blue_build_utils::check_command_exists("rpm-ostree")?; + + trace!("rpm-ostree status --json"); + let output = Command::new("rpm-ostree") + .args(["status", "--json"]) + .output()?; + + if !output.status.success() { + bail!("Failed to get `rpm-ostree` status!"); + } + + trace!("{}", String::from_utf8_lossy(&output.stdout)); + + Ok(serde_json::from_slice(&output.stdout)?) + } + + /// Checks if there is a transaction in progress. + #[must_use] + pub fn transaction_in_progress(&self) -> bool { + self.transactions.as_ref().is_some_and(|tr| !tr.is_empty()) + } + + /// Get the booted image's reference. + #[must_use] + pub fn booted_image(&self) -> Option { + Some( + self.deployments + .iter() + .find(|deployment| deployment.booted)? + .container_image_reference + .to_string(), + ) + } + + /// Get the booted image's reference. + #[must_use] + pub fn staged_image(&self) -> Option { + Some( + self.deployments + .iter() + .find(|deployment| deployment.staged)? + .container_image_reference + .to_string(), + ) + } + + #[must_use] + pub fn is_booted_on_archive

(&self, archive_path: P) -> bool + where + P: AsRef, + { + self.booted_image().is_some_and(|deployment| { + deployment + .split(':') + .last() + .is_some_and(|boot_ref| Path::new(boot_ref) == archive_path.as_ref()) + }) + } + + #[must_use] + pub fn is_staged_on_archive

(&self, archive_path: P) -> bool + where + P: AsRef, + { + self.staged_image().is_some_and(|deployment| { + deployment + .split(':') + .last() + .is_some_and(|boot_ref| Path::new(boot_ref) == archive_path.as_ref()) + }) + } +} + +#[cfg(test)] +mod test { + use std::path::Path; + + use blue_build_utils::constants::{ + ARCHIVE_SUFFIX, LOCAL_BUILD, OCI_ARCHIVE, OSTREE_IMAGE_SIGNED, OSTREE_UNVERIFIED_IMAGE, + }; + + use super::{RpmOstreeDeployments, RpmOstreeStatus}; + + fn create_image_status<'a>() -> RpmOstreeStatus<'a> { + RpmOstreeStatus { + deployments: vec![ + RpmOstreeDeployments { + container_image_reference: format!( + "{OSTREE_IMAGE_SIGNED}:docker://ghcr.io/blue-build/cli/test" + ) + .into(), + booted: true, + staged: false, + }, + RpmOstreeDeployments { + container_image_reference: format!( + "{OSTREE_IMAGE_SIGNED}:docker://ghcr.io/blue-build/cli/test:last" + ) + .into(), + booted: false, + staged: false, + }, + ] + .into(), + transactions: None, + } + } + + fn create_transaction_status<'a>() -> RpmOstreeStatus<'a> { + RpmOstreeStatus { + deployments: vec![ + RpmOstreeDeployments { + container_image_reference: format!( + "{OSTREE_IMAGE_SIGNED}:docker://ghcr.io/blue-build/cli/test" + ) + .into(), + booted: true, + staged: false, + }, + RpmOstreeDeployments { + container_image_reference: format!( + "{OSTREE_IMAGE_SIGNED}:docker://ghcr.io/blue-build/cli/test:last" + ) + .into(), + booted: false, + staged: false, + }, + ] + .into(), + transactions: Some(vec!["Upgrade".into(), "/".into()].into()), + } + } + + fn create_archive_status<'a>() -> RpmOstreeStatus<'a> { + RpmOstreeStatus { + deployments: vec![ + RpmOstreeDeployments { + container_image_reference: + format!("{OSTREE_UNVERIFIED_IMAGE}:{OCI_ARCHIVE}:{LOCAL_BUILD}/cli_test.{ARCHIVE_SUFFIX}").into(), + booted: true, + staged: false, + }, + RpmOstreeDeployments { + container_image_reference: + format!("{OSTREE_IMAGE_SIGNED}:docker://ghcr.io/blue-build/cli/test:last").into(), + booted: false, + staged: false, + }, + ] + .into(), + transactions: None, + } + } + + fn create_archive_staged_status<'a>() -> RpmOstreeStatus<'a> { + RpmOstreeStatus { + deployments: vec![ + RpmOstreeDeployments { + container_image_reference: + format!("{OSTREE_UNVERIFIED_IMAGE}:{OCI_ARCHIVE}:{LOCAL_BUILD}/cli_test.{ARCHIVE_SUFFIX}").into(), + booted: false, + staged: true, + }, + RpmOstreeDeployments { + container_image_reference: + format!("{OSTREE_UNVERIFIED_IMAGE}:{OCI_ARCHIVE}:{LOCAL_BUILD}/cli_test.{ARCHIVE_SUFFIX}").into(), + booted: true, + staged: false, + }, + RpmOstreeDeployments { + container_image_reference: + format!("{OSTREE_IMAGE_SIGNED}:docker://ghcr.io/blue-build/cli/test:last").into(), + booted: false, + staged: false, + }, + ] + .into(), + transactions: None, + } + } + + #[test] + fn test_booted_image() { + assert!(create_image_status() + .booted_image() + .expect("Contains image") + .ends_with("cli/test")); + } + + #[test] + fn test_staged_image() { + assert!(create_archive_staged_status() + .staged_image() + .expect("Contains image") + .ends_with(&format!("cli_test.{ARCHIVE_SUFFIX}"))); + } + + #[test] + fn test_transaction_in_progress() { + assert!(create_transaction_status().transaction_in_progress()); + assert!(!create_image_status().transaction_in_progress()); + } + + #[test] + fn test_is_booted_archive() { + assert!(!create_archive_status() + .is_booted_on_archive(Path::new(LOCAL_BUILD).join(format!("cli.{ARCHIVE_SUFFIX}")))); + assert!(create_archive_status().is_booted_on_archive( + Path::new(LOCAL_BUILD).join(format!("cli_test.{ARCHIVE_SUFFIX}")) + )); + } + + #[test] + fn test_is_staged_archive() { + assert!(!create_archive_staged_status() + .is_staged_on_archive(Path::new(LOCAL_BUILD).join(format!("cli.{ARCHIVE_SUFFIX}")))); + assert!(create_archive_staged_status().is_staged_on_archive( + Path::new(LOCAL_BUILD).join(format!("cli_test.{ARCHIVE_SUFFIX}")) + )); + } +} diff --git a/utils/Cargo.toml b/utils/Cargo.toml index 6a763e4c..dd3ec1c9 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -9,10 +9,10 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -atty = "0.2.14" +atty = "0.2" directories = "5" -process_control = { version = "4.0.3", features = ["crossbeam-channel"] } -syntect = "5.2.0" +process_control = { version = "4", features = ["crossbeam-channel"] } +syntect = "5" which = "6" anyhow.workspace = true diff --git a/utils/src/constants.rs b/utils/src/constants.rs index 745df12e..4eb7dc1a 100644 --- a/utils/src/constants.rs +++ b/utils/src/constants.rs @@ -62,6 +62,9 @@ pub const LC_TERMINAL_VERSION: &str = "LC_TERMINAL_VERSION"; pub const XDG_RUNTIME_DIR: &str = "XDG_RUNTIME_DIR"; // Misc +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 UNKNOWN_SHELL: &str = ""; pub const UNKNOWN_VERSION: &str = "";