Repository navigation
Rewrite Lab Instance service links to reverse-proxy vhost - #312
Draft
italovalcy wants to merge 7 commits into
Draft
italovalcy wants to merge 7 commits into
italovalcy wants to merge 7 commits into
Conversation
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
force-pushed
the
feat/proxy-domain-service-links
branch
from
September 9, 2026 14:18
a8c8a82 to
8eb09a0
Compare
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.
…h tunnel managed by systemd
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_DOMAINenables the behavior. On page load, a small JS routine probes each candidate vhost and, if it responds with a success/redirect, swaps the link'shrefto 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 withnginx -tand 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. Onlyhttp/httpsservices are proxied. The dashboard always links to the vhost overhttps://(the nginx proxy terminates TLS and translates https->http to the NodePort backend); anhttp://link would be blocked as mixed content on the https dashboard.Changes
Dashboard
apps/config.py— addPROXY_DOMAIN(env var, empty by default = feature disabled).apps/controllers/kubernetes.py—get_lab_resourcesnow also returnsservices_proxy, a list index-aligned withservices, each entry being the domain-free{scheme}://{port}-{service-name}prefix (the scheme marks web-service eligibility). The existingservicestuple[port_name, url]is unchanged.apps/home/routes.py—view_lab_instancereadsPROXY_DOMAINand, forhttp(s)services, emits an always-https://candidate URLhttps://{port}-{service-name}.{PROXY_DOMAIN}passed to the template.apps/templates/pages/lab_instance_view.html— service anchors gain adata-proxy-urlattribute; aprobeProxyLinkroutine fetches each candidate withredirect: 'manual'and a 5sAbortControllertimeout, swappinghrefon 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
https://and probes it cross-origin (the vhost is a different host than the dashboard). Withredirect: 'manual', a 3xx is detected reliably (surfaces as anopaqueredirect, no CORS headers needed). A bare 2xx is only readable if the reverse proxy sendsAccess-Control-Allow-Origin; without it,fetchrejects and the link stays on the NodePort URL (safe fallback).servicestuple shape is unchanged, so the existingtest_k8s_tape.pyassertions still hold. Full suite: 627 passed.scripts/(not imported by the app);python -m py_compileand--helpverified.🤖 Generated with Claude Code