Skip to content

Rewrite Lab Instance service links to reverse-proxy vhost - #312

Draft
italovalcy wants to merge 7 commits into
mainfrom
feat/proxy-domain-service-links
Draft

italovalcy wants to merge 7 commits into
mainfrom
feat/proxy-domain-service-links

Conversation

@italovalcy

@italovalcy italovalcy commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

What

Two coupled halves of the same reverse-proxy feature.

1. Dashboard — rewrite Lab Instance service links to a reverse-proxy vhost.
On the Lab Instance view, each web service link currently points at a NodePort URL (node IP + node port). When a reverse proxy fronts the cluster, those same services are reachable at a per-service vhost. This change makes the page prefer that vhost when it is live.

A new optional setting PROXY_DOMAIN enables the behavior. On page load, a small JS routine probes each candidate vhost and, if it responds with a success/redirect, swaps the link's href to the proxy URL. On any failure it silently keeps the original NodePort link.

2. scripts/k8s_nginx_revproxy/ — the server-side counterpart.
A standalone daemon that actually makes those vhosts exist. It polls the Kubernetes API, matches NodePort services to their running+ready pods, and renders one nginx server{}/upstream{} per service, validating with nginx -t and reloading only on change (rolling back on bad config).

Shared naming scheme: {service.spec.ports.port}-{service.metadata.name}.{PROXY_DOMAIN}. The dashboard builds this hostname to link users to a service; the daemon publishes the same hostname. Only http/https services are proxied. The dashboard always links to the vhost over https:// (the nginx proxy terminates TLS and translates https->http to the NodePort backend); an http:// link would be blocked as mixed content on the https dashboard.

Changes

Dashboard

  • apps/config.py — add PROXY_DOMAIN (env var, empty by default = feature disabled).
  • apps/controllers/kubernetes.py — get_lab_resources now also returns services_proxy, a list index-aligned with services, each entry being the domain-free {scheme}://{port}-{service-name} prefix (the scheme marks web-service eligibility). The existing services tuple [port_name, url] is unchanged.
  • apps/home/routes.py — view_lab_instance reads PROXY_DOMAIN and, for http(s) services, emits an always-https:// candidate URL https://{port}-{service-name}.{PROXY_DOMAIN} passed to the template.
  • apps/templates/pages/lab_instance_view.html — service anchors gain a data-proxy-url attribute; a probeProxyLink routine fetches each candidate with redirect: 'manual' and a 5s AbortController timeout, swapping href on a 2xx/3xx response.
  • CHANGELOG.md — entry under [UNRELEASED].

scripts/k8s_nginx_revproxy/

  • k8s_nginx_revproxy.py — the daemon: multi-kubeconfig failover, per-request timeouts, optional SOCKS5 tunnel, per-service annotation overrides (hostname/scheme/port/enable), opt-in mode, and --dry-run/--once.
  • README.md — what it does, the naming scheme, prerequisites (wildcard DNS + TLS, RBAC), full config reference, and how to run it manually or under systemd.
  • NGINX_SETUP.md — tuned & hardened nginx install guide (Debian 12).
  • k8s-nginx-revproxy.service — systemd unit template with sandboxing, plus a documented systemd-managed SSH tunnel setup for reaching the API.
  • requirements.txt — Python deps (pinned).

Reviewer notes

  • The dashboard links to the vhost over https:// and probes it cross-origin (the vhost is a different host than the dashboard). With redirect: 'manual', a 3xx is detected reliably (surfaces as an opaqueredirect, no CORS headers needed). A bare 2xx is only readable if the reverse proxy sends Access-Control-Allow-Origin; without it, fetch rejects and the link stays on the NodePort URL (safe fallback).
  • The services tuple shape is unchanged, so the existing test_k8s_tape.py assertions still hold. Full suite: 627 passed.
  • The daemon is standalone tooling under scripts/ (not imported by the app); python -m py_compile and --help verified.

🤖 Generated with Claude Code

On the Lab Instance view, each web service link points at a NodePort URL
(node IP + node port). When a reverse proxy fronts the cluster, those
services are also reachable at a per-service vhost. This adds an optional
PROXY_DOMAIN setting and a client-side routine that, on page load, probes
each candidate vhost ("{scheme}://{port}-{service-name}.{PROXY_DOMAIN}")
and swaps the link's href to it when the vhost responds with a
success/redirect, falling back silently to the NodePort link otherwise.

- config: add PROXY_DOMAIN (env, empty = disabled)
- kubernetes controller: expose services_proxy, an index-aligned list of
  "{scheme}://{port}-{service-name}" prefixes; the services tuple is
  unchanged
- view_lab_instance: append ".{PROXY_DOMAIN}" for http(s) services and
  pass the candidate URLs to the template
- template: add data-proxy-url to service anchors and a fetch-based probe
  (redirect:'manual' + 5s AbortController timeout)
@italovalcy
italovalcy force-pushed the feat/proxy-domain-service-links branch from a8c8a82 to 8eb09a0 Compare September 9, 2026 14:18
Standalone daemon (the server-side counterpart to the dashboard's
PROXY_DOMAIN feature) that publishes Kubernetes NodePort services through
an external nginx HTTPS reverse proxy. It polls the k8s API, matches
services to their running+ready pods, and renders one nginx vhost per
service at "{port}-{service-name}.{PROXY_DOMAIN}" -- the same hostname the
dashboard links to -- validating with `nginx -t` and reloading only on
change (rolling back on bad config). Supports multiple kubeconfigs with
failover, per-request timeouts, an optional SOCKS5 tunnel, per-service
annotation overrides, and dry-run/once modes.

Includes a README, a tuned/hardened nginx setup guide (Debian 12), a
systemd unit template, and requirements.txt.
The proxy candidate URL inherited the backend service scheme, so an http
service produced an http:// link. On the https dashboard page the browser
blocks that as mixed content ("requested insecure content ... blocked"),
so the link was never swapped. Always emit https:// for the proxy URL and
let the nginx reverse proxy terminate TLS and translate https->http to the
NodePort backend. Web-service eligibility (http/https only, ssh/vnc left on
the NodePort link) is unchanged.
The CORS-mode probe required every proxied lab service to emit valid CORS
headers, which they don't: auth-gated services return 401 (unreadable
cross-origin), and when a backend/nginx emit their own Access-Control-
Allow-Origin the browser rejects the whole response ("cannot contain more
than one origin"). Either way fetch threw and the link was never swapped.

Switch the probe to mode:'no-cors' and swap on any resolved (reachable)
response. An opaque response hides the status, but that's sufficient: a
real vhost (200/302/401/...) resolves and gets swapped, while a hostname
with no configured vhost is rejected by the reverse proxy's default server
(return 444) and makes fetch reject, leaving the NodePort link in place.
This also correctly links auth-gated services (401) that the previous
2xx/3xx-only rule skipped.
Restore the explicit 2xx/3xx status checks in the vhost probe alongside an
opaque-response branch. In mode:'no-cors' the cross-origin response is
opaque (status 0, ok false), so the status checks can never read it on
their own -- gating the swap on them alone would break the feature. The
opaque branch is what actually fires and drives detection; the status
checks are kept for clarity and remain correct if the probe is ever
switched back to CORS mode. Behavior is unchanged: swap on any resolved
response, keep the NodePort link when fetch rejects.
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