Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/nix/script-tests/agent-bao-fetch.sh
atlas ccb5bd3b38 hive-agent: read the per-agent queue secret from bao in process
The harness now reads swarm/agents/<agent>/queue from the store itself,
under the agent's own store certificate, and holds it in memory only.
It reads once before the first connect and again on every reconnect
attempt (async-nats `ConnectOptions::with_auth_callback`), so an agent
whose secret was re-minted reconnects with the new value instead of
being refused until the container restarts.

hive-agent-queue-credential.service, the /run file it wrote, and
HIVE_AGENT_QUEUE_AGENT_SECRET_FILE are gone; queue-identity.nix now
hands hive-agent.service the store address, its certificate paths and
the agent name.

A failed or empty read before the first connect still falls back to
the hive's shared client. Each read is bounded by a 10s timeout, and
retries wait out the existing reconnect backoff (500ms doubling, capped
at 60s).

Closes #4783
2026-09-29 10:18:07 +02:00

241 lines
7 KiB
Shell

# Cases for ./agent-bao-fetch.nix. Each case gets a fresh runtime directory and
# credentials directory, runs one unit's script under the unit's `UMask=0377`
# with a clean environment, and asserts on the exit code, the output, the
# files left behind and the `bao` calls made.
set -uo pipefail
failures=0
count=0
fail() {
echo "FAILED: $case: $*" >&2
failures=$((failures + 1))
}
# The umask cases are only meaningful if a 0400 file cannot be reopened for
# writing, which is not so for root.
probe=$TMPDIR/umask-probe
: >"$probe"
chmod 0400 "$probe"
if (: >"$probe") 2>/dev/null; then
echo "a 0400 file is writable by this builder, so the UMask=0377 cases would prove nothing" >&2
exit 1
fi
# setup FORGE <description>
setup() {
prefix=$1
case="$prefix: $2"
local unit_var=${prefix}_UNIT script_var=${prefix}_SCRIPT path_var=${prefix}_PATH addr_var=${prefix}_BAO_ADDR
unit=${!unit_var}
unit_path=${!path_var}
bao_addr=${!addr_var}
dir=$(mktemp -d)
rt=$dir/rt
creds=$dir/creds
log=$dir/bao.log
mkdir -m 0700 "$rt"
mkdir "$creds"
printf cert >"$creds/hive-agent-bao-cert"
printf key >"$creds/hive-agent-bao-key"
: >"$log"
if ! sed "s|/run/$unit/|$rt/|g" "${!script_var}" >"$dir/script"; then
echo "cannot read the rendered script for $unit" >&2
exit 1
fi
chmod +x "$dir/script"
if grep -q '/run/' "$dir/script"; then fail "the script still names /run after the rewrite"; fi
if ! grep -qF "$rt/bao.err" "$dir/script"; then fail "the rewrite did not reach the script's error file"; fi
login_rc=0
login_out=s.fake-token
login_err=
kv_rc=0
kv_out=the-secret
kv_err=
}
go() {
count=$((count + 1))
(
umask 0377
cd "$dir" || exit 99
exec env -i \
PATH="$FAKE_BAO_BIN:$unit_path" \
BAO_ADDR="$bao_addr" \
CREDENTIALS_DIRECTORY="$creds" \
FAKE_BAO_LOG="$log" \
FAKE_BAO_LOGIN_RC="$login_rc" \
FAKE_BAO_LOGIN_OUT="$login_out" \
FAKE_BAO_LOGIN_ERR="$login_err" \
FAKE_BAO_KV_RC="$kv_rc" \
FAKE_BAO_KV_OUT="$kv_out" \
FAKE_BAO_KV_ERR="$kv_err" \
"$dir/script"
) >"$dir/out" 2>"$dir/err"
rc=$?
}
expect_rc() {
if [ "$rc" != "$1" ]; then fail "exit $rc, expected $1; stderr: $(<"$dir/err")"; fi
}
expect_err() {
if ! grep -qF -- "$1" "$dir/err"; then fail "stderr lacks '$1'; stderr: $(<"$dir/err")"; fi
}
expect_no_err() {
if grep -qF -- "$1" "$dir/err"; then fail "stderr has '$1'; stderr: $(<"$dir/err")"; fi
}
expect_out() {
if ! grep -qF -- "$1" "$dir/out"; then fail "stdout lacks '$1'; stdout: $(<"$dir/out")"; fi
}
# expect_calls <line>... — the exact `bao` argument lines, in order.
expect_calls() {
local want
want=$(printf '%s\n' "$@")
if [ "$(<"$log")" != "${want%$'\n'}" ]; then fail "bao calls were '$(<"$log")', expected '$*'"; fi
}
expect_file() {
if [ ! -e "$1" ]; then
fail "$1 missing"
return
fi
if [ "$(<"$1")" != "$2" ]; then fail "$1 holds '$(<"$1")', expected '$2'"; fi
}
expect_no_file() {
if [ -e "$1" ]; then fail "$1 left behind"; fi
}
secret_name=token
path=secret/swarm/agents/a1/forge-token
missing_msg="no forge token at $path yet"
login_call="login -method=cert -token-only"
kv_call="kv get -field=value $path"
setup FORGE "no store identity delivered"
: >"$creds/hive-agent-bao-cert"
go
expect_rc 0
expect_err "has no store identity"
expect_calls
setup FORGE "a credential that cannot be read"
chmod 0000 "$creds/hive-agent-bao-key"
go
expect_rc 1
expect_err "cannot read $creds/hive-agent-bao-key"
expect_calls
setup FORGE "bao's stderr file cannot be created"
chmod 0500 "$rt"
go
chmod 0700 "$rt"
expect_rc 1
expect_err "could not create $rt/bao.err, so bao never ran"
expect_calls
setup FORGE "the store refuses the login with an HTTP 4xx"
login_rc=2
login_err=$'Error authenticating: Error making API request.\n\nURL: PUT '"$bao_addr"$'/v1/auth/cert/login\nCode: 403. Errors:\n\n* permission denied\n'
go
expect_rc 1
expect_err "refused this agent's certificate login with HTTP 403:"
expect_err "* permission denied"
expect_calls "$login_call"
setup FORGE "the store fails the login with another HTTP status"
login_rc=2
login_err=$'Error authenticating: Error making API request.\n\nCode: 500. Errors:\n\n* internal error\n'
go
expect_rc 1
expect_err "failed this agent's certificate login with HTTP 500:"
expect_err "* internal error"
expect_calls "$login_call"
setup FORGE "the store refuses the certificate with a TLS alert"
login_rc=2
login_err="Error authenticating: Put \"$bao_addr/v1/auth/cert/login\": remote error: tls: unknown certificate authority"
go
expect_rc 1
expect_err "refused this agent's certificate in the TLS handshake:"
expect_err "remote error: tls: unknown certificate authority"
expect_calls "$login_call"
setup FORGE "the store does not answer"
login_rc=2
login_err="Error authenticating: Put \"$bao_addr/v1/auth/cert/login\": dial tcp: lookup bao.t.local: no such host"
go
expect_rc 1
expect_err "got no answer from the swarm secret store at $bao_addr"
expect_err "no such host"
expect_calls "$login_call"
# The unit keeps its runtime directory between runs.
setup FORGE "nothing minted at the agent's path yet"
printf old >"$rt/$secret_name"
chmod 0400 "$rt/$secret_name"
kv_rc=2
kv_err="No value found at secret/data/swarm/agents/a1"
go
expect_rc 0
expect_err "$missing_msg"
expect_err "No value found"
expect_calls "$login_call" "$kv_call"
expect_file "$rt/$secret_name" old
setup FORGE "a first fetch writes the secret 0400"
go
expect_rc 0
expect_out "fetched this agent's"
expect_no_err "Permission denied"
expect_calls "$login_call" "$kv_call"
expect_file "$rt/$secret_name" the-secret
if [ "$(stat -c %a "$rt/$secret_name")" != 400 ]; then fail "$secret_name is mode $(stat -c %a "$rt/$secret_name"), expected 400"; fi
expect_no_file "$rt/bao.err"
expect_no_file "$rt/token.new"
# `UMask=0377` makes every file the script creates 0400, the login's own
# `bao.err` included, and a redirect into a 0400 file fails before bao starts.
setup FORGE "0400 files already in place do not block the fetch"
printf stale >"$rt/bao.err"
chmod 0400 "$rt/bao.err"
printf stale >"$rt/token.new"
chmod 0400 "$rt/token.new"
go
expect_rc 0
expect_out "fetched this agent's"
expect_no_err "Permission denied"
expect_no_err "refused"
expect_calls "$login_call" "$kv_call"
expect_file "$rt/$secret_name" the-secret
setup FORGE "an unchanged token is left in place"
printf the-secret >"$rt/token"
chmod 0400 "$rt/token"
before=$(stat -c %i "$rt/token")
go
expect_rc 0
expect_out "is unchanged"
expect_file "$rt/token" the-secret
if [ "$(stat -c %i "$rt/token")" != "$before" ]; then fail "the unchanged token was replaced"; fi
expect_no_file "$rt/token.new"
setup FORGE "a rotated token replaces the old one"
printf old >"$rt/token"
chmod 0400 "$rt/token"
go
expect_rc 0
expect_out "fetched this agent's forge token"
expect_file "$rt/token" the-secret
expect_no_file "$rt/token.new"
if [ "$failures" -ne 0 ]; then
echo "script-test-agent-bao-fetch: $failures failed assertions over $count runs" >&2
exit 1
fi
echo "script-test-agent-bao-fetch: $count runs, all assertions hold"