Update patchmon to 2.1.3

Chart rewrite for the 2.x single-server architecture: one stateless
server deployment (frontend embedded, /api served by the same process),
no PVC, single service and ingress path, optional guacd sidecar
(disabled by default), 2.x env vocabulary (TRUST_PROXY, readonly role),
probes send the public hostname as Host header.

Fixes #29: all three reported spots (PVC storageClass guard, RWX assets
PVC, frontend nginx 'backend' hostname) are removed by this rewrite.
This commit is contained in:
jklein committed 2026-08-22 13:18:59 +02:00
1 parent f12d6409fd
commit fbe0dbac81
10 files changed
+141 -259

No files matched your search

+3 -3
View File
@@ -4,8 +4,8 @@ description: A Helm chart to deploy Patchmon on Kubernetes
type: application
version: 4.1.0+up1.4.2
appVersion: "1.4.2"
version: 5.0.0+up2.1.3
appVersion: "2.1.3"
maintainers:
- name: jklein
- name: jklein
@@ -1,79 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-frontend
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-frontend
spec:
replicas: {{ include "patchmon.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.patchmon.frontend.replicas) }}
selector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-frontend
template:
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-frontend
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-frontend
spec:
automountServiceAccountToken: false
securityContext:
{{- toYaml .Values.patchmon.frontend.securityContext.pod | nindent 8 }}
containers:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-frontend
image: "ghcr.io/patchmon/patchmon-frontend:{{ .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent
securityContext:
{{- toYaml .Values.patchmon.frontend.securityContext.container | nindent 12 }}
ports:
- containerPort: 3000
protocol: TCP
name: http
env:
- name: BACKEND_URL
value: "{{ .Release.Name }}-{{ .Chart.Name }}-backend-service"
- name: BACKEND_PORT
value: "80"
resources:
{{- include "patchmon.resources" .Values.patchmon.frontend.resources | nindent 12 }}
livenessProbe:
httpGet:
path: /
port: 3000
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 3000
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# The startup probe gates liveness/readiness. Startup budget = 5s * 30 = 150s.
startupProbe:
httpGet:
path: /
port: 3000
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
volumeMounts:
- name: patchmon-volume
mountPath: /usr/share/nginx/html/assets
subPath: patchmon-assets
# nginx-unprivileged keeps its pid file and temp dirs in /tmp, and
# its entrypoint renders config templates into /etc/nginx/conf.d;
# both must be writable with a read-only root filesystem.
- name: tmp
mountPath: /tmp
- name: nginx-conf-d
mountPath: /etc/nginx/conf.d
restartPolicy: Always
volumes:
- name: tmp
emptyDir: {}
- name: nginx-conf-d
emptyDir: {}
- name: patchmon-volume
persistentVolumeClaim:
claimName: {{ .Release.Name }}-{{ .Chart.Name }}-pvc
@@ -1,13 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-frontend-service
spec:
selector:
app: {{ .Release.Name }}-{{ .Chart.Name }}-frontend
ports:
- protocol: TCP
port: 80
targetPort: http
name: http
type: ClusterIP
@@ -1,30 +1,37 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-backend
name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-backend
app: {{ .Release.Name }}-{{ .Chart.Name }}
spec:
replicas: {{ include "patchmon.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.patchmon.backend.replicas) }}
replicas: {{ include "patchmon.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.patchmon.server.replicas) }}
selector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-backend
app: {{ .Release.Name }}-{{ .Chart.Name }}
template:
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-backend
name: {{ .Release.Name }}-{{ .Chart.Name }}
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-backend
app: {{ .Release.Name }}-{{ .Chart.Name }}
spec:
automountServiceAccountToken: false
# Pin a stable pod hostname: the server identifies itself in the agent
# presence registry (Redis) by hostname, and a Deployment pod's default
# hostname changes on every rollout, which would leave stale presence
# records behind (upstream pins "patchmon-server" in its compose file
# for the same reason).
hostname: {{ .Release.Name }}-{{ .Chart.Name }}-server
securityContext:
fsGroup: 1000
fsGroupChangePolicy: Always
{{- toYaml .Values.patchmon.server.securityContext.pod | nindent 8 }}
containers:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-backend
image: "ghcr.io/patchmon/patchmon-backend:{{ .Chart.AppVersion }}"
- name: {{ .Release.Name }}-{{ .Chart.Name }}-server
image: "ghcr.io/patchmon/patchmon-server:{{ .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent
securityContext:
{{- toYaml .Values.patchmon.server.securityContext.container | nindent 12 }}
ports:
- containerPort: 3001
- containerPort: 3000
protocol: TCP
name: http
env:
@@ -58,8 +65,6 @@ spec:
secretKeyRef:
name: {{ .Release.Name }}-{{ .Chart.Name }}-redis-secret
key: db
- name: ASSETS_DIR
value: /app/assets
- name: JWT_SECRET
valueFrom:
secretKeyRef:
@@ -104,8 +109,11 @@ spec:
key: requested-scopes
- name: OIDC_AUTO_CREATE_USERS
value: "true"
# 2.x role vocabulary: superadmin, admin, host_manager, readonly,
# user. "viewer" (the 1.x default of this chart) no longer exists;
# readonly is its least-privilege equivalent.
- name: OIDC_DEFAULT_ROLE
value: "viewer"
value: "readonly"
- name: OIDC_DISABLE_LOCAL_AUTH
value: "true"
- name: OIDC_BUTTON_TEXT
@@ -125,51 +133,100 @@ spec:
secretKeyRef:
name: {{ .Release.Name }}-{{ .Chart.Name }}-oidc-secret
key: admin-group-name
- name: SERVER_PROTOCOL
value: "https"
- name: SERVER_HOST
value: "{{ .Values.ingress.domain }}"
- name: SERVER_PORT
value: "443"
- name: CORS_ORIGIN
value: "https://{{ .Values.ingress.domain }}"
- name: TRUST_PROXIES
value: "1"
# The backend exposes GET /health returning {"status": "ok"}.
value: "https://{{ required "A Domain/Host must be provided" .Values.ingress.domain }}"
# Renamed upstream in 2.x (was TRUST_PROXIES=<count> in 1.x): the
# server sits behind the ingress-nginx reverse proxy.
- name: TRUST_PROXY
value: "true"
{{- if .Values.patchmon.guacd.enabled }}
- name: GUACD_ADDRESS
value: "127.0.0.1:4822"
{{- end }}
# The server exposes GET /health returning {"status": "ok"}. The
# 2.x server rejects requests whose Host header is neither loopback
# nor a CORS_ORIGIN host with 403 — kubelet probes target the pod
# IP, so they must send the public hostname as Host header.
livenessProbe:
httpGet:
path: /health
port: 3001
port: 3000
httpHeaders:
- name: Host
value: {{ .Values.ingress.domain }}
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 3001
port: 3000
httpHeaders:
- name: Host
value: {{ .Values.ingress.domain }}
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# The startup probe gates liveness/readiness. The backend waits for
# the database and runs migrations on startup: budget 10s * 30 = 300s.
# The startup probe gates liveness/readiness. The server waits for
# the database and runs migrations on startup; upstream warns that
# the first 2.x boot rebuilds database indexes, which can take
# minutes on slow storage: budget 10s * 60 = 600s.
startupProbe:
httpGet:
path: /health
port: 3001
port: 3000
httpHeaders:
- name: Host
value: {{ .Values.ingress.domain }}
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 60
resources:
{{- include "patchmon.resources" .Values.patchmon.server.resources | nindent 12 }}
volumeMounts:
# The image is stateless, but the Go server needs a writable /tmp
# for scratch files with a read-only root filesystem.
- name: tmp
mountPath: /tmp
{{- if .Values.patchmon.guacd.enabled }}
- name: {{ .Release.Name }}-{{ .Chart.Name }}-guacd
image: {{ .Values.patchmon.guacd.image }}
imagePullPolicy: IfNotPresent
securityContext:
{{- toYaml .Values.patchmon.guacd.securityContext.container | nindent 12 }}
ports:
- containerPort: 4822
protocol: TCP
name: guacd
livenessProbe:
tcpSocket:
port: 4822
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
tcpSocket:
port: 4822
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
startupProbe:
tcpSocket:
port: 4822
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
resources:
{{- include "patchmon.resources" .Values.patchmon.backend.resources | nindent 12 }}
{{- include "patchmon.resources" .Values.patchmon.guacd.resources | nindent 12 }}
volumeMounts:
- mountPath: /app/assets
name: patchmon-volume
subPath: patchmon-assets
- mountPath: /app/agents
name: patchmon-volume
subPath: patchmon-agents
- name: guacd-tmp
mountPath: /tmp
{{- end }}
restartPolicy: Always
volumes:
- name: patchmon-volume
persistentVolumeClaim:
claimName: {{ .Release.Name }}-{{ .Chart.Name }}-pvc
- name: tmp
emptyDir: {}
{{- if .Values.patchmon.guacd.enabled }}
- name: guacd-tmp
emptyDir: {}
{{- end }}
+3 -8
View File
@@ -10,20 +10,15 @@ spec:
- host: {{ required "A Domain/Host must be provided" .Values.ingress.domain }}
http:
paths:
# The 2.x server serves the frontend and /api/* from one process,
# so a single path covers everything (WebSockets included).
- backend:
service:
name: {{ .Release.Name }}-{{ .Chart.Name }}-frontend-service
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
port:
name: http
path: /
pathType: Prefix
- backend:
service:
name: {{ .Release.Name }}-{{ .Chart.Name }}-backend-service
port:
name: http
path: /api
pathType: Prefix
tls:
- hosts:
- {{ required "A Domain/Host must be provided" .Values.ingress.domain }}
-13
View File
@@ -1,13 +0,0 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: {{ .Values.patchmon.storage.size }}
{{- if .Values.patchmon.storage.storageClass }}
storageClassName: {{ .Values.patchmon.storage.class }}
{{- end }}
@@ -1,10 +1,10 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-backend-service
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
spec:
selector:
app: {{ .Release.Name }}-{{ .Chart.Name }}-backend
app: {{ .Release.Name }}-{{ .Chart.Name }}
ports:
- protocol: TCP
port: 80
+34 -19
View File
@@ -5,24 +5,23 @@
scaleDown: false
patchmon:
storage:
size: 10Gi
storageClass: null
frontend:
server:
# Replica count for this workload; 0 is allowed and scales it down.
# The chart-global scaleDown flag takes precedence.
# Note: the server registers itself in the agent presence registry under a
# stable identity (the pod hostname, pinned by the chart). Running more
# than one replica requires a distinct identity per replica and is not
# supported by this chart.
replicas: 1
# The frontend image is based on nginx-unprivileged; the pod runs as its
# nginx user (uid 101). The image's root-only init script (it would chown
# the shared assets volume) is replaced by fsGroup 1000: the same group
# the backend runs with, so both can read/write the shared assets.
# The 2.x server image is a hardened Alpine image running as uid 65532
# (nonroot). It is stateless: frontend, agent binaries and SCAP content
# are baked into the image, so the root filesystem stays read-only; /tmp
# is provided as an emptyDir.
securityContext:
pod:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 1000
fsGroupChangePolicy: Always
runAsUser: 65532
runAsGroup: 65532
container:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
@@ -37,20 +36,36 @@ patchmon:
limitFactor: 3
requests:
cpu: 0.1
memory: 128Mi
memory: 256Mi
ephemeral-storage: 10Mi
backend:
# Replica count for this workload; 0 is allowed and scales it down.
# The chart-global scaleDown flag takes precedence.
replicas: 1
guacd:
# In-browser RDP for Windows hosts (beta) via the Apache Guacamole daemon,
# run as a sidecar container in the server pod. Disabled by default; when
# enabled, the server is pointed at it via GUACD_ADDRESS=127.0.0.1:4822.
enabled: false
# guacd is an independent upstream application, so its image is not tied
# to the chart's appVersion. Pinned to the version upstream ships with.
image: guacamole/guacd:1.6.0
# The image runs as its own unprivileged user (uid 1000); it only needs a
# writable /tmp, which the chart provides as an emptyDir.
securityContext:
container:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
capabilities:
drop:
- ALL
resources:
# Limits are optional: memory and ephemeral-storage limits default to
# limitFactor x the request, an explicitly set limit always wins, and a
# CPU limit is only set when configured explicitly under "limits".
limitFactor: 3
requests:
cpu: 0.1
memory: 512Mi
cpu: 0.05
memory: 64Mi
ephemeral-storage: 10Mi
ingress: