Utku Ozdemir 01bf663854
feat: support kernel args management in cluster templates
Implement kernel args support in cluster templates.

Managing kernel args via templates is opt-in: only and only if the `kernelArgs` YAML key is defined on a `Cluster`, `ControlPlane`, `Worker` or `Machine`, the matching `KernelArgs` resource will be created/updated.

Lower levels override higher levels (Cluster -> MachineSet -> Machine).

Unlike other cluster template managed resources, they will never be destroyed, i.e, when they are removed from a template (removed completely, as in, `kernelArgs` key doesn't exist) or when `omnictl cluster template delete` is run. They instead will get updated to have the annotation `omni.sidero.dev/managed-by-cluster-templates` removed from them.

Add the new flag `--include-kernel-args` to the `omnictl cluster template export` command to optionally include them in the exported template. Note: when this flag is set, `kernelArgs` key is always included at per-machine level, not pulled up even if they are the same for all machines in a machine set or a cluster.

Update the frontend, specifically the kernel args update screen to warn the user if kernel args for that machine is managed by templates, similar to what we do for clusters.

Signed-off-by: Utku Ozdemir <utku.ozdemir@siderolabs.com>
2026-01-07 12:48:53 +01:00

180 lines
4.5 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package models
import (
"fmt"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/google/uuid"
"github.com/hashicorp/go-multierror"
"github.com/siderolabs/gen/pair"
"github.com/siderolabs/omni/client/pkg/constants"
"github.com/siderolabs/omni/client/pkg/omni/resources/omni"
)
// MachineIDList is a list of Machine UUIDs.
type MachineIDList []MachineID
// MachineID is a Machine UUID.
type MachineID string
// KindMachine is a Machine model kind.
const KindMachine = "Machine"
// Machine provides customization for a specific machine.
type Machine struct { //nolint:govet
Meta `yaml:",inline"`
SystemExtensions `yaml:",inline"`
// Machine name (ID).
Name MachineID `yaml:"name"`
// Descriptors are the user descriptors to apply to the cluster.
Descriptors Descriptors `yaml:",inline"`
// Locked locks the machine, so no config updates, upgrades and downgrades will be performed on the machine.
Locked bool `yaml:"locked,omitempty"`
// Install specification.
Install MachineInstall `yaml:"install,omitempty"`
// ClusterMachine patches.
Patches PatchList `yaml:"patches,omitempty"`
// KernelArgs are the additional kernel arguments.
KernelArgs KernelArgs `yaml:",inline"`
}
// MachineInstall provides machine install configuration.
type MachineInstall struct {
// Disk device name.
Disk string `yaml:"disk"`
}
// Validate the list of machines.
func (l MachineIDList) Validate() error {
var multiErr error
for _, id := range l {
multiErr = joinErrors(multiErr, id.Validate())
}
return multiErr
}
// Validate the model.
func (id MachineID) Validate() error {
if _, err := uuid.Parse(string(id)); err != nil {
return fmt.Errorf("invalid machine ID %q: %w", id, err)
}
return nil
}
// Validate the model.
func (install *MachineInstall) Validate() error {
return nil
}
// Validate the model.
func (machine *Machine) Validate() error {
var multiErr error
if machine.Name == "" {
multiErr = multierror.Append(multiErr, fmt.Errorf("name is required for machine"))
}
if err := machine.Descriptors.Validate(); err != nil {
multiErr = multierror.Append(multiErr, err)
}
multiErr = joinErrors(multiErr, machine.Name.Validate(), machine.Install.Validate(), machine.Patches.Validate())
if multiErr != nil {
return fmt.Errorf("machine %q is invalid: %w", machine.Name, multiErr)
}
return nil
}
// Translate the model.
func (machine *Machine) Translate(ctx TranslateContext) ([]resource.Resource, error) {
var resourceList []resource.Resource
if machine.Install.Disk != "" {
patch := Patch{
Name: "install-disk",
Inline: map[string]any{
"machine": map[string]any{
"install": map[string]any{
"disk": machine.Install.Disk,
},
},
},
}
patchResource, err := patch.Translate(
fmt.Sprintf("cm-%s", machine.Name),
constants.PatchWeightInstallDisk,
pair.MakePair(omni.LabelCluster, ctx.ClusterName),
pair.MakePair(omni.LabelClusterMachine, string(machine.Name)),
pair.MakePair(omni.LabelSystemPatch, ""),
)
if err != nil {
return nil, err
}
resourceList = append(resourceList, patchResource)
}
patches, err := machine.Patches.Translate(
fmt.Sprintf("cm-%s", machine.Name),
constants.PatchBaseWeightClusterMachine,
pair.MakePair(omni.LabelCluster, ctx.ClusterName),
pair.MakePair(omni.LabelClusterMachine, string(machine.Name)),
)
if err != nil {
return nil, err
}
resourceList = append(resourceList, patches...)
schematicConfigurations := machine.translate(
ctx,
string(machine.Name),
pair.MakePair(omni.LabelClusterMachine, string(machine.Name)),
)
res, defined := machine.translateKernelArgs(ctx)
if defined {
resourceList = append(resourceList, res)
}
return append(resourceList, schematicConfigurations...), nil
}
func (machine *Machine) translateKernelArgs(ctx TranslateContext) (res resource.Resource, defined bool) {
if args, ok := machine.KernelArgs.Get(); ok {
return buildKernelArgsResource(machine.Name, args), true
}
msKernelArgs := ctx.MachineSetLevelKernelArgs[machine.Name]
if args, ok := msKernelArgs.Get(); ok {
return buildKernelArgsResource(machine.Name, args), true
}
if args, ok := ctx.ClusterLevelKernelArgs.Get(); ok {
return buildKernelArgsResource(machine.Name, args), true
}
return nil, false
}
func init() {
register[Machine](KindMachine)
}