feat(kubernetes): per-node-group kernel modules and Talos schematic - #3571
feat(kubernetes): per-node-group kernel modules and Talos schematic#3571mattia-eleuteri wants to merge 8 commits into
Conversation
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>
📝 WalkthroughWalkthroughThe 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. ChangesTalos worker configuration
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
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>
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
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
-
#3294 is open and adds a second surface for the same value:
nodeGroups.<name>.image.builtin.schematicIDandimage.factory.schematicID, both falling back to the cluster-widetalos.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 itsfactory.schematicIDredirects 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. -
hack/e2e-talos-image-cache.yamlpre-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>
|
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 shellReproduced first: rendering Guarded all three at render time with
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.
B2 — documentedYou are right that this is not just #3515. Follow-up 1 — #3294 already does this, more completelyChecking 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 That makes my flat 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 Follow-up 2 — e2e image cacheNoted, nothing changed. No e2e case sets a per-group override, so nothing misses the cache today. Worth saying that #3294 removes |
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
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.
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
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>
|
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 guardConfirmed the regression before changing anything: values carrying Now 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 B4 — closed by constructionReproduced: The emitted list is now rebuilt as B5 — droppedBoth 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
OwnershipTaking your call: the field stays here, and when #3294 rebases 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. |
|
The failing test is Reproduced against a pristine checkout of main at 879d0f6, with none of this branch: the guard's In the same run, everything else that matters passed: 70 helm suites with none failing, including all four suites this PR adds and 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 |
…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>
|
One more commit, 665c3f2, which changes something you already verified — flagging it rather than letting it slip past. The automatic NVIDIA set now carries On Talos that driver container is disabled (the driver comes from the system extension), so nothing applies the parameter and Consequences in the diff: the pinned auto-set patterns in both charts now include the parameter, the GPU group's content-hash fixture in 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 |
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
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.
|
The On a fresh single-node tenant cluster with an L40S and a 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, 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 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 |
… 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>
|
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 classReproduced: with Checked the premise rather than taking it: One addition beyond your fix, from testing it: Worth recording how I nearly reported the wrong result here: my first pass tested the backslash through Recommended, both taken
Both over-claims corrected. The nodes chart no longer says "byte-for-byte the same helper": the fail messages differ ( 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 incidentalBoth charts accept a multi-segment mirror path and a semicolon-bearing parameter, and still reject The Go
|
….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 -->
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
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>
|
Merged The installer image line now carries both: main's escape chain on all three coordinates, and this branch's One thing worth flagging because it touches a file from #3513 rather than one this PR owns. Two cases in Checks after the merge: 223 tests in Also took the two recommended items. The parent chart gained |
There was a problem hiding this comment.
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 winAdd 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 valueConsider guarding against a non-list
parametersvalue.
range .parameters | default listfails the render with a Go template error if a user setsparametersas 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. AkindIs "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 valueRegenerate the deep-copy files from controller-gen source instead of editing them.
hack/update-codegen.shis the repo generation entrypoint, and theseapi/apps/v1alpha1/kubernetes/zz_generated.deepcopy.goandapi/apps/v1alpha1/kubernetesnodes/zz_generated.deepcopy.gofiles 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
📒 Files selected for processing (25)
api/apps/v1alpha1/kubernetes/types.goapi/apps/v1alpha1/kubernetes/zz_generated.deepcopy.goapi/apps/v1alpha1/kubernetesnodes/types.goapi/apps/v1alpha1/kubernetesnodes/zz_generated.deepcopy.gohack/talos-reconcile-heredoc_test.batspackages/apps/kubernetes-nodes/README.mdpackages/apps/kubernetes-nodes/templates/_helpers.tplpackages/apps/kubernetes-nodes/templates/nodegroup.yamlpackages/apps/kubernetes-nodes/templates/talos-reconcile-job.yamlpackages/apps/kubernetes-nodes/tests/kernel_modules_test.yamlpackages/apps/kubernetes-nodes/tests/render-parity.shpackages/apps/kubernetes-nodes/tests/schematic_per_pool_test.yamlpackages/apps/kubernetes-nodes/values.schema.jsonpackages/apps/kubernetes-nodes/values.yamlpackages/apps/kubernetes/README.mdpackages/apps/kubernetes/templates/_helpers.tplpackages/apps/kubernetes/templates/cluster.yamlpackages/apps/kubernetes/templates/talos/talos-reconcile-job.yamlpackages/apps/kubernetes/tests/kernel_modules_test.yamlpackages/apps/kubernetes/tests/schematic_per_nodegroup_test.yamlpackages/apps/kubernetes/tests/talos_templates_test.yamlpackages/apps/kubernetes/values.schema.jsonpackages/apps/kubernetes/values.yamlpackages/system/kubernetes-nodes-rd/cozyrds/kubernetes-nodes.yamlpackages/system/kubernetes-rd/cozyrds/kubernetes.yaml
| // 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"` |
There was a problem hiding this comment.
🗄️ 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"
fiRepository: 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` | `[]` | |
There was a problem hiding this comment.
📐 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 asunsetorautomatic, 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` | `{}` | |
There was a problem hiding this comment.
🗄️ 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 || trueRepository: 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 || trueRepository: 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]}")
PYRepository: 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"]] |
There was a problem hiding this comment.
🎯 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
doneRepository: 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 -nRepository: 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.
|
NOT LGTM. Two things, both new since the last round: the merge of main dropped a paragraph main owns, and the Everything I checked from the previous rounds still holds. The helm suites are green (223 in B1: the merge of main reverted the air-gapped bullet in
|
| 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.
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
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.
|
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. |
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:
kernelModulesschematicIDtalos.schematicIDis 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-writtenTalosConfigTemplate, which is impractical because itsspecis 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:
packages/apps/kubernetesnodeGroups.<name>.kernelModulespackages/apps/kubernetes-nodeskernelModulesat the root, asgpusalready isDesign: 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-cozystacktranscribesvalues.schema.jsonby 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.nvidia.com/*GPU getsnvidia,nvidia_uvm,nvidia_drm,nvidia_modeset; any other group gets nokernelblock at all.[]— explicit opt-out: no modules even on a GPU node group.The order is not cosmetic: Talos loads the list in sequence,
nvidiahas to come first because the other three depend on it, andnvidia_uvmis 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, nodefaultinvalues.schema.json. A default of[]would collapse "absent" into "opted out" for every node group and make the automatic set unreachable, and a barekernelModules:(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 ofgpus, 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, whichhelm templateregenerates on every invocation), including thetalos-reconcileJob'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 intalos_templates_test.yamlis 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:
Both services need an NVIDIA card. On a node that has none they never come up, so the node reboots, forever.
talos.schematicIDis 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
Readyfor the whole 70 minutes andkubectl get nodeslooks healthy.kubectl top nodesshows up to 96% memory on small nodes just before the reboot, which reads as an OOM — it is page cache, andmin_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
schematicIDthat falls back totalos.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 theTalosConfigTemplate— 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
kernelModulesthis is deliberately not derived fromgpus. 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
KubevirtMachineTemplatekeeps 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.shcompared 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,serviceAccountNameand theRELEASEenv. 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 thegpuandschematiccases (the schematic case also carries annvidia.com/*GPU, so it inherits the automatic list), and the check passes onmain'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,
kernelModuleschanges nothing for existing node groups. That is #3515: theTalosConfigTemplateapply is rejected server-side while the HelmRelease staysReady, so akernelModulesadded 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.schematicIDis split across that line, and the split is worth being precise about. Its effect on the boot disk image goes through theKubevirtMachineTemplate, 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 theTalosConfigTemplateand 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
kernel_modules_test.yamlin both packages, 10 cases each: the NVIDIA set and its order for annvidia.com/*group, nokernelblock for a group with neither GPUs nor modules, an explicit list taken verbatim includingparameters, an explicit list replacing the NVIDIA default,[]opting out while leaving thegpu=onlabel 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+, becausemodules:one level out is still valid YAML, is still accepted by the apiserver, and silently loads nothing.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.shgains the machine-config comparison plus three cases (explicitkernelModules, a GPU pool opting out with[], and a per-poolschematicID).make generatein both packages, andcontroller-gen objectfor theapi/apps/v1alpha1submodule, 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 thenvidiacontainerd runtime default,install.imagederivation fromspec.talos.schematicID(already fixed by #3523), and themaxSurge/nodeStartupTimeoutdefaults (already raised in comments on #3523).Screenshots
No UI changes.
Downstream repositories
Both are issues rather than PRs, and deliberately so.
terraform-provider-cozystackis hand-written with no codegen fromvalues.schema.json, so both new fields need a schema entry, a model entry and an expand/flatten pair onKubernetesandKubernetesNodes. 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 materialiseskernel_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.websiteneeds 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 carrysiderolabs/nvidia-open-gpu-kernel-modules-production, because with the proprietarynonfree-kmod-nvidia-productionextension the module loads,/dev/nvidia0appears, andnvidia-smi -Lthen reportsNo devices foundwith 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
ApplicationDefinitionsemantics orpackages/core/platform/values.yaml.Release note
Summary by CodeRabbit