feat: Updated for api ingress routes
Chart Release / chart (push) Successful in 25s
Server Deploy / deploy (push) Successful in 4m26s

This commit is contained in:
2026-07-31 11:21:15 +01:00
parent 8528f14ed7
commit e6fe463216
6 changed files with 95 additions and 65 deletions
+20
View File
@@ -76,6 +76,20 @@ jobs:
--set ingress.tls.certResolver=letsencrypt \
--set server.env.grpcHost=agents.example.com:443 > /dev/null
# The shape the cloud deployment actually uses: a wildcard tenant
# namespace, /api and /auth routed at the edge, and no apex — that
# belongs to the marketing site, which this chart does not deploy.
- name: Render a wildcard host with edge-routed API paths
run: |
helm template test "$CHART_DIR" \
--set ingress.enabled=true \
--set 'ingress.web.host=*.vantage.example.com' \
--set ingress.api.enabled=true \
--set ingress.grpc.host=agents.example.com \
--set server.env.grpcHost=agents.example.com:443 \
--set ingress.tls.secretName=vantage-tls \
--set ingress.tls.grpcSecretName=agents-tls > /dev/null
# The guards are load-bearing, so their absence is a regression the
# same way a broken render is. Each of these must fail.
- name: Check the guards still refuse bad values
@@ -99,6 +113,12 @@ jobs:
--set server.replicaCount=2 --set server.persistence.enabled=true
refuses "ingress with no web host" \
--set ingress.enabled=true
refuses "edge-routed API with an empty path list" \
--set ingress.enabled=true \
--set ingress.web.host=vantage.example.com \
--set ingress.grpc.enabled=false \
--set ingress.api.enabled=true \
--set 'ingress.api.paths=null'
refuses "gRPC ingress with no host" \
--set ingress.enabled=true \
--set ingress.web.host=vantage.example.com \
+8 -5
View File
@@ -610,12 +610,15 @@ Windows: MSI built by CI (WiX), or `installer/setup.ps1` registering the agent a
`ingress.enabled` publishes **two** hostnames, because the two audiences arrive over different protocols:
| Values | Route |
| --------------------- | ----------------------------------------------------------------------------------------- |
| `ingress.web.host` | browsers → `web:3000`. Everything, including `/api` — see below |
| `ingress.grpc.host` | agents → a dedicated `<release>-server-grpc` Service on 9090, annotated `serversscheme: h2c` |
| Values | Route |
| -------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `ingress.web.host` (+ `web.extraHosts`) | browsers → `web:3000` |
| `ingress.api.paths` (when `api.enabled`) | `/api`, `/auth` → `<release>-server:8080`, bypassing the Next proxy |
| `ingress.grpc.host` | agents → a dedicated `<release>-server-grpc` Service on 9090, annotated `serversscheme: h2c` |
**The server's HTTP port is deliberately not publishable.** `web` already proxies `/api`, `/auth` and the install scripts to it (`web/next.config.ts`), so a second route would be a second front door to the same API with none of that routing — and the console WebSocket and ESO token path would then exist at two addresses with different behaviour.
**`ingress.web.host` is normally a wildcard.** `*.vantage.example.com` is the per-tenant instance namespace — `APP_ROOT_LABEL` resolves the instance from the label. A Kubernetes wildcard host matches **exactly one** label, so it does not match the apex, and here that is correct rather than a gap: `vantage.hostxtra.co.uk` is the marketing site (`site/`, in `docker-compose.site.yml`), which this chart does not deploy. `extraHosts` is for a genuine second name; adding the apex to it would put the control plane on the marketing host. Every host in the list gets identical paths.
**`ingress.api.enabled` routes `/api` and `/auth` straight to the server.** Both arrangements work — without it `web` proxies those prefixes onward itself (`web/next.config.ts`) — but edge routing is one hop shorter and matches what the Nginx Proxy Manager in front of the Docker deployment already does, so leaving it off makes the request path a different shape on Kubernetes than in production. It stays **off by default** because it only helps where the server is reachable on the same host and certificate as `web`; turning it on blindly moves the whole API onto a route that may not be provisioned. Traefik derives router priority from rule length, so `PathPrefix(/api)` outranks the catch-all `/` with no priority annotation needed.
**The gRPC route needs its own Service.** The server terminates no TLS; it speaks plain h2c and always has, with TLS terminated by whatever sits in front. Traefik will not use h2c to a backend unless the *Service* says so, and that annotation applies to every port on the Service — so annotating the shared two-port `<release>-server` would force h2c on its HTTP port too.
+2 -2
View File
@@ -2,5 +2,5 @@ apiVersion: v2
name: vantage
description: Helm chart for the Vantage stack (Redis, MongoDB, guacd, server, web)
type: application
version: 1.0.2
appVersion: "1.0.2"
version: 1.0.3
appVersion: "1.0.3"
+10 -3
View File
@@ -37,9 +37,16 @@ Scaling (server.replicaCount / web.replicaCount):
{{- if .Values.ingress.enabled }}
Ingress (Traefik):
- UI and API: https://{{ .Values.ingress.web.host }}
Everything browsers need goes here; web proxies /api, /auth and the install
scripts to the server, so the server's HTTP port is not published separately.
- Browsers: https://{{ .Values.ingress.web.host }}
{{- range .Values.ingress.web.extraHosts }}
https://{{ . }}
{{- end }}
{{- if .Values.ingress.api.enabled }}
{{ join ", " .Values.ingress.api.paths }} go straight to the server; everything else to web.
{{- else }}
Everything goes to web, which proxies /api and /auth onward. Set
ingress.api.enabled=true to route them at the edge instead.
{{- end }}
{{- if .Values.ingress.grpc.enabled }}
- Agents: {{ .Values.ingress.grpc.host }} (gRPC, h2c behind TLS)
Agents dial server.env.grpcHost, currently {{ tpl .Values.server.env.grpcHost . }}.
+49 -8
View File
@@ -2,10 +2,24 @@
{{/*
Two hostnames, because the two audiences arrive over different protocols.
Browsers reach `web`, and only `web`: it proxies /api, /auth and the install
scripts through to the server itself (see web/next.config.ts), so publishing the
server's HTTP port separately would be a second front door to the same API with
none of the same routing.
Browsers reach the web host. What answers there depends on the path: with
ingress.api.enabled, /api and /auth go straight to the server and everything
else goes to `web`. Without it, everything goes to `web`, which proxies those
prefixes onward itself (web/next.config.ts).
Both work. Routing at the edge is one hop shorter and is what the Nginx Proxy
Manager deployment in front of the Docker install already does, so leaving it
off changes the shape of the request path between the two deployments. It is
still off by default, because turning it on where `web` is the only thing with
a public certificate would strand /api behind a route nobody can reach.
The web host is normally a wildcard — `*.vantage.example.com` — because that is
the per-tenant instance namespace; APP_ROOT_LABEL resolves the instance from the
label. Kubernetes wildcard hosts match exactly one label, so this does not match
the apex, and on the deployment this chart was written for it must not: the apex
is the marketing site, a separate application (see site/ and
docker-compose.site.yml). extraHosts exists for a genuine second name, not for
reclaiming the apex.
Agents reach the server's gRPC port, which is plain h2c — the server holds no
certificates of its own, TLS has always been terminated by whatever sits in
@@ -16,6 +30,11 @@ its HTTP port too.
*/}}
{{- $tls := .Values.ingress.tls }}
{{- $webHost := required "ingress.enabled requires ingress.web.host" .Values.ingress.web.host }}
{{- $hosts := prepend .Values.ingress.web.extraHosts $webHost }}
{{- $apiPaths := .Values.ingress.api.paths }}
{{- if and .Values.ingress.api.enabled (not $apiPaths) }}
{{- fail "ingress.api.enabled requires at least one path in ingress.api.paths" }}
{{- end }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
@@ -41,20 +60,42 @@ spec:
{{- if and $tls.enabled $tls.secretName }}
tls:
- hosts:
- {{ $webHost | quote }}
{{- range $hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ $tls.secretName }}
{{- end }}
rules:
- host: {{ $webHost | quote }}
{{- range $host := $hosts }}
- host: {{ $host | quote }}
http:
paths:
{{- /*
The API paths come first and, more importantly, are longer. Traefik
derives router priority from rule length, so Host(x) &&
PathPrefix(/api) outranks Host(x) && PathPrefix(/) without anyone
having to assign priorities by hand. Order within the list is for
the reader; the length is what decides.
*/}}
{{- if $.Values.ingress.api.enabled }}
{{- range $apiPaths }}
- path: {{ . | quote }}
pathType: Prefix
backend:
service:
name: {{ $.Release.Name }}-server
port:
number: {{ $.Values.server.service.httpPort }}
{{- end }}
{{- end }}
- path: /
pathType: Prefix
backend:
service:
name: {{ .Release.Name }}-web
name: {{ $.Release.Name }}-web
port:
number: {{ .Values.web.service.port }}
number: {{ $.Values.web.service.port }}
{{- end }}
{{- if .Values.ingress.grpc.enabled }}
{{- $grpcHost := required "ingress.grpc.enabled requires ingress.grpc.host" .Values.ingress.grpc.host }}
{{/*
+6 -47
View File
@@ -1,9 +1,7 @@
# Default values for the vantage chart.
redis:
# false deploys no Redis and points the server at `redis.addr` instead.
enabled: true
# Only read when enabled is false. host:port of an external Redis.
addr: ""
image:
repository: redis
@@ -14,21 +12,14 @@ redis:
storageClass: ""
accessMode: ReadWriteOnce
port: 6379
# Both empty for an unauthenticated Redis. Redis 6+ ACL auth takes both; a
# legacy `requirepass` instance takes the password alone and must leave the
# username empty. Set existingSecret to keep the password out of values.
auth:
username: ""
password: ""
# Secret holding the credentials. When set, username/password above are
# ignored and these keys are read from the secret instead.
existingSecret: ""
usernameKey: username
passwordKey: password
mongo:
# false deploys no MongoDB. server.env.mongoUri must then point at an
# external one — the chart cannot guess it, and refuses to render without it.
enabled: true
image:
repository: mongo
@@ -49,22 +40,10 @@ guacd:
port: 4822
server:
# Safe to raise. Agent commands, step results and console relays are routed
# between replicas over Redis, workflow logs live in MongoDB, and the
# background jobs (monitor scheduler, reaper, retention sweeps) run under a
# Redis leader lock so exactly one replica performs them.
#
# Two requirements come with raising it: server.persistence.enabled must be
# false (or the volume ReadWriteMany), and Redis must be shared by every
# replica — the bus is not optional and a per-pod Redis would partition it.
replicaCount: 1
# Runs migrations, index builders and default-step seeding once, as a Helm
# pre-install/pre-upgrade hook, instead of in every starting pod. Leave it
# on for Kubernetes. Turning it off puts schema setup back in the pods.
migrationJob:
enabled: true
backoffLimit: 0
# 15 minutes: the instance rename alone carries a 10-minute budget.
activeDeadlineSeconds: 900
image:
repository: gitea.hostxtra.co.uk/mrhid6/vantage/server
@@ -78,16 +57,8 @@ server:
grpcHost: "{{ .Release.Name }}-server:9090"
keyEncryptionKey: ""
appRootLabel: vantage
# Ignored under Kubernetes: the chart sets POD_IP from the downward API
# and the server prefers it, because a console relay listener belongs to
# one pod and a Service address cannot name one.
proxyAdvertiseHost: "{{ .Release.Name }}-server"
proxyListenHost: "0.0.0.0"
# Off by default: nothing in the server writes to disk any more. Workflow
# logs, the only thing that ever did, are in MongoDB so that every replica
# can read and write them. Turn this on only to reach files left behind by
# a release that predates that move — and note a ReadWriteOnce volume caps
# replicaCount at 1 while it is on.
persistence:
enabled: false
size: 1Gi
@@ -96,8 +67,6 @@ server:
hostPath: /data
web:
# Stateless — safe to raise. Pin web.image.tag when you do: replicas on
# different builds serve mismatched chunk hashes and the UI 404s mid-session.
replicaCount: 1
image:
repository: gitea.hostxtra.co.uk/mrhid6/vantage/web
@@ -108,35 +77,25 @@ web:
env:
apiUrl: "http://{{ .Release.Name }}-server:8080"
# Traefik ingress. Two hostnames, because the two audiences arrive differently:
# browsers reach `web` (which proxies /api, /auth and the install scripts to the
# server), and agents reach the server's gRPC port directly.
#
# Publishing the server's HTTP port is deliberately not offered — it would be a
# second door to the same API, bypassing the routing web already performs.
ingress:
enabled: false
className: traefik
# Traefik entrypoint name. `websecure` is the default TLS entrypoint in the
# official chart; installs that renamed it must say so here.
entrypoint: websecure
# Applied to the web router only. Middlewares, rate limits, IP allow lists.
annotations: {}
web:
# Required when ingress.enabled. The hostname users open in a browser.
host: ""
extraHosts: []
api:
enabled: false
paths:
- /api
- /auth
grpc:
# Agents dial this. Turning it off means agents reach gRPC some other
# way — a LoadBalancer Service, a node port, or an in-cluster path.
enabled: true
host: ""
annotations: {}
tls:
enabled: true
# Either name pre-existing certificate Secrets, or leave both empty and
# set certResolver to have Traefik obtain them (ACME). Setting neither
# produces a TLS router with no certificate, which serves Traefik's
# self-signed default — valid-looking and trusted by nothing.
secretName: ""
grpcSecretName: ""
certResolver: ""