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:
parent
968cf3db97
commit
02b2fe5434
21 changed files with 672 additions and 62 deletions
|
|
@ -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
71
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
31
Cargo.toml
31
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
|
||||
|
|
|
|||
12
README.md
12
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
223
src/commands/switch.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
247
src/rpm_ostree_status.rs
Normal 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}"))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue