Add navi-admin service (extraction #7) (#7)

* Add navi-admin service (extraction #7)

Net-new fleet admin aggregator on :8427 — no port from recon (recon has no
/api/admin route; Phase A §3). Three @require_auth routes:
  GET /api/admin/fleet            fan-out to all 6 navi-* /api/admin/<svc>/info
                                  + recon /api/health, merged; never 5xx
                                  (failures land in errors[])
  GET /api/admin/recon/info       recon /api/health wrapped in the info shape
  GET /api/admin/navi-admin/info  self-describe

Fan-out forwards the caller's X-Authentik-Username so the @require_auth
upstreams accept it; per-service admin endpoints stay localhost-only (this is
the single edge-exposed admin surface). Service discovery: hardcoded list in
fleet.py (Option B). No secrets, no DB.

Deploy artifacts (NOT applied here): navi-admin.env.example, systemd unit,
nginx ^~ /api/admin snippet, and deploy/caddy notes for the @authed_api edit
(first Caddy change since #2).

12 hermetic tests (fleet happy-path, per-service timeout/500 → errors[],
auth-header forwarding, recon-down degraded-not-5xx, self-info no-secrets,
auth-required). Full monorepo suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* PR #7 review fixes

1. Symmetric degraded-entry handling in fleet.build_fleet — every
   probed service now appears in `services` with a uniform degraded
   dict on failure (matches recon's existing pattern), AND in errors[].
   Operators see "everything I tried + which broke" consistently.
2. Catch ValueError specifically in _get_json — non-JSON 200 responses
   now surface as `error: 'invalid JSON'` instead of opaque 'ValueError'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* PR #7 review fixes (round 2)

1. Unified degraded shape: wrap_recon_health calls _degraded_entry on
   failure — no more runtime.status vs runtime.recon_status asymmetry.
   Every probed service has the same shape on failure
   (runtime.status == 'unreachable'). recon-specific runtime fields
   (recon_status/recon_uptime/pipeline) remain only on the success path.
2. DRY'd git short-SHA helper into shared/git_sha.py — was duplicated in
   7 service app.py files + fleet.recon_git_sha. One implementation,
   one place to fix when behavior changes. Adds shared/tests (testpaths
   now includes "shared").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: zvx-echo6 <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
malice 2026-05-22 21:21:00 -06:00 committed by GitHub
commit d644741c75
21 changed files with 716 additions and 87 deletions

29
backend/shared/git_sha.py Normal file
View file

@ -0,0 +1,29 @@
"""Shared git short-SHA helper.
Used by every navi-* service's create_app() for the `version` field in
admin-info, and by navi-admin's fleet.recon_git_sha to read recon's deployed
SHA from its clone path. One implementation, one place to fix when behavior
needs changing.
"""
import subprocess
def git_short_sha(repo_path: str | None = None) -> str:
"""Return ``git rev-parse --short HEAD`` for the given repo path, or for the
current working directory if path is None. Returns 'unknown' on any failure
(no git, no repo, permission denied, detached HEAD, etc.) never raises.
repo_path: explicit repo to query (uses ``git -C <path>``); None = current
cwd (the systemd unit's WorkingDirectory in prod).
"""
cmd = ['git']
if repo_path is not None:
cmd.extend(['-C', repo_path])
cmd.extend(['rev-parse', '--short', 'HEAD'])
try:
sha = subprocess.check_output(
cmd, stderr=subprocess.DEVNULL, text=True, timeout=3,
).strip()
return sha or 'unknown'
except Exception:
return 'unknown'

View file

View file

@ -0,0 +1,14 @@
"""Hermetic tests for shared.git_sha.git_short_sha."""
from shared.git_sha import git_short_sha
def test_git_short_sha_returns_unknown_on_bad_path(tmp_path):
# tmp_path is an empty dir, not a git repo → graceful 'unknown', no raise.
assert git_short_sha(str(tmp_path)) == 'unknown'
def test_git_short_sha_in_real_repo():
# The suite runs from inside the navi-backend repo, so the cwd lookup works.
sha = git_short_sha()
assert sha != 'unknown'
assert len(sha) >= 7 # short SHA, sanity bound