Skip to content

Kubernetes/KubernetesNodes: model the new kernelModules and schematicID fields (upstream cozystack#3563) #20

Open

Description

@mattia-eleuteri

Context

cozystack/cozystack PR (linked below) adds a values surface for Talos worker kernel modules, to both apps that render a worker pool:

App Field Type
Kubernetes nodeGroups.<name>.kernelModules []KernelModule
KubernetesNodes kernelModules (root level, like gpus) []KernelModule

KernelModule is a new struct with two attributes:

Attribute Type Required
name string yes
parameters []string no

It emits Talos machine.kernel.modules into the worker machine config. A Talos system extension installs a kernel module but does not load it, so a GPU node group whose schematic carries the NVIDIA extension previously came up with no working driver and no way to fix it through values (upstream cozystack/cozystack#3563).

Why this repository is affected

Per the upstream trigger map (docs/agents/contributing.md, "Downstream Repositories"), this provider is hand-written: every Kind carries a hand-maintained Terraform schema with its own expand/flatten pair, with no codegen from values.schema.json. The entry that applies is "Add, remove or rename a field in an app's values.schema.json → the schema, the model, and the expand/flatten pair." Both Kubernetes and KubernetesNodes are affected, and KubernetesNodes takes the field at the root of its schema rather than nested under a node group.

What is needed

Add kernel_modules (or the naming this provider uses for list-of-object attributes) as a nested block or list of objects, with name required and parameters an optional list of strings, to the Kubernetes resource's node-group schema and to the KubernetesNodes resource schema, plus the expand/flatten pair for each.

One point that needs care: the field is deliberately undefaulted

This is the part I would get wrong without knowing this provider, and it is the reason I am filing rather than sending a PR. The upstream field distinguishes three states, and the distinction is load-bearing:

  • absent — the chart decides. A node group holding at least one nvidia.com/* GPU gets nvidia, nvidia_uvm, nvidia_drm, nvidia_modeset; any other group gets no kernel block at all.
  • non-empty list — taken verbatim, replacing what the chart would have picked.
  • empty list ([]) — explicit opt-out: emit no modules even on a GPU node group.

So there is no default to restate, and there must not be one: upstream deliberately carries no entry for this key in values.yaml and no default in values.schema.json, because a default of [] would collapse "absent" into "opted out" for every node group and make the automatic NVIDIA set unreachable. The upstream trigger map warns that this provider sends its own defaults rather than letting the API apply them, so a provider that materialises kernel_modules = [] when the user did not set it would silently disable the automatic NVIDIA modules for every GPU node group managed through Terraform — the exact production failure that motivated the upstream change, reintroduced through the provider.

Concretely: the attribute wants to stay Optional without Computed defaulting to an empty collection, and the flatten side needs to keep "unset" and "empty list" distinct rather than normalising a missing value to [].

Not done here

Reporting rather than sending a PR: I do not know this provider's conventions for list-of-object attributes, and the unset-versus-empty distinction above is precisely where a well-meaning Computed default would break users silently. Happy to follow up with a PR if a maintainer confirms the intended shape.

Filed as the downstream follow-up required by the upstream PR checklist.

Activity

  1. mattia-eleuteri commented on Aug 6, 2026

    @mattia-eleuteri
    Author
  2. mattia-eleuteri commented on Aug 6, 2026

    @mattia-eleuteri
    Author

    The upstream PR grew a second node-group-scoped field, so this issue covers two rather than one.

    schematicID — a plain optional string on both apps (nodeGroups.<name>.schematicID on Kubernetes, schematicID at the root on KubernetesNodes), overriding the cluster-wide talos.schematicID for a single node group. It applies to the node group's Talos boot disk image and its installer image.

    Simpler to model than kernelModules — no nested object, no list — but it carries the same defaulting hazard, and here it is sharper. An empty string means "inherit the cluster-wide value". A provider that materialises schematic_id = "" when the user did not set it is harmless, since the chart treats empty as unset; but a provider that helpfully substitutes the cluster-wide talos.schematicID as a concrete per-group default would pin every node group to the value read at apply time, and a later change to talos.schematicID would then silently stop propagating to the groups Terraform manages.

    Why the field exists, since it explains why getting the default wrong is expensive: a Talos schematic is a fixed set of system extensions, ext-nvidia-persistenced and ext-nvidia-cdi-gen require an NVIDIA card, and Talos refuses to finish booting when an extension service cannot start. A non-GPU node booting an NVIDIA schematic therefore reboots roughly every 70 minutes, indefinitely, while still reporting Ready. This field is what lets the NVIDIA schematic be scoped to the node group that has the cards.

    Also worth knowing for whoever picks this up: changing schematicID on a node group replaces its boot image and rolls that group's workers. That is inherent to the change, not a provider concern, but it means the attribute should not be marked as anything that would make Terraform reorder or batch it silently.

  3. changed the title [-]Kubernetes/KubernetesNodes: model the new kernelModules field (upstream cozystack#3563)[/-] [+]Kubernetes/KubernetesNodes: model the new kernelModules and schematicID fields (upstream cozystack#3563)[/+] on Aug 6, 2026
  4. mattia-eleuteri commented on Aug 7, 2026

    @mattia-eleuteri
    Author

    One more constraint for whoever models kernelModules, raised by Aleksei Sviridkin (@lexfrei) on the upstream PR and worth having here because it is the sharpest part of the three-state problem.

    The generated Go type is KernelModules []KernelModule with omitempty. An empty slice serialises away under omitempty, so that type cannot represent the [] opt-out — it collapses into "unset", which is the state that means "let the chart pick the NVIDIA modules". A typed Go client that builds the object would therefore silently re-enable the automatic set for an operator who explicitly opted out.

    The in-cluster path is unaffected: the Kubernetes CR spec travels as raw JSON and never passes through those structs, so applying YAML behaves exactly as documented. The exposure is specific to code that constructs the object through the generated types, which is what a Terraform provider does.

    So the flatten side needs to distinguish three states deliberately rather than relying on the Go zero values:

    • attribute null → omit the key entirely from the values sent
    • attribute set to an empty list → send kernelModules: []
    • attribute set to a non-empty list → send it as-is

    If the provider's serialisation goes through the upstream api/apps/v1alpha1 types rather than building the values map directly, the empty-list case will need something other than omitempty on that field, or an explicit marshalling path that keeps it.

    Upstream reference for the three states and why they exist: cozystack/cozystack#3571

  5. lexfrei commented on Aug 7, 2026

    @lexfrei
    Collaborator

    Confirming the shape so a PR has something fixed to build against.

    kernel_modules is a ListNestedAttribute on the node group, with name a required string and parameters an optional list of strings; schematic_id is a plain optional string alongside it. Both are Optional and nothing else: no Computed, no default, no plan modifier. A Computed attribute has no null state for an operator to write, and null is the state that means "let the chart pick".

    The omitempty hazard does not reach this provider. It builds the spec as a plain map[string]any and hands that to the dynamic client, so the generated Go types are never on the wire path. The map is where the three states live: a null attribute leaves the key out, an empty list writes kernelModules: [], a populated list goes through verbatim. The read side has to match, a stored [] must come back as an empty list rather than null, or every plan reports drift on a node group that deliberately opted out. The presence-preserving helpers for both directions are already in the 1.6 branch, ahead of the fields themselves.

    One asymmetry to know before writing it: on Kubernetes neither field carries a schema default, so both stay plain Optional, but on KubernetesNodes the root schematicID defaults to "", so the server materialises the key whether or not the provider sends it, and that one needs Optional plus Computed with an empty-string default. The KubernetesNodes resource itself is landing separately in #24, so by the time the fields exist that half is two attributes on an existing kind. schematic_id will not force replacement; the rolling behaviour belongs in the attribute description, not in a lifecycle rule.

    None of this can land until 3571 merges and a tagged api/apps/v1alpha1 release carries the fields; v1.6.1 is the newest and NodeGroup has neither. A PR against this shape is welcome once that tag exists, and I'm happy to review it.

  6. mattia-eleuteri commented on Sep 3, 2026

    @mattia-eleuteri
    Author

    Scope update: the upstream PR has been rebased and reduced, and this issue narrows with it.

    cozystack/cozystack#3571 originally added kernelModules and schematicID to both charts that rendered a worker pool. Upstream has since merged refactor(kubernetes): remove worker node pools from the chart, so nodeGroups no longer exists on the Kubernetes app and the parent-chart half of that PR was dropped.

    Both fields therefore land on KubernetesNodes only, at the root of its spec, alongside gpus and roles. There is no Kubernetes.nodeGroups[*].kernel_modules and no Kubernetes.nodeGroups[*].schematic_id to add — one resource instead of two.

    Everything else in this issue stands unchanged, including the part that matters most: kernel_modules carries no default on purpose, and a provider that materialises kernel_modules = [] when the user did not set it would disable the automatic NVIDIA module set for every GPU pool managed through Terraform.

  7. mattia-eleuteri commented on Sep 29, 2026

    @mattia-eleuteri
    Author

    Scope update from cozystack/cozystack#3571: the schematicID field has been dropped from that PR, since main now scopes the schematic per pool through osImage (cozystack/cozystack#4171), so this issue covers kernelModules on KubernetesNodes only. The trap described above still applies unchanged: the field has no default, and sending kernel_modules = [] when the user did not set it disables the automatic NVIDIA modules on every GPU pool.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions