Skip to content

feat(kubernetes): per-node-group kernel modules and Talos schematic - #3571

Open
mattia-eleuteri wants to merge 8 commits into
cozystack:mainfrom
mattia-eleuteri:feature/kubernetes-nodegroup-kernel-modules
Open

feat(kubernetes): per-node-group kernel modules and Talos schematic#3571
mattia-eleuteri wants to merge 8 commits into
cozystack:mainfrom
mattia-eleuteri:feature/kubernetes-nodegroup-kernel-modules

Conversation

@mattia-eleuteri

@mattia-eleuteri mattia-eleuteri commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Makes a GPU node group expressible on Talos workers, which today it is not. Two node-group-scoped fields, one commit each, both landing in the two charts that render a worker pool:

Field Fixes
kernelModules The extension ships the NVIDIA module and nothing loads it, so the GPU never initialises.
schematicID talos.schematicID is cluster-wide, so the NVIDIA schematic reaches non-GPU node groups and reboots them every ~70 minutes.

They are two halves of one problem: fixing only the first leaves an operator who follows the documentation — set the NVIDIA schematic, declare the modules — with every non-GPU node group in a reboot cycle. The second field was found in production this morning, after the first commit was already open for review, and is filed here rather than separately because the two are not independently usable.

Gap 1: nothing loads the kernel modules

Implements gap 1 of #3563: a values surface for the kernel modules a Talos worker node group loads at boot.

A Talos system extension installs a kernel module but does not load it — loading is machine.kernel.modules' job. Neither chart emitted that block and no values key could add one, so a GPU node group on the Talos workers introduced in 1.6.0 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. The only workaround was a hand-written TalosConfigTemplate, which is impractical because its spec is immutable. 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.

The field lands in both charts that render a worker pool, as flagged in the issue:

Chart Field
packages/apps/kubernetes nodeGroups.<name>.kernelModules
packages/apps/kubernetes-nodes kernelModules at the root, as gpus already is

Design: three states, and no default

The shape was arbitrated in #3563 (option 3 of the three proposed there) rather than decided here, because terraform-provider-cozystack transcribes values.schema.json by hand and a later rename breaks that side too. Andrei Kvapil (@kvaps) / Aleksei Sviridkin (@lexfrei): if you would rather have option 1 (explicit field only, no automatic set), say so — the change is small and local, and I will document the NVIDIA value to set instead.

  • absent — the chart chooses. A node group holding at least one nvidia.com/* GPU gets nvidia, nvidia_uvm, nvidia_drm, nvidia_modeset; any other group 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 node group.

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.

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 node group and make the automatic set unreachable, and a bare kernelModules: (null) fails schema validation under helm-unittest, which sees the null before Helm's coalescing drops it. Both traps are called out in comments where someone would be tempted to add one.

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

Loading the module is all this does. Which extension supplies it remains the schematic's business (talos.schematicID), and that part is a docs gap rather than a chart gap — see the website follow-up below.

No involuntary worker roll

A node group that resolves to no modules renders the machine config it rendered before this field existed, byte for byte. Verified against main: for a non-GPU group and for an AMD-GPU group, every rendered object is identical (the only diff is the per-render random Talos secrets, which helm template regenerates on every invocation), including the talos-reconcile Job's content-hash name — so the Job is not recreated and no worker is reconciled for this change alone. The existing hash assertion for the non-GPU group in talos_templates_test.yaml is untouched, which pins that property; the GPU group's expected hash moves, which is the point, since its config genuinely changed.

Gap 2: the schematic is cluster-wide, and that reboots every other node group

Found in production this morning, on a 1.6.0 cluster with one GPU node group and one non-GPU group. All four nodes of the non-GPU group were rebooting in a loop, each at almost exactly 4206 s of uptime, in staggered phases. Every pod on a node died at once (SandboxChanged, TaintManagerEviction), which is why every pod on a given node carried an identical restart count — about 26 over 30 hours.

The cause is that a Talos schematic is a fixed set of system extensions baked into one image, and Talos refuses to finish the boot sequence when an extension service in it cannot start:

[talos] task startAllServices (1/1): service "ext-nvidia-cdi-gen" to be "up", service "ext-nvidia-persistenced" to be "up"
[talos] task startAllServices (1/1): failed: 2 errors occurred:
[talos] phase startEverything (9/9): failed
[talos] boot sequence: failed        -> reboot

Both services need an NVIDIA card. On a node that has none they never come up, so the node reboots, forever. talos.schematicID is a single cluster-wide value feeding every node group's boot image, so a cluster that mixes GPU and non-GPU groups has no correct value to set: the GPU group needs the extensions and every other group is broken by them.

What makes this expensive to diagnose is that nothing reports it. kubelet starts before the failing phase, so the node holds Ready for the whole 70 minutes and kubectl get nodes looks healthy. kubectl top nodes shows up to 96% memory on small nodes just before the reboot, which reads as an OOM — it is page cache, and min_over_time(node_memory_MemAvailable_bytes[30h]) never drops below 1.2 GiB. The discriminant is the regularity: four nodes rebooting within a few seconds of the same uptime is a timer, never memory pressure.

So this adds an optional per-node-group schematicID that falls back to talos.schematicID, letting the NVIDIA schematic be scoped to the group that actually has the cards. Both consumers resolve it through one helper — the boot disk image the DataVolume pulls, and the installer image in the TalosConfigTemplate — because they have to agree: the installer is what an in-place Talos upgrade runs, so a mismatch would swap a node's extension set out from under it. A parity case covers both at once, so a chart that overrode one and not the other fails.

Unlike kernelModules this is deliberately not derived from gpus. A schematic ID is an opaque image-factory digest; the chart cannot know which one carries the NVIDIA extensions, and cannot synthesise one. Supplying it stays the operator's job — this PR only makes it possible to supply per group.

Unset renders byte-identically to before, so the content-hash-named KubevirtMachineTemplate keeps its name and no worker is rolled. Setting it does roll that group's workers, which is inherent rather than incidental: changing a node's boot image means replacing the node.

I could not find an existing issue for this; #3563 is the closest and covers the schematic only as "which one to use", not the per-group scoping. Happy to split it into its own issue if you prefer the paper trail.

Closing the parity gap this change would otherwise widen

packages/apps/kubernetes-nodes/tests/render-parity.sh compared the four pool objects and explicitly excluded the talos-reconcile Job, on the reasoning that the Job's content-hash name makes a divergence visible separately. That holds for one chart drifting over time; it does not hold for the two charts disagreeing with each other, which is exactly the risk of adding a field to a machine config both charts render from their own copy of the template. Adding the field into an unchecked duplication seemed worse than fixing the check, so the script now also compares the machine config the two Jobs apply.

That comparison is possible because the machine config refers to the release only through shell variables the Job expands at runtime (${RELEASE}, ${NS}, ${TALOS_TOKEN}, …), so the rendered text is release-name independent and any difference in it is a real difference in what the worker boots with. The Job as a whole is not comparable, since the two charts legitimately differ in release name, serviceAccountName and the RELEASE env. The extraction fails loudly rather than silently comparing two empty files if the Job or its heredoc marker ever changes shape.

Confirmed to be a real gate, not a passing no-op: reordering the module list in one chart's helper only makes the new MachineConfig(talos-reconcile) assertion fail, on the gpu and schematic cases (the schematic case also carries an nvidia.com/* GPU, so it inherits the automatic list), and the check passes on main's two charts unmodified.

Sequencing with #3523

Read together with #3523 (fix(kubernetes): name TalosConfigTemplate by content hash), which is mine and is rebased and mergeable. Aleksei Sviridkin (@lexfrei) agreed this surface goes first or together.

Before #3523 merges, kernelModules changes nothing for existing node groups. That is #3515: the TalosConfigTemplate apply is rejected server-side while the HelmRelease stays Ready, so a kernelModules added to a node group that already exists is silently not applied. Only node groups created afterwards get it, because their template does not exist yet. The release note says so explicitly rather than letting this read as a fix for running clusters. If both PRs go in the same release the caveat disappears; if this one ships alone, the note is the honest description.

schematicID is split across that line, and the split is worth being precise about. Its effect on the boot disk image goes through the KubevirtMachineTemplate, which is content-hash named and referenced by the MachineDeployment, so it lands on an existing node group today and the reboot loop is fixed without waiting for #3523. Its effect on the installer image goes through the TalosConfigTemplate and is therefore subject to #3515, so until #3523 an in-place Talos upgrade on a group with an overridden schematic would still run the previous schematic's installer. That is a narrower window than it sounds — the two only diverge during an upgrade — but it is the reason the two PRs belong in the same release.

Tests

  • New kernel_modules_test.yaml in both packages, 10 cases each: the NVIDIA set and its order for an nvidia.com/* group, no kernel block for a group 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 a non-NVIDIA vendor, a semicolon-separated module parameter accepted, a module name and a module parameter each rejected when they could inject shell into the reconcile Job, and an unvalidated extra key on an item dropped rather than copied through. The patterns pin absolute indentation rather than \s+, because modules: one level out is still valid YAML, is still accepted by the apiserver, and silently loads nothing.
  • New schematic_per_nodegroup_test.yaml / schematic_per_pool_test.yaml: the override reaching both the boot image and the installer, fallback to the cluster-wide value, an empty string treated as unset rather than rendering a URL with an empty path segment (which would 404 at the factory and hang the import), and — in the parent chart — a mixed cluster where the GPU group takes the NVIDIA schematic while md0 stays on the vanilla one.
  • render-parity.sh gains the machine-config comparison plus three cases (explicit kernelModules, a GPU pool opting out with [], and a per-pool schematicID).
  • make generate in both packages, and controller-gen object for the api/apps/v1alpha1 submodule, whose deepcopy the package-level target does not cover.

Not in scope

The rest of #3563, deliberately: the nvidia-operator-validator → device-plugin chain that cannot validate an OS-provided driver, CDI spec generation and the nvidia containerd runtime default, install.image derivation from spec.talos.schematicID (already fixed by #3523), and the maxSurge / nodeStartupTimeout defaults (already raised in comments on #3523).

Screenshots

No UI changes.

Downstream repositories

Both are issues rather than PRs, and deliberately so.

terraform-provider-cozystack is hand-written with no codegen from values.schema.json, so both new fields need a schema entry, a model entry and an expand/flatten pair on Kubernetes and KubernetesNodes. I filed an issue instead of a PR because the undefaulted three-state behaviour is exactly where that provider's habit of sending its own defaults would break users silently: a provider that materialises kernel_modules = [] when the user did not set it would disable the automatic NVIDIA modules for every GPU node group managed through Terraform, reintroducing the production failure this PR fixes. That needs a maintainer who knows the provider's conventions for list-of-object attributes, not a guess from me. The issue spells the trap out.

website needs the three-state behaviour written down (a generated parameter table cannot convey it), and separately it needs the Blackwell constraint that is documented nowhere today: on GB202 the schematic must carry siderolabs/nvidia-open-gpu-kernel-modules-production, because with the proprietary nonfree-kmod-nvidia-production extension the module loads, /dev/nvidia0 appears, and nvidia-smi -L then reports No devices found with no error anywhere. That cost us about an hour, and Aleksei Sviridkin (@lexfrei) acknowledged the gap. #561 already asks for GPU passthrough documentation but predates the Talos worker rollover and covers different ground, so #643 is filed as new and cross-references it.

No other repository in the map is touched: this change adds no package, no platform component, no bundle or variant, no release asset, and does not alter ApplicationDefinition semantics or packages/core/platform/values.yaml.

Release note

feat(kubernetes): Worker node groups of tenant Kubernetes clusters accept two new node-group-scoped fields, together making a GPU node group usable on Talos workers. `kernelModules` is emitted as Talos `machine.kernel.modules` (`nodeGroups.<name>.kernelModules` on the `Kubernetes` app, `kernelModules` on `KubernetesNodes`): a Talos system extension installs a kernel module without loading it, so a GPU node group previously came up with no working driver and no way to fix it through values. Left unset, a node group holding at least one `nvidia.com/*` GPU now loads `nvidia`, `nvidia_uvm`, `nvidia_drm` and `nvidia_modeset` automatically; an explicit list replaces that set, and an explicit `[]` opts out. `schematicID` overrides the cluster-wide `talos.schematicID` for one node group, applying to both its boot disk image and its Talos installer image. This fixes a reboot loop in clusters that mix GPU and non-GPU node groups: a schematic is a fixed set of system extensions, `ext-nvidia-persistenced` and `ext-nvidia-cdi-gen` require an NVIDIA card, and Talos refuses to finish booting when an extension service cannot start, so a non-GPU node booting an NVIDIA schematic rebooted roughly every 70 minutes while still reporting `Ready`. Scope the NVIDIA schematic to the GPU node group instead. Node groups that set neither field render exactly what they rendered before, so no existing worker is reconciled by this change; setting `schematicID` on a group does replace its boot image and therefore rolls that group's workers. NOTE: until the fix for the `TalosConfigTemplate` naming (#3523) is released, setting `kernelModules` on a node group that already exists has no effect — the apply is rejected server-side while the HelmRelease still reports `Ready` (#3515). Only node groups created after this release pick it up. `schematicID` is affected by #3515 in one half only: its change to the boot disk image takes effect on an existing node group, because the `KubevirtMachineTemplate` is content-hash named and the MachineDeployment follows it, so the reboot loop is fixed immediately; its change to the installer image in the `TalosConfigTemplate` is not applied until #3523, so an in-place Talos upgrade on such a group would run the previous schematic's installer until then. On Blackwell (GB202) the schematic must carry the open kernel modules extension, not the proprietary one.

Summary by CodeRabbit

  • New Features
    • Configure kernel modules and optional parameters for worker node groups.
    • Automatically configure NVIDIA modules for GPU pools, with explicit opt-out support.
    • Override the Talos schematic ID per node group or pool, with cluster-wide fallback.
  • Validation
    • Reject unsafe schematic IDs, module names, and parameters.
  • Documentation
    • Added configuration guidance covering GPU defaults, rollouts, compatibility, and supported schematic formats.

A Talos system extension installs a kernel module but does not load it;
loading is machine.kernel.modules' job. Neither chart emitted that block
and no values key could add one, so a GPU node group on the Talos workers
introduced in 1.6.0 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. The only workaround was a hand-written
TalosConfigTemplate, which is impractical because its spec is immutable.

Add kernelModules to both charts that render a worker pool:
nodeGroups.<name>.kernelModules in packages/apps/kubernetes, and
kernelModules at the root of packages/apps/kubernetes-nodes, which takes
pool fields flat as it already does for gpus.

The field is three-state, and the states are distinguishable because it
carries no default: absent lets the chart choose (a group holding at
least one nvidia.com/* GPU gets nvidia, nvidia_uvm, nvidia_drm,
nvidia_modeset, in that order, since Talos loads the list in sequence and
the last three depend on the first; any other group gets nothing), a
non-empty list replaces that choice outright, and an explicit [] opts out
even on a GPU group. A default of [] would collapse absent into opted-out
for every group and make the automatic set unreachable.

A group that resolves to no modules renders the machine config it
rendered before, byte for byte, so its content-hash Job name does not
rotate and no worker is reconciled for this change alone.

Also extend the render-parity check to compare the machine config the
two talos-reconcile Jobs apply. It previously stopped at the four pool
objects, on the reasoning that the Job's content-hash name makes a
divergence visible separately; that holds for one chart drifting over
time, not for two charts disagreeing with each other, and the machine
config is the one thing they duplicate outright. It is comparable because
it refers to the release only through shell variables the Job expands at
runtime, so the rendered text is release-name independent.

Refs: cozystack#3563
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds per-pool Talos schematic overrides and kernel-module configuration to Kubernetes APIs and Helm charts. It adds validation, GPU defaults, rendering support, deep-copy methods, chart tests, parity checks, schemas, and documentation.

Changes

Talos worker configuration

Layer / File(s) Summary
API contracts and deep-copy support
api/apps/v1alpha1/kubernetes/types.go, api/apps/v1alpha1/kubernetes/zz_generated.deepcopy.go, api/apps/v1alpha1/kubernetesnodes/types.go, api/apps/v1alpha1/kubernetesnodes/zz_generated.deepcopy.go
Public types now support schematicID overrides and ordered kernel modules with parameters. Generated methods deep-copy module slices and parameters.
Kubernetes-nodes rendering
packages/apps/kubernetes-nodes/templates/*, packages/apps/kubernetes-nodes/values.yaml, packages/apps/kubernetes-nodes/values.schema.json
The chart resolves per-pool schematics, validates values, renders explicit or GPU-derived kernel modules, and propagates both settings to images and reconcile jobs.
Kubernetes chart rendering
packages/apps/kubernetes/templates/*, packages/apps/kubernetes/values.yaml, packages/apps/kubernetes/values.schema.json
The chart applies shared schematic and kernel-module helpers to cluster images and Talos reconcile jobs.
Chart validation
packages/apps/kubernetes-nodes/tests/*, packages/apps/kubernetes/tests/*, hack/talos-reconcile-heredoc_test.bats
Tests cover defaults, overrides, opt-outs, fallback behavior, image rendering, machine configuration, parity, and unsafe input rejection.
Documentation and system schemas
packages/apps/kubernetes/README.md, packages/apps/kubernetes-nodes/README.md, packages/system/*
Documentation and schemas describe configuration fields, validation, GPU behavior, image requirements, and rollout semantics.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

  • cozystack/cozystack issue 3563: The change implements the kernel-module and per-node-group NVIDIA schematic behavior described by the issue.
  • cozystack/website issue 643: The change adds the documented GPU worker schematic and kernel-module configuration.

Possibly related PRs

Suggested labels: kind/api-change

Suggested reviewers: ivanhunters, kvaps, lexfrei

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: per-node-group kernel modules and Talos schematic configuration for Kubernetes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Trivy (0.72.0)

Trivy execution failed: 2026-08-10T11:49:28Z FATAL Fatal error run error: fs scan error: scan error: scan failed: failed analysis: post analysis error: post analysis error: terraformplan-json scan error: fs filter error: fs filter error: walk error range error: stat smartylint.json: no such file or directory: range error: stat smartylint.json: no such file or directory


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A Talos schematic is a fixed set of system extensions baked into one
image, and Talos refuses to finish booting when an extension service in
it cannot start. ext-nvidia-persistenced and ext-nvidia-cdi-gen require
an NVIDIA card, so a node with no GPU that boots an NVIDIA schematic
fails startAllServices and reboots, indefinitely.

talos.schematicID was cluster-wide, so a cluster mixing GPU and non-GPU
node groups had no correct value: the GPU group needs the extensions and
every other group is broken by them. Setting the NVIDIA schematic for the
sake of the GPU group put every other group into a reboot cycle of about
70 minutes per node. Nothing reports it — kubelet starts before the
failing phase, so the node holds Ready and only the pod restart counters,
identical across a node, betray it. Seen in production on 1.6.0.

Add an optional per-node-group schematicID falling back to
talos.schematicID, so the NVIDIA schematic can be scoped to the group
that has the cards. Both consumers resolve it through one helper: the
boot disk image the DataVolume pulls, and the installer image in the
TalosConfigTemplate. They have to agree, or an in-place Talos upgrade
swaps a node's extension set out from under it.

Deliberately not derived from gpus, unlike kernelModules in the previous
commit: a schematic ID is an opaque image-factory digest, so the chart
cannot know which one carries the NVIDIA extensions or synthesise one.

Unset renders byte-identically to before, so the content-hash-named
KubevirtMachineTemplate keeps its name and no worker is rolled. Setting
it does roll that group's workers, which is inherent — changing a node's
boot image means replacing the node.

Refs: cozystack#3563

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
@github-actions github-actions Bot added size/XXL This PR changes 1000+ lines, ignoring generated files and removed size/XL This PR changes 500-999 lines, ignoring generated files labels Aug 6, 2026
@mattia-eleuteri mattia-eleuteri changed the title feat(kubernetes): expose kernel modules for Talos worker node groups feat(kubernetes): per-node-group kernel modules and Talos schematic Aug 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOT LGTM — two things: the new fields reach the shell unvalidated, and kernelModules does not apply to workers that already exist, which the field description does not say.

Business context: a GPU node group is not expressible on Talos workers today. Nothing loads the NVIDIA modules, and one cluster-wide schematic cannot serve a cluster that mixes GPU and non-GPU groups.

On the API shape you asked me to arbitrate in #3563: take option 3, the one you implemented. Option 1 leaves every GPU user to discover four module names and their order, and that missing knowledge is the actual gap, not the typing. Keying the automatic set on the nvidia.com/ prefix instead of on gpus being non-empty is the right call too.

Blockers

B1: kernelModules and schematicID are not validated before they reach the shell

File: packages/apps/kubernetes/templates/talos/talos-reconcile-job.yaml:401, mirrored at packages/apps/kubernetes-nodes/templates/talos-reconcile-job.yaml:303

The machine config is written through cat <<EOF | kubectl apply -f - with an unquoted delimiter, which the script needs so ${RELEASE} and friends expand. That also means the shell expands everything else in the block. kernelModules[].name, kernelModules[].parameters[] and schematicID are free-form CR strings typed as plain string, and they land there verbatim.

Evidence: rendering with kernelModules: [{name: "nvidia$(id)"}] emits - name: nvidia$(id) into command[2], and an unquoted heredoc of that shape substitutes instead of passing through (printf 'cat <<EOF\n- name: x$(echo IN)\nEOF\n' | sh prints - name: xIN). The neighbouring tenant-supplied strings in the same block are guarded before they get there: cluster.yaml runs regexMatch over systemReservedMemory, kubeReservedMemory, systemReservedCpu and kubeReservedCpu. That guard is the file's existing convention for this hazard, and the new fields skip it.

Fix: a render-time fail in both new helpers, with a failedTemplate assert next to the cases already in kernel_modules_test.yaml. ^[a-z0-9_-]+$ for module names and ^[0-9a-f]{64}$ for a schematic ID cover the real syntax. cozyvalues-gen has no pattern vocabulary, so the schema cannot carry this.

B2: kernelModules does not reach workers that already exist

File: packages/apps/kubernetes/values.yaml:94

MachineDeployment.spec.template.spec.bootstrap.configRef.name is the fixed <release>-<group> (cluster.yaml:797), so rewriting the TalosConfigTemplate leaves spec.template untouched and CAPI starts no rollout. A running Machine keeps the config it booted with. I rendered a GPU group with neither field set against main: the KubevirtMachineTemplate hash is identical, only the Job hash moves.

Impact: an operator with the dead GPU node group from #3563 upgrades, gets the automatic NVIDIA set, and still has no GPU until the Machines are replaced. The schematicID description states its rollout semantics; kernelModules says nothing, so the asymmetry reads as "this one applies immediately".

Fix: one sentence in the values.yaml annotation. It propagates to README, schema and CRD.

Non-blocking follow-ups

  1. #3294 is open and adds a second surface for the same value: nodeGroups.<name>.image.builtin.schematicID and image.factory.schematicID, both falling back to the cluster-wide talos.schematicID. It edits the same two files, so whichever lands second conflicts textually, and merging both leaves two ways to set one thing with no precedence. This PR is the better base for it. #3294's own description says its factory.schematicID redirects only the boot-disk import while the installer keeps coming from the cluster-wide value, which is exactly the divergence this PR closes by routing both through one helper.

  2. hack/e2e-talos-image-cache.yaml pre-fetches a single schematic. Nothing to do now, since no e2e case sets a per-group override, but one that does would miss the cache and fall back to the public factory that cache exists to avoid.

What I checked rather than took from the description: reordering the module list in one chart's helper makes render-parity.sh exit 1 on the gpu and schematic cases, and it exits 0 unmutated, so the machine-config comparison is a real gate. With no new fields set, the md0 template and Job hashes match main exactly. The rendered kernel.modules block lands at the right level, and the per-group schematic reaches both the boot image URL and the installer. make generate in both packages produces no drift.

Review B1. The worker machine config is written through
`cat <<EOF | kubectl apply -f -` with an unquoted delimiter, which the
script needs so ${RELEASE} and friends expand and which therefore expands
everything else in the block. kernelModules[].name,
kernelModules[].parameters[] and schematicID are free-form strings from a
tenant-facing CR that land in it verbatim: a module name of `nvidia$(id)`
runs `id` inside the talos-reconcile pod, whose ServiceAccount can write
TalosConfigTemplates and read Talos secrets.

Guard all three at render time with regexMatch and fail, the convention
cluster.yaml already uses for the kubelet reservation strings in the same
block. cozyvalues-gen has no pattern vocabulary, so values.schema.json
cannot carry this.

Module names take ^[a-z0-9_-]+$ and parameters ^[A-Za-z0-9_.,:=+/-]+$,
validated only when the operator supplied the list — the automatic NVIDIA
set is valid by construction. schematicID takes ^[0-9a-f]{64}$, the whole
of an image-factory digest's syntax, and is validated on the EFFECTIVE
value, which also closes the pre-existing path through the cluster-wide
talos.schematicID.

Review B2. Document that kernelModules does not reach workers that
already exist: MachineDeployment.spec.template.spec.bootstrap.configRef
names a fixed TalosConfigTemplate, so rewriting the template leaves
spec.template untouched, CAPI starts no rollout, and a running Machine
keeps the config it booted with. The asymmetry with schematicID, which
rolls the group by changing its boot image, is now stated in both.

Refs: cozystack#3563

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
@mattia-eleuteri

Copy link
Copy Markdown
Collaborator Author

Both blockers fixed in c6444ab. One correction to follow-up 1, with evidence, because it changes who should own the schematic field.

B1 — validated before the shell

Reproduced first: rendering kernelModules: [{name: "nvidia$(id)"}] emitted - name: nvidia$(id) into command[2], and printf 'cat <<EOF\n- name: x$(echo IN)\nEOF\n' | sh prints - name: xIN, so the unquoted delimiter substitutes as you described.

Guarded all three at render time with regexMatch + fail, following the convention cluster.yaml already uses for the kubelet reservation strings in the same block:

  • kernelModules[].name^[a-z0-9_-]+$
  • kernelModules[].parameters[]^[A-Za-z0-9_.,:=+/-]+$, which admits NVreg_EnableGpuFirmware=1 and excludes $, backticks, quotes and whitespace
  • schematicID^[0-9a-f]{64}$

Two choices worth flagging rather than burying:

Module names and parameters are validated only when the operator supplied the list, mirroring the comment on the kubelet guards — the automatic NVIDIA set is valid by construction. schematicID is validated on the effective value instead, so it also closes the pre-existing path through the cluster-wide talos.schematicID, which reached the same heredoc before this PR. That does tighten an existing field: a cluster carrying a non-64-hex talos.schematicID today renders fine and would now fail. Every image-factory digest is 64 lowercase hex and anything else 404s at the factory, so I judged a loud render failure better than a VM stuck importing a URL that cannot resolve — but say the word and I will scope the guard to the per-group override only.

failedTemplate cases sit next to the existing ones in kernel_modules_test.yaml and the schematic suites, four in total. The schematic ones carry a template: scope with a comment saying why: the guard is in a shared helper reached from both templates in the suite, and Helm aborts on the first to hit it, so the assert names the template that reports the failure rather than the only one able to.

B2 — documented

You are right that this is not just #3515. configRef.name is the fixed <release>-<group> at cluster.yaml:797, so the MachineDeployment's spec.template never changes and CAPI starts no rollout; a running Machine keeps the config it booted with, independently of whether the apply succeeded. Added to the values.yaml annotation in both charts, so it propagates to README, schema and CRD, and stated the asymmetry explicitly — schematicID rolls the group by changing its boot image, kernelModules does not roll anything by itself.

Follow-up 1 — #3294 already does this, more completely

Checking it, I do not think the schematic field should stay in my PR at all, and the reason cuts against what you wrote. The NOTE you are quoting is about imageFactoryURL, not the schematic: it says redirecting a group's factory URL does not redirect the installer, because talos.installerRepository has no per-group override. On the schematic #3294 already routes both consumers per group. Its reconcile job renders image: {{ ... }}/{{ .talosSchematicID }}:{{ .talosVersion }}, with $grpSchematicID resolved from image.builtin.schematicID or image.factory.schematicID falling back to the cluster-wide value, and it ships tests pinning exactly that (install.image pins the group's per-group schematicID/version, and a fallback case). So it is not the divergence my helper closes — it closes it too, and adds per-group version and the golden-clone path on top.

That makes my flat nodeGroups.<name>.schematicID a strict subset of nodeGroups.<name>.image.factory.schematicID, which is exactly the two-ways-to-set-one-thing you want to avoid, and #3294 is the richer surface. Concretely, the two also collide in a way that shows the duplication is not benign: #3294's test uses schematicID: deadbeef, which my new ^[0-9a-f]{64}$ guard rejects.

My proposal, yours to overrule:

If instead you want the flat field kept as the canonical one and #3294's union to defer to it, that works mechanically, but I would argue against: #3294 needs per-group version and builtin anyway, so the union has to exist regardless, and a flat field beside it is the redundancy rather than a simplification.

Follow-up 2 — e2e image cache

Noted, nothing changed. No e2e case sets a per-group override, so nothing misses the cache today. Worth saying that #3294 removes hack/e2e-talos-image-cache.yaml outright and has the suites clone a golden instead, so if the schematic field lands there this concern resolves itself rather than needing a guard here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOT LGTM — the schematic guard rejects a configuration that renders on main today.

Both previous blockers are closed. I re-ran the payload against this head: nvidia$(id) now fails the render in both charts, and validating the effective value closes the cluster-wide path as well, which is the better of the two options you offered. Weakening ^[a-z0-9_-]+$ to ^.*$ turns kernel_modules_test.yaml red, so the new asserts are load-bearing rather than decorative. The B2 wording is accurate and landed in both charts.

You are right about #3294 and I was wrong. The NOTE I quoted sits on imageFactoryURL, not on the schematic, and #3294 does route the per-group schematic into install.image through .talosSchematicID, with a test pinning it. My follow-up 1 argued from a misreading of that field. Disregard the reasoning; the ownership question it raised is answered below on different grounds.

Blockers

B3: the 64-hex rule rejects air-gapped and mirrored configurations that work today

File: packages/apps/kubernetes/templates/_helpers.tpl:283, mirrored in packages/apps/kubernetes-nodes/templates/_helpers.tpl

^[0-9a-f]{64}$ on the effective value encodes the public factory's naming as the only legal one. The chart supports more: talos.imageFactoryURL documents "a self-hosted Image Factory, a caching mirror, or an internal HTTP file server", and talos.installerRepository documents mirrored registries. On a file server or a mirrored registry the operator picks the path, and a readable name is the obvious choice.

Evidence: values carrying imageFactoryURL: http://images.internal.example.com/talos, installerRepository: registry.internal.example.com/talos/installer and schematicID: talos-gpu-nvidia-open render on main into .../image/talos-gpu-nvidia-open/v1.13.6/openstack-amd64.raw.xz and .../installer/talos-gpu-nvidia-open:v1.13.6, and fail the render on this head. Same values, same command, only the checkout differs. #3294's schematicID: deadbeef test hits the same wall, which makes it two independent cases rather than one contrived one.

Impact: a cluster running against a mirror stops reconciling after the upgrade, on a field its operator never touched.

Fix: make the guard the injection guard the comment says it is, instead of a format guard. A class such as ^[A-Za-z0-9._:-]+$ on the effective value rejects $, backticks, quotes and whitespace, keeps the cluster-wide path closed, and leaves mirror naming alone. A wrong ID still 404s at the factory, exactly as it did before this PR, and that failure is visible in the DataVolume rather than hidden.

On who owns the schematic field

Keep it here for now, and I will take the redundancy. #3294 is the richer surface and should own this in the end, but it is not close to landing: it brings a new package, a golden catalog and an e2e rework, while the reboot loop is live. When #3294 rebases, have image.factory.schematicID fall back to nodeGroups.<name>.schematicID rather than replace it, so nothing that shipped breaks, the union stays canonical, and the flat field can be deprecated on its own schedule. One documented redundancy for a release is cheaper than leaving a production reboot loop waiting on a large PR.

Follow-up 2 is answered and nothing is owed there: #3294 deletes hack/e2e-talos-image-cache.yaml outright, so the concern resolves itself if the schematic ends up there.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOT LGTM — the injection guard is bypassable. This lands on the same head as my previous review, which I posted before finishing this pass.

B4: any extra key on a kernelModules item walks past the guard

File: packages/apps/kubernetes/templates/_helpers.tpl:194, and the same copy in packages/apps/kubernetes-nodes/templates/_helpers.tpl

The validator walks .name and .parameters, but the emit is toYaml over the raw user dict, so every other key on the item is copied into the heredoc verbatim.

Evidence: a group carrying

kernelModules:
  - name: dummy
    evil: "$(touch /tmp/pwned)"

renders - evil: $(touch /tmp/pwned) inside cat <<EOF | kubectl apply -f -, in both charts. Nothing upstream catches it either: the items object in values.schema.json has no additionalProperties: false, and the aggregated apiserver does not enforce that schema on write. specSchema in pkg/registry/apps/application/ is wired into rest_defaulting.go only, with no pruning and no validation on the Create or Update path. So the render is the last gate, exactly as your comment in the helper says.

Fix: build the emitted list from validated fields, dict "name" $name "parameters" $params, instead of passing user input through toYaml. Then anything unvalidated cannot reach the output by construction rather than by enumeration. A third case in kernel_modules_test.yaml with an extra key carrying $(...) fails today and pins it.

B5: review provenance in two committed test files

packages/apps/kubernetes/tests/kernel_modules_test.yaml:260 and packages/apps/kubernetes-nodes/tests/kernel_modules_test.yaml:156 both open with # B1 from review:. Comments here are held to the same standard as commit messages: self-contained, no review-iteration references, because the next reader has no access to this thread. The rest of the sentence is good, just drop the first four words.

Smaller

schematic_per_nodegroup_test.yaml has the $(id) rejection case but not the plain not-64-hex one that schematic_per_pool_test.yaml:117 carries. And since the guard now constrains the effective value, talos.schematicID's own description should say so instead of reading as a free-form override. Both of these are moot if B3 changes the guard's shape.

One sentence is also worth adding to the kernelModules description: the automatic NVIDIA set keys on the nvidia.com/ prefix alone, with no relation to whether the effective schematic actually carries those modules. In Talos v1.13 a module that cannot load leaves the kernel-module controller restart-backing-off rather than bricking the node, so it is a soft failure, but it is the same silent-mismatch class the schematic half of this PR exists to remove.

Not yours to fix here: talos.installerRepository and talos.version reach the same heredoc unvalidated and are unchanged from the merge base. I am tracking those separately.

…uard

Review B3. `^[0-9a-f]{64}$` on the effective schematic encoded the public
factory's naming as the only legal one, but the chart supports more:
talos.imageFactoryURL documents a self-hosted factory, a caching mirror or
an internal HTTP file server, and talos.installerRepository documents
mirrored registries. On those the operator picks the path, and a readable
name is the obvious choice. Verified against the merge base: values with
schematicID `talos-gpu-nvidia-open` and an internal factory URL render on
main and failed on this branch, so the guard stopped a cluster
reconciling on a field its operator never touched.

Replace it with `^[A-Za-z0-9._:-]+$`, which is the injection guard the
comment claimed to be: it rejects $, backticks, quotes and whitespace,
keeps the cluster-wide path closed, and leaves mirror naming alone. A
wrong ID still 404s at the factory, visibly, on the DataVolume.

Review B4. The validator walked .name and .parameters but the emit was
toYaml over the raw user dict, so any other key was copied into the
heredoc verbatim: `- {name: dummy, evil: "$(touch /tmp/pwned)"}` reached
it in both charts. Nothing upstream prunes it either — `items` in
values.schema.json has no additionalProperties: false, and the aggregated
apiserver wires that schema into defaulting only, with no validation on
Create or Update. Rebuild the emitted list from validated fields instead,
so unvalidated input cannot reach the output by construction rather than
by enumeration. Rendering for valid input is byte-identical.

Review B5. Drop the review-iteration reference from the test comments;
the next reader has no access to that thread.

Also: pin mirror-style naming as accepted in both charts so B3 cannot
regress, replace the now-legitimate not-64-hex case with one the class
actually rejects, add the extra-key case in both charts, and document
that the automatic NVIDIA set keys on the resource prefix alone with no
cross-check against the effective schematic.

Refs: cozystack#3563

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
@mattia-eleuteri

Copy link
Copy Markdown
Collaborator Author

B3, B4 and B5 fixed in 88ae4de, plus the smaller items. Both blockers reproduced against the merge base first.

B3 — the guard was a format rule, not an injection guard

Confirmed the regression before changing anything: values carrying imageFactoryURL: http://images.internal.example.com/talos, installerRepository: registry.internal.example.com/talos/installer and schematicID: talos-gpu-nvidia-open render on main into .../image/talos-gpu-nvidia-open/v1.13.6/openstack-amd64.raw.xz and fail on the previous head. Same values, same command, only the checkout differs — so it broke a cluster on a field nobody touched, which is not a trade I should have made. I flagged the tightening and still misjudged it: I reasoned from the public factory's naming and treated the documented mirror and file-server paths as though they did not exist.

Now ^[A-Za-z0-9._:-]+$ on the effective value, as you proposed. It rejects $, backticks, quotes and whitespace, keeps the cluster-wide path closed, and leaves mirror naming alone. A wrong ID 404s at the factory exactly as before, on the DataVolume where it is visible.

Both charts now pin the mirror case as accepted — readable name, internal factory URL, mirrored installer repository, asserting both rendered image references — so this cannot regress into a format rule again. The not-64-hex case in schematic_per_pool_test.yaml was itself testing the wrong rule and is replaced by sch`whoami`, which the class does reject, mirrored into the parent chart's suite so the two are symmetric.

B4 — closed by construction

Reproduced: - {name: dummy, evil: "$(touch /tmp/pwned)"} rendered - evil: $(touch /tmp/pwned) inside the heredoc in both charts. I also checked the two upstream claims rather than take them: items in both values.schema.json files has no additionalProperties: false, and specSchema in pkg/registry/apps/application/ appears only in rest_defaulting.go and the constructor in rest.go, with no pruning or validation on the write path. So the render is the last gate, and enumerating fields was the wrong shape for the fix.

The emitted list is now rebuilt as dict "name" $name plus parameters when non-empty, from the validated values, so an unknown key cannot reach the output at all. Rendering for valid input is byte-identical — the explicit br_netfilter + parameters case and the render-parity comparison both still pass unchanged. A case in kernel_modules_test.yaml in each chart pins it: the extra key is absent and - name: dummy is still emitted.

B5 — dropped

Both files now open with "These values reach…". Point taken on the standard: a test comment referencing a review round is dead weight to whoever reads it next.

Smaller items

  • talos.schematicID's own description now says the effective per-group value is constrained to ^[A-Za-z0-9._:-]+$ and why, and that both a 64-hex digest and an operator-chosen mirror path satisfy it.
  • kernelModules now says the automatic set keys on the nvidia.com/ prefix alone with nothing cross-checking that the effective schematic carries those modules, and that on Talos v1.13 such a module leaves its controller retrying rather than failing the boot, so the symptom is a missing driver rather than a dead node. I took the v1.13 behaviour from your description and scoped the sentence to that version rather than stating it unqualified, since I have not verified it myself.
  • talos.installerRepository and talos.version left alone, as you said.

Ownership

Taking your call: the field stays here, and when #3294 rebases image.factory.schematicID falls back to nodeGroups.<name>.schematicID rather than replacing it, so the union stays canonical and nothing that shipped breaks. I have noted that on #3294 so it is not discovered at rebase time. Once it lands I will open the deprecation of the flat field rather than leave the redundancy undated.

State on this head: 207 tests in the parent chart, 23 in kubernetes-nodes, render-parity green including the machine-config comparison and the new schematic case. Rendering for a node group setting neither field is still byte-identical to main, KubevirtMachineTemplate and Job content hashes included, so nothing rolls.

@mattia-eleuteri

Copy link
Copy Markdown
Collaborator Author

Unit & controller tests is red on this head and it is not this PR. Filing the detail so nobody re-derives it.

The failing test is the unconverted bats files hold no more EXIT traps than they already did in hack/cozyreport.bats. hack/multus-install-cni-plugins.bats arrived on main in 7739d1e carrying 12 EXIT traps, one day before that ratchet was written, so the frozen list never learned about it. The guard fails on main itself, and this PR's CI builds a merge with main.

Reproduced against a pristine checkout of main at 879d0f6, with none of this branch: the guard's found set differs from frozen by exactly multus-install-cni-plugins.bats=12. Fix is a one-line count update, which is what the guard's own comment prescribes for a file that predates it — #3584. This PR goes green on its own once that lands; nothing to rebase here.

In the same run, everything else that matters passed: 70 helm suites with none failing, including all four suites this PR adds and GOLDEN PARITY on the render-parity check, plus pre-commit, Verify generated code is up to date, CodeQL and the API owner gate. Exactly one ❌ Test failed in the whole job, and it is the ratchet.

Separately, while probing the same class as B4 I checked the two remaining shapes an unenforced schema would let through, and both are already closed: Helm validates values against values.schema.json at render time, so a bare string in place of a module item (kernelModules: ["nvidia"]) and a scalar where parameters should be a list are both rejected before any template runs. What the schema does not reject is extra keys, because it carries no additionalProperties: false — which is exactly B4, and is now closed by construction rather than by enumeration. So the field is guarded on three independent axes: types by the schema, characters by the regex, keys by rebuilding the emitted list.

…odules

The chart already sets NVreg_NvLinkDisable=1 for every tenant GPU cluster:
the gpu-operator addon default turns on kernelModuleConfig, whose ConfigMap
content is exactly that one line, and the operator's driver container
appends it to the nvidia modprobe options. Cozystack passes individual GPUs
into worker VMs without the NVSwitches, so without it the driver waits
forever for an NVLink fabric that cannot come up: Fabric State stays "In
Progress" and every CUDA call fails with "system not yet initialized".

On Talos the driver comes from a system extension and that container is
disabled, so nothing applies the parameter and machine.kernel.modules is
the only place left to carry it. A GPU node group moved to Talos therefore
loses it silently — the same class of quiet failure the rest of this branch
removes.

Add it to the automatic set so the Talos path reproduces what the chart
already chose, rather than making every operator rediscover it. A no-op on
a PCIe card with no NVLink. An explicit kernelModules list still replaces
the set outright, so an operator who does have a fabric can drop it.

Refs: cozystack#3563

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
@mattia-eleuteri

Copy link
Copy Markdown
Collaborator Author

One more commit, 665c3f2, which changes something you already verified — flagging it rather than letting it slip past.

The automatic NVIDIA set now carries NVreg_NvLinkDisable=1 on the nvidia module. Reason: the chart already applies that parameter to every tenant GPU cluster today. cozystack.defaultGpuOperatorValues turns on kernelModuleConfig, whose ConfigMap content in packages/system/gpu-operator/values.yaml is exactly that one line, and the operator's driver container appends it to the nvidia modprobe options. Cozystack passes individual GPUs into worker VMs without the NVSwitches, so without it the driver waits forever for a fabric that cannot come up — Fabric State stays In Progress and every CUDA call fails with system not yet initialized.

On Talos that driver container is disabled (the driver comes from the system extension), so nothing applies the parameter and machine.kernel.modules is the only place left to carry it. A GPU node group moved to Talos loses it silently, which is the same class of quiet failure this PR exists to remove. So the automatic set reproduces what the chart already chose rather than making every operator rediscover it. It is a no-op on a PCIe card with no NVLink, and an explicit kernelModules list still replaces the set outright, so an operator who genuinely has a fabric can drop it.

Consequences in the diff: the pinned auto-set patterns in both charts now include the parameter, the GPU group's content-hash fixture in talos_templates_test.yaml moves again (the non-GPU one still does not), and the field descriptions name the parameter and say why Talos cannot inherit it. Rendering for a node group setting neither field is still byte-identical to main.

This is the first half of the gap 3 + gap 4 work from #3563. The second half — a gpu-operator addon default for the OS-provided-driver shape, which is what makes driver.enabled: false correct in the first place — is coming as its own PR against main, since it touches a different file and should not enlarge this one further.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOT LGTM — the schematic class I suggested last round is one character too narrow, and it rejects a value that renders on main. My recommendation, my miss; details below.

Everything else from the previous rounds is closed and I re-ran the checks rather than reading the diff. The allowlist closes B4 by construction: the extra-key payload now renders - name: dummy and nothing else, a nested extra.nested.deep: "$(...)" is dropped the same way, and swapping the rebuild back to deepCopy . turns kernel_modules_test.yaml red, so the new case is load-bearing. Both injection paths stay closed, including the effective-value one through the cluster-wide field. B5 is gone from both files. The schematic suites gained a positive case pinning a readable mirror name, so that boundary is now tested rather than incidental.

On 665c3f21: for a GPU node group setting neither field the KubevirtMachineTemplate hash is unchanged against main, so the parameter does not roll an existing GPU group by itself, and the non-GPU group stays byte-identical on both objects. Suites are green in both charts (207 and 23), GOLDEN PARITY passes, make generate leaves no drift.

Blocker

B6: the schematic class has no /, and a mirror path needs one

File: packages/apps/kubernetes/templates/_helpers.tpl:320, mirrored in packages/apps/kubernetes-nodes/templates/_helpers.tpl

Evidence: same values file at both revisions, imageFactoryURL: http://images.internal.example.com/talos, installerRepository: registry.internal.example.com/talos/installer, schematicID: gpu/nvidia-open. Main renders http://images.internal.example.com/talos/image/gpu/nvidia-open/v1.13.6/openstack-amd64.raw.xz and image: registry.internal.example.com/talos/installer/gpu/nvidia-open:v1.13.6, both well-formed, since an OCI repository path takes multiple segments. This head fails the render. That is the same regression class as B3, one character narrower, and the error text plus all four field descriptions call the value "the path an operator chose", which is exactly what a class without / forbids.

Fix, and the shape of it matters more than the character: / is inert in an unquoted heredoc, as are ;, |, & and parentheses. Only $, a backtick and a backslash are special there. So the guard is better written as a rejection of those three plus quotes and whitespace, rather than as an allowlist of permitted characters. An allowlist is right for kernelModules items because the set of legal keys is closed and the chart owns it, which is why B4's fix is correct. Here the set of legal values is open, it belongs to whatever registry or file server the operator runs, and every round of enumerating it has cost a legitimate configuration. Extend the existing positive case with a path-shaped value so the boundary is pinned.

Recommended

The parameters class bars ;, and NVIDIA's own multi-value form needs it: NVreg_RegistryDwords="PowerMizerEnable=0x1;PerfLevelSrc=0x2222" is the documented shape, semicolon-separated key=value pairs read by the module at load time. Same reasoning as above, and worth widening while that class is being touched.

Two wordings over-claim slightly. "Byte-for-byte the same helper as the parent kubernetes chart's" is not literally true: the fail messages differ (nodeGroup %s: against pool %s:) and so do the comments. What is identical is the emitted output, which render-parity.sh proves. And "that container is disabled there" describes a configuration the chart does not produce: templates/helmreleases/gpu-operator.yaml sets only kernelModuleConfig.create and driver.kernelModuleConfig.name, so on Talos an operator still has to disable the driver container through addons.gpuOperator.valuesOverride. The conclusion holds, only the passive voice claims the chart already did it. That sentence lives in six generated copies, so it is one edit plus make generate.

Outside this PR

talos.installerRepository and talos.version reach the same unquoted heredoc unvalidated, unchanged at the merge base, and the support-matrix guard does not catch the version one because regexFind extracts the prefix and the membership check that follows tests the Kubernetes version rather than the Talos one. Not yours to fix here, and I am filing them separately, but the new field descriptions now assert the sink is constrained, which reads as broader coverage than exists.

The red Unit & controller tests is not this branch. I reproduced it on a pristine checkout of main at 879d0f6: the EXIT-trap ratchet in hack/cozyreport.bats differs from its frozen set by exactly multus-install-cni-plugins.bats=12, so every PR inherits it through the merge commit CI builds.

One non-blocking note for the terraform provider follow-up rather than for this chart: kernelModules []KernelModule with omitempty in the generated types cannot represent the [] opt-out, because an empty slice serialises away and becomes indistinguishable from unset. The in-cluster path is unaffected, since the CR spec travels as raw JSON and never passes through those structs, so applying YAML behaves as documented. A typed Go client building the object would silently lose the opt-out.

@mattia-eleuteri

Copy link
Copy Markdown
Collaborator Author

The NVreg_NvLinkDisable=1 addition from 665c3f2 is now verified on hardware, not just argued from the ConfigMap.

On a fresh single-node tenant cluster with an L40S and a machine.kernel.modules block byte-identical to what this branch renders for a nvidia.com/* node group:

$ cat /proc/modules | grep ^nvidia
nvidia_uvm 2068480 0 - Live
nvidia_drm 159744 0 - Live
nvidia_modeset 2150400 1 nvidia_drm, Live
nvidia 15994880 7 nvidia_uvm,nvidia_modeset, Live

$ grep -i nvlinkdisable /proc/driver/nvidia/params
NvLinkDisable: 1

So the parameter does reach the driver through the machine config, which is the part I could only infer before. The four modules load in the declared order, nvidia-cuda-validator reports Succeeded, the node advertises nvidia.com/gpu: 1, and a tenant pod gets GPU 0: NVIDIA L40S (UUID: …) from nvidia-smi -L. The node has held one bootID for 93 minutes, so nothing about the block destabilises the boot.

Two honest limits. The run cannot show the parameter is needed: an L40S is PCIe with no NVLink, so this establishes that it is harmless and that CUDA initialises with it — its necessity rests on the SXM passthrough case in production. And the modules were added by hand, since this branch is not released; what was verified is the rendered content, not the chart applying it.

Useful diagnostic side-note, since /proc is where this is visible: /proc/modules and /proc/driver/nvidia/params are not namespaced, so a plain unprivileged pod can read both. /sys/module/nvidia/parameters/* cannot be read without root, which is the wrong place to look.

Also for the record: that cluster has a single GPU node group, and it does not show the ~70 minute reboot loop — consistent with the loop being confined to node groups whose nodes do not match the cluster-wide schematic, which is what the schematicID half of this PR exists to fix.

… class

Review B6. `^[A-Za-z0-9._:-]+$` has no `/`, and a mirror path needs one:
with an internal factory URL and a mirrored installer repository, a
schematicID of `gpu/nvidia-open` renders
`http://images.internal.example.com/talos/image/gpu/nvidia-open/...` and
`installer/gpu/nvidia-open:v1.13.6` on main, both well-formed, because an
OCI repository path takes several segments and so does a URL path. This
branch rejected it. Same regression class as B3, one character narrower.

The shape was the mistake, not the character. Inside an unquoted heredoc
only $, a backtick and a backslash are special — verified: `/ ; | & ( )`
and spaces pass through as text. So both guards now deny those three plus
quotes, whitespace and control characters, rather than permitting an
enumerated set. An allowlist stays correct for kernelModules item keys,
where the set is closed and the chart owns it, which is why B4's fix is
built that way; the schematic's set is open and belongs to whatever
registry or file server the operator runs, and enumerating it cost a
legitimate configuration twice.

Control characters are excluded because `\s` covers only tab, newline,
form feed, carriage return and space. A bare one otherwise slips through
and surfaces later as `yaml: control characters are not allowed`,
pointing at the template rather than at the value.

Also per the review, the `parameters` class barred `;`, which NVIDIA's
documented multi-value form needs:
NVreg_RegistryDwords=PowerMizerEnable=0x1;PerfLevelSrc=0x2222. It takes
the same denylist for the same reason.

Two wordings corrected. The nodes chart claimed its helpers were
"byte-for-byte the same" as the parent's; the fail messages and comments
differ, and what is identical is the emitted output, which
render-parity.sh proves. And the NvLinkDisable rationale said the driver
container "is disabled" on Talos, which the chart does not do — an
operator turns it off through addons.gpuOperator.valuesOverride today.
The schematic descriptions also no longer read as though every value
reaching that heredoc were guarded; only this field is.

New cases pin the boundary in both charts: a multi-segment mirror path is
accepted, and a semicolon-bearing module parameter renders.

Refs: cozystack#3563

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
@mattia-eleuteri

Copy link
Copy Markdown
Collaborator Author

B6 fixed in c1ea547, along with both recommendations and both wording corrections. Verified your evidence before changing anything, and the mistake was the shape, not the character — twice now, which is the actual lesson.

B6 — denylist, not a class

Reproduced: with imageFactoryURL: http://images.internal.example.com/talos, installerRepository: registry.internal.example.com/talos/installer and schematicID: gpu/nvidia-open, main renders .../talos/image/gpu/nvidia-open/v1.13.6/openstack-amd64.raw.xz and installer/gpu/nvidia-open:v1.13.6, and the previous head failed the render.

Checked the premise rather than taking it: printf 'cat <<EOF\nA: a/b;c|d&e(f) g\nB: $(echo SUBST)\nC: echo BACKTICK\nEOF\n' | sh prints the first line verbatim and expands only the last two. So / ; | & ( ) and spaces are inert and $, backtick, backslash are not. Both guards now deny those three plus quotes and whitespace, instead of permitting an enumerated set. The allowlist stays where you said it belongs — kernelModules item keys, a closed set the chart owns.

One addition beyond your fix, from testing it: \s covers only tab, newline, form feed, carriage return and space, so a bare control character passed the class and surfaced later as yaml: control characters are not allowed, pointing at the template rather than at the value. [:cntrl:] is in the class now. Not a shell concern — it corrupts the YAML the heredoc carries, which is the other thing these guards are for.

Worth recording how I nearly reported the wrong result here: my first pass tested the backslash through --set-string and then through a values file, and both times the shell and YAML ate the escape before Helm saw it, so it read as ACCEPTED. It is rejected; a five-line template evaluating regexMatch against a literal sch\back returns false. The harness was wrong, not the guard.

Recommended, both taken

parameters takes the same denylist, so NVreg_RegistryDwords=PowerMizerEnable=0x1;PerfLevelSrc=0x2222 renders — pinned by a new case.

Both over-claims corrected. The nodes chart no longer says "byte-for-byte the same helper": the fail messages differ (pool against nodeGroup) and so do the comments, and what is identical is the emitted output, which render-parity.sh proves. And the NvLinkDisable rationale no longer says the driver container "is disabled" on Talos, since the chart does not do that — an operator turns it off through addons.gpuOperator.valuesOverride today, which is exactly what #3585 changes. That sentence lives in the generated copies, so it went through make generate.

On your point about the descriptions reading as broader coverage than exists: fixed too. They now say the guard covers this field only, not every value reaching that heredoc.

Boundaries now pinned rather than incidental

Both charts accept a multi-segment mirror path and a semicolon-bearing parameter, and still reject $(id), a backtick, a backslash, quotes and whitespace on the effective schematic. Suites green in both charts (208 and 25), GOLDEN PARITY passes, make generate idempotent, and a node group setting neither field still renders byte-identically to main including both content hashes.

The Go omitempty note

Good catch, and it is the sharper half of the terraform follow-up rather than a chart issue. Recorded on cozystack/terraform-provider-cozystack#20: []KernelModule with omitempty cannot represent the [] opt-out, so a typed Go client building the object drops it silently, while the YAML path is unaffected because the CR spec travels as raw JSON and never passes through those structs. That makes the provider's flatten side the place where the three states have to be preserved deliberately.

Also confirming your ratchet finding independently reproduced mine — #3584 is the one-line fix for it.

Aleksei Sviridkin (lexfrei) added a commit that referenced this pull request Aug 7, 2026
….bats (#3584)

<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does

`hack/multus-install-cni-plugins.bats` arrived on main in 7739d1e
carrying 12 EXIT traps, one day before the EXIT-trap ratchet in
`hack/cozyreport.bats` was written, so the frozen list never learned
about it. The guard therefore fails on main itself, and on every PR
whose CI builds a merge with main — `make bats-unit-tests` exits 1 with:

```
FAIL: the set of unconverted EXIT-trap files changed.
  frozen: ... nightly-mirror_test.bats=5 ...
  found:  ... multus-install-cni-plugins.bats=12 nightly-mirror_test.bats=5 ...
```

This records the count, which is what the guard's own comment prescribes
for exactly this case: *"a file that did not exist when this guard was
written arrives carrying its own \[traps\] ... Counts are updated rather
than the files converted: all of them are owned by other branches, and a
conflict there costs more than an uncovered trap."* Converting the file
is not in scope here and belongs to whoever owns the multus branch.

Verified by reproducing the guard's logic against a pristine checkout of
`main` at 879d0f6: the `found` set differs from `frozen` by exactly
this one entry before the change, and matches after it. Found while
investigating a red `Unit & controller tests` on #3571, which carries
none of this.

### Screenshots

No UI changes.

### Screenshots

<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->

### Downstream repositories

<!--
Cozystack is upstream for repositories that are NOT kept in sync with it
automatically, and nothing in CI compares the two sides, so a change
here can break them silently.

Which change forces what, and which file to touch, is the trigger map in
docs/agents/contributing.md under "Downstream Repositories". Walk it
before you tick anything.

IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM
TO FILL IN:

1. Walk the trigger map against the actual diff of this PR, file by
file. Do not decide from the PR title or from your memory of what you
changed.
2. For every repository you tick, open the follow-up PR there and link
it on the same line. A ticked box with no link claims work that does not
exist, which is worse than an empty box. Search that repository for an
open PR or issue covering it first, and link that instead of filing a
duplicate.
3. When the follow-up is out of scope, or needs a decision that is not
yours to make, open an issue in that repository instead, link that, and
say so in the PR body. Do not push a speculative PR into another
repository just to fill a line here.
4. Do not tick "No downstream repository is affected" to make the
checklist go away. If you are unsure, leave every box empty and say so
in the PR body, so a human decides.
-->

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[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:

### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same `type(scope):` prefix as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
NONE
```


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

## Summary by CodeRabbit

* **Tests**
* Updated the expected EXIT-trap inventory to include
`multus-install-cni-plugins.bats`.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

B6 is closed in both charts, and I checked it by rendering rather than by reading the diff. With imageFactoryURL: http://images.internal.example.com/talos and installerRepository: registry.internal.example.com/talos/installer, gpu/nvidia-open renders http://images.internal.example.com/talos/image/gpu/nvidia-open/v1.13.6/openstack-amd64.raw.xz and registry.internal.example.com/talos/installer/gpu/nvidia-open:v1.13.6 (the same output main produces), and so do talos-gpu-nvidia-open, deadbeef and a 64-hex digest, in packages/apps/kubernetes and packages/apps/kubernetes-nodes alike. The rejection side holds: $(...), a backtick, a backslash, both quote characters, a space, a real tab, a real newline and a bare control character all fail the render, each naming the group or the pool. ; | & ( ) > ~ * pass, which is the point of the reshape.

The denylist is also complete for the sink it guards, not merely wider than the last one. Inside an unquoted here-document the shell performs parameter expansion, command substitution and arithmetic expansion, and \ keeps its meaning only before $, a backtick, \ or a newline, so those three are the whole set, and everything else in the class is there for YAML integrity rather than for the shell. Validating the effective value keeps the cluster-wide path closed; talos.schematicID: nvidia$(id) with no per-group override still fails.

Both guards are pinned in both directions and in both charts. Reverting the schematic class to ^[A-Za-z0-9._:-]+$ (last round's suggestion, the one missing /) turns schematic_per_nodegroup_test.yaml and schematic_per_pool_test.yaml red; widening both guards to ^.*$ turns all four suites red. As they stand the suites are green (208 and 25), GOLDEN PARITY passes, and make generate in both packages leaves no drift.

The parity script's new machine-config comparison is a real gate, which matters more than usual here because it is the only thing holding the two copies of the helper together. Adding one rendered line to the nodes chart's kernel: block fails it on three cases, and cutting the block out entirely fails the same three. Worth knowing for whoever tests it next: a {{- /* ... */}} comment renders nothing and so passes, which is the check being right rather than asleep. I also diffed the two helper bodies by hand. They differ only in nodeGroup against pool in the fail messages.

NVreg_NvLinkDisable=1 lands where it should and nowhere else. It appears only when the group resolves to the automatic set; an explicit list is emitted verbatim with nothing added, [] still emits no kernel block on a GPU group, and a non-NVIDIA vendor still gets nothing. Against the merge base with neither new field set, the only difference in the entire render is the GPU group's talos-reconcile Job: new content hash, new module block. Every KubevirtMachineTemplate name is unchanged and the non-GPU group is byte-identical, so no worker is rolled. The hardware run answers the half I cannot reach by rendering, which is whether the parameter actually arrives at the driver.

Both wording corrections landed, in all twelve files carrying the description, and the schematic descriptions now say the guard covers that field alone.

On the semicolon: you took the bare form and I had quoted the modprobe.conf spelling at you, which is the wrong shape for a YAML list element. NVreg_RegistryDwords=PowerMizerEnable=0x1;PerfLevelSrc=0x2222 renders, the field description and the error text both prescribe it, and the two agree. Nothing owed.

Recommended

The semicolon regression is pinned in one chart out of two. kernel_modules_test.yaml in kubernetes-nodes gained accepts a semicolon-separated module parameter; the parent chart has no equivalent. I narrowed only the parent's parameters class back to bar ; (the exact defect that case exists to prevent), and the parent suite stayed 208/208, the nodes suite stayed 25/25, and GOLDEN PARITY passed. The parity script does not cover it either, since its kernelmodules case carries nf_conntrack_helper=0. It is the same asymmetry you closed on the schematic side when the parent gained the readable-name case, and the same fifteen lines in the other file.

Two claims in the PR body have drifted from the branch. "New kernel_modules_test.yaml in both packages, 6 cases each" is 9 in the parent and 10 in the nodes chart now. And "reordering the module list in one chart's helper only makes the new MachineConfig(talos-reconcile) assertion fail, for the GPU case only" is off too: reordering the nodes chart's automatic set fails on gpu and on schematic, because the schematic case also carries an nvidia.com/* GPU and inherits the same automatic list. The body becomes the merge commit, so both are worth a pass before this leaves draft.

CI

None of the red is this branch. All twenty-seven Build packages/* jobs die at exporting to image with failed to push iad.ocir.io/..., on packages this PR does not touch (mariadb, clickhouse and metallb among them), so it is a registry-side failure and a re-run question. Unit & controller tests is still the EXIT-trap ratchet in hack/cozyreport.bats that every PR inherits through the merge with main. The run dates from the day the head commit was pushed, and nothing since asks for a code change here.

Resolves against 3a1292d (escape tenant values in the worker reconcile
heredoc) and 58910c0 (registry mirror passthrough), both of which edit
the machine-config block this branch also touches.

The guard and the escaping compose rather than compete, so both are kept
at every site:

  - The installer image line takes main's escape chain on all three
    coordinates and this branch's `kubernetes.schematicID` include, so
    the per-node-group schematic still resolves and the value is still
    escaped on the way into the heredoc.
  - `nodegroup.yaml` keeps both new group keys, `logSerialConsole` and
    `schematicID`.
  - `render-parity.sh` keeps main's guest-console-log case alongside the
    kernel-module, opt-out and schematic cases, and keeps this branch's
    machine-config comparison, which supersedes the "four pool objects"
    scope note it replaced.

Two cases in hack/talos-reconcile-heredoc_test.bats fed a hostile
`talos.schematicID` and asserted the heredoc kept it literal. That value
is now refused at render time by this branch's guard, which the INVARIANT
above the data block admits as the alternative to escaping. The cases
keep their hostile `installerRepository` and `version`, which only the
escaping protects, and the schematicID half moves to a new pair of cases
asserting the render is refused and no heredoc is produced. Its escape
chain stays in the template as a second line of defence.

The default render of the reconcile Job is byte-identical to main's, so
the merge rolls no worker.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
…t too

The kubernetes-nodes chart carries `accepts a semicolon-separated module
parameter`; the parent chart had no equivalent, so the exact defect that
case exists to prevent was pinned in one chart out of two. render-parity
does not cover it either: its kernelmodules case uses
`nf_conntrack_helper=0`, which carries no semicolon.

Load-bearing check: narrowing the parent's `parameters` class to an
allowlist that bars `;` turns the new case red on the guard's own error
message, and restoring it leaves the suite at 223 green.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
@mattia-eleuteri

Copy link
Copy Markdown
Collaborator Author

Merged main and resolved. The guard and the escaping are kept at every site rather than one side being picked.

The installer image line now carries both: main's escape chain on all three coordinates, and this branch's kubernetes.schematicID include so the per-node-group schematic still resolves. nodegroup.yaml keeps both new group keys (logSerialConsole and schematicID). render-parity.sh keeps main's guest-console-log case alongside the kernel-module, opt-out and schematic cases, and keeps this branch's machine-config comparison, which supersedes the "four pool objects" scope note it replaced.

One thing worth flagging because it touches a file from #3513 rather than one this PR owns. Two cases in hack/talos-reconcile-heredoc_test.bats set a hostile talos.schematicID and asserted the heredoc kept it literal. That value is now refused at render time by this branch's guard, so those two cases went red on the merge. The INVARIANT above the data block admits escaping or render-time validation, and schematicID now takes the second route, so the resolution was to keep the hostile installerRepository and version in those cases (they only have the escaping) and move the schematicID half to a new pair of cases asserting the opposite property: the render is refused and no heredoc is produced at all. The escape chain on schematicID stays in the template as a second line of defence, unreachable for hostile input while the guard holds. Happy to reshape that if you would rather the bats file stayed as you wrote it.

Checks after the merge: 223 tests in packages/apps/kubernetes, 31 in packages/apps/kubernetes-nodes, all 7 cases in hack/talos-reconcile-heredoc_test.bats, GOLDEN PARITY clean, make generate leaves no drift in either package and api/apps/v1alpha1 builds with every type covered by a deepcopy. The default render of the reconcile Job is byte-identical to main's, so the merge rolls no worker.

Also took the two recommended items. The parent chart gained accepts a semicolon-separated module parameter, the case that existed only in kubernetes-nodes; narrowing the parent's parameters class to an allowlist barring ; turns it red on the guard's own error message, so it is load-bearing. And both drifted claims in the body are corrected: 10 cases each rather than 6, and the reordering check fails on gpu and schematic rather than the GPU case alone, which I reproduced by swapping nvidia_drm and nvidia_modeset in the nodes chart's automatic set.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
packages/apps/kubernetes-nodes/tests/schematic_per_pool_test.yaml (1)

103-115: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case that pins validation of the cluster-wide talos.schematicID.

The comment states that validating the effective value also closes the pre-existing path through talos.schematicID. No test covers that path. A future change that moves the guard to the per-pool override only would keep every test green.

🧪 Proposed extra case
+  - it: rejects an unsafe cluster-wide schematicID inherited by the pool
+    set:
+      talos:
+        schematicID: "$(id)"
+    asserts:
+      - failedTemplate:
+          errorPattern: 'invalid schematicID'
+        template: templates/talos-reconcile-job.yaml
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/apps/kubernetes-nodes/tests/schematic_per_pool_test.yaml` around
lines 103 - 115, Extend the schematic validation tests with a case that sets the
cluster-wide talos.schematicID to a shell-injection value while leaving the
per-pool override unset. Assert Helm rendering fails with the existing “invalid
schematicID” error through templates/talos-reconcile-job.yaml, proving
validation covers the effective cluster-wide value.
packages/apps/kubernetes/templates/_helpers.tpl (1)

240-246: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider guarding against a non-list parameters value.

range .parameters | default list fails the render with a Go template error if a user sets parameters as a string or map. The schema declares an array, but the aggregated apiserver does not validate on write, per the comment at lines 213-221. A kindIs "slice" check would produce the same clear message as the other guards.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/apps/kubernetes/templates/_helpers.tpl` around lines 240 - 246,
Guard the parameters handling in the node-group validation before ranging over
`.parameters` by checking that it is a slice with `kindIs "slice"`. For
non-slice values, fail using the same clear invalid-kernelModules-parameter
message pattern as the existing guards; retain the current validation and append
behavior for valid lists.
api/apps/v1alpha1/kubernetes/zz_generated.deepcopy.go (1)

340-359: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Regenerate the deep-copy files from controller-gen source instead of editing them.

hack/update-codegen.sh is the repo generation entrypoint, and these api/apps/v1alpha1/kubernetes/zz_generated.deepcopy.go and api/apps/v1alpha1/kubernetesnodes/zz_generated.deepcopy.go files are controller-gen output. Avoid leaving manual edits in generated Go files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/apps/v1alpha1/kubernetes/zz_generated.deepcopy.go` around lines 340 -
359, Regenerate the DeepCopyInto and DeepCopy implementations using
hack/update-codegen.sh and the controller-gen source definitions instead of
manually editing generated output. Apply this to
api/apps/v1alpha1/kubernetes/zz_generated.deepcopy.go at lines 340-359 and
432-438, and api/apps/v1alpha1/kubernetesnodes/zz_generated.deepcopy.go at lines
68-74 and 120-139; commit only the resulting generated changes.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/apps/v1alpha1/kubernetes/types.go`:
- Around line 281-282: Remove omitempty from NodeGroup.KernelModules in
api/apps/v1alpha1/kubernetes/types.go (lines 281-282) and
ConfigSpec.KernelModules in api/apps/v1alpha1/kubernetesnodes/types.go (lines
51-52) so an explicit [] remains serialized as opt-out; then regenerate the
generated deepcopy outputs.

In `@packages/apps/kubernetes-nodes/README.md`:
- Line 45: Update the generator input for kernelModules so its three-state
behavior is explicit: unset/automatic selects chart defaults, while [] is an
explicit opt-out. Regenerate both generated tables:
packages/apps/kubernetes-nodes/README.md:45-45 and
packages/apps/kubernetes/README.md:126-126, ensuring neither presents [] as the
sole default; do not edit the generated READMEs manually.

In `@packages/apps/kubernetes/README.md`:
- Line 229: Keep the Phase 1 contract for talos.registryMirrors consistent
across packages/apps/kubernetes/README.md:229-229 and
packages/system/kubernetes-rd/cozyrds/kubernetes.yaml:35-35. Since per-tenant
registries.mirrors has no consumer before Phase 2, remove or mark
talos.registryMirrors unsupported in both the documentation and generated
schema, unless implementing the consumer and restoring the Phase 2 behavior is
intended.

In `@packages/system/kubernetes-nodes-rd/cozyrds/kubernetes-nodes.yaml`:
- Line 41: Add ["spec", "kernelModules"] to the keysOrder list in the Kubernetes
node configuration, placing it immediately after ["spec", "schematicID"] and
before ["spec", "kubelet"], while preserving the existing ordering.

---

Nitpick comments:
In `@api/apps/v1alpha1/kubernetes/zz_generated.deepcopy.go`:
- Around line 340-359: Regenerate the DeepCopyInto and DeepCopy implementations
using hack/update-codegen.sh and the controller-gen source definitions instead
of manually editing generated output. Apply this to
api/apps/v1alpha1/kubernetes/zz_generated.deepcopy.go at lines 340-359 and
432-438, and api/apps/v1alpha1/kubernetesnodes/zz_generated.deepcopy.go at lines
68-74 and 120-139; commit only the resulting generated changes.

In `@packages/apps/kubernetes-nodes/tests/schematic_per_pool_test.yaml`:
- Around line 103-115: Extend the schematic validation tests with a case that
sets the cluster-wide talos.schematicID to a shell-injection value while leaving
the per-pool override unset. Assert Helm rendering fails with the existing
“invalid schematicID” error through templates/talos-reconcile-job.yaml, proving
validation covers the effective cluster-wide value.

In `@packages/apps/kubernetes/templates/_helpers.tpl`:
- Around line 240-246: Guard the parameters handling in the node-group
validation before ranging over `.parameters` by checking that it is a slice with
`kindIs "slice"`. For non-slice values, fail using the same clear
invalid-kernelModules-parameter message pattern as the existing guards; retain
the current validation and append behavior for valid lists.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 29c0e880-7cd9-422a-b4db-08b830ff9f7d

📥 Commits

Reviewing files that changed from the base of the PR and between cfaf2e0 and 63f6485.

📒 Files selected for processing (25)
  • api/apps/v1alpha1/kubernetes/types.go
  • api/apps/v1alpha1/kubernetes/zz_generated.deepcopy.go
  • api/apps/v1alpha1/kubernetesnodes/types.go
  • api/apps/v1alpha1/kubernetesnodes/zz_generated.deepcopy.go
  • hack/talos-reconcile-heredoc_test.bats
  • packages/apps/kubernetes-nodes/README.md
  • packages/apps/kubernetes-nodes/templates/_helpers.tpl
  • packages/apps/kubernetes-nodes/templates/nodegroup.yaml
  • packages/apps/kubernetes-nodes/templates/talos-reconcile-job.yaml
  • packages/apps/kubernetes-nodes/tests/kernel_modules_test.yaml
  • packages/apps/kubernetes-nodes/tests/render-parity.sh
  • packages/apps/kubernetes-nodes/tests/schematic_per_pool_test.yaml
  • packages/apps/kubernetes-nodes/values.schema.json
  • packages/apps/kubernetes-nodes/values.yaml
  • packages/apps/kubernetes/README.md
  • packages/apps/kubernetes/templates/_helpers.tpl
  • packages/apps/kubernetes/templates/cluster.yaml
  • packages/apps/kubernetes/templates/talos/talos-reconcile-job.yaml
  • packages/apps/kubernetes/tests/kernel_modules_test.yaml
  • packages/apps/kubernetes/tests/schematic_per_nodegroup_test.yaml
  • packages/apps/kubernetes/tests/talos_templates_test.yaml
  • packages/apps/kubernetes/values.schema.json
  • packages/apps/kubernetes/values.yaml
  • packages/system/kubernetes-nodes-rd/cozyrds/kubernetes-nodes.yaml
  • packages/system/kubernetes-rd/cozyrds/kubernetes.yaml

Comment on lines +281 to +282
// Kernel modules loaded on every worker in this node group, emitted as Talos `machine.kernel.modules`. A Talos system extension installs a module but does not load it, so an extension-provided driver needs its modules declared here. Leave unset to let the chart decide: a node group holding at least one `nvidia.com/*` GPU gets `nvidia` (with `NVreg_NvLinkDisable=1`, which the gpu-operator driver container applies on non-Talos workers; on Talos that container has to be turned off or it clashes with the system extension, and nothing then mounts the ConfigMap carrying the parameter), `nvidia_uvm`, `nvidia_drm`, `nvidia_modeset` (that order — Talos loads the list in sequence and the last three depend on the first), and any other group gets nothing. Set a non-empty list to replace the chart's choice entirely, or `[]` to opt out and emit no modules even on a GPU group. The module still has to be in the image: which extension supplies it is set by `talos.schematicID`, and on Blackwell (GB202) it must be the open-kernel-modules extension. Changing this on a node group that already has running workers does not reach them: the `TalosConfigTemplate` its MachineDeployment references has a fixed name, so rewriting the template leaves `spec.template` untouched, CAPI starts no rollout, and a running Machine keeps the machine config it booted with. Replace the group's Machines to apply it. Unlike `schematicID`, which changes the boot image and therefore rolls the group by itself. Note the automatic set keys on the `nvidia.com/` resource prefix alone: nothing cross-checks that the effective schematic actually carries those modules, so declaring a module the image does not ship is possible. On Talos v1.13 such a module leaves its controller retrying rather than failing the boot, so the symptom is a missing driver, not a dead node.
KernelModules []KernelModule `json:"kernelModules,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 4 'KernelModules|kernelModules|json.Marshal|sigs.k8s.io/(json|yaml)' \
  api internal packages --glob '*.go'

Repository: cozystack/cozystack

Length of output: 39558


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "KernelModules definitions:"
cat -n api/apps/v1alpha1/kubernetes/types.go | sed -n '260,290p'
cat -n api/apps/v1alpha1/kubernetesnodes/types.go | sed -n '35,60p'

echo
echo "All KernelModules fields:"
rg -n '^\s*KernelModules\s+' api --glob '*.go'

echo
echo "Generated slices referencing KernelModules:"
rg -n 'KernelModules' api/apps/v1alpha1/kubernetes api/apps/v1alpha1/kubernetesnodes --glob 'zz_generated.deepcopy.go'
for f in api/apps/v1alpha1/kubernetes/zz_generated.deepcopy.go api/apps/v1alpha1/kubernetesnodes/zz_generated.deepcopy.go; do
  echo "--- $f"
  rg -n -C 8 'KernelModules' "$f" || true
done

echo
echo "Go serialization behavior for nil vs empty slice fields with omitempty:"
cat > /tmp/omitempty_slice.go <<'EOF'
package main

import (
	"encoding/json"
	"fmt"
)

type ConfigSpec struct {
	KernelModules []string `json:"kernelModules,omitempty"`
}

func main() {
	objNil := ConfigSpec{}
	objEmpty := ConfigSpec{KernelModules: []string{}}
	bNil, _ := json.Marshal(objNil)
	bEmpty, _ := json.Marshal(objEmpty)
	fmt.Printf("nil=%q\n", string(bNil))
	fmt.Printf("empty=%q\n", string(bEmpty))
	fmt.Printf("same=%v\n", string(bNil) == string(bEmpty))
}
EOF
if command -v go >/dev/null 2>&1; then
  go run /tmp/omitempty_slice.go
else
  echo "go not available"
fi

Repository: cozystack/cozystack

Length of output: 12081


Keep kernelModules serialized so [] remains opt-out.

Both fields document omission as automatic selection and [] as opt-out, but Go omits empty slices with omitempty. Typed-client create or update paths can turn opt-out into automatic behavior. Remove omitempty for NodeGroup.KernelModules in api/apps/v1alpha1/kubernetes/types.go and for ConfigSpec.KernelModules in api/apps/v1alpha1/kubernetesnodes/types.go, then regenerate the generated deepcopy outputs.

📍 Affects 2 files
  • api/apps/v1alpha1/kubernetes/types.go#L281-L282 (this comment)
  • api/apps/v1alpha1/kubernetesnodes/types.go#L51-L52
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/apps/v1alpha1/kubernetes/types.go` around lines 281 - 282, Remove
omitempty from NodeGroup.KernelModules in api/apps/v1alpha1/kubernetes/types.go
(lines 281-282) and ConfigSpec.KernelModules in
api/apps/v1alpha1/kubernetesnodes/types.go (lines 51-52) so an explicit []
remains serialized as opt-out; then regenerate the generated deepcopy outputs.

| `gpus` | List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM). | `[]object` | `[]` |
| `gpus[i].name` | Name of GPU, such as "nvidia.com/AD102GL_L40S". | `string` | `""` |
| `schematicID` | Per-pool override for `talos.schematicID`, applied to both the worker boot disk image and the Talos installer image. When empty, the cluster-wide `talos.schematicID` applies. A schematic is a fixed set of Talos system extensions, and Talos refuses to finish booting when an extension service in it cannot start: `ext-nvidia-persistenced` and `ext-nvidia-cdi-gen` require an NVIDIA card, so a pool with no GPU that boots an NVIDIA schematic fails `startAllServices` and reboots roughly every 70 minutes, indefinitely, while still reporting `Ready` (kubelet starts before the failing phase). A cluster mixing GPU and non-GPU pools therefore has no correct cluster-wide value, and this field is what makes it expressible: set the NVIDIA schematic on the GPU pool only. Changing it replaces the pool's boot image and so rolls its workers. | `string` | `""` |
| `kernelModules` | Kernel modules loaded on every worker in this pool, emitted as Talos `machine.kernel.modules`. A Talos system extension installs a module but does not load it, so an extension-provided driver needs its modules declared here. Leave unset to let the chart decide: a pool holding at least one `nvidia.com/*` GPU gets `nvidia` (with `NVreg_NvLinkDisable=1`, which the gpu-operator driver container applies on non-Talos workers; on Talos that container has to be turned off or it clashes with the system extension, and nothing then mounts the ConfigMap carrying the parameter), `nvidia_uvm`, `nvidia_drm`, `nvidia_modeset` (that order — Talos loads the list in sequence and the last three depend on the first), and any other pool gets nothing. Set a non-empty list to replace the chart's choice entirely, or `[]` to opt out and emit no modules even on a GPU pool. The module still has to be in the image: which extension supplies it is set by `talos.schematicID`, and on Blackwell (GB202) it must be the open-kernel-modules extension. Changing this on a pool that already has running workers does not reach them: the `TalosConfigTemplate` its MachineDeployment references has a fixed name, so rewriting the template leaves `spec.template` untouched, CAPI starts no rollout, and a running Machine keeps the machine config it booted with. Replace the pool's Machines to apply it. Unlike `schematicID`, which changes the boot image and therefore rolls the pool by itself. Note the automatic set keys on the `nvidia.com/` resource prefix alone: nothing cross-checks that the effective schematic actually carries those modules, so declaring a module the image does not ship is possible. On Talos v1.13 such a module leaves its controller retrying rather than failing the boot, so the symptom is a missing driver, not a dead node. | `[]object` | `[]` |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the three-state kernelModules default explicit in both READMEs. Both tables use [] for a field where [] is also the explicit opt-out, while the generated schemas omit a default.

  • packages/apps/kubernetes-nodes/README.md#L45-L45: label the value as unset or automatic, or state that [] is opt-out only.
  • packages/apps/kubernetes/README.md#L126-L126: apply the same correction to the per-node-group table.

Based on learnings, update the generator input and regenerate both READMEs instead of editing generated tables manually.

📍 Affects 2 files
  • packages/apps/kubernetes-nodes/README.md#L45-L45 (this comment)
  • packages/apps/kubernetes/README.md#L126-L126
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/apps/kubernetes-nodes/README.md` at line 45, Update the generator
input for kernelModules so its three-state behavior is explicit: unset/automatic
selects chart defaults, while [] is an explicit opt-out. Regenerate both
generated tables: packages/apps/kubernetes-nodes/README.md:45-45 and
packages/apps/kubernetes/README.md:126-126, ensuring neither presents [] as the
sole default; do not edit the generated READMEs manually.

Source: Learnings

| `talos.schematicID` | Talos image-factory schematic ID. Defaults to the cozystack-tested vanilla schematic. Operators using custom schematics (system extensions, kernel args) override here. A node group can override it for itself via `nodeGroups.<name>.schematicID`. The effective value is rejected if it contains `$`, a backtick, a backslash, quotes or whitespace, because it is interpolated into a worker machine config applied through a shell heredoc; a 64-hex factory digest, a readable name and a multi-segment mirror path such as `gpu/nvidia-open` all pass. That guard covers this field only, not every value reaching that heredoc. | `string` | `ce4c980550dd2ab1b17bbf2b08801c7eb59418eafe8f279833297925d67c7515` |
| `talos.imageFactoryURL` | Base URL of the Talos Image Factory that serves the worker OS disk image (the `openstack-amd64.raw.xz` raw artifact streamed in by CDI over HTTP). Defaults to the public factory. Point at a self-hosted Image Factory, a caching mirror, or an internal HTTP file server for air-gapped, rate-limited, or flaky-egress environments. No trailing slash. | `string` | `https://factory.talos.dev` |
| `talos.installerRepository` | OCI repository prefix for the Talos installer image used by the in-guest `talos-reconcile` upgrade Job. Resolved as `<installerRepository>/<schematicID>:<version>`. Defaults to the public factory's installer path. Override for air-gapped or mirrored registries. No trailing slash. | `string` | `factory.talos.dev/installer` |
| `talos.registryMirrors` | Talos `machine.registries.mirrors` passthrough for worker nodes: a map of upstream registry host to `{ endpoints: [ ... ] }`. Empty by default, so workers pull container images (the Talos `kubelet` image included) directly from the upstream registry. Point a host such as `ghcr.io` at an in-cluster pull-through mirror for air-gapped, rate-limited, or flaky-egress environments so a worker's boot does not depend on live public egress. Talos still falls back to the upstream registry unless a host also sets `skipFallback: true`, so a mirror alone does not enforce air-gap. | `object` | `{}` |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)packages/apps/kubernetes/README.md|packages/system/kubernetes-rd/cozyrds/kubernetes.yaml|cozyrds|kubernetes-rd' || true

echo
echo "== README references =="
if [ -f packages/apps/kubernetes/README.md ]; then
  rg -n "registryMirrors|registry mirror|registry mirrors|registry.*mirror|Phase 1|unavailable|supported" packages/apps/kubernetes/README.md
  sed -n '70,110p' packages/apps/kubernetes/README.md
  sed -n '218,236p' packages/apps/kubernetes/README.md
fi

echo
echo "== generated schema references =="
if [ -f packages/system/kubernetes-rd/cozyrds/kubernetes.yaml ]; then
  rg -n "registryMirrors|talos\.registryMirrors|machine\.registries\.mirrors|images|unsupported" packages/system/kubernetes-rd/cozyrds/kubernetes.yaml
  sed -n '28,38p' packages/system/kubernetes-rd/cozyrds/kubernetes.yaml
  sed -n '215,235p' packages/system/kubernetes-rd/cozyrds/kubernetes.yaml
fi

echo
echo "== repository references =="
rg -n "registryMirrors|talos.registryMirrors|machine.registries.mirrors|registryMirrors" . -g '!node_modules' -g '!dist' -g '!build' -g '!vendor' | head -n 200 || true

Repository: cozystack/cozystack

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package tree =="
git ls-files packages/system/kubernetes-rd | sort

echo
echo "== kubernetes-rd values.yaml talos section =="
sed -n '/^talos:/,/^\w\+:/p' packages/system/kubernetes-rd/values.yaml | sed -n '1,120p'

echo
echo "== kubernetes-rd templates matching registry/mirror/machine =="
rg -n "registry|mirror|machine\.registries|registries\.mirrors|talosRegistryMirrors|talamd|talosconfig|patches-containerd|noConsumer|Phase 2" packages/system/kubernetes-rd -g '!cozyrds/kubernetes.yaml' -g '!values.yaml' -g '!Chart.yaml' | head -n 200 || true

echo
echo "== precise talos.registryMirrors references in kubernetes-rd templates only =="
rg -n "registryMirrors|registries\.mirrors|machine\.registries|noConsumer|Phase 2|Phase 1" packages/system/kubernetes-rd/templates packages/system/kubernetes-rd/*.tpl packages/system/kubernetes-rd -g '.*\.(yaml|tpl|yaml\.tpl|gotmpl)$' || true

echo
echo "== values generator config maybe =="
rg -n "talos.registryMirrors|registryMirrors|keysOrder|Chart Values|cozystack-options" packages/system/kubernetes-rd packages/system -g '!cozyrds/*' | head -n 200 || true

Repository: cozystack/cozystack

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== kubernetes-rd files =="
git ls-files packages/system/kubernetes-rd | sed -n '1,120p'

echo
echo "== values talos outline =="
sed -n '/^talos:/,/^[A-Za-z0-9_-]\+:/p' packages/system/kubernetes-rd/values.yaml | sed -n '1,80p' || true

echo
echo "== exact talos.registryMirrors references in packages/system/kubernetes-rd/templates =="
rg -n "registryMirrors|registries\.mirrors|machine\.registries|noConsumer|Phase 2|Phase 1|patch-containerd" packages/system/kubernetes-rd/templates packages/system/kubernetes-rd/*.tpl packages/system/kubernetes-rd -g '.*\.(yaml|tpl|yaml\.tpl|gotmpl)$' || true

echo
echo "== exact generated schema registryMirrors lines =="
python3 - <<'PY'
from pathlib import Path
p=Path("packages/system/kubernetes-rd/cozyrds/kubernetes.yaml")
for i,line in enumerate(p.read_text().splitlines(),1):
    if "registryMirrors" in line or "registries.mirrors" in line:
        s=max(1,i-3); e=min(80,i+3)
        print(f"--- near line {i} ---")
        for n in range(s,e+1):
            print(f"{n}: {p.read_text().splitlines()[n-1]}")
PY

echo
echo "== exact README registryMirrors lines =="
python3 - <<'PY'
from pathlib import Path
p=Path("packages/apps/kubernetes/README.md")
for i,line in enumerate(p.read_text().splitlines(),1):
    if "registryMirrors" in line or "registries.mirrors" in line:
        s=max(1,i-1); e=min(300,i+1)
        print(f"--- near line {i} ---")
        for n in range(s,e+1):
            print(f"{n}: {p.read_text().splitlines()[n-1]}")
PY

Repository: cozystack/cozystack

Length of output: 46020


Keep talos.registryMirrors contract consistent. Line 90 says per-tenant registries.mirrors overrides still have no consumer until Phase 2, while the Parameters table and generated schema advertise talos.registryMirrors as supported. Mark talos.registryMirrors unsupported or removed from the Phase 1 docs/schema, or implement the consumer and restore the Phase 2 text.

📍 Affects 2 files
  • packages/apps/kubernetes/README.md#L229-L229 (this comment)
  • packages/system/kubernetes-rd/cozyrds/kubernetes.yaml#L35-L35
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/apps/kubernetes/README.md` at line 229, Keep the Phase 1 contract
for talos.registryMirrors consistent across
packages/apps/kubernetes/README.md:229-229 and
packages/system/kubernetes-rd/cozyrds/kubernetes.yaml:35-35. Since per-tenant
registries.mirrors has no consumer before Phase 2, remove or mark
talos.registryMirrors unsupported in both the documentation and generated
schema, unless implementing the consumer and restoring the Phase 2 behavior is
intended.

description: Worker node pool for a managed Kubernetes cluster
icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDQiIGhlaWdodD0iMTQ0IiByeD0iMjQiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl82ODFfMjg0NSkiLz4KPHBhdGggZD0iTTcxLjk5NjggMTlDNzAuMzAzOSAxOS4wMDAyIDY4LjkzMTIgMjAuNTMyMiA2OC45MzE0IDIyLjQyMjFDNjguOTMxNCAyMi40NTExIDY4LjkzNzMgMjIuNDc4OCA2OC45Mzc5IDIyLjUwNzZDNjguOTM1NCAyMi43NjQ0IDY4LjkyMzEgMjMuMDczNyA2OC45MzE0IDIzLjI5NzNDNjguOTcxNyAyNC4zODczIDY5LjIwODIgMjUuMjIxNiA2OS4zNTA2IDI2LjIyNThDNjkuNjA4NCAyOC4zNzUyIDY5LjgyNDUgMzAuMTU2OSA2OS42OTEyIDMxLjgxM0M2OS41NjE1IDMyLjQzNzUgNjkuMTAzNyAzMy4wMDg2IDY4LjY5NTYgMzMuNDA1Nkw2OC42MjM1IDM0LjcwODZDNjYuNzgzOSAzNC44NjE3IDY0LjkzMTkgMzUuMTQyMSA2My4wODIxIDM1LjU2NDFDNTUuMTIyNiAzNy4zNzk4IDQ4LjI2OTUgNDEuNDk5MSA0My4wNTIgNDcuMDYwOUM0Mi43MTM0IDQ2LjgyODggNDIuMTIxMSA0Ni40MDE5IDQxLjk0NSA0Ni4yNzEyQzQxLjM5NzcgNDYuMzQ1NCA0MC44NDQ1IDQ2LjUxNTEgNDAuMTI0MSA0Ni4wOTM1QzM4Ljc1MjIgNDUuMTY1NyAzNy41MDI4IDQzLjg4NTEgMzUuOTkxIDQyLjM0MjRDMzUuMjk4MiA0MS42MDQ0IDM0Ljc5NjYgNDAuOTAxOCAzMy45NzM1IDQwLjE5MDRDMzMuNzg2NiA0MC4wMjg5IDMzLjUwMTQgMzkuODEwNCAzMy4yOTIzIDM5LjY0NDJDMzIuNjQ4OSAzOS4xMjg4IDMxLjg5IDM4Ljg2IDMxLjE1NyAzOC44MzQ4QzMwLjIxNDcgMzguODAyNCAyOS4zMDc1IDM5LjE3MjUgMjguNzEzOCAzOS45MjA2QzI3LjY1ODQgNDEuMjUwNiAyNy45OTYzIDQzLjI4MzMgMjkuNDY3MSA0NC40NjE0QzI5LjQ4MiA0NC40NzM0IDI5LjQ5NzkgNDQuNDgyNyAyOS41MTI5IDQ0LjQ5NDNDMjkuNzE1IDQ0LjY1ODkgMjkuOTYyNSA0NC44Njk4IDMwLjE0ODMgNDUuMDA3NkMzMS4wMjE3IDQ1LjY1NTUgMzEuODE5NSA0NS45ODcyIDMyLjY4OTcgNDYuNTAxNUMzNC41MjMxIDQ3LjYzOTEgMzYuMDQzIDQ4LjU4MjMgMzcuMjQ4NiA0OS43MTk2QzM3LjcxOTQgNTAuMjIzNyAzNy44MDE2IDUxLjExMjIgMzcuODY0MyA1MS40OTY0TDM4Ljg0NjggNTIuMzc4MkMzMy41ODcyIDYwLjMzMDggMzEuMTUzIDcwLjE1MzkgMzIuNTkxNSA4MC4xNjI3TDMxLjMwNzcgODAuNTM3OEMzMC45NjkzIDgwLjk3NjggMzAuNDkxMiA4MS42Njc2IDI5Ljk5MTEgODEuODczOEMyOC40MTM4IDgyLjM3MjkgMjYuNjM4NyA4Mi41NTYyIDI0LjQ5NTYgODIuNzgxOUMyMy40ODk0IDgyLjg2NiAyMi42MjEzIDgyLjgxNTggMjEuNTU0NiA4My4wMTg4QzIxLjMxOTggODMuMDYzNSAyMC45OTI3IDgzLjE0OTEgMjAuNzM1OCA4My4yMDk3QzIwLjcyNjkgODMuMjExNiAyMC43MTg2IDgzLjIxNDIgMjAuNzA5NiA4My4yMTYyQzIwLjY5NTYgODMuMjE5NSAyMC42NzcyIDgzLjIyNjMgMjAuNjYzOCA4My4yMjk0QzE4Ljg1NyA4My42NjggMTcuNjk2MyA4NS4zMzY1IDE4LjA2OTkgODYuOTgwNUMxOC40NDM3IDg4LjYyNDggMjAuMjA4NiA4OS42MjQ4IDIyLjAyNjIgODkuMjMxMkMyMi4wMzkzIDg5LjIyODIgMjIuMDU4NCA4OS4yMjc3IDIyLjA3MiA4OS4yMjQ2QzIyLjA5MjYgODkuMjE5OSAyMi4xMTA2IDg5LjIwOTkgMjIuMTMxIDg5LjIwNDlDMjIuMzg0NCA4OS4xNDkgMjIuNzAxOSA4OS4wODY4IDIyLjkyMzYgODkuMDI3MkMyMy45NzIzIDg4Ljc0NTEgMjQuNzMxOCA4OC4zMzA2IDI1LjY3NDYgODcuOTY3N0MyNy43MDI5IDg3LjIzNjggMjkuMzgyOCA4Ni42MjYyIDMxLjAxOTUgODYuMzg4M0MzMS43MDMgODYuMzM0NSAzMi40MjMyIDg2LjgxMiAzMi43ODE0IDg3LjAxMzRMMzQuMTE3NyA4Ni43ODMxQzM3LjE5MjYgOTYuMzYxMyA0My42MzY2IDEwNC4xMDMgNTEuNzk2MyAxMDguOTYxTDUxLjIzOTYgMTEwLjMwM0M1MS40NDAzIDExMC44MjQgNTEuNjYxNiAxMTEuNTMgNTEuNTEyMSAxMTIuMDQ1QzUwLjkxNzEgMTEzLjU5NSA0OS44OTggMTE1LjIzMSA0OC43Mzc0IDExNy4wNTVDNDguMTc1NSAxMTcuODk4IDQ3LjYwMDQgMTE4LjU1MiA0Ny4wOTM0IDExOS41MTZDNDYuOTcyIDExOS43NDcgNDYuODE3NSAxMjAuMTAyIDQ2LjcwMDQgMTIwLjM0NkM0NS45MTI1IDEyMi4wMzkgNDYuNDkwNCAxMjMuOTkgNDguMDAzOCAxMjQuNzIyQzQ5LjUyNjggMTI1LjQ1OSA1MS40MTcxIDEyNC42ODIgNTIuMjM1MiAxMjIuOTg1QzUyLjIzNjQgMTIyLjk4MiA1Mi4yNDA2IDEyMi45OCA1Mi4yNDE3IDEyMi45NzhDNTIuMjQyNiAxMjIuOTc2IDUyLjI0MDkgMTIyLjk3MyA1Mi4yNDE3IDEyMi45NzFDNTIuMzU4MiAxMjIuNzMxIDUyLjUyMzMgMTIyLjQxNSA1Mi42MjE2IDEyMi4xODhDNTMuMDU2IDEyMS4xODkgNTMuMjAwNSAxMjAuMzMyIDUzLjUwNTkgMTE5LjM2NUM1NC4zMTcgMTE3LjMxOCA1NC43NjI2IDExNS4xNyA1NS44NzkxIDExMy44MzJDNTYuMTg0OSAxMTMuNDY2IDU2LjY4MzMgMTEzLjMyNSA1Ny4yMDAxIDExMy4xODZMNTcuODk0NCAxMTEuOTIyQzY1LjAwOCAxMTQuNjY1IDcyLjk3MDUgMTE1LjQwMiA4MC45MjQ1IDExMy41ODdDODIuNzM5MSAxMTMuMTczIDg0LjQ5MDggMTEyLjYzNyA4Ni4xODQzIDExMS45OTRDODYuMzc5NCAxMTIuMzQyIDg2Ljc0MiAxMTMuMDExIDg2LjgzOTMgMTEzLjE3OUM4Ny4zNjQ0IDExMy4zNTEgODcuOTM3NyAxMTMuNDM5IDg4LjQwNDcgMTE0LjEzM0M4OS4yNDAxIDExNS41NjcgODkuODExNCAxMTcuMjYzIDkwLjUwNzMgMTE5LjMxMkM5MC44MTI4IDEyMC4yNzkgOTAuOTYzOCAxMjEuMTM2IDkxLjM5ODEgMTIyLjEzNkM5MS40OTcxIDEyMi4zNjMgOTEuNjYxNCAxMjIuNjg0IDkxLjc3OCAxMjIuOTI1QzkyLjU5NDQgMTI0LjYyOCA5NC40OTA3IDEyNS40MDcgOTYuMDE1OSAxMjQuNjY5Qzk3LjUyOTIgMTIzLjkzNyA5OC4xMDc3IDEyMS45ODYgOTcuMzE5NCAxMjAuMjkzQzk3LjIwMjMgMTIwLjA0OSA5Ny4wNDEyIDExOS42OTUgOTYuOTE5OCAxMTkuNDY0Qzk2LjQxMjcgMTE4LjQ5OSA5NS44Mzc3IDExNy44NTIgOTUuMjc1OCAxMTcuMDA5Qzk0LjExNTIgMTE1LjE4NSA5My4xNTI2IDExMy42NyA5Mi41NTc1IDExMi4xMkM5Mi4zMDg3IDExMS4zMiA5Mi41OTk1IDExMC44MjMgOTIuNzkzMyAxMTAuMzAzQzkyLjY3NzIgMTEwLjE3IDkyLjQyODggMTA5LjQxNCA5Mi4yODI0IDEwOS4wNTlDMTAwLjc2MiAxMDQuMDI5IDEwNy4wMTcgOTUuOTk4NSAxMDkuOTU1IDg2LjcyMzlDMTEwLjM1MSA4Ni43ODY1IDExMS4wNDEgODYuOTA5MSAxMTEuMjY1IDg2Ljk1NDJDMTExLjcyNiA4Ni42NDg3IDExMi4xNDkgODYuMjUwMSAxMTIuOTgxIDg2LjMxNTlDMTE0LjYxNyA4Ni41NTM3IDExNi4yOTcgODcuMTY0NSAxMTguMzI2IDg3Ljg5NTNDMTE5LjI2OCA4OC4yNTgxIDEyMC4wMjggODguNjc5MyAxMjEuMDc3IDg4Ljk2MTRDMTIxLjI5OCA4OS4wMjEgMTIxLjYxNiA4OS4wNzY2IDEyMS44NjkgODkuMTMyNUMxMjEuODg5IDg5LjEzNzUgMTIxLjkwOCA4OS4xNDc1IDEyMS45MjggODkuMTUyMkMxMjEuOTQyIDg5LjE1NTMgMTIxLjk2MSA4OS4xNTU4IDEyMS45NzQgODkuMTU4OEMxMjMuNzkyIDg5LjU1MiAxMjUuNTU3IDg4LjU1MjYgMTI1LjkzIDg2LjkwODFDMTI2LjMwMyA4NS4yNjQxIDEyNS4xNDMgODMuNTk1MiAxMjMuMzM2IDgzLjE1N0MxMjMuMDc0IDgzLjA5NyAxMjIuNzAxIDgyLjk5NSAxMjIuNDQ2IDgyLjk0NjVDMTIxLjM3OSA4Mi43NDM1IDEyMC41MTEgODIuNzkzNSAxMTkuNTA1IDgyLjcwOTVDMTE3LjM2MSA4Mi40ODM5IDExNS41ODYgODIuMzAwNCAxMTQuMDA5IDgxLjgwMTRDMTEzLjM2NiA4MS41NTA3IDExMi45MDggODAuNzgxOSAxMTIuNjg2IDgwLjQ2NTVMMTExLjQ0OCA4MC4xMDM1QzExMi4wOSA3NS40MzggMTExLjkxNyA3MC41ODI1IDExMC44MDYgNjUuNzI0M0MxMDkuNjg1IDYwLjgyMDggMTA3LjcwNCA1Ni4zMzYxIDEwNS4wNjIgNTIuMzg0OEMxMDUuMzc5IDUyLjA5NDggMTA1Ljk3OSA1MS41NjEyIDEwNi4xNDkgNTEuNDA0M0MxMDYuMTk5IDUwLjg1MTcgMTA2LjE1NiA1MC4yNzIyIDEwNi43MjUgNDkuNjYwM0MxMDcuOTMxIDQ4LjUyMyAxMDkuNDUxIDQ3LjU3OTkgMTExLjI4NCA0Ni40NDIzQzExMi4xNTQgNDUuOTI3OSAxMTIuOTU5IDQ1LjU5NjQgMTEzLjgzMiA0NC45NDg0QzExNC4wMyA0NC44MDE5IDExNC4yOTkgNDQuNTY5OSAxMTQuNTA3IDQ0LjQwMjJDMTE1Ljk3NyA0My4yMjM3IDExNi4zMTYgNDEuMTkxMSAxMTUuMjYgMzkuODYxNEMxMTQuMjA0IDM4LjUzMTcgMTEyLjE1OSAzOC40MDY1IDExMC42ODggMzkuNTg1QzExMC40NzkgMzkuNzUxNiAxMTAuMTk1IDM5Ljk2ODggMTEwLjAwNyA0MC4xMzEyQzEwOS4xODQgNDAuODQyNiAxMDguNjc2IDQxLjU0NTIgMTA3Ljk4MyA0Mi4yODMyQzEwNi40NzEgNDMuODI1OSAxMDUuMjIyIDQ1LjExMyAxMDMuODUgNDYuMDQwOUMxMDMuMjU1IDQ2LjM4ODUgMTAyLjM4NSA0Ni4yNjgyIDEwMS45OSA0Ni4yNDQ5TDEwMC44MjQgNDcuMDgwNkM5NC4xNzUzIDQwLjA3NjMgODUuMTIzNSAzNS41OTgyIDc1LjM3NjYgMzQuNzI4M0M3NS4zNDk0IDM0LjMxNzkgNzUuMzEzNyAzMy41NzYxIDc1LjMwNDYgMzMuMzUyOUM3NC45MDU2IDMyLjk2OTMgNzQuNDIzNSAzMi42NDE4IDc0LjMwMjQgMzEuODEzQzc0LjE2OTEgMzAuMTU2OSA3NC4zOTE3IDI4LjM3NTIgNzQuNjQ5NiAyNi4yMjU4Qzc0Ljc5MTkgMjUuMjIxNiA3NS4wMjg0IDI0LjM4NzMgNzUuMDY4OCAyMy4yOTczQzc1LjA3OCAyMy4wNDk1IDc1LjA2MzIgMjIuNjkgNzUuMDYyMiAyMi40MjIxQzc1LjA2MiAyMC41MzIyIDczLjY4OTggMTguOTk5OCA3MS45OTY4IDE5Wk02OC4xNTg1IDQyLjg4ODZMNjcuMjQ4IDU5LjA0NDdMNjcuMTgyNSA1OS4wNzc2QzY3LjEyMTQgNjAuNTIyOSA2NS45Mzc1IDYxLjY3NyA2NC40ODM5IDYxLjY3N0M2My44ODg0IDYxLjY3NyA2My4zMzg4IDYxLjQ4NDkgNjIuODkyMiA2MS4xNTcxTDYyLjg2NiA2MS4xNzAzTDQ5LjY4MDcgNTEuNzc5NEM1My43MzMxIDQ3Ljc3NTkgNTguOTE2NCA0NC44MTcyIDY0Ljg5IDQzLjQ1NDZDNjUuOTgxMiA0My4yMDU2IDY3LjA3MTkgNDMuMDIwOSA2OC4xNTg1IDQyLjg4ODZaTTc1Ljg0MTcgNDIuODg4NkM4Mi44MTU5IDQzLjc1MDQgODkuMjY1NyA0Ni45MjMyIDk0LjIwODEgNTEuNzg2TDgxLjEwOCA2MS4xMTc2TDgxLjA2MjEgNjEuMDk3OUM3OS44OTk0IDYxLjk1MTIgNzguMjYxMSA2MS43Mzk0IDc3LjM1NDggNjAuNTk3OEM3Ni45ODM1IDYwLjEzMDEgNzYuNzg4NyA1OS41ODAxIDc2Ljc2NTMgNTkuMDI0OUw3Ni43NTIyIDU5LjAxODRMNzUuODQxNyA0Mi44ODg2Wk00NC44OTkxIDU3LjgxNEw1Ni45MzgyIDY4LjYzM0w1Ni45MjUxIDY4LjY5ODhDNTguMDExNyA2OS42NDc5IDU4LjE3MiA3MS4yOTQ5IDU3LjI2NTcgNzIuNDM2OEM1Ni44OTQ0IDcyLjkwNDUgNTYuMzk3NSA3My4yMTgyIDU1Ljg2MzkgNzMuMzY0N0w1NS44NTA4IDczLjQxNzNMNDAuNDE4OCA3Ny44OTIzQzM5LjYzMzQgNzAuNjc2NSA0MS4zMjYxIDYzLjY2MjEgNDQuODk5MSA1Ny44MTRaTTk5LjAwOTQgNTcuODIwNkMxMDAuNzk4IDYwLjczMzYgMTAyLjE1MyA2My45ODcxIDEwMi45NTkgNjcuNTE0M0MxMDMuNzU2IDcwLjk5OTEgMTAzLjk1NiA3NC40Nzc4IDEwMy42MjcgNzcuODM5N0w4OC4xMTY2IDczLjM1MTVMODguMTAzNSA3My4yODU3Qzg2LjcxNDUgNzIuOTA0MyA4NS44NjA5IDcxLjQ4NDggODYuMTg0MyA3MC4wNjExQzg2LjMxNjggNjkuNDc3OCA4Ni42MjQ5IDY4Ljk4NDQgODcuMDQyMyA2OC42MTk4TDg3LjAzNTggNjguNTg2OUw5OS4wMDk0IDU3LjgyMDZaTTY5LjUyNzQgNjkuNDY4OEg3NC40NTk2TDc3LjUyNTEgNzMuMzE4Nkw3Ni40MjQ3IDc4LjEyMjZMNzEuOTk2OCA4MC4yNjE0TDY3LjU1NTggNzguMTE2MUw2Ni40NTU0IDczLjMxMkw2OS41Mjc0IDY5LjQ2ODhaTTg1LjMzOTMgODIuNjQzN0M4NS41NDg5IDgyLjYzMzEgODUuNzU3NiA4Mi42NTIgODUuOTYxNiA4Mi42ODk4TDg1Ljk4NzggODIuNjU2OUwxMDEuOTUgODUuMzY4MkM5OS42MTQyIDkxLjk2MjQgOTUuMTQ0IDk3LjY3NSA4OS4xNzExIDEwMS40OThMODIuOTc0NyA4Ni40NjA2TDgyLjk5NDQgODYuNDM0M0M4Mi40MjUyIDg1LjEwNTUgODIuOTk0OCA4My41NDcyIDg0LjMwNDQgODIuOTEzNUM4NC42Mzk3IDgyLjc1MTMgODQuOTkgODIuNjYxNCA4NS4zMzkzIDgyLjY0MzdaTTU4LjUyOTggODIuNzA5NUM1OS43NDggODIuNzI2NyA2MC44NDA2IDgzLjU3NjEgNjEuMTIzNyA4NC44MjJDNjEuMjU2MiA4NS40MDUyIDYxLjE5MTcgODUuOTgzMSA2MC45NzMgODYuNDkzNUw2MS4wMTg5IDg2LjU1MjdMNTQuODg4IDEwMS40MzlDNDkuMTU1OSA5Ny43NDMyIDQ0LjU5MDQgOTIuMjA5OSA0Mi4xNDgxIDg1LjQyMDhMNTcuOTczMSA4Mi43MjI3TDU3Ljk5OTMgODIuNzU1NkM1OC4xNzYzIDgyLjcyMjkgNTguMzU1OCA4Mi43MDcxIDU4LjUyOTggODIuNzA5NVpNNzEuODk4NiA4OS4yMzEyQzcyLjMyMjkgODkuMjE1NSA3Mi43NTM0IDg5LjMwMyA3My4xNjI3IDg5LjUwMUM3My42OTkyIDg5Ljc2MDYgNzQuMTEzNiA5MC4xNjkyIDc0LjM3NDUgOTAuNjU5Mkg3NC40MzM0TDgyLjIzNDYgMTA0LjgyMUM4MS4yMjIxIDEwNS4xNjIgODAuMTgxMyAxMDUuNDU0IDc5LjExNjcgMTA1LjY5N0M3My4xNTA1IDEwNy4wNTggNjcuMjAzMiAxMDYuNjQ1IDYxLjgxOCAxMDQuODAyTDY5LjU5OTUgOTAuNjY1OEg2OS42MTI2QzcwLjA3OTUgODkuNzg4OCA3MC45NjUgODkuMjY1NiA3MS44OTg2IDg5LjIzMTJaIiBmaWxsPSJ3aGl0ZSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIwLjI1Ii8+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfNjgxXzI4NDUiIHgxPSIxMCIgeTE9IjE1LjUiIHgyPSIxNDQiIHkyPSIxMzEuNSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjNEQ4N0ZGIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzA1NDdEMCIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=
keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "cluster"], ["spec", "storageClass"], ["spec", "minReplicas"], ["spec", "maxReplicas"], ["spec", "instanceType"], ["spec", "diskSize"], ["spec", "roles"], ["spec", "resources"], ["spec", "gpus"], ["spec", "kubelet"], ["spec", "logSerialConsole"], ["spec", "maxUnhealthy"], ["spec", "nodeStartupTimeout"], ["spec", "version"], ["spec", "talos"], ["spec", "talos", "version"], ["spec", "talos", "schematicID"], ["spec", "talos", "imageFactoryURL"], ["spec", "talos", "installerRepository"], ["spec", "talos", "registryMirrors"], ["spec", "images"], ["spec", "images", "kubectl"]]
keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "cluster"], ["spec", "storageClass"], ["spec", "minReplicas"], ["spec", "maxReplicas"], ["spec", "instanceType"], ["spec", "diskSize"], ["spec", "roles"], ["spec", "resources"], ["spec", "gpus"], ["spec", "schematicID"], ["spec", "kubelet"], ["spec", "logSerialConsole"], ["spec", "maxUnhealthy"], ["spec", "nodeStartupTimeout"], ["spec", "version"], ["spec", "talos"], ["spec", "talos", "version"], ["spec", "talos", "schematicID"], ["spec", "talos", "imageFactoryURL"], ["spec", "talos", "installerRepository"], ["spec", "talos", "registryMirrors"], ["spec", "images"], ["spec", "images", "kubectl"]]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate kubernetes-nodes.yaml =="
fd -a 'kubernetes-nodes\.yaml$' . || true

echo "== relevant file lines =="
if [ -f packages/system/kubernetes-nodes-rd/cozyrds/kubernetes-nodes.yaml ]; then
  wc -l packages/system/kubernetes-nodes-rd/cozyrds/kubernetes-nodes.yaml
  sed -n '1,120p' packages/system/kubernetes-nodes-rd/cozyrds/kubernetes-nodes.yaml | cat -n
fi

echo "== search for kernelModules and keysOrder references =="
rg -n 'kernelModules|keysOrder|kubernetes-nodes-rd|kubernetes-nodes\.yaml' .

Repository: cozystack/cozystack

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate keysOrder dashboard implementation =="
rg -n --fixed-strings 'keysOrder' dashboards packages web frontend apps api . \
  --glob '!**/dist/**' --glob '!**/build/**' --glob '!**/node_modules/**' \
  --glob '!packages/system/kubernetes-nodes-rd/cozyrds/kubernetes-nodes.yaml' \
  --glob '!packages/system/*-rd/cozyrds/*.yaml' \
  --glob '!packages/system/*-rd/Chart.yaml' \
  --glob '!**/*.svg' | head -200

echo "== inspect likely dashboard renderer snippets =="
for f in $(rg -l --fixed-strings 'keysOrder' . --glob '!**/dist/**' --glob '!**/build/**' --glob '!**/node_modules/**' | head -30); do
  echo "--- $f ---"
  sed -n '1,180p' "$f" | cat -n
done

Repository: cozystack/cozystack

Length of output: 6123


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== keys-order implementation =="
sed -n '1,220p' packages/system/dashboard/images/console/apps/console/src/lib/keys-order.ts | cat -n

echo "== keys-order tests =="
sed -n '1,160p' packages/system/dashboard/images/console/apps/console/src/lib/keys-order.test.ts | cat -n

echo "== SchemaForm keysOrder usage =="
sed -n '240,320p' packages/system/dashboard/images/console/apps/console/src/components/SchemaForm.tsx | cat -n

echo "== update-crd generation context =="
sed -n '80,165p' hack/update-crd.sh | cat -n

Repository: cozystack/cozystack

Length of output: 15157


Add spec.kernelModules to keysOrder.

spec.kernelModules is present in the schema, but keysOrder currently jumps from spec.schematicID to spec.kubelet, causing omitted spec fields to render unordered in the dashboard. Insert ["spec", "kernelModules"] after ["spec", "schematicID"].

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/system/kubernetes-nodes-rd/cozyrds/kubernetes-nodes.yaml` at line
41, Add ["spec", "kernelModules"] to the keysOrder list in the Kubernetes node
configuration, placing it immediately after ["spec", "schematicID"] and before
["spec", "kubelet"], while preserving the existing ordering.

@lexfrei

Copy link
Copy Markdown
Contributor

NOT LGTM. Two things, both new since the last round: the merge of main dropped a paragraph main owns, and the kernelModules field description states a property that stops being true the moment #3523 lands.

Everything I checked from the previous rounds still holds. The helm suites are green (223 in kubernetes, 31 in kubernetes-nodes), render-parity.sh reports GOLDEN PARITY on all seven cases, the heredoc suite passes all seven of its tests, and make generate in both packages plus controller-gen object for the api submodule produce no diff.

B1: the merge of main reverted the air-gapped bullet in packages/apps/kubernetes/README.md

The merge commit resolved README.md by keeping the branch side, so the paragraph #3575 rewrote is gone and the pre-#3575 text is back. Relative to its branch parent the merge added 2 lines to that file; relative to main it changed 39 and removed 35.

$ git show origin/main:packages/apps/kubernetes/README.md | grep -c 'talos.registryMirrors` maps an upstream registry host'
1
$ git show HEAD:packages/apps/kubernetes/README.md       | grep -c 'talos.registryMirrors` maps an upstream registry host'
0
$ git show origin/main:packages/apps/kubernetes/README.md | grep -c 'Phase 2 follow-up'
0
$ git show HEAD:packages/apps/kubernetes/README.md       | grep -c 'Phase 2 follow-up'
1

So the shipped README now tells operators that per-tenant registry mirrors "remain a Phase 2 follow-up, file an issue if you depend on this and it is not yet landed", while talos.registryMirrors is shipped, is in the parameter table two screens below, and is in the RD schema. The bot flagged the inconsistency at README:229 and proposed removing talos.registryMirrors from the docs and the schema; that is backwards, the prose is the stale half. Restore main's paragraph.

I checked whether anything else was lost in that merge and nothing was: the set of files where the merge result differs from main is exactly the set the PR declares it touches, and in packages/apps/kubernetes-nodes/README.md only generated table rows changed.

B2: the kernelModules description is falsified by #3523, and it ships in the CRD

The description says, in ten files including both cozyrds ResourceDefinitions:

Changing this on a node group that already has running workers does not reach them: the TalosConfigTemplate its MachineDeployment references has a fixed name, so rewriting the template leaves spec.template untouched, CAPI starts no rollout, and a running Machine keeps the machine config it booted with. Replace the group's Machines to apply it. Unlike schematicID, which changes the boot image and therefore rolls the group by itself.

That is true on today's main and false with #3523 in the tree. I merged the two branches, ported the two hunks into the shared _talosconfigtemplate.tpl, and read the name off MachineDeployment.spec.template.spec.bootstrap.configRef:

tree node group TalosConfigTemplate
#3523 only one nvidia.com/* GPU t-md0-52cee2
#3523 + this PR, kernelModules unset same t-md0-cc5ae0
#3523 + this PR, kernelModules: [] same t-md0-52cee2
#3523 only no GPU t-md0-35293f
#3523 + this PR no GPU t-md0-35293f

With both in a release, the automatic NVIDIA set rotates the content-hashed template name of every existing GPU node group with no operator action, and CAPI rolls those workers. kernelModules then rolls the group by itself, exactly like schematicID. Non-GPU groups are untouched, and [] reproduces the pre-change hash byte for byte, which is the opt-out working as designed.

The release note carries the #3515 caveat and says it disappears when the naming fix ships. The field description does not: it states the fixed-name mechanism as a permanent property of the system, and it is the text an operator reads in the dashboard long after the release note is history. Either drop the mechanism and say the change may not reach existing workers until the TalosConfigTemplate naming fix is released, or qualify it in place.

Recommendations

The release note says setting kernelModules on an existing node group has no effect before #3523. It does not say the automatic set fires unasked. On this head a nvidia.com/* group's reconcile Job name moves from t-talos-reconcile-md0-752f85 to t-talos-reconcile-md0-80ebc9, so an upgrade creates a fresh Job for every existing GPU node group and that Job applies a changed TalosConfigTemplate under the fixed name. The script runs under sh -ec and the apply carries no tolerance, so if the immutability webhook rejects it the way #3515 describes, the Job exits non-zero, retries to backoffLimit: 30 and ends Failed. I could not exercise the webhook here, so treat the rejection as read from #3515 rather than measured. One sentence in the release note covers it.

invalid schematicID "" names a rule the value does not break. --set talos.schematicID= renders on the merge base (producing image//v1.13.6/openstack-amd64.raw.xz, which 404s at the factory) and now fails the render with "must not contain $, a backtick, a backslash, quotes or whitespace". Refusing it is right; the message should say the value must also be non-empty.

treats an empty per-node-group schematic as unset asserts only the boot disk image. Its two sibling cases assert both consumers, and the whole point of routing both through one helper is that they cannot disagree. One more assert on the installer image closes it.

^[a-z0-9_-]+$ rejects an uppercase module name. Nothing in the kernel forbids one; the convention is lowercase and the guard is otherwise correct, so this is a note rather than a request.

Sequencing with #3523

The two branches conflict in four files, so whoever merges second resolves by hand, and the interesting part is what a plausible resolution does. I took #3523's side wherever it had rewritten a region, which is the obvious call since it moved the machine config out of the Job template. packages/apps/kubernetes-nodes/templates/nodegroup.yaml auto-merged and kept this PR's "schematicID" line, while the Job's own $group dict came from #3523's side and lost both new keys. The two sites then hash different specs:

MachineDeployment configRef -> kubernetes-myk8s-md0-4522a1
Job TCT_NAME                -> kubernetes-myk8s-md0-ffdbe1

That is the cannot create a new MachineSet when templates do not exist deadlock, produced by a resolution nobody would call careless. The good news is that this PR's own tests catch it: render-parity.sh fails on MachineDeployment and on MachineConfig(talos-reconcile), and both kubernetes-nodes suites go red. Worth knowing before the rebase rather than during it.

Two smaller integration details for that rebase. #3523's kubernetes.talosConfigTemplateSpec takes root and group but no groupName, which both helpers here need for their fail messages. And nodegroup.yaml's $group will need kernelModules added, because after #3523 that dict feeds the template hash rather than only the KubevirtMachineTemplate.

What I verified

No worker roll for anything that resolves to no modules: rendering the parent chart at the merge base and at this head, for the default node group and for an amd.com/gpu group, the only difference is the six per-render random Talos secret lines. The nvidia.com/* group differs by the Job hash and the eight-line kernel: block, which is the intended change.

The tests are not decorative. I broke each thing a test claims to guard and every one went red: reordering the module list in one chart's helper alone fails MachineConfig(talos-reconcile) on the gpu and schematic cases and nowhere else; dropping the schematic guard turns the new heredoc test red; kindIs "slice" replaced by truthiness, the module-name regex removed, and the rebuilt dict replaced by the raw item each fail exactly one case; removing the kernel: block or gating it on true fails five and three; nindent 28 moved to 26 or 30 fails five, so the absolute indentation really is pinned in both directions; routing the installer image or the boot disk URL around the helper fails five and four cases of the schematic suite. The machine-config comparison is also not a no-op: run against main's two unmodified charts it passes.

The three-state contract holds at the schema layer too. A bare kernelModules: is refused by Helm with at '/nodeGroups/md0/kernelModules': got null, want array, and [] is accepted, which is what the undefaulted field buys.

Every consumer of the schematic goes through the helper. There is no remaining .Values.talos.schematicID in a rendered position in either chart; the four call sites are the two boot disk URLs and the two installer images.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two blockers, both in the delta since the last approval rather than in the earlier work.

First, the merge commit resolved packages/apps/kubernetes/README.md in favour of the branch and dropped a paragraph that belongs to main. The shipped README now says per-tenant registry mirrors "remain a Phase 2 follow-up", while talos.registryMirrors landed in #3575 and is documented in the parameter table of that same README. Measured both ways: the sentence about mapping an upstream registry host is present on main and absent on this head, and "Phase 2 follow-up" is the reverse. Nothing else was lost in that merge.

Second, the description of kernelModules becomes false the moment #3523 lands, and it ships in the CRD, so it is what an operator reads in the dashboard. It states that the fixed TalosConfigTemplate name means CAPI starts no rollout and that machines must be replaced, unlike schematicID. With both changes in a release, the automatic NVIDIA set turns the content hash of every existing GPU node group, with no operator action, and CAPI rolls those workers. Measured by assembling both branches in one tree: the same group renders t-md0-52cee2 with 3523 alone and t-md0-cc5ae0 with both, returning to 52cee2 when kernelModules is set to an empty list. On today's main the text is true, which is why this is worth fixing now rather than after.

The branches conflict textually in four files, so nothing merges silently, but the obvious resolution in favour of 3523 drops both new keys from the Job dict while nodegroup.yaml keeps them, producing two different hashes. The suites of this PR catch that. Details in the comment above.

@lexfrei

Copy link
Copy Markdown
Contributor

Checked both PRs for a re-review pass today. The branches haven't moved since Aug 10, so the Aug 14 reviews stand: the hash-input duplication in #3523 and the README/values claims here are still open. This branch has also picked up merge conflicts with main in 6 files since then. Happy to re-review as soon as a new revision lands — flagging in case the review notifications got lost.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/kubernetes Issues or PRs related to the tenant Kubernetes app kind/feature Categorizes issue or PR as related to a new feature size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants