Skip to content

Latest commit

 

History

History
155 lines (125 loc) · 8.48 KB

api-surface.md

File metadata and controls

155 lines (125 loc) · 8.48 KB
description The itsUP management REST API — apikey-guarded webhook endpoints that trigger deploys (including a production self-update that hard-resets to origin/main) and list projects.

API Surface — Spec

What it is

A small FastAPI app (api/main.py, title itsUP API v2.0) that lets external systems trigger deploys via webhook and query projects. It is not containerized — it runs as a host process on :8888, supervised by the host as a daemon unit.

Its Traefik routes are scoped to the two endpoints that need the internet, each its own Host(...) && PathPrefix(...) router: the unauthenticated /redirect bouncer and the origin-gated /file endpoint. Each is a path prefix, not an exact route — it admits any method and any path beginning with its prefix, and nothing else is served under either prefix, so a request under a prefix that no handler serves is answered 405 or 404 by FastAPI. /redirect is unauthenticated; /file carries a route-scoped source-IP allowlist at the proxy (local/projects/itsup/spec/feature/deployment/route-scoped-ip-allowlist) so only the allowlisted origin reaches it.

The apikey-guarded endpoints share no prefix with it and so carry no public route at all — an internet request for one of them fails to match a router and is refused by the proxy before the API sees it, rather than reaching the API and being rejected by the API key. Those endpoints are served over plain HTTP on :8888 to callers already inside the boundary: the container host itself over loopback, and LAN or VPN clients at the host's LAN address. The hostname and its Let's Encrypt certificate are unaffected — ACME HTTP-01 resolves at the web entrypoint, ahead of router matching. Every mutating/data endpoint is guarded by an API key (verify_apikey, lib/auth.py, via FastAPI Depends). Deploy work runs in a FastAPI BackgroundTask; the endpoint returns immediately.

Canonical fields

Endpoints (api/main.py)

Method/Path Auth Reachable from Behaviour
GET /update-upstream/{project} apikey host loopback, LAN/VPN Background-deploys one project via .venv/bin/itsup apply {project} (:27-36,84-93). Unknown project ⇒ logged and ignored.
GET /update-upstream/{project}/{service} apikey host loopback, LAN/VPN Same, scoped to one service.
POST /reconcile apikey host loopback, LAN/VPN Background full-stack reconcile: pulls the projects/secrets config repos then runs itsup apply; single-flight with trailing-run coalescing (lib/reconcile.py).
GET /projects apikey host loopback, LAN/VPN Returns list_projects() (@cached, :96-100).
GET /redirect?url= none internet 307-redirects, but only message:// / imessage:// schemes; rejects other schemes or whitespace (:103-116). Consumer: OtoMo (lib/deep_links.py) wraps iMessage deep links in this endpoint so Telegram renders them as clickable https links.

| PUT /upload/{name} | apikey and LAN origin | LAN (over HTTPS) | Stores the request body as {name} in the itsUP-owned upload directory, replacing any existing file, and reports the stored location. {name} must be a single path segment with an allowlisted extension; the body is capped at 1 MB, enforced during the read. Feeds GET /file. Contract: local/projects/itsup/spec/feature/api/gated-file-serving. |

Gated file endpoint (GET /file)

GET /file?path=<host-path> serves the bytes of a local host file selected by path, over the internet, to an origin-gated caller. It carries no app-side authentication: the origin gate is a route-scoped source-IP allowlist at the proxy (local/projects/itsup/spec/feature/deployment/route-scoped-ip-allowlist), and the app-side security boundary is a positive file-extension allowlist (initially .lsrules) that refuses config/secret formats by construction. A non-allowlisted extension, or a path that is not an existing regular file, is refused with a client-error status; an allowlisted-extension file is returned with its bytes and the content type mapped for that extension. path is a required query parameter, so omitting it is rejected by FastAPI's own request validation rather than by the endpoint. Behaviour contract: local/projects/itsup/spec/feature/api/gated-file-serving.

The GitOps chain reaches the apikey-guarded endpoints over loopback: the shared reconcile workflow's curl runs on the container host itself, inside the SSH step that follows its VPN connection. Ad-hoc triggering from outside the network is not available; an operator reaches these endpoints over LAN or VPN.

Publishing the upload route

PUT /upload/{name} is reached over HTTPS on the same hostname as /file, which requires a router for its path prefix. That row lives in the separate itsUP-projects repository and is published there, not by the change that adds the endpoint:

  - domain: itsup.srv.instrukt.ai
    path_prefix: /upload
    port: 8888
    router: http
    allow_source_ips:
      - 192.168.1.1/32

The allowlist is the same one /file carries, and for the same reason: Traefik attributes LAN-hairpin traffic to the gateway, so the address admits the LAN and denies the internet. The endpoint therefore gains TLS from the existing certificate without becoming internet-reachable.

The origin gate is not the write boundary. Because every hairpinned LAN client presents as the gateway address, the gate cannot distinguish one LAN device from another — it authorises the network, not the caller. The API key is what authorises the caller, which is why the upload route carries both while the read route carries only the origin gate: its consumer is a header-less fetcher that can carry no credential at all.

Publishing the row makes this section's opening description stale — the routers on the hostname become three rather than two, and the third is both origin-gated and API-key-guarded. Whoever publishes it updates those statements in the same change.

The row is published after the endpoint exists, not before: a router for a path the app does not yet serve answers 404. Until it is published the endpoint is reachable over plain HTTP on :8888 from inside the boundary, like the other API-key endpoints.

Self-update (project == "itsUP")

GET /update-upstream/itsUP triggers _handle_itsup_update (:39-66): in PYTHON_ENV=production it git fetch origin main + git reset --hard origin/main (destructive — discards local changes to the itsUP checkout), then redeploys DNS + proxy stacks (smart_deploy) and .venv/bin/itsup apply (all projects), then restarts the API. This is the unattended self-update path.

After the dependency sync — on Linux only, where the container host runs — the self-update detects whether the delivery left the host's installed systemd units drifted from the delivered samples/systemd/* templates (bin/install-bringup.sh --check-drift: a read-only render-and-compare of each template against /etc/systemd/system, dispatched before the installer's host gate and any mutating step, so it holds no privilege and mutates nothing). On drift it always logs the drifted units and the one-command remedy make install-runtime to the journal, and — when alert.command is configured — additionally raises the operator alert with the same content through bin/alert.py --drift-units (a third alert kind alongside unit-failure and the apply deadman), suppressing exactly as those do when the transport is unset. Installing the units stays the operator's privileged, gated step; the self-update guarantees the staleness is recorded rather than passing unnoticed. The detection and alert are non-fatal — a failure logs but never aborts the self-update — and are skipped with a logged notice on macOS.

Server

Uvicorn on 0.0.0.0:8888; proxy_headers/forwarded IPs trusted only in production (:119-130). OpenAPI schema is extractable via api/extract-openapi.py.

Known caveats

  • Webhook deploys are GETs with side effects — chosen for webhook-provider compatibility; the API key is the only guard, so treat the key as a deploy credential.
  • Self-update is a hard reset — git reset --hard origin/main in production means any uncommitted change on the host checkout is lost on a self-update webhook.

See Also

  • docs/local/design/deployment-orchestration.md