Skip to content

install: Add --preserve-var and --merge-etc to to-existing-root - #2384

Draft
ckyrouac wants to merge 2 commits into
bootc-dev:mainfrom
ckyrouac:image-to-package
Draft

install: Add --preserve-var and --merge-etc to to-existing-root#2384
ckyrouac wants to merge 2 commits into
bootc-dev:mainfrom
ckyrouac:image-to-package

Conversation

@ckyrouac

Copy link
Copy Markdown
Collaborator

This is AI generated with lots of guidance.


Add two flags to bootc install to-existing-root that are most useful
when migrating a package-mode system to a bootc image built externally:

--preserve-var Copy the running system's /var data into the new
deployment, and write a package-mode GRUB rollback
boot entry.

--merge-etc Apply the running system's /etc customisations onto
the new deployment via a 3-way merge.

Typical usage:

podman run --rm --privileged --pid=host --user=root:root
-v /dev:/dev --security-opt label=type:unconfined_t
-v /:/target -v /var/lib/containers:/var/lib/containers

bootc install to-existing-root
--preserve-var
--merge-etc
--acknowledge-destructive

Two robustness fixes surfaced while exercising the 3-way merge against
real package-mode `/etc` trees (see the upcoming `--merge-etc` install
flag):

- In merge_leaf(), when copying a modified file into the new /etc,
  skip (with a warning) rather than fail when the cap-std copy hits
  "a path led outside of the filesystem". This happens when a path
  component in the current /etc leads through an absolute symlink
  that escapes the cap-std root (e.g. /etc/alternatives symlinks);
  the image's existing content wins for that path instead of
  aborting the whole merge. Clarify the comment on the analogous
  lstat-based removed-path check for consistency.

- Warn and skip, instead of bailing, when a modified host file/symlink
  newly defaults to a directory in the target image (e.g.
  /etc/ssl/certs is a symlink in Fedora package mode but a directory in
  the bootc base image).  The image's directory wins and the host's
  customization is dropped for that path.

Update the file-to-directory unit test to match the new behavior.

Assisted-by: AI
Add two flags to `bootc install to-existing-root` that are most useful
when migrating a package-mode system to a bootc image built externally:

  --preserve-var   Copy the running system's /var data into the new
                    deployment, and write a package-mode GRUB rollback
                    boot entry.

  --merge-etc      Apply the running system's /etc customisations onto
                    the new deployment via a 3-way merge.

Typical usage:

  podman run --rm --privileged --pid=host --user=root:root \
      -v /dev:/dev --security-opt label=type:unconfined_t \
      -v /:/target -v /var/lib/containers:/var/lib/containers \
      <fleet-image> \
      bootc install to-existing-root \
          --preserve-var \
          --merge-etc \
          --acknowledge-destructive

The implementation lives in the new `install/migrate` module; see its
module-level docs for the on-disk layout and the /var and /etc
migration strategies (reflink copy vs. plain copy, the pkgmode-rollback
BLS entry, and the etc-merge 3-way merge). install_to_existing_root()
extracts the two flags before opts is consumed, stashes the running
kernel/initramfs ahead of install_to_filesystem() when --preserve-var
is set (since that wipes /boot), and runs the post-install migration
steps afterward.

migrate.rs includes unit tests covering the pkgmode-rollback path
layout and the new /var path derivation from the deployment dir.

Assisted-by: AI
@bootc-bot
bootc-bot Bot requested a review from cgwalters August 17, 2026 16:06
@github-actions github-actions Bot added the area/install Issues related to `bootc install` label Aug 17, 2026
// "a path led outside of the filesystem". Warn and skip rather than aborting the
// whole merge; the image's existing content wins for that path.
if let Err(ref e) = copy_result {
if e.to_string().contains("a path led outside of the filesystem") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid string matching errors, we can use downcasts

But perhaps better we should be using https://docs.rs/cap-std-ext/5.1.2/cap_std_ext/struct.RootDir.html ? And yes we'd need a copy api there.

Perhaps simplest to open the parent dir rooted, then do a copy that way

Comment on lines +56 to +61
//! When `--preserve-var` is passed, the running kernel and initramfs are saved
//! to `<root_path>/var/lib/pkgmode-rollback/` **before** the install wipes
//! `/boot`. After the install a third BLS entry (`pkgmode-rollback.conf`) is
//! written into the active `loader.N/entries/` directory so GRUB presents a
//! "Previous OS" option. The entry is invisible to ostree (which only reads
//! `ostree-*.conf` files) and to bootupd (which does not touch `loader/entries/`).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of hacky, I think it'd be cleaner to just not delete the entries (and kernel/initramfs) at all which should be a separate option.

.context("Locating new ostree deployment directory")?;
println!(" Deployment directory: {}", deploy_dir.display());

// The deployment's var/ is two levels up from the deploy dir:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's APIs for this

/// We enumerate `<root_path>/ostree/deploy/` and return the most recently
/// modified entry under each stateroot's `deploy/` subdirectory.
#[context("Locating new ostree deployment directory")]
fn find_deploy_dir(root_path: &Path) -> Result<PathBuf> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's APIs for htis too

/// `root_path` is the host root as seen from inside the install container.
/// `kver` is the running kernel version string returned by `save_pkgmode_kernel`.
#[context("Running post-install /var preservation and rollback entry")]
pub(crate) fn preserve_var_and_write_rollback(root_path: &Path, kver: &str) -> Result<()> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general let's try to use cap-std

/// Returns true if the filesystem hosting `new_var` supports reflinks.
///
/// Probes by attempting a zero-byte reflink from `src_var` into `new_var`.
fn reflinks_supported(src_var: &Path, new_var: &Path) -> bool {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our storage code already probes this I believe

let src_entry = src_var.join(name_str.as_ref());
let dst_entry = new_var.join(name_str.as_ref());

// For `log/`, skip only the `journal` subdirectory (large; regenerated by journald).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this special cased?

continue;
}

// For `lib/`, skip `containers` (podman/docker storage — overlay mounts cannot

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto, I don't think we should have special cases buried in this code. We should make it configurable of course in general, via e.g. --preserve-var-skip=log/,lib/containers or something

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/install Issues related to `bootc install`

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants