summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/rootfs.rs21
-rw-r--r--tests/rootfs_finalization.rs36
2 files changed, 54 insertions, 3 deletions
diff --git a/src/rootfs.rs b/src/rootfs.rs
index 06b2557..9246aa8 100644
--- a/src/rootfs.rs
+++ b/src/rootfs.rs
@@ -167,7 +167,7 @@ fn copy_entry(
if let Some(parent) = output.parent() {
fs::create_dir_all(parent)?;
}
- symlink(&target, &output).with_context(|| format!("create symlink {}", output.display()))?;
+ replace_with_symlink(&target, &output)?;
manifest.record(destination);
} else if metadata.file_type().is_file() {
if let Some(parent) = output.parent() {
@@ -204,8 +204,23 @@ fn enable_service(rootfs: &Path, service: &ServiceName, manifest: &mut MutationM
))?;
let output = rootfs.join(destination.as_path());
fs::create_dir_all(output.parent().expect("service path has a parent"))?;
- symlink(format!("/usr/lib/systemd/system/{}", service.0), &output)
- .with_context(|| format!("enable service {}", service.0))?;
+ replace_with_symlink(
+ Path::new(&format!("/usr/lib/systemd/system/{}", service.0)),
+ &output,
+ )
+ .with_context(|| format!("enable service {}", service.0))?;
manifest.record(destination.as_path());
Ok(())
}
+
+fn replace_with_symlink(target: &Path, output: &Path) -> Result<()> {
+ match fs::symlink_metadata(output) {
+ Ok(metadata) if metadata.file_type().is_dir() => {
+ bail!("cannot replace directory with symlink: {}", output.display());
+ }
+ Ok(_) => fs::remove_file(output).with_context(|| format!("replace {}", output.display()))?,
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
+ Err(error) => return Err(error).with_context(|| format!("inspect {}", output.display())),
+ }
+ symlink(target, output).with_context(|| format!("create symlink {}", output.display()))
+}
diff --git a/tests/rootfs_finalization.rs b/tests/rootfs_finalization.rs
index fa0cf3b..3a98290 100644
--- a/tests/rootfs_finalization.rs
+++ b/tests/rootfs_finalization.rs
@@ -79,3 +79,39 @@ fn finalization_rejects_rootfs_escaping_destinations_and_symlinks() {
.expect_err("unsafe symlink must be rejected");
assert!(error.to_string().contains("escapes copied tree"));
}
+
+#[test]
+fn finalization_is_idempotent_for_matching_symlinks() {
+ let fixture = tempdir().expect("fixture directory");
+ let source = fixture.path().join("overlay");
+ fs::create_dir_all(&source).expect("create source tree");
+ symlink("controller.conf", source.join("controller-link")).expect("create source symlink");
+ fs::write(source.join("controller.conf"), "controller\n").expect("write source file");
+ let rootfs = tempdir().expect("rootfs directory");
+ let finalization = RootfsFinalization::new(
+ vec![CopyTree::new(&source, "etc/controller").expect("valid copy destination")],
+ InitrdOem::new(std::iter::empty::<&str>(), std::iter::empty::<&str>()),
+ vec![ServiceName::new("chronyd.service").expect("valid service")],
+ );
+
+ finalization.apply(rootfs.path()).expect("first finalization succeeds");
+ let second = finalization.apply(rootfs.path()).expect("repeat finalization succeeds");
+
+ assert_eq!(
+ fs::read_link(rootfs.path().join("etc/controller/controller-link")).expect("copied symlink"),
+ Path::new("controller.conf")
+ );
+ assert_eq!(
+ fs::read_link(
+ rootfs
+ .path()
+ .join("etc/systemd/system/multi-user.target.wants/chronyd.service")
+ )
+ .expect("enabled service"),
+ Path::new("/usr/lib/systemd/system/chronyd.service")
+ );
+ assert!(second
+ .created_paths()
+ .iter()
+ .any(|path| path == "etc/controller/controller-link"));
+}