refactor!: Rename template to generate and move rebase/upgrade under switch (#116)

This updates the `template` subcommand to be `generate`. The `template`
usage will continue to work as an alias to `generate`. A new `switch`
command is added that will manage both `rpm-ostree rebase` and
`rpm-ostree upgrade` and is fully replacing the respective subcommands
as a breaking change.

The new `switch` command is under the feature flag `switch` and will
currently only build for the `main` branch builds until it is moved as a
default feature (`v0.9.0`).

Closes #159
This commit is contained in:
Gerald Pinder 2024-05-26 22:47:34 -04:00 committed by GitHub
parent 968cf3db97
commit 02b2fe5434
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 672 additions and 62 deletions

View file

@ -1,5 +1,5 @@
[language-server.rust-analyzer.config]
cargo.features = []
cargo.features = "all"
[language-server.rust-analyzer.config.check]
command = "clippy"

71
Cargo.lock generated
View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 <<EOF
{
"deployments": [
{
"container-image-reference": "ostree-image-signed:docker://ghcr.io/blue-build/cli/test",
"booted": true,
"staged": false
},
{
"container-image-reference": "ostree-image-signed:docker://ghcr.io/blue-build/cli/test:last",
"booted": false,
"staged": false
}
]
}
EOF
else
echo "Arg $1 is not recognized"
exit 1

View file

@ -10,12 +10,12 @@ license.workspace = true
[dependencies]
blue-build-utils = { version = "=0.8.9", path = "../utils" }
chrono = "0.4"
indexmap = { version = "2", features = ["serde"] }
anyhow.workspace = true
chrono.workspace = true
colored.workspace = true
log.workspace = true
indexmap.workspace = true
serde.workspace = true
serde_yaml.workspace = true
serde_json.workspace = true

View file

@ -19,13 +19,25 @@ fn main() {
match args.command {
#[cfg(feature = "init")]
CommandArgs::Init(mut command) => 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(),
}
}

View file

@ -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),

View file

@ -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)

View file

@ -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()");

223
src/commands/switch.rs Normal file
View file

@ -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::<Vec<_>>();
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(())
}
}

View file

@ -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)

View file

@ -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);

View file

@ -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)

View file

@ -8,3 +8,4 @@ pub mod commands;
pub mod credentials;
pub mod drivers;
pub mod image_metadata;
pub mod rpm_ostree_status;

247
src/rpm_ostree_status.rs Normal file
View file

@ -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<Cow<'a, [Cow<'a, str>]>>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
struct RpmOstreeDeployments<'a> {
container_image_reference: Cow<'a, str>,
booted: bool,
staged: bool,
}
impl<'a> RpmOstreeStatus<'a> {
/// Creates a status struct for `rpm-ostree`.
///
/// # Errors
/// Errors if the command fails or deserialization fails.
pub fn try_new() -> Result<Self> {
blue_build_utils::check_command_exists("rpm-ostree")?;
trace!("rpm-ostree status --json");
let output = 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<String> {
Some(
self.deployments
.iter()
.find(|deployment| deployment.booted)?
.container_image_reference
.to_string(),
)
}
/// Get the booted image's reference.
#[must_use]
pub fn staged_image(&self) -> Option<String> {
Some(
self.deployments
.iter()
.find(|deployment| deployment.staged)?
.container_image_reference
.to_string(),
)
}
#[must_use]
pub fn is_booted_on_archive<P>(&self, archive_path: P) -> bool
where
P: AsRef<Path>,
{
self.booted_image().is_some_and(|deployment| {
deployment
.split(':')
.last()
.is_some_and(|boot_ref| Path::new(boot_ref) == archive_path.as_ref())
})
}
#[must_use]
pub fn is_staged_on_archive<P>(&self, archive_path: P) -> bool
where
P: AsRef<Path>,
{
self.staged_image().is_some_and(|deployment| {
deployment
.split(':')
.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}"))
));
}
}

View file

@ -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

View file

@ -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 = "<unknown shell>";
pub const UNKNOWN_VERSION: &str = "<unknown version>";