From d6a8ce68580d303c74923a4123812fd018f37d09 Mon Sep 17 00:00:00 2001 From: Josh Poole Date: Sun, 16 Aug 2026 12:37:53 +0100 Subject: [PATCH] 20260816 - Add retina-telemetry to the node stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The node-side telemetry uplink, pinned like every other image via ${TELEMETRY_V:-v0.1.0}. One container per node, owning everything sent to api.retina.fm: registration, detection streaming, heartbeat and config sync. Three things about this service differ from the rest of the file, and all three are deliberate. No depends_on. Every other service waits on config-merger. This one must keep reporting while the rest of the stack is crash-looping — that is the entire point of it — so gating it would leave it dead exactly when its telemetry matters most. It re-reads its inputs on every cycle, so configuration that appears later is picked up with no restart and no ordering constraint. No docker socket. blah2 and ADS-B liveness are derived from the detection poll instead, which distinguishes "down" from "wedged" where container state cannot, and the versions in the heartbeat come from the image-tag env vars passed here. network_mode: host, to reach blah2_api on 127.0.0.1:3000. It binds no listening ports of its own; every input is a poll or a file read, and there is no inbound path to it at all. Of its four mounts only /data/retina-telemetry is writable. It holds the bearer token at 0600 and the status document at 0644, and lives under /data so the token survives an OS update — losing it means re-registering, which needs an operator to reactivate the node, because the server answers every registration refusal with the same opaque 403. DISK_PATH is set to /data/mender rather than left at its default of /data, which is not mounted here — only its subdirectories are. Without it statvfs would measure the container's own overlay and report a plausible, wrong disk_free_mb. Requires retina-gui >= v0.7.0, the first version that persists the three consent records. Without them the service refuses to register, deliberately and by design: a missing record means the owner was never shown that text, and nothing is ever synthesised. owl-os#51 moves that pin. Not tagged — this lands ahead of cutting a release. --- .env.example | 5 +++++ docker-compose.yml | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/.env.example b/.env.example index 2bdac4c..c385d33 100644 --- a/.env.example +++ b/.env.example @@ -27,3 +27,8 @@ ADSBLOL_RADIUS=40 # ADSB2DD_V=v0.1.3 # SPECTRUM_V=v0.1.0 # RETINA_TRACKER_V=dev +# TELEMETRY_V=v0.1.0 + +# retina-telemetry logging. INFO is quiet by design: it logs starting, +# registering, and the failures that need an operator. DEBUG logs every request. +# TELEMETRY_LOG_LEVEL=DEBUG diff --git a/docker-compose.yml b/docker-compose.yml index bba3c19..ae9d92d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -172,3 +172,48 @@ services: - ${DATA_DIR:-/data/retina-node}/retina-tracker/output:/app/output - ${CONFIG_DIR:-/data/retina-node/config}:/config:ro container_name: retina-tracker + + retina-telemetry: + restart: always + image: ghcr.io/offworldlabs/retina-telemetry:${TELEMETRY_V:-v0.1.0} + # network_mode: host to reach blah2_api on 127.0.0.1:3000. It binds no + # listening ports of its own — every input here is a poll or a file read. + network_mode: host + # No depends_on, unlike every other service. This is the one that must keep + # reporting while the rest of the stack is crash-looping, which is the whole + # point of it: gating it on config-merger would leave it dead exactly when + # its telemetry matters most. It re-reads its inputs on every cycle, so + # config that appears later is picked up without a restart. + volumes: + # Identity: node_id and device_type. node_id comes from here and nowhere + # else — it is never derived — and these files are 0600 root-owned, which + # is the only reason this container runs as root. + - ${MENDER_DATA_DIR:-/data/mender}:/data/mender:ro + - ${CONFIG_DIR:-/data/retina-node/config}:/data/retina-node/config:ro + # retina-gui's device state, for the telemetry consent record. Read-only: + # retina-gui writes it, we only ever read. Needs retina-gui >= v0.7.0, + # which is the first version that persists the three records; without + # them a node refuses to register, deliberately. + - ${GUI_DATA_DIR:-/data/retina-gui}:/data/retina-gui:ro + # The only writable mount. Holds the bearer token at 0600 and the status + # document at 0644, under /data so the token survives an OS update. + # Losing it means re-registering, which needs an operator to reactivate + # the node — the server returns one opaque 403 for every refusal. + - ${TELEMETRY_DATA_DIR:-/data/retina-telemetry}:/data/retina-telemetry + # No docker socket. blah2 and ADS-B liveness are derived from the detection + # poll, which distinguishes down from wedged where container state cannot, + # and versions come from the image tags below. + environment: + # host.py would otherwise default to /data, which is not mounted — only + # its subdirectories are — so statvfs would measure this container's + # overlay instead of the node's storage and disk_free_mb would be + # fiction. /data/mender is mounted, read-only, and on the same filesystem. + - DISK_PATH=/data/mender + # Image tags, so `versions` in the heartbeat reports something true. + # RETINA_NODE_V has no source on a node yet; it is passed so it starts + # working the moment owl-os provides one. + - BLAH2_V=${BLAH2_V:-v0.4.2} + - OWL_OS_V=${OWL_OS_V:-} + - RETINA_NODE_V=${RETINA_NODE_V:-} + - LOG_LEVEL=${TELEMETRY_LOG_LEVEL:-INFO} + container_name: retina-telemetry