93 lines
4.2 KiB
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"
|
|
}
|
|
''
|