test: end-to-end test of the agent path, fix first CI findings
ci / Web (lint, test, audit, build) (push) Successful in 1m1s
ci / Shell and licence headers (push) Successful in 7s
ci / Go (lint, test, vuln) (push) Failing after 2m40s
ci / Protobuf (push) Successful in 6s
ci / Release build (dry run) (push) Successful in 2m27s
ci / End-to-end (server, agents, BIND, Kea) (push) Failing after 1m26s
ci / Development environment (compose) (push) Successful in 13s
ci / Integration (real BIND and Kea) (push) Failing after 1m58s

test/e2e runs server, PostgreSQL, BIND and Kea with both agents in
containers (make test-e2e, CI job End-to-end): enrollment with the
command from the API, revision 1 and 2 applied and served by BIND and
Kea, unchanged data creates no revision, revoking disconnects the agent
while BIND keeps serving.

Fixes from the first CI run: quote a spec description containing a
comma, exempt the agent lock-out routes in the route protection test,
whole-second certificate lifetimes, a push replacing queued pulls, the
truncation marker containing the letter checked, and curl|grep -q under
pipefail in the server smoke test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
beug.k@beugsiud.de
2026-10-13 19:29:58 +02:00
co-authored by Claude Opus 5.5
parent 56892631be
commit 56347ec5ff
23 changed files with 485 additions and 13 deletions
+19
View File
@@ -135,6 +135,25 @@ jobs:
DDI_DATABASE_URL: postgres://ddi:ddi-ci-only@postgres:5432/ddi?sslmode=disable
run: scripts/ci/smoke-server.sh dist
# Needs a runner with access to a Docker daemon (see docs/adr/0003).
e2e:
name: End-to-end (server, agents, BIND, Kea)
runs-on: ubuntu-latest
env:
# The Docker daemon may be shared: unique compose project.
COMPOSE_PROJECT_NAME: ddi-e2e-${{ github.run_id }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "24"
- name: API change reaches BIND and Kea through the agents
run: make test-e2e
# Needs a runner with access to a Docker daemon (see docs/adr/0003).
integration:
name: Integration (real BIND and Kea)
+1
View File
@@ -38,3 +38,4 @@ Thumbs.db
# Local worktrees of Claude Code agents
/.claude/worktrees/
/test/e2e/bin/
+5
View File
@@ -139,6 +139,11 @@ test-go:
test-web:
cd web && npm test
## test-e2e: API change -> revision -> agents -> BIND/Kea in containers (needs docker)
.PHONY: test-e2e
test-e2e: build
test/e2e/run.sh
## test-integration: generated configs and agent roles against real BIND and Kea (needs docker)
.PHONY: test-integration
test-integration:
+11 -4
View File
@@ -444,11 +444,18 @@ func TestDesiredAndPush(t *testing.T) {
t.Fatal("request queued for revision 0 or manual mode")
}
f.agent.Desired(3, true)
if r, ok := f.agent.dequeue(); !ok || r.revision != 3 || r.trigger != TriggerPull {
t.Errorf("pull request = %+v, %v", r, ok)
}
// A push replaces queued pulls up to its revision.
f.agent.Desired(3, true)
f.agent.Push(4, "dep")
r1, _ := f.agent.dequeue()
r2, _ := f.agent.dequeue()
if r1.revision != 3 || r1.trigger != TriggerPull || r2.revision != 4 || r2.deploymentID != "dep" {
t.Errorf("requests = %+v %+v", r1, r2)
r, _ := f.agent.dequeue()
if r.revision != 4 || r.trigger != TriggerPush || r.deploymentID != "dep" {
t.Errorf("push request = %+v", r)
}
if extra, ok := f.agent.dequeue(); ok {
t.Errorf("pull for revision 3 was not replaced: %+v", extra)
}
}
+2 -1
View File
@@ -65,7 +65,8 @@ func TestExcerptAndLimit(t *testing.T) {
_, _ = l.Write([]byte("cc"))
_, _ = l.Write([]byte("dd"))
b := l.Bytes()
if !strings.HasSuffix(string(b), "[output truncated]") || strings.Count(string(b), "c") != 1 {
if !strings.HasSuffix(string(b), "[output truncated]") || strings.Count(string(b[:MaxCapture]), "c") != 1 ||
strings.Contains(string(b), "d") {
t.Errorf("limited buffer kept %d bytes", len(b))
}
}
+4 -4
View File
@@ -62,9 +62,9 @@ fail() {
exit 1
}
curl -fsS "$base/readyz" | grep -q '"database":"ok"' || fail "readyz"
curl -fsS "$base/metrics" | grep -q '^ddi_license_writable 1' || fail "metrics"
curl -fsS "$base/api/v1/system/info" | grep -q '"license_state":"trial"' || fail "system info"
grep -q '"database":"ok"' <<<"$(curl -fsS "$base/readyz")" || fail "readyz"
grep -q '^ddi_license_writable 1' <<<"$(curl -fsS "$base/metrics")" || fail "metrics"
grep -q '"license_state":"trial"' <<<"$(curl -fsS "$base/api/v1/system/info")" || fail "system info"
# The web UI is embedded: index.html references hashed assets that load.
index="$(curl -fsS -D "$work/headers" "$base/")"
@@ -75,7 +75,7 @@ asset="$(grep -o 'src="/assets/[^"]*\.js"' <<<"$index" | head -n1 | sed 's/src="
curl -fsS -o /dev/null -D "$work/asset-headers" "$base$asset" || fail "asset $asset"
grep -qi '^cache-control: .*immutable' "$work/asset-headers" || fail "assets must be immutable"
# Client-side routes fall back to the UI.
curl -fsS "$base/addresses?q=x" | grep -q '<div id="root">' || fail "SPA fallback"
grep -q '<div id="root">' <<<"$(curl -fsS "$base/addresses?q=x")" || fail "SPA fallback"
# Unknown API paths are JSON errors, not the UI.
code="$(curl -s -o /dev/null -w '%{http_code}' "$base/api/v1/nope")"
[[ "$code" == "401" || "$code" == "404" ]] || fail "unknown API path returned $code"
+1 -1
View File
@@ -704,7 +704,7 @@ paths:
parameters:
- { name: network_id, in: query, schema: { type: string } }
- { name: state, in: query, schema: { $ref: "#/components/schemas/LeaseState" } }
- { name: q, in: query, schema: { type: string }, description: Substring of IP, MAC or hostname. }
- { name: q, in: query, schema: { type: string }, description: "Substring of IP, MAC or hostname." }
- { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 1000 } }
- { name: after, in: query, schema: { type: string }, description: Return leases with a higher IP (cursor). }
responses:
+9 -1
View File
@@ -45,6 +45,13 @@ func TestSpecMatchesRoutes(t *testing.T) {
// TestEveryRouteIsProtected pins the list of routes reachable without a
// session and checks that permission routes name a permission.
func TestEveryRouteIsProtected(t *testing.T) {
// Locking agents out is a security function that read-only mode must
// not block (routes.go).
lockOutRoutes := []string{
"DELETE /api/v1/agents/{id}",
"POST /api/v1/agents/{id}/revoke",
"DELETE /api/v1/enrollment-tokens/{id}",
}
publicAllowed := []string{
"GET /api/v1/system/info",
"POST /api/v1/auth/login",
@@ -67,7 +74,8 @@ func TestEveryRouteIsProtected(t *testing.T) {
t.Errorf("%s: GET routes must not be write routes", key)
}
if r.Method != http.MethodGet && !r.Write && r.Access == permission && !strings.Contains(r.Path, "/license") &&
!strings.Contains(r.Path, "/saved-filters") && !strings.HasSuffix(r.Path, "/preview") {
!strings.Contains(r.Path, "/saved-filters") && !strings.HasSuffix(r.Path, "/preview") &&
!slices.Contains(lockOutRoutes, key) {
t.Errorf("%s changes state but is not marked Write (read-only mode would not block it)", key)
}
}
+2 -1
View File
@@ -109,7 +109,8 @@ func TestSignAgentCSR(t *testing.T) {
if len(cert.DNSNames) != 0 || len(cert.ExtKeyUsage) != 1 || cert.ExtKeyUsage[0] != x509.ExtKeyUsageClientAuth || cert.IsCA {
t.Errorf("%s: unexpected certificate contents", name)
}
if cert.NotAfter.Sub(clock) != pki.AgentCertLifetime {
// Certificates carry whole seconds.
if d := cert.NotAfter.Sub(clock); d > pki.AgentCertLifetime || d <= pki.AgentCertLifetime-time.Second {
t.Errorf("%s: lifetime %v", name, cert.NotAfter.Sub(clock))
}
if cert.SerialNumber.BitLen() > 128 || seen[pki.SerialHex(cert.SerialNumber)] {
+9
View File
@@ -0,0 +1,9 @@
# SPDX-License-Identifier: Apache-2.0
# BIND with the DNS agent for the end-to-end test. The layout matches a
# managed host: the agent owns /etc/bind/ddi-suite and the zone directory.
FROM internetsystemsconsortium/bind9:9.20
COPY --chmod=0755 bin/ddi-agent-dns /usr/local/bin/ddi-agent-dns
COPY --chmod=0644 bind/named.conf /etc/bind/named.conf
COPY --chmod=0644 bind/agent-dns.yaml /etc/ddi-suite/agent-dns.yaml
COPY --chmod=0755 bind/entrypoint.sh /e2e/entrypoint.sh
ENTRYPOINT ["/e2e/entrypoint.sh"]
+10
View File
@@ -0,0 +1,10 @@
# SPDX-License-Identifier: Apache-2.0
# DNS agent configuration for the end-to-end test.
log:
level: debug
dns:
conf_dir: /etc/bind/ddi-suite
zone_dir: /var/lib/bind/ddi-suite
named_conf: /etc/bind/named.conf
rndc_args: ["-k", "/etc/bind/rndc.key", "-s", "127.0.0.1", "-p", "953"]
dns_address: 127.0.0.1:53
+21
View File
@@ -0,0 +1,21 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# Prepares the host like the installer would and runs named in the
# foreground. The agent is started later by run.sh (after enrollment).
set -eu
conf=/etc/bind/ddi-suite
zones=/var/lib/bind/ddi-suite
mkdir -p "$conf" "$zones" /run/named /var/cache/bind
if [ ! -f /etc/bind/rndc.key ]; then
rndc-confgen -a -A hmac-sha256 -k rndc-key -c /etc/bind/rndc.key
fi
# Empty includes until the agent writes the first revision.
for f in ddi-suite.conf ddi-suite-options.conf; do
[ -f "$conf/$f" ] || : >"$conf/$f"
done
chown -R root:bind "$conf" /etc/bind/rndc.key
chmod 750 "$conf"
chmod 640 "$conf"/*.conf /etc/bind/rndc.key
chown bind:bind "$zones" /run/named /var/cache/bind
chmod 750 "$zones"
exec named -u bind -g -c /etc/bind/named.conf
+22
View File
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: Apache-2.0
//
// Distribution-like named.conf for the end-to-end test: it only adds the
// rndc channel and the two include files written by the DNS agent. Options
// the generator sets (recursion, forwarders, transfers) must not appear here.
include "/etc/bind/rndc.key";
controls {
inet 127.0.0.1 port 953 allow { 127.0.0.1; } keys { "rndc-key"; };
};
options {
directory "/var/cache/bind";
pid-file "/run/named/named.pid";
listen-on { 127.0.0.1; };
listen-on-v6 { none; };
dnssec-validation no;
include "/etc/bind/ddi-suite/ddi-suite-options.conf";
};
include "/etc/bind/ddi-suite/ddi-suite.conf";
+77
View File
@@ -0,0 +1,77 @@
# SPDX-License-Identifier: Apache-2.0
#
# End-to-end environment (M3): management server, PostgreSQL, BIND with the
# DNS agent and Kea with the DHCP agent, all in containers. Driven by
# test/e2e/run.sh through `docker compose exec`, so it works with a remote or
# shared Docker daemon: no bind mounts, no published ports, no fixed subnet.
#
# TEST ONLY. Passwords and keys are throwaway values.
name: ddi-e2e
services:
postgres:
image: postgres:17.11-alpine
environment:
POSTGRES_USER: ddi
POSTGRES_DB: ddi
POSTGRES_PASSWORD: e2e-only
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ddi -d ddi"]
interval: 2s
timeout: 3s
retries: 30
server:
build:
context: .
dockerfile: server.Dockerfile
environment:
DDI_DATABASE_URL: postgres://ddi:e2e-only@postgres:5432/ddi?sslmode=disable
DDI_HTTP_LISTEN: 127.0.0.1:8080
DDI_MASTER_KEY_FILE: /var/lib/ddi/master.key
DDI_STATE_DIR: /var/lib/ddi/state
DDI_AGENT_API_LISTEN: 0.0.0.0:8444
DDI_AGENT_API_PUBLIC_ADDRESS: server:8444
DDI_AGENT_API_SERVER_NAMES: server
DDI_LOG_LEVEL: debug
# Unreachable license server: the server runs in trial mode.
KEYGEN_URL: http://127.0.0.1:1
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/readyz | grep -q '\"database\":\"ok\"'"]
interval: 2s
timeout: 3s
retries: 60
bind:
build:
context: .
dockerfile: bind.Dockerfile
user: "0:0"
depends_on:
server:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "rndc -k /etc/bind/rndc.key -s 127.0.0.1 -p 953 status >/dev/null"]
interval: 2s
timeout: 5s
retries: 30
kea:
build:
context: .
dockerfile: kea.Dockerfile
user: "0:0"
environment:
KEA_CONTROL_SOCKET_DIR: /run/kea
depends_on:
server:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "test -S /run/kea/kea4-ctrl-socket"]
interval: 2s
timeout: 3s
retries: 30
+8
View File
@@ -0,0 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# Kea DHCPv4 with the DHCP agent for the end-to-end test.
FROM docker.cloudsmith.io/isc/docker/kea-dhcp4:3.2.1
COPY --chmod=0755 bin/ddi-agent-dhcp /usr/local/bin/ddi-agent-dhcp
COPY --chmod=0644 kea/kea-dhcp4.conf /etc/kea/kea-dhcp4.conf
COPY --chmod=0644 kea/agent-dhcp.yaml /etc/ddi-suite/agent-dhcp.yaml
COPY --chmod=0755 kea/entrypoint.sh /e2e/entrypoint.sh
ENTRYPOINT ["/e2e/entrypoint.sh"]
+8
View File
@@ -0,0 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# DHCP agent configuration for the end-to-end test.
log:
level: debug
dhcp:
config_file: /etc/kea/kea-dhcp4.conf
control_socket: /run/kea/kea4-ctrl-socket
lease_interval: 2s
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# Kea 3.x accepts the control socket only in KEA_CONTROL_SOCKET_DIR, which
# must not be accessible by others. The agent is started later by run.sh.
set -eu
mkdir -p "$KEA_CONTROL_SOCKET_DIR" /var/lib/kea
chmod 750 "$KEA_CONTROL_SOCKET_DIR"
exec kea-dhcp4 -c /etc/kea/kea-dhcp4.conf
+20
View File
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: Apache-2.0
//
// Initial Kea configuration of the end-to-end test: no subnets, control
// socket and lease_cmds hook as generated later by the server.
{
"Dhcp4": {
"interfaces-config": { "interfaces": [] },
"control-socket": {
"socket-type": "unix",
"socket-name": "/run/kea/kea4-ctrl-socket"
},
"lease-database": {
"type": "memfile",
"persist": true,
"name": "/var/lib/kea/kea-leases4.csv"
},
"hooks-libraries": [{ "library": "libdhcp_lease_cmds.so" }],
"subnet4": []
}
}
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0
#
# End-to-end test of the M3 path: API change -> revision -> agent fetches,
# verifies and applies -> BIND/Kea serve it. Needs Docker with compose and
# the binaries from `make build` (BIN_DIR, default bin/).
#
# Usage: test/e2e/run.sh (make test-e2e)
# Environment: BIN_DIR, KEEP=1 keeps the containers after the run.
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
root="$(cd "$here/../.." && pwd)"
bin_dir="${BIN_DIR:-$root/bin}"
case "$bin_dir" in /*) ;; *) bin_dir="$root/$bin_dir" ;; esac
dc() { docker compose -f "$here/compose.yaml" "$@"; }
api() { dc exec -T server /e2e/api.sh "$@"; }
step() { printf '\n=== %s\n' "$*"; }
cleanup() {
local rc=$?
if [[ $rc -ne 0 ]]; then
echo "FAILED (exit $rc); logs follow" >&2
dc logs --no-color --tail 300 >&2 || true
for svc in bind kea; do
echo "--- agent log ($svc)" >&2
dc exec -T "$svc" sh -c 'cat /tmp/agent.log 2>/dev/null' >&2 || true
done
fi
if [[ "${KEEP:-}" != 1 ]]; then
dc down -v --remove-orphans >/dev/null 2>&1 || true
fi
rm -rf "${here:?}/bin"
}
trap cleanup EXIT
step "Build images"
mkdir -p "$here/bin"
for b in ddi-server ddi-agent-dns ddi-agent-dhcp; do
cp "$bin_dir/$b" "$here/bin/$b"
done
dc build --pull
dc up -d --wait
step "Set up data through the API"
password="$(dc exec -T server ddi-server bootstrap-admin | sed -n 's/^password: //p')"
[[ -n "$password" ]] || { echo "bootstrap-admin printed no password" >&2; exit 1; }
api setup "$password"
step "Enroll the agents"
dns_cmd="$(api token dns)"
dhcp_cmd="$(api token dhcp)"
[[ "$dns_cmd" == *'--ca-hash sha256:'* ]] || { echo "unexpected command: $dns_cmd" >&2; exit 1; }
# The commands come from our own API and are run as argument lists.
read -r -a dns_args <<<"$dns_cmd"
read -r -a dhcp_args <<<"$dhcp_cmd"
dc exec -T bind "${dns_args[@]}"
dc exec -T kea "${dhcp_args[@]}"
dc exec -T -d bind sh -c 'exec ddi-agent-dns run >/tmp/agent.log 2>&1'
dc exec -T -d kea sh -c 'exec ddi-agent-dhcp run >/tmp/agent.log 2>&1'
step "Revision 1 reaches both agents"
rev="$(api revision "initial data")"
[[ "$rev" == 1 ]] || { echo "first revision is $rev" >&2; exit 1; }
api wait-applied 1 2
dig_short() { dc exec -T bind dig @127.0.0.1 "$@" +short +time=2 +tries=3; }
expect() {
local want="$1"
shift
local got
got="$("$@")"
if [[ "$got" != "$want" ]]; then
echo "expected '$want', got '$got' from: $*" >&2
exit 1
fi
echo "ok $* -> $got"
}
expect "192.0.2.10" dig_short www.e2e.test A
expect "www.e2e.test." dig_short -x 192.0.2.10
expect "1" sh -c "docker compose -f '$here/compose.yaml' exec -T bind dig @127.0.0.1 e2e.test SOA +short | awk '{print \$3}'"
dc exec -T kea grep -q '"hw-address": "00:11:22:33:44:55"' /etc/kea/kea-dhcp4.conf
echo "ok Kea configuration contains the reservation"
step "Changing an address deploys revision 2"
api address 192.0.2.20 mail
rev="$(api revision "mail server")"
[[ "$rev" == 2 ]] || { echo "second revision is $rev" >&2; exit 1; }
api wait-applied 2 2
expect "192.0.2.20" dig_short mail.e2e.test A
expect "mail.e2e.test." dig_short -x 192.0.2.20
step "Unchanged data creates no revision"
rev="$(api revision "nothing changed")"
[[ "$rev" == 2 ]] || { echo "unchanged data produced revision $rev" >&2; exit 1; }
step "Deployments were reported"
api deployments | grep -c '"outcome": "succeeded"' | {
read -r n
[[ "$n" -ge 4 ]] || { echo "only $n successful deployments" >&2; exit 1; }
echo "ok $n successful deployments"
}
step "Revoking an agent disconnects it; BIND keeps serving"
api revoke dns
api wait-disconnected dns
expect "192.0.2.20" dig_short mail.e2e.test A
step "PASSED"
+9
View File
@@ -0,0 +1,9 @@
# SPDX-License-Identifier: Apache-2.0
# Management server for the end-to-end test. bin/ is filled by run.sh with
# the binaries built by `make build` (static, CGO disabled).
FROM alpine:3.22
RUN apk add --no-cache curl jq
COPY --chmod=0755 bin/ddi-server /usr/local/bin/ddi-server
COPY --chmod=0755 server/entrypoint.sh /e2e/entrypoint.sh
COPY --chmod=0755 server/api.sh /e2e/api.sh
ENTRYPOINT ["/e2e/entrypoint.sh"]
+118
View File
@@ -0,0 +1,118 @@
#!/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
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"; }
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
;;
*)
echo "unknown step $1" >&2
exit 2
;;
esac
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# Creates the master key on first start and runs the server.
set -eu
mkdir -p "$(dirname "$DDI_MASTER_KEY_FILE")" "$DDI_STATE_DIR"
if [ ! -f "$DDI_MASTER_KEY_FILE" ]; then
ddi-server gen-master-key >/dev/null
fi
exec ddi-server serve
Generated Vendored
+1 -1
View File
@@ -2209,7 +2209,7 @@ export interface operations {
query?: {
network_id?: string;
state?: components["schemas"]["LeaseState"];
/** @description Substring of IP */
/** @description Substring of IP, MAC or hostname. */
q?: string;
limit?: number;
/** @description Return leases with a higher IP (cursor). */