ci: probe runtime through Docker exec
CI / validate (pull_request) Successful in 1m51s

This commit is contained in:
Dennis Juhler Aagaard
2026-09-24 20:15:51 +02:00
parent b34d9b80fa
commit 4c9f9a5f4c
2 changed files with 31 additions and 23 deletions
+13 -3
View File
@@ -151,6 +151,15 @@ def test_runtime_copies_options_without_a_host_bind_mount() -> None:
assert "--mount" not in runtime_test assert "--mount" not in runtime_test
def test_runtime_probes_service_inside_container_network_namespace() -> None:
runtime_test = (ROOT / "tests/test_runtime.sh").read_text(encoding="utf-8")
assert 'probe_root "$container"' in runtime_test
assert 'post_invalid_worker "$first_container"' in runtime_test
assert 'assert_8080_loopback_mapping "$first_container"' in runtime_test
assert 'assert_8080_loopback_mapping "$second_container"' in runtime_test
assert 'f"http://127.0.0.1:{sys.argv[1]}/"' not in runtime_test
def test_ci_builds_and_loads_only_the_amd64_test_image() -> None: def test_ci_builds_and_loads_only_the_amd64_test_image() -> None:
workflow = (ROOT / ".gitea/workflows/ci.yml").read_text(encoding="utf-8") workflow = (ROOT / ".gitea/workflows/ci.yml").read_text(encoding="utf-8")
parsed = yaml.safe_load(workflow) parsed = yaml.safe_load(workflow)
@@ -253,9 +262,10 @@ def test_runtime_requires_process_baseline_after_cdp_close_and_zero_stopped_pid(
baseline = ( baseline = (
f'{prefix}_baseline="$(capture_process_baseline "{container}")"' f'{prefix}_baseline="$(capture_process_baseline "{container}")"'
) )
ready_index = runtime_test.index( mapping_index = runtime_test.index(
f'wait_ready "{container}" "${prefix}_port"' f'assert_8080_loopback_mapping "{container}"'
) )
ready_index = runtime_test.index(f'wait_ready "{container}"')
baseline_index = runtime_test.index(baseline) baseline_index = runtime_test.index(baseline)
close_index = runtime_test.index( close_index = runtime_test.index(
f'probe_and_close_cdp "{container}"', baseline_index f'probe_and_close_cdp "{container}"', baseline_index
@@ -264,7 +274,7 @@ def test_runtime_requires_process_baseline_after_cdp_close_and_zero_stopped_pid(
f'assert_processes_return_to_baseline "{container}" "${prefix}_baseline"', f'assert_processes_return_to_baseline "{container}" "${prefix}_baseline"',
close_index, close_index,
) )
assert ready_index < baseline_index < close_index < return_index assert mapping_index < ready_index < baseline_index < close_index < return_index
assert "{{.State.Pid}}" in runtime_test assert "{{.State.Pid}}" in runtime_test
assert '[ "$state" = "exited 0 0" ]' in runtime_test assert '[ "$state" = "exited 0 0" ]' in runtime_test
+18 -20
View File
@@ -66,22 +66,23 @@ start_container() {
docker start "$container" >/dev/null docker start "$container" >/dev/null
} }
host_port() { assert_8080_loopback_mapping() {
local container="$1" local container="$1"
local mapping local mapping
mapping="$(docker port "$container" 8080/tcp)" mapping="$(docker port "$container" 8080/tcp)"
[ -n "$mapping" ] || fail "container 8080 has no host mapping" case "$mapping" in
printf '%s\n' "${mapping##*:}" 127.0.0.1:[0-9]*) ;;
*) fail "$container 8080 mapping is '$mapping', want 127.0.0.1:<port>" ;;
esac
} }
probe_root() { probe_root() {
local port="$1" local container="$1"
python3 - "$port" <<'PY' docker exec -i "$container" python3 - <<'PY'
import sys
import urllib.request import urllib.request
opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
with opener.open(f"http://127.0.0.1:{sys.argv[1]}/", timeout=2) as response: with opener.open("http://127.0.0.1:8080/", timeout=2) as response:
if response.status != 200: if response.status != 200:
raise SystemExit(f"root status {response.status}") raise SystemExit(f"root status {response.status}")
response.read() response.read()
@@ -90,7 +91,6 @@ PY
wait_ready() { wait_ready() {
local container="$1" local container="$1"
local port="$2"
local deadline=$((SECONDS + 90)) local deadline=$((SECONDS + 90))
while (( SECONDS < deadline )); do while (( SECONDS < deadline )); do
if [ "$(docker inspect --format '{{.State.Running}}' "$container")" != "true" ]; then if [ "$(docker inspect --format '{{.State.Running}}' "$container")" != "true" ]; then
@@ -98,7 +98,7 @@ wait_ready() {
fail "$container exited during readiness" fail "$container exited during readiness"
fi fi
if docker logs "$container" 2>&1 | grep -Fq "Stelloauth listening on 0.0.0.0:8080"; then if docker logs "$container" 2>&1 | grep -Fq "Stelloauth listening on 0.0.0.0:8080"; then
if probe_root "$port" >/dev/null 2>&1; then if probe_root "$container" >/dev/null 2>&1; then
return return
fi fi
fi fi
@@ -265,16 +265,14 @@ PY
} }
post_invalid_worker() { post_invalid_worker() {
local port="$1" local container="$1"
local response_artifact="$2" local response_artifact="$2"
python3 - "$port" "$response_artifact" <<'PY' docker exec -i "$container" python3 - > "$response_artifact" <<'PY'
import json import json
import pathlib
import sys import sys
import urllib.error import urllib.error
import urllib.request import urllib.request
port, artifact = sys.argv[1:]
body = { body = {
"url": ( "url": (
"https://example.invalid/am/oauth2/authorize" "https://example.invalid/am/oauth2/authorize"
@@ -292,7 +290,7 @@ body = {
"refresh_token": "SENTINEL_REFRESH_TOKEN_4e73", "refresh_token": "SENTINEL_REFRESH_TOKEN_4e73",
} }
request = urllib.request.Request( request = urllib.request.Request(
f"http://127.0.0.1:{port}/worker", "http://127.0.0.1:8080/worker",
data=json.dumps(body, separators=(",", ":")).encode(), data=json.dumps(body, separators=(",", ":")).encode(),
headers={"Content-Type": "application/json"}, headers={"Content-Type": "application/json"},
method="POST", method="POST",
@@ -307,7 +305,7 @@ except urllib.error.HTTPError as error:
response_body = error.read() response_body = error.read()
if status != 400: if status != 400:
raise SystemExit(f"invalid worker status {status}, want 400") raise SystemExit(f"invalid worker status {status}, want 400")
pathlib.Path(artifact).write_bytes(response_body) sys.stdout.buffer.write(response_body)
PY PY
} }
@@ -374,8 +372,8 @@ if [ "${SKIP_BUILD:-0}" != "1" ]; then
fi fi
start_container "$first_container" start_container "$first_container"
first_port="$(host_port "$first_container")" assert_8080_loopback_mapping "$first_container"
wait_ready "$first_container" "$first_port" wait_ready "$first_container"
first_baseline="$(capture_process_baseline "$first_container")" first_baseline="$(capture_process_baseline "$first_container")"
assert_no_9222_mapping "$first_container" assert_no_9222_mapping "$first_container"
assert_loopback_cdp_listener \ assert_loopback_cdp_listener \
@@ -384,14 +382,14 @@ assert_loopback_cdp_listener \
"${artifacts_dir}/runtime-proc-net-tcp6.txt" "${artifacts_dir}/runtime-proc-net-tcp6.txt"
probe_and_close_cdp "$first_container" probe_and_close_cdp "$first_container"
assert_processes_return_to_baseline "$first_container" "$first_baseline" assert_processes_return_to_baseline "$first_container" "$first_baseline"
post_invalid_worker "$first_port" "${artifacts_dir}/runtime-invalid-worker-response.json" post_invalid_worker "$first_container" "${artifacts_dir}/runtime-invalid-worker-response.json"
scan_logs "$first_container" scan_logs "$first_container"
stop_and_assert "$first_container" "${artifacts_dir}/runtime-first-stop.txt" stop_and_assert "$first_container" "${artifacts_dir}/runtime-first-stop.txt"
scan_logs "$first_container" scan_logs "$first_container"
start_container "$second_container" start_container "$second_container"
second_port="$(host_port "$second_container")" assert_8080_loopback_mapping "$second_container"
wait_ready "$second_container" "$second_port" wait_ready "$second_container"
second_baseline="$(capture_process_baseline "$second_container")" second_baseline="$(capture_process_baseline "$second_container")"
assert_no_9222_mapping "$second_container" assert_no_9222_mapping "$second_container"
assert_loopback_cdp_listener \ assert_loopback_cdp_listener \