satisfactory: dev and server are null-safe as of 7.0.1
Chart page: version snapshot, the dev and server blocks documented as erasable, a table of what a single nulled key used to render (empty Service ports, a sizeless PVC), and why server.resources is the one key without its own default there. CI section: the notes are rendered by no stage of this chart's pipeline, and how to render them without a cluster. Style guide 2: the rule is about the shape, not about probes, security contexts and resources specifically - it covers every chart-owned block a template dereferences, and a half-protected chart is worse than an unprotected one. Lessons learned: new entry for the half-protected chart and for NOTES.txt as template code the pipeline is blind to.
3 files changed
+30
-4
No files matched your search
+5
-1
@@ -132,7 +132,7 @@ Before putting an Ingress behind `ingress.enabled`, check whether anything else
|
||||
|
||||
This happened in six charts, and matrix-synapse shows why it matters more than the count: its delegation nginx serves `ingress.domain` in the `/.well-known/matrix/{server,client}` endpoints through which **federation finds the homeserver**. Gating without the guard publishes `{"m.server": ":443"}` — rendered cleanly, pod green, federation broken. Check per chart and in both directions: in `calibre-web-automated` nothing outside the Ingress reads the domain, and there the gate frees both values.
|
||||
|
||||
### 2. Probes, security contexts and resources go through the null-safe helper, never behind an `if`
|
||||
### 2. Every chart-owned block goes through the null-safe helper, never behind an `if`
|
||||
|
||||
An override written without children is YAML null and **erases** the block: the container renders `securityContext: null` (unhardened, healthy, silent), a probe disappears, or `resources` loses its requests and computed limits.
|
||||
|
||||
@@ -140,6 +140,10 @@ The `defaultedDict` helper defines the opposite semantics — an empty block mea
|
||||
|
||||
Do **not** wrap these blocks in `{{- if }}`: a guard does not render a visible `null`, it removes the probe entirely — the same bug with no evidence left.
|
||||
|
||||
**The rule is about the shape, not about those three block kinds.** It applies to every chart-owned block a template dereferences, plain app-config blocks included. `satisfactory` shipped 7.0.0 with `game` null-safe and its neighbours `dev` and `server` not, and half-safe is worse than unsafe: whoever sees `game=null` fall back assumes `server=null` does too. The single erased key is the quieter half — a nulled `server.gamePort` rendered `port:` and `nodePort:` empty into the Service, a nulled `server.storage` a PVC asking for no size, and nothing rejects either until the API server does. A block deliberately left un-defaulted must say so in the schema **and** survive the check that erasing its *parent* still renders something usable; `satisfactory.storage` carried such a note and it did not hold.
|
||||
|
||||
**And `helm template` does not render `NOTES.txt`.** A value read there stays broken after a fix that looks green in every render, until a real `helm install` — and for a chart excluded from the install-test, no stage renders the notes at all. `helm install --dry-run=client` is no way around it: it still wants a reachable API server (measured on helm v3.22.0). Cluster-free method: in a throwaway copy of the chart, wrap the verbatim `NOTES.txt` body in a `{{- define }}` and `include` it into a scratch ConfigMap, which `helm template` *does* render — same engine, same root context, only the sink differs.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -42,6 +42,14 @@ Wrapping the block in `{{- if }}` is the same bug with the evidence removed —
|
||||
|
||||
**Now:** probes, security contexts and resources go through the null-safe helper, whose semantics are the opposite — an empty block means "chart defaults apply", while an explicitly set value still wins, `false` and `0` included. See [§12.2](Chart-Style-Guide#12-values-that-must-not-lie).
|
||||
|
||||
## The half-protected chart, and the template no pipeline renders
|
||||
|
||||
The fix above was applied per block, and `satisfactory` reached 7.0.0 with `game` null-safe and its two neighbours not. That is worse than none of them being safe: an operator who watches `--set game=null` fall back cleanly concludes the chart handles erased blocks, and `--set server=null` then aborts the render. The quieter half was the single erased key — a nulled `server.gamePort` rendered a Service with `port:` and `nodePort:` empty, a nulled `server.storage` a PVC asking for no size. Nothing rejects either until the API server does, at apply time.
|
||||
|
||||
The second finding is the one that generalises. `server` was read in **four** files, and the fourth was `templates/NOTES.txt` — which **`helm template` does not render**. A fix confined to the manifests would have passed every local render, passed lint, passed the pipeline, and still aborted on the first real `helm install`. For a chart excluded from the install-test, as this one is, no stage renders the notes at all: it is template code the whole pipeline is blind to. `helm install --dry-run=client` is not the workaround — it still wants a reachable API server.
|
||||
|
||||
**Now:** every chart-owned block a template dereferences goes through the fallback helper, not just the three kinds named above; a block deliberately left un-defaulted says so in the schema *and* has to survive the check that erasing its **parent** still renders something usable. And when a values change touches a chart's notes, the notes are rendered explicitly, without a cluster: wrap the verbatim `NOTES.txt` body in a `{{- define }}` in a throwaway copy of the chart and `include` it into a scratch ConfigMap — same engine, same root context, and `helm template` renders that.
|
||||
|
||||
## The strict schema that had to meet reality first
|
||||
|
||||
A schema draft demanded a boolean for `tikaGotenberg.enabled`. All three live paperless releases write it as the string `"true"`. Rolling that out would have broken them at upgrade time.
|
||||
|
||||
+17
-3
@@ -4,7 +4,7 @@ Deploys a [Satisfactory](https://www.satisfactorygame.com/) dedicated server ([w
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Chart version | `7.0.0+upv1.9.10` |
|
||||
| Chart version | `7.0.1+upv1.9.10` (snapshot; `Chart.yaml` is the source of truth) |
|
||||
| App version | `v1.9.10` (image `ghcr.io/wolveix/satisfactory-server`) |
|
||||
| Workloads | StatefulSet (1 replica) |
|
||||
| Storage | one RWO PVC `<release>-satisfactory-vc`, storage class **hardcoded** `longhorn` |
|
||||
@@ -65,6 +65,8 @@ This chart keeps `env`, the three probes and `securityContext` at the **top leve
|
||||
|---|---|---|---|
|
||||
| `dev.debug` | bool/string | `false` | Passed to the image as `DEBUG`. Changes what you read, not what the server serves — but it makes the log noticeably larger while the pod is pulling 10 GB. |
|
||||
|
||||
The block may be erased (`null`) and falls back to this default. Before **7.0.1** it did not: `--set dev=null`, or a leftover `dev:` whose last sub-key was deleted, aborted the render with a nil-pointer error at `.Values.dev.debug`.
|
||||
|
||||
### `game`
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
@@ -82,11 +84,21 @@ The whole block may be erased (`null`) — the chart then falls back to exactly
|
||||
| `server.replicas` | int | `1` | Replica count; `0` is allowed. More than one does not work — see [One replica](#one-replica-one-volume-one-port). |
|
||||
| `server.gamePort` | int | `30777` | Game traffic (UDP) **and** the server's HTTPS API (TCP), as `containerPort` and as a pinned `nodePort`. Also passed to the image as `SERVERGAMEPORT`, so the port a player types is the port the server believes it runs on. |
|
||||
| `server.messagingPort` | int | `30888` | Messaging port (TCP), `SERVERMESSAGINGPORT`, likewise pinned as a `nodePort`. |
|
||||
| `server.storage` | string | `20Gi` | Size of the data PVC — game files *and* saves. Not null-safe on purpose: an erased value yields an invalid PVC rather than a silently degraded workload. |
|
||||
| `server.storage` | string | `20Gi` | Size of the data PVC — game files *and* saves. Since **7.0.1** an erased value falls back to this default; up to 7.0.0 it was deliberately left un-defaulted and rendered a PVC asking for no size at all. |
|
||||
| `server.resources.limitFactor` | number | `2` | Memory and ephemeral-storage limits are computed as factor × request. **`2`, not the repo-wide `3`, on purpose** — that is what holds the memory limit at the 8Gi this chart has always published ([style guide §12.4](Chart-Style-Guide#12-values-that-must-not-lie)). |
|
||||
| `server.resources.requests` | object | `memory: 4Gi`, `cpu: 1`, `ephemeral-storage: 5Gi` | |
|
||||
| `server.resources.limits` | object | unset | An explicit limit always wins. A CPU limit is only rendered when set here — CPU is compressible, so an unlimited container is throttled rather than killed. |
|
||||
|
||||
The block, and every single key in it, may be erased (`null`) and falls back to exactly these defaults. That is new in **7.0.1** and it closed two different failures. The loud one: `--set server=null` aborted the render at `.Values.server.replicas`. The quiet one, which is the reason to care — a *single* nulled key rendered without complaint into a manifest nothing rejects until the API server sees it:
|
||||
|
||||
| Erased key | What 7.0.0 rendered |
|
||||
|---|---|
|
||||
| `server.gamePort` / `server.messagingPort` | Service with `port:` and `nodePort:` empty, StatefulSet with an empty `containerPort` |
|
||||
| `server.storage` | PVC requesting no size |
|
||||
| `server.replicas` | fell back to `1` already (the replicas helper was null-safe) |
|
||||
|
||||
`server.resources` is the one key with no default of its own here — it is passed through to the resources fallback, which carries `limitFactor: 2` and the requests. Keeping that in one place is deliberate: a second copy in the `server` fallback would be a third place for the numbers to drift apart.
|
||||
|
||||
### `env`
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
@@ -108,7 +120,7 @@ The whole block may be erased (`null`) — the chart then falls back to exactly
|
||||
| `securityContext.pod` | object | non-root `1000:1000`, `fsGroup: 1000`, `fsGroupChangePolicy: OnRootMismatch`, `seccompProfile: RuntimeDefault` | Passed into the pod spec. The uid is pinned because the image supports rootless operation **only** as its own `steam` user; its entrypoint rejects any other non-root uid/gid. `OnRootMismatch` is deliberate ([style guide §12.4](Chart-Style-Guide#12-values-that-must-not-lie)) — see [The uid is pinned](#the-uid-is-pinned-and-the-chown-is-skipped). |
|
||||
| `securityContext.container` | object | `readOnlyRootFilesystem: true`, all caps dropped, no privilege escalation, non-root | Works because the chart mounts emptyDirs at `/tmp` and over the steam user's `~/.steam`, `~/.config` and `~/.local`; everything else is written to the PVC at `/config`. |
|
||||
|
||||
Probe, `securityContext`, `game` and `resources` blocks may be erased (`null`) — the chart then falls back to exactly these defaults instead of rendering an unhardened, unprobed or unaccounted container.
|
||||
Probe, `securityContext`, `game` and `resources` blocks may be erased (`null`) — the chart then falls back to exactly these defaults instead of rendering an unhardened, unprobed or unaccounted container. Since **7.0.1** that holds for **every** chart-owned block, `dev` and `server` included, so no block in this chart is half-safe any more.
|
||||
|
||||
## Operating notes
|
||||
|
||||
@@ -183,6 +195,8 @@ Back up `/config` before setting `game.experimental: true`. A save opened by an
|
||||
|
||||
An excluded chart gets **no install-test job at all** — it is dropped from the matrix rather than skipped inside a job, so no check can report green for a chart whose image was never pulled; the PR's job summary lists it with this justification instead ([CI/CD](CI-CD)). Everything else still runs: strict lint, package, the version-bump check and the resource-quantity render test. **No CI job has ever started this workload — treat the first rollout in a new cluster as the real test.**
|
||||
|
||||
One consequence is easy to miss: none of those steps renders `templates/NOTES.txt`. `helm lint` and `helm template` skip it by design, and the install-test — the only stage that would have run it — does not exist for this chart. This chart's notes read `server` and `dev` heavily, so a values bug can be invisible in every check and still abort a real `helm install`; that is exactly how it went in 7.0.0 (see the `server` block above). Render the notes by hand when changing them: `helm install --dry-run=client` still wants a reachable API server, so the cluster-free way is to wrap the verbatim `NOTES.txt` body in a `{{- define }}` in a throwaway copy of the chart and `include` it into a scratch ConfigMap that `helm template` does render.
|
||||
|
||||
## See also
|
||||
|
||||
- [Chart style guide](Chart-Style-Guide) — the rules this chart implements, and the two deviations it is allowed
|
||||
|
||||
Reference in new issue
Block a user