mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 09:51:13 +00:00
[AZ-777] Phase 1: wire e2e-runner to real satellite-provider + C11 contract adapt
Adapt C11 HttpTileDownloader to the AZ-505 v1.0.0 tile-inventory
contract (POST /api/satellite/tiles/inventory + GET /tiles/{z}/{x}/{y})
and wire the Jetson e2e harness against the real parent-suite
satellite-provider service. Closes Phase 1 of 5 for AZ-777; STOP
gate before Phase 2 (Derkachi catalog seed).
C11 changes:
- _LIST_PATH / _GET_PATH replaced with _INVENTORY_PATH + _TILES_PATH.
- _do_enumerate enumerates bbox tile coords client-side and posts
chunked inventory requests (5000-entry cap per the contract).
- _download_one_tile parses tile_id_str into (z,x,y) and fetches
the slippy-map URL.
- Common GET / POST retry+auth ladder consolidated into _send_request.
- New module helpers: _enumerate_bbox_tile_coords,
_tile_center_latlon, _tile_size_meters_at, _format_tile_id_str,
_parse_tile_id_str, _chunk_iter.
- _DEFAULT_ESTIMATED_TILE_BYTES (50 KiB) replaces the inventory-side
estimatedBytes field the v1.0.0 contract dropped.
Tests:
- 14/14 unit tests in tests/unit/c11_tile_manager/test_tile_downloader.py
rewritten for the new POST inventory + slippy-map GET handler.
_StubTileWriter rekeyed by call-index (the downloader now derives
lat/lon from the slippy-map coord, so fixtures can't fabricate
arbitrary positions).
- New Tier-2 smoke at tests/e2e/satellite_provider/test_smoke.py:
validates inventory POST schema + drives HttpTileDownloader against
the real service. Gated by RUN_REPLAY_E2E=1 + tier2.
Compose / env:
- e2e-runner SATELLITE_PROVIDER_URL switched from mock-sat:5100 to
https://satellite-provider:8080; TLS_INSECURE + Bearer JWT env +
depends_on satellite-provider added.
- .env.test.example documents SATELLITE_PROVIDER_API_KEY + dev TLS
bypass security note.
- scripts/mint_dev_jwt.py mints HS256 dev JWTs from env / .env.test.
- pyjwt added to dev extras.
Tracker hygiene:
- AZ-777 row in _dependencies_table.md bumped 5pt -> 8pt to match
the 2026-05-21 override decision log.
Code review: PASS_WITH_WARNINGS (3 medium/low findings, all deferred
to later AZ-777 phases) -- see batch_104_review.md. Batch report at
batch_104_cycle3_report.md.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Mint a dev JWT for the parent-suite satellite-provider (AZ-777).
|
||||
|
||||
Reads JWT_SECRET / JWT_ISSUER / JWT_AUDIENCE from environment or from
|
||||
`.env.test` (when run from the repo root). Prints the JWT to stdout so
|
||||
the caller can pipe / paste / `export` it.
|
||||
|
||||
DEV-ONLY: the same secret signs the JWT and validates it on the
|
||||
provider side; production deploys retrieve operator JWTs from the admin
|
||||
API (AZ-690) instead. Mirrors `SatelliteProvider.TestSupport.JwtTokenFactory.Create`
|
||||
on the .NET side so dev tokens behave identically to integration-test ones.
|
||||
|
||||
Usage::
|
||||
|
||||
python scripts/mint_dev_jwt.py
|
||||
python scripts/mint_dev_jwt.py --lifetime-hours 12 --subject e2e-runner
|
||||
export SATELLITE_PROVIDER_API_KEY="$(python scripts/mint_dev_jwt.py)"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import jwt
|
||||
except ImportError as exc:
|
||||
sys.stderr.write(
|
||||
"ERROR: pyjwt not installed. Run `pip install pyjwt>=2.8,<3.0`\n"
|
||||
"(or `pip install -e .[dev]` from the repo root) and retry.\n"
|
||||
f"Underlying ImportError: {exc}\n"
|
||||
)
|
||||
sys.exit(72)
|
||||
|
||||
|
||||
_MIN_SECRET_BYTES = 32
|
||||
|
||||
|
||||
def _load_env_file(path: Path) -> dict[str, str]:
|
||||
"""Parse a minimal KEY=VALUE env file. Honours quoting; ignores comments."""
|
||||
|
||||
if not path.is_file():
|
||||
return {}
|
||||
out: dict[str, str] = {}
|
||||
for raw in path.read_text("utf-8").splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if "=" not in line:
|
||||
continue
|
||||
key, _, value = line.partition("=")
|
||||
key = key.strip()
|
||||
value = value.strip().strip('"').strip("'")
|
||||
if key:
|
||||
out[key] = value
|
||||
return out
|
||||
|
||||
|
||||
def _resolve(name: str, env_file_values: dict[str, str]) -> str | None:
|
||||
value = os.environ.get(name)
|
||||
if value:
|
||||
return value
|
||||
return env_file_values.get(name)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--lifetime-hours",
|
||||
type=float,
|
||||
default=8.0,
|
||||
help="Token lifetime in hours (default: 8).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--subject",
|
||||
default="gps-denied-onboard-e2e",
|
||||
help="`sub` claim (default: gps-denied-onboard-e2e).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--env-file",
|
||||
default=".env.test",
|
||||
help="Fallback env file (default: .env.test in CWD).",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
env_file_values = _load_env_file(Path(args.env_file))
|
||||
secret = _resolve("JWT_SECRET", env_file_values)
|
||||
issuer = _resolve("JWT_ISSUER", env_file_values)
|
||||
audience = _resolve("JWT_AUDIENCE", env_file_values)
|
||||
|
||||
missing = [
|
||||
name
|
||||
for name, value in [
|
||||
("JWT_SECRET", secret),
|
||||
("JWT_ISSUER", issuer),
|
||||
("JWT_AUDIENCE", audience),
|
||||
]
|
||||
if not value
|
||||
]
|
||||
if missing:
|
||||
sys.stderr.write(
|
||||
"ERROR: required env var(s) not set: "
|
||||
+ ", ".join(missing)
|
||||
+ f"\n(looked at environment + {args.env_file})\n"
|
||||
)
|
||||
return 73
|
||||
|
||||
assert secret is not None
|
||||
if len(secret.encode("utf-8")) < _MIN_SECRET_BYTES:
|
||||
sys.stderr.write(
|
||||
f"ERROR: JWT_SECRET is {len(secret)} bytes; HMAC-SHA256 requires "
|
||||
f">= {_MIN_SECRET_BYTES} bytes per the provider's contract.\n"
|
||||
)
|
||||
return 74
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
payload = {
|
||||
"sub": args.subject,
|
||||
"iss": issuer,
|
||||
"aud": audience,
|
||||
"jti": uuid.uuid4().hex,
|
||||
"iat": int(now.timestamp()),
|
||||
"nbf": int(now.timestamp()),
|
||||
"exp": int((now + timedelta(hours=args.lifetime_hours)).timestamp()),
|
||||
}
|
||||
|
||||
token = jwt.encode(payload, secret, algorithm="HS256")
|
||||
sys.stdout.write(token + "\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user