Skip to content

Commit 8941353

Browse files
cfs/stage: Compute etc diff before staging
Before we write the staged deployment file at `/run/composefs` and before we create new boot entries, we now compute the etc diff in advance to check whethere we will face any merge conflicts while finalizing the deployment. If we find conflicts we exit with and error and detailing where conflicts were found Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
1 parent 6ae1ace commit 8941353

5 files changed

Lines changed: 66 additions & 29 deletions

File tree

crates/etc-merge/src/lib.rs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ fn stat_eq_ignore_mtime(this: &Stat, other: &Stat) -> bool {
8080

8181
#[derive(Debug)]
8282
pub struct UnmergablePaths {
83-
path: PathBuf,
84-
reason: String,
83+
pub path: PathBuf,
84+
pub reason: String,
8585
}
8686

8787
/// Represents the differences between two directory trees.
@@ -95,7 +95,7 @@ pub struct Diff {
9595
/// Paths that exist in the pristine /etc but not in the current one
9696
removed: Vec<PathBuf>,
9797
/// Paths that are unmergable
98-
unmergable_paths: Vec<UnmergablePaths>,
98+
pub unmergable_paths: Vec<UnmergablePaths>,
9999
}
100100

101101
fn collect_all_files(
@@ -486,6 +486,19 @@ pub fn compute_diff(
486486
Ok(diff)
487487
}
488488

489+
pub fn print_unmergable_paths(diff: &Diff, writer: &mut impl Write) {
490+
use owo_colors::OwoColorize;
491+
492+
for unmergable in &diff.unmergable_paths {
493+
let _ = writeln!(
494+
writer,
495+
"{} {}",
496+
ModificationType::Unmergable.magenta(),
497+
unmergable.reason
498+
);
499+
}
500+
}
501+
489502
/// Prints a colorized summary of differences to standard output.
490503
pub fn print_diff(diff: &Diff, writer: &mut impl Write) {
491504
use owo_colors::OwoColorize;
@@ -502,14 +515,7 @@ pub fn print_diff(diff: &Diff, writer: &mut impl Write) {
502515
let _ = writeln!(writer, "{} {removed:?}", ModificationType::Removed.red());
503516
}
504517

505-
for unmergable in &diff.unmergable_paths {
506-
let _ = writeln!(
507-
writer,
508-
"{} {}",
509-
ModificationType::Unmergable.magenta(),
510-
unmergable.reason
511-
);
512-
}
518+
print_unmergable_paths(diff, writer);
513519
}
514520

515521
#[context("Collecting xattrs")]

crates/lib/src/bootc_composefs/finalize.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,17 @@ use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
1414
use cap_std_ext::dirext::CapStdExtDirExt;
1515
use composefs::generic_tree::{FileSystem, Stat};
1616
use composefs_ctl::composefs;
17-
use etc_merge::{compute_diff, merge, print_diff, traverse_etc};
17+
use etc_merge::{Diff, compute_diff, merge, print_diff, traverse_etc};
1818
use rustix::fs::fsync;
1919

2020
use fn_error_context::context;
2121

22-
pub(crate) async fn get_etc_diff(storage: &Storage, booted_cfs: &BootedComposefs) -> Result<()> {
22+
pub(crate) async fn get_etc_diff(
23+
storage: &Storage,
24+
booted_cfs: &BootedComposefs,
25+
new_etc: Option<&Dir>,
26+
print: bool,
27+
) -> Result<Diff> {
2328
let host = get_composefs_status(storage, booted_cfs).await?;
2429
let booted_composefs = host.require_composefs_booted()?;
2530

@@ -37,16 +42,22 @@ pub(crate) async fn get_etc_diff(storage: &Storage, booted_cfs: &BootedComposefs
3742
Dir::open_ambient_dir(erofs_tmp_mnt.dir.path().join("etc"), ambient_authority())?;
3843
let current_etc = Dir::open_ambient_dir("/etc", ambient_authority())?;
3944

40-
let (pristine_files, current_files, _) = traverse_etc(&pristine_etc, &current_etc, None)?;
45+
let (pristine_files, current_files, new_etc_files) =
46+
traverse_etc(&pristine_etc, &current_etc, new_etc)?;
47+
4148
let diff = compute_diff(
4249
&pristine_files,
4350
&current_files,
44-
&FileSystem::new(Stat::uninitialized()),
51+
&new_etc_files
52+
.as_ref()
53+
.unwrap_or(&FileSystem::new(Stat::uninitialized())),
4554
)?;
4655

47-
print_diff(&diff, &mut std::io::stdout());
56+
if print {
57+
print_diff(&diff, &mut std::io::stdout());
58+
}
4859

49-
Ok(())
60+
Ok(diff)
5061
}
5162

5263
pub(crate) async fn composefs_backend_finalize(

crates/lib/src/bootc_composefs/update.rs

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ use composefs_ctl::composefs;
77
use composefs_ctl::composefs_boot;
88
use composefs_ctl::composefs_oci;
99
use composefs_oci::image::create_filesystem;
10+
use etc_merge::print_unmergable_paths;
1011
use fn_error_context::context;
1112
use ocidir::cap_std::ambient_authority;
1213
use ostree_ext::container::ManifestDiff;
1314

15+
use crate::bootc_composefs::finalize::get_etc_diff;
1416
use crate::bootc_composefs::gc::GCOpts;
1517
use crate::spec::BootloaderKind;
1618
use crate::{
@@ -288,6 +290,18 @@ pub(crate) async fn do_upgrade(
288290
.context("Failed to mount composefs image")?,
289291
)?;
290292

293+
// Check if three way etc merge is possible without conflicts
294+
let new_etc = mounted_fs
295+
.open_dir("etc")
296+
.context("Opening deployment's etc")?;
297+
298+
let diff = get_etc_diff(storage, booted_cfs, Some(&new_etc), false).await?;
299+
300+
if !diff.unmergable_paths.is_empty() {
301+
print_unmergable_paths(&diff, &mut std::io::stderr());
302+
anyhow::bail!("Merge conflicts found in etc");
303+
}
304+
291305
let boot_type = BootType::from(entry);
292306

293307
let boot_digest = match boot_type {
@@ -307,14 +321,16 @@ pub(crate) async fn do_upgrade(
307321
)?,
308322
};
309323

324+
let staged_state = StagedDeployment {
325+
depl_id: id.to_hex(),
326+
finalization_locked: opts.download_only,
327+
};
328+
310329
write_composefs_state(
311330
&Utf8PathBuf::from("/sysroot"),
312331
&id,
313332
imgref,
314-
Some(StagedDeployment {
315-
depl_id: id.to_hex(),
316-
finalization_locked: opts.download_only,
317-
}),
333+
Some(staged_state),
318334
boot_type,
319335
boot_digest,
320336
&manifest_digest,
@@ -393,16 +409,20 @@ pub(crate) async fn upgrade_composefs(
393409

394410
start_finalize_stated_svc()?;
395411

396-
// Make the staged deployment not download_only
397-
let new_staged = StagedDeployment {
398-
depl_id: staged.require_composefs()?.verity.clone(),
399-
finalization_locked: false,
400-
};
401-
402412
let staged_depl_dir =
403413
Dir::open_ambient_dir(COMPOSEFS_TRANSIENT_STATE_DIR, ambient_authority())
404414
.context("Opening transient state directory")?;
405415

416+
let current = staged_depl_dir
417+
.read_to_string(COMPOSEFS_STAGED_DEPLOYMENT_FNAME)
418+
.context("Reading staged file")?;
419+
420+
let mut new_staged: StagedDeployment =
421+
serde_json::from_str(&current).context("Deserialzing staged file")?;
422+
423+
// Make the staged deployment not download_only
424+
new_staged.finalization_locked = false;
425+
406426
staged_depl_dir
407427
.atomic_replace_with(
408428
COMPOSEFS_STAGED_DEPLOYMENT_FNAME,

crates/lib/src/cli.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2455,7 +2455,8 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
24552455
anyhow::bail!("ConfigDiff is only supported for composefs backend")
24562456
}
24572457
BootedStorageKind::Composefs(booted_cfs) => {
2458-
get_etc_diff(storage, &booted_cfs).await
2458+
get_etc_diff(storage, &booted_cfs, None, true).await?;
2459+
Ok(())
24592460
}
24602461
}
24612462
}

crates/lib/src/store/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,6 @@ impl<'a> BootedOstree<'a> {
249249
}
250250

251251
/// Represents a composefs-based boot environment
252-
#[allow(dead_code)]
253252
pub struct BootedComposefs {
254253
pub repo: Arc<ComposefsRepository>,
255254
pub cmdline: &'static ComposefsCmdline,

0 commit comments

Comments
 (0)