Repository navigation
v0.4.0
Changelog
Most of this release is about what a failed update leaves behind. The previous
release's services now come back up with their own secrets; an update whose
migrations left a database the old release cannot read stops at exit 12 instead
of reporting a rollback that did not happen; an unattended update that failed is
tried once rather than on every tick; and a failed apply or update no longer
removes the secrets running containers read. The update timer gets a real
maintenance window, update.schedule, after the documented drop-in turned out to
add a trigger rather than narrow one. release trust rotates signing keys
without re-creating the installation, and operations.migrate may list several
steps.
Upgrading is automatic: the installation schema moves 10 → 11 on the first read
and converts nothing. Going back is not, because a 0.3.x manager declines the
installation rather than dropping update.schedule. If you followed the old
unattended-updates page and added an OnCalendar= drop-in to the update timer,
remove it and set update.schedule instead. A bundle whose operations.migrate
is a list declares min_manager_version: "0.4.0", which a 0.3.x manager refuses
as needing a newer manager.
Added
morzer config set update.schedule: when the update timer follows the channel — and installs, ifupdate.auto_applyallows — as a systemdOnCalendarexpression. This is the maintenance window RFC 0016 always saidOnCalendarwas, and nothing let you set it: every installation ticked daily at 03:30. Unsetting it returns to that default. A sandbox that should followmainwithin minutes sets*:0/5.morzer statusshows the update channel a machine follows: when the timer looks at it, and whether it installs what it finds or only stages it. A channel set whileupdate.checkis off is reported as not polled, since no timer exists then. Until now the schedule was visible only in the unit file.morzer release trust add|remove|list: the keys a release must be signed with can change afterinit. Rotating a vendor key used to mean re-creating the installation. Trust the new key, install a release it signed, then remove the old one. A key is parsed before it is trusted, adding one already trusted changes nothing, and the last key cannot be removed while a signature is required, whichupdate.auto_applyalso needs.operations.migratemay list several steps, run in order before services start, onapplyand everyupdate: migrations, then the application's own bootstrap, for example. A failure in any of them stops for manual intervention as a failed migration always has. A manifest using a list declarescompatibility.min_manager_version: "0.4.0", so an older manager refuses it as needing a newer manager instead of reporting a malformed operation.
Changed
- Building from source needs Go 1.26 or newer. The
golang.org/xmodules moved theirgodirective to 1.26.0 together, and that directive is a requirement rather than a preference, so taking any of their updates raises this floor. Published binaries are unaffected. - A dev-mode installation's update timer has no
RandomizedDelaySec. The half-hour spread exists so a vendor's registry does not see every customer at once, and a sandbox polls its own vendor's registry; with it, a five-minute schedule ticked every five to thirty-five minutes. Production timers keep it whatever the schedule. - Installation schema 10 → 11, for
update.schedule. Migration is automatic and converts nothing. The bump is for the write path, as schema 7 was forpolicy.backup_schedule: without it, an older manager'sconfig setwould drop the field and its next reconciliation would put the timer back on 03:30 without saying so. With it, an older manager declines the installation instead. init --dry-runrefuses a bundle the real run would refuse. It reported "would create an installation" for a manifest the next command rejected. It now validates a local bundle's manifest and exits 2 naming it, as the run would, and--jsoncarriesmanifest_validated. A remote reference is still not fetched for a plan, and the plan now says it did not validate the manifest instead of staying silent.- Release notes print as the vendor wrote them. Where a bundle's
RELEASE.mdis shown,update --checkamong them, it is no longer styled or reflowed: reflowing broke the fenced command blocks an operator copies. Dropping the renderer took ten modules out of the build and 6.7 MB out of the binary.
Fixed
- A failed
applyorupdateremoved the secrets of running services. When anapplyfailed after starting services, for example on a health check that never passed, its rollback deleted the rendered secrets while those containers kept running, so any that restarted could not read them. Every boot runs anapply, so a database slow to come up after a reboot was enough. The same happened to services already running when anapplyorupdatebegan, if it failed before starting anything, for example on an image pull. The secrets now stay whenever services may be reading them; an operation that fails with nothing running still removes them. - A failed unattended update was installed again on every tick. The staged release stayed staged and nothing recorded that installing it had failed, so each tick took another pre-update backup, which retention never removes, ran the migrations again, and failed again. The tick now tries a release once: after a failure it leaves the release alone until the channel moves or an operator runs
morzer update --to <version>, andstatussays so. A tick also does nothing while an operation waits for manual intervention, where it used to fetch and stage first. initaccepted a--signing-keythat is not a minisign public key. Only its presence was checked, so a typo surfaced at the first update, after the installation had been created around it. The key is now parsed before anything is created.- A migration run as a
runtime-servicecould not reach the database on a first install. It ran withdocker compose run --no-deps, and migrations run before services start, so nothing had started the database it migrates. Itsdepends_onservices are now started first, and any condition it names is waited for. On an update, a dependency whose configuration changed is now recreated before the migration rather than after it. - A migration service that exited 2 stopped the deployment for manual intervention. Exit 2 means "nothing to do" under the hook contract, and the runtime adapter already returned it as data for that reading. It is now treated as such.
- A failed update left the new release's containers running. Compensation put the release pointer, configuration and secrets back, but not the services, so the new release's containers ran against the old release's configuration until somebody ran
apply— on an unattended machine, until the next boot. When the update got as far as starting services, it now brings the previous release back up with its own secrets and waits for its health checks; if that fails too, the update ends at exit 12 with both failures in its error. Restored configuration files also keep their own mode instead of coming back as 0640. - An update that failed after its migrations reported a rollback that had not happened. When a step after
migratefailed — a service that would not start, a health check that never passed — the update compensated back toward the release that was running and exited 11, "earlier changes were rolled back", although the database stayed migrated. The old release was left recorded at its old schema over a database it could not read, nothing was flagged, and the nextapply, including the one every boot runs, ran its migrations against it. Now, when the migrations leave a database that release cannot read (judged asrollbackjudges it), the update stops at exit 12 without rolling back, leaving the new release's configuration and containers in place, and the error names the way forward (update --to <version>) and the way back (the pre-update backup). - A schedule systemd cannot parse is refused instead of stored.
config set backup.schedule,config set update.scheduleandinit --backup-scheduleaccepted any single line, so a typo such asevery 5mexited 0 and left a timer systemd would not load: no scheduled backups, or no unattended updates, and nothing said so. The expression is now checked withsystemd-analyze calendaron the machine that will run it, before anything is written. A host withoutsystemd-analyzeaccepts the value unchecked, as before. - The published manifest schema rejected every bundle this manager accepts.
schemas/selfhost-v1alpha1-manifest.jsonstill required theruntime:block that stopped being read in 0.3.0, and did not require theruntimes:the manager refuses a manifest without, so an editor following$schemaor a CI validating against the file failed a valid bundle. It now requiresruntimesand no longer offersruntime, and refuses an emptyruntimes,imagesor runtimefilesas the loader does. Tests remove each top-level key of a manifest in turn, and empty each of those three, and fail when the loader and the schema disagree. - The documented maintenance window did not narrow anything. The unattended-updates page told you to add
OnCalendar=Sun *-*-* 04:00:00to the update timer, and a drop-in'sOnCalendar=adds a trigger to the one in the unit — so updates ran on Sunday morning and every night as well. Remove any such drop-in and useupdate.scheduleinstead. apply --dry-runandupdate --dry-rundescribed a plan in the past tense. They printed "demo 1.2.0 applied" and "updated demo from 1.2.0 to 1.2.0" beneath the line saying nothing was changed. They now say what they would do, asinit --dry-runalready did.- A manifest refusal named a temporary copy instead of the bundle you passed. A plan, or an install from an archive, read the manifest from a staging directory and named
/tmp/morzer-plan-…/manifest.yaml, which was gone before you could look. It now names the--releasepath or the archive. installation.yamlcould describe a change the manager never recorded. It was written before the state store, so a refused state write left the report ahead of the record, and every backup copies the report, so a later restore brought back as true an installation that was never recorded. The state is written first now: a failure can only leave the report stale, and the next successful save rewrites it.
Security
- A release reference's password could reach the update timer's journal. When a reference was refused, as malformed, as plaintext
http, or as afile://URL naming a host, the error quoted it in full, password included, and a malformed one twice.config set update.channelrefuses such a channel, but a state file edited by hand, imported or restored could still carry one, and the timer then logged its password on every tick. The refusal now quotes the reference without its password, or withholds a malformed one entirely when it has userinfo. The https source's own errors, a 404, a refused login, an unreachable server, quoted the URL with its password too, and now leave it out, as they do a malformed redirect that echoes it back. - An
oci://reference carrying a username and password is refused. The registry source takes its credentials fromdocker loginand read userinfo as part of the repository name, so the password authenticated nothing and appeared only in error messages. Remove it from the reference and log in to the registry instead. - A release's notes could write to the operator's terminal and clipboard. A bundle's
RELEASE.mdreached the terminal byte for byte, so an escape sequence in it, such as one that sets the clipboard, ran the moment you ranupdate --check, and the same bytes went into notification bodies. Terminal control sequences and bare control characters are now stripped from notes in every output mode, before they reach a notifier.
Installing this release
curl -fsSL https://morzecrew.github.io/morzer/install.sh | sh -s -- --version 0.4.0The script verifies the checksum always and the signature whenever
minisign is present; --require-signature makes a missing minisign fatal,
which is what a production runbook sets. --print-only shows everything it
detected and would do, and changes nothing. install.sh is an asset of this
release and is covered by SHA256SUMS below.
Verifying this release by hand
ARCHIVE=morzer_0.4.0_linux_amd64.tar.zst # the one you downloaded
curl -fsSLO https://raw.githubusercontent.com/morzecrew/morzer/main/morzer.pub
minisign -Vm SHA256SUMS -p morzer.pub
grep " ${ARCHIVE}$" SHA256SUMS | sha256sum -c -SHA256SUMS covers every architecture, so checking it whole fails on the
archives you did not download — and --ignore-missing, the usual way
around that, reports OK when no archive is present at all. Pulling out
the line for the file you have fails when there is nothing to check.
The signing key is the same for every release. If you have verified a
morzer release before, use the copy you already have rather than fetching
it again — a key that changes between releases is the thing worth noticing.
Release bundles are a separate artifact with their own signature and their
own key; see
https://morzecrew.github.io/morzer/reference/release-commands/#verification