The last open gap of #99. minecraft-server was the one chart of 21 where an erased security context removed the hardening in silence: it never got the defaultedDict helper, the StatefulSet rendered .Values.securityContext.pod straight through toYaml, and the schema typed securityContext.pod as a strict "object" without required. Measured against ci/minecraft-server/test-values.yaml on 3.0.1: --set securityContext.pod=null --set securityContext.container=null -> helm exit 0, no schema error, "securityContext: null" for BOTH the pod and the container - no runAsNonRoot, no readOnlyRootFilesystem, no dropped capabilities, in a chart with a PVC. Bitter place for it: the README lists this chart as the pilot for #68. It got its schema early and was therefore never caught by the later null-safety wave. Both halves are added, because they cover different cases. For a key WITH a chart default Helm's coalescing deletes the null before validation ever sees it, and only the defaultedDict fallback catches that; for a key WITHOUT one (server.resources.limits) the null survives into validation, where the strict "object" aborted the render. Now widened to ["object","null"]. The fallback table reproduces THIS chart's defaults, deviations included: uid/gid pinned to 1000 (the image's minecraft user - starting as it directly takes the entrypoint's non-root branch, which skips the recursive chown of /data and the write to /etc/nsswitch.conf that a read-only root filesystem would reject), fsGroupChangePolicy OnRootMismatch (a recursive chown of a 32Gi world on every start costs minutes), the explicit empty capabilities.add list, and all three probes as exec probes running the image's mc-health script - this image has no HTTP endpoint, so a probe default rebuilt as httpGet would leave the workload with no working probe at all. The startup budget stays 15s x 80. server.resources goes through the same helper while keeping its memory request deliberately unset, so it is still derived from the JVM heap. Also, the same class one level down: init.sh chowned the migrated serverpack to a hardcoded 1000:1000 while the uid it has to match was overridable. Both reads now go through the same helper, so they cannot disagree. With the chart defaults the rendered script is unchanged, so checksum/config does not move and no existing pod rolls. minecraft.resources.storage and .memory stay deliberately not null-safe: an erased value yields an invalid PVC or an unparseable quantity, which is the loud class #99 excludes. Evidence, all against ci/minecraft-server/test-values.yaml unless noted: - the reported call now renders the defaults, byte-identical to the unmodified rendering. Fallback, not rejection. - rendering with securityContext, all three probes and server.resources set to null is byte-identical to rendering the chart defaults - which is what keeps the table and values.yaml in sync instead of leaving it to discipline. - no regression: rendering before and after the change is byte-identical, on the CI values, on the chart defaults, and with curseforgeServerpack enabled (the init-container path the CI values do not exercise). Confirmed structurally too, not only by raw diff. - partial nulling, 14 keys swept one at a time. On 3.0.1 twelve of them changed the rendering silently, including capabilities (the container kept every default capability instead of dropping ALL) and requests (cpu and ephemeral-storage disappeared entirely). On 4.0.0 all fourteen render identical to the defaults. - explicit false and 0 still win over the defaults; a scalar in a mapping slot is still rejected, now naming the values path. Major bump: erasing one of these blocks used to change the rendered workload and now does not, which is a runtime behaviour change. appVersion is untouched. Refs #99
239 lines
11 KiB
YAML
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:
|
|
{{- include "minecraft-server.podSecurityContext" .Values.securityContext | nindent 8 }}
|
|
{{- if .Values.curseforgeServerpack.enabled }}
|
|
initContainers:
|
|
- name: download-serverpack
|
|
image: {{ .Values.server.initImage }}
|
|
command: [ 'sh', '/init/init.sh' ]
|
|
securityContext:
|
|
{{- include "minecraft-server.containerSecurityContext" .Values.securityContext | 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:
|
|
{{- include "minecraft-server.containerSecurityContext" .Values.securityContext | 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:
|
|
{{- include "minecraft-server.livenessProbe" .Values.livenessProbe | nindent 12 }}
|
|
readinessProbe:
|
|
{{- include "minecraft-server.readinessProbe" .Values.readinessProbe | nindent 12 }}
|
|
startupProbe:
|
|
{{- include "minecraft-server.startupProbe" .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: {}
|