-
Notifications
You must be signed in to change notification settings - Fork 0
550 lines (526 loc) · 32.8 KB
/
Copy pathdeep-test.yml
File metadata and controls
550 lines (526 loc) · 32.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
name: Deep test
# Manually-dispatched full-platform validation, beyond what the regular CI job
# covers: the OS service lifecycle (install -> auto-start -> probes -> stop /
# restart -> uninstall), data-dir permissions, netinfo/traceroute paths, and the
# Docker image + the canonical compose flow. Dispatch after platform-sensitive
# changes; artifacts carry assertion output and diagnostic log tails per leg.
on:
workflow_dispatch:
schedule:
# Saturdays 05:37 UTC - a slot of its own, clear of ci.yml's Monday vuln
# re-scan and the fleet probe's Thursday, so service-lifecycle drift is
# never tangled with either's noise. Dispatch-only coverage rotted quietly:
# the best platform validation in the pipeline only ran when someone
# remembered to ask for it.
- cron: '37 5 * * 6'
permissions:
contents: read
jobs:
windows:
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version-file: go.mod
- name: full test suite
shell: bash
run: go test ./... -count=1 2>&1 | tee full-test.log
- name: build
run: go build -o pingularity.exe .
- name: service lifecycle (install auto-starts, probes, stop, restart, uninstall)
shell: pwsh
run: |
# Failures are counted, then asserted at the end - never thrown.
# $ErrorActionPreference stays 'Continue' (overriding the 'stop' the
# runner prepends) so one dead probe cannot end the script before the
# log is complete, and Tee-Object is a cmdlet that always succeeds,
# so no native exit code ever reaches the step on its own. Unlike the
# launchd leg there is no subshell here, so a plain counter survives
# to the final assertion.
$ErrorActionPreference = 'Continue'
$script:fails = 0
function Note($msg) {
"ASSERT FAIL: $msg" | Tee-Object svc.log -Append
$script:fails += 1
}
# Judges the exit code of the native command that just ran; Tee-Object
# and Start-Sleep are cmdlets and do not touch $LASTEXITCODE, but call
# this straight after the pipeline it belongs to anyway.
function Must($what) { if ($LASTEXITCODE -ne 0) { Note "$what exited $LASTEXITCODE" } }
function Probe($what, $url) {
try { $r = Invoke-WebRequest -UseBasicParsing -TimeoutSec 5 $url
"PROBE OK $what -> $($r.StatusCode)" | Tee-Object svc.log -Append
if ($r.StatusCode -ne 200) { Note "$what returned $($r.StatusCode), want 200" } }
catch { "PROBE FAIL $what -> $($_.Exception.Message)" | Tee-Object svc.log -Append
Note "$what did not return 200" }
}
"=== install (must register AND start) ===" | Tee-Object svc.log -Append
.\pingularity.exe install 2>&1 | Tee-Object svc.log -Append
Must "install"
Start-Sleep 8
.\pingularity.exe status 2>&1 | Tee-Object svc.log -Append
Must "status"
foreach ($i in 1..15) { try { Invoke-WebRequest -UseBasicParsing -TimeoutSec 3 http://127.0.0.1:9000/metrics | Out-Null; break } catch { Start-Sleep 2 } }
Probe "metrics" http://127.0.0.1:9000/metrics
Probe "status" http://127.0.0.1:9000/api/status
Probe "dashboard" http://127.0.0.1:9000/
try { Invoke-WebRequest -UseBasicParsing -Method POST -ContentType 'application/json' -TimeoutSec 10 http://127.0.0.1:9000/api/netinfo | Out-Null } catch { Note "netinfo POST failed" }
Start-Sleep 20
try { (Invoke-WebRequest -UseBasicParsing -TimeoutSec 5 http://127.0.0.1:9000/api/netinfo).Content | Tee-Object svc.log -Append } catch { Note "netinfo GET failed" }
"=== stop / status / start / status ===" | Tee-Object svc.log -Append
.\pingularity.exe stop 2>&1 | Tee-Object svc.log -Append
Must "stop"
Start-Sleep 3
# `status` exits 0 whether running, stopped or not installed, so the
# port going quiet is the assertion here, not an exit code.
.\pingularity.exe status 2>&1 | Tee-Object svc.log -Append
try { Invoke-WebRequest -UseBasicParsing -TimeoutSec 3 http://127.0.0.1:9000/metrics | Out-Null
Note "the dashboard still answered after stop" } catch {}
.\pingularity.exe start 2>&1 | Tee-Object svc.log -Append
Must "start"
Start-Sleep 5
.\pingularity.exe status 2>&1 | Tee-Object svc.log -Append
Must "status after restart"
Probe "metrics-after-restart" http://127.0.0.1:9000/metrics
"=== data dir + DACL (want owner-only: no Users/Everyone entry) ===" | Tee-Object svc.log -Append
# The Windows analogue of the launchd leg's 700/600 check: osperm sets
# a protected DACL naming only the service account, SYSTEM and
# Administrators, so a broad principal on the data dir or the database
# means every local user can read the monitoring history.
$dirAcl = icacls C:\ProgramData\pingularity 2>&1
Must "icacls data dir"
$dirAcl | Tee-Object svc.log -Append
$dbAcl = icacls C:\ProgramData\pingularity\pingularity.db 2>&1
Must "icacls database"
$dbAcl | Tee-Object svc.log -Append
$broad = @($dirAcl) + @($dbAcl) | Select-String 'Everyone|BUILTIN\\Users|Authenticated Users'
if ($broad) { Note "the DACL grants a broad principal: $(($broad.Line.Trim()) -join '; ')" }
.\pingularity.exe reset-auth 2>&1 | Tee-Object svc.log -Append
Must "reset-auth"
.\pingularity.exe uninstall -y 2>&1 | Tee-Object svc.log -Append
Must "uninstall"
Start-Sleep 3
.\pingularity.exe status 2>&1 | Tee-Object svc.log -Append
if (Test-Path C:\ProgramData\pingularity\pingularity.db) {
"db survives uninstall: True" | Tee-Object svc.log -Append
} else {
Note "uninstall deleted the database - removing the service must not take the user's history with it"
}
if ($script:fails -ne 0) { "::error::$script:fails assertion(s) failed in the service lifecycle - see svc.log"; exit 1 }
- name: upload logs
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: deep-test-windows
path: |
full-test.log
svc.log
macos:
runs-on: macos-latest
timeout-minutes: 30
# `shell: bash` is what selects `bash -eo pipefail`; without it GitHub runs
# `bash -e`, where `cmd | tee log` reports tee's exit status and a failing
# command inside the pipeline passes silently. Every block below pipes to tee.
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version-file: go.mod
- name: full test suite
run: go test ./... -count=1 2>&1 | tee full-test.log
- name: build
run: go build -o pingularity .
- name: service lifecycle (launchd; install auto-starts, probes, stop, restart, uninstall)
run: |
# Failures are counted in a FILE. The block below is the left side of a
# pipe, so it runs in a subshell and a shell variable would not survive
# back out to the step's exit status - the step would pass while the log
# it just wrote was full of failures.
FAILS=$(mktemp)
note() { echo "ASSERT FAIL: $*"; echo 1 >> "$FAILS"; }
probe() {
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$2" || echo FAIL)
echo "PROBE $1 -> $code"
[ "$code" = "200" ] || note "$1 returned $code, want 200"
}
DIR="/Library/Application Support/pingularity"
{
echo "=== install (must register AND start) ==="
sudo ./pingularity install
sleep 8
sudo ./pingularity status
for i in $(seq 1 15); do curl -fsS --max-time 3 http://127.0.0.1:9000/metrics >/dev/null 2>&1 && break; sleep 2; done
probe metrics http://127.0.0.1:9000/metrics
probe status http://127.0.0.1:9000/api/status
probe dashboard http://127.0.0.1:9000/
echo "=== netinfo refresh (macOS ICMP-socket traceroute path) ==="
curl -fsS -X POST -H 'Content-Type: application/json' --max-time 10 http://127.0.0.1:9000/api/netinfo >/dev/null 2>&1 \
|| note "netinfo POST failed"
sleep 20
curl -fsS --max-time 5 http://127.0.0.1:9000/api/netinfo || note "netinfo GET failed"
echo
echo "=== stop / status / start / status ==="
sudo ./pingularity stop; sleep 3
# `status` reports NOT running here, so its exit code is not the
# assertion - the port going quiet is.
sudo ./pingularity status || true
curl -fsS --max-time 3 http://127.0.0.1:9000/metrics >/dev/null 2>&1 \
&& note "the dashboard still answered after stop"
sudo ./pingularity start; sleep 5
sudo ./pingularity status
probe metrics-after-restart http://127.0.0.1:9000/metrics
echo "=== data dir perms (root daemon path, want 700/600) ==="
sudo ls -la "$DIR" || true
dperm=$(sudo stat -f '%Sp' "$DIR")
fperm=$(sudo stat -f '%Sp' "$DIR/pingularity.db")
echo "perms: dir=$dperm db=$fperm"
[ "$dperm" = "drwx------" ] || note "data dir is $dperm, want drwx------"
[ "$fperm" = "-rw-------" ] || note "database is $fperm, want -rw-------"
echo "=== reset-auth ==="
sudo ./pingularity reset-auth
echo "=== uninstall -y ==="
sudo ./pingularity uninstall -y
sleep 3
sudo ./pingularity status || true # not installed: non-zero is correct
if sudo test -f "$DIR/pingularity.db"; then
echo "db survives uninstall: True"
else
note "uninstall deleted the database - removing the service must not take the user's history with it"
fi
} 2>&1 | tee svc.log
n=$(wc -l < "$FAILS" | tr -d ' ')
[ "$n" -eq 0 ] || { echo "::error::$n assertion(s) failed in the launchd lifecycle - see svc.log"; exit 1; }
- name: upload logs
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: deep-test-macos
path: |
full-test.log
svc.log
docker-linux:
runs-on: ubuntu-latest
timeout-minutes: 20
# `shell: bash` is what selects `bash -eo pipefail`; without it GitHub runs
# `bash -e`, where `cmd | tee log` reports tee's exit status and a failing
# command inside the pipeline passes silently. Every block below pipes to tee.
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version-file: go.mod
- name: build static binary + image (same shape goreleaser ships)
run: |
# dockers_v2 stages the binary at linux/<arch>/pingularity and the
# Dockerfile COPYs $TARGETPLATFORM/pingularity, so mirror that layout here
# (BuildKit sets TARGETPLATFORM=linux/amd64 on this amd64 runner).
mkdir -p linux/amd64
CGO_ENABLED=0 GOARCH=amd64 go build -o linux/amd64/pingularity .
# `deep-test` is a purely local tag (never pushed): a version number
# here went stale the release after it was written and made the logs
# claim a version this build is not.
docker build -t ghcr.io/pingular/pingularity:deep-test .
- name: arm64 image actually runs (the release is multi-arch; only amd64 was ever executed)
run: |
docker run --privileged --rm tonistiigi/binfmt --install arm64
# Stage the arm64 binary where the Dockerfile's COPY $TARGETPLATFORM/... expects it.
mkdir -p linux/arm64
CGO_ENABLED=0 GOARCH=arm64 go build -o linux/arm64/pingularity .
docker buildx build --platform linux/arm64 -t ping-arm64 --load .
# No host networking here: under QEMU we only care that the arm64
# binary starts, opens its port and serves - the measurements it takes
# inside an emulated container are meaningless either way.
docker run -d --name ping-a64 -e PINGULARITY_ACCESS=network -p 9111:9000 ping-arm64
ok=""
for i in $(seq 1 30); do
curl -fsS --max-time 3 http://127.0.0.1:9111/metrics >/dev/null 2>&1 && { ok=1; break; }
sleep 2
done
echo "arm64 metrics -> $(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:9111/metrics)"
echo "arm64 status -> $(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:9111/api/status)"
echo "--- arm64 container logs ---"; docker logs ping-a64 2>&1 | tail -15
docker rm -f ping-a64
[ -n "$ok" ] || { echo "arm64 image never became ready"; exit 1; }
- name: image ships /var/lib/pingularity 0700 65532:65532 (exported image, BEFORE any run)
run: |
# The old form of this assertion read the volume AFTER the daemon ran,
# and the daemon tightens its own container data dir at boot (store.go)
# - so a 0755-shipping image passed anyway. Vacuous. docker create +
# export reads what the layers say before any process ever runs.
docker create --name perm-probe ghcr.io/pingular/pingularity:deep-test
docker export perm-probe -o image-rootfs.tar
docker rm perm-probe
python3 - image-rootfs.tar <<'EOF' 2>&1 | tee -a docker.log
import sys, tarfile
entries = {}
with tarfile.open(sys.argv[1]) as tf:
for e in tf:
entries[e.name.lstrip("./").rstrip("/")] = e
failed = False
d = entries.get("var/lib/pingularity")
if d is None or not d.isdir():
print("/var/lib/pingularity missing from the exported image"); failed = True
else:
if d.mode & 0o7777 != 0o700:
print(f"/var/lib/pingularity mode {oct(d.mode & 0o7777)}, want 0700"); failed = True
if (d.uid, d.gid) != (65532, 65532):
print(f"/var/lib/pingularity owner {d.uid}:{d.gid}, want 65532:65532"); failed = True
if "var/lib/pingularity/.pingularity-image-dir" not in entries:
print("volume-lineage marker .pingularity-image-dir missing"); failed = True
if not failed:
print("exported image: /var/lib/pingularity 0700 65532:65532 with marker - as shipped")
sys.exit(1 if failed else 0)
EOF
- name: host-mode docker run (the README command)
run: |
# See the launchd step for why failures are counted in a file.
FAILS=$(mktemp)
note() { echo "ASSERT FAIL: $*"; echo 1 >> "$FAILS"; }
code() { curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$1" || echo FAIL; }
{
docker run -d --name ping-host --network=host --cap-add=NET_RAW \
-v pingularity-data:/var/lib/pingularity ghcr.io/pingular/pingularity:deep-test
for i in $(seq 1 15); do curl -fsS --max-time 3 http://127.0.0.1:9000/metrics >/dev/null 2>&1 && break; sleep 2; done
m=$(code http://127.0.0.1:9000/metrics); echo "metrics -> $m"
st=$(code http://127.0.0.1:9000/api/status); echo "status -> $st"
[ "$m" = "200" ] || note "host-mode /metrics returned $m, want 200"
[ "$st" = "200" ] || note "host-mode /api/status returned $st, want 200"
# The SHIPPED dir mode is proven on the exported image in the step
# above - measuring it here, post-run, would be vacuous because the
# daemon tightens its own dir at boot. What IS runtime behaviour is
# the database the daemon just created: it must be 0600.
fperm=$(docker run --rm -v pingularity-data:/d alpine stat -c '%a' /d/pingularity.db)
echo "volume perms: db=$fperm"
[ "$fperm" = "600" ] || note "container database is $fperm, want 600"
docker logs ping-host 2>&1 | grep -q "group/world-accessible" \
&& note "the daemon warned about its own data directory on startup"
echo "--- container logs ---"; docker logs ping-host 2>&1 | tail -20
docker rm -f ping-host
} 2>&1 | tee -a docker.log
n=$(wc -l < "$FAILS" | tr -d ' ')
[ "$n" -eq 0 ] || { echo "::error::$n assertion(s) failed in the host-mode run - see docker.log"; exit 1; }
- name: compose one-shot flow (mirrors install.pingularity.dev/compose.yaml)
run: |
# See the launchd step for why failures are counted in a file.
FAILS=$(mktemp)
note() { echo "ASSERT FAIL: $*"; echo 1 >> "$FAILS"; }
{
# This block mirrors the canonical hosted compose (served by the dl
# Worker at install.pingularity.dev/compose.yaml; template lives in the
# pingularity.dev repo, dl/worker.js). Keep shapes in sync.
mkdir -p compose-test
cd compose-test
# Leave nothing running if any assertion path dies mid-flow (local
# reruns would otherwise find port 9000 taken); explicit downs still
# run between phases.
trap 'docker compose down --remove-orphans >/dev/null 2>&1 || true' EXIT
cat > compose.yaml <<'EOF'
services:
pingularity:
image: ghcr.io/pingular/pingularity:deep-test
container_name: pingularity
network_mode: host
environment:
- PINGULARITY_ACCESS=network
cap_add:
- NET_RAW
volumes:
- pingularity-data:/var/lib/pingularity
restart: unless-stopped
volumes:
pingularity-data:
name: pingularity-data
EOF
docker compose up -d
for i in $(seq 1 15); do curl -fsS --max-time 3 http://127.0.0.1:9000/metrics >/dev/null 2>&1 && break; sleep 2; done
cm=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:9000/metrics || echo FAIL)
echo "compose host-mode metrics -> $cm"
[ "$cm" = "200" ] || note "compose host-mode /metrics returned $cm, want 200"
echo "--- volume carries the docker-run data (interchange check) ---"
docker run --rm -v pingularity-data:/d alpine ls -la /d
docker compose down
echo "--- Docker Desktop knob variant (bridge + published port) ---"
sed -i 's/^ network_mode: host$/ ports: ["9000:9000"]/' compose.yaml
docker compose up -d
for i in $(seq 1 15); do curl -fsS --max-time 3 http://127.0.0.1:9000/metrics >/dev/null 2>&1 && break; sleep 2; done
bm=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:9000/metrics || echo FAIL)
echo "compose bridge-mode metrics -> $bm"
[ "$bm" = "200" ] || note "compose bridge-mode /metrics returned $bm, want 200"
docker compose down
echo "--- reverse-proxy shape: command: flags reach the Host guard (issue #16) ---"
# PINGULARITY_OPTS is the native unit's convention; a container must get
# flags via command:. Assert the chain on the DEFAULT image - compose
# command: syntax, its exec-form entrypoint, the flag parser, and the
# DNS-rebinding guard - by admitting one public Host and rejecting
# another. The iperf variant gets the same pair in its own step below.
sed -i 's/^ ports: \["9000:9000"\]$/ ports: ["9000:9000"]\n command: ["-allow-host=ping.example.com"]/' compose.yaml
docker compose up -d
for i in $(seq 1 15); do curl -fsS --max-time 3 http://127.0.0.1:9000/metrics >/dev/null 2>&1 && break; sleep 2; done
ah=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 -H 'Host: ping.example.com' http://127.0.0.1:9000/ || echo FAIL)
oh=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 -H 'Host: other.example.com' http://127.0.0.1:9000/ || echo FAIL)
echo "allow-host admitted -> $ah, other public Host -> $oh"
[ "$ah" = "200" ] || note "command:-supplied -allow-host did not admit its Host (got $ah, want 200)"
[ "$oh" = "403" ] || note "unlisted public Host was not rejected (got $oh, want 403)"
args=$(docker inspect pingularity --format '{{json .Args}}')
echo "argv: $args"
case "$args" in *"-allow-host=ping.example.com"*) ;; *) note "compose command: flag missing from container argv";; esac
# The variable itself must stay inert and WARNED about in a container.
docker compose down
# The sentinel is a value shape /etc/default/pingularity really carries
# (-metrics-token) and must never surface in any log; the allow-host
# part is what the inertness 403 is asserted against. Do NOT sentinel
# on the domain - the Host guard legitimately logs rejected Hosts.
sed -i '/^ command: \["-allow-host=ping.example.com"\]$/d' compose.yaml
sed -i 's/^ - PINGULARITY_ACCESS=network$/ - PINGULARITY_ACCESS=network\n - PINGULARITY_OPTS=-allow-host=ping.example.com -metrics-token=DEEP_SECRET_SENTINEL/' compose.yaml
docker compose up -d
for i in $(seq 1 15); do curl -fsS --max-time 3 http://127.0.0.1:9000/metrics >/dev/null 2>&1 && break; sleep 2; done
eh=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 -H 'Host: ping.example.com' http://127.0.0.1:9000/ || echo FAIL)
echo "env-var variant Host -> $eh (must stay 403: nothing expands PINGULARITY_OPTS in a container)"
[ "$eh" = "403" ] || note "PINGULARITY_OPTS env var unexpectedly took effect (got $eh, want 403)"
# Capture logs first: a bare `docker logs | grep -q` under pipefail can
# turn a successful early match into SIGPIPE pipeline failure.
clogs=$(docker logs pingularity 2>&1)
grep -Fq 'WARNING: PINGULARITY_OPTS' <<<"$clogs" || note "ignored-PINGULARITY_OPTS stderr warning missing from container logs"
# The stderr grep above cannot see the structured replay (different
# text, different sink) - assert the About-ring copy via /api/logs,
# or the whole replay could be deleted with everything still green.
# Fail CLOSED: a partial body could carry the replay phrase yet
# truncate before leaked data, so a failed fetch is its own failure.
if api_logs=$(curl -fsS --max-time 5 http://127.0.0.1:9000/api/logs); then
grep -Fq 'official container images do not expand it' <<<"$api_logs" \
|| note "structured ignored-PINGULARITY_OPTS warning missing from /api/logs (About ring)"
grep -Fq 'DEEP_SECRET_SENTINEL' <<<"$api_logs" && note "the PINGULARITY_OPTS value leaked into /api/logs"
else
note "fetching /api/logs failed"
fi
grep -Fq 'DEEP_SECRET_SENTINEL' <<<"$clogs" && note "the PINGULARITY_OPTS value leaked into container logs"
# Keep diagnostics in docker.log without ever printing a leaked value.
echo "--- env-leg container logs (sentinel-filtered tail) ---"
{ grep -Fv 'DEEP_SECRET_SENTINEL' <<<"$clogs" || true; } | tail -15
docker compose down
} 2>&1 | tee -a docker.log
n=$(wc -l < "$FAILS" | tr -d ' ')
[ "$n" -eq 0 ] || { echo "::error::$n assertion(s) failed in the compose flow - see docker.log"; exit 1; }
- name: iperf3 image variant (nothing but .goreleaser.yaml ever referenced it)
run: |
# See the launchd step for why failures are counted in a file.
FAILS=$(mktemp)
note() { echo "ASSERT FAIL: $*"; echo 1 >> "$FAILS"; }
{
# Dockerfile.iperf ships in every release and no test ever built it, so a
# break in it - the apt layer, the hand-recreated 65532 account debian has
# no distroless `nonroot` for, the /data COPY that gives the volume its
# mode - would first surface in a user's release. The amd64 binary is
# already staged at linux/amd64/pingularity by the build step above.
trap 'docker rm -fv ping-iperf ping-iperf-cmd ping-iperf-env ping-iperf-run iperf-srv >/dev/null 2>&1 || true; docker network rm iperf-net >/dev/null 2>&1 || true' EXIT
docker build -f Dockerfile.iperf -t ping-iperf .
uid=$(docker run --rm --entrypoint id ping-iperf -u)
perm=$(docker run --rm --entrypoint stat ping-iperf -c '%a' /var/lib/pingularity)
echo "iperf image: uid=$uid data-dir=$perm"
[ "$uid" = "65532" ] || note "iperf image runs as uid $uid, want 65532 (what the default image uses)"
[ "$perm" = "700" ] || note "iperf image data dir is $perm, want 700"
docker run --rm --entrypoint iperf3 ping-iperf --version \
|| note "iperf3 does not run in the image that exists to carry it"
docker run -d --name ping-iperf -e PINGULARITY_ACCESS=network -p 9112:9000 ping-iperf
for i in $(seq 1 15); do curl -fsS --max-time 3 http://127.0.0.1:9112/metrics >/dev/null 2>&1 && break; sleep 2; done
im=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:9112/metrics || echo FAIL)
echo "iperf image metrics -> $im"
[ "$im" = "200" ] || note "iperf image /metrics returned $im, want 200"
ilogs=$(docker logs ping-iperf 2>&1)
grep -Fq "group/world-accessible" <<<"$ilogs" \
&& note "the iperf image warned about its own data directory on startup"
echo "--- iperf container logs ---"; tail -15 <<<"$ilogs"
docker rm -fv ping-iperf
echo "--- issue #16 parity: same Host-guard pair on the variant the report used ---"
docker run -d --name ping-iperf-cmd -e PINGULARITY_ACCESS=network -p 9113:9000 ping-iperf -allow-host=ping.example.com
for i in $(seq 1 15); do curl -fsS --max-time 3 http://127.0.0.1:9113/metrics >/dev/null 2>&1 && break; sleep 2; done
iah=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 -H 'Host: ping.example.com' http://127.0.0.1:9113/ || echo FAIL)
ioh=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 -H 'Host: other.example.com' http://127.0.0.1:9113/ || echo FAIL)
iargs=$(docker inspect ping-iperf-cmd --format '{{json .Args}}')
echo "iperf command-arg: allowed -> $iah, other -> $ioh, argv: $iargs"
[ "$iah" = "200" ] || note "iperf image: command-arg -allow-host did not admit its Host (got $iah, want 200)"
[ "$ioh" = "403" ] || note "iperf image: unlisted public Host not rejected (got $ioh, want 403)"
case "$iargs" in *"-allow-host=ping.example.com"*) ;; *) note "iperf image: run-arg flag missing from container argv";; esac
docker rm -fv ping-iperf-cmd
docker run -d --name ping-iperf-env -p 9113:9000 -e PINGULARITY_ACCESS=network -e "PINGULARITY_OPTS=-allow-host=ping.example.com -metrics-token=DEEP_SECRET_SENTINEL" ping-iperf
for i in $(seq 1 15); do curl -fsS --max-time 3 http://127.0.0.1:9113/metrics >/dev/null 2>&1 && break; sleep 2; done
ieh=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 -H 'Host: ping.example.com' http://127.0.0.1:9113/ || echo FAIL)
echo "iperf env-var: Host -> $ieh (must stay 403)"
[ "$ieh" = "403" ] || note "iperf image: PINGULARITY_OPTS env var unexpectedly took effect (got $ieh, want 403)"
elogs=$(docker logs ping-iperf-env 2>&1)
grep -Fq 'WARNING: PINGULARITY_OPTS' <<<"$elogs" || note "iperf image: ignored-PINGULARITY_OPTS stderr warning missing from container logs"
if iapi_logs=$(curl -fsS --max-time 5 http://127.0.0.1:9113/api/logs); then
grep -Fq 'official container images do not expand it' <<<"$iapi_logs" \
|| note "iperf image: structured warning missing from /api/logs (About ring)"
grep -Fq 'DEEP_SECRET_SENTINEL' <<<"$iapi_logs" && note "iperf image: the PINGULARITY_OPTS value leaked into /api/logs"
else
note "iperf image: fetching /api/logs failed"
fi
grep -Fq 'DEEP_SECRET_SENTINEL' <<<"$elogs" && note "iperf image: the PINGULARITY_OPTS value leaked into container logs"
echo "--- iperf env-leg container logs (sentinel-filtered tail) ---"
{ grep -Fv 'DEEP_SECRET_SENTINEL' <<<"$elogs" || true; } | tail -15
docker rm -fv ping-iperf-env
echo "--- real iperf3 speedtest: sidecar server, engine selected via the API ---"
# The engine needs a server, and the image itself carries iperf3, so
# the sidecar runs from the SAME image (--entrypoint iperf3 -s): no
# third-party image, and it doubles as proof the bundled binary works
# in server mode. A user-defined bridge network gives the daemon
# container DNS for the sidecar's name.
docker network create iperf-net
docker run -d --name iperf-srv --network iperf-net --entrypoint iperf3 ping-iperf -s
docker run -d --name ping-iperf-run --network iperf-net -p 9114:9000 -e PINGULARITY_ACCESS=network ping-iperf
for i in $(seq 1 15); do curl -fsS --max-time 3 http://127.0.0.1:9114/metrics >/dev/null 2>&1 && break; sleep 2; done
# quick_setup_done releases the first-run consent hold; the rest
# selects the engine and points it at the sidecar. The defaults
# already run both directions plus the UDP loss/jitter pass.
curl -fsS -X POST -H 'Content-Type: application/json' --max-time 10 \
-d '{"quick_setup_done":true,"speed_engine":"iperf3","iperf_server":"iperf-srv:5201"}' \
http://127.0.0.1:9114/api/settings >/dev/null || note "selecting the iperf3 engine via /api/settings failed"
# POST /api/speedtest runs the measurement synchronously and returns
# the stored sample (~5s per direction + the UDP pass; 180s is slack).
sample=$(curl -fsS -X POST -H 'Content-Type: application/json' --max-time 180 http://127.0.0.1:9114/api/speedtest || true)
echo "iperf3 sample: $sample"
printf '%s' "$sample" > iperf-sample.json
python3 - iperf-sample.json <<'EOF' || note "the iperf3 run did not produce a full sample (see lines above)"
import json, sys
try:
with open(sys.argv[1]) as f:
s = json.load(f)
except Exception as e:
print(f"sample is not JSON: {e}"); sys.exit(1)
bad = False
if s.get("engine") != "iperf3":
print(f"engine = {s.get('engine')!r}, want 'iperf3'"); bad = True
for k in ("down_mbps", "up_mbps"):
if not isinstance(s.get(k), (int, float)) or s.get(k) <= 0:
print(f"{k} = {s.get(k)!r}, want > 0"); bad = True
for k in ("jitter_ms", "packet_loss"):
if not isinstance(s.get(k), (int, float)):
print(f"{k} = {s.get(k)!r}, want a number (the UDP pass measures it)"); bad = True
if s.get("udp_direction") not in ("down", "up"):
print(f"udp_direction = {s.get('udp_direction')!r}, want down|up"); bad = True
sys.exit(1 if bad else 0)
EOF
# ...and the sample must have LANDED, not just been returned: the
# history API is what the dashboard and exports read.
hist=$(curl -fsS --max-time 5 'http://127.0.0.1:9114/api/speed?mins=60' || echo '[]')
grep -Fq '"engine":"iperf3"' <<<"$hist" || note "no iperf3 sample landed in /api/speed history"
docker rm -fv ping-iperf-run iperf-srv
docker network rm iperf-net
} 2>&1 | tee -a docker.log
n=$(wc -l < "$FAILS" | tr -d ' ')
[ "$n" -eq 0 ] || { echo "::error::$n assertion(s) failed in the iperf3 image - see docker.log"; exit 1; }
- name: upload logs
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: deep-test-docker
path: docker.log