A plain apt upgrade -y that pulled in a new krill server version silently
overwrote /etc/krill/config.json, discarding any values the user had
customized (e.g. a non-default port). New keys the packaging wanted to
introduce in later versions had no way to land alongside a user’s existing
settings either — there was only “keep everything” or “lose everything.”
server/package/etc/krill/config.json is shipped as a plain packaged file.
Debian’s dpkg only treats a file specially across upgrades (prompt / keep
user changes) when it’s declared in DEBIAN/conffiles — this package never
had one, and postinst had no config-migration logic of its own. So on every
upgrade, dpkg’s normal unpack behavior applied: the new package’s default
config.json unconditionally replaced whatever was on disk. This has been
true since the packaging was first added (ed2ea1913) — genuinely
greenfield, not a regression from a specific later commit.
server/package/DEBIAN/preinst: on upgrade, snapshot the existing
/etc/krill/config.json to config.json.pre-upgrade before dpkg unpacks
the new package’s default file over it.server/package/DEBIAN/postinst: after the new default is in place, merge
the backup back in with jq -c -s '.[0] * .[1]' (new defaults as the base,
the user’s previous values overriding on conflicting keys) — this way
future versions can add new keys and they still land, while values the user
changed survive. Falls back to keeping the user’s file verbatim (no merge)
if jq is unavailable or either file fails to parse as JSON, so no upgrade
path can silently lose data.jq to DEBIAN/control’s Depends.server/src/jvmTest/kotlin/krill/zone/server/ConfigMergeTest.kt extracts
the preinst/postinst sentinel-delimited blocks (same pattern as
PostinstPinPromptTest) and runs them against temp files, covering:
backup-only-on-upgrade, successful merge (old value kept + new key added),
no-op when there’s no backup (fresh install), and the no-jq fallback.DEBIAN/conffiles entry (dpkg’s
built-in “ask/keep” semantics) or, when the file’s shape can grow over
time and a real merge is wanted, a preinst-backup + postinst-merge pair
like this one. Shipping it as a bare file under etc/ in the package tree
is an upgrade-data-loss bug waiting to happen the moment a second version
ships.preinst/postinst) can be regression-tested without
root or a real dpkg run by wrapping the risky block in
# >>> <name> / # <<< <name> sentinels and extracting+executing just
that block against fixture paths — see PostinstPinPromptTest for the
original pattern this test reused.