Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ Everything's in the tree; what's left is shipping it to strangers.

Small, coherent UX follow-ups on top of the current admin console.

- **Roll out split persistence for HA.** Validate and migrate canary/prod to a
runtime/cache volume plus a small durable state volume so replicated CSI only
carries save/config identity state, not SteamCMD, Proton, and WindowsServer
cache.
- **SSE log stream**. UI's "Log" card today shows ephemeral client-side
events. Since the game-container's stdout has `tail -F R5.log`, we can
expose `/api/logs/stream` as a Server-Sent-Events endpoint from
Expand Down
81 changes: 76 additions & 5 deletions helm/windrose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,82 @@ the chart is a thin wrapper around those vars plus Kubernetes-level knobs

| Value | Default | Purpose |
|---|---|---|
| `persistence.existingClaim` | `""` | If set, use an existing PVC instead of creating one. |
| `persistence.size` | `20Gi` | New-PVC size request. `WindowsServer/` is ~3 GiB; saves + backups + GE-Proton cache add headroom. |
| `persistence.accessMode` | `ReadWriteOnce` | |
| `persistence.storageClassName` | `""` | `""` = cluster default. |
| `persistence.subPath` | `steam-root` | Mount subdir; gives the pod a clean `/home/steam` view. |
| `persistence.claims` | one `data` claim, `20Gi` | PVCs to create/use. Set `existingClaim` on a claim to mount an operator-managed PVC instead of rendering one. |
| `persistence.mounts` | `data` mounted at `/home/steam`, `subPath: steam-root` | Kubernetes `volumeMounts` rendered into the game and UI containers. Mount names must match `persistence.claims[].name`. |
| `env` | `[]` | Additional env vars rendered into the game and UI containers. Names must be unique and cannot duplicate chart-managed env vars; use the matching chart value for those. |

The default persistence values preserve the original single-PVC layout:

```yaml
persistence:
claims:
- name: data
existingClaim: ""
size: 20Gi
accessMode: ReadWriteOnce
storageClassName: ""
mounts:
- name: data
mountPath: /home/steam
subPath: steam-root
```

Customized old scalar keys such as `persistence.size` and
`persistence.existingClaim` fail chart rendering with a migrated
`claims[]` / `mounts[]` snippet. The old default scalar values are
tolerated so `helm upgrade --reuse-values` from `0.2.1` can preserve the
single-PVC layout without a manual values rewrite.

For split runtime/state/backup storage, override the same block with direct
mountpoints. This keeps the runtime cache replaceable while the R5 root,
server identity/config, saves, and mod metadata live on the state PVC. The
large Steam-managed R5 content directories are mounted back from the runtime
PVC, and mod payload directories are nested back onto the state PVC:

```yaml
persistence:
claims:
- name: runtime
size: 20Gi
storageClassName: local-path
accessMode: ReadWriteOnce
- name: state
size: 5Gi
storageClassName: longhorn-rf3
accessMode: ReadWriteOnce
- name: backups
size: 20Gi
storageClassName: local-path
accessMode: ReadWriteOnce
mounts:
- name: runtime
mountPath: /home/steam
subPath: steam-root
- name: state
mountPath: /home/steam/windrose/WindowsServer/R5
subPath: r5-state
- name: runtime
mountPath: /home/steam/windrose/WindowsServer/R5/Binaries
subPath: steam-root/windrose/WindowsServer/R5/Binaries
- name: runtime
mountPath: /home/steam/windrose/WindowsServer/R5/Config
subPath: steam-root/windrose/WindowsServer/R5/Config
- name: runtime
mountPath: /home/steam/windrose/WindowsServer/R5/Content
subPath: steam-root/windrose/WindowsServer/R5/Content
- name: runtime
mountPath: /home/steam/windrose/WindowsServer/R5/Plugins
subPath: steam-root/windrose/WindowsServer/R5/Plugins
- name: state
mountPath: /home/steam/windrose/WindowsServer/R5/Content/Paks/~mods
subPath: r5-state/mods/enabled
- name: state
mountPath: /home/steam/windrose/WindowsServer/R5/Content/Paks/~mods.disabled
subPath: r5-state/mods/disabled
- name: backups
mountPath: /home/steam/backups
subPath: windrose-backups
```

### Service + Ingress

Expand Down
158 changes: 154 additions & 4 deletions helm/windrose/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,161 @@ app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
{{- end -}}

{{- define "windrose.pvcName" -}}
{{- if .Values.persistence.existingClaim -}}
{{- .Values.persistence.existingClaim -}}
{{- define "windrose.persistenceClaimName" -}}
{{- $root := .root -}}
{{- $claim := .claim -}}
{{- if $claim.existingClaim -}}
{{- $claim.existingClaim -}}
{{- else -}}
{{- printf "%s-data" (include "windrose.fullname" .) -}}
{{- printf "%s-%s" (include "windrose.fullname" $root) $claim.name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}

{{- define "windrose.reservedEnvNames" -}}
- WINDROSE_CONFIG_MODE
- WINDROSE_LAUNCH_STRATEGY
- SERVER_NAME
- INVITE_CODE
- IS_PASSWORD_PROTECTED
- MAX_PLAYER_COUNT
- P2P_PROXY_ADDRESS
- USE_DIRECT_CONNECTION
- DIRECT_CONNECTION_SERVER_ADDRESS
- DIRECT_CONNECTION_SERVER_PORT
- DIRECT_CONNECTION_PROXY_ADDRESS
- WORLD_ISLAND_ID
- WORLD_NAME
- WORLD_PRESET_TYPE
- PROTON_USE_XALIA
- DISABLE_SENTRY
- WINDROSE_PATCH_IDLE_CPU
- FILES_WAIT_TIMEOUT_SECONDS
- SERVER_LAUNCH_ARGS
- NET_SERVER_MAX_TICK_RATE
- WINDROSE_SERVER_SOURCE
- DISPLAY
- WINDROSE_MANAGED_CONFIG_TEMPLATE
- WINDROSE_MANAGED_CONFIG_PASSWORD_FILE
- EXTERNAL_CONFIG
- SERVER_PASSWORD
- UI_BIND
- UI_PORT
- UI_PASSWORD
- UI_ENABLE_ADMIN_WITHOUT_PASSWORD
- UI_SERVE_STATIC
- WINDROSE_WEBHOOK_EVENTS
- WINDROSE_WEBHOOK_POLL_SECONDS
- WINDROSE_WEBHOOK_TIMEOUT
- WINDROSE_WEBHOOK_URL
- WINDROSE_DISCORD_WEBHOOK_URL
- WINDROSE_GAME_CPU_LIMIT
- WINDROSE_GAME_MEM_LIMIT
{{- end -}}

{{- define "windrose.validateEnv" -}}
{{- $env := .Values.env | default list -}}
{{- if not (kindIs "slice" $env) -}}
{{- fail "env must be a list of Kubernetes EnvVar objects" -}}
{{- end -}}
{{- $reserved := include "windrose.reservedEnvNames" . | fromYamlArray -}}
{{- $seen := dict -}}
{{- range $i, $item := $env -}}
{{- $name := get $item "name" | default "" -}}
{{- if not $name -}}
{{- fail (printf "env[%d].name is required" $i) -}}
{{- end -}}
{{- if hasKey $seen $name -}}
{{- fail (printf "env contains duplicate name %q" $name) -}}
{{- end -}}
{{- $_ := set $seen $name true -}}
{{- if has $name $reserved -}}
{{- fail (printf "env[%d].name %q is managed by the chart; use the matching chart value instead of env[]" $i $name) -}}
{{- end -}}
{{- end -}}
{{- end -}}

{{- define "windrose.validatePersistence" -}}
{{- $p := .Values.persistence | default dict -}}
{{- $legacyKeys := list "existingClaim" "size" "accessMode" "storageClassName" "subPath" -}}
{{- $hasLegacy := false -}}
{{- range $legacyKeys -}}
{{- if hasKey $p . -}}
{{- $hasLegacy = true -}}
{{- end -}}
{{- end -}}
{{- if $hasLegacy -}}
{{- $existingClaim := (get $p "existingClaim" | default "") -}}
{{- $size := (get $p "size" | default "20Gi") -}}
{{- $accessMode := (get $p "accessMode" | default "ReadWriteOnce") -}}
{{- $storageClassName := (get $p "storageClassName" | default "") -}}
{{- $subPath := (get $p "subPath" | default "steam-root") -}}
{{- $legacyIsDefault := and (eq $existingClaim "") (eq $size "20Gi") (eq $accessMode "ReadWriteOnce") (eq $storageClassName "") (eq $subPath "steam-root") -}}
{{- if not $legacyIsDefault -}}
{{- fail (printf `persistence.* scalar values were removed in this chart version.

Use the new format:

persistence:
claims:
- name: data
existingClaim: %q
size: %s
accessMode: %s
storageClassName: %q
mounts:
- name: data
mountPath: /home/steam
subPath: %s
` $existingClaim $size $accessMode $storageClassName $subPath) -}}
{{- end -}}
{{- end -}}
{{- if and (not (kindIs "slice" $p.claims)) (not $hasLegacy) -}}
{{- fail "persistence.claims must be a list of PVC claim definitions" -}}
{{- end -}}
{{- if and (not (kindIs "slice" $p.mounts)) (not $hasLegacy) -}}
{{- fail "persistence.mounts must be a list of volumeMount definitions" -}}
{{- end -}}
{{- $spec := include "windrose.persistenceSpec" . | fromYaml -}}
{{- $claims := dict -}}
{{- range $spec.claims -}}
{{- if not .name -}}
{{- fail "each persistence.claims[] entry must set name" -}}
{{- end -}}
{{- $_ := set $claims .name true -}}
{{- end -}}
{{- range $spec.mounts -}}
{{- if not .name -}}
{{- fail "each persistence.mounts[] entry must set name" -}}
{{- end -}}
{{- if not (hasKey $claims .name) -}}
{{- fail (printf "persistence.mounts[] references unknown claim name %q" .name) -}}
{{- end -}}
{{- end -}}
{{- end -}}

{{- define "windrose.persistenceSpec" -}}
{{- $p := .Values.persistence | default dict -}}
{{- if kindIs "slice" $p.claims -}}
claims:
{{ toYaml $p.claims | indent 2 }}
mounts:
{{ toYaml $p.mounts | indent 2 }}
{{- else -}}
{{- $existingClaim := (get $p "existingClaim" | default "") -}}
{{- $size := (get $p "size" | default "20Gi") -}}
{{- $accessMode := (get $p "accessMode" | default "ReadWriteOnce") -}}
{{- $storageClassName := (get $p "storageClassName" | default "") -}}
{{- $subPath := (get $p "subPath" | default "steam-root") -}}
claims:
- name: data
existingClaim: {{ $existingClaim | quote }}
size: {{ $size }}
accessMode: {{ $accessMode }}
storageClassName: {{ $storageClassName | quote }}
mounts:
- name: data
mountPath: /home/steam
subPath: {{ $subPath | quote }}
{{- end -}}
{{- end -}}

Expand Down
22 changes: 14 additions & 8 deletions helm/windrose/templates/pvc.yaml.tpl
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
{{- if not .Values.persistence.existingClaim }}
{{- include "windrose.validatePersistence" . -}}
{{- $root := . -}}
{{- $persistence := include "windrose.persistenceSpec" . | fromYaml -}}
{{- range $i, $claim := $persistence.claims }}
{{- if not $claim.existingClaim }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "windrose.pvcName" . }}
namespace: {{ .Values.namespace }}
name: {{ include "windrose.persistenceClaimName" (dict "root" $root "claim" $claim) }}
namespace: {{ $root.Values.namespace }}
labels:
{{ include "windrose.labels" . | indent 4 }}
{{ include "windrose.labels" $root | indent 4 }}
spec:
{{- if .Values.persistence.storageClassName }}
storageClassName: {{ .Values.persistence.storageClassName | quote }}
{{- if $claim.storageClassName }}
storageClassName: {{ $claim.storageClassName | quote }}
{{- end }}
accessModes:
- {{ .Values.persistence.accessMode }}
- {{ $claim.accessMode | default "ReadWriteOnce" }}
resources:
requests:
storage: {{ .Values.persistence.size }}
storage: {{ $claim.size | default "20Gi" }}
{{- end }}
{{- end }}
24 changes: 16 additions & 8 deletions helm/windrose/templates/statefulset.yaml.tpl
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{{- include "windrose.validatePersistence" . -}}
{{- include "windrose.validateEnv" . -}}
{{- $persistence := include "windrose.persistenceSpec" . | fromYaml -}}
apiVersion: apps/v1
kind: StatefulSet
metadata:
Expand Down Expand Up @@ -111,6 +114,9 @@ spec:
value: {{ .Values.serverConfig.source | default "steamcmd" | quote }}
- name: DISPLAY
value: ":{{ .Values.xvfb.display | default 99 }}"
{{- with .Values.env }}
{{ toYaml . | indent 12 }}
{{- end }}
{{- if eq .Values.serverConfig.mode "managed" }}
- name: WINDROSE_MANAGED_CONFIG_TEMPLATE
value: "/etc/windrose/managed/ServerDescription.json"
Expand All @@ -135,9 +141,7 @@ spec:
resources:
{{ toYaml .Values.resources.game | indent 12 }}
volumeMounts:
- name: data
mountPath: /home/steam
subPath: {{ .Values.persistence.subPath | quote }}
{{ toYaml $persistence.mounts | indent 12 }}
{{- if .Values.xvfb.enabled }}
- name: x11-socket
mountPath: /tmp/.X11-unix
Expand Down Expand Up @@ -253,20 +257,24 @@ spec:
value: {{ .Values.resources.game.limits.cpu | default "" | quote }}
- name: WINDROSE_GAME_MEM_LIMIT
value: {{ .Values.resources.game.limits.memory | default "" | quote }}
{{- with .Values.env }}
{{ toYaml . | indent 12 }}
{{- end }}
ports:
- name: ui
containerPort: {{ .Values.service.port }}
protocol: TCP
resources:
{{ toYaml .Values.resources.ui | indent 12 }}
volumeMounts:
- name: data
mountPath: /home/steam
subPath: {{ .Values.persistence.subPath | quote }}
{{ toYaml $persistence.mounts | indent 12 }}
volumes:
- name: data
{{- $root := . }}
{{- range $persistence.claims }}
- name: {{ .name }}
persistentVolumeClaim:
claimName: {{ include "windrose.pvcName" . }}
claimName: {{ include "windrose.persistenceClaimName" (dict "root" $root "claim" .) }}
{{- end }}
{{- if .Values.xvfb.enabled }}
- name: x11-socket
emptyDir: {}
Expand Down
21 changes: 16 additions & 5 deletions helm/windrose/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,22 @@ ui:
key: discord-webhook-url

persistence:
existingClaim: ""
size: 20Gi
accessMode: ReadWriteOnce
storageClassName: ""
subPath: steam-root
# Default is the legacy-compatible single PVC. For split runtime/state/backups
# storage, override both claims[] and mounts[] with direct mountpoints.
claims:
- name: data
existingClaim: ""
size: 20Gi
accessMode: ReadWriteOnce
storageClassName: ""
mounts:
- name: data
mountPath: /home/steam
subPath: steam-root

# Additional env vars injected into the game and UI containers. Names must be
# unique and cannot duplicate chart-managed env vars.
env: []

# hostNetwork so Windrose's UPnP / NAT-punched UDP bindings are reachable at
# the node's real interface.
Expand Down
Loading
Loading