175 lines
5.8 KiB
Go
175 lines
5.8 KiB
Go
package node
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/gob"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"dario.cat/mergo"
|
|
|
|
"github.com/warewulf/warewulf/internal/pkg/util"
|
|
"github.com/warewulf/warewulf/internal/pkg/wwlog"
|
|
)
|
|
|
|
// copyProfile creates a deep copy of the given Profile object.
|
|
// It uses encoding/gob to serialize and deserialize the Profile, ensuring
|
|
// that all nested fields are copied.
|
|
//
|
|
// Parameters:
|
|
// - original: The Profile object to be copied.
|
|
//
|
|
// Returns:
|
|
// - A new Profile object that is a deep copy of the input.
|
|
// - An error if serialization or deserialization fails.
|
|
func copyProfile(original Profile) (Profile, error) {
|
|
var buf bytes.Buffer
|
|
enc := gob.NewEncoder(&buf)
|
|
dec := gob.NewDecoder(&buf)
|
|
profile := Profile{}
|
|
if err := enc.Encode(original); err != nil {
|
|
return profile, err
|
|
} else {
|
|
if err := dec.Decode(&profile); err != nil {
|
|
return profile, err
|
|
} else {
|
|
return profile, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// getNodeProfiles retrieves a list of profile IDs associated with a specific node ID.
|
|
// It retrives nested profiles and ensures the list is cleaned of duplicates
|
|
// and negations (denoted with a '~' prefix).
|
|
//
|
|
// Parameters:
|
|
// - id: The identifier of the node whose profiles are to be retrieved.
|
|
//
|
|
// Returns:
|
|
// - A slice of profile IDs associated with the given node ID.
|
|
func (config *NodesYaml) getNodeProfiles(id string) (profiles []string) {
|
|
if node, ok := config.Nodes[id]; ok {
|
|
for _, profileID := range node.Profiles {
|
|
profiles = cleanList(append(profiles, profileID))
|
|
if !strings.HasPrefix(profileID, "~") {
|
|
profiles = config.appendProfileProfiles(profiles, profileID)
|
|
}
|
|
}
|
|
}
|
|
return cleanList(profiles)
|
|
}
|
|
|
|
// appendProfileProfiles recursively appends profile IDs associated with a given profile ID
|
|
// to the provided list of profile IDs. It recursively processes nested profiles and ensures
|
|
// the list is cleaned of duplicates and negations (denoted with a '~' prefix).
|
|
//
|
|
// Profiles are only added if they do not already exist in the list.
|
|
//
|
|
// Parameters:
|
|
// - profiles: A slice of strings representing the current list of profiles by ID.
|
|
// - id: The identifier of the profile whose associated profiles are to be appended.
|
|
//
|
|
// Returns:
|
|
// - A slice of strings containing the updated list of profile IDs.
|
|
func (config *NodesYaml) appendProfileProfiles(profiles []string, id string) []string {
|
|
if profile, ok := config.NodeProfiles[id]; ok {
|
|
for _, subID := range profile.Profiles {
|
|
if !util.InSlice(profiles, subID) {
|
|
profiles = cleanList(append(profiles, subID))
|
|
if !strings.HasPrefix(subID, "~") {
|
|
profiles = config.appendProfileProfiles(profiles, subID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return profiles
|
|
}
|
|
|
|
// MergeNode merges the configuration of a node identified by `id` with all the profiles
|
|
// associated with it, producing a fully composed `Node` and a `fieldMap` detailing the
|
|
// sources of various configuration fields.
|
|
//
|
|
// It works by:
|
|
// - Retrieving the base node configuration using `GetNodeOnly`.
|
|
// - Gathering all profile IDs associated with the node via `getNodeProfiles`.
|
|
// - For each profile:
|
|
// - Merging fields from a deep copy of each profile into the node,
|
|
// recording the origin of each configuration field (i.e., which profile provided it)
|
|
// in a `fieldMap` so that traceability is maintained.
|
|
// - Finally, merging the original node configuration back into the processed node, ensuring
|
|
// that any fields not set by the profiles are preserved, and updating the `fieldMap`
|
|
// accordingly.
|
|
//
|
|
// Parameters:
|
|
// - id: The identifier of the node to be merged with its profiles.
|
|
//
|
|
// Returns:
|
|
// - node: The resulting merged `Node` configuration.
|
|
// - fields: A `fieldMap` detailing the source(s) of each configuration field.
|
|
// - err: An error if any node or profile retrieval or merging operations fail.
|
|
func (config *NodesYaml) MergeNode(id string) (node Node, fields fieldMap, err error) {
|
|
node, err = config.GetNodeOnly(id)
|
|
if err != nil {
|
|
return node, fields, err
|
|
}
|
|
originalNode := node
|
|
node = EmptyNode()
|
|
|
|
fields = make(fieldMap)
|
|
|
|
for _, profileID := range config.getNodeProfiles(id) {
|
|
if profile, err := config.GetProfile(profileID); err != nil {
|
|
wwlog.Warn("profile not found: %s", profileID)
|
|
continue
|
|
} else if profile, err := copyProfile(profile); err != nil {
|
|
wwlog.Warn("error processing profile %s: %v", profileID, err)
|
|
continue
|
|
} else {
|
|
if err = mergo.Merge(&node.Profile, profile, mergo.WithAppendSlice, mergo.WithOverride); err != nil {
|
|
return node, fields, err
|
|
}
|
|
for _, fieldName := range listFields(profile) {
|
|
if value, err := getNestedFieldValue(profile, fieldName); err == nil && valueStr(value) != "" {
|
|
source := profileID
|
|
prevSource := fields.Source(fieldName)
|
|
if value.Kind() == reflect.Slice && prevSource != "" {
|
|
source = strings.Join([]string{prevSource, source}, ",")
|
|
}
|
|
if value, err := getNestedFieldString(node, fieldName); err == nil {
|
|
fields.Set(fieldName, source, value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if err = mergo.Merge(&node, originalNode, mergo.WithAppendSlice, mergo.WithOverride); err != nil {
|
|
return node, fields, err
|
|
}
|
|
for _, fieldName := range listFields(originalNode) {
|
|
if value, err := getNestedFieldValue(originalNode, fieldName); err == nil && valueStr(value) != "" {
|
|
source := ""
|
|
prevSource := fields.Source(fieldName)
|
|
if value.Kind() == reflect.Slice && prevSource != "" {
|
|
source = strings.Join([]string{prevSource, id}, ",")
|
|
}
|
|
if value, err := getNestedFieldString(node, fieldName); err == nil {
|
|
fields.Set(fieldName, source, value)
|
|
}
|
|
}
|
|
}
|
|
|
|
node.Profiles = originalNode.Profiles
|
|
if len(node.Profiles) > 0 {
|
|
fields.Set("Profiles", "", strings.Join(originalNode.Profiles, ","))
|
|
fields["Profiles"].Source = ""
|
|
} else {
|
|
delete(fields, "Profiles")
|
|
}
|
|
|
|
node.id = id
|
|
node.valid = true
|
|
node.updatePrimaryNetDev()
|
|
return node, fields, nil
|
|
}
|