diff --git a/satisfactory/Chart.yaml b/satisfactory/Chart.yaml index d9a2dcd..2530dd1 100644 --- a/satisfactory/Chart.yaml +++ b/satisfactory/Chart.yaml @@ -3,7 +3,7 @@ name: satisfactory description: A Helm chart to deploy a Satisfactory Gameserver on Kubernetes icon: https://raw.githubusercontent.com/wolveix/satisfactory-server/main/.github/logo.png type: application -version: 3.1.1+upv1.9.10 +version: 4.0.0+upv1.9.10 appVersion: "v1.9.10" maintainers: - name: jklein \ No newline at end of file diff --git a/satisfactory/templates/satisfactory-server.sfs.yaml b/satisfactory/templates/satisfactory-server.sfs.yaml deleted file mode 100644 index 6cf9e55..0000000 --- a/satisfactory/templates/satisfactory-server.sfs.yaml +++ /dev/null @@ -1,91 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: "{{ .Release.Name }}-{{ .Chart.Name }}-sf-server" -spec: - selector: - matchLabels: - app: "{{ .Release.Name }}-{{ .Chart.Name }}-sf-server" - serviceName: "{{ .Release.Name }}-{{ .Chart.Name }}-sf-service" - replicas: {{ include "satisfactory.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.server.replicas) }} - template: - metadata: - labels: - app: "{{ .Release.Name }}-{{ .Chart.Name }}-sf-server" - spec: - automountServiceAccountToken: false - containers: - - name: "{{ .Release.Name }}-{{ .Chart.Name }}-sf-server" - image: "ghcr.io/wolveix/satisfactory-server:{{ .Chart.AppVersion }}" - env: - - name: DEBUG - value: "{{ .Values.dev.debug }}" - - name: MAXPLAYERS - value: "{{ .Values.game.maxPlayers }}" - - name: PGID - value: "1000" - - name: PUID - value: "1000" - - name: SKIPUPDATE - value: "false" - - name: STEAMBETA - value: "{{ .Values.game.experimental }}" - - name: DISABLESEASONALEVENTS - value: "{{ .Values.game.seasonalEvents }}" - - name: SERVERGAMEPORT - value: "{{ .Values.server.gamePort }}" - - name: SERVERMESSAGINGPORT - value: "{{ .Values.server.messagingPort }}" - ports: - - name: "api" - containerPort: {{ .Values.server.gamePort }} - protocol: TCP - - name: "game" - containerPort: {{ .Values.server.gamePort }} - protocol: UDP - - name: "messaging" - containerPort: {{ .Values.server.messagingPort }} - protocol: TCP - livenessProbe: - exec: - command: - - "/bin/sh" - - "-c" - - "/home/steam/healthcheck.sh" - failureThreshold: 4 - successThreshold: 1 - periodSeconds: 30 - timeoutSeconds: 10 - readinessProbe: - tcpSocket: - port: api - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 - # The startup probe gates liveness/readiness. The server downloads / - # updates the game files via Steam on startup, which can take a long - # time: budget 30s * 40 = 20 minutes. - startupProbe: - exec: - command: - - "/bin/sh" - - "-c" - - "/home/steam/healthcheck.sh" - periodSeconds: 30 - timeoutSeconds: 10 - failureThreshold: 40 - resources: - {{- include "satisfactory.resources" .Values.server.resources | nindent 12 }} - volumeMounts: - - name: satisfactory-volume - mountPath: /config - subPath: config/ - - name: satisfactory-volume - mountPath: /config/gamefiles - subPath: gameFiles/ - securityContext: - allowPrivilegeEscalation: false - volumes: - - name: satisfactory-volume - persistentVolumeClaim: - claimName: {{ .Release.Name }}-{{ .Chart.Name }}-vc diff --git a/satisfactory/templates/satisfactory.service.yaml b/satisfactory/templates/satisfactory.service.yaml index 928401f..e9d3b2f 100644 --- a/satisfactory/templates/satisfactory.service.yaml +++ b/satisfactory/templates/satisfactory.service.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: Service metadata: - name: "{{ .Release.Name }}-{{ .Chart.Name }}-sf-service" + name: {{ .Release.Name }}-{{ .Chart.Name }}-service labels: - app: "{{ .Release.Name }}-{{ .Chart.Name }}-sf-service" + app: {{ .Release.Name }}-{{ .Chart.Name }}-service spec: type: NodePort ports: @@ -23,4 +23,4 @@ spec: nodePort: {{ .Values.server.messagingPort }} targetPort: {{ .Values.server.messagingPort }} selector: - app: "{{ .Release.Name }}-{{ .Chart.Name }}-sf-server" \ No newline at end of file + app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs diff --git a/satisfactory/templates/satisfactory.sfs.yaml b/satisfactory/templates/satisfactory.sfs.yaml new file mode 100644 index 0000000..70c080b --- /dev/null +++ b/satisfactory/templates/satisfactory.sfs.yaml @@ -0,0 +1,117 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs + labels: + app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs +spec: + selector: + matchLabels: + app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs + serviceName: {{ .Release.Name }}-{{ .Chart.Name }}-service + replicas: {{ include "satisfactory.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.server.replicas) }} + template: + metadata: + labels: + app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs + spec: + automountServiceAccountToken: false + securityContext: + {{- toYaml .Values.securityContext.pod | nindent 8 }} + containers: + - name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs + image: "ghcr.io/wolveix/satisfactory-server:{{ .Chart.AppVersion }}" + env: + - name: DEBUG + value: "{{ .Values.dev.debug }}" + - name: MAXPLAYERS + value: "{{ .Values.game.maxPlayers }}" + # PUID/PGID are only used by the image's root entrypoint path; + # with the default non-root security context they are ignored. + - name: PGID + value: "1000" + - name: PUID + value: "1000" + - name: SKIPUPDATE + value: "false" + - name: STEAMBETA + value: "{{ .Values.game.experimental }}" + - name: DISABLESEASONALEVENTS + value: "{{ .Values.game.seasonalEvents }}" + - name: SERVERGAMEPORT + value: "{{ .Values.server.gamePort }}" + - name: SERVERMESSAGINGPORT + value: "{{ .Values.server.messagingPort }}" + {{- range $value := .Values.env }} + {{- if $value.value }} + - name: {{ $value.name }} + value: {{ tpl $value.value $ }} + {{- else if $value.valueFrom }} + {{- if $value.valueFrom.secretKeyRef }} + - name: {{ $value.name }} + valueFrom: + secretKeyRef: + name: {{ tpl $value.valueFrom.secretKeyRef.name $ }} + key: {{ tpl $value.valueFrom.secretKeyRef.key $ }} + {{- else if $value.valueFrom.configMapKeyRef }} + - name: {{ $value.name }} + valueFrom: + configMapKeyRef: + name: {{ tpl $value.valueFrom.configMapKeyRef.name $ }} + key: {{ tpl $value.valueFrom.configMapKeyRef.key $ }} + {{- end }} + {{- end }} + {{- end }} + ports: + - name: "api" + containerPort: {{ .Values.server.gamePort }} + protocol: TCP + - name: "game" + containerPort: {{ .Values.server.gamePort }} + protocol: UDP + - name: "messaging" + containerPort: {{ .Values.server.messagingPort }} + protocol: TCP + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + startupProbe: + {{- toYaml .Values.startupProbe | nindent 12 }} + resources: + {{- include "satisfactory.resources" .Values.server.resources | nindent 12 }} + volumeMounts: + - name: satisfactory-volume + mountPath: /config + subPath: config/ + - name: satisfactory-volume + mountPath: /config/gamefiles + subPath: gameFiles/ + # Writable paths required by readOnlyRootFilesystem: the steam + # user's home dot-dirs (steamcmd bootstraps itself into + # ~/.local/share/Steam, keeps state under ~/.steam, and the game + # saves symlink lives under ~/.config) and /tmp (crash dumps). + # Everything else is written to the PVC at /config. + - name: steam-home + mountPath: /home/steam/.steam + subPath: dot-steam + - name: steam-home + mountPath: /home/steam/.config + subPath: dot-config + - name: steam-home + mountPath: /home/steam/.local + subPath: dot-local + - name: tmp + mountPath: /tmp + securityContext: + {{- toYaml .Values.securityContext.container | nindent 12 }} + volumes: + - name: satisfactory-volume + persistentVolumeClaim: + # Deliberate exception from the naming scheme: renaming the PVC + # would detach existing data (see README, naming exceptions). + claimName: {{ .Release.Name }}-{{ .Chart.Name }}-vc + - name: steam-home + emptyDir: {} + - name: tmp + emptyDir: {} diff --git a/satisfactory/values.yaml b/satisfactory/values.yaml index 247dcec..8c5d7aa 100644 --- a/satisfactory/values.yaml +++ b/satisfactory/values.yaml @@ -30,3 +30,82 @@ server: memory: "4Gi" cpu: "1" ephemeral-storage: "5Gi" + +# Extra environment variables for the server container, appended to the ones +# the chart always sets (DEBUG, MAXPLAYERS, PGID, PUID, SKIPUPDATE, STEAMBETA, +# DISABLESEASONALEVENTS, SERVERGAMEPORT, SERVERMESSAGINGPORT). Values are +# rendered through tpl and support the value, secretKeyRef and configMapKeyRef +# forms: +# - name: SOME_ENV +# value: "{{ .Release.Name }}" +# - name: FROM_SECRET +# valueFrom: +# secretKeyRef: +# name: "some-secret" +# key: "someKey" +# - name: FROM_CONFIGMAP +# valueFrom: +# configMapKeyRef: +# name: "some-configmap" +# key: "someKey" +env: [] + +# The image's healthcheck script queries the server's HTTPS API on the game +# port (SERVERGAMEPORT), so liveness/startup keep working under port overrides. +livenessProbe: + exec: + command: + - "/bin/sh" + - "-c" + - "/home/steam/healthcheck.sh" + failureThreshold: 4 + successThreshold: 1 + periodSeconds: 30 + timeoutSeconds: 10 +readinessProbe: + tcpSocket: + port: api + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 +# The startup probe gates liveness/readiness. The server downloads / updates +# the game files via Steam on startup, which can take a long time: +# budget 30s * 40 = 20 minutes. +startupProbe: + exec: + command: + - "/bin/sh" + - "-c" + - "/home/steam/healthcheck.sh" + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 40 + +securityContext: + pod: + runAsNonRoot: true + # The wolveix/satisfactory-server image supports rootless operation only + # as its built-in "steam" user (uid/gid 1000) - the entrypoint rejects any + # other non-root uid/gid. The PUID/PGID env vars only apply to the root + # entrypoint path and are ignored when running non-root. + runAsUser: 1000 + runAsGroup: 1000 + # fsGroup keeps the data volume writable for the steam user. + # OnRootMismatch (instead of Always) skips the recursive chown/chmod of + # the multi-GB game files on every pod start; volumes provisioned by chart + # versions < 4.0.0 were already chowned to 1000:1000 by the image's root + # entrypoint. + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch + container: + allowPrivilegeEscalation: false + # Works because the chart mounts emptyDirs at /tmp and the steam user's + # ~/.steam, ~/.config and ~/.local (steamcmd bootstraps itself into + # ~/.local/share/Steam on start); all other writes go to the PVC at + # /config. + readOnlyRootFilesystem: true + runAsNonRoot: true + capabilities: + drop: + - ALL + add: []