Files
helm-charts/minecraft-server/templates/minecraft-server.sfs.yaml
T
jklein 58ea3b62d4 Restart the server when its config changes (3.0.0+up2026.8.2.java8)
The StatefulSet now carries two annotations over its two configuration
sources, so a change to either produces a new pod template and a controlled
restart:

  checksum/config      the rendered ConfigMap (init.sh) - complete, Helm sees
                       the whole content
  checksum/secret-ref  the rendered OnePasswordItem - the op:// item path only,
                       because the Secret's content is materialized by the
                       operator and is unknown to Helm at render time

Two separate annotations rather than one combined hash: they carry different
guarantees, and a single number would blur "covers everything" and "covers the
path but not the content" into one value that nobody can interpret in a
kubectl describe.

The operator's own operator.1password.io/auto-restart is deliberately NOT set:
it restarts Deployments only. Verified in the operator source (main, 9c3b1b9):
restartWorkloadsWithUpdatedSecrets lists nothing but appsv1.DeploymentList and
getPodTemplate has a single case for *appsv1.Deployment. On this StatefulSet
the annotation would be inert while suggesting the opposite, so the remaining
gap - a rotated value inside the vault item - is documented in values.yaml
instead.

First of the three stage-3-style publishes, so the java8 line goes first and
main ends up on java21. Only the published Java line differs between them:
appVersion 2026.8.2-java8 and the matching minecraft.version default 1.12.2.

Major bump: runtime behaviour changes. A configuration change now costs a full
server restart - 124 s measured for ftb-skies on Longhorn.

Refs #82.
2026-08-23 19:04:03 +02:00

239 lines
11 KiB
YAML

{{- include "minecraft-server.validateModpackSources" . }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
spec:
replicas: {{ include "minecraft-server.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.server.replicas) }}
selector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
serviceName: {{ .Release.Name }}-{{ .Chart.Name }}-service
template:
metadata:
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
annotations:
# Rolls the pod whenever the rendered ConfigMap changes. init.sh is
# mounted with subPath (kubelet never refreshes those in a running
# container) and the init container only reads it at pod start, so
# without this a changed download URL or working directory would stay
# inert while the release still reported success.
checksum/config: {{ include (print $.Template.BasePath "/minecraft-server.configmap.yaml") . | sha256sum }}
# Deliberately a SEPARATE annotation, and deliberately NOT a hash of the
# secret's content: the Kubernetes Secret is materialized by the
# 1Password operator, so Helm never sees what is in it. This hash covers
# the rendered OnePasswordItem, i.e. the op:// item path - repointing
# secrets.minecraftServerRef at a different item now takes effect
# instead of silently leaving the old credentials in the running pod.
# A change of the item's CONTENT inside the vault is not covered; see
# the comment on secrets in values.yaml for why that gap cannot be
# closed here and what it means operationally.
checksum/secret-ref: {{ include (print $.Template.BasePath "/minecraft-server.secret.yaml") . | sha256sum }}
spec:
automountServiceAccountToken: false
securityContext:
{{- toYaml .Values.securityContext.pod | nindent 8 }}
{{- if .Values.curseforgeServerpack.enabled }}
initContainers:
- name: download-serverpack
image: {{ .Values.server.initImage }}
command: [ 'sh', '/init/init.sh' ]
securityContext:
{{- toYaml .Values.securityContext.container | nindent 12 }}
volumeMounts:
- name: "mc-volume"
mountPath: /modpacks
subPath: mc-modpacks/
- name: "mc-volume"
mountPath: /data
subPath: mc-data/
- name: init-script
mountPath: /init/init.sh
subPath: init.sh
- name: tmp
mountPath: /tmp
{{- end }}
containers:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
image: "itzg/minecraft-server:{{ .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent
securityContext:
{{- toYaml .Values.securityContext.container | nindent 12 }}
ports:
- name: mc-port
containerPort: {{ .Values.server.port }}
protocol: TCP
{{- if .Values.server.rcon.enabled }}
- name: minecraft-rcon
containerPort: {{ .Values.server.rcon.port }}
protocol: TCP
{{- end }}
- name: metrics
containerPort: 19565
protocol: TCP
env:
- name: EULA
value: "true"
- name: DUMP_SERVER_PROPERTIES
value: "true"
- name: MOTD
value: "{{ .Values.server.motd }}"
- name: DIFFICULTY
value: {{ .Values.minecraft.difficulty }}
- name: SERVER_PORT
value: "{{ .Values.server.port }}"
# ENABLE_WHITELIST is set in both positions on purpose - it is the
# only variable that can switch the whitelist OFF again. The image
# enables the whitelist when WHITELIST/WHITELIST_FILE is non-empty
# OR ENABLE_WHITELIST is true, and disables it (white-list=false)
# when ENABLE_WHITELIST is false, rewriting an existing
# server.properties on the volume. Because a non-empty WHITELIST
# wins over ENABLE_WHITELIST=false, players must stay unset while
# disabled - hence the guard below rather than emitting both.
- name: ENABLE_WHITELIST
value: "{{ .Values.minecraft.whitelist.enabled }}"
# enforce-whitelist is mapped from ENFORCE_WHITELIST in the image's
# property-definitions.json. The image only ever sets it to true (in
# its enable branch) and never resets it, so without this line a
# server that once had the whitelist on keeps enforce-whitelist=true
# in its persisted server.properties after switching off. It is
# inert while white-list=false, but leaving stale state behind is
# exactly what this chart version fixes, so it follows the switch.
- name: ENFORCE_WHITELIST
value: "{{ .Values.minecraft.whitelist.enabled }}"
{{- if .Values.minecraft.whitelist.enabled }}
- name: EXISTING_WHITELIST_FILE
value: {{ .Values.minecraft.whitelist.overridePolicy }}
{{- with .Values.minecraft.whitelist.players }}
- name: WHITELIST
value: "{{ . }}"
{{- end }}
{{- end }}
{{- with .Values.minecraft.management.opPlayers }}
- name: OPS
value: "{{ . }}"
{{- end }}
- name: EXISTING_OPS_FILE
value: "SYNCHRONIZE"
- name: MAX_PLAYERS
value: "{{ .Values.minecraft.management.maxPlayers }}"
- name: FORCE_GAMEMODE
value: "{{ .Values.minecraft.management.forceGameMode }}"
- name: SNOOPER_ENABLED
value: "false"
- name: MODE
value: "{{ .Values.minecraft.management.gameMode }}"
- name: LEVEL
value: "{{ .Values.minecraft.management.worldSaveName }}"
- name: SERVER_NAME
value: "{{ .Values.minecraft.management.serverName }}"
- name: VERSION
value: "{{ .Values.minecraft.version }}"
- name: ENABLE_RCON
value: "{{ .Values.server.rcon.enabled }}"
{{- if .Values.server.rcon.enabled }}
- name: RCON_PORT
value: "{{ .Values.server.rcon.port }}"
- name: RCON_PASSWORD
valueFrom:
secretKeyRef:
key: rcon-password
name: {{ .Release.Name }}-{{ .Chart.Name }}-secret
{{- end }}
- name: ENABLE_AUTOPAUSE
value: "false"
- name: MEMORY
value: "{{ .Values.minecraft.resources.memory }}G"
{{- if .Values.curseforge.enabled }}
- name: TYPE
value: "AUTO_CURSEFORGE"
- name: CF_API_KEY
valueFrom:
secretKeyRef:
key: curseforge-api-key
name: {{ .Release.Name }}-{{ .Chart.Name }}-secret
- name: CF_SLUG
value: "{{ .Values.curseforge.modpack.slug }}"
- name: CF_FILE_ID
value: "{{ .Values.curseforge.modpack.versionId }}"
- name: CF_EXCLUDE_MODS
value: "{{ .Values.curseforge.modpack.exclude }}"
- name: CF_OVERRIDES_EXCLUSIONS
value: "{{ .Values.curseforge.modpack.excludeOverride }}"
- name: CF_FORCE_INCLUDE_MODS
value: "{{ .Values.curseforge.modpack.include }}"
- name: CF_FORCE_SYNCHRONIZE
value: "{{ .Values.curseforge.forceReload }}"
{{- end }}
{{- if .Values.curseforgeServerpack.enabled }}
- name: TYPE
value: "CURSEFORGE"
- name: CF_SERVER_MOD
value: "/modpacks/serverpack.zip"
- name: CF_BASE_DIR
value: "{{ .Values.curseforgeServerpack.workingDirectory }}"
{{- end }}
{{- if .Values.ftb.enabled }}
- name: TYPE
value: FTBA
- name: FTB_MODPACK_ID
value: "{{ .Values.ftb.modpackId }}"
- name: FTB_MODPACK_VERSION_ID
value: "{{ .Values.ftb.versionId }}"
{{- end }}
{{- 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 }}
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
startupProbe:
{{- toYaml .Values.startupProbe | nindent 12 }}
resources:
{{- include "minecraft-server.serverResources" . | nindent 12 }}
volumeMounts:
- name: "mc-volume"
mountPath: /data
subPath: mc-data/
- name: "mc-volume"
mountPath: /modpacks
subPath: mc-modpacks/
# Writable path required by readOnlyRootFilesystem: the image writes
# its server-icon scratch file and mc-server-runner's console named
# pipe to /tmp. Everything else goes to the PVC at /data.
- name: tmp
mountPath: /tmp
volumes:
- name: "mc-volume"
persistentVolumeClaim:
# Deliberate exception from the naming scheme: renaming the PVC
# would make Kubernetes provision a new, empty volume and orphan
# the existing world (see README, naming exceptions).
claimName: {{ .Release.Name }}-mc-volume-claim
- name: init-script
configMap:
name: {{ .Release.Name }}-{{ .Chart.Name }}-configmap
- name: tmp
emptyDir: {}