Skip to content

Commit 9391952

Browse files
Johan-Liebert1jeckersb
authored andcommitted
etc-merge: Handle unmergable paths properly
We would have unmergable paths if a modified file was changed to a directory in the new etc or vice-versa. One of the major issues was that we were erroring out during "merge" which is not ideal. A new vector now keeps track of all unmergable paths and the reason they're not mergable. Ref: composefs/composefs-rs/issues/335 Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
1 parent 2625209 commit 9391952

1 file changed

Lines changed: 145 additions & 30 deletions

File tree

crates/etc-merge/src/lib.rs

Lines changed: 145 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ fn stat_eq_ignore_mtime(this: &Stat, other: &Stat) -> bool {
7878
return true;
7979
}
8080

81+
#[derive(Debug)]
82+
pub struct UnmergablePaths {
83+
path: PathBuf,
84+
reason: String,
85+
}
86+
8187
/// Represents the differences between two directory trees.
8288
#[derive(Debug)]
8389
pub struct Diff {
@@ -88,6 +94,8 @@ pub struct Diff {
8894
modified: Vec<PathBuf>,
8995
/// Paths that exist in the pristine /etc but not in the current one
9096
removed: Vec<PathBuf>,
97+
/// Paths that are unmergable
98+
unmergable_paths: Vec<UnmergablePaths>,
9199
}
92100

93101
fn collect_all_files(
@@ -170,6 +178,61 @@ fn get_deletions(
170178
Ok(())
171179
}
172180

181+
fn check_if_mergable(
182+
new: &Directory<CustomMetadata>,
183+
current_inode: &Inode<CustomMetadata>,
184+
current_path: &Path,
185+
diff: &mut Diff,
186+
) -> anyhow::Result<()> {
187+
match current_inode {
188+
// If currently 'file' is a directory, make sure it's not a regular file
189+
// new_etc as well, else we can't merge
190+
Inode::Directory(..) => {
191+
let new_dir = new.get_directory(&current_path.as_os_str());
192+
193+
match new_dir {
194+
Ok(_) => {}
195+
Err(e) => match e {
196+
ImageError::NotADirectory(..) => {
197+
diff.unmergable_paths.push(UnmergablePaths {
198+
path: current_path.to_path_buf(),
199+
reason: format!(
200+
"Directory '{}' now defaults to a file in new etc",
201+
current_path.display()
202+
),
203+
});
204+
}
205+
ImageError::NotFound(..) => {}
206+
207+
_ => Err(e)?,
208+
},
209+
}
210+
}
211+
212+
// If currently 'file' is not a directory, make sure it's not a directory in the
213+
// new_etc either, else we can't merge
214+
Inode::Leaf(..) => {
215+
let new_dir = new.get_directory(&current_path.as_os_str());
216+
217+
match new_dir {
218+
Ok(..) => {
219+
diff.unmergable_paths.push(UnmergablePaths {
220+
path: current_path.to_path_buf(),
221+
reason: format!(
222+
"File '{}' now defaults to a directory in new etc",
223+
current_path.display()
224+
),
225+
});
226+
}
227+
Err(ImageError::NotFound(..)) => {},
228+
Err(e) => Err(e)?,
229+
}
230+
}
231+
}
232+
233+
Ok(())
234+
}
235+
173236
// 1. Files in the currently booted deployment’s /etc which were modified from the default /usr/etc (of the same deployment) are retained.
174237
//
175238
// 2. Files in the currently booted deployment’s /etc which were not modified from the default /usr/etc (of the same deployment)
@@ -221,21 +284,39 @@ fn get_modifications(
221284
diff,
222285
)?;
223286

224-
// This directory or its contents were modified/added
225-
// Check if the new directory was deleted from new_etc
226-
// If it was, we want to add the directory back
227-
if new.get_directory_opt(&current_path.as_os_str())?.is_none() {
228-
if diff.added.len() != total_added {
229-
diff.added.insert(total_added, current_path.clone());
230-
} else if diff.modified.len() != total_modified {
231-
diff.modified.insert(total_modified, current_path.clone());
287+
match new.get_directory(&current_path.as_os_str()) {
288+
Ok(..) => {
289+
// Directory exists in both current and new etc.
290+
// Modifications/additions within this directory are handled recursively.
291+
// No additional action needed here.
292+
}
293+
294+
Err(ImageError::NotFound(..)) | Err(ImageError::NotADirectory(..)) => {
295+
// This directory was deleted in new_etc
296+
// If it was modified in the current etc, we want this back
297+
//
298+
// Was a directory in current etc, but is now
299+
// a file/symlink in the new etc
300+
if diff.added.len() != total_added {
301+
diff.added.insert(total_added, current_path.clone());
302+
} else if diff.modified.len() != total_modified {
303+
diff.modified.insert(total_modified, current_path.clone());
304+
}
232305
}
306+
307+
Err(e) => Err(e)?,
308+
}
309+
310+
if diff.modified.len() != total_modified || diff.added.len() != total_added
311+
{
312+
check_if_mergable(new, inode, &current_path, diff)?;
233313
}
234314
}
235315

236316
Err(ImageError::NotFound(..)) => {
237317
// Dir not found in original /etc, dir was added
238318
diff.added.push(current_path.clone());
319+
check_if_mergable(new, inode, &current_path, diff)?;
239320

240321
// Also add every file inside that dir
241322
collect_all_files(&curr_dir, current_path.clone(), &mut diff.added);
@@ -245,18 +326,21 @@ fn get_modifications(
245326
// Some directory was changed to a file/symlink
246327
// This should be counted in the diff, but we don't really merge this
247328
diff.modified.push(current_path.clone());
329+
check_if_mergable(new, inode, &current_path, diff)?;
248330
}
249331

250-
Err(e) => Err(e)?,
332+
Err(e) => Err(e).with_context(|| format!("Opening pristine {path:?}"))?,
251333
}
252334
}
253335

254336
Inode::Leaf(leaf_id, _) => match pristine.leaf_id(path) {
255337
Ok(old_leaf_id) => {
256338
let leaf = &current_leaves[leaf_id.0];
257339
let old_leaf = &pristine_leaves[old_leaf_id.0];
340+
258341
if !stat_eq_ignore_mtime(&old_leaf.stat, &leaf.stat) {
259342
diff.modified.push(current_path.clone());
343+
check_if_mergable(new, inode, &current_path, diff)?;
260344
current_path.pop();
261345
continue;
262346
}
@@ -266,19 +350,22 @@ fn get_modifications(
266350
if old_meta.content_hash != current_meta.content_hash {
267351
// File modified in some way
268352
diff.modified.push(current_path.clone());
353+
check_if_mergable(new, inode, &current_path, diff)?;
269354
}
270355
}
271356

272357
(Symlink(old_link), Symlink(current_link)) => {
273358
if old_link != current_link {
274359
// Symlink modified in some way
275360
diff.modified.push(current_path.clone());
361+
check_if_mergable(new, inode, &current_path, diff)?;
276362
}
277363
}
278364

279365
(Symlink(..), Regular(..)) | (Regular(..), Symlink(..)) => {
280366
// File changed to symlink or vice-versa
281367
diff.modified.push(current_path.clone());
368+
check_if_mergable(new, inode, &current_path, diff)?;
282369
}
283370

284371
(a, b) => {
@@ -290,14 +377,16 @@ fn get_modifications(
290377
Err(ImageError::IsADirectory(..)) => {
291378
// A directory was changed to a file
292379
diff.modified.push(current_path.clone());
380+
check_if_mergable(new, inode, &current_path, diff)?;
293381
}
294382

295383
Err(ImageError::NotFound(..)) => {
296384
// File not found in original /etc, file was added
297385
diff.added.push(current_path.clone());
386+
check_if_mergable(new, inode, &current_path, diff)?;
298387
}
299388

300-
Err(e) => Err(e).context(format!("{path:?}"))?,
389+
Err(e) => Err(e).with_context(|| format!("Opening pristine {path:?}"))?,
301390
},
302391
}
303392

@@ -385,6 +474,7 @@ pub fn compute_diff(
385474
added: vec![],
386475
modified: vec![],
387476
removed: vec![],
477+
unmergable_paths: vec![],
388478
};
389479

390480
get_modifications(
@@ -422,6 +512,15 @@ pub fn print_diff(diff: &Diff, writer: &mut impl Write) {
422512
for removed in &diff.removed {
423513
let _ = writeln!(writer, "{} {removed:?}", ModificationType::Removed.red());
424514
}
515+
516+
for unmergable in &diff.unmergable_paths {
517+
let _ = writeln!(
518+
writer,
519+
"{} {}",
520+
ModificationType::Unmergable.magenta(),
521+
unmergable.reason
522+
);
523+
}
425524
}
426525

427526
#[context("Collecting xattrs")]
@@ -589,6 +688,7 @@ enum ModificationType {
589688
Added,
590689
Modified,
591690
Removed,
691+
Unmergable,
592692
}
593693

594694
impl std::fmt::Display for ModificationType {
@@ -603,6 +703,7 @@ impl ModificationType {
603703
ModificationType::Added => "+",
604704
ModificationType::Modified => "~",
605705
ModificationType::Removed => "-",
706+
ModificationType::Unmergable => "*",
606707
}
607708
}
608709
}
@@ -613,23 +714,35 @@ fn create_dir_with_perms(
613714
stat: &Stat,
614715
new_inode: Option<&Inode<CustomMetadata>>,
615716
) -> anyhow::Result<()> {
616-
// The new directory is not present in the new_etc, so we create it, else we only copy the
617-
// metadata
618-
if new_inode.is_none() {
619-
// Here we use `create_dir_all` to create every parent as we will set the permissions later
620-
// on. Due to the fact that we have an ordered (sorted) list of directories and directory
621-
// entries and we have a DFS traversal, we will always have directory creation starting from
622-
// the parent anyway.
623-
//
624-
// The exception being, if a directory is modified in the current_etc, and a new directory
625-
// is added inside the modified directory, say `dir/prems` has its permissions modified and
626-
// `dir/prems/new` is the new directory created. Since we handle added files/directories first,
627-
// we will create the directories `perms/new` with directory `new` also getting its
628-
// permissions set, but `perms` will not. `perms` will have its permissions set up when we
629-
// handle the modified directories.
630-
new_etc_fd
631-
.create_dir_all(&dir_name)
632-
.context(format!("Failed to create dir {dir_name:?}"))?;
717+
match new_inode {
718+
Some(inode) => match inode {
719+
Inode::Directory(..) => { /* no-op */ }
720+
721+
Inode::Leaf(..) => {
722+
anyhow::bail!(
723+
"Modified config directory {dir_name:?} newly defaults to file. Cannot merge"
724+
)
725+
}
726+
},
727+
728+
// The new directory is not present in the new_etc, so we create it, else we only copy the
729+
// metadata
730+
None => {
731+
// Here we use `create_dir_all` to create every parent as we will set the permissions later
732+
// on. Due to the fact that we have an ordered (sorted) list of directories and directory
733+
// entries and we have a DFS traversal, we will always have directory creation starting from
734+
// the parent anyway.
735+
//
736+
// The exception being, if a directory is modified in the current_etc, and a new directory
737+
// is added inside the modified directory, say `dir/prems` has its permissions modified and
738+
// `dir/prems/new` is the new directory created. Since we handle added files/directories first,
739+
// we will create the directories `perms/new` with directory `new` also getting its
740+
// permissions set, but `perms` will not. `perms` will have its permissions set up when we
741+
// handle the modified directories.
742+
new_etc_fd
743+
.create_dir_all(&dir_name)
744+
.context(format!("Failed to create dir {dir_name:?}"))?;
745+
}
633746
}
634747

635748
new_etc_fd
@@ -730,12 +843,14 @@ fn merge_modified_files(
730843
file,
731844
current_inode.stat(current_leaves),
732845
new_inode,
733-
)?;
846+
)
847+
.context("Merging directory")?;
734848
}
735849

736850
Inode::Leaf(leaf_id, _) => {
737851
let leaf = &current_leaves[leaf_id.0];
738-
merge_leaf(current_etc_fd, new_etc_fd, leaf, new_inode, file)?
852+
merge_leaf(current_etc_fd, new_etc_fd, leaf, new_inode, file)
853+
.context("Merging leaf")?
739854
}
740855
};
741856
}
@@ -755,7 +870,7 @@ fn merge_modified_files(
755870
}
756871
},
757872

758-
Err(e) => Err(e)?,
873+
Err(e) => Err(e).with_context(|| format!("Opening {file:?} in new etc"))?,
759874
};
760875
}
761876

0 commit comments

Comments
 (0)