Files
dejima-guard/bin/dejima-guard-loop
branderson 58a81b2856 Run in a container, for a host that has a fleet of them and no timer
Optional and additive: the program stays single-shot, a systemd host still
uses the timer, and nothing about the design changes. What changes is that
the scheduler can be a container runtime.

TWO THINGS HAD TO CHANGE FOR IT TO WORK AT ALL, and the first would have
failed on the very first run.

known_hosts had nowhere to live. StrictHostKeyChecking is left at the ssh
default on purpose - this is the one connection whose whole point is noticing
that something about that machine changed - but a container filesystem is new
every run, so there is no known_hosts, and BatchMode turns the prompt into a
failure. Every collection would have reported "could not reach the devbox" at
max priority. A false alarm that never stops is worse than no alarm, because
it is the one that gets muted. It now points at the state volume.

And configuration was file-only, which is friction in a compose file where
everything else about a deployment already lives. The environment is read
first, the file second. Secrets belong in the file: an environment variable
is visible in `docker inspect` and to anything that can read /proc.

The loop is its own program rather than a flag, because single-shot is what a
timer wants, what a test can drive, and what an operator can run by hand
without wondering whether they have started something. A failed cycle does
not stop it - the failure this design exists to catch is a devbox that stops
answering, and a loop that gave up there would go quiet exactly when it had
something to say.

Debian rather than Alpine, and it is correctness rather than habit: the diff
producing the change list uses --unchanged-line-format, which is GNU
diffutils and not busybox.

No port is published and none ever should be. What makes a container an
acceptable home for a program holding an SSH key to the devbox is that
nothing can connect to it.

NOT BUILT OR RUN. There is no docker in the environment this was written in,
so the Dockerfile and compose file are reviewed rather than tested. One error
was caught that way: a secret with `target:` naming an absolute path is a
swarm feature and does nothing under plain `docker compose`, so the config
secret is read from /run/secrets and DEJIMA_GUARD_CONF points at it.

`bats test/` 20 passing, 4 new.
2026-08-28 01:12:05 -07:00

44 lines
1.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright (C) 2026 Brad Anderson
#
# Runs the guard on an interval. For containers, where there is no systemd
# timer to do it.
#
# A separate program rather than a flag, because dejima-guard is single-shot
# and worth keeping that way: single-shot is what a timer wants, what a test
# can drive, and what an operator can run by hand without wondering whether
# they have started something. This wraps it; it does not become it.
#
# The loop still listens on nothing. That is the property that made a container
# acceptable in the first place - not that the process is short-lived, but that
# nothing can connect to it.
#
# A FAILED CYCLE MUST NOT STOP THE LOOP. The failure this whole design exists
# to catch is a devbox that stops answering, and the guard reports that by
# exiting non-zero. A loop that gave up there would go quiet at the exact
# moment it had something to say - and the heartbeat, which only fires on a
# clean cycle, would stop too, so the dead-man's switch would report the guard
# as dead rather than the devbox as unreachable. Both true, one useful.
set -uo pipefail
INTERVAL=${DEJIMA_GUARD_INTERVAL:-900}
GUARD=${DEJIMA_GUARD_BIN:-/usr/local/bin/dejima-guard}
case "$INTERVAL" in
''|*[!0-9]*) echo "dejima-guard-loop: interval must be seconds, got '$INTERVAL'" >&2; exit 64 ;;
esac
[ "$INTERVAL" -ge 60 ] || { echo "dejima-guard-loop: refusing an interval under 60s" >&2; exit 64; }
# Terminate promptly on the signals a container runtime sends, rather than
# after however much of the sleep is left.
trap 'exit 0' TERM INT
echo "dejima-guard-loop: every ${INTERVAL}s" >&2
while :; do
"$GUARD" check || true
sleep "$INTERVAL" &
wait $!
done