DD-0045 — Development loop interface: image catalogue, deployment receipt and takeover lease
Related ADR: ADR-0045.
Status: Draft for review. The lease, mode, expiry and catalogue decisions were reviewed with danias before implementation; dev-box qualification follows each pull request.
Date: 2026-10-14.
Scope
Three commands share one dev box:
On macOS, deploy and reconcile send the exact Git commit as a bundle to an
SSH Linux runner. The runner uses the existing Linux tools and the same receipt,
image and lease protocols. Its owned checkout is separate from operator source;
an explicit REMOTE_ROOT can reuse an existing installation's private state.
make reconcile and takeover share the same delivery interfaces. They agree on three
interfaces, described here: the image catalogue, the deployment receipt and the
takeover lease. The workstation watcher follows saved files directly. The root
make tilt adds a dashboard: saved files invoke takeover, while hand-back,
delivery, status and log snapshots remain explicit actions.
Image catalogue
One file, gitops/manifests/images.yaml, lists every image the platform builds. It
replaces devops/parallel/build-inventory.json from #626 and the inputs that
dx/common/scripts/devbox/source.py and themelio_compat.py encode today.
It sits in gitops/manifests, since only root Applications, apps and manifests
may appear at the GitOps root.
inputsare the paths whose content decides the image, including shared modules, contracts and lockfiles. The Dockerfile,args,targetandplatformare always inputs. Test files and documentation are excluded by the existingruntime_pathrule.- The fingerprint of an image is the SHA-256 of its inputs' paths, modes and
contents, plus the Dockerfile,
args,targetandplatform. It is computed from a Git tree formake reconcileand from the working tree for takeover, with the same function. - A check (
check_platform_gitops.py) fails when a DockerfileCOPYs a path that is not an input, when an input does not exist, or when an image named bygitops/manifestsis not in the catalogue. applicationnames the Argo CD Application whose workloads use the image; the lease and the receipt use it.
Deployment receipt
make reconcile and make deploy write one receipt per delivery, and takeover
reads the last one as its baseline.
- In the cluster: the existing
pharos-platform-ownershipConfigMap gains the delivered commit (already itsrevision) and the image map. That map is part of the root's inputs, so the root, not a patch on a child, passes each digest to its Application'skustomize.images, as #679 does forthemelio-compat. - Locally:
.local/metallo/<target>/receipt.jsonrepeats it with each image's fingerprint, so takeover can compare without a cluster round trip:
- Order: the receipt and the root's inputs change only after every changed image has built and been pushed. A failed build leaves the previous delivery in place.
- Takeover's comparison: an image whose working-tree fingerprint equals the
receipt's runs in the cluster. One that differs is a candidate for takeover if it
supports a runtime there, or needs
make reconcileif not (actors, migrations).
Builds in the cluster
- A BuildKit Job per changed image in the
pharos-buildnamespace. Its context is the delivered commit, fetched from the in-cluster Git server, so the build uses the commit that was delivered and nothing is uploaded from the workstation. - An in-cluster registry in the same namespace, on a Ceph block volume, which also
holds the BuildKit cache. k3s pulls from it through a
registries.yamlmirror thek3srole installs, over the cluster network only. - Requests and limits are set per Job; builds run one at a time by default.
- Images are named by digest in the root's inputs and never by tag.
The pharos service
gitops/apps/base/pharos.yaml is installed on every dev box on wave 3, in the
bootstrap's pharos namespace; apps/overlays/devbox sets its path to
gitops/manifests/pharos/overlays/devbox. The overlay removes the base's Namespace
and the Minikube Ingress, so the web has no edge route until the new Themelio
replaces themelio-compat. It adds the namespace's administration, in sync waves:
- Credentials. Every key in
devops/secret-keys.jsonhas one value, kept inpharos-generated, and each<owner>-credentialsSecret receives only its own keys, so shared keys agree. A key the inventory gains is added; an existing value is never changed, and a Secret that differs from the record is refused. A lost record is recovered from the workloads' Secrets. The Job prints names only. - Databases. The provision Job waits for PostgreSQL, then creates each
context's role and database and applies its migrations from the bindings in
metallo-credentials. It alone receives the administrator password. database-adminimage. PostgreSQL's client with Python, the inventory, the context registry, the migrations and the scripts, running as UID 70.check_platform_gitops.pyrequires every registered context's binding in the Job and the inventory, its migrations in the image's inputs, and every namespace-wide policy before the hooks.- Transition.
overlays/metallo-devboxinstalls the same base inmetallo-devboxfor the Metallo controller's own loop, until its takeover moves topharos.
Takeover lease
A takeover borrows the Applications whose workloads it replaces. While it holds them, Argo CD syncs neither those Applications automatically nor by hand. Every other Application, and the shared GitLab, Waldur, AAI and edge, keep syncing.
Mode: one developer per dev box. A target has at most one lease. It covers
every Application the takeover touches (pharos, themelio-compat or both). A
second checkout is refused rather than given its own lease.
The lease is a sync window
The lease has two parts:
- A record in the ownership ConfigMap: the holder (checkout and workstation), the Applications, the receipt's commit, when it was taken and when it expires.
- A deny sync window on the Applications' AppProject, naming only the leased
Applications, with
manualSync: false. A deny window stops automated syncs, self-heal included, and withmanualSync: falsealso manual ones, for those Applications only.
The Applications' own sync policy does not change. The root therefore has nothing to restore while the lease is held, and Argo CD shows the leased Applications as OutOfSync without reverting them.
The AppProjects belong to the Ansible bootstrap and the project-writer admission
fence. The controller uses the existing bootstrap administrator identity, which
the fence already permits; Argo CD service accounts remain denied. It preserves
other windows and adds or removes only its own deny window with
manualSync: false, matching the Applications in the ownership record.
Expiry
- A window has a start (
schedule, a cron expression for the minute it begins) and aduration. The lease's window starts when it is acquired and lasts the lease's time to live, 12 hours by default. - While the takeover runs, it renews the lease every 5 minutes, moving the
record's expiry and the window's
durationforward. - If the workstation stops renewing (closed laptop, lost connection, crash), the window ends on its own. Argo CD then syncs the Applications again, and self-heal restores their workloads from the receipt's commit without any command.
- The next command for the target (takeover,
make reconcileormake deploy) removes the expired record and window and continues. A later takeover from the same checkout starts a new lease.
Acquire
- Refuse if an unexpired lease, an unfinished
make deployormake reconcile, or a different checkout already owns the target. - Write the record, then add the window.
- Wait for any operation in progress on the leased Applications to finish, or end it if it is for a superseded revision, as the health wait does today.
- Only then replace the workloads.
While held
make reconcile and make deploy refuse to deliver while an unexpired lease
exists. The holder hands back first.
Hand-back
- Stop the local runtime.
- Detach replacements and restore container aliases. Queue agent removal until every local runtime has stopped; namespace-scoped uninstall can then reconnect without disrupting another service. Journal each cleanup before proceeding.
- Remove the window and then the record. Argo CD syncs the Applications to the receipt's commit, and self-heal restores their workloads.
- Wait until the Applications are Synced and Healthy at that commit.
Interruption and recovery
- The record and a local journal mark each step, so a retried acquire or hand-back continues from the last completed step.
- A window without a record, or a record without a window, is completed or removed by the next command for the target.
- Qualification includes an interrupted acquire, an interrupted hand-back, a dropped connection, a sleeping workstation and an expired lease.
Decisions from review
- Manual sync is blocked only for the Applications under takeover, through the deny sync window; Argo CD's RBAC does not change.
- One developer per dev box, so one lease per target.
- A lease expires when its holder stops renewing it.
- The catalogue's inputs as listed above.
Page source
Source snapshot · Revision f928834c8.