Resolving conflicts for etc merge
- Dominant language
- Rust
- Stars
- 2.3k
- Forks
- 230
- Avg merge
- 3d 12h
- Merged PRs (30d)
- 38
Description
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
```
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:
```toml
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()`.
Contributor guide
Research direction
Start in the etc-merge crate with compute_diff, check_if_mergable, and merge to trace how unmergeable paths and MergeStrategy are handled. Then follow EtcMergeStrategyOpts through UpgradeOpts and SwitchOpts, and inspect write_composefs_state and composefs_backend_finalize for finalize.toml and staged-deployment handling. Done means fail, skip, and replace behave as documented for upgrade and switch conflicts.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- operating-systems
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100