Make the dashboard usable by keyboard and with a screen reader #313
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| # Weekly re-run so govulncheck re-scans the shipped code against newly disclosed | |
| # vulnerabilities even when nothing changed - the vuln DB moves, the code doesn't. | |
| schedule: | |
| - cron: '17 4 * * 1' # Mondays 04:17 UTC | |
| # Callable so the release workflow can gate a tag on this exact-SHA CI before it | |
| # publishes anything (see .github/workflows/release.yml). | |
| workflow_call: | |
| permissions: | |
| contents: read | |
| jobs: | |
| test: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 | |
| - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 | |
| with: | |
| go-version-file: go.mod | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version: '22' | |
| - name: gofmt | |
| run: | | |
| unformatted=$(gofmt -l .) | |
| if [ -n "$unformatted" ]; then | |
| echo "gofmt needs to run on:"; echo "$unformatted"; exit 1 | |
| fi | |
| - name: vet | |
| run: go vet ./... | |
| - name: dead code | |
| # With tests as entry points (-test) the legitimate test seams | |
| # (raceCities, bestResult, resultScore, stats.ResetForTest, ...) are | |
| # reachable and only truly-orphaned functions fail the gate. | |
| run: | | |
| out=$(go run golang.org/x/tools/cmd/deadcode@v0.48.0 -test ./...) | |
| if [ -n "$out" ]; then | |
| echo "dead code found:"; echo "$out"; exit 1 | |
| fi | |
| - name: build | |
| run: go build ./... | |
| - name: test | |
| # -race is the gate: three test files only observe the unsynchronized | |
| # access they were written to catch when the race detector is on. | |
| run: go test ./... -count=1 -race | |
| - name: test (file-backed store) | |
| # The ordinary run's ":memory:" stores pin the SQLite pool to ONE | |
| # connection, which makes every multi-connection WAL failure mode | |
| # structurally invisible - the snapshot-upgrade class busy_timeout | |
| # cannot absorb (SQLITE_BUSY_SNAPSHOT) shipped a measurement-loss bug | |
| # past this entire matrix exactly that way. This leg reruns the | |
| # database-heavy packages against real files and the production | |
| # four-connection pool (see PINGULARITY_TEST_DB_DIR in store.go). | |
| # speedtest is excluded deliberately: its wall clock is capture | |
| # windows, not database work, and its file-backed coverage rides its | |
| # own concurrency hammer test. | |
| env: | |
| PINGULARITY_TEST_DB_DIR: ${{ runner.temp }} | |
| run: go test ./internal/store/ ./internal/settings/ . -count=1 | |
| - name: ui test | |
| run: node --test internal/web/ui/*.test.mjs | |
| - name: promtool /metrics lint | |
| # Validate the live /metrics exposition against the real Prometheus parser, | |
| # so a malformed label/escape or type mismatch fails CI instead of a | |
| # scraper. Runs a real daemon briefly (probing on) to populate the dynamic | |
| # families (targets, latency histograms, stat counters) before scraping. | |
| run: | | |
| set -euo pipefail | |
| PROM_VER=2.53.3 | |
| curl -fsSL -o prom.tgz "https://github.com/prometheus/prometheus/releases/download/v${PROM_VER}/prometheus-${PROM_VER}.linux-amd64.tar.gz" | |
| tar xzf prom.tgz | |
| PROMTOOL="$PWD/prometheus-${PROM_VER}.linux-amd64/promtool" | |
| go build -o pingularity-ci . | |
| DBDIR=$(mktemp -d) | |
| ./pingularity-ci -listen 127.0.0.1:19099 -db "$DBDIR/ci.db" -speedtest=false -speedtest-on-reconnect=false & | |
| PID=$! | |
| trap 'kill $PID 2>/dev/null || true' EXIT | |
| sleep 6 # let a couple of probe rounds populate targets + histograms | |
| curl -fsS http://127.0.0.1:19099/metrics | "$PROMTOOL" check metrics | |
| curl -fsS -o /dev/null -w 'healthz=%{http_code}\n' http://127.0.0.1:19099/healthz | |
| - name: govulncheck | |
| # Pin the version for reproducibility: a floating @latest can change the | |
| # analyzer (and thus pass/fail) between two runs of the same commit. | |
| run: | | |
| go install golang.org/x/vuln/cmd/govulncheck@v1.6.0 | |
| "$(go env GOPATH)/bin/govulncheck" ./... | |
| # Cheap cross-compile floor: catches platform-specific build regressions. | |
| xbuild: | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| goos: [windows, darwin] | |
| goarch: [amd64, arm64] | |
| # Two BSD builds keep the stub files honest: freebsd exercises the | |
| # !linux/!darwin/!windows stubs (resolver_stub, trace_stub, bytes_other) | |
| # plus diskfree_unix's syscall use; openbsd exercises diskfree_other | |
| # (its Statfs_t has different field names, which once broke the build). | |
| include: | |
| - goos: freebsd | |
| goarch: amd64 | |
| - goos: openbsd | |
| goarch: amd64 | |
| env: | |
| GOOS: ${{ matrix.goos }} | |
| GOARCH: ${{ matrix.goarch }} | |
| CGO_ENABLED: 0 | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 | |
| - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 | |
| with: | |
| go-version-file: go.mod | |
| - name: build | |
| run: go build ./... | |
| - name: vet | |
| run: go vet ./... | |
| # Native run: macOS, Windows and arm64-Linux runners EXECUTE the OS-tagged tests (which xbuild | |
| # only cross-compiles - trace_darwin/windows, resolver, netstat, disk_free, the | |
| # Windows DACL), run the frontend tests, and smoke-boot the binary. The two | |
| # privileged paths - raw-socket traceroute and service install - are exercised | |
| # by the manually-dispatched deep-test workflow (.github/workflows/deep-test.yml), | |
| # not here. | |
| native: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # ubuntu-24.04-arm is the closest CI gets to the Raspberry Pi class | |
| # this daemon most often runs on: before it, linux/arm64 was BUILT here | |
| # but never EXECUTED anywhere in the pipeline. | |
| os: [macos-latest, windows-latest, ubuntu-24.04-arm] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 | |
| - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 | |
| with: | |
| go-version-file: go.mod | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version: '22' | |
| - name: test | |
| run: go test ./... -count=1 | |
| - name: ui test | |
| run: node --test internal/web/ui/*.test.mjs | |
| - name: smoke (boot + serve /metrics) | |
| shell: bash | |
| run: | | |
| exe=$(go env GOEXE) | |
| go build -o "pingularity_smoke$exe" . | |
| "./pingularity_smoke$exe" -listen 127.0.0.1:9000 -db smoke.db & | |
| pid=$! | |
| ok= | |
| for i in $(seq 1 30); do | |
| if curl -fsS http://127.0.0.1:9000/metrics >/dev/null 2>&1; then ok=1; break; fi | |
| sleep 1 | |
| done | |
| kill "$pid" 2>/dev/null || true | |
| [ -n "$ok" ] || { echo "binary did not serve /metrics within 30s"; exit 1; } | |
| echo "smoke OK: booted and served /metrics" | |
| # Validate the release config so a broken .goreleaser.yaml is caught on a PR | |
| # instead of at tag-push time (when it would abort a release mid-flight). Pinned | |
| # Browser smoke: ui.test.mjs proves the page's FUNCTIONS; nothing proved the | |
| # page PAINTS. A real Chromium loads the dashboard from a live daemon and | |
| # asserts the floor - panels and chart render, zero console errors, zero | |
| # failed requests. Coarse anchors only; fine-grained selectors rot into | |
| # flakes. release.yml gates on this via its `ci` job like everything here. | |
| browser-smoke: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| 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 | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version: '22' | |
| - name: install playwright chromium | |
| run: | | |
| npm install --no-save playwright@1.55.0 | |
| npx playwright install --with-deps chromium | |
| - name: boot the daemon and drive the browser | |
| run: | | |
| go build -o smoke_bin . | |
| ./smoke_bin run -listen 127.0.0.1:9109 -db "$RUNNER_TEMP/browser-smoke.db" & | |
| pid=$! | |
| ok= | |
| for i in $(seq 1 30); do | |
| if curl -fsS http://127.0.0.1:9109/healthz >/dev/null 2>&1; then ok=1; break; fi | |
| sleep 1 | |
| done | |
| [ -n "$ok" ] || { echo "daemon did not come up"; exit 1; } | |
| node internal/web/ui/browser_smoke.mjs 9109 | |
| rc=$? | |
| kill "$pid" 2>/dev/null || true | |
| exit $rc | |
| # to the exact goreleaser version release.yml publishes with, so what CI checks is | |
| # what the release runs. | |
| goreleaser-check: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 | |
| - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 | |
| with: | |
| go-version-file: go.mod | |
| - name: goreleaser check | |
| uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 | |
| with: | |
| version: 'v2.17.0' | |
| args: check | |
| # Artifact smoke: the one distribution class nothing else executes. The native | |
| # jobs test SOURCE builds and the docker job compiles its own binary - the | |
| # goreleaser artifacts users actually download (release ldflags, -s -w, the | |
| # stamped version) were never run before publish. This builds the snapshot | |
| # artifacts with the same pinned goreleaser the release uses, asserts the | |
| # version stamp landed, and boots the Linux binary until /healthz answers. | |
| # release.yml gates on this via its `ci` job, like every job in this file. | |
| artifact-smoke: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| 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 snapshot artifacts (binaries, archives, deb/rpm) | |
| uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 | |
| with: | |
| version: 'v2.17.0' | |
| # The docker pipeline has its own dedicated gate (the `docker` job); | |
| # everything publish-shaped is skipped, everything package-shaped is | |
| # built - the deb/rpm below are installed for real further down. | |
| args: release --snapshot --clean --skip=docker,publish,announce,homebrew,winget,sign,sbom | |
| - name: smoke the Linux artifact (version stamp + boot) | |
| run: | | |
| bin=$(echo dist/pingularity_linux_amd64*/pingularity) | |
| v=$("$bin" version) | |
| echo "artifact reports: $v" | |
| # An unstamped artifact reports the source default; the ldflags path | |
| # is exactly what source-built test binaries never exercise. | |
| case "$v" in | |
| *SNAPSHOT*) ;; | |
| *) echo "version stamp missing - ldflags did not apply"; exit 1;; | |
| esac | |
| "$bin" run -listen 127.0.0.1:9100 -db "$RUNNER_TEMP/artifact-smoke.db" & | |
| pid=$! | |
| ok= | |
| for i in $(seq 1 30); do | |
| if curl -fsS http://127.0.0.1:9100/healthz >/dev/null 2>&1; then ok=1; break; fi | |
| sleep 1 | |
| done | |
| kill "$pid" 2>/dev/null || true | |
| [ -n "$ok" ] || { echo "the shipped-shape binary did not serve /healthz within 30s"; exit 1; } | |
| echo "artifact smoke OK: stamped, booted, healthy" | |
| - name: install the deb and run the service lifecycle | |
| # The packages were built on every push and installed by no one until a | |
| # user did it. This does what a Debian user does: install (postinstall | |
| # creates the service account and auto-starts the unit), verify the | |
| # daemon answers AND runs de-rooted as the dedicated account with only | |
| # CAP_NET_RAW, stop it, remove the package (preremove stops/disables). | |
| run: | | |
| sudo dpkg -i dist/pingularity_*_linux_amd64.deb | |
| ok= | |
| for i in $(seq 1 30); do | |
| if curl -fsS http://127.0.0.1:9000/healthz >/dev/null 2>&1; then ok=1; break; fi | |
| sleep 1 | |
| done | |
| [ -n "$ok" ] || { echo "packaged service did not serve /healthz"; sudo systemctl status pingularity || true; sudo journalctl -u pingularity --no-pager | tail -30 || true; exit 1; } | |
| svcuser=$(ps -o user= -p "$(systemctl show -p MainPID --value pingularity)") | |
| [ "$svcuser" = "pingularity" ] || { echo "service runs as '$svcuser', want the dedicated 'pingularity' account"; exit 1; } | |
| sudo systemctl stop pingularity | |
| sudo dpkg -r pingularity | |
| systemctl is-active pingularity >/dev/null 2>&1 && { echo "service still active after package removal"; exit 1; } | |
| echo "deb lifecycle OK: installed, de-rooted, healthy, removed cleanly" | |
| - name: install the rpm in a Rocky Linux container | |
| # No systemd in the container - the scriptlets are written to degrade | |
| # gracefully there (verified in packaging/postinstall.sh) - so this leg | |
| # asserts the rpm installs, the account is created, and the binary runs. | |
| run: | | |
| docker run --rm -v "$PWD/dist:/dist:ro" rockylinux:9 bash -ec ' | |
| rpm -i /dist/pingularity_*_linux_amd64.rpm | |
| getent passwd pingularity >/dev/null | |
| pingularity version | |
| ' | |
| echo "rpm install OK" | |
| # Downgrade gate: the previous RELEASED binary must open a database created by | |
| # this commit's schema. Migration tests synthesize old schemas in-process; this | |
| # is the other direction with the real prior artifact - the safety net for a | |
| # user who upgrades, hits trouble, and steps back. Rehearsed against v0.70.1: | |
| # healthz answers and the log stays clean. | |
| downgrade: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| 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: create a current-schema database | |
| run: | | |
| go build -o new_pingularity . | |
| ./new_pingularity run -listen 127.0.0.1:9107 -db "$RUNNER_TEMP/downgrade.db" & | |
| pid=$! | |
| ok= | |
| for i in $(seq 1 30); do | |
| if curl -fsS http://127.0.0.1:9107/healthz >/dev/null 2>&1; then ok=1; break; fi | |
| sleep 1 | |
| done | |
| kill "$pid" 2>/dev/null || true | |
| [ -n "$ok" ] || { echo "current build failed to create its own database"; exit 1; } | |
| - name: previous release opens it | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| if ! gh release download --pattern '*linux_amd64.tar.gz' -D prev; then | |
| echo "no prior release to test against - skipping" | |
| exit 0 | |
| fi | |
| tar xzf prev/*.tar.gz -C prev | |
| ./prev/pingularity version | |
| ./prev/pingularity run -listen 127.0.0.1:9108 -db "$RUNNER_TEMP/downgrade.db" > old.log 2>&1 & | |
| pid=$! | |
| ok= | |
| for i in $(seq 1 30); do | |
| if curl -fsS http://127.0.0.1:9108/healthz >/dev/null 2>&1; then ok=1; break; fi | |
| sleep 1 | |
| done | |
| kill "$pid" 2>/dev/null || true | |
| [ -n "$ok" ] || { echo "the PREVIOUS release cannot open a database this commit creates - a user who downgrades is stranded"; tail -30 old.log; exit 1; } | |
| if grep -ciE 'panic' old.log >/dev/null 2>&1 && [ "$(grep -ciE 'panic' old.log)" -gt 0 ]; then | |
| echo "previous release panicked on the new schema:"; tail -30 old.log; exit 1 | |
| fi | |
| echo "downgrade OK: previous release serves healthz on the new schema" | |
| # Image gate: builds BOTH Dockerfiles from the exact context layout goreleaser | |
| # stages (linux/<arch>/pingularity, COPY'd via $TARGETPLATFORM) and proves the | |
| # three properties a broken image would otherwise first show in a user's | |
| # release: the CAP_NET_RAW xattr survives the COPY chain, the data dir ships | |
| # 0700 65532:65532 with the volume-lineage marker BEFORE any daemon run, and a | |
| # hardened `docker run` actually becomes healthy. Runs under buildx (the | |
| # builder the release uses) because plain `docker build` was observed dropping | |
| # COPY --chmod on directories - this job must measure what ships, not what a | |
| # different builder happens to produce. release.yml gates on this via its `ci` | |
| # job (workflow_call runs every job in this file). | |
| docker: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| 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: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 | |
| - name: build both images from the goreleaser context layout | |
| run: | | |
| mkdir -p linux/amd64 | |
| CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o linux/amd64/pingularity . | |
| docker buildx build --load -t ping-ci:default . | |
| docker buildx build --load -f Dockerfile.iperf -t ping-ci:iperf . | |
| - name: xattr chain (security.capability must survive COPY into the final layer) | |
| run: | | |
| # setcap stamps the security.capability xattr in a throwaway stage and | |
| # the final stage COPYs the binary across. If any link in that chain | |
| # drops the xattr, the shipped binary silently loses CAP_NET_RAW and | |
| # raw-socket traceroute dies for every non-root container. docker save | |
| # exposes the layer tars, where the xattr rides as a PAX | |
| # SCHILY.xattr.security.capability record on the /pingularity entry - | |
| # assert it is present in every such entry, in BOTH images. | |
| docker save ping-ci:default -o default.tar | |
| docker save ping-ci:iperf -o iperf.tar | |
| python3 - default.tar iperf.tar <<'EOF' | |
| import sys, tarfile | |
| failed = False | |
| for path in sys.argv[1:]: | |
| checked = missing = 0 | |
| with tarfile.open(path) as outer: | |
| for m in outer.getmembers(): | |
| if not m.isfile(): | |
| continue | |
| f = outer.extractfile(m) | |
| try: | |
| inner = tarfile.open(fileobj=f, mode="r:*") | |
| except tarfile.ReadError: | |
| continue # manifest/config JSON, not a layer tar | |
| with inner: | |
| for e in inner: | |
| if e.isfile() and e.name.lstrip("./") == "pingularity": | |
| checked += 1 | |
| if "SCHILY.xattr.security.capability" not in e.pax_headers: | |
| missing += 1 | |
| print(f"{path}: layer {m.name}: /pingularity has NO security.capability PAX record") | |
| if checked == 0: | |
| print(f"{path}: no /pingularity layer entry found - the check matched nothing") | |
| failed = True | |
| elif missing: | |
| failed = True | |
| else: | |
| print(f"{path}: {checked} /pingularity layer entry(ies), all carry security.capability") | |
| sys.exit(1 if failed else 0) | |
| EOF | |
| - name: shipped permissions (docker export, BEFORE any daemon run) | |
| run: | | |
| # Measured on created-but-never-started containers on purpose: the | |
| # daemon tightens its own container data dir at boot (store.go), so | |
| # any post-run measurement passes even when the image ships 0755. | |
| # docker export reads what the layers actually say. | |
| docker create --name ci-default ping-ci:default | |
| docker export ci-default -o default-rootfs.tar | |
| docker rm ci-default | |
| docker create --name ci-iperf ping-ci:iperf | |
| docker export ci-iperf -o iperf-rootfs.tar | |
| docker rm ci-iperf | |
| python3 - default-rootfs.tar iperf-rootfs.tar <<'EOF' | |
| import sys, tarfile | |
| failed = False | |
| for path in sys.argv[1:]: | |
| entries = {} | |
| with tarfile.open(path) as tf: | |
| for e in tf: | |
| entries[e.name.lstrip("./").rstrip("/")] = e | |
| ok = True | |
| d = entries.get("var/lib/pingularity") | |
| if d is None or not d.isdir(): | |
| print(f"{path}: /var/lib/pingularity missing from the exported image"); ok = False | |
| else: | |
| mode = d.mode & 0o7777 | |
| if mode != 0o700: | |
| print(f"{path}: /var/lib/pingularity mode {oct(mode)}, want 0700"); ok = False | |
| if (d.uid, d.gid) != (65532, 65532): | |
| print(f"{path}: /var/lib/pingularity owner {d.uid}:{d.gid}, want 65532:65532"); ok = False | |
| if "var/lib/pingularity/.pingularity-image-dir" not in entries: | |
| print(f"{path}: volume-lineage marker .pingularity-image-dir missing"); ok = False | |
| if ok: | |
| print(f"{path}: /var/lib/pingularity 0700 65532:65532 with marker - as shipped") | |
| failed = failed or not ok | |
| sys.exit(1 if failed else 0) | |
| EOF | |
| - name: smoke (hardened run must reach healthy via the baked-in HEALTHCHECK) | |
| run: | | |
| # distroless has no shell, so `docker exec ... curl` cannot exist and a | |
| # CMD-string healthcheck could never run. The liveness mechanism is the | |
| # image's own exec-form HEALTHCHECK (/pingularity healthz), which hits | |
| # /healthz from inside the container's netns. Polling docker inspect's | |
| # .State.Health proves the HEALTHCHECK wiring, the healthz subcommand, | |
| # and the endpoint in one assertion - and fails if the HEALTHCHECK | |
| # instruction is ever dropped (no .State.Health at all then). The | |
| # interval is tightened at run time only so the poll converges fast. | |
| smoke() { | |
| name=$1 image=$2 | |
| docker run -d --name "$name" --cap-drop=ALL --cap-add=NET_RAW \ | |
| --health-interval=2s --health-timeout=5s --health-start-period=2s \ | |
| -v "$name-data:/var/lib/pingularity" "$image" | |
| s=none | |
| for i in $(seq 1 45); do | |
| s=$(docker inspect -f '{{.State.Health.Status}}' "$name" 2>/dev/null || echo none) | |
| [ "$s" = "healthy" ] && break | |
| sleep 2 | |
| done | |
| echo "--- $name health state ---" | |
| docker inspect -f '{{json .State.Health}}' "$name" 2>/dev/null || true | |
| echo "--- $name daemon logs ---" | |
| docker logs "$name" 2>&1 | tail -10 | |
| docker rm -f "$name" >/dev/null | |
| if [ "$s" != "healthy" ]; then | |
| echo "::error::$image never reported healthy (last state: $s) - the HEALTHCHECK or the healthz subcommand is broken" | |
| return 1 | |
| fi | |
| echo "$image: healthy under --cap-drop=ALL --cap-add=NET_RAW" | |
| } | |
| smoke ping-ci-default ping-ci:default | |
| smoke ping-ci-iperf ping-ci:iperf |