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>
277 lines
9.2 KiB
Bash
Executable File
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
|