Repository navigation
feat(services): let operators override release service files - #635
Merged
Medformatik merged 8 commits intoOct 26, 2026
Merged
Conversation
Medformatik
force-pushed
the
feat/service-file-overrides
branch
from
October 26, 2026 00:14
01f4504 to
bf2cbea
Compare
Whole files in infra/docker/overrides/services/<id>/<path> replace the release's, owned by the deployment user, writable by no one else and never through a link; Traefik's dynamic directory also takes files the release doesn't ship (auth.yml). An unusable override of a shipped file fails the render, and the admin panel shows why through configuration.failure.inspect; one of an unshipped path is reported and ignored. Render, the agent's log, check and Run diagnostics list the overrides and flag those whose release file changed since they were made, which replaces the drift warning. The first render with the release's files imports, once, checkout edits that match no version a release shipped, never files a container writes. A host render reads the selected release's files from its agent image and refuses while the checkout's manifests or files differ from them.
An imported override keeps its checkout file's mode, never writable by others, and owner-only under traefik/. The authority inventory lists the new configuration.failure.inspect operation, and the fixed CLI's test checks the release's service file report instead of the replaced drift warning.
Under umask 002 a plain mkdir or write leaves an override group-writable, which render rightly refuses before the case the test means. The tests set the mode they mean, and the docs and release note tell operators on Debian and Ubuntu to create overrides under umask 022.
…lf-anchored pattern
Medformatik
force-pushed
the
feat/service-file-overrides
branch
from
October 26, 2026 00:27
b8017c9 to
e67f152
Compare
…rides # Conflicts: # packages/core/src/services/writable-copies.ts
…d the release A checkout ahead of the selected release holds upstream versions its history doesn't know, which the import took for operator edits. It now waits there, importing and recording nothing, and render, check and the agent's log say why and which checkout files go unused meanwhile. - The host CLI asks git whether HEAD is the release's source revision or one before it, and runs the refusal check before the import: only the edits the import will take are exempt from it. - The agent, without git, compares the checkout's own services/service-file-history.json with the release's: a version the release doesn't list means the checkout may be ahead. - Under OPENMAPX_RELEASE=local the agent's start-up report reads no bundled files, as its renders already don't. - A directory at a shipped file's path in overrides fails the render instead of leaving the release's file in use.
… carry over A checkout auth.yml (or another dynamic file the release doesn't ship) or a changed traefik.yml would otherwise give way to the release's files and drop a basic-auth gate. The render, host or agent, names them and says to put them in overrides, which ends the wait for those files; check reports it as an error. Other differing files keep the release's bytes and are reported.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed and why
This is PR 5b of the plan to stop mounting the whole checkout into the operations agent, stacked on 5a (#634). 5a makes services mount the release's files. This PR makes that safe to ship: operators' deliberate changes become overrides, their checkout edits are carried over once, and a host render can no longer let the checkout decide what runs. Merge it into 5a's branch only once both are approved, so main never gets 5a alone.
Overrides: whole files in
infra/docker/overrides/services/<id>/<same relative path>, used in place of the release's file. The directory is git-ignored and Docker-ignored.O_NOFOLLOW, thenfstat). Debian's and Ubuntu's default umask of 002 makes a plainmkdir/cpgroup-writable, which render refuses. The overrides docs, the basic-auth steps and the release note therefore say to create overrides underumask 022, and how to fix existing ones (chmod -R g-w,o-w). CI'suid 1001, umask 002job found this.mkdir -pof the file's path leaves).generated-routes.yml, which render writes itself. Ignoring them means a release that drops a file never blocks an upgrade.config/dynamic/(an operator'sauth.yml) is added to Traefik's dynamic directory, even though the release doesn't ship it.valhalla.jsonis the seed of its writable copy.state/ops-agent/overrides.jsonrecords each override's bytes and the release file it replaced. Render, the agent's log,checkand Run diagnostics flag an override whose release file has changed since. When the override's bytes change, render records the current release file as reviewed: editing the override is how the operator acknowledges the change. The overrides docs say so.Reporting. Render's output, the agent's log (on each commit and at start),
pnpm openmapx checkand Run diagnostics report on the service files. Run diagnostics runs the agent's owncheck. This replacesdescribeBuiltInServiceDrift(removed). The report covers:services/. Their names come fromdocker inspect, and Traefik's dynamic directory keeps its own notice from fix(traefik): keep its dynamic directory in the agent's state, not the checkout #633.An unusable override reaches the admin panel. A configuration saved there (service config, selection, integration config, enable or disable) is then not applied, and the panel shows the reason instead of "Service administration failed":
configuration.failure.inspect, after anot_committed/failedresult.{ error }. 409 is declared on the five operations; the OpenAPI document and SDK were regenerated. Only file names reach the response.The one-time import runs inside the commit, before the first generation that uses the release's files. It runs on the agent and in the CLI alike.
auth.yml), and a differingtraefik.yml(thebasic-auth@filereference).checkand Run diagnostics say why, listing the checkout files not used meanwhile. It tries again on every render.git merge-base --is-ancestor HEAD <release revision>. No revision label, a missing revision object, or no git means it waits.services/service-file-history.jsonmust list no version of a release file that the release's history lacks. CI keeps that file current on every commit (5a), so a commit ahead of the release that changes a service file adds a version the release doesn't know. Files the release no longer names are skipped, except Traefik's dynamic files, which would be imported. A checkout without the file predates 5a, so it is behind. An unreadable or malformed file means it waits. I chose this over comparingservice.jsonhashes because a checkout behind the release, the panel-updated case, usually has older manifests too, so the import would never run there.main, so only local or unchecked commits, which are then treated as the operator's own edits); a history file deleted from a checkout that is ahead; and a fork with its own service-file versions, which waits (safe).auth.ymlor another dynamic*.yml/*.yaml/*.tomlthe release doesn't ship, or a differingtraefik.yml), the render refuses, on the host and in the agent, so the panel shows the reason. The message names the files and says to put them ininfra/docker/overrides/services/traefik/underumask 022; that ends the wait for those files.checkreports it as an error.checkand Run diagnostics.writableCopyfile (valhalla.json), andgenerated-routes.yml.traefik/.state/ops-agent/overrides-import.jsonrecords it. Afterwards, checkout edits are reported as unused.OPENMAPX_RELEASE=local. Underlocalthat now holds for the bundled agent as well (5a), and its start-up report reads no bundled files.e5b89eb. Of its 21 service files, onlyvalhalla.jsonmatches no released version, and that file is a writable copy. So the import there imports nothing. The fixture test reproduces this.Host render rule (decision 6).
docker run --rm --network none --read-only <image> node dist/cli.mjs compose release-files, a hidden command that prints the bundle's index;service.jsonfiles or service files differ from the release's. Overridden files and writable copies are exempt, and so are the edits the import is about to take, which exist only when git says the checkout is at or behind the release. A refused render therefore imports nothing and leaves no marker. Otherwise the commit runs the import with git's answer. When the import waits, the refusal also says why the checkout's edits weren't taken along.compose releaseagain, orgit checkout <the release's source revision>. It also points atoverrides/.getting-startednow runscompose releasebeforecompose render.Rollback to N−1:
overrides/, the store and the Valhalla copy. Edits made only inoverrides/don't apply there; the release note says so.Panel self-update order:
services update --no-deps data-manager app-web transitous-runner) runs the import and writes the trees.PR 8 preconditions, added to the plan:
services/;Check's ownership walk. Valhalla runs without a
user, so the walk exempts its read-write mount of the copy,data/valhalla/config/valhalla.json, as it does other user-less services' read-write mounts. A root-owned rewrite isn't reported. A test covers it.Verification
Overrides and the import (
service-file-overrides.test.ts, against a copy of the repository's real services):martin.yaml,traefik.yml,auth.yml(in the dynamic directory) andvalhalla.jsonare used, and listed by name with no hash in the output. Removingauth.ymldrops it;martin.yaml's path and a group-writableoverrides/serviceseach fail by name;security.txtincluded, and well-known keeps exactly its two files;martin.yaml,auth.ymland atraefik.ymlwithbasic-auth@file. The basic-auth gate reaches the generation and Traefik's directory, and the checkout is untouched;generated-routes.ymlandvalhalla.jsonare not imported;martin.yamland its history lists the new bytes; the operator changed OTP'sbuild-config.json): no overrides and no marker. The report names both files, Martin mounts the release's file, and the import waits again on the next render and for a malformed history. With the release of that commit, onlybuild-config.jsonis imported;auth.ymland a gatedtraefik.yml: the render refuses with the documented message, never with the hash, and commits nothing. With the same two files inoverrides/, it renders: the generation'straefik.ymlnamesbasic-auth@file, Traefik's directory servesauth.yml, andmartin.yamlis still reported as waiting;OPENMAPX_RELEASE=localwith the bundle's modules swapped in (release-service-files-local.test.ts): the agent's render mounts the checkout's edit and imports nothing. Withoutlocal, the same render imports it.Host rule (
cli-compose.test.ts, in a real git checkout):auth.ymlit refuses by that name; once the file is an override, only themartin.yamldifference remains. The same holds when the image names no revision;service.jsonis refused with the documented message.Admin panel path:
configuration.failure.inspect, and commits once the override is fixed;TrustedConfigurationApplyErrorwith status 409 and that message, and falls back with an older agent;POST /admin/services/:id/configanswers 409{ error }after restoring the saved values;serviceOrThrowshows the reason.Self-update and check:
checkreports that the import waits and what a host render would refuse when git says the checkout is ahead; otherwise the import to come, the overrides, a container on the checkout and, after the import, the host render's refusal;Release image: I built the ops-agent image locally from this branch. The host's exact
docker run … --read-only … compose release-filesprinted the index: 21 files, 30 manifests, the history, identical to the local bundle's.Checks:
promote.test.ts) failed once and passed on its own rerun;serviceAdmintest;tsc --noEmitfor core, CLI, ops-agent and API;check-service-files,check-ops-authority,check-dockerfile-sync,check-openapi,check-api-sdkandcheck-docker-context-secrets.Not performed:
getting-started's newcompose releasefirst on a fresh install. It needs the registry;compose releasealready writes its own state directory.Nothing on the production host was changed; it was only read.
Release and upgrade impact
Product impact: migration, operator-action
release-notes/service-files-from-the-release.mdcovers 5a and 5b:compose uprecreates every service whose mounts moved, PostGIS and Traefik included, while the documented host update (services updatewith a list) recreates only the listed ones;Docs:
writableCopy, generating the file list).No package release: I compared tsup builds of
@openmapx/extension-cliat 5a and at this branch, with esbuild's identifier renumbering normalized away.chunk-*.jsandserver-*.jsare identical.cli.jsgains only:configuration.failure.inspectkind, policy and schemas;release-service-files.ts, moved.None of the functions it calls change. 5a's changeset covers
writableCopy. After the review fixes, I rebuilt@openmapx/extension-cliat this branch: it bundles none of the changed modules (import, overrides, writable copies, release service files).