Closes the three-step round that carried the #99 null-safety to every Java line. Pure restoration of the directory to the newest line: version: 4.0.0+up2026.8.2.java17 -> 4.0.1+up2026.8.2.java21 appVersion: "2026.8.2-java17" -> "2026.8.2-java21" minecraft.version: "1.20.1" -> "1.21.1" Nothing changes in content, and that is not a claim but a measurement. Against the java21 state published from #156, the ENTIRE chart tree differs in exactly one line - the chart version - and .Chart.Version is not read by any template, so the rendering is byte-identical: 4.0.0+up2026.8.2.java21 (published) -> this chart: file tree: 1 differing line, Chart.yaml version rendering: byte-identical (CI values, serverpack init container enabled) main (java17) -> this chart: exactly TWO differences, both the line selection itself - image itzg/minecraft-server:2026.8.2-java17 -> :2026.8.2-java21 and env VERSION 1.20.1 -> 1.21.1. Why 4.0.1 and not 4.0.0: 4.0.0+up2026.8.2.java21 is already in the registry from #156, and helm push overwrites an existing OCI tag without complaint. The version check in validate-charts.yml does not catch this - it compares only against github.base_ref, where the version would read java17, so a republished 4.0.0 would pass the gate and silently overwrite the published artifact. Tracked as #160. What this round achieves, measured across the three published line states: templates/ and values.schema.json are byte-identical on java8 (f7047b8), java17 (2b22ce5) and java21 (this chart); values.yaml differs only in minecraft.version and the comment naming the appVersion; and all three now carry initImage alpine:3.24, so the bump that had reached only java21 is no longer missing on the other two. All three lines therefore run the same hardening, the same three exec probes, the same resources handling and the same null-safe fallbacks. Left untouched on purpose: the float64 quantity finding (#157) and seccompProfile (#161) - the latter has to be pulled through all three lines and has its own unknowns in gosu and the serverpack init container. Refs #99
257 lines
12 KiB
YAML
257 lines
12 KiB
YAML
# Scale the whole chart down without uninstalling it: scaleDown: true renders
|
|
# every Deployment/StatefulSet with replicas: 0 (and suspends CronJobs) and
|
|
# takes precedence over any per-workload replicas value. PVCs, Secrets and
|
|
# Services stay in place; scaleDown: false brings the workloads back up.
|
|
scaleDown: false
|
|
|
|
secrets:
|
|
# op:// item path for the 1Password operator. Only needs to carry a
|
|
# "curseforge-api-key" key when curseforge.enabled is true and an
|
|
# "rcon-password" key when server.rcon.enabled is true.
|
|
#
|
|
# Documented limit (chart 3.0.0): changing this path rolls the pod (the
|
|
# StatefulSet hashes the rendered OnePasswordItem into checksum/secret-ref),
|
|
# but changing the item's CONTENT in the vault does not. The operator updates
|
|
# the Kubernetes Secret, while the running pod keeps the values it read at
|
|
# startup - the keys are consumed as environment variables, which Kubernetes
|
|
# never refreshes in a running container.
|
|
#
|
|
# The operator's own "operator.1password.io/auto-restart: true" annotation
|
|
# does NOT close this gap here, and is therefore deliberately not set: it only
|
|
# restarts Deployments. Verified in the operator source (main, 9c3b1b9,
|
|
# 2026-08-11): restartWorkloadsWithUpdatedSecrets lists nothing but
|
|
# appsv1.DeploymentList, and getPodTemplate has a single case for
|
|
# *appsv1.Deployment and returns "unsupported type" for everything else. This
|
|
# chart is a StatefulSet, so the annotation would do nothing while suggesting
|
|
# the opposite.
|
|
#
|
|
# So after rotating rcon-password or curseforge-api-key in the vault, restart
|
|
# the workload by hand (kubectl rollout restart statefulset/...) - and expect
|
|
# the full startup cost of ~124 s measured for ftb-skies on Longhorn.
|
|
minecraftServerRef: null
|
|
|
|
server:
|
|
# Replica count for this workload; 0 is allowed and scales it down.
|
|
# The chart-global scaleDown flag takes precedence.
|
|
replicas: 1
|
|
|
|
# Port the Minecraft server listens on inside the container. It is passed to
|
|
# the image as SERVER_PORT, used as the containerPort and as the Service's
|
|
# target, and the probes go through the image's mc-health script, which reads
|
|
# SERVER_PORT too - so an override stays consistent everywhere.
|
|
port: 25565
|
|
|
|
service:
|
|
# ClusterIP is the default because routing to these servers is done by the
|
|
# static mappings of the minecraft-router chart, which reaches the server
|
|
# over its in-cluster DNS name. Set NodePort to expose the server directly
|
|
# on a node port instead (then outwardPort applies).
|
|
# NOTE: before chart 1.0.0 this was implied by "mcRouterIntegration.enabled",
|
|
# which also added a misleading mc-router auto-discovery annotation. The
|
|
# annotation is gone; only the service type remains, now named for what it
|
|
# actually does.
|
|
type: ClusterIP
|
|
# Port the Service exposes in the cluster.
|
|
port: 25565
|
|
# Node port used only when type is NodePort. Must be inside the cluster's
|
|
# node-port range (30000-32767 by default).
|
|
outwardPort: 30565
|
|
|
|
# The \u00A7 sequences are Java-properties escapes for the Minecraft colour
|
|
# code character and must stay literal backslash sequences - server.properties
|
|
# is a Java properties file and unescapes them itself.
|
|
motd: 'Running \u00A72\u00A7o%MODPACK_NAME% \u00A70version \u00A7r%env:MODPACK_VERSION%'
|
|
|
|
rcon:
|
|
# RCON is off by default. Only when this is true does the chart set
|
|
# ENABLE_RCON, expose the RCON container port and wire RCON_PASSWORD from
|
|
# the secret - before chart 1.0.0 the password was pulled from the secret
|
|
# unconditionally although RCON was hardcoded off, which made the secret
|
|
# key mandatory for a switched-off feature.
|
|
enabled: false
|
|
port: 25575
|
|
|
|
# Init container that downloads and unpacks a CurseForge server pack; only
|
|
# used when curseforgeServerpack.enabled is true. Pinned per repo convention
|
|
# (auxiliary images are never unpinned): before chart 1.0.0 this was a bare
|
|
# "alpine", i.e. an implicit :latest from Docker Hub.
|
|
initImage: alpine:3.24
|
|
|
|
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"
|
|
# (CPU is compressible - throttling is preferable to an OOM kill).
|
|
limitFactor: 3
|
|
requests:
|
|
cpu: "2"
|
|
ephemeral-storage: "100Mi"
|
|
# Deliberately unset: when no memory request is given, the chart derives
|
|
# it from minecraft.resources.memory (the JVM heap) so that the heap and
|
|
# the container's memory request/limit can never drift apart. With the
|
|
# default heap of 4G this yields a 4Gi request and, via limitFactor 3, a
|
|
# 12Gi limit - exactly the values that were hardcoded before chart 1.0.0.
|
|
# Set it explicitly to decouple the two.
|
|
memory: null
|
|
|
|
# Extra environment variables for the server container, appended to the ones
|
|
# the chart always sets. 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: []
|
|
|
|
# All three probes run the image's own mc-health script, which performs a real
|
|
# Minecraft server-list ping against SERVER_PORT via mc-monitor - so "Ready"
|
|
# means the server answers players, not merely that the container process is
|
|
# alive. (Before chart 1.0.0 this chart had no probes at all, which is why a
|
|
# pod counted as Ready the moment the container started.)
|
|
livenessProbe:
|
|
exec:
|
|
command: ["mc-health"]
|
|
periodSeconds: 30
|
|
timeoutSeconds: 10
|
|
failureThreshold: 3
|
|
readinessProbe:
|
|
exec:
|
|
command: ["mc-health"]
|
|
periodSeconds: 15
|
|
timeoutSeconds: 10
|
|
failureThreshold: 3
|
|
# The startup probe gates liveness and readiness, so its budget has to cover
|
|
# the slowest legitimate start: on a fresh volume the image downloads the
|
|
# server jar and, for FTB/CurseForge modpacks, the whole modpack before the
|
|
# world even begins to load. Budget 15s * 80 = 20 minutes; the liveness probe
|
|
# only starts counting afterwards, so a slow start can no longer be killed.
|
|
startupProbe:
|
|
exec:
|
|
command: ["mc-health"]
|
|
periodSeconds: 15
|
|
timeoutSeconds: 10
|
|
failureThreshold: 80
|
|
|
|
securityContext:
|
|
pod:
|
|
runAsNonRoot: true
|
|
# uid/gid 1000 is the image's "minecraft" user. The image's entrypoint
|
|
# normally starts as root and drops to this user via gosu; starting as the
|
|
# user directly makes it take the non-root branch, which additionally skips
|
|
# the entrypoint's recursive chown of /data and its write to
|
|
# /etc/nsswitch.conf - the latter would fail on a read-only root filesystem.
|
|
runAsUser: 1000
|
|
runAsGroup: 1000
|
|
# fsGroup keeps the world volume writable for the minecraft user.
|
|
# OnRootMismatch (instead of Always) avoids a recursive chown of the whole
|
|
# world on every single pod start - on a 32Gi world that is minutes of
|
|
# startup time. It still runs once if the volume root does not already
|
|
# carry the group.
|
|
fsGroup: 1000
|
|
fsGroupChangePolicy: OnRootMismatch
|
|
container:
|
|
allowPrivilegeEscalation: false
|
|
# Works because the chart mounts an emptyDir at /tmp - the image writes the
|
|
# server-icon scratch file and mc-server-runner's console named pipe there.
|
|
# Everything else the server writes goes to the PVC at /data.
|
|
readOnlyRootFilesystem: true
|
|
runAsNonRoot: true
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
add: []
|
|
|
|
minecraft:
|
|
# Valid values are: peaceful, easy, normal, and hard
|
|
difficulty: "easy"
|
|
whitelist:
|
|
# Boolean, wired to the image's ENABLE_WHITELIST in BOTH positions since
|
|
# chart 2.0.0 - which is what makes "false" actually switch the whitelist
|
|
# off. server.properties lives on the PVC, so before 2.0.0 a release that
|
|
# had once written white-list=true kept it forever: the chart only ever
|
|
# emitted whitelist variables while enabled was true and had no way to say
|
|
# "off". Now enabled: false emits ENABLE_WHITELIST=false, and the image
|
|
# rewrites white-list=false into the existing file.
|
|
#
|
|
# >>> enabled: true WITHOUT players locks the server down completely. <<<
|
|
# That is the image's own semantics and it is intended: white-list=true with
|
|
# an empty whitelist.json means nobody can join - not even the ops. Set
|
|
# players whenever this is turned on, or leave it at false.
|
|
#
|
|
# The default is false, matching both the image and vanilla Minecraft, which
|
|
# start with white-list=false. (Before chart 2.0.0 the default was true with
|
|
# players: null; that combination did nothing back then, because an empty
|
|
# WHITELIST never enabled anything - under the new semantics it would be a
|
|
# locked-out server, so the default is flipped.)
|
|
enabled: false
|
|
# How the image treats an ALREADY EXISTING whitelist.json on the volume:
|
|
# SKIP leaves it alone, MERGE adds players to it, SYNCHRONIZE makes it match
|
|
# players exactly (removing everyone else). Only applies while enabled.
|
|
overridePolicy: "MERGE"
|
|
# Comma-separated List of Players. Only passed to the image (as WHITELIST)
|
|
# while enabled is true - a non-empty WHITELIST turns the whitelist on in
|
|
# the image regardless of ENABLE_WHITELIST, so leaving it out is what keeps
|
|
# enabled: false switched off even for a release that still lists players.
|
|
players: null
|
|
management:
|
|
# Comma-separated List of Players. The image resolves every name against
|
|
# the external Playerdb API and aborts startup if one does not resolve.
|
|
opPlayers: null
|
|
maxPlayers: 10
|
|
forceGameMode: true
|
|
# Valid values are: creative, survival, adventure, spectator (only for Minecraft 1.8 or later)
|
|
gameMode: survival
|
|
worldSaveName: world
|
|
serverName: mc-server
|
|
resources:
|
|
storage: "32Gi"
|
|
# Storage class of the world PVC. Was hardcoded to "longhorn" before chart
|
|
# 1.0.0. Changing it on an existing release has no effect on the already
|
|
# bound volume - storageClassName is immutable once the PVC exists.
|
|
storageClassName: longhorn
|
|
# JVM heap in gigabytes (passed to the image as MEMORY). Also drives the
|
|
# container's memory request/limit unless server.resources.requests.memory
|
|
# is set explicitly.
|
|
memory: "4"
|
|
# Minecraft version to run. Pinned rather than "LATEST", which would silently
|
|
# drift to a new release on every restart.
|
|
# This value must match the Java line of the chart version: the image tag
|
|
# comes from .Chart.AppVersion, so the JVM is fixed per published chart
|
|
# version. Java 8 runs Minecraft up to 1.16.5, 1.18-1.20.4 need Java 17 and
|
|
# 1.20.5+ need Java 21. This chart publishes appVersion "2026.8.2-java21".
|
|
version: "1.21.1"
|
|
|
|
# curseforge, ftb and curseforgeServerpack are mutually exclusive - enabling
|
|
# more than one is rejected by the chart (before 1.0.0 it silently rendered a
|
|
# duplicate TYPE env entry and the last one won).
|
|
curseforge:
|
|
enabled: false
|
|
forceReload: false
|
|
modpack:
|
|
slug: null
|
|
versionId: null
|
|
exclude: null
|
|
excludeOverride: null
|
|
include: null
|
|
ftb:
|
|
enabled: false
|
|
modpackId: null
|
|
versionId: null
|
|
curseforgeServerpack:
|
|
enabled: false
|
|
# Both values below are rendered into the init.sh ConfigMap. Since chart 3.0.0
|
|
# a change here rolls the pod (checksum/config on the StatefulSet), which is
|
|
# what makes it take effect at all - init.sh is mounted with subPath and only
|
|
# read at pod start. The restart is not free: ~124 s measured for ftb-skies on
|
|
# Longhorn.
|
|
downloadUrl: null
|
|
workingDirectory: '/data/serverpack'
|