Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/nix/host-modules/lib/refresh-consumer.nix

93 lines
4.2 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
# that is neither running nor failed (stopped on purpose, never started, or
# a per-connection daemon with no unit of its own) is left alone, and
# nothing is logged for it. `--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"
elif systemctl "$@" is-active --quiet "$unit"; then
echo "$path changed after $unit last started — restarting it"
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"
}
''