Skip to content

Resolving conflicts for etc merge #2285

Description

@Johan-Liebert1

The following doc is mostly AI generated. I've made a few tweaks

/etc Three-Way Merge Conflict Resolution

Problem

When upgrading a bootc deployment, /etc undergoes a three-way merge between:

  1. Pristine etc -- The unmodified /etc from the currently booted image.
  2. Current etc -- The live /etc with user modifications.
  3. New etc -- The /etc from the new deployment image.

User modifications (added files, changed permissions, edited content) are preserved
across upgrades. However, a type conflict can occur when a path changes between
file and directory across deployments:

  • A user-modified file now defaults to a directory in the new image.
  • A user-modified directory now defaults to a file in the new image.

These conflicts are unmergeable, i.e. the merge cannot reconcile both the user's
modification and the new image's structural change at the same path.

MergeStrategy

MergeStrategy is an enum that controls how unmergeable paths are handled during
the three-way /etc merge. It is defined in the etc-merge crate and derives
clap::ValueEnum, Serialize, and Deserialize for use as both a CLI argument
and a configuration value.

Variants

Strategy Behavior
fail (Default) Abort the merge. The deployment finalization fails and the system continues booting the current deployment.
skip Keep the new image's default for conflicting paths. The user's modification at that path is dropped. The merge succeeds.
replace Keep the user's version. The conflicting entry in the new /etc is removed and replaced with the user's file or directory (including children). The merge succeeds.

Conflict detection

Note

We now detect any conflicts during upgrade/switch and throw an error if we find conflicts
and merge_strategy is Fail

During compute_diff, the check_if_mergable function compares each added or
modified path against the new /etc tree. If a file-vs-directory type mismatch is
found, the path is recorded in diff.unmergable_paths with a human-readable reason.

The merge() function checks this list before proceeding:

  • With Fail, it bails immediately if any unmergeable paths exist.
  • With Skip or Replace, it proceeds and handles each conflict according to the
    strategy.

Configuration

The merge strategy is determined through the following sources, in order of precedence:

1. CLI option (--merge-strategy)

Available on bootc upgrade and bootc switch:

bootc upgrade --merge-strategy skip
bootc switch --merge-strategy replace <image>

The --merge-strategy flag accepts fail, skip, or replace. The default is fail.

This is implemented via the EtcMergeStrategyOpts struct which is flattened into
UpgradeOpts and SwitchOpts.

2. Image configuration file (/usr/lib/bootc/finalize.toml)

Image authors can ship a default merge strategy inside the container image:

merge_strategy = "skip"

This file is read from the staged deployment's mounted EROFS image at
usr/lib/bootc/finalize.toml. It is consulted only when conflicts are detected
and the CLI-provided strategy is fail (the default). If the config file specifies
a non-fail strategy, it overrides the default.

This allows image authors to opt their users into a safe default without requiring
CLI flags on every upgrade.

Finalization

The resolved merge strategy is persisted in the staged deployment JSON file at
/run/composefs/staged-deployment. This file is written during write_composefs_state
and read during composefs_backend_finalize.

The resolution flow is:

  1. CLI provides --merge-strategy (default: fail).
  2. During write_composefs_state, if conflicts exist and strategy is fail, check
    the image's finalize.toml for a configured strategy.
  3. If a non-fail strategy is found in the config, use it. Otherwise, fail with a
    hint about --merge-strategy and finalize.toml.
  4. The resolved strategy is serialized into the staged deployment file.
  5. On reboot, composefs_backend_finalize reads the staged deployment file and
    passes the strategy to merge().

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions