Files
gps-denied-onboard/e2e/docker/docker-compose.test.yml
T
Oleksandr Bezdieniezhnykh 7d53cef0cf
ci/woodpecker/push/02-build-push Pipeline failed
[AZ-701] HTTP replay API service (FastAPI + magic-byte upload validation)
New replay_api component: FastAPI service wrapping the offline
gps-denied-replay pipeline. POST tlog+video (multipart) → either
sync 200 with result/map/report URLs, or async 202 + job id with
/jobs/{id} polling. Magic-byte validation, bearer auth, in-memory
JobRegistry with concurrency + queue caps (429 on overflow).

Helper accuracy_report.py promoted from tests/ to src/ because the
API needs the Markdown report writer at runtime; all AZ-699 imports
re-pointed. OpenAPI spec exported to docs.

18/18 unit tests pass (AC-1 sync, AC-2 async, AC-3 state machine,
AC-5 auth, AC-6 health, AC-8 concurrency, AC-9 magic-byte). Full
unit suite: 2251 pass, 86 skip, 1 pre-existing C12 cold-start flake
(unchanged). mypy --strict clean on the new surface.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 17:30:26 +03:00

180 lines
6.3 KiB
YAML

# Tier-1 docker-compose entrypoint for the gps-denied-onboard blackbox e2e harness.
#
# Spec sources (single source of truth):
# _docs/02_document/tests/environment.md § Docker Environment
# _docs/02_tasks/todo/AZ-406_test_infrastructure.md
#
# Layout note: AZ-406 introduces this file; later test-task batches may add
# per-scenario override files alongside it (e.g. negative path injectors).
# This base file MUST stay self-contained — every override is purely additive.
#
# Build context (`build.context: ../..`) is the repo root, so the SUT image
# build sees `src/`, `cpp/`, `docker/Dockerfile`, and `pyproject.toml`.
services:
gps-denied-onboard:
build:
context: ../..
dockerfile: docker/companion-tier1.Dockerfile
args:
BUILD_VINS_MONO: "OFF"
image: gps-denied-onboard:e2e
networks: [e2e-net]
volumes:
- tile-cache-fixture:/var/azaion/tile-cache:ro
- fdr-output:/var/azaion/fdr
environment:
ONBOARD_FC_ADAPTER: ${FC_ADAPTER:-ardupilot}
ONBOARD_VIO_STRATEGY: ${VIO_STRATEGY:-okvis2}
MAVLINK_SIGNING_PASSKEY_FILE: /run/secrets/mavlink_passkey
secrets:
- mavlink_passkey
depends_on:
- mock-suite-sat-service
healthcheck:
test: ["CMD", "python", "-c", "from gps_denied_onboard.healthcheck import check; check()"]
interval: 5s
retries: 12
ardupilot-plane-sitl:
image: ardupilot/ardupilot-sitl:plane-stable
networks: [e2e-net]
command: ["--vehicle=ArduPlane", "--gps-type=14"]
environment:
# GPS_TYPE=14 selects MAV (external positioning) per ArduPilot SITL params.
AP_PARAM_GPS_TYPE: "14"
inav-sitl:
image: inavflight/inav-sitl:9.0.0
networks: [e2e-net]
# iNav SITL exposes MSP on TCP 5760 (UART1) per docs/SITL/SITL.md
mock-suite-sat-service:
build: ../fixtures/mock-suite-sat
image: mock-suite-sat-service:e2e
networks: [e2e-net]
environment:
MOCK_SUITE_SAT_AUDIT_PATH: /audit
volumes:
- mock-audit:/audit
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request, sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8080/mock/health', timeout=2).status==200 else 1)"]
interval: 5s
retries: 12
# AZ-701 — operator-side replay HTTP API.
#
# Profile-gated so the default `docker compose up` flow (the
# blackbox e2e suite) is unaffected. To start the API alongside
# the suite, run:
# docker compose --profile replay-api up replay-api
# The container exposes /healthz on 8080 and refuses /replay
# uploads without a bearer token unless REPLAY_API_AUTH_REQUIRED
# is explicitly set to false (dev only — WARN logged).
replay-api:
profiles: ["replay-api"]
build:
context: ../..
dockerfile: docker/replay-api.Dockerfile
image: gps-denied-replay-api:e2e
networks: [e2e-net]
ports:
- "${REPLAY_API_HOST_PORT:-8080}:8080"
environment:
REPLAY_API_AUTH_REQUIRED: "${REPLAY_API_AUTH_REQUIRED:-true}"
REPLAY_API_BEARER_TOKEN: "${REPLAY_API_BEARER_TOKEN:-}"
REPLAY_API_MAX_CONCURRENT_JOBS: "${REPLAY_API_MAX_CONCURRENT_JOBS:-1}"
REPLAY_API_MAX_QUEUED_JOBS: "${REPLAY_API_MAX_QUEUED_JOBS:-8}"
REPLAY_API_STORAGE_ROOT: /var/azaion/replay_api
volumes:
- replay-api-storage:/var/azaion/replay_api
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
interval: 10s
retries: 5
mavproxy-listener:
image: ardupilot/mavproxy:latest
networks: [e2e-net]
command:
- "--master=udp:0.0.0.0:14551"
- "--logfile=/var/log/tlogs/${RUN_ID:-local}.tlog"
- "--out=udp:e2e-runner:14552"
volumes:
- tlog-output:/var/log/tlogs
e2e-runner:
build: ../runner
image: gps-denied-onboard-e2e-runner:latest
networks: [e2e-net]
environment:
RUN_ID: ${RUN_ID:-local}
FC_ADAPTER: ${FC_ADAPTER:-ardupilot}
VIO_STRATEGY: ${VIO_STRATEGY:-okvis2}
TIER: tier1-docker
MAVLINK_PASSKEY_PATH: /test-fixtures/secrets/mavlink-test-passkey.txt
MOCK_SUITE_SAT_URL: http://mock-suite-sat-service:8080
AP_SITL_HOST: ardupilot-plane-sitl
INAV_SITL_HOST: inav-sitl
MAVPROXY_LISTENER_HOST: mavproxy-listener
volumes:
- ../../_docs/00_problem/input_data:/test-data:ro
- ../../_docs/00_problem/input_data/expected_results:/expected:ro
- ../fixtures:/test-fixtures:ro
- ../tests:/test-suite:ro
- fdr-output:/fdr:ro
- tlog-output:/tlogs:ro
- e2e-results:/e2e-results
- mock-audit:/mock-audit:ro
command:
- "pytest"
- "/test-suite"
- "--csv=/e2e-results/run-${RUN_ID:-local}/report.csv"
- "--csv-columns=test_id,test_name,traces_to,fc_adapter,vio_strategy,tier,started_at_utc,execution_time_ms,result,error_message,evidence_paths"
- "--evidence-out=/e2e-results/run-${RUN_ID:-local}/evidence"
depends_on:
gps-denied-onboard:
condition: service_healthy
mock-suite-sat-service:
condition: service_healthy
ardupilot-plane-sitl:
condition: service_started
inav-sitl:
condition: service_started
mavproxy-listener:
condition: service_started
networks:
e2e-net:
driver: bridge
# CRITICAL: enforces RESTRICT-SAT-1 / NFT-SEC-02 / NFT-SEC-05 at the network layer.
# The SUT, mock, runner, and SITLs can talk to each other but none of them can
# reach the public internet (no DNS, no egress). The e2e-runner verifies this
# at runtime by attempting a TCP connect to 1.1.1.1:443 (AC-5).
internal: true
volumes:
# Size cap follows AC-NEW-3: each FDR file ≤ 64 GB. The volume layer cap is
# belt-and-suspenders; the SUT enforces the cap internally per NFT-LIM-02.
# `--storage-opt size=64g` requires overlay2 with xfs backing on the host —
# most CI runners and macOS Docker Desktop hosts lack that combination, so
# this base file uses the documented fallback (a plain named volume) and
# relies on the SUT-internal cap. CI runners with overlay2+xfs can override
# via a docker-compose.override.yml that re-introduces the tmpfs driver_opts.
fdr-output: {}
tile-cache-fixture: {}
tlog-output: {}
mock-audit: {}
replay-api-storage: {}
e2e-results:
driver: local
driver_opts:
type: none
device: ${PWD}/../../e2e-results
o: bind
secrets:
mavlink_passkey:
file: ./secrets/mavlink_passkey