Skip to content

Commit 490b2ce

Browse files
committed
system-reinstall-bootc: fallback image from /usr/lib/os-release
Add a fallback option when arguments are specified for an image. It works in this order: 1. BOOTC_REINSTALL_CONFIG env var to a YAML file 2. `--image` flag 3. reads /etc/os-release for BOOTC_IMAGE 4. reads /usr/lib/os-release for BOOTC_IMAGE Fixes: #1300 Signed-off-by: Terence Lee <hone02@gmail.com>
1 parent 8ef2ae9 commit 490b2ce

7 files changed

Lines changed: 173 additions & 9 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/system-reinstall-bootc/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ log = { workspace = true }
2828
rustix = { workspace = true }
2929
serde = { workspace = true, features = ["derive"] }
3030
serde_json = { workspace = true }
31+
shlex = { workspace = true }
3132
tempfile = { workspace = true }
3233
tracing = { workspace = true }
3334
uzers = { workspace = true }

crates/system-reinstall-bootc/src/main.rs

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! The main entrypoint for the bootc system reinstallation CLI
22
3-
use anyhow::{Context, Result, ensure};
4-
use bootc_utils::CommandRunExt;
3+
use anyhow::{ensure, Context, Result};
4+
use bootc_utils::{CommandRunExt, ResultExt};
55
use clap::Parser;
66
use fn_error_context::context;
77
use rustix::process::getuid;
@@ -10,11 +10,14 @@ use std::time::Duration;
1010
mod btrfs;
1111
mod config;
1212
mod lvm;
13+
mod os_release;
1314
mod podman;
1415
mod prompt;
1516
pub(crate) mod users;
1617

1718
const ROOT_KEY_MOUNT_POINT: &str = "/bootc_authorized_ssh_keys/root";
19+
const ETC_OS_RELEASE: &str = "/etc/os-release";
20+
const USR_LIB_OS_RELEASE: &str = "/usr/lib/os-release";
1821

1922
/// Reinstall the system using the provided bootc container.
2023
///
@@ -24,17 +27,52 @@ const ROOT_KEY_MOUNT_POINT: &str = "/bootc_authorized_ssh_keys/root";
2427
/// If the environment variable BOOTC_REINSTALL_CONFIG is set, it must be a YAML
2528
/// file with a single member `bootc_image` that specifies the image to install.
2629
/// This will take precedence over the CLI.
27-
#[derive(clap::Parser)]
2830
pub(crate) struct ReinstallOpts {
2931
/// The bootc image to install
3032
pub(crate) image: String,
3133
// Note if we ever add any other options here,
34+
pub(crate) composefs_backend: bool,
35+
}
36+
37+
#[derive(clap::Parser)]
38+
pub(crate) struct ReinstallOptsArgs {
39+
/// The bootc image to install
40+
pub(crate) image: Option<String>,
41+
// Note if we ever add any other options here,
3242
#[arg(long)]
3343
pub(crate) composefs_backend: bool,
3444
}
3545

46+
impl ReinstallOptsArgs {
47+
pub(crate) fn build(self) -> Result<ReinstallOpts> {
48+
let image = if let Some(image) = self.image {
49+
image
50+
} else {
51+
[ETC_OS_RELEASE, USR_LIB_OS_RELEASE]
52+
.iter()
53+
.find_map(|path| {
54+
os_release::get_bootc_image_from_file(path)
55+
.log_err_default()
56+
.filter(|s| !s.is_empty())
57+
})
58+
.ok_or_else(|| {
59+
anyhow::anyhow!(
60+
"No image provided. Specify an image or set BOOTC_IMAGE in os-release."
61+
)
62+
})?
63+
};
64+
65+
Ok(ReinstallOpts {
66+
image,
67+
composefs_backend: self.composefs_backend,
68+
})
69+
}
70+
}
71+
3672
#[context("run")]
3773
fn run() -> Result<()> {
74+
let args = ReinstallOptsArgs::parse();
75+
3876
// We historically supported an environment variable providing a config to override the image, so
3977
// keep supporting that. I'm considering deprecating that though.
4078
let opts = if let Some(config) = config::ReinstallConfig::load().context("loading config")? {
@@ -43,8 +81,8 @@ fn run() -> Result<()> {
4381
composefs_backend: config.composefs_backend,
4482
}
4583
} else {
46-
// Otherwise an image is required.
47-
ReinstallOpts::parse()
84+
// Otherwise an image is specified via the CLI or fallback to the os-release
85+
args.build()?
4886
};
4987

5088
bootc_utils::initialize_tracing();
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use std::fs::File;
2+
use std::io::{BufRead, BufReader};
3+
use std::path::Path;
4+
5+
use anyhow::{Context, Result};
6+
7+
/// Searches for the BOOTC_IMAGE key in a given os-release file.
8+
/// Follows standard os-release(5) quoting rules.
9+
fn parse_bootc_image_from_reader<R: BufRead>(reader: R) -> Result<Option<String>> {
10+
let mut last_found = None;
11+
12+
for line in reader.lines() {
13+
let line = line?;
14+
let line = line.trim();
15+
16+
if line.is_empty() || line.starts_with('#') {
17+
continue;
18+
}
19+
20+
let Some((key, value)) = line.split_once('=') else {
21+
continue;
22+
};
23+
24+
if key.trim() == "BOOTC_IMAGE" {
25+
if let Some(unquoted) = shlex::split(value).and_then(|mut values| values.pop()) {
26+
last_found = Some(unquoted);
27+
}
28+
}
29+
}
30+
31+
Ok(last_found)
32+
}
33+
34+
/// Reads the provided os-release file and returns the BOOTC_IMAGE value if found.
35+
pub(crate) fn get_bootc_image_from_file<P: AsRef<Path>>(path: P) -> Result<Option<String>> {
36+
let file = File::open(path.as_ref()).with_context(|| format!("Opening {:?}", path.as_ref()))?;
37+
let reader = BufReader::new(file);
38+
parse_bootc_image_from_reader(reader)
39+
}
40+
41+
#[cfg(test)]
42+
mod tests {
43+
use super::*;
44+
use indoc::indoc;
45+
use std::io::Cursor;
46+
47+
fn parse_str(content: &str) -> Option<String> {
48+
let reader = Cursor::new(content);
49+
parse_bootc_image_from_reader(reader).unwrap()
50+
}
51+
52+
#[test]
53+
fn test_parse_os_release_standard() {
54+
let content = indoc! { "
55+
NAME=Fedora
56+
BOOTC_IMAGE=quay.io/example/image:latest
57+
VERSION=39
58+
" };
59+
assert_eq!(parse_str(content).unwrap(), "quay.io/example/image:latest");
60+
}
61+
62+
#[test]
63+
fn test_parse_os_release_double_quotes() {
64+
let content = "BOOTC_IMAGE=\"quay.io/example/image:latest\"";
65+
assert_eq!(parse_str(content).unwrap(), "quay.io/example/image:latest");
66+
}
67+
68+
#[test]
69+
fn test_parse_os_release_single_quotes() {
70+
let content = "BOOTC_IMAGE='quay.io/example/image:latest'";
71+
assert_eq!(parse_str(content).unwrap(), "quay.io/example/image:latest");
72+
}
73+
74+
#[test]
75+
fn test_parse_os_release_escaped() {
76+
let content = indoc! { r#"
77+
BOOTC_IMAGE="quay.io/img/with\"quote"
78+
"# };
79+
assert_eq!(parse_str(content).unwrap(), "quay.io/img/with\"quote");
80+
}
81+
82+
#[test]
83+
fn test_parse_os_release_missing() {
84+
let content = indoc! { "
85+
NAME=Fedora
86+
VERSION=39
87+
" };
88+
assert!(parse_str(content).is_none());
89+
}
90+
91+
#[test]
92+
fn test_parse_os_release_comments_and_spaces() {
93+
let content = indoc! { "
94+
# comment
95+
BOOTC_IMAGE= \"quay.io/img\"
96+
" };
97+
assert_eq!(parse_str(content).unwrap(), "quay.io/img");
98+
}
99+
100+
#[test]
101+
fn test_parse_os_release_last_wins() {
102+
let content = indoc! { "
103+
BOOTC_IMAGE=quay.io/old/image
104+
BOOTC_IMAGE=quay.io/new/image
105+
" };
106+
assert_eq!(parse_str(content).unwrap(), "quay.io/new/image");
107+
}
108+
}

hack/provision-packit.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,8 @@ podman pull -q --retry 5 --retry-delay 5s quay.io/curl/curl:latest quay.io/curl/
9393

9494
# Run system-reinstall-bootc
9595
# TODO make it more scriptable instead of expect + send
96-
./system-reinstall-bootc.exp
96+
if grep -q "^BOOTC_IMAGE=" /etc/os-release /usr/lib/os-release 2>/dev/null; then
97+
./system-reinstall-bootc.exp
98+
else
99+
./system-reinstall-bootc.exp localhost/bootc
100+
fi

hack/system-reinstall-bootc.exp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
# Set a timeout
44
set timeout 600
55

6-
spawn system-reinstall-bootc localhost/bootc
6+
set image [lindex $argv 0]
7+
8+
if { $image != "" } {
9+
spawn system-reinstall-bootc $image
10+
} else {
11+
spawn system-reinstall-bootc
12+
}
713

814
expect {
915
"Then you can login as * using those keys. \\\[Y/n\\\]" {

tmt/plans/integration.fmf

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ prepare:
88
# Run on package mode VM running on Packit and Gating
99
# order 9x means run it at the last job of prepare
1010
- how: install
11-
order: 97
11+
order: 96
1212
package:
1313
- podman
1414
- skopeo
@@ -25,12 +25,18 @@ prepare:
2525
- e2fsprogs
2626
when: running_env != image_mode
2727
- how: shell
28-
order: 98
28+
order: 97
2929
script:
3030
- mkdir -p bootc && cp /var/share/test-artifacts/*.src.rpm bootc
3131
- 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
3232
- pwd && ls -al && cd bootc/hack && ./provision-packit.sh
3333
when: running_env != image_mode
34+
- how: shell
35+
order: 98
36+
script:
37+
- echo 'BOOTC_IMAGE=localhost/bootc' | tee -a /usr/lib/os-release
38+
- pwd && ls -al && cd bootc/hack && ./provision-packit.sh
39+
when: running_env != image_mode
3440
# tmt-reboot and reboot do not work in this case
3541
# reboot in ansible is the only way to reboot in tmt prepare
3642
- how: ansible

0 commit comments

Comments
 (0)