Artem Chernyshev 75e881fca9
feat: resolve patches/kubernetes manifests relative to the templates dir
Previous behavior: resolve all includes relative to where `omnictl`
runs.
Current behavior: resolve all includes relative to where targeted
template YAML file is stored.

The new behavior should be more straightforward.

Signed-off-by: Artem Chernyshev <artem.chernyshev@talos-systems.com>
2026-05-05 13:03:20 +03:00

210 lines
5.9 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"
"slices"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/hashicorp/go-multierror"
"github.com/siderolabs/gen/maps"
"github.com/siderolabs/gen/xslices"
"github.com/siderolabs/omni/client/pkg/omni/resources/omni"
)
// List implements a list of models, essentially it's a template.
type List []Model
// Validate the set of models as a complete template.
//
// Each model should be valid, but also the set of models should be complete.
//
//nolint:gocyclo,cyclop
func (l List) Validate(opts ValidateOptions) error {
var multiErr error
for _, model := range l {
multiErr = joinErrors(multiErr, model.Validate(opts))
}
// complete template should contain 1 cluster, 1 controlplane, 0-N workers
// and machines mentioned in the controlplane or worker models
var (
clusterCount int
controlplaneCount int
controlPlaneMachines MachineIDList
workerMachineSetNameToCount = make(map[string]int)
workerMachineIDToWorkerMachineSets = make(map[MachineID][]string)
)
lockedMachines := make(map[MachineID]struct{})
for _, model := range l {
switch m := model.(type) {
case *Cluster:
clusterCount++
case *ControlPlane:
controlplaneCount++
controlPlaneMachines = append(controlPlaneMachines, m.Machines...)
case *Workers:
workerMachineSetNameToCount[m.Name]++
for _, machineID := range m.Machines {
workerMachineIDToWorkerMachineSets[machineID] = append(workerMachineIDToWorkerMachineSets[machineID], m.Name)
}
case *Machine:
if m.Locked {
lockedMachines[m.Name] = struct{}{}
}
}
}
if clusterCount != 1 {
multiErr = multierror.Append(multiErr, fmt.Errorf("template should contain 1 cluster, got %d", clusterCount))
}
if controlplaneCount != 1 {
multiErr = multierror.Append(multiErr, fmt.Errorf("template should contain 1 controlplane, got %d", controlplaneCount))
}
for name, count := range workerMachineSetNameToCount {
if count > 1 {
multiErr = multierror.Append(multiErr, fmt.Errorf("duplicate workers with name %q", name))
}
}
for machineID, machineSets := range workerMachineIDToWorkerMachineSets {
if len(machineSets) > 1 {
multiErr = multierror.Append(multiErr, fmt.Errorf("machine %q is used in multiple workers: %q", machineID, machineSets))
}
}
cpMachinesSet := xslices.ToSet(controlPlaneMachines)
workerMachines := maps.Keys(workerMachineIDToWorkerMachineSets)
intersection := xslices.Filter(workerMachines, func(id MachineID) bool {
_, ok := cpMachinesSet[id]
return ok
})
if len(intersection) != 0 {
multiErr = multierror.Append(multiErr, fmt.Errorf("machines %v are used in both controlplane and workers", intersection))
}
allMachines := slices.Concat(controlPlaneMachines, workerMachines)
allMachinesSet := xslices.ToSet(allMachines)
for _, model := range l {
if machine, ok := model.(*Machine); ok {
if _, ok := allMachinesSet[machine.Name]; !ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("machine %q is not used in controlplane or workers", machine.Name))
}
}
}
for _, cpMachine := range controlPlaneMachines {
if _, ok := lockedMachines[cpMachine]; ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("machine %q is locked and used in controlplane", cpMachine))
}
}
return multiErr
}
// Translate a set of models (template) to a set of Omni resources.
//
// Translate assumes that the template is valid.
func (l List) Translate(fc FileContext) ([]resource.Resource, error) {
context := TranslateContext{
FileContext: fc,
LockedMachines: map[MachineID]struct{}{},
MachineDescriptors: map[MachineID]Descriptors{},
MachineSetLevelKernelArgs: map[MachineID]KernelArgs{},
}
for _, model := range l {
switch m := model.(type) {
case *Cluster:
context.ClusterName = m.Name
context.ClusterLevelKernelArgs = m.KernelArgs
case *ControlPlane:
for _, machineID := range m.Machines {
context.MachineSetLevelKernelArgs[machineID] = m.KernelArgs
}
case *Workers:
for _, machineID := range m.Machines {
context.MachineSetLevelKernelArgs[machineID] = m.KernelArgs
}
case *Machine:
context.MachineDescriptors[m.Name] = m.Descriptors
if m.Locked {
context.LockedMachines[m.Name] = struct{}{}
}
}
}
var (
multiErr error
resourcesList []resource.Resource
)
for _, model := range l {
resources, err := model.Translate(context)
if err != nil {
multiErr = multierror.Append(multiErr, err)
continue
}
resourcesList = append(resourcesList, resources...)
}
// perform additional validation:
// - all resources except for cluster itself should have a cluster label
// - all resources should have a unique ID
resourceIDs := map[resource.Type]map[resource.ID]struct{}{}
for _, r := range resourcesList {
if _, ok := resourceIDs[r.Metadata().Type()]; !ok {
resourceIDs[r.Metadata().Type()] = map[resource.ID]struct{}{}
}
if _, ok := resourceIDs[r.Metadata().Type()][r.Metadata().ID()]; ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("resource ID duplicate %s", resource.String(r)))
}
resourceIDs[r.Metadata().Type()][r.Metadata().ID()] = struct{}{}
switch r.Metadata().Type() {
case omni.ClusterType, omni.KernelArgsType:
continue
default:
}
if l, _ := r.Metadata().Labels().Get(omni.LabelCluster); l != context.ClusterName {
multiErr = multierror.Append(multiErr, fmt.Errorf("resource %q is missing cluster label", r.Metadata().ID()))
}
}
return resourcesList, multiErr
}
// ClusterName returns the name of the cluster in the template.
func (l List) ClusterName() (string, error) {
for _, model := range l {
if cluster, ok := model.(*Cluster); ok {
return cluster.Name, nil
}
}
return "", fmt.Errorf("cluster model not found")
}