Files
cozystack/hack
Aleksei Sviridkin 3e4030d4d9 feat(kubernetes): per-pool kernel modules for Talos worker nodes (#3571)
## What this PR does

Implements gap 1 of #3563: a pool-scoped `kernelModules` field on
`KubernetesNodes`, emitted as Talos `machine.kernel.modules`.

A Talos system extension installs a kernel module but does not load it —
loading is `machine.kernel.modules`' job. The worker machine config had
no such block and no values key could add one, so a GPU pool on Talos
workers was unusable as shipped: the extension supplied the NVIDIA
module, nothing loaded it, and the failure was silent end to end. The VM
held the PCI device, the node advertised no GPU, and no component logged
an error. Found in a production 1.6.0 environment, where the worker
rollover took the 4 GPUs of a tenant cluster out of service with no
error anywhere.

### Design: three states, and no default

The shape was arbitrated in #3563 (option 3 of the three proposed
there).

- **absent** — the chart chooses. A pool holding at least one
`nvidia.com/*` GPU gets `nvidia` (with `NVreg_NvLinkDisable=1`),
`nvidia_uvm`, `nvidia_drm`, `nvidia_modeset`; any other pool gets no
`kernel` block at all.
- **non-empty list** — taken verbatim, replacing the chart's choice
outright.
- **`[]`** — explicit opt-out: no modules even on a GPU pool.

The order is not cosmetic: Talos loads the list in sequence, `nvidia`
has to come first because the other three depend on it, and `nvidia_uvm`
is what CUDA unified memory needs. This is the order validated against a
production GB202 passthrough node. `NVreg_NvLinkDisable=1` is what the
gpu-operator driver container applies on non-Talos workers; on Talos
that container has to be off, so nothing else would carry it, and
without it the driver waits forever for an NVLink fabric that the
passed-through GPU does not have.

The three states are distinguishable only because the field carries no
default — no entry in `values.yaml`, no `default` in
`values.schema.json`. A default of `[]` would collapse "absent" into
"opted out" for every pool and make the automatic set unreachable. The
generated parameter table prints `[]` in the Default column for any
array with no default; the field description says so explicitly. For the
same reason `keysOrder` does not list the field, so the dashboard
renders it at the bottom of the form.

Automatic emission is keyed on the `nvidia.com/` resource prefix, not on
the mere presence of `gpus`, so an AMD pool is not given NVIDIA modules
on a guess.

The field loads modules; it does not put them in the image. Which
extensions the image carries is the pool's schematic, so a GPU pool
points `osImage.factory.schematicID` (or `osImage.builtin.schematicID`)
at a schematic carrying the NVIDIA extensions.

Module names and parameters reach the reconcile Job's unquoted heredoc,
so both are validated at render, and the emitted list is rebuilt from
those two fields alone: the schema does not prune unknown keys, and any
other key passed through `toYaml` would reach the shell unchecked. A
parameter must be printable ASCII with no whitespace, and must not
contain `$`, a backtick, a backslash or quotes. The ASCII half is there
because RE2's `\s` and `[:cntrl:]` are ASCII-only: without it U+2028 and
U+2029 pass, `toYaml` writes them raw, and Helm's YAML parser reads them
as line breaks that end the Job's `command` block scalar early. Inside
ASCII the guard denies what the shell expands rather than allowing a
fixed set, because NVIDIA's documented multi-value parameter is
semicolon-separated.

### Rollout, and #3523

A pool that resolves to no modules renders the machine config it
rendered before, byte for byte: `tests/render_snapshot_test.yaml` and
its committed golden snapshot pass unmodified, including the
`talos-reconcile` Job's content-hash name.

For a pool that does get modules, what happens depends on how its
`TalosConfigTemplate` is named, and the field description states both
cases:

- **On main today** the template has a fixed name and an immutable spec,
so the reconcile Job's apply of a changed machine config is refused
(#3515). Running workers keep what they booted with, and new workers
boot from the old template too. This covers the upgrade itself: every
existing pool holding an `nvidia.com/*` GPU with the field unset gets
the automatic set, and its apply is refused the same way. Only pools
created afterwards pick the modules up.
- **Once #3523 lands** the template is named by its content, so the same
change renames it and CAPI rolls the pool. On the upgrade that brings
both, every existing pool holding an `nvidia.com/*` GPU with the field
unset rolls onto the automatic set. That roll is the intended outcome —
those pools have no working driver without it — but it is a roll, and
the release note says so.

### Tests

- `packages/apps/kubernetes-nodes/tests/kernel_modules_test.yaml`, 12
cases: the NVIDIA set and its order for an `nvidia.com/*` pool, no
`kernel` block for a pool with neither GPUs nor modules, an explicit
list taken verbatim including `parameters`, an explicit list replacing
the NVIDIA default, `[]` opting out while leaving the `gpu=on` label
intact, no NVIDIA assumption for another vendor, a semicolon-separated
parameter accepted, a hostile module name and a hostile parameter each
rejected, a parameter carrying U+2028 and one carrying U+2029 each
rejected with the kernelModules message rather than a YAML parse error,
and an unvalidated extra key on an item dropped. The patterns pin
absolute indentation, because `modules:` one level out is still valid
YAML and silently loads nothing.
- `hack/talos-reconcile-heredoc_test.bats` lists `kernelModules` in its
validated arm and gains two execution-level cases: a hostile name and a
hostile parameter each refused at render, and a semicolon-separated
parameter surviving the real shell literally. Loosening the guards to
`^.+$` makes the refusal case fail.
- `helm unittest packages/apps/kubernetes-nodes`: 31 suites, 174 tests,
12 snapshots, all passing. `hack/cozytest.sh
hack/talos-reconcile-heredoc_test.bats`: 8 passed.
- `make generate` in the package, and `controller-gen object` for the
`api/apps/v1alpha1` submodule.

### Not in scope

The rest of #3563: the `nvidia-operator-validator` → device-plugin chain
that cannot validate an OS-provided driver, CDI spec generation and the
`nvidia` containerd runtime default, and the `maxSurge` /
`nodeStartupTimeout` defaults.

### Screenshots

No UI changes.

### Downstream repositories

- [ ] No downstream repository is affected by this change
- [x] [cozystack/website](https://github.com/cozystack/website) -
follow-up: https://github.com/cozystack/website/issues/643
- [x]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
https://github.com/cozystack/terraform-provider-cozystack/issues/20
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:

Both follow-ups are issues rather than PRs.
`terraform-provider-cozystack` is hand-written, so the field needs a
schema entry, a model entry and an expand/flatten pair on
`KubernetesNodes`; the undefaulted three-state behaviour is exactly
where that provider's habit of sending its own defaults would break
users silently, since materialising `kernel_modules = []` would disable
the automatic NVIDIA set for every GPU pool managed through Terraform.
`website` needs the three-state behaviour written down, the GPU pool
example pointed at `osImage.factory.schematicID`, and the Blackwell
constraint documented: on GB202 the schematic must carry
`siderolabs/nvidia-open-gpu-kernel-modules-production`, because with the
proprietary extension the module loads, `/dev/nvidia0` appears, and
`nvidia-smi -L` then reports `No devices found`.

The `hack/talos-reconcile-heredoc_test.bats` change adds test cases and
does not change node prerequisites, so `ansible-cozystack` and `talm`
are not reached. No other repository in the map is touched: no package,
platform component, bundle, variant or release asset is added, and
`ApplicationDefinition` semantics are unchanged.

### Release note

```release-note
feat(kubernetes): Worker node pools (`KubernetesNodes`) accept a `kernelModules` field, emitted as Talos `machine.kernel.modules`. A Talos system extension installs a kernel module without loading it, so a GPU pool previously came up with no working driver and no way to fix it through values. Left unset, a pool holding at least one `nvidia.com/*` GPU now loads `nvidia` (with `NVreg_NvLinkDisable=1`), `nvidia_uvm`, `nvidia_drm` and `nvidia_modeset` automatically; an explicit list replaces that set, and an explicit `[]` opts out. The modules still have to be in the image: point the GPU pool's `osImage.factory.schematicID` at a schematic carrying the NVIDIA extensions, and on Blackwell (GB202) use the open kernel modules extension, not the proprietary one. Pools without GPUs and without the field render exactly what they rendered before. NOTE: while the worker `TalosConfigTemplate` keeps a fixed name (#3515), a changed module list does not reach a pool that already exists — including the automatic set on existing GPU pools — and only pools created afterwards pick it up. Once that template is named by its content (#3523), the same change rolls the pool, so existing GPU pools that leave the field unset are rolled onto the automatic set.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Worker node pools can now configure Talos kernel modules, including
optional module parameters.
* When no list is specified, NVIDIA GPU pools automatically receive
NVIDIA modules; other pools receive none. A custom list replaces this
selection, while an empty list disables modules.
* Invalid module names or parameters are rejected during configuration
rendering.
* **Documentation**
* Added guidance on module selection, parameters, image-extension
requirements, and pool rollout behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-29 21:43:43 +03:00
..
2026-02-17 22:54:12 +01:00