bootc-dev / bootc-dev/bootc

Resolving conflicts for etc merge

Open
#2,285 2 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.