Conversation
There was a problem hiding this comment.
Code Review
This pull request implements a composefs backend for source-tracked kernel arguments, enabling direct modification of BLS entry files on /boot. This approach is more efficient for composefs-booted systems as it avoids ostree-specific staging and finalization. The changes include a new loader_entries module for composefs, CLI updates to route commands based on storage type, and comprehensive integration tests. Feedback focuses on improving the robustness of BLS entry updates: specifically, ensuring all matching entries are updated when multiple files share a version string and properly propagating errors when updating staged entries to prevent inconsistent system states.
| ) -> Result<bool> { | ||
| for entry in entries_dir.entries_utf8()? { | ||
| let entry = entry?; | ||
| let file_name = entry.file_name()?; | ||
| if !file_name.ends_with(".conf") { | ||
| continue; | ||
| } | ||
| let content = entries_dir | ||
| .read_to_string(&file_name) | ||
| .with_context(|| format!("Reading BLS entry {file_name}"))?; | ||
| let mut bls = | ||
| parse_bls_config(&content).with_context(|| format!("Parsing BLS entry {file_name}"))?; | ||
|
|
||
| if bls.version().to_string() != target_version { | ||
| continue; | ||
| } | ||
|
|
||
| // Skip EFI/UKI entries — can't modify their options | ||
| if !matches!(bls.cfg_type, BLSConfigType::NonEFI { .. }) { | ||
| continue; | ||
| } | ||
|
|
||
| let current_options = get_options_str(&bls)?; | ||
| let source_options = extract_source_options_from_extra(&bls); | ||
| let merged = compute_merged_options(¤t_options, &source_options, source, new_options); | ||
|
|
||
| update_bls_config(&mut bls, &merged, source, new_options, &source_options)?; | ||
|
|
||
| entries_dir | ||
| .atomic_write(&file_name, bls.to_string().as_bytes()) | ||
| .with_context(|| format!("Writing updated BLS entry {file_name}"))?; | ||
|
|
||
| tracing::info!("Updated BLS entry '{file_name}' with kargs for source '{source}'"); | ||
| return Ok(true); | ||
| } | ||
| Ok(false) | ||
| } |
There was a problem hiding this comment.
The update_bls_entry_in_dir function currently returns early after finding the first entry that matches target_version. If multiple BLS entries share the same version (which can happen with rollback entries as noted in your comments on line 234), only the first one encountered in the directory iteration will be updated. This can lead to inconsistent kernel arguments across deployments that share a version string.
Consider iterating through all entries and updating every match to ensure consistency.
fn update_bls_entry_in_dir(
entries_dir: &Dir,
target_version: &str,
source: &SourceName,
new_options: Option<&str>,
) -> Result<bool> {
let mut found = false;
for entry in entries_dir.entries_utf8()? {
let entry = entry?;
let file_name = entry.file_name()?;
if !file_name.ends_with(".conf") {
continue;
}
let content = entries_dir
.read_to_string(&file_name)
.with_context(|| format!("Reading BLS entry {file_name}"))?;
let mut bls =
parse_bls_config(&content).with_context(|| format!("Parsing BLS entry {file_name}"))?;
if bls.version().to_string() != target_version {
continue;
}
// Skip EFI/UKI entries — can't modify their options
if !matches!(bls.cfg_type, BLSConfigType::NonEFI { .. }) {
continue;
}
let current_options = get_options_str(&bls)?;
let source_options = extract_source_options_from_extra(&bls);
let merged = compute_merged_options(¤t_options, &source_options, source, new_options);
update_bls_config(&mut bls, &merged, source, new_options, &source_options)?;
entries_dir
.atomic_write(&file_name, bls.to_string().as_bytes())
.with_context(|| format!("Writing updated BLS entry {file_name}"))?;
tracing::info!("Updated BLS entry '{file_name}' with kargs for source '{source}'");
found = true;
}
Ok(found)
}| let staged_version = staged_bls.version().to_string(); | ||
| // Update each staged entry (best effort — some may be rollback | ||
| // entries that share our version) | ||
| let _ = update_bls_entry_in_dir(&staged_dir, &staged_version, &source, new_options); |
There was a problem hiding this comment.
Errors encountered while updating staged BLS entries are currently ignored. If an update fails (e.g., due to I/O issues), the system might be left in an inconsistent state where the booted entry is updated but staged entries are not. This could lead to the kernel argument changes being lost after a subsequent upgrade finalization. It is recommended to propagate these errors.
update_bls_entry_in_dir(&staged_dir, &staged_version, &source, new_options)
.with_context(|| format!("Updating staged BLS entry (version '{staged_version}')"))?;|
I don't like the "Changes" section in commit messages like this. I did bootc-dev/infra#171 to try to tweak that. |
| match &mut bls.cfg_type { | ||
| BLSConfigType::NonEFI { options, .. } => { | ||
| *options = Some(merged_options.clone()); | ||
| } | ||
| _ => anyhow::bail!("BLS entry is not a NonEFI (BLS) type"), | ||
| } |
There was a problem hiding this comment.
Some overlap with get_options_str above
| @@ -0,0 +1,209 @@ | |||
| # number: 43 | |||
| # tmt: | |||
| # summary: Test bootc loader-entries set-options-for-source on composefs | |||
There was a problem hiding this comment.
But can't we have just one test that works on both backends?
| //! # Composefs backend for source-tracked kernel arguments | ||
| //! | ||
| //! This module implements `set-options-for-source` for composefs-booted systems. | ||
| //! Unlike the ostree path (which stages a new deployment via ostree APIs), the |
There was a problem hiding this comment.
That's not true, but we were missing docs for it. I took a stab at this in #2168
This came up during review of PR bootc-dev#2161, where the PR description incorrectly claimed composefs status detection conflates booted and staged deployments. The composefs-finalize-staged man page was mostly a stub, and what we really want to document is the service, not the command. Make the command hidden and move the man page to be about the service. Update the internals docs for more about this too. Assisted-by: OpenCode (Claude Opus 4) Signed-off-by: Colin Walters <walters@verbum.org>
This came up during review of PR bootc-dev#2161, where the PR description incorrectly claimed composefs status detection conflates booted and staged deployments. The composefs-finalize-staged man page was mostly a stub, and what we really want to document is the service, not the command. Make the command hidden and move the man page to be about the service. Update the internals docs for more about this too. Assisted-by: OpenCode (Claude Opus 4) Signed-off-by: Colin Walters <walters@verbum.org>
ddfa14f to
8b5372a
Compare
8b5372a to
93123d8
Compare
| /// | ||
| /// Finds the entry matching the given version, parses it, applies the | ||
| /// source kargs change, and writes it back atomically. | ||
| fn update_bls_entry_in_dir( |
There was a problem hiding this comment.
We already have a function to achieve this get_sorted_type1_boot_entries, and I think this can be refactored with the rollback code
| //! | ||
| //! This module implements `set-options-for-source` for composefs-booted systems. | ||
| //! Unlike the ostree path (which stages a new deployment via ostree APIs), the | ||
| //! composefs path directly modifies BLS entry files on /boot. This is both |
There was a problem hiding this comment.
I thought about this but it's not compatible with how ostree does things. I think users will expect a similar behaviour to ostree, i.e. expecting a rollback deployment w/o the new kargs.
Re-applying an unchanged source staged a new deployment whenever other kargs followed its own on the options line: the old options were removed and the new ones appended at the end, so the line changed. That happens as soon as a second source or e.g. `rpm-ostree kargs` adds something after it, which is TuneD re-applying its profile. Replace the options where the old ones were. This also keeps kernel argument order stable, which matters where the last occurrence wins. Generated-by: AI I am knowledgeable in this problem domain and reviewed it carefully. Signed-off-by: Joseph Marrero Corchado <jmarrero@redhat.com>
Upgrade and switch computed kargs from the booted deployment, so a change that was only staged (`loader-entries set-options-for-source`, `rpm-ostree kargs`) was silently dropped when the staged deployment was replaced -- e.g. TuneD applying a profile and an automatic `bootc upgrade` running before the reboot. rpm-ostree deliberately builds on the pending deployment so operations chain; do the same. Image-provided kargs are unaffected, since the kargs.d diff is applied relative to whichever deployment we build on. Generated-by: AI I am knowledgeable in this problem domain and reviewed it carefully. Signed-off-by: Joseph Marrero Corchado <jmarrero@redhat.com>
Cover bootc staging source-tracked kargs and rpm-ostree re-staging on the same boot: the x-options-source-* keys must survive, which is the fallback fixed in ostreedev/ostree#3611. Also cover the two bootc fixes before this: re-applying an unchanged source is a no-op, and a `bootc switch` on top of a staged source removal keeps the removal. Along the way, find the booted BLS entry by its ostree= karg instead of taking the last one, and expect a removed source to leave an empty tombstone key, since set-options-for-source never deletes keys. Generated-by: AI I am knowledgeable in this problem domain and reviewed it carefully. Signed-off-by: Joseph Marrero Corchado <jmarrero@redhat.com>
kernel-arguments.md still said bootc has no API for per-machine kernel arguments. Describe `loader-entries set-options-for-source`: the ownership keys, how a call is processed, how upgrade/switch and rpm-ostree interact with it, and what TuneD does with it, with a link to the ostree documentation for how the keys survive staging. Generated-by: AI I am knowledgeable in this problem domain and reviewed it carefully. Signed-off-by: Joseph Marrero Corchado <jmarrero@redhat.com>
Both write a primary and a secondary Type1 entry the same way, and rollback hardcoded the OS id in the file names it regenerated; take it from the sort key instead. Prep for `loader-entries set-options-for-source` writing entries too. Generated-by: AI I am knowledgeable in this problem domain and reviewed it carefully.
Everything identifies a boot entry by its composefs= digest, but a kernel-argument change (`loader-entries set-options-for-source`) is going to keep the booted deployment's previous entry as its rollback, so up to three entries can share the booted digest. Pick the booted entry as the one whose options are all on /proc/cmdline, preferring the largest such set; classify the other active entry as the rollback and a pending one as staged; and never report a same-digest entry as soft-reboot capable, since kernel arguments need a real reboot. Generated-by: AI I am knowledgeable in this problem domain and reviewed it carefully.
Upgrade and switch built the new entry from the booted entry, so a change that was only staged (`loader-entries set-options-for-source`, an earlier switch) was silently dropped when the staged deployment was replaced. Build on the pending entry instead, diffing kargs.d against that deployment's tree rather than the booted one, as rpm-ostree and the ostree backend do. Generated-by: AI I am knowledgeable in this problem domain and reviewed it carefully.
bootc owns the BLS entries on composefs systems, so instead of staging through ostree, stage the booted deployment itself: write a new default entry with the merged options line and keep the current entry as the rollback, recorded like any staged deployment so finalization installs it at shutdown and `bootc rollback` undoes it, as on ostree. If an upgrade is already staged, its pending entry is rewritten so the change rides along with it. Upgrades carry the x-options-source-* keys of the entry they build on into the new one; without them a source could never be removed again. Finalizing such a deployment skips the /etc merge since it shares the state directory, and `bootc upgrade` no longer takes it for an update that is already staged. UKI boot is rejected: the arguments are embedded in the image. A removed source's key is deleted rather than tombstoned, since bootc writes the whole entry. The integration test now runs on both backends and covers rolling back a kernel-argument change; the rpm-ostree cross-consumer scenario stays ostree-only. Closes: bootc-dev#899 Generated-by: AI I am knowledgeable in this problem domain and reviewed it carefully.
93123d8 to
0daec42
Compare
The ostree path for set-options-for-source (merged in fb8c668) stages a new deployment via ostree APIs. On composefs-booted systems this approach cannot work because the status detection logic treats any entry whose verity matches the booted deployment as "booted" rather than "staged", making same-image kargs-only changes invisible to finalization.
Instead, the composefs path directly modifies BLS entry files on /boot. This is architecturally correct because bootc already manages BLS entries directly on composefs systems, and the BLSConfig.extra HashMap already preserves x-options-source-* extension keys through parse/write roundtrips. No GVariant serialization, ostree version gating, or finalization service is needed.
Changes:
Closes: #899
Assisted-by: OpenCode (Claude Opus 4.6)