Recast Profiles as a Profile field and support recursion

Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
Jonathon Anderson
2024-12-17 01:34:19 -07:00
parent deaf9c99d7
commit 83af6021c0
11 changed files with 356 additions and 7 deletions

View File

@@ -28,7 +28,6 @@ type Node struct {
// exported values
Discoverable wwtype.WWbool `yaml:"discoverable,omitempty" lopt:"discoverable" sopt:"e" comment:"Make discoverable in given network (true/false)"`
AssetKey string `yaml:"asset key,omitempty" lopt:"asset" comment:"Set the node's Asset tag (key)"`
Profiles []string `yaml:"profiles,omitempty" lopt:"profile" sopt:"P" comment:"Set the node's profile members (comma separated)"`
Profile `yaml:"-,inline"` // include all values set in the profile, but inline them in yaml output if these are part of Node
}
@@ -38,6 +37,7 @@ Holds the data which can be set for profiles and nodes.
type Profile struct {
id string
// exported values
Profiles []string `yaml:"profiles,omitempty" lopt:"profile" sopt:"P" comment:"Set the node's profile members (comma separated)"`
Comment string `yaml:"comment,omitempty" lopt:"comment" comment:"Set arbitrary string comment"`
ClusterName string `yaml:"cluster name,omitempty" lopt:"cluster" sopt:"c" comment:"Set cluster group"`
ContainerName string `yaml:"container name,omitempty" lopt:"container" sopt:"C" comment:"Set container name"`

View File

@@ -158,6 +158,7 @@ func Test_listFields(t *testing.T) {
},
},
fields: []string{
"Profiles",
"Comment",
"ClusterName",
"ContainerName",

View File

@@ -8,15 +8,26 @@ import (
"dario.cat/mergo"
"github.com/warewulf/warewulf/internal/pkg/util"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
)
func copyProfile(this Profile) (Profile, error) {
// 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(this); err != nil {
if err := enc.Encode(original); err != nil {
return profile, err
} else {
if err := dec.Decode(&profile); err != nil {
@@ -27,6 +38,75 @@ func copyProfile(this Profile) (Profile, error) {
}
}
// 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 {
@@ -37,7 +117,7 @@ func (config *NodesYaml) MergeNode(id string) (node Node, fields fieldMap, err e
fields = make(fieldMap)
for _, profileID := range cleanList(originalNode.Profiles) {
for _, profileID := range config.getNodeProfiles(id) {
if profile, err := config.GetProfile(profileID); err != nil {
wwlog.Warn("profile not found: %s", profileID)
continue
@@ -79,6 +159,14 @@ func (config *NodesYaml) MergeNode(id string) (node Node, fields fieldMap, err e
}
}
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()

View File

@@ -8,6 +8,188 @@ import (
"github.com/warewulf/warewulf/internal/pkg/testenv"
)
func Test_getNodeProfiles(t *testing.T) {
var tests = map[string]struct {
nodesConf string
node string
profiles []string
}{
"no profiles": {
nodesConf: `
nodes:
n1: {}`,
node: "n1",
profiles: nil,
},
"one profile": {
nodesConf: `
nodes:
n1:
profiles:
- p1`,
node: "n1",
profiles: []string{"p1"},
},
"two profiles": {
nodesConf: `
nodes:
n1:
profiles:
- p1
- p2`,
node: "n1",
profiles: []string{"p1", "p2"},
},
"negated profiles": {
nodesConf: `
nodes:
n1:
profiles:
- p1
- p2
nodeprofiles:
p2:
profiles:
- "~p1"`,
node: "n1",
profiles: []string{"p2"},
},
"negated missing profile": {
nodesConf: `
nodes:
n1:
profiles:
- p1
- p2
nodeprofiles:
p2:
profiles:
- "~p3"`,
node: "n1",
profiles: []string{"p1", "p2"},
},
"single nested profile": {
nodesConf: `
nodes:
n1:
profiles:
- p1
nodeprofiles:
p1:
profiles:
- p2`,
node: "n1",
profiles: []string{"p1", "p2"},
},
"double nested profile": {
nodesConf: `
nodes:
n1:
profiles:
- p1
nodeprofiles:
p1:
profiles:
- p2
p2:
profiles:
- p3`,
node: "n1",
profiles: []string{"p1", "p2", "p3"},
},
"negated nested profile": {
nodesConf: `
nodes:
n1:
profiles:
- p1
nodeprofiles:
p1:
profiles:
- p2
p2:
profiles:
- "~p2"
- p3`,
node: "n1",
profiles: []string{"p1", "p3"},
},
"cicular nested profile": {
nodesConf: `
nodes:
n1:
profiles:
- p1
nodeprofiles:
p1:
profiles:
- p2
p2:
profiles:
- p3
p3:
profiles:
- p1`,
node: "n1",
profiles: []string{"p1", "p2", "p3"},
},
"cicular nested profile negation": {
nodesConf: `
nodes:
n1:
profiles:
- p1
nodeprofiles:
p1:
profiles:
- p2
p2:
profiles:
- "~p1"
- p3
p3:
profiles:
- p1`,
node: "n1",
profiles: []string{"p2", "p3", "p1"},
},
"repeated nested profile": {
nodesConf: `
nodes:
n1:
profiles:
- pa1
- pb1
nodeprofiles:
pa1:
profiles:
- pa2
pb1:
profiles:
- "~pa2"
- pb2
pb2:
profiles:
- pa2`,
node: "n1",
profiles: []string{"pa1", "pb1", "pb2", "pa2"},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
env := testenv.New(t)
defer env.RemoveAll(t)
env.WriteFile(t, "/etc/warewulf/nodes.conf", tt.nodesConf)
registry, regErr := New()
assert.NoError(t, regErr)
assert.Equal(t, tt.profiles, registry.getNodeProfiles(tt.node))
})
}
}
func Test_MergeNode(t *testing.T) {
var tests = map[string]struct {
nodesConf string
@@ -389,6 +571,49 @@ nodeprofiles:
source: "p1,p2,n1",
value: "po1,po2,po3,po4,no1,no2",
},
"node profiles": {
nodesConf: `
nodes:
n1:
profiles:
- p1
- p2`,
node: "n1",
field: "Profiles",
source: "",
value: "p1,p2",
},
"nested profiles": {
nodesConf: `
nodes:
n1:
profiles:
- p1
nodeprofiles:
p1:
profiles:
- p2`,
node: "n1",
field: "Profiles",
source: "",
value: "p1",
},
"negated profiles": {
nodesConf: `
nodes:
n1:
profiles:
- p1
- p2
nodeprofiles:
p2:
profiles:
- "~p1"`,
node: "n1",
field: "Profiles",
source: "",
value: "p1,p2",
},
"node netdev tag": {
nodesConf: `
nodes: