Skip to content

Commit a095e34

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 b1fe5d6 commit a095e34

5 files changed

Lines changed: 179 additions & 8 deletions

File tree

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

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! The main entrypoint for the bootc system reinstallation CLI
22
3-
use anyhow::{Context, Result, ensure};
3+
use anyhow::{ensure, Context, Result};
44
use bootc_utils::CommandRunExt;
55
use clap::Parser;
66
use fn_error_context::context;
@@ -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,15 +27,49 @@ 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+
os_release::get_bootc_image_from_file(ETC_OS_RELEASE)
52+
.ok()
53+
.flatten()
54+
.or_else(|| {
55+
os_release::get_bootc_image_from_file(USR_LIB_OS_RELEASE)
56+
.ok()
57+
.flatten()
58+
})
59+
.ok_or_else(|| {
60+
anyhow::anyhow!(
61+
"No image provided. Specify an image or set BOOTC_IMAGE in os-release."
62+
)
63+
})?
64+
};
65+
66+
Ok(ReinstallOpts {
67+
image,
68+
composefs_backend: self.composefs_backend,
69+
})
70+
}
71+
}
72+
3673
#[context("run")]
3774
fn run() -> Result<()> {
3875
// We historically supported an environment variable providing a config to override the image, so
@@ -43,8 +80,8 @@ fn run() -> Result<()> {
4380
composefs_backend: config.composefs_backend,
4481
}
4582
} else {
46-
// Otherwise an image is required.
47-
ReinstallOpts::parse()
83+
// Otherwise an image is specified via the CLI or fallback to the os-release
84+
ReinstallOptsArgs::parse().build()?
4885
};
4986

5087
bootc_utils::initialize_tracing();
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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+
if let Some((key, value)) = line.split_once('=') {
21+
if key.trim() == "BOOTC_IMAGE" {
22+
let value = value.trim();
23+
24+
if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 {
25+
let unquoted = &value[1..value.len() - 1];
26+
let processed = unquoted
27+
.replace(r#"\""#, "\"")
28+
.replace(r#"\\"#, "\\")
29+
.replace(r#"\$"#, "$")
30+
.replace(r#"\`"#, "`");
31+
last_found = Some(processed);
32+
} else if value.starts_with('\'') && value.ends_with('\'') && value.len() >= 2 {
33+
last_found = Some(value[1..value.len() - 1].to_string());
34+
} else {
35+
last_found = Some(value.to_string());
36+
}
37+
}
38+
}
39+
}
40+
41+
Ok(last_found)
42+
}
43+
44+
/// Reads the provided os-release file and returns the BOOTC_IMAGE value if found.
45+
pub(crate) fn get_bootc_image_from_file<P: AsRef<Path>>(path: P) -> Result<Option<String>> {
46+
let file = File::open(path.as_ref()).with_context(|| format!("Opening {:?}", path.as_ref()))?;
47+
let reader = BufReader::new(file);
48+
parse_bootc_image_from_reader(reader)
49+
}
50+
51+
#[cfg(test)]
52+
mod tests {
53+
use super::*;
54+
use indoc::indoc;
55+
use std::io::Cursor;
56+
57+
fn parse_str(content: &str) -> Option<String> {
58+
let reader = Cursor::new(content);
59+
parse_bootc_image_from_reader(reader).unwrap()
60+
}
61+
62+
#[test]
63+
fn test_parse_os_release_standard() {
64+
let content = indoc! { "
65+
NAME=Fedora
66+
BOOTC_IMAGE=quay.io/example/image:latest
67+
VERSION=39
68+
" };
69+
assert_eq!(parse_str(content).unwrap(), "quay.io/example/image:latest");
70+
}
71+
72+
#[test]
73+
fn test_parse_os_release_double_quotes() {
74+
let content = "BOOTC_IMAGE=\"quay.io/example/image:latest\"";
75+
assert_eq!(parse_str(content).unwrap(), "quay.io/example/image:latest");
76+
}
77+
78+
#[test]
79+
fn test_parse_os_release_single_quotes() {
80+
let content = "BOOTC_IMAGE='quay.io/example/image:latest'";
81+
assert_eq!(parse_str(content).unwrap(), "quay.io/example/image:latest");
82+
}
83+
84+
#[test]
85+
fn test_parse_os_release_escaped() {
86+
let content = indoc! { r#"
87+
BOOTC_IMAGE="quay.io/img/with\"quote"
88+
"# };
89+
assert_eq!(parse_str(content).unwrap(), "quay.io/img/with\"quote");
90+
}
91+
92+
#[test]
93+
fn test_parse_os_release_missing() {
94+
let content = indoc! { "
95+
NAME=Fedora
96+
VERSION=39
97+
" };
98+
assert!(parse_str(content).is_none());
99+
}
100+
101+
#[test]
102+
fn test_parse_os_release_comments_and_spaces() {
103+
let content = indoc! { "
104+
# comment
105+
BOOTC_IMAGE= \"quay.io/img\"
106+
" };
107+
assert_eq!(parse_str(content).unwrap(), "quay.io/img");
108+
}
109+
110+
#[test]
111+
fn test_parse_os_release_last_wins() {
112+
let content = indoc! { "
113+
BOOTC_IMAGE=quay.io/old/image
114+
BOOTC_IMAGE=quay.io/new/image
115+
" };
116+
assert_eq!(parse_str(content).unwrap(), "quay.io/new/image");
117+
}
118+
}

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)