Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/system-reinstall-bootc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ log = { workspace = true }
rustix = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
shlex = { workspace = true }
tempfile = { workspace = true }
tracing = { workspace = true }
uzers = { workspace = true }
Expand Down
46 changes: 42 additions & 4 deletions crates/system-reinstall-bootc/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! The main entrypoint for the bootc system reinstallation CLI

use anyhow::{Context, Result, ensure};
use bootc_utils::CommandRunExt;
use bootc_utils::{CommandRunExt, ResultExt};
use clap::Parser;
use fn_error_context::context;
use rustix::process::getuid;
Expand All @@ -10,11 +10,14 @@ use std::time::Duration;
mod btrfs;
mod config;
mod lvm;
mod os_release;
mod podman;
mod prompt;
pub(crate) mod users;

const ROOT_KEY_MOUNT_POINT: &str = "/bootc_authorized_ssh_keys/root";
const ETC_OS_RELEASE: &str = "/etc/os-release";
const USR_LIB_OS_RELEASE: &str = "/usr/lib/os-release";

/// Reinstall the system using the provided bootc container.
///
Expand All @@ -24,17 +27,52 @@ const ROOT_KEY_MOUNT_POINT: &str = "/bootc_authorized_ssh_keys/root";
/// If the environment variable BOOTC_REINSTALL_CONFIG is set, it must be a YAML
/// file with a single member `bootc_image` that specifies the image to install.
/// This will take precedence over the CLI.
#[derive(clap::Parser)]
pub(crate) struct ReinstallOpts {
/// The bootc image to install
pub(crate) image: String,
// Note if we ever add any other options here,
pub(crate) composefs_backend: bool,
}

#[derive(clap::Parser)]
pub(crate) struct ReinstallOptsArgs {
/// The bootc image to install
pub(crate) image: Option<String>,
// Note if we ever add any other options here,
#[arg(long)]
pub(crate) composefs_backend: bool,
}

impl ReinstallOptsArgs {
pub(crate) fn build(self) -> Result<ReinstallOpts> {
let image = if let Some(image) = self.image {
image
} else {
[ETC_OS_RELEASE, USR_LIB_OS_RELEASE]
.iter()
.find_map(|path| {
os_release::get_bootc_image_from_file(path)
.log_err_default()
.filter(|s| !s.is_empty())
})
.ok_or_else(|| {
anyhow::anyhow!(
"No image provided. Specify an image or set BOOTC_IMAGE in os-release."
)
})?
};

Ok(ReinstallOpts {
image,
composefs_backend: self.composefs_backend,
})
}
}

#[context("run")]
fn run() -> Result<()> {
let args = ReinstallOptsArgs::parse();

// We historically supported an environment variable providing a config to override the image, so
Comment thread
hone marked this conversation as resolved.
// keep supporting that. I'm considering deprecating that though.
let opts = if let Some(config) = config::ReinstallConfig::load().context("loading config")? {
Expand All @@ -43,8 +81,8 @@ fn run() -> Result<()> {
composefs_backend: config.composefs_backend,
}
} else {
// Otherwise an image is required.
ReinstallOpts::parse()
// Otherwise an image is specified via the CLI or fallback to the os-release
args.build()?
};

bootc_utils::initialize_tracing();
Expand Down
108 changes: 108 additions & 0 deletions crates/system-reinstall-bootc/src/os_release.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;

use anyhow::{Context, Result};

/// Searches for the BOOTC_IMAGE key in a given os-release file.
/// Follows standard os-release(5) quoting rules.
fn parse_bootc_image_from_reader<R: BufRead>(reader: R) -> Result<Option<String>> {
let mut last_found = None;

for line in reader.lines() {
let line = line?;
let line = line.trim();

if line.is_empty() || line.starts_with('#') {
continue;
}

let Some((key, value)) = line.split_once('=') else {
continue;
};

if key.trim() == "BOOTC_IMAGE" {
if let Some(unquoted) = shlex::split(value).and_then(|mut values| values.pop()) {
last_found = Some(unquoted);
}
}
}

Ok(last_found)
}

/// Reads the provided os-release file and returns the BOOTC_IMAGE value if found.
pub(crate) fn get_bootc_image_from_file<P: AsRef<Path>>(path: P) -> Result<Option<String>> {
let file = File::open(path.as_ref()).with_context(|| format!("Opening {:?}", path.as_ref()))?;
let reader = BufReader::new(file);
parse_bootc_image_from_reader(reader)
}

#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
use std::io::Cursor;

fn parse_str(content: &str) -> Option<String> {
let reader = Cursor::new(content);
parse_bootc_image_from_reader(reader).unwrap()
}

#[test]
fn test_parse_os_release_standard() {
let content = indoc! { "
NAME=Fedora
BOOTC_IMAGE=quay.io/example/image:latest
VERSION=39
" };
assert_eq!(parse_str(content).unwrap(), "quay.io/example/image:latest");
}

#[test]
fn test_parse_os_release_double_quotes() {
let content = "BOOTC_IMAGE=\"quay.io/example/image:latest\"";
assert_eq!(parse_str(content).unwrap(), "quay.io/example/image:latest");
}

#[test]
fn test_parse_os_release_single_quotes() {
let content = "BOOTC_IMAGE='quay.io/example/image:latest'";
assert_eq!(parse_str(content).unwrap(), "quay.io/example/image:latest");
}

#[test]
fn test_parse_os_release_escaped() {
let content = indoc! { r#"
BOOTC_IMAGE="quay.io/img/with\"quote"
"# };
assert_eq!(parse_str(content).unwrap(), "quay.io/img/with\"quote");
}

#[test]
fn test_parse_os_release_missing() {
let content = indoc! { "
NAME=Fedora
VERSION=39
" };
assert!(parse_str(content).is_none());
}

#[test]
fn test_parse_os_release_comments_and_spaces() {
let content = indoc! { "
# comment
BOOTC_IMAGE= \"quay.io/img\"
" };
assert_eq!(parse_str(content).unwrap(), "quay.io/img");
}

#[test]
fn test_parse_os_release_last_wins() {
let content = indoc! { "
BOOTC_IMAGE=quay.io/old/image
BOOTC_IMAGE=quay.io/new/image
" };
assert_eq!(parse_str(content).unwrap(), "quay.io/new/image");
}
}
1 change: 0 additions & 1 deletion hack/packages.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Needed by tmt
rsync
cloud-init
/usr/bin/flock
/usr/bin/awk
# Needed by tmt avc check
Expand Down
2 changes: 1 addition & 1 deletion hack/provision-derived.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ cat <<KARGEOF >> /usr/lib/bootc/kargs.d/20-console.toml
kargs = ["console=ttyS0,115200n8"]
KARGEOF
if test $cloudinit = 1; then
dnf -y install cloud-init
dnf -y install --allowerasing cloud-init
ln -s ../cloud-init.target /usr/lib/systemd/system/default.target.wants
# Allow root SSH login for testing with bcvk/tmt
mkdir -p /etc/cloud/cloud.cfg.d
Expand Down
6 changes: 5 additions & 1 deletion hack/provision-packit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,8 @@ podman pull -q --retry 5 --retry-delay 5s quay.io/curl/curl:latest quay.io/curl/

# Run system-reinstall-bootc
# TODO make it more scriptable instead of expect + send
./system-reinstall-bootc.exp
if grep -q "^BOOTC_IMAGE=" /etc/os-release /usr/lib/os-release 2>/dev/null; then
./system-reinstall-bootc.exp
else
./system-reinstall-bootc.exp localhost/bootc
fi
8 changes: 7 additions & 1 deletion hack/system-reinstall-bootc.exp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
# Set a timeout
set timeout 600

spawn system-reinstall-bootc localhost/bootc
set image [lindex $argv 0]

if { $image != "" } {
spawn system-reinstall-bootc $image
} else {
spawn system-reinstall-bootc
}

expect {
"Then you can login as * using those keys. \\\[Y/n\\\]" {
Expand Down
10 changes: 8 additions & 2 deletions tmt/plans/integration.fmf
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ prepare:
# Run on package mode VM running on Packit and Gating
# order 9x means run it at the last job of prepare
- how: install
order: 97
order: 96
package:
- podman
- skopeo
Expand All @@ -25,12 +25,18 @@ prepare:
- e2fsprogs
when: running_env != image_mode
- how: shell
order: 98
order: 97
script:
- mkdir -p bootc && cp /var/share/test-artifacts/*.src.rpm bootc
- cd bootc && rpm2cpio *.src.rpm | cpio -idmv && rm -f *-vendor.tar.zstd && zstd -d *.tar.zstd && tar -xvf *.tar -C . --strip-components=1 && ls -al
- pwd && ls -al && cd bootc/hack && ./provision-packit.sh
when: running_env != image_mode
- how: shell
order: 98
script:
- echo 'BOOTC_IMAGE=localhost/bootc' | tee -a /usr/lib/os-release
- pwd && ls -al && cd bootc/hack && ./provision-packit.sh
when: running_env != image_mode
Comment on lines +28 to +39
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These two shell steps are redundant and inefficient. Both steps perform the same setup and run provision-packit.sh, which includes a time-consuming podman build operation. Since provision-packit.sh is already designed to handle both the explicit argument and the os-release fallback, you should combine these into a single step to avoid rebuilding the image and repeating the setup in CI.

    order: 97
    script:
      - mkdir -p bootc && cp /var/share/test-artifacts/*.src.rpm bootc
      - cd bootc && rpm2cpio *.src.rpm | cpio -idmv && rm -f *-vendor.tar.zstd && zstd -d *.tar.zstd && tar -xvf *.tar -C . --strip-components=1 && ls -al
      - pwd && ls -al && cd bootc/hack && ./provision-packit.sh
      - echo 'BOOTC_IMAGE=localhost/bootc' | tee -a /usr/lib/os-release
      - ./provision-packit.sh
    when: running_env != image_mode

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was easier to just build the image twice than try to do a cleanup step for testing the BOOTC_IMAGE path on reinstall.

Comment thread
hone marked this conversation as resolved.
# tmt-reboot and reboot do not work in this case
# reboot in ansible is the only way to reboot in tmt prepare
- how: ansible
Expand Down
Loading