7.8 KiB
Fleet agent reconciliation and upgrade plan
Decisions
- One agent instance owns workload reconciliation for a device at a time.
- NATS desired state is authoritative. Podman labels are the durable observed state. The agent rebuilds its in-memory state from both after every restart.
- Desired entries carry their JetStream revision. Older watch or snapshot data never replaces a newer revision.
- KV watches accelerate convergence. A periodic full snapshot repairs missed events and drives orphan cleanup.
- One worker serializes runtime mutations. New events replace older intent before the next reconciliation pass.
- A deployment upgrade follows Kubernetes replacement semantics: validate and pull first, send the old container its normal stop signal, wait up to the agent's fixed 30-second termination grace period, force it only after that deadline, remove it, then start the replacement. A score-level override is deferred until a real deployment needs one.
- An existing container owned by another deployment or by a human is a conflict. The agent reports it and does not delete the container.
- Agent upgrade never runs two workload reconcilers. The candidate runs only a non-workload-mutating probe. A root-owned updater then switches the permanent systemd service and rolls back if the new agent does not become ready.
Deployment reconciliation
Inputs and reconstruction
The reconciler owns one map from DeploymentName to the latest desired value.
A value is a valid unresolved score or an invalid payload with its error. At
startup and every periodic resync it replaces that map from the current
desired-state.<device>.> KV snapshot. Live puts and deletes update the same
map and wake the worker. A failed or incomplete snapshot cannot authorize
deletion. Because KV key enumeration and value reads are not atomic, a managed
deployment must be absent from two consecutive complete snapshots before it is
removed as an orphan. Any newer watch revision resets that absence count.
An explicit revisioned delete schedules removal immediately; the two-snapshot
rule applies only when reconstructing an absent key.
No local deployment database is needed. After a reboot or power loss, current KV values reconstruct intent and Harmony-managed Podman labels reconstruct observed state.
Reconciliation pass
For one complete desired snapshot:
- Validate every deployment. A duplicate service name or explicit host port fails the conflicting deployments but does not block unrelated deployments.
- Resolve secrets for the current score revision.
- Ask the runtime to converge each valid deployment.
- List Harmony-managed runtime deployments and remove deployments absent from two complete snapshots. Cleanup runs before retrying a desired deployment that needs the same service name.
- Publish coarse
Phaseplus boundedlastError. Only an acknowledged phase and error pair is deduplicated; failed status writes remain dirty.
The runtime convergence operation preflights the full score before stopping anything. It validates ports and service names, pulls missing images, and checks container ownership. Preflight prevents known destructive failures; it cannot make Podman operations transactional. For each changed service the runtime performs stop, remove, create, start, and inspect in that order. Successfully changed services remain changed if a later service fails, and the next pass resumes from observed labels. Unchanged running services are untouched. Missing or stopped desired services are recreated or restarted. Services no longer in the score are removed with the same graceful-stop behavior.
A malformed current payload remains present for orphan accounting, reports
Failed, and causes no workload mutation. The last valid workload is preserved
but no longer reported healthy until the payload is corrected or deleted.
If a newer revision arrives during a runtime call, that call may finish but its
result is not published. The worker immediately reconciles the latest revision;
it never interrupts one Podman operation halfway.
Edge cases
The first implementation and its tests cover:
- new deployment;
- unchanged deployment as a no-op;
- upgrade with stop-before-replace ordering;
- preflight failure without stopping the old version;
- exited desired container reported as failed with its exit details; Podman restart count is included for diagnosis but no time-window crashloop classifier is introduced;
- online deletion;
- offline deletion followed by orphan cleanup when the device returns;
- agent restart and host reboot;
- power loss before stop, after stop, after remove, after create, and after start, each repaired by the next full pass;
- service-name conflict between deployments;
- conflict with an unmanaged or differently owned container;
- desired container manually stopped or deleted;
- malformed desired payload replacing, rather than preserving, old intent;
- partial failure in a multi-service deployment;
- Podman unavailability and retry;
- failed state publication and retry;
- watch interruption or missed events repaired by full resync;
- failed and incomplete full snapshots;
- puts and deletes before, during, and after snapshot enumeration;
- invalid desired keys and failed state deletion;
- a newer desired revision arriving during reconciliation.
Secret values remain cached for one desired revision. Secret-only rotation is out of scope until the secret source exposes a revision or watch contract.
Agent upgrade
The signed authorization and probation design was replaced by stop-switch-start.
The maintained protocol is
docs/design/fleet-agent-upgrades.md;
the original implementation plan remains as a link-preserving pointer in
agent-upgrade-stop-switch-start-plan.md.
Deferred follow-up:
- retry and classify activation readiness failures before rollback or quarantine;
- add an isolated VM agent-upgrade E2E covering success and failure recovery;
- rerun
FleetDeviceSetupScoreto refresh the non-self-updating bootstrap updater, then verify an anonymous pull from a digest-pinned OCI artifact and a target/binary version mismatch. The mismatch must fail during candidate self-test without stopping the active agent; updater child processes must not emit stray systemd notifications or expected broken-pipe warnings; - replace the operator's two-second full-device scan before fleet scale;
- add binary garbage collection when disk pressure makes retention relevant.
Device commands
The dashboard sends bounded one-shot exec requests over Core NATS request/reply.
Exec is an administrative capability, not a sandbox: it runs as the
fleet-agent Unix account and can access that account's credentials and rootless
Podman runtime. Only a verified dashboard session with the exact fleet-admin
project role may use it.
Manual QA must cover stdout, stderr and non-zero exit status, timeout cleanup,
output truncation, an offline device, and denial for a dashboard user without the
fleet-admin role.
Delivery checkpoints
- Rework deployment reconciliation around a complete snapshot and a fakeable runtime boundary. Add the full unit matrix and retain the existing VM E2E.
- Update Podman convergence for ownership checks, preflight, graceful stop-before-replace, inspection, and orphan inventory.
- Amend ADR-022 and add attempt-scoped upgrade contracts.
- Add the updater, probe mode, exclusive agent lock, durable transaction, and operator coordination.
- Run unit tests, compile environment-gated E2E, run focused local VM tests if available without production credentials, and complete independent reviews.
- Production QA is manual and stepwise: deployment recovery, bounded device commands, bootstrap-updater diagnostics, then one agent upgrade attempt with rollback rehearsed before any canary rollout.