Skip to content

Add 'datumctl api proxy': a local authenticated API proxy#247

Draft
scotwells wants to merge 1 commit into
mainfrom
feat/api-proxy
Draft

Add 'datumctl api proxy': a local authenticated API proxy#247
scotwells wants to merge 1 commit into
mainfrom
feat/api-proxy

Conversation

@scotwells

@scotwells scotwells commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

What users get

A new command that starts a loopback proxy to the Datum Cloud API of your datumctl session, injecting your credentials on every request and refreshing them transparently. Any local tool — a dev server, a test harness, curl — talks to the platform with zero token plumbing:

$ datumctl api proxy --port 8001
  Session:    maya@datum.net (api.datum.net)
  Upstream:   https://api.datum.net
  Scope:      full endpoint (use --project/--organization to serve one control plane)
  Listening:  http://127.0.0.1:8001

$ curl http://127.0.0.1:8001/apis/resourcemanager.miloapis.com/v1alpha1/organizations
  • Pure passthrough by default — the same paths that work against the real API work against the local port, so API_URL swapping gives dev/prod parity. --project/--organization re-base the proxy at one control plane for short URLs. Scope and session are pinned at startup; datumctl auth switch never repoints a running proxy.
  • Streaming works — Kubernetes-style watches, SSE, and chunked responses pass through unbuffered (hard requirement for the cloud-portal plugin-registry watch client).
  • Conservative security — binds 127.0.0.1 only (no override flag), Host-header validation against DNS rebinding, no CORS headers, inbound Authorization stripped, tokens never logged or served.
  • Actionable failures — a dead local session synthesizes 502 with a Status body and X-Datum-Proxy-Error: true telling the user to run datumctl login (never a fake 401); upstream errors pass through untouched. Logging out mid-run degrades gracefully and logging back in recovers without a proxy restart.
  • Harness-friendly — random free port by default, bare proxy URL printed as the first stdout line once the listener is bound (the readiness signal), graceful shutdown on SIGINT/SIGTERM. User docs at docs/api-proxy.md.

Design rationale, security model, and prior art: see the companion enhancement proposal #246.

How it's built

  • internal/apiproxy/ — the engine (no Cobra/keyring imports; token source and upstream injected): httputil.ReverseProxy with Rewrite + FlushInterval: -1, refresh-failure cooldown, redacting request log with streaming start/end lines.
  • internal/cmd/api/ — the api command group and proxy subcommand, registered in root.go.
  • internal/client/session_endpoint.go — session/endpoint/TLS resolution extracted into a shared helper; ToRESTConfig now uses the same code paths (flag precedence and error messages preserved) so the proxy and resource commands can't drift.
  • internal/authutil/credentials.gopersistingTokenSource now re-reads the keyring when a refresh is needed, so long-running consumers track logout/re-login instead of silently refreshing a session the user ended. This touches shared auth code — the diff deserves reviewer attention.
  • Drive-by fixes surfaced during validation: auth loginlogin in user-facing hints (per repo convention that primary login is top-level), and HOME isolation in internal/authutil/migrate_test.go, which could previously read the developer's real ~/.datumctl/credentials.json through the mock-keyring fallback.

Validation

  • 40+ tests across the engine and command, all green under go test ./... -race; go build, go vet, go install clean.
  • Watch/SSE streaming tests are deterministic: the fake upstream blocks until the client has observed each event through the proxy, proving zero buffering without sleeps.
  • Token lifecycle: single-flight refresh under concurrency, mid-stream expiry survival, cooldown gating, 502 mapping.
  • Security: host-validation matrix, Authorization-stripping, no-CORS, upstream-401-unmarked passthrough, loopback bind.
  • Help output verified free of kubectl/Kubernetes references (one pre-existing exception: the inherited global --namespace flag text, which predates this PR).

Known minor follow-ups (reported by review, deliberately not bundled)

Upstream CORS headers pass through un-stripped; hidden root flags like --server are silently ignored by the proxy; degenerate session endpoints can surface a raw error; per-request IDs / -v6 transport dumps not wired; host allowlist is port-agnostic (matches kubectl proxy prior art).

🤖 Generated with Claude Code

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant