# 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" ] } # [systemctl option…]. Returns 0 when was last # written after last entered `active` (or 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 } # [systemctl option…]. Nothing unless `written_after_start`. # Otherwise a running 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 } # . `refresh_unit` inside `container@`, 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" } ''