Files
DDI/test/e2e/server/api.sh
T
beug.k@beugsiud.deandClaude Opus 5.5 e7a0fbb123 test(e2e): pending changes, manual mode with push, diff, rollback, events
New steps before the revoke step: /config/pending reports nothing after
revision 2 and the new address afterwards; the DHCP agent switched to
manual mode stays on revision 2 while the DNS agent reaches 3, and an
explicit push of revision 3 brings it there with a succeeded push
deployment; the diff of revision 3 contains the new host; rolling back
to revision 1 creates revision 4 with SOA serial 4 and without the later
names; /events announces a revision created while listening.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:11:27 +02:00

277 lines
9.2 KiB
Bash
Executable File

#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
#
# REST API steps of the end-to-end test, run inside the server container
# (curl and jq are available there; the API listens on 127.0.0.1 only).
#
# Usage: api.sh <step> [args]
# setup <bootstrap-password> change the password, settings, network, addresses
# token <dns|dhcp> print the enrollment command
# address <ip> <hostname> add a static address
# revision <comment> create a revision, print its number
# wait-applied <rev> <count> wait until <count> agents run <rev> healthily
# agents print the agent list
# deployments print the deployment list
# revoke <role> revoke the agent of a role
# wait-disconnected <role> wait until that agent is disconnected
# pending print "<changed> <#changes> <#files>" of /config/pending
# mode <role> <mode> set the operating mode of the agent of a role
# wait-mode <role> <mode> wait until that agent is connected in that mode
# wait-agent <role> <rev> wait until that agent runs <rev> healthily
# agent-revision <role> print the applied revision of that agent
# deploy <rev> <role> push <rev> to the agent of a role, print the deployment ID
# wait-deployment <id> <outcome> wait until the push deployment <id> has <outcome>
# diff-added <rev> <text> print the number of added diff lines of <rev> containing <text>
# rollback <rev> roll back to <rev>, print the new revision number
# events-revision <comment> create a revision while listening to /events,
# print its number; fails without an "event: revision"
set -eu
base=http://127.0.0.1:8080/api/v1
jar=/tmp/e2e-cookies
csrf_file=/tmp/e2e-csrf
password=e2e-admin-password-1
# req METHOD PATH [JSON] prints the response body; fails on HTTP >= 400.
req() {
csrf=$(cat "$csrf_file" 2>/dev/null || true)
out=$(curl -sS -b "$jar" -c "$jar" -X "$1" -H 'Content-Type: application/json' \
-H "X-CSRF-Token: $csrf" ${3:+--data "$3"} -w '\n%{http_code}' "$base$2")
code=$(printf '%s\n' "$out" | tail -n1)
body=$(printf '%s\n' "$out" | sed '$d')
case "$code" in
2*) printf '%s\n' "$body" ;;
*)
echo "$1 $2 -> HTTP $code: $body" >&2
return 1
;;
esac
}
login() {
req POST /auth/login "{\"username\":\"admin\",\"password\":\"$1\"}" | jq -r .csrf_token >"$csrf_file"
}
session() { login "$password"; }
# agent_id ROLE prints the ID of the (not revoked) agent of a role.
agent_id() {
id=$(req GET /agents | jq -r --arg role "$1" \
'[.items[] | select(.role == $role and .revoked_at == null)][0].id // empty')
if [ -z "$id" ]; then
echo "no agent with role $1" >&2
return 1
fi
printf '%s\n' "$id"
}
# agent_json ROLE prints the agent of a role.
agent_json() {
req GET /agents | jq --arg role "$1" '[.items[] | select(.role == $role and .revoked_at == null)][0]'
}
# dump prints agents and recent deployments for diagnosis.
dump() {
echo "--- agents" >&2
req GET /agents | jq . >&2 || true
echo "--- deployments" >&2
req GET '/deployments?limit=20' | jq . >&2 || true
}
case "$1" in
setup)
login "$2"
req POST /auth/password "{\"current_password\":\"$2\",\"new_password\":\"$password\"}" >/dev/null
session
req GET /settings/generator | jq '
.dns.primary_ns = "ns1.e2e.test"
| .dns.hostmaster = "hostmaster@e2e.test"
| .dns.name_servers = [{"name": "ns1.e2e.test", "addresses": ["192.0.2.53"]}]' >/tmp/settings.json
req PUT /settings/generator "$(cat /tmp/settings.json)" >/dev/null
req POST /networks '{"cidr":"192.0.2.0/24","name":"E2E","gateway":"192.0.2.1","domain":"e2e.test",
"dhcp_enabled":true,"pools":[{"start":"192.0.2.100","end":"192.0.2.199"}],"dns_servers":["192.0.2.53"]}' >/dev/null
req POST /addresses '{"ip":"192.0.2.53","type":"static","hostname":"ns1"}' >/dev/null
req POST /addresses '{"ip":"192.0.2.10","type":"static","hostname":"www"}' >/dev/null
req POST /addresses '{"ip":"192.0.2.150","type":"manual_dhcp","hostname":"printer","mac":"00:11:22:33:44:55"}' >/dev/null
;;
token)
session
req POST /enrollment-tokens "{\"role\":\"$2\",\"description\":\"e2e\"}" | jq -r .command
;;
address)
session
req POST /addresses "{\"ip\":\"$2\",\"type\":\"static\",\"hostname\":\"$3\"}" >/dev/null
;;
revision)
session
req POST /revisions "{\"comment\":\"$2\"}" | jq -r .number
;;
wait-applied)
session
i=0
while [ "$i" -lt 90 ]; do
n=$(req GET /agents | jq --argjson rev "$2" \
'[.items[] | select(.applied_revision == $rev and .health.state == "ok")] | length')
[ "$n" -ge "$3" ] && exit 0
i=$((i + 1))
sleep 1
done
echo "agents did not reach revision $2:" >&2
req GET /agents | jq . >&2
req GET /deployments | jq . >&2
exit 1
;;
agents)
session
req GET /agents | jq .
;;
deployments)
session
req GET /deployments | jq .
;;
revoke)
session
id=$(req GET /agents | jq -r --arg role "$2" '.items[] | select(.role == $role) | .id')
req POST "/agents/$id/revoke" >/dev/null
;;
wait-disconnected)
session
i=0
while [ "$i" -lt 30 ]; do
c=$(req GET /agents | jq -r --arg role "$2" '.items[] | select(.role == $role) | .connected')
[ "$c" = "false" ] && exit 0
i=$((i + 1))
sleep 1
done
echo "agent $2 is still connected" >&2
exit 1
;;
pending)
session
req GET /config/pending >/tmp/pending.json
# Summary for the log, result on stdout.
jq -c '{latest_revision, changed, files: [.files[] | "\(.status) \(.path)"],
changes: [.changes[] | "\(.action) \(.object_type) \(.object_name)"]}' /tmp/pending.json >&2
jq -r '"\(.changed) \(.changes | length) \(.files | length)"' /tmp/pending.json
;;
mode)
session
id=$(agent_id "$2")
req PATCH "/agents/$id" "{\"mode\":\"$3\"}" | jq -e --arg mode "$3" '.mode == $mode' >/dev/null
;;
wait-mode)
session
i=0
while [ "$i" -lt 30 ]; do
ok=$(agent_json "$2" | jq --arg mode "$3" '.mode == $mode and .connected')
if [ "$ok" = "true" ]; then
# The server reconnects the agent to tell it the new mode; give
# the new stream time to settle.
sleep 3
exit 0
fi
i=$((i + 1))
sleep 1
done
echo "agent $2 is not connected in mode $3" >&2
dump
exit 1
;;
wait-agent)
session
i=0
while [ "$i" -lt 90 ]; do
ok=$(agent_json "$2" | jq --argjson rev "$3" '.applied_revision == $rev and .health.state == "ok"')
[ "$ok" = "true" ] && exit 0
i=$((i + 1))
sleep 1
done
echo "agent $2 did not reach revision $3" >&2
dump
exit 1
;;
agent-revision)
session
agent_json "$2" | jq -r .applied_revision
;;
deploy)
session
id=$(agent_id "$3")
req POST "/revisions/$2/deploy" "{\"agent_ids\":[\"$id\"]}" >/tmp/deploy.json
jq -c . /tmp/deploy.json >&2
jq -e --arg id "$id" '.items | length == 1 and .[0].agent_id == $id' /tmp/deploy.json >/dev/null || {
echo "unexpected deploy response" >&2
exit 1
}
jq -r '.items[0].deployment_id' /tmp/deploy.json
;;
wait-deployment)
session
i=0
while [ "$i" -lt 60 ]; do
d=$(req GET '/deployments?limit=100' | jq -c --argjson id "$2" '.items[] | select(.id == $id)')
if [ -n "$d" ]; then
outcome=$(printf '%s\n' "$d" | jq -r .outcome)
trigger=$(printf '%s\n' "$d" | jq -r .trigger)
if [ "$outcome" = "$3" ] && [ "$trigger" = "push" ]; then
printf '%s\n' "$d" >&2
exit 0
fi
case "$outcome" in
pending) ;;
*)
echo "deployment $2 ended with $outcome ($trigger): $d" >&2
dump
exit 1
;;
esac
fi
i=$((i + 1))
sleep 1
done
echo "deployment $2 did not reach $3" >&2
dump
exit 1
;;
diff-added)
session
req GET "/revisions/$2/diff" >/tmp/diff.json
n=$(jq --arg t "$3" '[.files[].diff | split("\n")[] | select(startswith("+") and contains($t))] | length' /tmp/diff.json)
if [ "$n" -eq 0 ]; then
echo "no added line with '$3' in the diff of revision $2:" >&2
jq -r '.files[] | "--- \(.role) \(.path) (\(.status))\n\(.diff)"' /tmp/diff.json >&2
fi
printf '%s\n' "$n"
;;
rollback)
session
req POST "/revisions/$2/rollback" "{\"comment\":\"e2e rollback to $2\"}" | jq -r .number
;;
events-revision)
session
out=/tmp/events.out
rm -f "$out"
curl -sSN --max-time 10 -b "$jar" -H 'Accept: text/event-stream' "$base/events" >"$out" 2>/tmp/events.err &
pid=$!
sleep 2
n=$(req POST /revisions "{\"comment\":\"$2\"}" | jq -r .number)
i=0
while [ "$i" -lt 8 ] && ! grep -q '^event: revision' "$out"; do
i=$((i + 1))
sleep 1
done
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
if ! grep -q '^event: revision' "$out"; then
echo "no revision event received; captured stream:" >&2
cat "$out" /tmp/events.err >&2
exit 1
fi
printf '%s\n' "$n"
;;
*)
echo "unknown step $1" >&2
exit 2
;;
esac