Skip to content

Commit 63c9997

Browse files
authored
Merge pull request #5551 from jmarrero/fix-kernel-install
kernel-install: Support drop-in config directories for layout=ostree
2 parents 6528e39 + 3592bee commit 63c9997

File tree

3 files changed

+309
-19
lines changed

3 files changed

+309
-19
lines changed

docs/treefile.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ It supports the following parameters:
6161
and above.
6262
* "kernel-install": The system is integrated with `/sbin/kernel-install`
6363
from systemd. You likely want to additionally pair this with configuring `layout=ostree`
64-
in `/usr/lib/kernel/install.conf`, and adding a wrapper script to
64+
in `/usr/lib/kernel/install.conf` or as a drop-in in
65+
`/usr/lib/kernel/install.conf.d/` (recommended to avoid being overwritten
66+
by systemd-udev updates), and adding a wrapper script to
6567
`/usr/lib/kernel/install.d/05-rpmostree.install`
6668

6769
* `etc-group-members`: Array of strings, optional: Unix groups in this

rust/src/kernel_install.rs

Lines changed: 304 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,111 @@ const SKIP: u8 = 77;
3232
const MODULES: &str = "usr/lib/modules";
3333
/// The default name for the initramfs.
3434
const INITRAMFS: &str = "initramfs.img";
35-
/// The path to the instal.conf that sets layout.
36-
const KERNEL_INSTALL_CONF: &str = "usr/lib/kernel/install.conf";
35+
/// Config paths per kernel-install(8), checked in priority order.
36+
/// /etc takes precedence over /usr/lib (user/distro config over vendor defaults).
37+
const KERNEL_INSTALL_CONF_ETC: &str = "etc/kernel/install.conf";
38+
const KERNEL_INSTALL_CONF_ETC_D: &str = "etc/kernel/install.conf.d";
39+
const KERNEL_INSTALL_CONF_USR: &str = "usr/lib/kernel/install.conf";
40+
const KERNEL_INSTALL_CONF_USR_D: &str = "usr/lib/kernel/install.conf.d";
3741

38-
#[context("Verifying kernel-install layout file")]
39-
pub fn is_ostree_layout(rootfs: &Dir) -> Result<bool> {
40-
let Some(conf) = rootfs.open_optional(KERNEL_INSTALL_CONF)? else {
41-
return Ok(false);
42-
};
43-
let buf = BufReader::new(conf);
44-
// Check for "layout=ostree" in the file
42+
/// Parse a config file and return the layout value if found.
43+
fn get_layout_from_file(file: std::fs::File) -> Result<Option<String>> {
44+
let buf = BufReader::new(file);
4545
for line in buf.lines() {
4646
let line = line?;
47-
if line.trim() == "layout=ostree" {
48-
return Ok(true);
47+
let trimmed = line.trim();
48+
if let Some(value) = trimmed.strip_prefix("layout=") {
49+
return Ok(Some(value.to_string()));
50+
}
51+
}
52+
Ok(None)
53+
}
54+
55+
/// Parse all *.conf files in a drop-in directory and return the layout value.
56+
/// Files are processed in lexicographic order; later files override earlier ones.
57+
fn get_layout_from_dropin_dir(rootfs: &Dir, dir_path: &str) -> Result<Option<String>> {
58+
let Some(dir) = rootfs.open_dir_optional(dir_path)? else {
59+
return Ok(None);
60+
};
61+
62+
// Collect and sort entries lexicographically
63+
let mut entries: Vec<_> = dir
64+
.entries()?
65+
.filter_map(|entry| {
66+
let entry = entry.ok()?;
67+
let file_name = entry.file_name();
68+
let path = std::path::Path::new(&file_name);
69+
(path.extension() == Some(std::ffi::OsStr::new("conf"))).then_some(entry)
70+
})
71+
.collect();
72+
entries.sort_by_key(|e| e.file_name());
73+
74+
let mut layout = None;
75+
for entry in entries {
76+
if let Some(file) = dir.open_optional(entry.file_name())? {
77+
if let Some(value) = get_layout_from_file(file.into_std())? {
78+
layout = Some(value);
79+
}
4980
}
5081
}
82+
Ok(layout)
83+
}
84+
85+
/// Get the layout from a config directory level (main conf + drop-ins).
86+
/// Per systemd drop-in semantics, drop-in files are parsed AFTER the main config
87+
/// and can override values from it.
88+
fn get_layout_from_config_dir(
89+
rootfs: &Dir,
90+
main_conf: &str,
91+
dropin_dir: &str,
92+
) -> Result<Option<String>> {
93+
// Start with the main config file value (if it exists)
94+
let mut layout = None;
95+
if let Some(conf) = rootfs.open_optional(main_conf)? {
96+
layout = get_layout_from_file(conf.into_std())?;
97+
}
98+
99+
// Drop-ins override the main config (parsed after, per systemd semantics)
100+
if let Some(dropin_layout) = get_layout_from_dropin_dir(rootfs, dropin_dir)? {
101+
layout = Some(dropin_layout);
102+
}
103+
104+
Ok(layout)
105+
}
106+
107+
/// Check if the kernel-install layout is configured as "ostree".
108+
///
109+
/// NOTE: We cannot simply rely on the KERNEL_INSTALL_LAYOUT environment variable
110+
/// because this function is called in contexts where kernel-install is not running:
111+
/// - At compose time (FilesystemScriptPrep, cliwrap_write_wrappers)
112+
/// - During cliwrap interception of direct kernel-install calls
113+
///
114+
/// The shell hook 05-rpmostree.install can rely on the env var directly since
115+
/// it's invoked by kernel-install itself, which parses the config and exports
116+
/// the variable.
117+
///
118+
/// Per kernel-install(8) and systemd drop-in semantics:
119+
/// - /etc/kernel/ takes precedence over /usr/lib/kernel/
120+
/// - Within each directory, drop-in files (install.conf.d/*.conf) are parsed
121+
/// AFTER the main config (install.conf) and can override its values
122+
/// - Drop-in files are processed in lexicographic order; later files override earlier ones
123+
#[context("Verifying kernel-install layout")]
124+
pub fn is_ostree_layout(rootfs: &Dir) -> Result<bool> {
125+
// 1. Check /etc/kernel/ level (main conf + drop-ins merged)
126+
// /etc takes precedence over /usr/lib
127+
if let Some(layout) =
128+
get_layout_from_config_dir(rootfs, KERNEL_INSTALL_CONF_ETC, KERNEL_INSTALL_CONF_ETC_D)?
129+
{
130+
return Ok(layout == LAYOUT_OSTREE);
131+
}
132+
133+
// 2. Check /usr/lib/kernel/ level (main conf + drop-ins merged)
134+
if let Some(layout) =
135+
get_layout_from_config_dir(rootfs, KERNEL_INSTALL_CONF_USR, KERNEL_INSTALL_CONF_USR_D)?
136+
{
137+
return Ok(layout == LAYOUT_OSTREE);
138+
}
139+
51140
Ok(false)
52141
}
53142

@@ -142,14 +231,14 @@ mod tests {
142231
use super::*;
143232

144233
#[test]
145-
fn test_ostree_layout_parse() -> Result<()> {
234+
fn test_ostree_layout_usr_conf() -> Result<()> {
146235
let td = &cap_tempfile::tempdir(cap_std::ambient_authority())?;
147236
assert!(!is_ostree_layout(&td).unwrap());
148-
td.create_dir_all(Path::new(KERNEL_INSTALL_CONF).parent().unwrap())?;
149-
td.write(KERNEL_INSTALL_CONF, "")?;
237+
td.create_dir_all(Path::new(KERNEL_INSTALL_CONF_USR).parent().unwrap())?;
238+
td.write(KERNEL_INSTALL_CONF_USR, "")?;
150239
assert!(!is_ostree_layout(&td).unwrap());
151240
td.write(
152-
KERNEL_INSTALL_CONF,
241+
KERNEL_INSTALL_CONF_USR,
153242
indoc::indoc! { r#"
154243
# some comments
155244
@@ -158,7 +247,7 @@ mod tests {
158247
)?;
159248
assert!(!is_ostree_layout(&td).unwrap());
160249
td.write(
161-
KERNEL_INSTALL_CONF,
250+
KERNEL_INSTALL_CONF_USR,
162251
indoc::indoc! { r#"
163252
# this is an ostree layout
164253
layout=ostree
@@ -170,4 +259,203 @@ mod tests {
170259

171260
Ok(())
172261
}
262+
263+
#[test]
264+
fn test_ostree_layout_usr_dropin() -> Result<()> {
265+
let td = &cap_tempfile::tempdir(cap_std::ambient_authority())?;
266+
267+
// No config at all
268+
assert!(!is_ostree_layout(&td).unwrap());
269+
270+
// Create the drop-in directory
271+
td.create_dir_all(KERNEL_INSTALL_CONF_USR_D)?;
272+
273+
// Drop-in file without layout=ostree
274+
td.write(
275+
format!("{}/00-layout.conf", KERNEL_INSTALL_CONF_USR_D),
276+
indoc::indoc! { r#"
277+
# some config
278+
layout=bls
279+
"# },
280+
)?;
281+
assert!(!is_ostree_layout(&td).unwrap());
282+
283+
// Drop-in file with layout=ostree
284+
td.write(
285+
format!("{}/00-layout.conf", KERNEL_INSTALL_CONF_USR_D),
286+
indoc::indoc! { r#"
287+
# kernel-install will not try to run dracut and allow rpm-ostree to
288+
# take over. Rpm-ostree will use this to know that it is responsible
289+
# to run dracut and ensure that there is only one kernel in the image
290+
layout=ostree
291+
"# },
292+
)?;
293+
assert!(is_ostree_layout(&td).unwrap());
294+
295+
Ok(())
296+
}
297+
298+
#[test]
299+
fn test_ostree_layout_dropin_only() -> Result<()> {
300+
let td = &cap_tempfile::tempdir(cap_std::ambient_authority())?;
301+
302+
// Only drop-in, no main install.conf
303+
td.create_dir_all(KERNEL_INSTALL_CONF_USR_D)?;
304+
td.write(
305+
format!("{}/00-layout.conf", KERNEL_INSTALL_CONF_USR_D),
306+
"layout=ostree\n",
307+
)?;
308+
assert!(is_ostree_layout(&td).unwrap());
309+
310+
Ok(())
311+
}
312+
313+
#[test]
314+
fn test_ostree_layout_dropin_ordering() -> Result<()> {
315+
let td = &cap_tempfile::tempdir(cap_std::ambient_authority())?;
316+
317+
// Create the drop-in directory
318+
td.create_dir_all(KERNEL_INSTALL_CONF_USR_D)?;
319+
320+
// First file sets ostree, second file overrides to bls
321+
// Later files (lexicographically) should win
322+
td.write(
323+
format!("{}/00-ostree.conf", KERNEL_INSTALL_CONF_USR_D),
324+
"layout=ostree\n",
325+
)?;
326+
td.write(
327+
format!("{}/99-bls.conf", KERNEL_INSTALL_CONF_USR_D),
328+
"layout=bls\n",
329+
)?;
330+
assert!(!is_ostree_layout(&td).unwrap());
331+
332+
// Now reverse: bls first, ostree second - ostree should win
333+
td.write(
334+
format!("{}/00-bls.conf", KERNEL_INSTALL_CONF_USR_D),
335+
"layout=bls\n",
336+
)?;
337+
td.write(
338+
format!("{}/99-ostree.conf", KERNEL_INSTALL_CONF_USR_D),
339+
"layout=ostree\n",
340+
)?;
341+
assert!(is_ostree_layout(&td).unwrap());
342+
343+
Ok(())
344+
}
345+
346+
#[test]
347+
fn test_ostree_layout_etc_takes_precedence() -> Result<()> {
348+
let td = &cap_tempfile::tempdir(cap_std::ambient_authority())?;
349+
350+
// Set up /usr/lib with ostree layout
351+
td.create_dir_all(Path::new(KERNEL_INSTALL_CONF_USR).parent().unwrap())?;
352+
td.write(KERNEL_INSTALL_CONF_USR, "layout=ostree\n")?;
353+
assert!(is_ostree_layout(&td).unwrap());
354+
355+
// Now /etc overrides to bls - should take precedence
356+
td.create_dir_all(Path::new(KERNEL_INSTALL_CONF_ETC).parent().unwrap())?;
357+
td.write(KERNEL_INSTALL_CONF_ETC, "layout=bls\n")?;
358+
assert!(!is_ostree_layout(&td).unwrap());
359+
360+
// /etc with ostree should also work
361+
td.write(KERNEL_INSTALL_CONF_ETC, "layout=ostree\n")?;
362+
assert!(is_ostree_layout(&td).unwrap());
363+
364+
Ok(())
365+
}
366+
367+
#[test]
368+
fn test_ostree_layout_etc_dropin_precedence() -> Result<()> {
369+
let td = &cap_tempfile::tempdir(cap_std::ambient_authority())?;
370+
371+
// Set up /usr/lib/kernel/install.conf.d with ostree
372+
td.create_dir_all(KERNEL_INSTALL_CONF_USR_D)?;
373+
td.write(
374+
format!("{}/00-layout.conf", KERNEL_INSTALL_CONF_USR_D),
375+
"layout=ostree\n",
376+
)?;
377+
assert!(is_ostree_layout(&td).unwrap());
378+
379+
// /etc/kernel/install.conf.d overrides - should take precedence
380+
td.create_dir_all(KERNEL_INSTALL_CONF_ETC_D)?;
381+
td.write(
382+
format!("{}/00-layout.conf", KERNEL_INSTALL_CONF_ETC_D),
383+
"layout=bls\n",
384+
)?;
385+
assert!(!is_ostree_layout(&td).unwrap());
386+
387+
Ok(())
388+
}
389+
390+
/// Test the critical scenario this PR fixes: drop-in overrides main config
391+
/// within the same directory level.
392+
///
393+
/// Real-world scenario:
394+
/// - systemd-udev installs /usr/lib/kernel/install.conf with layout=bls
395+
/// - bootc adds /usr/lib/kernel/install.conf.d/00-kernel-layout.conf with layout=ostree
396+
/// - Expected: drop-in overrides main conf → layout=ostree
397+
#[test]
398+
fn test_ostree_layout_dropin_overrides_main_conf() -> Result<()> {
399+
let td = &cap_tempfile::tempdir(cap_std::ambient_authority())?;
400+
401+
// Main conf has layout=bls (simulating systemd-udev default)
402+
td.create_dir_all(Path::new(KERNEL_INSTALL_CONF_USR).parent().unwrap())?;
403+
td.write(KERNEL_INSTALL_CONF_USR, "layout=bls\n")?;
404+
assert!(!is_ostree_layout(&td).unwrap());
405+
406+
// Drop-in overrides to layout=ostree (simulating bootc config)
407+
td.create_dir_all(KERNEL_INSTALL_CONF_USR_D)?;
408+
td.write(
409+
format!("{}/00-kernel-layout.conf", KERNEL_INSTALL_CONF_USR_D),
410+
"layout=ostree\n",
411+
)?;
412+
// Drop-in should override main conf per systemd semantics!
413+
assert!(is_ostree_layout(&td).unwrap());
414+
415+
Ok(())
416+
}
417+
418+
/// Test reverse scenario: main conf has ostree, drop-in overrides to bls
419+
#[test]
420+
fn test_dropin_overrides_main_conf_to_non_ostree() -> Result<()> {
421+
let td = &cap_tempfile::tempdir(cap_std::ambient_authority())?;
422+
423+
// Main conf has layout=ostree
424+
td.create_dir_all(Path::new(KERNEL_INSTALL_CONF_USR).parent().unwrap())?;
425+
td.write(KERNEL_INSTALL_CONF_USR, "layout=ostree\n")?;
426+
assert!(is_ostree_layout(&td).unwrap());
427+
428+
// Drop-in overrides to layout=bls
429+
td.create_dir_all(KERNEL_INSTALL_CONF_USR_D)?;
430+
td.write(
431+
format!("{}/99-override.conf", KERNEL_INSTALL_CONF_USR_D),
432+
"layout=bls\n",
433+
)?;
434+
// Drop-in should override main conf
435+
assert!(!is_ostree_layout(&td).unwrap());
436+
437+
Ok(())
438+
}
439+
440+
/// Test /etc drop-in overrides /etc main conf
441+
#[test]
442+
fn test_etc_dropin_overrides_etc_main_conf() -> Result<()> {
443+
let td = &cap_tempfile::tempdir(cap_std::ambient_authority())?;
444+
445+
// /etc main conf has layout=bls
446+
td.create_dir_all(Path::new(KERNEL_INSTALL_CONF_ETC).parent().unwrap())?;
447+
td.write(KERNEL_INSTALL_CONF_ETC, "layout=bls\n")?;
448+
assert!(!is_ostree_layout(&td).unwrap());
449+
450+
// /etc drop-in overrides to layout=ostree
451+
td.create_dir_all(KERNEL_INSTALL_CONF_ETC_D)?;
452+
td.write(
453+
format!("{}/50-ostree.conf", KERNEL_INSTALL_CONF_ETC_D),
454+
"layout=ostree\n",
455+
)?;
456+
// Drop-in should override main conf
457+
assert!(is_ostree_layout(&td).unwrap());
458+
459+
Ok(())
460+
}
173461
}

src/libpriv/05-rpmostree.install

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/bash
2-
# Check if install.conf is missing or does not include layout=ostree
3-
if [ ! -f /usr/lib/kernel/install.conf ] || ! grep -q layout=ostree /usr/lib/kernel/install.conf; then
2+
# kernel-install sets KERNEL_INSTALL_LAYOUT from install.conf and install.conf.d/
3+
if [[ "$KERNEL_INSTALL_LAYOUT" != "ostree" ]]; then
44
exit 0
55
fi
66
# This is the hook that has kernel-install call into rpm-ostree kernel-install

0 commit comments

Comments
 (0)