Compare commits
7
Commits
757e4bea6f
...
f49bfa4af8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f49bfa4af8 | ||
|
|
9c0039685b
|
||
|
|
63662c8ea8 | ||
|
|
485f4fa52b
|
||
|
|
b1709e6336 | ||
|
|
f761767630
|
||
|
|
f8acdfdfe1
|
No files matched your search
@@ -35,7 +35,7 @@ helm template <chart-name> --set ingress.domain=example.com --set ingress.cluste
|
||||
|
||||
## Creating a new chart
|
||||
|
||||
Copy `template-chart/` and replace every `xxx` placeholder — in file names (`templates/xxx.deployment.yaml` → `templates/<app>.deployment.yaml`), in `Chart.yaml`, in the `values.yaml` keys and their `.Values.xxx.*` references inside templates, and in `values.schema.json` (both the values paths and the `xxx.` helper prefixes). The schema and the null-safe fallback helpers come with the template but their content is chart-specific — adapt them along the five points of the README section "Values that must not lie", and validate the result against the chart's real bundle values, not only against its defaults. Then wire up CI:
|
||||
Copy `template-chart/` and replace every `xxx` placeholder — in file names (`templates/xxx.deployment.yaml` → `templates/<app>.deployment.yaml`), in `Chart.yaml`, in the `values.yaml` keys and their `.Values.xxx.*` references inside templates, and in `values.schema.json` (both the values paths and the `xxx.` helper prefixes). The schema and the null-safe fallback helpers come with the template but their content is chart-specific — adapt them along the six points of the README section "Values that must not lie", and validate the result against the chart's real bundle values, not only against its defaults. Then wire up CI:
|
||||
|
||||
1. Add `.github/workflows/publish-<chart-name>.yml` (copy an existing one, e.g. `publish-outline.yml`) — it triggers on pushes to `main` touching `<chart-name>/**` and calls the reusable `publish-helm-chart.yml`.
|
||||
2. Add the chart name to the `chart-name` choice list in `.github/workflows/change-app-version.yml`.
|
||||
|
||||
@@ -15,7 +15,7 @@ Collection of some self-made helm-charts. Each top-level directory is a standalo
|
||||
| `homeassistant` | Home Assistant | StatefulSet; the only chart carrying Helm tests (`templates/tests/`) |
|
||||
| `matrix-synapse` | Matrix Synapse | StatefulSet plus an nginx delegation Deployment/Service |
|
||||
| `minecraft-router` | itzg/mc-router | Deployment + routing ConfigMap + NodePort Service (TCP only, no ingress); `mappings` is a required value |
|
||||
| `minecraft-server` | itzg/minecraft-server | StatefulSet + PVC + Service + init-script ConfigMap (no ingress); service type is a value (`ClusterIP` by default, routed via `minecraft-router`). Published once per Java line — the image tag comes from `.Chart.AppVersion`, so the Java line is chosen by picking the chart version, and `minecraft.version` ships a default matching that line. Since chart 2.0.0 the tag is an **immutable dated upstream release** (`appVersion: <release>-java8` / `-java17` / `-java21` / `-java25`, e.g. `2026.8.2-java8`) instead of the continuously rebuilt rolling line tag. Pilot for the strict `values.schema.json` (#68), which is now binding for every chart |
|
||||
| `minecraft-server` | itzg/minecraft-server | StatefulSet + PVC + Service + init-script ConfigMap (no ingress); service type is a value (`ClusterIP` by default, routed via `minecraft-router`). **Only the newest Java line is maintained** (chart 5.0.0, #158): the directory carries one line, older ones stay published as frozen artifacts (java8/java17 at `4.0.0`, java21 at `4.1.0`) and receive no further chart changes — publishing once per line out of one directory is what made the lines drift. To run an older JVM with the current chart, set `image.tagOverride`; the chart then checks that `minecraft.version` fits the Java line of the **effective** tag and fails the render with both values named. Two cases pass unchecked on purpose: a tag without a `java<N>` part and a `minecraft.version` that is not a plain numeric `x[.y[.z]]`. `image.tagOverride` is deliberately outside Renovate's reach — `renovate.json5` tracks `appVersion` only. Since chart 2.0.0 the tag is an **immutable dated upstream release** (`appVersion: <release>-java8` / `-java17` / `-java21` / `-java25`, e.g. `2026.8.2-java8`) instead of the continuously rebuilt rolling line tag. Pilot for the strict `values.schema.json` (#68), which is now binding for every chart |
|
||||
| `outline` | Outline | StatefulSet, separate DB/Redis secrets |
|
||||
| `paperless-ngx` | Paperless-ngx | Largest chart: consume CronJob, SMB-connector PV/PVC, pre/post-consumption scripts ConfigMap |
|
||||
| `patchmon` | PatchMon | Two Deployments (frontend + backend), OIDC secret |
|
||||
@@ -141,7 +141,7 @@ If the dependency dashboard reports `Failed to look up github-releases package
|
||||
|
||||
## Creating a new chart
|
||||
|
||||
1. Copy `template-chart/` to `<chart-name>/` and replace every `xxx` placeholder: template file names (`xxx.deployment.yaml` → `<app>.deployment.yaml`), `Chart.yaml`, the `values.yaml` keys, the `.Values.xxx.*` references inside the templates, **and both the `xxx.` helper prefixes and the values paths in `values.schema.json`**. Then walk the five points in "Values that must not lie" below — the schema and the null-safe fallbacks are copied with the template, but their content is chart-specific and wrong until you adapt it. The same goes for `templates/NOTES.txt`: it comes with the template as a worked example and is placeholder text until you replace it with what an operator of *your* app needs after the install (see "Everything else" below); its leading `{{/* … */}}` block explains the bar and is meant to be deleted.
|
||||
1. Copy `template-chart/` to `<chart-name>/` and replace every `xxx` placeholder: template file names (`xxx.deployment.yaml` → `<app>.deployment.yaml`), `Chart.yaml`, the `values.yaml` keys, the `.Values.xxx.*` references inside the templates, **and both the `xxx.` helper prefixes and the values paths in `values.schema.json`**. Then walk the six points in "Values that must not lie" below — the schema and the null-safe fallbacks are copied with the template, but their content is chart-specific and wrong until you adapt it. The same goes for `templates/NOTES.txt`: it comes with the template as a worked example and is placeholder text until you replace it with what an operator of *your* app needs after the install (see "Everything else" below); its leading `{{/* … */}}` block explains the bar and is meant to be deleted.
|
||||
2. Add `.github/workflows/publish-<chart-name>.yml` (copy an existing one, e.g. `publish-outline.yml`).
|
||||
3. Add the chart name to the choice list in `.github/workflows/change-app-version.yml`.
|
||||
4. Add a `customManagers` block for the chart's main image in `renovate.json5` (copy a neighbouring one, swap `managerFilePatterns` and `depNameTemplate`) — without it the chart's app version is never updated. Check the image's tag list first: if it carries a flavour, date or prerelease suffix, add a `versioningTemplate` with a comment saying why. **Floating tags belong outside the pattern**: `ytdlp-web-player` anchors its versioning regex on the `v` prefix precisely so `latest` and `stable` — both of which move — can never be selected.
|
||||
@@ -245,6 +245,7 @@ These guidelines are **binding for every current and future chart** in this repo
|
||||
|
||||
- Every workload container offers an additive `env` value block, appended to the env entries the chart always sets.
|
||||
- Entries are rendered through `tpl` (so values may contain Helm templating) and support the `value`, `secretKeyRef` and `configMapKeyRef` forms (pattern: `template-chart/templates/xxx.deployment.yaml`).
|
||||
- **Additive means additive: an entry repeating a name the chart already sets does not replace it, and what the pair does depends on when it arrives.** At install both entries reach the pod and the later one — the user's — wins. On an *existing* release the same entry is lost: Kubernetes merges `env` by name, the pair collapses to the chart's own value, and `helm upgrade` still reports success. Measured on `urlaubsverwaltung` (helm 3.19.0, Kubernetes 1.35): `helm get manifest` shows the variable twice while the live Deployment shows it once, carrying the chart's value. The failure mode is silence, so a chart whose `NOTES.txt` tells operators to override one of its own variables has to say that the override only takes at install time — and that a green upgrade proves nothing, the value belongs read back off the Deployment.
|
||||
|
||||
### 11. Scaling
|
||||
|
||||
@@ -252,7 +253,7 @@ These guidelines are **binding for every current and future chart** in this repo
|
||||
- Every chart has a global `scaleDown` flag (default `false`): `scaleDown: true` takes precedence over all `replicas` values, renders every Deployment/StatefulSet with `replicas: 0` and suspends CronJobs. It **stops** the app but does not uninstall it — PVCs, Secrets, Services and Ingress stay in place; setting it back to `false` brings the workloads back up.
|
||||
- Implemented via a per-chart `<chart>.replicas` helper in `_helpers.tpl` (reference in `template-chart`); the helper uses an explicit nil check (`kindIs "invalid"`) because Helm's `default` would swallow an explicit `0`.
|
||||
|
||||
### 12. Values that must not lie (#68, #93, #99)
|
||||
### 12. Values that must not lie (#68, #93, #99, #215)
|
||||
|
||||
Every chart ships a **`values.schema.json`** with `additionalProperties: false` on **every chart-owned object level** — not just the top one. The gap this closes is a key that looks like configuration and does nothing: the pilot (#68) found three of them in one chart, one of them nested three levels deep and quietly leaving a game world unpinned. Objects that are passed **verbatim** into the pod spec stay open (probes, `securityContext.pod`/`.container`, `global`, free-form annotation maps): Kubernetes owns their schema, and restricting them would only reject fields that do not exist yet. The schema **adds to** `required()`, it does not replace it.
|
||||
|
||||
@@ -270,10 +271,14 @@ Reference implementation: `template-chart/values.schema.json` and `template-char
|
||||
|
||||
**3. Do not hang liveness on an external dependency where a dependency-free path exists.** `zipline` deliberately probes `/` for liveness and `/api/healthcheck` only for readiness and startup, so a database outage cannot restart the pod. Where no such path exists — `outline` returns 500 on `/` when the database is down — all three probes may share the endpoint, but the reason belongs in a comment on the helper, or the next reader will "fix" the inconsistency.
|
||||
|
||||
The other half of the same rule: readiness **must** hang on the dependency, and a separate path is not proof that it does. `urlaubsverwaltung` probed `/actuator/health/liveness` and `/actuator/health/readiness` for two years and still stayed 1/1 Ready through a total database outage (#218), because Spring Boot's `readiness` group contains `readinessState` **alone** — the split paths were right, the group content was empty of anything external. Whatever aggregates behind a probe path (Spring health groups, a `/healthz` a framework assembles for you) has to be read off the **running** endpoint, not off the path name; the chart names the members explicitly (`MANAGEMENT_ENDPOINT_HEALTH_GROUP_READINESS_INCLUDE=readinessState,db`) so the composition is in the template rather than in an upstream default.
|
||||
|
||||
**4. A fallback reproduces *this chart's* defaults, not the repo standard.** Five charts deviate from the hardening baseline on purpose. A "cleaned up" standard default is not tidying, it is an outage: `calibre-web-automated` must keep `readOnlyRootFilesystem: false`, because in read-only mode the image disables its PUID/PGID remapping and the runtime uid silently moves from 1000 to 911 — **making existing volume data inaccessible**. The same applies to non-standard values that look like mistakes: `satisfactory` keeps `limitFactor: 2` (that is what holds the memory limit at the published 8Gi) and `fsGroupChangePolicy: OnRootMismatch` (which avoids a recursive chown of multi-GB game files on every start).
|
||||
|
||||
**5. A probe default carries the probe's context, not just its timings.** Two different symptoms, both from real charts: `teamspeak-server` addresses its TCP port by the name `file-transfer` because the voice service is UDP and cannot be probed — a default without that name leaves the workload **with no working probe at all**. `patchmon` sends a `Host` header because the server answers **403** to any request whose Host is neither loopback nor a CORS_ORIGIN host — a default without it means the pod never becomes Ready. Port names, `Host` headers and `exec` commands belong in the fallback; where the context is dynamic, pass the root context in and rebuild it (patchmon derives the header from `ingress.domain`).
|
||||
|
||||
**6. A value that reads as the negation of the variable it feeds must actually be negated — and `not` cannot do it.** `satisfactory` shipped `game.seasonalEvents` straight into the image's `DISABLESEASONALEVENTS` (#215): the default `true` switched the events **off** in every install for the chart's whole life, while the schema described the same key as "true means events are enabled". The fix is the loud one — rename the key so it says what it does (`game.enableSeasonalEvents`) rather than silently inverting the template under existing releases, because the old key then fails schema validation instead of quietly meaning the opposite. Two mechanics come with it: Go's `not` treats **every non-empty string as true**, so `not "false"` is `false` and the string form these schemas deliberately accept flips the wrong way — compare explicitly (`eq (lower (printf "%v" $v)) "true"`) instead. And a value that cannot be read as either boolean belongs in a `fail`, not in an implicit branch: `enableSeasonalEvents: "yes"` must abort the render, not be quietly filed under "off". When such a rename lands, the new default is the one that **reproduces what the chart already rendered**; changing the meaning and the behaviour in the same release turns the loud fix back into a silent one.
|
||||
|
||||
**Two things about the mechanics that are easy to lose:**
|
||||
|
||||
- **`null` reaches the schema in one case and not the other.** For a key **with** a chart default, Helm's coalescing treats the `null` as a deletion and removes it *before* schema validation — the schema never sees it, and only the helper can catch it. For a key **without** a default (e.g. `resources.limits`), there is nothing to delete, the `null` survives into validation, and a strict `"type": "object"` aborts the render. That is why the helper **and** the widened `["object", "null"]` types are both needed: each covers a case the other cannot. Everything else stays strictly typed.
|
||||
|
||||
@@ -5,16 +5,25 @@ icon: https://avatars.githubusercontent.com/u/988985
|
||||
|
||||
type: application
|
||||
|
||||
# The Java line of this chart version is chosen through appVersion, which is
|
||||
# the image tag: the chart is published once per line (java8 / java17 / java21 /
|
||||
# java25) and minecraft.version in values.yaml must match the line published
|
||||
# here.
|
||||
# ONLY THE NEWEST JAVA LINE IS MAINTAINED (chart 5.0.0, issue #158).
|
||||
# appVersion is the image tag and therefore fixes the JVM. Up to chart 4.1.0 the
|
||||
# chart was published once per line (java8 / java17 / java21 / java25) out of
|
||||
# this single directory, which is what made the lines drift apart: whatever
|
||||
# change landed while a line happened to lie in the directory reached that line
|
||||
# only. That is over. This directory carries exactly one line - the newest -
|
||||
# and it is the only one that gets chart changes.
|
||||
# The older lines stay published as frozen artifacts and are still installable:
|
||||
# java8 and java17 at 4.0.0, java21 at 4.1.0. They will not be updated again.
|
||||
# To run an older JVM with the CURRENT chart, set image.tagOverride in values
|
||||
# instead of installing a frozen chart version; the chart then checks that
|
||||
# minecraft.version fits the line of that tag and fails the render if it does
|
||||
# not.
|
||||
# Since chart 2.0.0 the tag is a DATED upstream release ("<release>-java<N>")
|
||||
# instead of the rolling line tag ("java<N>"): the rolling tags are rebuilt
|
||||
# continuously, so a pod restart could pull a different image than the last one
|
||||
# without anything being recorded anywhere. The dated tags are immutable, so the
|
||||
# image is now pinned by the chart version like every other chart in this repo.
|
||||
version: 4.1.0+up2026.8.2.java25
|
||||
version: 5.0.0+up2026.8.2.java25
|
||||
appVersion: "2026.8.2-java25"
|
||||
|
||||
maintainers:
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
{{- if and (kindIs "float64" $startup.periodSeconds) (kindIs "float64" $startup.failureThreshold) -}}
|
||||
{{- $budget = printf "%.0f seconds" (mulf $startup.periodSeconds $startup.failureThreshold) -}}
|
||||
{{- end -}}
|
||||
{{- $line := regexFind "java[0-9]+" (printf "%v" .Chart.AppVersion) -}}
|
||||
{{- $tag := include "minecraft-server.imageTag" . -}}
|
||||
{{- $line := include "minecraft-server.javaLine" . -}}
|
||||
{{- $tagOverridden := ne (include "minecraft-server.imageTagSource" .) ".Chart.AppVersion" -}}
|
||||
{{- $svc := printf "%s-%s-service" .Release.Name .Chart.Name -}}
|
||||
{{- $sfs := printf "%s-%s-sfs" .Release.Name .Chart.Name -}}
|
||||
{{- $pod := printf "%s-%s-sfs-0" .Release.Name .Chart.Name -}}
|
||||
@@ -26,7 +28,7 @@
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $memPinned := not (kindIs "invalid" (index $givenReq "memory")) -}}
|
||||
minecraft-server {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
||||
minecraft-server {{ $tag }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
||||
|
||||
ACCESS — A MINECRAFT SERVER, NOT A WEB SERVICE
|
||||
This release has no Ingress, no certificate and no URL. Clients speak the
|
||||
@@ -76,25 +78,53 @@ ACCESS — A MINECRAFT SERVER, NOT A WEB SERVICE
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ $svc }} {{ .Values.server.service.port }}:{{ .Values.server.service.port }}
|
||||
|
||||
THIS CHART IS PUBLISHED ONCE PER JAVA LINE — THIS IS {{ $line | default "an unrecognized line" | upper }}
|
||||
The JVM is fixed by the image tag, which comes from the chart's appVersion
|
||||
({{ .Chart.AppVersion }}), not from a value. This build runs
|
||||
minecraft.version {{ .Values.minecraft.version }}.
|
||||
THE JAVA LINE OF THIS RELEASE IS {{ $line | default "NOT RECOGNIZABLE FROM ITS TAG" | upper }}
|
||||
The JVM is fixed by the image tag, not by a value of the game. This release
|
||||
runs
|
||||
|
||||
Java 8 runs Minecraft up to 1.16.5, 1.18-1.20.4 need Java 17, 1.20.5-1.21.11
|
||||
need Java 21 and 26.1 and later need Java 25 (read off Mojang's version
|
||||
manifest, javaVersion.majorVersion per release). Nothing in this chart checks
|
||||
that minecraft.version fits the line: a mismatch renders and installs without
|
||||
a word, and only the container fails afterwards — CrashLoopBackOff once the
|
||||
startup budget{{ if $budget }} ({{ $budget }}){{ end }} is spent, with the reason in the log and
|
||||
itzg/minecraft-server:{{ $tag }}
|
||||
|
||||
with minecraft.version {{ .Values.minecraft.version }}.
|
||||
{{- if $tagOverridden }}
|
||||
|
||||
That tag comes from image.tagOverride, NOT from the chart's appVersion
|
||||
({{ .Chart.AppVersion }}). So the chart version no longer says what this release runs:
|
||||
"helm list" reports {{ .Chart.Version }} while the pod is on the tag
|
||||
above. That is the price of the override, and the reason it is not called
|
||||
image.tag.
|
||||
{{- else }}
|
||||
|
||||
That tag comes from the chart's appVersion; image.tagOverride is empty, so
|
||||
the chart version tells the whole truth about the image.
|
||||
{{- end }}
|
||||
|
||||
Since chart 5.0.0 THAT PAIRING IS CHECKED: if the line of the tag above does
|
||||
not fit minecraft.version, the render fails and names both values. Two cases
|
||||
are let through on purpose — a tag without a recognizable "java<N>" part, and
|
||||
a minecraft.version that is not a plain numeric x[.y[.z]] (a snapshot, a
|
||||
pre-release, "LATEST"). In those two the old failure mode still applies: the
|
||||
release installs, and only the container fails afterwards — CrashLoopBackOff
|
||||
once the startup budget{{ if $budget }} ({{ $budget }}){{ end }} is spent, with the reason in the log and
|
||||
nowhere else:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} logs {{ $pod }}
|
||||
|
||||
So moving the server across a line boundary is a change of CHART VERSION, not
|
||||
a change of minecraft.version: pick the published chart of the target line and
|
||||
set minecraft.version to match it. Changing only minecraft.version leaves the
|
||||
old JVM in place.
|
||||
The boundaries are Mojang's (javaVersion.majorVersion in the piston-meta
|
||||
version manifest, per release):
|
||||
|
||||
up to 1.16.5 Java 8
|
||||
1.17 - 1.17.1 Java 16
|
||||
1.18 - 1.20.4 Java 17
|
||||
1.20.5 - 1.21.11 Java 21
|
||||
26.1 and later Java 25
|
||||
|
||||
ONLY THE NEWEST LINE IS MAINTAINED. This chart directory carries one line and
|
||||
gets chart changes only for that one; java8 and java17 stay frozen at chart
|
||||
4.0.0, java21 at 4.1.0. So moving the server across a line boundary is no
|
||||
longer a change of chart version: set image.tagOverride to a tag of the target
|
||||
line AND minecraft.version to a release of it, in the same upgrade. Changing
|
||||
only one of the two now fails the render instead of leaving a broken pod
|
||||
behind.
|
||||
{{- if eq $replicas "0" }}
|
||||
|
||||
SCALED TO ZERO
|
||||
|
||||
@@ -183,6 +183,133 @@ last one silently won. Fail loudly instead.
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The tag of the itzg/minecraft-server image this release actually runs.
|
||||
|
||||
Default: .Chart.AppVersion, exactly as before chart 5.0.0. image.tagOverride
|
||||
replaces it when set - that is the one way to run a Java line other than the
|
||||
one this chart version publishes, now that only the newest line is maintained
|
||||
(see Chart.yaml). The value is deliberately NOT called image.tag: it is an
|
||||
emergency exit, not a knob to turn in passing, and it is not managed by
|
||||
Renovate (renovate.json5 tracks appVersion only).
|
||||
*/}}
|
||||
{{- define "minecraft-server.imageTag" -}}
|
||||
{{- $override := "" -}}
|
||||
{{- if kindIs "map" .Values.image -}}
|
||||
{{- if not (kindIs "invalid" .Values.image.tagOverride) -}}
|
||||
{{- $override = printf "%v" .Values.image.tagOverride | trim -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if $override -}}{{ $override }}{{- else -}}{{ .Chart.AppVersion }}{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Where the effective tag came from - used in the guard's message so the reader
|
||||
knows which of the two values to change.
|
||||
*/}}
|
||||
{{- define "minecraft-server.imageTagSource" -}}
|
||||
{{- $override := "" -}}
|
||||
{{- if kindIs "map" .Values.image -}}
|
||||
{{- if not (kindIs "invalid" .Values.image.tagOverride) -}}
|
||||
{{- $override = printf "%v" .Values.image.tagOverride | trim -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if $override -}}image.tagOverride{{- else -}}.Chart.AppVersion{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The Java line the effective image tag carries ("java25", "java17", ...), or the
|
||||
empty string when the tag does not name one.
|
||||
|
||||
regexFind rather than a suffix comparison, so flavoured tags keep working:
|
||||
"2026.8.2-java25", "java21-alpine" and "latest-java17-jdk" all resolve.
|
||||
*/}}
|
||||
{{- define "minecraft-server.javaLine" -}}
|
||||
{{- regexFind "java[0-9]+" (include "minecraft-server.imageTag" .) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The Java line minecraft.version requires, or the empty string when the value is
|
||||
not a version this chart can compare.
|
||||
|
||||
The boundaries are Mojang's, not this chart's: they are javaVersion.majorVersion
|
||||
from the piston-meta version manifest, swept over all 102 releases on 2026-09-02.
|
||||
The transitions are exactly four, and monotonic in release order:
|
||||
|
||||
1.0 - 1.16.5 java 8 (1.6.1-1.6.4 predate the field and carry none)
|
||||
1.17 - 1.17.1 java 16
|
||||
1.18 - 1.20.4 java 17
|
||||
1.20.5 - 1.21.11 java 21
|
||||
26.1 and later java 25
|
||||
|
||||
DELIBERATE GAP: a minecraft.version that is not a plain numeric "x", "x.y" or
|
||||
"x.y.z" - a snapshot ("26w13a"), a pre-release ("1.21.11-pre1"), "LATEST" -
|
||||
yields the empty string and is let through unchecked. The chart cannot compare
|
||||
it, and a guard that rejects everything it does not understand gets worked
|
||||
around instead of respected. The mismatch then still surfaces the old way:
|
||||
CrashLoopBackOff once the startup budget is spent, with the reason in the log.
|
||||
*/}}
|
||||
{{- define "minecraft-server.requiredJavaLine" -}}
|
||||
{{- $v := printf "%v" .Values.minecraft.version | trim -}}
|
||||
{{- if regexMatch "^[0-9]+(\\.[0-9]+){0,2}$" $v -}}
|
||||
{{- $n := len (splitList "." $v) -}}
|
||||
{{- $sv := $v -}}
|
||||
{{- if eq $n 1 -}}{{- $sv = printf "%s.0.0" $v -}}{{- else if eq $n 2 -}}{{- $sv = printf "%s.0" $v -}}{{- end -}}
|
||||
{{- if semverCompare "< 1.17.0" $sv -}}java8
|
||||
{{- else if semverCompare "< 1.18.0" $sv -}}java16
|
||||
{{- else if semverCompare "< 1.20.5" $sv -}}java17
|
||||
{{- else if semverCompare "< 26.1.0" $sv -}}java21
|
||||
{{- else -}}java25
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Guard: minecraft.version must fit the Java line of the effective image tag.
|
||||
|
||||
Until chart 5.0.0 nothing checked this - a mismatch rendered and installed
|
||||
without a word, and only the container failed afterwards. It now fails at render
|
||||
time, which is a behaviour change for an installation that already runs a
|
||||
mismatched pair (it does not run: it fails earlier and visibly instead).
|
||||
|
||||
The check runs ALWAYS, not only when image.tagOverride is set: Helm merges user
|
||||
values into the chart defaults indistinguishably, so a template cannot tell a
|
||||
deliberately set minecraft.version from the chart's own default.
|
||||
|
||||
DELIBERATE GAP, the second one: an image tag without a recognizable java<N> part
|
||||
is let through - see minecraft-server.javaLine. Same reasoning as for an
|
||||
uncomparable minecraft.version.
|
||||
*/}}
|
||||
{{- define "minecraft-server.validateJavaLine" -}}
|
||||
{{- $tag := include "minecraft-server.imageTag" . -}}
|
||||
{{- $line := include "minecraft-server.javaLine" . -}}
|
||||
{{- $required := include "minecraft-server.requiredJavaLine" . -}}
|
||||
{{- if and $line $required -}}
|
||||
{{- if ne $line $required -}}
|
||||
{{- $source := include "minecraft-server.imageTagSource" . -}}
|
||||
{{- $swapped := regexReplaceAll "java[0-9]+" $tag $required -}}
|
||||
{{- fail (printf (join "\n" (list
|
||||
"minecraft-server: the image tag and minecraft.version belong to different Java lines."
|
||||
""
|
||||
" image tag %-22s is the %s line (from %s)"
|
||||
" minecraft.version %-22s needs the %s line"
|
||||
""
|
||||
"Java 8 runs Minecraft up to 1.16.5, 1.17-1.17.1 need Java 16, 1.18-1.20.4 need"
|
||||
"Java 17, 1.20.5-1.21.11 need Java 21, and 26.1 and later need Java 25 (Mojang's"
|
||||
"version manifest, javaVersion.majorVersion per release)."
|
||||
""
|
||||
"Change one of the two:"
|
||||
" - set minecraft.version to a release of the %s line, or"
|
||||
" - set image.tagOverride to an itzg/minecraft-server tag of the %s line"
|
||||
" (this tag with its line swapped would be \"%s\" - check that upstream"
|
||||
" published it before using it)."))
|
||||
(printf "%q" $tag) $line $source
|
||||
(printf "%q" (printf "%v" .Values.minecraft.version)) $required
|
||||
$line $required $swapped) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Merge a user-supplied values sub-tree onto a dict of chart defaults so that a
|
||||
null / empty override falls back to the defaults instead of erasing them.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{- include "minecraft-server.validateModpackSources" . }}
|
||||
{{- include "minecraft-server.validateJavaLine" . }}
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
@@ -58,7 +59,11 @@ spec:
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
image: "itzg/minecraft-server:{{ .Chart.AppVersion }}"
|
||||
{{- /* Tag from .Chart.AppVersion unless image.tagOverride replaces
|
||||
it; validateJavaLine above rejects a tag whose Java line does
|
||||
not fit minecraft.version. Template comment on purpose - a
|
||||
YAML comment here would change the rendered manifest. */}}
|
||||
image: "itzg/minecraft-server:{{ include "minecraft-server.imageTag" . }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
{{- include "minecraft-server.containerSecurityContext" .Values.securityContext | nindent 12 }}
|
||||
|
||||
@@ -12,6 +12,17 @@
|
||||
"scaleDown": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"image": {
|
||||
"description": "May be null / empty: the effective tag then falls back to the chart's appVersion, which is the pre-5.0.0 behaviour.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tagOverride": {
|
||||
"description": "Tag of the itzg/minecraft-server image. Empty or null means the chart's appVersion. Deliberately not called \"tag\": it decouples the running image from the chart version and is not managed by Renovate. The Java line of the effective tag is validated against minecraft.version at render time; a tag without a recognizable \"java<N>\" part is let through on purpose.",
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"secrets": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -4,6 +4,38 @@
|
||||
# Services stay in place; scaleDown: false brings the workloads back up.
|
||||
scaleDown: false
|
||||
|
||||
image:
|
||||
# Tag of the itzg/minecraft-server image. Empty means "use the chart's
|
||||
# appVersion" (2026.8.2-java25 here), which is what every release before chart
|
||||
# 5.0.0 did unconditionally.
|
||||
#
|
||||
# WHY THIS EXISTS. Since chart 5.0.0 only the NEWEST Java line is maintained
|
||||
# (see Chart.yaml). The older lines stay published as frozen artifacts - java8
|
||||
# and java17 at chart 4.0.0, java21 at 4.1.0 - and never receive another chart
|
||||
# change. To run an older JVM with the CURRENT chart, set the tag here instead
|
||||
# of installing a frozen chart version:
|
||||
#
|
||||
# image:
|
||||
# tagOverride: "2026.8.2-java21"
|
||||
# minecraft:
|
||||
# version: "1.21.11"
|
||||
#
|
||||
# It is called tagOverride, not tag, because it is an emergency exit: it
|
||||
# decouples the running image from the chart version, so the release no longer
|
||||
# says in its metadata what it actually runs. Prefer moving to the current
|
||||
# line; use this when a modpack pins you to an older Minecraft version.
|
||||
#
|
||||
# DELIBERATELY NOT MANAGED BY RENOVATE. renovate.json5 tracks appVersion in
|
||||
# Chart.yaml (partitioned by its "java<N>" compatibility group) and nothing
|
||||
# else. A tag set here is never bumped by anything - it is yours to review.
|
||||
#
|
||||
# The Java line of the effective tag (this value, or appVersion when it is
|
||||
# empty) is checked against minecraft.version at render time; a mismatch fails
|
||||
# the render with both values named. Two cases pass unchecked on purpose: a
|
||||
# tag without a recognizable "java<N>" part, and a minecraft.version that is
|
||||
# not a plain numeric x[.y[.z]] (snapshots, pre-releases, "LATEST").
|
||||
tagOverride: ""
|
||||
|
||||
secrets:
|
||||
# op:// item path for the 1Password operator. Only needs to carry a
|
||||
# "curseforge-api-key" key when curseforge.enabled is true and an
|
||||
@@ -223,14 +255,31 @@ minecraft:
|
||||
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,
|
||||
# 1.20.5-1.21.11 need Java 21 and 26.1 and later need Java 25. The two upper
|
||||
# boundaries are read off Mojang's version manifest (piston-meta, field
|
||||
# javaVersion.majorVersion per release), not estimated: 1.21.11 (2025-12-09)
|
||||
# is the last release asking for 21, and every release from 26.1 (2026-03-24)
|
||||
# on asks for 25. This chart publishes appVersion "2026.8.2-java25".
|
||||
#
|
||||
# This value must match the Java line of the EFFECTIVE image tag - appVersion,
|
||||
# or image.tagOverride when that is set. Since chart 5.0.0 the chart checks it
|
||||
# and FAILS THE RENDER on a mismatch, naming both values; before 5.0.0 a
|
||||
# mismatch installed silently and only the container failed afterwards.
|
||||
#
|
||||
# up to 1.16.5 Java 8
|
||||
# 1.17 - 1.17.1 Java 16 (never published as a chart line here)
|
||||
# 1.18 - 1.20.4 Java 17
|
||||
# 1.20.5 - 1.21.11 Java 21
|
||||
# 26.1 and later Java 25
|
||||
#
|
||||
# These are Mojang's boundaries, not this chart's: javaVersion.majorVersion
|
||||
# from the piston-meta version manifest, swept over all 102 releases on
|
||||
# 2026-09-02. There are exactly four transitions and they are monotonic in
|
||||
# release order (1.6.1-1.6.4 predate the field and declare none).
|
||||
#
|
||||
# Only a plain numeric x[.y[.z]] is compared. A snapshot ("26w13a"), a
|
||||
# pre-release ("1.21.11-pre1") or "LATEST" is let through unchecked - see
|
||||
# image.tagOverride for why the two gaps are deliberate.
|
||||
#
|
||||
# Quote the value. Unquoted YAML reads 1.20 as the number 1.2, and the chart
|
||||
# would pass "1.2" to the image as VERSION.
|
||||
#
|
||||
# This chart publishes appVersion "2026.8.2-java25".
|
||||
version: "26.2"
|
||||
|
||||
# curseforge, ftb and curseforgeServerpack are mutually exclusive - enabling
|
||||
|
||||
@@ -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: 6.1.0+upv1.9.10
|
||||
version: 7.0.0+upv1.9.10
|
||||
appVersion: "v1.9.10"
|
||||
maintainers:
|
||||
- name: jklein
|
||||
@@ -17,9 +17,10 @@
|
||||
{{- if and (kindIs "float64" $startup.periodSeconds) (kindIs "float64" $startup.failureThreshold) -}}
|
||||
{{- $budget = printf "%.0fs x %.0f = %.0f minutes" $startup.periodSeconds $startup.failureThreshold (divf (mulf $startup.periodSeconds $startup.failureThreshold) 60.0) -}}
|
||||
{{- end -}}
|
||||
{{- $seasonalRaw := printf "%v" .Values.game.seasonalEvents -}}
|
||||
{{- $seasonalOff := eq (lower $seasonalRaw) "true" -}}
|
||||
{{- $beta := eq (lower (printf "%v" .Values.game.experimental)) "true" -}}
|
||||
{{- $game := fromYaml (include "satisfactory.game" .Values.game) -}}
|
||||
{{- $seasonalEnv := include "satisfactory.disableSeasonalEvents" $game.enableSeasonalEvents -}}
|
||||
{{- $seasonalOff := eq $seasonalEnv "true" -}}
|
||||
{{- $beta := eq (lower (printf "%v" $game.experimental)) "true" -}}
|
||||
{{- $debug := eq (lower (printf "%v" .Values.dev.debug)) "true" -}}
|
||||
{{- $outOfRange := or (lt $gamePort 30000) (gt $gamePort 32767) (lt $msgPort 30000) (gt $msgPort 32767) -}}
|
||||
{{- $skipRaw := "" -}}
|
||||
@@ -213,14 +214,37 @@ RESOURCE LIMITS ARE COMPUTED FROM limitFactor ({{ $factor }})
|
||||
unlimited container gets throttled rather than killed — a memory limit can
|
||||
end a session, a missing CPU limit only slows it down.
|
||||
|
||||
game.seasonalEvents IS PASSED THROUGH, NOT INVERTED
|
||||
The chart hands game.seasonalEvents to the container as
|
||||
DISABLESEASONALEVENTS unchanged. It is currently {{ $seasonalRaw }}, so the container
|
||||
runs with DISABLESEASONALEVENTS={{ $seasonalRaw }} — seasonal events are {{ if $seasonalOff }}OFF{{ else }}ON{{ end }}.
|
||||
{{ if $seasonalOff }}To turn them ON, set game.seasonalEvents: false.{{ else }}To turn them OFF, set game.seasonalEvents: true.{{ end }}
|
||||
Read that twice: the value behaves as the opposite of what its name suggests
|
||||
and of what values.schema.json claims. Believe this paragraph and the
|
||||
rendered env var, not the key name.
|
||||
SEASONAL EVENTS — THE KEY WAS RENAMED IN 7.0.0, AND IT IS BREAKING
|
||||
Seasonal (holiday) events are {{ if $seasonalOff }}OFF{{ else }}ON{{ end }} in this release: game.enableSeasonalEvents is
|
||||
{{ $game.enableSeasonalEvents }}, which the chart negates into DISABLESEASONALEVENTS={{ $seasonalEnv }}.
|
||||
{{ if $seasonalOff }}To turn them ON, set game.enableSeasonalEvents: true.{{ else }}To turn them OFF, set game.enableSeasonalEvents: false.{{ end }}
|
||||
|
||||
UPGRADING FROM 6.x — WHAT YOU HAVE TO DO
|
||||
The old key game.seasonalEvents IS GONE. It is not deprecated and not
|
||||
quietly accepted: values.schema.json rejects it, so a values file that still
|
||||
carries it fails the upgrade with a validation error instead of installing
|
||||
something you did not ask for. That noise is deliberate — see below.
|
||||
|
||||
Remove the old key and, if you want the events, add the new one:
|
||||
|
||||
game:
|
||||
enableSeasonalEvents: true # false, the default, keeps them off
|
||||
|
||||
AND THE MEANING IS INVERTED FROM WHAT THE OLD KEY'S NAME SUGGESTED
|
||||
Up to 6.1.0 the value was handed to the container UNCHANGED, into a variable
|
||||
whose name says the opposite: game.seasonalEvents: true wrote
|
||||
DISABLESEASONALEVENTS=true. So the shipped default had the events switched
|
||||
OFF, in every install, for as long as this chart has existed — while
|
||||
values.schema.json described the same key as "true means events are
|
||||
enabled". If you left the default alone believing that description, you have
|
||||
never had seasonal events, and the new default (enableSeasonalEvents: false)
|
||||
changes nothing about that: it renders exactly the DISABLESEASONALEVENTS
|
||||
this chart has always rendered, so upgrading alone does not move your
|
||||
server. You have to ask for the events, and now the key name means what it
|
||||
says when you do.
|
||||
|
||||
Only true and false are accepted, boolean or quoted; anything else aborts
|
||||
the render rather than being guessed at.
|
||||
|
||||
THE USER ID IS PINNED, AND PUID/PGID DO NOTHING
|
||||
The pod runs as 1000:1000 because the image supports rootless operation only
|
||||
|
||||
@@ -288,6 +288,52 @@ Probe specifics that a rebuilt default must not lose:
|
||||
{{- include "satisfactory.defaultedDict" (dict "defaults" $defaults "given" . "path" "startupProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The effective game block (issue #99, found alongside #215).
|
||||
|
||||
"--set game=null" — or a leftover "game:" with all sub-keys deleted — used to
|
||||
abort the render with a nil-pointer error at .Values.game.maxPlayers, because
|
||||
this block never got the null-safe treatment the probes, security contexts and
|
||||
resources have. It now falls back to the same defaults values.yaml documents.
|
||||
|
||||
The defaults below MUST stay in sync with the game block in values.yaml.
|
||||
Note that enableSeasonalEvents defaults to FALSE: that is what keeps
|
||||
DISABLESEASONALEVENTS at "true", the value this chart has always rendered.
|
||||
*/}}
|
||||
{{- define "satisfactory.game" -}}
|
||||
{{- $defaults := dict
|
||||
"experimental" false
|
||||
"maxPlayers" 4
|
||||
"enableSeasonalEvents" false -}}
|
||||
{{- include "satisfactory.defaultedDict" (dict "defaults" $defaults "given" . "path" "game") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Render the DISABLESEASONALEVENTS env value from game.enableSeasonalEvents
|
||||
(issue #215).
|
||||
|
||||
The image only offers the negative switch, so the chart negates the positive
|
||||
value here — that is the whole point of the rename: the key says what it does
|
||||
and the env var keeps the name the image expects.
|
||||
|
||||
Go's "not" cannot do this: it treats every non-empty string as true, so the
|
||||
string form "false" (which fleet bundles habitually write for booleans, and
|
||||
which the schema therefore accepts) would flip the wrong way. Hence the
|
||||
explicit comparison — and an explicit fail for anything that is neither, so a
|
||||
typo like "yes" cannot be silently read as "events off". Null never reaches
|
||||
here; satisfactory.game has already replaced it with the default.
|
||||
*/}}
|
||||
{{- define "satisfactory.disableSeasonalEvents" -}}
|
||||
{{- $raw := printf "%v" . -}}
|
||||
{{- if eq (lower $raw) "true" -}}
|
||||
false
|
||||
{{- else if eq (lower $raw) "false" -}}
|
||||
true
|
||||
{{- else -}}
|
||||
{{- fail (printf "game.enableSeasonalEvents must be true or false (boolean or string), got %q" $raw) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The effective resources block, fed into the resources helper above so that an
|
||||
erased "resources:" still yields the chart's requests and computed limits.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{{- $game := fromYaml (include "satisfactory.game" .Values.game) -}}
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
@@ -25,7 +26,7 @@ spec:
|
||||
- name: DEBUG
|
||||
value: "{{ .Values.dev.debug }}"
|
||||
- name: MAXPLAYERS
|
||||
value: "{{ .Values.game.maxPlayers }}"
|
||||
value: "{{ include "satisfactory.formatNumber" $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
|
||||
@@ -35,9 +36,11 @@ spec:
|
||||
- name: SKIPUPDATE
|
||||
value: "false"
|
||||
- name: STEAMBETA
|
||||
value: "{{ .Values.game.experimental }}"
|
||||
value: "{{ $game.experimental }}"
|
||||
# The image only knows the negative switch, so game.enableSeasonalEvents
|
||||
# is negated here (#215): true -> "false" (events run).
|
||||
- name: DISABLESEASONALEVENTS
|
||||
value: "{{ .Values.game.seasonalEvents }}"
|
||||
value: "{{ include "satisfactory.disableSeasonalEvents" $game.enableSeasonalEvents }}"
|
||||
- name: SERVERGAMEPORT
|
||||
value: "{{ .Values.server.gamePort }}"
|
||||
- name: SERVERMESSAGINGPORT
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
}
|
||||
},
|
||||
"game": {
|
||||
"type": "object",
|
||||
"description": "May be null / empty: the chart then falls back to its documented game defaults (issue #99) instead of aborting the render with a nil-pointer error, which is what an erased block used to do.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"experimental": {
|
||||
@@ -33,8 +34,8 @@
|
||||
"maxPlayers": {
|
||||
"type": ["integer", "string", "null"]
|
||||
},
|
||||
"seasonalEvents": {
|
||||
"description": "Inverted by the chart into DISABLESEASONALEVENTS, so true means events are enabled.",
|
||||
"enableSeasonalEvents": {
|
||||
"description": "Seasonal (holiday) events. NEGATED by the chart into the image's DISABLESEASONALEVENTS: true renders DISABLESEASONALEVENTS=\"false\" and the events run, false renders \"true\" and they do not. Only true/false are accepted (boolean or string); anything else aborts the render. Default false, which reproduces what the chart has always rendered - up to 6.1.0 the key was game.seasonalEvents and was passed through unchanged, so its default true wrote DISABLESEASONALEVENTS=true and the events were off despite this description claiming the opposite (issue #215). The old key game.seasonalEvents no longer exists and is rejected here rather than silently ignored.",
|
||||
"type": ["boolean", "string", "null"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,23 @@ dev:
|
||||
game:
|
||||
experimental: false
|
||||
maxPlayers: 4
|
||||
seasonalEvents: true
|
||||
# Seasonal (holiday) events. The image only knows the negative switch
|
||||
# DISABLESEASONALEVENTS, so the chart NEGATES this value on the way in:
|
||||
# enableSeasonalEvents: true renders DISABLESEASONALEVENTS="false" (events
|
||||
# run), false renders "true" (events off). Only true/false are accepted, in
|
||||
# boolean or string form; anything else aborts the render instead of being
|
||||
# read as one of the two.
|
||||
#
|
||||
# The default is false on purpose, and it is not a preference: it reproduces
|
||||
# what this chart has ALWAYS rendered. Up to 6.1.0 the key was called
|
||||
# game.seasonalEvents and was passed through UNCHANGED, so its default true
|
||||
# wrote DISABLESEASONALEVENTS=true — the events were off in every install,
|
||||
# while values.schema.json claimed "true means events are enabled" (#215).
|
||||
# Keeping the rendered result identical is what makes this rename loud
|
||||
# instead of silent: nobody's server changes behaviour by upgrading, and the
|
||||
# old key is now rejected by the schema, so anyone who set it has to decide
|
||||
# again. Set enableSeasonalEvents: true to actually switch the events on.
|
||||
enableSeasonalEvents: false
|
||||
|
||||
server:
|
||||
# Replica count for this workload; 0 is allowed and scales it down.
|
||||
|
||||
@@ -4,5 +4,5 @@ description: A Helm chart to deploy urlaubsverwaltung on Kubernetes
|
||||
icon: https://raw.githubusercontent.com/urlaubsverwaltung/urlaubsverwaltung/main/src/main/resources/static/favicons/apple-touch-icon.png
|
||||
type: application
|
||||
|
||||
version: 8.1.0+up6.7.0
|
||||
version: 9.0.0+up6.7.0
|
||||
appVersion: 6.7.0
|
||||
@@ -199,33 +199,89 @@ THE MEMORY LIMIT SIZES THE JAVA HEAP — IT IS NOT JUST A CEILING
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} logs deployment/{{ .Release.Name }}-{{ .Chart.Name }}-deployment | head -1
|
||||
|
||||
DO NOT HEALTH-CHECK THIS RELEASE ON /actuator/health
|
||||
That endpoint aggregates every health indicator, and one of them is the mail
|
||||
sender pointed at localhost:1025 by this chart (see below). Nothing listens
|
||||
there, so the aggregate answers 503 DOWN on a completely healthy release,
|
||||
for the lifetime of that release:
|
||||
THE THREE HEALTH PATHS AND WHAT EACH ONE ANSWERS FOR
|
||||
This chart sets two health settings in the deployment template, and they are
|
||||
what makes the three paths mean different things:
|
||||
|
||||
{"components":{ ... "mail":{"status":"DOWN"} ... },"status":"DOWN"}
|
||||
MANAGEMENT_ENDPOINT_HEALTH_GROUP_READINESS_INCLUDE=readinessState,db
|
||||
MANAGEMENT_HEALTH_MAIL_ENABLED=false
|
||||
|
||||
Everything else in it — db, ping, diskSpace, ssl — is UP. The probes this
|
||||
chart configures deliberately use the /actuator/health/liveness and
|
||||
/actuator/health/readiness GROUPS instead, which contain only the
|
||||
application's lifecycle state and answer 200. Any external monitor you point
|
||||
at this release has to use the same two paths, or it will page you forever
|
||||
about a mail server you never configured.
|
||||
/actuator/health everything the release actually runs: db,
|
||||
diskSpace, ping, ssl and the two lifecycle
|
||||
states. 200 UP on a healthy release. The mail
|
||||
indicator is NOT in it — see the mail section.
|
||||
→ point external monitoring here. This is the only path that also fails on
|
||||
diskSpace or ssl.
|
||||
|
||||
WHAT A READY POD DOES NOT PROVE
|
||||
Those two groups report lifecycle state and nothing else — the datasource is
|
||||
not in them. Measured here: with the database scaled away underneath a
|
||||
running pod, both groups kept answering 200, the pod stayed 1/1 Ready and was
|
||||
never restarted. The database is verified exactly once, at boot, by the
|
||||
migration.
|
||||
/actuator/health/readiness readinessState AND db. Goes DOWN (503) on a
|
||||
database outage, which takes the pod out of Ready
|
||||
and stops traffic reaching it.
|
||||
→ the readinessProbe.
|
||||
|
||||
Know the resulting picture in advance: during a database outage the pod stays
|
||||
Ready, its endpoint keeps receiving traffic{{ if .Values.ingress.enabled }}, the Ingress keeps routing to
|
||||
it{{ end }}, and users get application errors while Kubernetes reports everything
|
||||
healthy. Nothing in the pod's status or restart count points at the database.
|
||||
Look there first anyway.
|
||||
/actuator/health/liveness livenessState alone, deliberately WITHOUT db.
|
||||
Stays 200 through a database outage.
|
||||
→ the livenessProbe and the startupProbe.
|
||||
|
||||
The split is the point: a database outage has to keep traffic away, but
|
||||
restarting the pod does not bring the database back. Adding db to the
|
||||
liveness group would turn every outage into a crash loop on top of the
|
||||
outage. Measured on this chart, with the database scaled to zero underneath a
|
||||
running pod: readiness 503, liveness 200, pod 0/1 Ready, restart count 0 —
|
||||
the pod left Ready about 20 seconds after the database went away.
|
||||
|
||||
The other half of that measurement matters just as much: when the database
|
||||
came back, the pod returned to Ready by itself within about five seconds,
|
||||
still with restart count 0. Nothing had to be rolled, deleted or scaled by
|
||||
hand. That is what leaving the liveness group alone buys — the outage is
|
||||
ridden out and then simply over.
|
||||
|
||||
How readiness actually fails is worth knowing before you tune it. The db
|
||||
indicator does not answer 503 quickly — it waits out the JDBC connection
|
||||
timeout first, measured at 30 seconds against a database whose Service has no
|
||||
endpoints at all. readinessProbe.timeoutSeconds is 5, so the kubelet gives up
|
||||
long before the answer arrives and logs
|
||||
|
||||
Readiness probe failed: ... context deadline exceeded
|
||||
|
||||
rather than a 503. The pod leaves Ready either way, and faster; the 503 is
|
||||
what a client with patience sees. Raising timeoutSeconds past the JDBC
|
||||
timeout would swap the timeout for the 503 and delay the reaction — there is
|
||||
no reason to.
|
||||
|
||||
WHAT A READY POD DOES AND DOES NOT PROVE
|
||||
Ready now means the pod can reach the database — that is checked on every
|
||||
readiness period, not once at boot. It still does not mean the identity
|
||||
provider is reachable: OIDC is resolved once at startup (see below) and no
|
||||
health indicator watches it afterwards, so a provider that goes down after
|
||||
the rollout leaves a Ready pod on which nobody can log in.
|
||||
|
||||
Ready also proves reachability, not correctness: the db indicator validates a
|
||||
connection, it never looks at the schema. Measured here — a database that came
|
||||
back EMPTY put the pod straight back to Ready with /actuator/health at 200,
|
||||
while not one table existed in it. A restored-but-wrong database reads as
|
||||
healthy, so a probe going green after a database incident is not on its own
|
||||
evidence that the data came back with it.
|
||||
|
||||
A database outage now shows up in the pod's status: READY drops to 0/1 while
|
||||
STATUS stays Running and RESTARTS stays put. That combination — running, not
|
||||
restarting, not Ready — is the signature of a dependency outage rather than
|
||||
of a broken release. The reason is in the endpoint, not in the log:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} get pod -l app={{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80
|
||||
curl -s localhost:8080/actuator/health
|
||||
|
||||
The body names components only when show-details is on; the chart does not
|
||||
set it, so the aggregate answers {"status":"DOWN","groups":[...]} and the
|
||||
status code carries the message. To see WHICH component is down, add
|
||||
|
||||
urlaubsverwaltung.env:
|
||||
- name: MANAGEMENT_ENDPOINT_HEALTH_SHOW_DETAILS
|
||||
value: always
|
||||
|
||||
and know what that publishes to anyone who can reach the endpoint: the
|
||||
database product, the container's filesystem path and free bytes, the
|
||||
certificate chains, and the stack-trace class of whatever failed.
|
||||
|
||||
Note also that there is no shell in this image: "kubectl exec" cannot open
|
||||
one, and neither can a debug sidecar sharing the process namespace run the
|
||||
@@ -241,25 +297,61 @@ MAIL AND THE APPLICATION URL ARE PLACEHOLDERS UNTIL YOU SET THEM
|
||||
UV_MAIL_APPLICATIONURL = http://localhost:8080
|
||||
|
||||
Nothing in this pod listens on localhost:1025, so no notification mail leaves
|
||||
the release and the mail health indicator stays DOWN. UV_MAIL_APPLICATIONURL
|
||||
is what the app puts into the links inside those mails — it is NOT derived
|
||||
from ingress.domain. Left as it is, every link points at the reader's own
|
||||
machine.
|
||||
the release. UV_MAIL_APPLICATIONURL is what the app puts into the links
|
||||
inside those mails — it is NOT derived from ingress.domain. Left as it is,
|
||||
every link points at the reader's own machine.
|
||||
|
||||
urlaubsverwaltung.env IS ADDITIVE ONLY, AND A COLLISION BREAKS THE UPGRADE
|
||||
BECAUSE that wiring is a placeholder, the chart sets
|
||||
|
||||
MANAGEMENT_HEALTH_MAIL_ENABLED=false
|
||||
|
||||
and the mail indicator is gone from /actuator/health. Otherwise the aggregate
|
||||
would answer 503 DOWN on a perfectly healthy release for its whole lifetime,
|
||||
and any monitor pointed at it would page forever about a mail server nobody
|
||||
configured.
|
||||
|
||||
THE PART THAT BITES LATER: the day you configure real mail delivery, switch
|
||||
the indicator back on in the SAME change —
|
||||
|
||||
urlaubsverwaltung.env:
|
||||
- name: MANAGEMENT_HEALTH_MAIL_ENABLED
|
||||
value: "true"
|
||||
|
||||
Left at false it does not report a false alarm, it reports nothing: the
|
||||
endpoint answers 200 while the mail server is unreachable and notifications
|
||||
are silently not delivered. The setting is only honest while there is no mail
|
||||
server to be honest about.
|
||||
|
||||
Note that this variable repeats a name the chart already sets, so per the rule
|
||||
below it only takes effect when it goes in at INSTALL time. Put into an
|
||||
already installed release it does nothing at all, and says so nowhere: helm
|
||||
reports a successful upgrade while the container keeps "false".
|
||||
|
||||
urlaubsverwaltung.env IS ADDITIVE ONLY, AND A COLLISION IS LOST ON UPGRADE
|
||||
The entries are appended AFTER the chart's own list{{ if $envNames }} (this release appends:
|
||||
{{ join ", " $envNames }}){{ end }}. An entry that repeats a name the
|
||||
chart already sets does not replace it — it renders the variable twice, and
|
||||
that has a sharp edge: adding such a name to an ALREADY INSTALLED release
|
||||
makes the next upgrade fail in the API server, before anything changes, with
|
||||
what happens to that pair depends on WHEN it first appears:
|
||||
|
||||
UPGRADE FAILED: cannot patch "{{ .Release.Name }}-{{ .Chart.Name }}-deployment" ...
|
||||
The order in patch list ... doesn't match $setElementOrder list
|
||||
AT INSTALL both entries reach the pod and the later one — yours — wins in the
|
||||
container. The API server accepts it with a warning:
|
||||
|
||||
because env entries are merged by name and a duplicate name breaks that
|
||||
ordering. A fresh install with the same values goes through, so the failure
|
||||
looks arbitrary until you know it. Treat the variables above as fixed for the
|
||||
life of a release: to really change one, reinstall.
|
||||
Warning: spec.template.spec.containers[0].env[N]: hides previous definition
|
||||
of "NAME", which may be dropped when using apply
|
||||
|
||||
AT UPGRADE that warning comes true. Kubernetes merges env by name, so the pair
|
||||
collapses back to ONE entry and the surviving value is the CHART's, not yours.
|
||||
Nothing fails: helm prints "Release has been upgraded", the revision counts up,
|
||||
and the override is simply gone. Measured on this chart (helm 3.19.0,
|
||||
Kubernetes 1.35): "helm get manifest" shows the variable twice while the live
|
||||
Deployment shows it once, with the chart's value.
|
||||
|
||||
So the trap is silence, not an error. Treat the variables above as fixed for
|
||||
the life of a release: to really change one, reinstall. And never read a green
|
||||
upgrade as proof that an override took effect — read it off the object:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} get deployment {{ .Release.Name }}-{{ .Chart.Name }}-deployment \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].env}'
|
||||
|
||||
Anything sensitive belongs in valueFrom.secretKeyRef rather than a plain
|
||||
value: a value is stored in the pod spec and readable by anyone who can read
|
||||
|
||||
@@ -50,6 +50,32 @@ spec:
|
||||
secretKeyRef:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
|
||||
key: password
|
||||
# Health indicators and probe groups
|
||||
#
|
||||
# Spring Boot creates the "liveness" and "readiness" groups by
|
||||
# itself here (measured: /actuator/health answers
|
||||
# {"groups":["liveness","readiness"]} with nothing configured), but
|
||||
# the readiness group ships with readinessState ALONE. Without the
|
||||
# line below a total database outage leaves /actuator/health/readiness
|
||||
# at 200 and the pod 1/1 Ready while every request fails (#218).
|
||||
# Naming the group's members REPLACES the default, so readinessState
|
||||
# has to be repeated. "db" is the datasource indicator's real name,
|
||||
# read off the running endpoint.
|
||||
- name: MANAGEMENT_ENDPOINT_HEALTH_GROUP_READINESS_INCLUDE
|
||||
value: "readinessState,db"
|
||||
# The liveness group is deliberately NOT extended: a database
|
||||
# outage has to keep traffic away, but restarting the pod does not
|
||||
# bring the database back — it would only add a crash loop to the
|
||||
# outage.
|
||||
#
|
||||
# The mail indicator is switched off because the mail wiring below
|
||||
# points at localhost:1025, where nothing listens. Left on, it holds
|
||||
# the aggregate /actuator/health at 503 DOWN for the whole life of a
|
||||
# healthy release (#217). Anyone who later configures real mail
|
||||
# delivery has to switch it back on via urlaubsverwaltung.env, or
|
||||
# the endpoint stays blind to a broken mail server.
|
||||
- name: MANAGEMENT_HEALTH_MAIL_ENABLED
|
||||
value: "false"
|
||||
# Mail
|
||||
- name: UV_MAIL_FROM
|
||||
value: urlaubsverwaltung@example.org
|
||||
|
||||
@@ -45,6 +45,42 @@ urlaubsverwaltung:
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
# HEALTH INDICATORS — TWO SETTINGS THE CHART FIXES IN THE TEMPLATE
|
||||
#
|
||||
# The chart sets these two as plain environment variables, not as values,
|
||||
# because urlaubsverwaltung.env below already overrides anything:
|
||||
#
|
||||
# MANAGEMENT_ENDPOINT_HEALTH_GROUP_READINESS_INCLUDE=readinessState,db
|
||||
# puts the datasource into the readiness group, so a database outage
|
||||
# takes the pod out of Ready instead of leaving it serving errors (#218).
|
||||
# The liveness group stays untouched on purpose — an outage must keep
|
||||
# traffic away, not restart the pod in a loop.
|
||||
# MANAGEMENT_HEALTH_MAIL_ENABLED=false
|
||||
# removes the mail indicator, which otherwise pins the aggregate
|
||||
# /actuator/health at 503 DOWN forever, because the chart's mail wiring
|
||||
# points at localhost:1025 where nothing listens (#217).
|
||||
#
|
||||
# THE CONSEQUENCE TO KNOW BEFORE IT BITES: whoever later configures real mail
|
||||
# delivery (SPRING_MAIL_HOST/SPRING_MAIL_PORT via urlaubsverwaltung.env) MUST
|
||||
# switch the indicator back on in the same change:
|
||||
#
|
||||
# env:
|
||||
# - name: MANAGEMENT_HEALTH_MAIL_ENABLED
|
||||
# value: "true"
|
||||
#
|
||||
# Left at false, /actuator/health keeps answering 200 while the configured
|
||||
# mail server is unreachable and no notification mail is delivered. The
|
||||
# setting trades a permanently false alarm for silence, and silence is only
|
||||
# correct as long as there is nothing to report about.
|
||||
#
|
||||
# That override repeats a name the chart already sets, and env is additive, so
|
||||
# it renders the variable twice. At INSTALL time both entries reach the pod and
|
||||
# the later one — yours — wins in the container. Added to an ALREADY INSTALLED
|
||||
# release it is lost instead: Kubernetes merges env by name, the pair collapses
|
||||
# to the chart's own entry, and helm still reports a successful upgrade. The
|
||||
# override does not fail loudly, it fails silently. See the "env IS ADDITIVE
|
||||
# ONLY" section of NOTES.txt — the switch has to go in at install time, and
|
||||
# after any such change the result belongs verified on the live Deployment.
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
port: 8080
|
||||
|
||||
Reference in new issue
Block a user