Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/nix/host-modules/lib/refresh-consumer.nix
atlas 8d554477ca swarm-bao-agent-pki: decide openbao's restart on the shared locale-independent compare
`swarm-bao-agent-pki` carried its own copy of the mtime-vs-start compare,
with the same `stat -c %.6Y | tr -d .` read: on a comma-locale host the
`-le` errored and, under `seal = "pkcs11"`, openbao was restarted on
every run of the unit.

The compare moves out of `refresh_unit` into `written_after_start`
(0 newer, 1 not newer, 2 unreadable and logged at `err`), and both
`refresh_unit` and the agent-pki unit call it. The agent-pki unit cannot
call `refresh_unit` itself: it restarts with `restart`, not
`try-restart`, runs a fail-closed check of the bundle between the
compare and the restart, and under shamir prints the step instead of
restarting. It acts only on 0, so an unreadable time skips the restart
as `refresh_unit` does.

`script-test-refresh-consumer` covers the three return codes in the
agent-pki call shape; `module-eval-bao-basics` asserts both seal arms
decide on that call and that no `%.6Y` read is left in the unit.

Refs #5230
2026-10-13 17:00:56 +02:00

91 lines
4 KiB
Nix

# Shared shell steps for a host oneshot that fetches a credential into a
# file a service loads at start (`LoadCredential`, or a config file expanded
# once while parsing). Such a service never sees a value that lands after it
# started — late or rotated — until it restarts.
#
# The file's mtime is the record of a change, so write the file only when
# `secret_differs` says so. The restart decision is then re-derived from
# the file on every run: a run that wrote the file but failed before the
# restart leaves a retry that still sees the file newer than the service.
#
# Pure function — NOT a NixOS module. Call it from a module's `let`:
#
# refreshConsumer = import ./lib/refresh-consumer.nix { };
# script = ''
# ${refreshConsumer}
# if secret_differs "$path" "$secret"; then
# atomic_write_secret 0400 root:root "$path" "$secret"
# fi
# refresh_consumer my-machine my.service "$path" # a unit in a container
# refresh_unit my.service "$path" # a host unit
# '';
#
# Requires `systemd` and `coreutils` on the caller's `path`.
{ }:
''
# True when $1 is missing or holds something other than $2. `$(< path)`
# is a bash builtin, so the value never becomes an argument in /proc;
# both sides lose their trailing newlines, which is how
# `atomic_write_secret` writes and `$(bao …)` reads.
secret_differs() {
[ ! -f "$1" ] || [ "$(< "$1")" != "$2" ]
}
# <unit> <path> [systemctl option…]. Returns 0 when <path> was last
# written after <unit> last entered `active` (or <unit> never has), 1 when
# it was not, and 2, logged at `err`, when either time does not read. Both
# are bare microsecond counts, with no decimal separator for a locale to
# change. A caller acts only on 0: the mtime stays, so the first run that
# can read both still acts, where acting on doubt acts on every run. The
# options are passed to `systemctl`, which is how a caller reaches into a
# container.
written_after_start() {
local unit="$1" path="$2" started started_us=0 written_us
shift 2
# Empty for a unit that has never been active, which `date` would
# otherwise read as today's midnight.
started="$(systemctl "$@" show --timestamp=us+utc -p ActiveEnterTimestamp --value "$unit")"
if [ -n "$started" ]; then
started_us="$(date -u -d "$started" +%s%6N)" || started_us=
fi
written_us="$(date -u -r "$path" +%s%6N)" || written_us=
if [[ ! "$started_us" =~ ^[0-9]+$ || ! "$written_us" =~ ^[0-9]+$ ]]; then
echo "<3>cannot compare the mtime of $path ('$written_us') with the last start of $unit ('$started') — leaving $unit as it is" >&2
return 2
fi
if [ "$written_us" -le "$started_us" ]; then
return 1
fi
}
# <unit> <path> [systemctl option…]. Nothing unless `written_after_start`.
# Otherwise a running <unit> is restarted and a failed one — it may have
# hit its start limit without the credential — is reset and started; one
# stopped on purpose stays stopped. `--no-block` because a caller ordered
# `Before=` its consumer must not wait on a job that waits on the caller
# (see `swarm-services-cert`'s propagation in ../hive-tls.nix).
refresh_unit() {
local unit="$1" path="$2"
shift 2
written_after_start "$unit" "$path" "$@" || return 0
if systemctl "$@" is-failed --quiet "$unit"; then
echo "$path changed after $unit last started, and $unit had failed — starting it"
systemctl "$@" reset-failed "$unit"
systemctl "$@" start --no-block "$unit"
else
echo "$path changed after $unit last started — restarting it if it runs"
systemctl "$@" try-restart --no-block "$unit"
fi
}
# <machine> <unit> <path>. `refresh_unit` inside `container@<machine>`, and
# nothing while that container is not active: a container that starts
# later loads the file as it starts.
refresh_consumer() {
local machine="$1" unit="$2" path="$3"
if ! systemctl is-active --quiet "container@$machine.service"; then
return 0
fi
refresh_unit "$unit" "$path" --machine="$machine"
}
''