From e6fe4632162205399ce4edc299fd86c266e56ff9 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Fri, 31 Jul 2026 11:21:15 +0100 Subject: [PATCH] feat: Updated for api ingress routes --- .gitea/workflows/chart-release.yml | 20 ++++++++ claude.md | 13 +++-- deploy/chart/vantage/Chart.yaml | 4 +- deploy/chart/vantage/templates/NOTES.txt | 13 +++-- deploy/chart/vantage/templates/ingress.yaml | 57 ++++++++++++++++++--- deploy/chart/vantage/values.yaml | 53 +++---------------- 6 files changed, 95 insertions(+), 65 deletions(-) diff --git a/.gitea/workflows/chart-release.yml b/.gitea/workflows/chart-release.yml index 7a73e2b..adcc009 100644 --- a/.gitea/workflows/chart-release.yml +++ b/.gitea/workflows/chart-release.yml @@ -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 \ diff --git a/claude.md b/claude.md index d8203e0..80bd263 100644 --- a/claude.md +++ b/claude.md @@ -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 `-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` → `-server:8080`, bypassing the Next proxy | +| `ingress.grpc.host` | agents → a dedicated `-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 `-server` would force h2c on its HTTP port too. diff --git a/deploy/chart/vantage/Chart.yaml b/deploy/chart/vantage/Chart.yaml index bb2371f..3d0a322 100644 --- a/deploy/chart/vantage/Chart.yaml +++ b/deploy/chart/vantage/Chart.yaml @@ -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" diff --git a/deploy/chart/vantage/templates/NOTES.txt b/deploy/chart/vantage/templates/NOTES.txt index 53862de..1f41850 100644 --- a/deploy/chart/vantage/templates/NOTES.txt +++ b/deploy/chart/vantage/templates/NOTES.txt @@ -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 . }}. diff --git a/deploy/chart/vantage/templates/ingress.yaml b/deploy/chart/vantage/templates/ingress.yaml index 0745272..44cd073 100644 --- a/deploy/chart/vantage/templates/ingress.yaml +++ b/deploy/chart/vantage/templates/ingress.yaml @@ -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 }} {{/* diff --git a/deploy/chart/vantage/values.yaml b/deploy/chart/vantage/values.yaml index 2ee4164..e833fcb 100644 --- a/deploy/chart/vantage/values.yaml +++ b/deploy/chart/vantage/values.yaml @@ -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: ""