Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/nix/script-tests/refresh-consumer.sh
atlas be7eaa8181 refresh-consumer: compare mtimes locale-independently so an unchanged secret doesn't restart its unit
`refresh_unit` read the credential file's mtime with
`stat -c %.6Y | tr -d .`. coreutils prints that fraction with the
locale's decimal separator, so on a host whose locale uses a comma
(muede-lpt2: `1791232888,193159`) `tr` stripped nothing, the `-le`
against the unit's start errored, the "unchanged" early return was
skipped, and every run try-restarted the consumer. Every unit on a
store-refresh timer was restarted every ~10 minutes; for
`swarm-bao-ci-runner` that is `gitea-runner-swarm`, which killed every
CI job running at the time.

The mtime is now read with `date -r … +%s%6N`, the same digit-only
format the start time already used (`date -d … +%s%6N` on systemd's
fixed English timestamp), so neither side has a separator for a locale
to change.

A value that still does not read as digits — an unparsable start time,
a missing file — now restarts nothing and logs at `err`. Skipping on
doubt rather than restarting on doubt: the file's mtime is kept, so a
genuinely changed credential still restarts its consumer on the first
run that can read both times, whereas restarting on doubt restarts it
on every run, which is this bug. Both reads also fall back to empty
instead of aborting the caller's `set -e` script.

`script-test-refresh-consumer` runs the rendered helper under
`de_DE.UTF-8` against a stub `systemctl`, and checks first that `stat`
really prints a comma there. The helper on main fails 14 of its
assertions.

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

130 lines
4.4 KiB
Shell

# Cases for ./refresh-consumer.nix. Each case sets the credential file's mtime
# and the consumer's last start, runs the helper with a stub `systemctl` that
# records every call it is asked to act on, and compares that record and what
# the helper wrote to stderr.
set -uo pipefail
failures=0
count=0
fail() {
echo "FAILED: $case: $*" >&2
failures=$((failures + 1))
}
dir=$(mktemp -d)
mkdir "$dir/bin"
calls=$dir/calls
path=$dir/token
: >"$path"
# `show` prints $STARTED as systemd's `--timestamp=us+utc` does; `is-active`
# and `is-failed` answer from $ACTIVE and $FAILED; every other verb is
# appended to $calls.
cat >"$dir/bin/systemctl" <<STUB
#!$BASH
args=" \$* "
case "\$args" in
*" show "*) printf '%s\n' "\$STARTED" ;;
*" is-active "*) [ "\$ACTIVE" = 1 ] ;;
*" is-failed "*) [ "\$FAILED" = 1 ] ;;
*) printf '%s\n' "\$*" >>"$calls" ;;
esac
STUB
chmod +x "$dir/bin/systemctl"
export PATH="$dir/bin:$PATH" ACTIVE=1 FAILED=0 STARTED=
export LC_ALL=de_DE.UTF-8
# shellcheck source=/dev/null
. "$HELPER"
# The locale the cases run under has to print the comma the helper must not
# depend on, or none of them tests it.
case="setup"
touch -d @1791232888.193159 "$path"
got=$(stat -c %.6Y "$path")
if [ "$got" != "1791232888,193159" ]; then fail "stat under $LC_ALL printed '$got', wanted 1791232888,193159"; fi
# systemd's `--timestamp=us+utc` rendering of <epoch seconds>.<micros>.
systemd_ts() {
LC_ALL=C date -u -d "@$1" '+%a %Y-%m-%d %H:%M:%S.%6N UTC'
}
# check <description> <want systemctl calls, one per line, or ""> <want stderr, or "">
# <helper call…>
check() {
case=$1
local want_calls=$2 want_err=$3 got err
shift 3
count=$((count + 1))
: >"$calls"
# `set -e` as the callers' NixOS `script`s run: a failed read must not end the
# caller. Not under `||` or `if`, which would switch `set -e` off.
local rc
(
set -e
"$@" >/dev/null 2>"$dir/err"
)
rc=$?
if [ "$rc" != 0 ]; then fail "exited $rc"; fi
err=$(cat "$dir/err")
got=$(cat "$calls")
if [ "$got" != "$want_calls" ]; then
fail "systemctl was asked:"$'\n'"$got"$'\n'"wanted:"$'\n'"$want_calls"
fi
# A failed read also leaves `date`'s own message ahead of the helper's line.
if [ -n "$want_err" ]; then err=$(printf '%s\n' "$err" | tail -n 1); fi
if [ "$err" != "$want_err" ]; then
fail "stderr:"$'\n'"$err"$'\n'"wanted:"$'\n'"$want_err"
fi
}
touch -d @1791232888.193159 "$path"
STARTED=$(systemd_ts 1791232900.000001)
check "a file older than the start restarts nothing" "" "" \
refresh_unit gitea-runner-swarm.service "$path"
check "nor through a container" "" "" \
refresh_consumer swarm-ci-runner gitea-runner-swarm.service "$path"
STARTED=$(systemd_ts 1791232888.193159)
check "a file written in the start's microsecond restarts nothing" "" "" \
refresh_unit gitea-runner-swarm.service "$path"
STARTED=$(systemd_ts 1791232888.193158)
check "a file one microsecond newer than the start restarts the unit" \
"try-restart --no-block gitea-runner-swarm.service" "" \
refresh_unit gitea-runner-swarm.service "$path"
check "through a container, with --machine" \
"--machine=swarm-ci-runner try-restart --no-block gitea-runner-swarm.service" "" \
refresh_consumer swarm-ci-runner gitea-runner-swarm.service "$path"
FAILED=1
check "a failed unit is reset and started" \
"reset-failed gitea-runner-swarm.service"$'\n'"start --no-block gitea-runner-swarm.service" "" \
refresh_unit gitea-runner-swarm.service "$path"
FAILED=0
STARTED=
check "a unit never active is restarted" \
"try-restart --no-block gitea-runner-swarm.service" "" \
refresh_unit gitea-runner-swarm.service "$path"
ACTIVE=0
check "nothing while the container is down" "" "" \
refresh_consumer swarm-ci-runner gitea-runner-swarm.service "$path"
ACTIVE=1
STARTED="not a time"
check "an unreadable start restarts nothing and says so at err" "" \
"<3>cannot compare the mtime of $path ('1791232888193159') with the last start of gitea-runner-swarm.service ('not a time') — leaving gitea-runner-swarm.service as it is" \
refresh_unit gitea-runner-swarm.service "$path"
STARTED=$(systemd_ts 1791232900.000001)
check "a missing file restarts nothing and says so at err" "" \
"<3>cannot compare the mtime of $dir/missing ('') with the last start of gitea-runner-swarm.service ('$STARTED') — leaving gitea-runner-swarm.service as it is" \
refresh_unit gitea-runner-swarm.service "$dir/missing"
echo "$count cases, $failures failed"
[ "$failures" = 0 ]