Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57a9b18102 | ||
|
|
36995fa62b | ||
|
|
9121fc461f | ||
|
|
fc56bae5f9 | ||
|
|
ac75b3ef76 | ||
|
|
e6fe463216 |
@@ -59,6 +59,23 @@ jobs:
|
||||
--set server.replicaCount=3 \
|
||||
--set web.replicaCount=3 > /dev/null
|
||||
|
||||
# The reaper deletes whole instances, so "does this env appear only
|
||||
# in cloud mode" is worth asserting rather than eyeballing.
|
||||
- name: Check the reaper is cloud-only
|
||||
run: |
|
||||
set -eu
|
||||
if helm template test "$CHART_DIR" | grep -q FREE_INSTANCE_REAP_AFTER; then
|
||||
echo "FREE_INSTANCE_REAP_AFTER is set on a self-hosted render"
|
||||
exit 1
|
||||
fi
|
||||
if ! helm template test "$CHART_DIR" \
|
||||
--set server.env.deploymentType=cloud \
|
||||
| grep -q FREE_INSTANCE_REAP_AFTER; then
|
||||
echo "FREE_INSTANCE_REAP_AFTER is missing from a cloud render"
|
||||
exit 1
|
||||
fi
|
||||
echo "ok: reaper configured in cloud mode only"
|
||||
|
||||
- name: Render against external Redis and MongoDB
|
||||
run: |
|
||||
helm template test "$CHART_DIR" \
|
||||
@@ -76,6 +93,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 +130,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 \
|
||||
|
||||
@@ -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,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.6
|
||||
appVersion: "1.0.6"
|
||||
|
||||
@@ -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 . }}.
|
||||
|
||||
@@ -72,11 +72,12 @@ both read it.
|
||||
value: {{ .Values.server.env.proxyAdvertiseHost | quote }}
|
||||
- name: PROXY_LISTEN_HOST
|
||||
value: {{ .Values.server.env.proxyListenHost | quote }}
|
||||
# The address guacd dials to reach a console relay. It must name one pod, not
|
||||
# the Service: the relay listener is bound by whichever pod holds that agent's
|
||||
# command stream, and a Service would send guacd to a different one. POD_IP
|
||||
# takes precedence over PROXY_ADVERTISE_HOST in the server for exactly this
|
||||
# reason, so the setting above stays meaningful only outside Kubernetes.
|
||||
{{- if eq .Values.server.env.deploymentType "cloud" }}
|
||||
- name: VANTAGE_DEPLOYMENT
|
||||
value: "cloud"
|
||||
- name: FREE_INSTANCE_REAP_AFTER
|
||||
value: {{ .Values.server.env.freeInstanceReapAfter | quote }}
|
||||
{{- end }}
|
||||
- name: POD_IP
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
|
||||
@@ -36,6 +36,9 @@ metadata:
|
||||
app.kubernetes.io/component: guacd
|
||||
spec:
|
||||
type: {{ .Values.guacd.service.type }}
|
||||
{{- if eq .Values.guacd.service.type "ClusterIP" }}
|
||||
clusterIP: None
|
||||
{{- end }}
|
||||
selector:
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/component: guacd
|
||||
|
||||
@@ -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 }}
|
||||
{{/*
|
||||
|
||||
@@ -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,29 @@ 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
|
||||
- /update
|
||||
- /install
|
||||
- /update.ps1
|
||||
- /install.ps1
|
||||
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: ""
|
||||
|
||||
@@ -142,7 +142,6 @@ func newServer(c *gin.Context) {
|
||||
}
|
||||
services.LogEvent(auth.InstanceID(c), "server.created", actorFromCtx(c), s.ServerID, "", "pre-registration token issued")
|
||||
|
||||
|
||||
host := publicHostFromRequest(c)
|
||||
|
||||
installCmd := fmt.Sprintf(
|
||||
@@ -415,7 +414,7 @@ if [ -z "$LATEST" ]; then
|
||||
fi
|
||||
|
||||
VERSION="${LATEST#agent/}"
|
||||
LATEST_ENCODED="${LATEST/\
|
||||
LATEST_ENCODED="${LATEST/\//%%2F}"
|
||||
BINARY_URL="https://${GITEA_HOST}/mrhid6/vantage/releases/download/${LATEST_ENCODED}/vantage-agent-linux-${ARCH}"
|
||||
CHECKSUM_URL="https://${GITEA_HOST}/mrhid6/vantage/releases/download/${LATEST_ENCODED}/checksums.txt"
|
||||
|
||||
@@ -522,7 +521,7 @@ if [ -z "$LATEST" ]; then
|
||||
fi
|
||||
|
||||
VERSION="${LATEST#agent/}"
|
||||
LATEST_ENCODED="${LATEST/\
|
||||
LATEST_ENCODED="${LATEST/\//%%2F}"
|
||||
BINARY_URL="https://${GITEA_HOST}/mrhid6/vantage/releases/download/${LATEST_ENCODED}/vantage-agent-linux-${ARCH}"
|
||||
CHECKSUM_URL="https://${GITEA_HOST}/mrhid6/vantage/releases/download/${LATEST_ENCODED}/checksums.txt"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user