From ec6884a87822989a1c8b4d4eb16e962fcde81110 Mon Sep 17 00:00:00 2001 From: Jonathon Anderson Date: Sat, 21 Mar 2026 21:15:12 -0600 Subject: [PATCH] Refactor gRPC-based node functions Signed-off-by: Jonathon Anderson --- internal/app/wwctl/node/add/main.go | 53 +++++++-- internal/app/wwctl/node/delete/main.go | 60 +++++++--- internal/app/wwctl/node/imprt/main.go | 19 ++- internal/app/wwctl/node/list/main.go | 125 ++++++++++++++----- internal/app/wwctl/node/set/main.go | 132 +++++++++++++++------ internal/app/wwctl/node/set/main_test.go | 16 +++ internal/app/wwctl/node/set/root.go | 2 +- internal/app/wwctl/node/status/main.go | 43 +++++-- internal/pkg/api/node/add.go | 76 ------------ internal/pkg/api/node/delete.go | 104 ---------------- internal/pkg/api/node/edit.go | 41 ------- internal/pkg/api/node/list.go | 126 -------------------- internal/pkg/api/node/set.go | 145 ----------------------- internal/pkg/api/node/status.go | 94 --------------- internal/pkg/node/update.go | 88 ++++++++++++++ 15 files changed, 430 insertions(+), 694 deletions(-) delete mode 100644 internal/pkg/api/node/add.go delete mode 100644 internal/pkg/api/node/delete.go delete mode 100644 internal/pkg/api/node/edit.go delete mode 100644 internal/pkg/api/node/list.go delete mode 100644 internal/pkg/api/node/set.go delete mode 100644 internal/pkg/api/node/status.go create mode 100644 internal/pkg/node/update.go diff --git a/internal/app/wwctl/node/add/main.go b/internal/app/wwctl/node/add/main.go index 25811322..3e299ce7 100644 --- a/internal/app/wwctl/node/add/main.go +++ b/internal/app/wwctl/node/add/main.go @@ -2,14 +2,14 @@ package add import ( "fmt" + "net" "strings" - "gopkg.in/yaml.v3" - "github.com/spf13/cobra" - apinode "github.com/warewulf/warewulf/internal/pkg/api/node" - "github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1" + "github.com/warewulf/warewulf/internal/pkg/hostlist" "github.com/warewulf/warewulf/internal/pkg/node" + "github.com/warewulf/warewulf/internal/pkg/util" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" "github.com/warewulf/warewulf/internal/pkg/wwlog" ) @@ -60,16 +60,45 @@ func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) error { } } } - buffer, err := yaml.Marshal(vars.nodeConf) + nodeDB, err := node.New() if err != nil { - wwlog.Error("Can't marshall nodeInfo", err) - return err + return fmt.Errorf("failed to open node database: %w", err) } - set := wwapiv1.NodeAddParameter{ - NodeConfYaml: string(buffer[:]), - NodeNames: args, - Force: true, + nodeArgs := hostlist.Expand(args) + changed := cmd.Flags().Changed + var ipv4, ipmiaddr net.IP + for _, a := range nodeArgs { + n, err := nodeDB.AddNode(a) + if err != nil { + return fmt.Errorf("failed to add node: %w", err) + } + n.UpdateFrom(&vars.nodeConf, changed) + if !changed("profile") && len(vars.nodeConf.Profiles) > 0 { + n.Profiles = vars.nodeConf.Profiles + } + wwlog.Info("Added node: %s", a) + for _, dev := range n.NetDevs { + if !ipv4.IsUnspecified() && ipv4 != nil { + ipv4 = util.IncrementIPv4(ipv4, 1) + wwlog.Verbose("Incremented IP addr to %s", ipv4) + dev.Ipaddr = ipv4 + } else if !dev.Ipaddr.IsUnspecified() { + ipv4 = dev.Ipaddr + } + } + if n.Ipmi != nil { + if !ipmiaddr.IsUnspecified() && ipmiaddr != nil { + ipmiaddr = util.IncrementIPv4(ipmiaddr, 1) + wwlog.Verbose("Incremented ipmi IP addr to %s", ipmiaddr) + n.Ipmi.Ipaddr = ipmiaddr + } else if !n.Ipmi.Ipaddr.IsUnspecified() { + ipmiaddr = n.Ipmi.Ipaddr + } + } } - return apinode.NodeAdd(&set) + if err := nodeDB.Persist(); err != nil { + return fmt.Errorf("failed to persist new node: %w", err) + } + return warewulfd.DaemonReload() } } diff --git a/internal/app/wwctl/node/delete/main.go b/internal/app/wwctl/node/delete/main.go index e8f22b37..b13421b6 100644 --- a/internal/app/wwctl/node/delete/main.go +++ b/internal/app/wwctl/node/delete/main.go @@ -2,36 +2,64 @@ package delete import ( "fmt" + "os" "github.com/spf13/cobra" - apiNode "github.com/warewulf/warewulf/internal/pkg/api/node" - "github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1" + "github.com/warewulf/warewulf/internal/pkg/hostlist" "github.com/warewulf/warewulf/internal/pkg/node" "github.com/warewulf/warewulf/internal/pkg/util" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" + "github.com/warewulf/warewulf/internal/pkg/wwlog" ) func CobraRunE(cmd *cobra.Command, args []string) (err error) { + nodeDB, err := node.New() + if err != nil { + return fmt.Errorf("failed to open node database: %w", err) + } - ndp := wwapiv1.NodeDeleteParameter{ - Force: SetForce, - NodeNames: args, + nodes, err := nodeDB.FindAllNodes() + if err != nil { + return fmt.Errorf("could not get node list: %w", err) + } + + nodeArgs := hostlist.Expand(args) + var nodeList []node.Node + for _, r := range nodeArgs { + var match bool + for _, n := range nodes { + if n.Id() == r { + nodeList = append(nodeList, n) + match = true + } + } + if !match { + fmt.Fprintf(os.Stderr, "ERROR: No match for node: %s\n", r) + } + } + + if len(nodeList) == 0 { + fmt.Printf("No nodes found\n") + return } if !SetYes { - var nodeList []node.Node - // The checks run twice in the prompt case. - // Avoiding putting in a blocking prompt in an API. - nodeList, err = apiNode.NodeDeleteParameterCheck(&ndp, false) - if err != nil { - return - } - if len(nodeList) == 0 { - return - } yes := util.Confirm(fmt.Sprintf("Are you sure you want to delete %d nodes(s)", len(nodeList))) if !yes { return } } - return apiNode.NodeDelete(&ndp) + + for _, n := range nodeList { + if err := nodeDB.DelNode(n.Id()); err != nil { + wwlog.Error("%s", err) + } else { + wwlog.Verbose("Deleting node: %s\n", n.Id()) + } + } + + if err := nodeDB.Persist(); err != nil { + return fmt.Errorf("failed to persist nodedb: %w", err) + } + return warewulfd.DaemonReload() } diff --git a/internal/app/wwctl/node/imprt/main.go b/internal/app/wwctl/node/imprt/main.go index 0e73699b..cc1c42e5 100644 --- a/internal/app/wwctl/node/imprt/main.go +++ b/internal/app/wwctl/node/imprt/main.go @@ -6,8 +6,6 @@ import ( "os" "github.com/spf13/cobra" - apinode "github.com/warewulf/warewulf/internal/pkg/api/node" - "github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1" "github.com/warewulf/warewulf/internal/pkg/node" "github.com/warewulf/warewulf/internal/pkg/util" "gopkg.in/yaml.v3" @@ -32,9 +30,22 @@ func CobraRunE(cmd *cobra.Command, args []string) error { } if setYes || util.Confirm(fmt.Sprintf("Are you sure you want to modify %d nodes", len(importMap))) { - err = apinode.NodeAddFromYaml(&wwapiv1.NodeYaml{NodeConfMapYaml: string(buffer)}) + nodeDB, err := node.New() if err != nil { - return fmt.Errorf("got following problem when writing back yaml: %s", err) + return fmt.Errorf("could not open NodeDB: %w", err) + } + for nodeName, nodeData := range importMap { + if _, err := nodeDB.GetNodeOnly(nodeName); err == node.ErrNotFound { + if _, err := nodeDB.AddNode(nodeName); err != nil { + return fmt.Errorf("couldn't add new node: %w", err) + } + } + if err := nodeDB.SetNode(nodeName, *nodeData); err != nil { + return fmt.Errorf("couldn't set node: %w", err) + } + } + if err := nodeDB.Persist(); err != nil { + return fmt.Errorf("failed to persist nodedb: %w", err) } } diff --git a/internal/app/wwctl/node/list/main.go b/internal/app/wwctl/node/list/main.go index 06f9cea2..ce9da095 100644 --- a/internal/app/wwctl/node/list/main.go +++ b/internal/app/wwctl/node/list/main.go @@ -1,48 +1,117 @@ package list import ( + "encoding/json" + "sort" "strings" "github.com/spf13/cobra" + "gopkg.in/yaml.v3" "github.com/warewulf/warewulf/internal/app/wwctl/table" - apinode "github.com/warewulf/warewulf/internal/pkg/api/node" - "github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1" + "github.com/warewulf/warewulf/internal/pkg/hostlist" + "github.com/warewulf/warewulf/internal/pkg/node" "github.com/warewulf/warewulf/internal/pkg/wwlog" ) func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) { return func(cmd *cobra.Command, args []string) (err error) { - req := wwapiv1.GetNodeList{ - Nodes: args, - Type: wwapiv1.GetNodeList_Simple, + nodeDB, err := node.New() + if err != nil { + return } - if vars.showAll { - req.Type = wwapiv1.GetNodeList_All - } else if vars.showIpmi { - req.Type = wwapiv1.GetNodeList_Ipmi - } else if vars.showNet { - req.Type = wwapiv1.GetNodeList_Network - } else if vars.showLong { - req.Type = wwapiv1.GetNodeList_Long - } else if vars.showYaml { - req.Type = wwapiv1.GetNodeList_YAML - } else if vars.showJson { - req.Type = wwapiv1.GetNodeList_JSON + nodes, err := nodeDB.FindAllNodes() + if err != nil { + return } - nodeInfo, err := apinode.NodeList(&req) + nodeNames := hostlist.Expand(args) + sort.Strings(nodeNames) + filtered := node.FilterNodeListByName(nodes, nodeNames) - if len(nodeInfo.Output) > 0 { - if req.Type == wwapiv1.GetNodeList_YAML || req.Type == wwapiv1.GetNodeList_JSON { - wwlog.Info(nodeInfo.Output[0]) - } else { - t := table.New(cmd.OutOrStdout()) - t.AddHeader(table.Prep(strings.Split(nodeInfo.Output[0], ":=:"))...) - for _, val := range nodeInfo.Output[1:] { - t.AddLine(table.Prep(strings.Split(val, ":=:"))...) - } - t.Print() + if vars.showYaml || vars.showJson { + nodeMap := make(map[string]node.Node) + for _, n := range filtered { + nodeMap[n.Id()] = n } + var buf []byte + if vars.showJson { + buf, _ = json.MarshalIndent(nodeMap, "", " ") + } else { + buf, _ = yaml.Marshal(nodeMap) + } + wwlog.Info(string(buf)) + } else if vars.showAll { + t := table.New(cmd.OutOrStdout()) + t.AddHeader("NODE", "FIELD", "PROFILE", "VALUE") + for _, n := range filtered { + if _, fields, err := nodeDB.MergeNode(n.Id()); err != nil { + wwlog.Error("unable to merge node %v: %v", n.Id(), err) + continue + } else { + for _, f := range fields.List(n) { + t.AddLine(table.Prep([]string{n.Id(), f.Field, f.Source, f.Value})...) + } + } + } + t.Print() + } else if vars.showIpmi { + t := table.New(cmd.OutOrStdout()) + t.AddHeader("NODE", "IPMI IPADDR", "IPMI PORT", "IPMI USERNAME", "IPMI INTERFACE") + for _, n := range filtered { + ipaddr, port, username, iface := "", "", "", "" + if n.Ipmi != nil { + ipaddr = n.Ipmi.Ipaddr.String() + port = n.Ipmi.Port + username = n.Ipmi.UserName + iface = n.Ipmi.Interface + } + t.AddLine(table.Prep([]string{n.Id(), ipaddr, port, username, iface})...) + } + t.Print() + } else if vars.showNet { + t := table.New(cmd.OutOrStdout()) + t.AddHeader("NODE", "NETWORK", "HWADDR", "IPADDR", "GATEWAY", "DEVICE") + for _, n := range filtered { + if len(n.NetDevs) > 0 { + for name := range n.NetDevs { + t.AddLine(table.Prep([]string{n.Id(), name, + n.NetDevs[name].Hwaddr, + n.NetDevs[name].Ipaddr.String(), + n.NetDevs[name].Gateway.String(), + n.NetDevs[name].Device})...) + } + } else { + t.AddLine(table.Prep([]string{n.Id(), "", "", "", "", ""})...) + } + } + t.Print() + } else if vars.showLong { + t := table.New(cmd.OutOrStdout()) + t.AddHeader("NODE NAME", "KERNEL VERSION", "IMAGE", "OVERLAYS (S/R)") + for _, n := range filtered { + kernelVersion := "" + if n.Kernel != nil { + kernelVersion = n.Kernel.Version + } + t.AddLine(table.Prep([]string{n.Id(), + kernelVersion, + n.ImageName, + strings.Join(n.SystemOverlay, ",") + "/" + strings.Join(n.RuntimeOverlay, ",")})...) + } + t.Print() + } else { + // Simple (default) + t := table.New(cmd.OutOrStdout()) + t.AddHeader("NODE NAME", "PROFILES", "NETWORK") + for _, n := range filtered { + var netNames []string + for k := range n.NetDevs { + netNames = append(netNames, k) + } + sort.Strings(netNames) + t.AddLine(table.Prep([]string{n.Id(), strings.Join(n.Profiles, ","), strings.Join(netNames, ", ")})...) + } + t.Print() } return } diff --git a/internal/app/wwctl/node/set/main.go b/internal/app/wwctl/node/set/main.go index 8972a535..b87a1e9c 100644 --- a/internal/app/wwctl/node/set/main.go +++ b/internal/app/wwctl/node/set/main.go @@ -5,13 +5,11 @@ import ( "strings" "github.com/spf13/cobra" - apinode "github.com/warewulf/warewulf/internal/pkg/api/node" - "github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1" "github.com/warewulf/warewulf/internal/pkg/hostlist" "github.com/warewulf/warewulf/internal/pkg/node" "github.com/warewulf/warewulf/internal/pkg/util" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" "github.com/warewulf/warewulf/internal/pkg/wwlog" - "gopkg.in/yaml.v3" ) func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) { @@ -48,44 +46,108 @@ func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err err } delete(vars.nodeConf.Disks, "UNDEF") vars.nodeConf.Ipmi.Tags = vars.nodeAdd.IpmiTagsAdd - buffer, err := yaml.Marshal(vars.nodeConf) - if err != nil { - return fmt.Errorf("can not marshall nodeInfo: %s", err) - } - wwlog.Debug("sending following values: %s", string(buffer)) - args = hostlist.Expand(args) - set := wwapiv1.ConfSetParameter{ - NodeConfYaml: string(buffer), - NetdevDelete: vars.nodeDel.NetDel, - PartitionDelete: vars.nodeDel.PartDel, - DiskDelete: vars.nodeDel.DiskDel, - FilesystemDelete: vars.nodeDel.FsDel, - TagAdd: vars.nodeAdd.TagsAdd, - TagDel: vars.nodeDel.TagsDel, - NetTagAdd: vars.nodeAdd.NetTagsAdd, - NetTagDel: vars.nodeDel.NetTagsDel, - IpmiTagAdd: vars.nodeAdd.IpmiTagsAdd, - IpmiTagDel: vars.nodeDel.IpmiTagsDel, - Netdev: vars.nodeAdd.Net, - AllConfs: vars.setNodeAll, - Force: vars.setForce, - ConfList: args, + nodeDB, err := node.New() + if err != nil { + return fmt.Errorf("could not open configuration: %w", err) + } + + args = hostlist.Expand(args) + if len(args) == 0 && !vars.setNodeAll { + return fmt.Errorf("no nodes specified; use --all to modify all nodes") + } + if vars.setNodeAll { + args = nodeDB.ListAllNodes() + wwlog.Warn("this command will modify all nodes") + } else if len(nodeDB.ListAllNodes()) == 0 { + wwlog.Warn("no nodes/profiles found") + return nil + } + + changed := cmd.Flags().Changed + var count uint + for _, nId := range args { + wwlog.Debug("evaluating node: %s", nId) + nodePtr, err := nodeDB.GetNodeOnlyPtr(nId) + if err != nil { + wwlog.Warn("invalid node: %s", nId) + continue + } + nodePtr.UpdateFrom(&vars.nodeConf, changed) + if vars.nodeDel.NetDel != "" { + if _, ok := nodePtr.NetDevs[vars.nodeDel.NetDel]; !ok { + return fmt.Errorf("network device name doesn't exist: %s", vars.nodeDel.NetDel) + } + wwlog.Verbose("Node: %s, Deleting network device: %s", nId, vars.nodeDel.NetDel) + delete(nodePtr.NetDevs, vars.nodeDel.NetDel) + } + if vars.nodeDel.PartDel != "" { + for diskname, disk := range nodePtr.Disks { + if _, ok := disk.Partitions[vars.nodeDel.PartDel]; ok { + wwlog.Verbose("Node: %s, on disk %s, deleting partition: %s", nId, diskname, vars.nodeDel.PartDel) + delete(disk.Partitions, vars.nodeDel.PartDel) + } else { + return fmt.Errorf("partition doesn't exist: %s", vars.nodeDel.PartDel) + } + } + } + if vars.nodeDel.DiskDel != "" { + if _, ok := nodePtr.Disks[vars.nodeDel.DiskDel]; ok { + wwlog.Verbose("Node: %s, deleting disk: %s", nId, vars.nodeDel.DiskDel) + delete(nodePtr.Disks, vars.nodeDel.DiskDel) + } else { + return fmt.Errorf("disk doesn't exist: %s", vars.nodeDel.DiskDel) + } + } + if vars.nodeDel.FsDel != "" { + if _, ok := nodePtr.FileSystems[vars.nodeDel.FsDel]; ok { + wwlog.Verbose("Node: %s, deleting filesystem: %s", nId, vars.nodeDel.FsDel) + delete(nodePtr.FileSystems, vars.nodeDel.FsDel) + } else { + return fmt.Errorf("filesystem doesn't exist: %s", vars.nodeDel.FsDel) + } + } + for _, key := range vars.nodeDel.TagsDel { + delete(nodePtr.Tags, key) + } + for key, val := range vars.nodeAdd.TagsAdd { + if nodePtr.Tags == nil { + nodePtr.Tags = make(map[string]string) + } + nodePtr.Tags[key] = val + } + for key, val := range vars.nodeAdd.IpmiTagsAdd { + if nodePtr.Ipmi.Tags == nil { + nodePtr.Ipmi.Tags = make(map[string]string) + } + nodePtr.Ipmi.Tags[key] = val + } + for _, key := range vars.nodeDel.IpmiTagsDel { + delete(nodePtr.Ipmi.Tags, key) + } + if netDev, ok := nodePtr.NetDevs[vars.nodeAdd.Net]; ok { + for _, key := range vars.nodeDel.NetTagsDel { + delete(netDev.Tags, key) + } + if len(vars.nodeAdd.NetTagsAdd) > 0 && netDev.Tags == nil { + netDev.Tags = make(map[string]string) + } + for key, val := range vars.nodeAdd.NetTagsAdd { + netDev.Tags[key] = val + } + } + count++ } if !vars.setYes { - var nodeCount uint - // The checks run twice in the prompt case. - // Avoiding putting in a blocking prompt in an API. - _, nodeCount, err = apinode.NodeSetParameterCheck(&set) - if err != nil { - return nil - } - yes := util.Confirm(fmt.Sprintf("Are you sure you want to modify %d nodes(s)", nodeCount)) - if !yes { + if !util.Confirm(fmt.Sprintf("Are you sure you want to modify %d nodes(s)", count)) { return nil } } - return apinode.NodeSet(&set) + + if err := nodeDB.Persist(); err != nil { + return err + } + return warewulfd.DaemonReload() } } diff --git a/internal/app/wwctl/node/set/main_test.go b/internal/app/wwctl/node/set/main_test.go index f0247101..21ccd3b7 100644 --- a/internal/app/wwctl/node/set/main_test.go +++ b/internal/app/wwctl/node/set/main_test.go @@ -681,6 +681,22 @@ nodes: number: "1" wipe_partition_entry: true`, }, + "--all modifies all nodes": { + args: []string{"--all", "--comment=batch-update"}, + wantErr: false, + inDB: ` +nodeprofiles: {} +nodes: + n01: {} + n02: {}`, + outDB: ` +nodeprofiles: {} +nodes: + n01: + comment: batch-update + n02: + comment: batch-update`, + }, } for name, tt := range tests { diff --git a/internal/app/wwctl/node/set/root.go b/internal/app/wwctl/node/set/root.go index 96b3aafd..7176c229 100644 --- a/internal/app/wwctl/node/set/root.go +++ b/internal/app/wwctl/node/set/root.go @@ -26,7 +26,7 @@ func GetCommand() *cobra.Command { Short: "Configure node properties", Long: "This command sets configuration properties for nodes matching PATTERN.\n\nNote: use the string 'UNSET' to remove a configuration\n" + hostlist.Docstring, Aliases: []string{"modify"}, - Args: cobra.MinimumNArgs(1), // require pattern as a mandatory arg + Args: cobra.ArbitraryArgs, RunE: CobraRunE(&vars), ValidArgsFunction: completions.Nodes, } diff --git a/internal/app/wwctl/node/status/main.go b/internal/app/wwctl/node/status/main.go index 1e917297..f3cd331f 100644 --- a/internal/app/wwctl/node/status/main.go +++ b/internal/app/wwctl/node/status/main.go @@ -1,21 +1,29 @@ package nodestatus import ( + "encoding/json" "fmt" + "net/http" "sort" "strings" "time" "github.com/fatih/color" "github.com/spf13/cobra" - apinode "github.com/warewulf/warewulf/internal/pkg/api/node" - "github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1" warewulfconf "github.com/warewulf/warewulf/internal/pkg/config" "github.com/warewulf/warewulf/internal/pkg/hostlist" "github.com/warewulf/warewulf/internal/pkg/wwlog" "golang.org/x/term" ) +type nodeStatus struct { + NodeName string `json:"node name"` + Stage string `json:"stage"` + Sent string `json:"sent"` + Ipaddr string `json:"ipaddr"` + Lastseen int64 `json:"last seen"` +} + func displayStage(stage string) string { switch stage { case "efiboot": @@ -54,10 +62,21 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) { var count int rightnow := time.Now().Unix() - var nodeStatusResponse *wwapiv1.NodeStatusResponse - nodeStatusResponse, err = apinode.NodeStatus([]string{}) + statusURL := fmt.Sprintf("http://%s:%d/status", controller.Ipaddr, controller.Warewulf.Port) + wwlog.Verbose("Connecting to: %s", statusURL) + + resp, err := http.Get(statusURL) if err != nil { - return err + return fmt.Errorf("could not connect to Warewulf server: %w", err) + } + + var wwNodeStatus struct { + Nodes map[string]*nodeStatus `json:"nodes"` + } + err = json.NewDecoder(resp.Body).Decode(&wwNodeStatus) + _ = resp.Body.Close() + if err != nil { + return fmt.Errorf("could not decode JSON: %w", err) } if SetWatch { @@ -73,20 +92,20 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) { fmt.Printf("%s\n", strings.Repeat("=", 80)) wwlog.Verbose("Building sort index") - var statuses []*wwapiv1.NodeStatus + var statuses []*nodeStatus if len(args) > 0 { nodeList := hostlist.Expand(args) - for i := 0; i < len(nodeStatusResponse.NodeStatus); i++ { - for j := 0; j < len(nodeList); j++ { - if nodeStatusResponse.NodeStatus[i].NodeName == nodeList[j] { - statuses = append(statuses, nodeStatusResponse.NodeStatus[i]) + for _, v := range wwNodeStatus.Nodes { + for _, name := range nodeList { + if v.NodeName == name { + statuses = append(statuses, v) break } } } } else { - for i := 0; i < len(nodeStatusResponse.NodeStatus); i++ { - statuses = append(statuses, nodeStatusResponse.NodeStatus[i]) + for _, v := range wwNodeStatus.Nodes { + statuses = append(statuses, v) } } diff --git a/internal/pkg/api/node/add.go b/internal/pkg/api/node/add.go deleted file mode 100644 index 1a1d0baf..00000000 --- a/internal/pkg/api/node/add.go +++ /dev/null @@ -1,76 +0,0 @@ -package apinode - -import ( - "encoding/hex" - "fmt" - "net" - - "github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1" - "github.com/warewulf/warewulf/internal/pkg/hostlist" - "github.com/warewulf/warewulf/internal/pkg/node" - "github.com/warewulf/warewulf/internal/pkg/util" - "github.com/warewulf/warewulf/internal/pkg/warewulfd" - "github.com/warewulf/warewulf/internal/pkg/wwlog" - "gopkg.in/yaml.v3" -) - -// NodeAdd adds nodes for management by Warewulf. -func NodeAdd(nap *wwapiv1.NodeAddParameter) (err error) { - - if nap == nil { - return fmt.Errorf("NodeAddParameter is nil") - } - - nodeDB, err := node.New() - if err != nil { - return fmt.Errorf("failed to open node database: %w", err) - } - dbHash := nodeDB.Hash() - if hex.EncodeToString(dbHash[:]) != nap.Hash && !nap.Force { - return fmt.Errorf("got wrong hash, not modifying node database") - } - node_args := hostlist.Expand(nap.NodeNames) - var ipv4, ipmiaddr net.IP - for _, a := range node_args { - n, err := nodeDB.AddNode(a) - if err != nil { - return fmt.Errorf("failed to add node: %w", err) - } - err = yaml.Unmarshal([]byte(nap.NodeConfYaml), &n) - if err != nil { - return fmt.Errorf("failed to decode nodeConf: %w", err) - } - wwlog.Info("Added node: %s", a) - for _, dev := range n.NetDevs { - if !ipv4.IsUnspecified() && ipv4 != nil { - // if more nodes are added increment IPv4 address - ipv4 = util.IncrementIPv4(ipv4, 1) - wwlog.Verbose("Incremented IP addr to %s", ipv4) - dev.Ipaddr = ipv4 - - } else if !dev.Ipaddr.IsUnspecified() { - ipv4 = dev.Ipaddr - } - } - if n.Ipmi != nil { - if !ipmiaddr.IsUnspecified() && ipmiaddr != nil { - ipmiaddr = util.IncrementIPv4(ipmiaddr, 1) - wwlog.Verbose("Incremented ipmi IP addr to %s", ipmiaddr) - n.Ipmi.Ipaddr = ipmiaddr - } else if !n.Ipmi.Ipaddr.IsUnspecified() { - ipmiaddr = n.Ipmi.Ipaddr - } - } - } - - err = nodeDB.Persist() - if err != nil { - return fmt.Errorf("failed to persist new node: %w", err) - } - - err = warewulfd.DaemonReload() - if err != nil { - return fmt.Errorf("failed to reload warewulf daemon: %w", err) - } - return -} diff --git a/internal/pkg/api/node/delete.go b/internal/pkg/api/node/delete.go deleted file mode 100644 index 41e60917..00000000 --- a/internal/pkg/api/node/delete.go +++ /dev/null @@ -1,104 +0,0 @@ -package apinode - -import ( - "encoding/hex" - "fmt" - "os" - - "github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1" - "github.com/warewulf/warewulf/internal/pkg/hostlist" - "github.com/warewulf/warewulf/internal/pkg/node" - "github.com/warewulf/warewulf/internal/pkg/warewulfd" - "github.com/warewulf/warewulf/internal/pkg/wwlog" -) - -// NodeDelete adds nodes for management by Warewulf. -func NodeDelete(ndp *wwapiv1.NodeDeleteParameter) (err error) { - - var nodeList []node.Node - nodeList, err = NodeDeleteParameterCheck(ndp, false) - if err != nil { - return - } - - nodeDB, err := node.New() - if err != nil { - wwlog.Error("Failed to open node database: %s", err) - return - } - dbHash := nodeDB.Hash() - if hex.EncodeToString(dbHash[:]) != ndp.Hash && !ndp.Force { - return fmt.Errorf("got wrong hash, not modifying node database") - } - - for _, n := range nodeList { - err := nodeDB.DelNode(n.Id()) - if err != nil { - wwlog.Error("%s", err) - } else { - wwlog.Verbose("Deleting node: %s\n", n.Id()) - } - } - - err = nodeDB.Persist() - if err != nil { - return fmt.Errorf("failed to persist nodedb: %w", err) - } - - err = warewulfd.DaemonReload() - if err != nil { - return fmt.Errorf("failed to reload warewulf daemon: %w", err) - } - return -} - -// NodeDeleteParameterCheck does error checking on NodeDeleteParameter. -// Output to the console if console is true. -// Returns the nodes to delete. -func NodeDeleteParameterCheck(ndp *wwapiv1.NodeDeleteParameter, console bool) (nodeList []node.Node, err error) { - - if ndp == nil { - err = fmt.Errorf("NodeDeleteParameter is nil") - return - } - - nodeDB, err := node.New() - if err != nil { - wwlog.Error("Failed to open node database: %s", err) - return - } - dbHash := nodeDB.Hash() - if hex.EncodeToString(dbHash[:]) != ndp.Hash && !ndp.Force { - wwlog.Debug("got hash: %s", ndp.Hash) - wwlog.Debug("actual hash: %s", hex.EncodeToString(dbHash[:])) - err = fmt.Errorf("got wrong hash, not modifying node database") - return - } - - nodes, err := nodeDB.FindAllNodes() - if err != nil { - wwlog.Error("Could not get node list: %s", err) - return - } - - node_args := hostlist.Expand(ndp.NodeNames) - - for _, r := range node_args { - var match bool - for _, n := range nodes { - if n.Id() == r { - nodeList = append(nodeList, n) - match = true - } - } - - if !match { - fmt.Fprintf(os.Stderr, "ERROR: No match for node: %s\n", r) - } - } - - if len(nodeList) == 0 { - fmt.Printf("No nodes found\n") - } - return -} diff --git a/internal/pkg/api/node/edit.go b/internal/pkg/api/node/edit.go deleted file mode 100644 index 915ace54..00000000 --- a/internal/pkg/api/node/edit.go +++ /dev/null @@ -1,41 +0,0 @@ -package apinode - -import ( - "fmt" - - "github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1" - "github.com/warewulf/warewulf/internal/pkg/node" - "gopkg.in/yaml.v3" -) - -/* -Add nodes from yaml -*/ -func NodeAddFromYaml(nodeList *wwapiv1.NodeYaml) (err error) { - nodeDB, err := node.New() - if err != nil { - return fmt.Errorf("could not open NodeDB: %w", err) - } - nodeMap := make(map[string]*node.Node) - err = yaml.Unmarshal([]byte(nodeList.NodeConfMapYaml), nodeMap) - if err != nil { - return fmt.Errorf("could not unmarshal Yaml: %w", err) - } - for nodeName, nodeData := range nodeMap { - if _, err = nodeDB.GetNodeOnly(nodeName); err == node.ErrNotFound { - _, err = nodeDB.AddNode(nodeName) - if err != nil { - return fmt.Errorf("couldn't add new node: %w", err) - } - } - err = nodeDB.SetNode(nodeName, *nodeData) - if err != nil { - return fmt.Errorf("couldn't set node: %w", err) - } - } - err = nodeDB.Persist() - if err != nil { - return fmt.Errorf("failed to persist nodedb: %w", err) - } - return nil -} diff --git a/internal/pkg/api/node/list.go b/internal/pkg/api/node/list.go deleted file mode 100644 index abe1b2c5..00000000 --- a/internal/pkg/api/node/list.go +++ /dev/null @@ -1,126 +0,0 @@ -package apinode - -import ( - "encoding/json" - "fmt" - "sort" - "strings" - - "github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1" - "github.com/warewulf/warewulf/internal/pkg/hostlist" - "github.com/warewulf/warewulf/internal/pkg/node" - "github.com/warewulf/warewulf/internal/pkg/wwlog" - "gopkg.in/yaml.v3" -) - -/* -NodeList lists all to none of the nodes managed by Warewulf. Returns -a formated string slice, with each line as separate string -*/ -func NodeList(nodeGet *wwapiv1.GetNodeList) (nodeList wwapiv1.NodeList, err error) { - // nil is okay for nodeNames - nodeDB, err := node.New() - if err != nil { - return - } - nodes, err := nodeDB.FindAllNodes() - if err != nil { - return - } - nodeGet.Nodes = hostlist.Expand(nodeGet.Nodes) - sort.Strings(nodeGet.Nodes) - - if nodeGet.Type == wwapiv1.GetNodeList_Simple { - nodeList.Output = append(nodeList.Output, - fmt.Sprintf("%s:=:%s:=:%s", "NODE NAME", "PROFILES", "NETWORK")) - for _, n := range node.FilterNodeListByName(nodes, nodeGet.Nodes) { - var netNames []string - for k := range n.NetDevs { - netNames = append(netNames, k) - } - sort.Strings(netNames) - nodeList.Output = append(nodeList.Output, - fmt.Sprintf("%s:=:%s:=:%s", n.Id(), strings.Join(n.Profiles, ","), strings.Join(netNames, ", "))) - } - } else if nodeGet.Type == wwapiv1.GetNodeList_Network { - nodeList.Output = append(nodeList.Output, - fmt.Sprintf("%s:=:%s:=:%s:=:%s:=:%s:=:%s", "NODE", "NETWORK", "HWADDR", "IPADDR", "GATEWAY", "DEVICE")) - for _, n := range node.FilterNodeListByName(nodes, nodeGet.Nodes) { - if len(n.NetDevs) > 0 { - for name := range n.NetDevs { - nodeList.Output = append(nodeList.Output, - fmt.Sprintf("%s:=:%s:=:%s:=:%s:=:%s:=:%s", n.Id(), name, - n.NetDevs[name].Hwaddr, - n.NetDevs[name].Ipaddr, - n.NetDevs[name].Gateway, - n.NetDevs[name].Device)) - } - } else { - nodeList.Output = append(nodeList.Output, - fmt.Sprintf("%s:=:%s:=:%s:=:%s:=:%s:=:%s", n.Id(), "--", "--", "--", "--", "--")) - } - } - } else if nodeGet.Type == wwapiv1.GetNodeList_Ipmi { - nodeList.Output = append(nodeList.Output, - fmt.Sprintf("%s:=:%s:=:%s:=:%s:=:%s", "NODE", "IPMI IPADDR", "IPMI PORT", "IPMI USERNAME", "IPMI INTERFACE")) - for _, n := range node.FilterNodeListByName(nodes, nodeGet.Nodes) { - ipaddr, port, username, iface := "", "", "", "" - if n.Ipmi != nil { - ipaddr = n.Ipmi.Ipaddr.String() - port = n.Ipmi.Port - username = n.Ipmi.UserName - iface = n.Ipmi.Interface - } - nodeList.Output = append(nodeList.Output, - fmt.Sprintf("%s:=:%s:=:%s:=:%s:=:%s", - n.Id(), - ipaddr, - port, - username, - iface)) - } - } else if nodeGet.Type == wwapiv1.GetNodeList_Long { - nodeList.Output = append(nodeList.Output, - fmt.Sprintf("%s:=:%s:=:%s:=:%s", "NODE NAME", "KERNEL VERSION", "IMAGE", "OVERLAYS (S/R)")) - for _, n := range node.FilterNodeListByName(nodes, nodeGet.Nodes) { - kernelVersion := "" - if n.Kernel != nil { - kernelVersion = n.Kernel.Version - } - nodeList.Output = append(nodeList.Output, - fmt.Sprintf("%s:=:%s:=:%s:=:%s", n.Id(), - kernelVersion, - n.ImageName, - strings.Join(n.SystemOverlay, ",")+"/"+strings.Join(n.RuntimeOverlay, ","))) - } - } else if nodeGet.Type == wwapiv1.GetNodeList_All { - nodeList.Output = append(nodeList.Output, - fmt.Sprintf("%s:=:%s:=:%s:=:%s", "NODE", "FIELD", "PROFILE", "VALUE")) - for _, n := range node.FilterNodeListByName(nodes, nodeGet.Nodes) { - if _, fields, err := nodeDB.MergeNode(n.Id()); err != nil { - wwlog.Error("unable to merge node %v: %v", n.Id(), err) - continue - } else { - for _, f := range fields.List(n) { - nodeList.Output = append(nodeList.Output, - fmt.Sprintf("%s:=:%s:=:%s:=:%s", n.Id(), f.Field, f.Source, f.Value)) - } - } - } - } else if nodeGet.Type == wwapiv1.GetNodeList_YAML || nodeGet.Type == wwapiv1.GetNodeList_JSON { - nodeMap := make(map[string]node.Node) - for _, n := range node.FilterNodeListByName(nodes, nodeGet.Nodes) { - nodeMap[n.Id()] = n - } - var buf []byte - if nodeGet.Type == wwapiv1.GetNodeList_JSON { - buf, _ = json.MarshalIndent(nodeMap, "", " ") - } - if nodeGet.Type == wwapiv1.GetNodeList_YAML { - buf, _ = yaml.Marshal(nodeMap) - } - nodeList.Output = append(nodeList.Output, string(buf)) - - } - return -} diff --git a/internal/pkg/api/node/set.go b/internal/pkg/api/node/set.go deleted file mode 100644 index 62e26209..00000000 --- a/internal/pkg/api/node/set.go +++ /dev/null @@ -1,145 +0,0 @@ -package apinode - -import ( - "fmt" - - "dario.cat/mergo" - "github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1" - "github.com/warewulf/warewulf/internal/pkg/node" - "github.com/warewulf/warewulf/internal/pkg/util" - "github.com/warewulf/warewulf/internal/pkg/warewulfd" - "github.com/warewulf/warewulf/internal/pkg/wwlog" - "gopkg.in/yaml.v3" -) - -// NodeSet is the wwapiv1 implmentation for updating node fields. -func NodeSet(set *wwapiv1.ConfSetParameter) (err error) { - if set == nil { - return fmt.Errorf("NodeSetParameter is nil") - } - var nodeDB node.NodesYaml - nodeDB, _, err = NodeSetParameterCheck(set) - if err != nil { - return err - } - if err = nodeDB.Persist(); err != nil { - return err - } - if err = warewulfd.DaemonReload(); err != nil { - return err - } - return -} - -/* -NodeSetParameterCheck does error checking and returns a modified -NodeYml which than can be persisted -*/ -func NodeSetParameterCheck(set *wwapiv1.ConfSetParameter) (nodeDB node.NodesYaml, count uint, err error) { - nodeDB, err = node.New() - if err != nil { - wwlog.Error("Could not open configuration: %s", err) - return - } - if set == nil { - err = fmt.Errorf("node set parameter is nil") - return - } - if set.ConfList == nil { - err = fmt.Errorf("node nodes to set") - return - } - confs := nodeDB.ListAllNodes() - // Note: This does not do expansion on the nodes. - if set.AllConfs || (len(set.ConfList) == 0) { - wwlog.Warn("this command will modify all nodes/profiles") - } else if len(confs) == 0 { - wwlog.Warn("no nodes/profiles found") - return - } - for _, nId := range set.ConfList { - if util.InSlice(set.ConfList, nId) { - wwlog.Debug("evaluating node: %s", nId) - var nodePtr *node.Node - nodePtr, err = nodeDB.GetNodeOnlyPtr(nId) - if err != nil { - wwlog.Warn("invalid node: %s", nId) - continue - } - newConf := node.EmptyNode() - err = yaml.Unmarshal([]byte(set.NodeConfYaml), &newConf) - if err != nil { - return - } - // merge in - err = mergo.Merge(nodePtr, &newConf, mergo.WithOverride, mergo.WithTransformers(node.Transformer{})) - if err != nil { - return - } - if set.NetdevDelete != "" { - if _, ok := nodePtr.NetDevs[set.NetdevDelete]; !ok { - err = fmt.Errorf("network device name doesn't exist: %s", set.NetdevDelete) - wwlog.Error(fmt.Sprintf("%v", err.Error())) - return - } - wwlog.Verbose("Profile: %s, Deleting network device: %s", nId, set.NetdevDelete) - delete(nodePtr.NetDevs, set.NetdevDelete) - } - if set.PartitionDelete != "" { - for diskname, disk := range nodePtr.Disks { - if _, ok := disk.Partitions[set.PartitionDelete]; ok { - wwlog.Verbose("Node: %s, on disk %s, deleting partition: %s", nId, diskname, set.PartitionDelete) - delete(disk.Partitions, set.PartitionDelete) - } else { - return nodeDB, count, fmt.Errorf("partition doesn't exist: %s", set.PartitionDelete) - - } - } - } - if set.DiskDelete != "" { - if _, ok := nodePtr.Disks[set.DiskDelete]; ok { - wwlog.Verbose("Node: %s, deleting disk: %s", nId, set.DiskDelete) - delete(nodePtr.Disks, set.DiskDelete) - } else { - return nodeDB, count, fmt.Errorf("disk doesn't exist: %s", set.DiskDelete) - } - } - if set.FilesystemDelete != "" { - if _, ok := nodePtr.FileSystems[set.FilesystemDelete]; ok { - wwlog.Verbose("Node: %s, deleting filesystem: %s", nId, set.FilesystemDelete) - delete(nodePtr.FileSystems, set.FilesystemDelete) - } else { - return nodeDB, count, fmt.Errorf("disk doesn't exist: %s", set.FilesystemDelete) - } - } - for _, key := range set.TagDel { - delete(nodePtr.Tags, key) - } - for key, val := range set.TagAdd { - if nodePtr.Tags == nil { - nodePtr.Tags = make(map[string]string) - } - nodePtr.Tags[key] = val - } - for key, val := range set.IpmiTagAdd { - if nodePtr.Ipmi.Tags == nil { - nodePtr.Ipmi.Tags = make(map[string]string) - } - nodePtr.Ipmi.Tags[key] = val - } - for _, key := range set.IpmiTagDel { - delete(nodePtr.Ipmi.Tags, key) - } - if _, ok := nodePtr.NetDevs[set.Netdev]; ok { - for _, key := range set.NetTagDel { - delete(nodePtr.NetDevs[set.Netdev].Tags, key) - } - for key, val := range set.NetTagAdd { - nodePtr.NetDevs[set.Netdev].Tags[key] = val - } - } - count++ - } - } - return -} diff --git a/internal/pkg/api/node/status.go b/internal/pkg/api/node/status.go deleted file mode 100644 index 3bbb2bdd..00000000 --- a/internal/pkg/api/node/status.go +++ /dev/null @@ -1,94 +0,0 @@ -package apinode - -import ( - "encoding/json" - "fmt" - "net/http" - - warewulfconf "github.com/warewulf/warewulf/internal/pkg/config" - - "github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1" - "github.com/warewulf/warewulf/internal/pkg/hostlist" - "github.com/warewulf/warewulf/internal/pkg/wwlog" -) - -// NodeStatus returns the imaging state for nodes. -// This requires warewulfd. -func NodeStatus(nodeNames []string) (nodeStatusResponse *wwapiv1.NodeStatusResponse, err error) { - - // Local structs for translating json from warewulfd. - type nodeStatusInternal struct { - NodeName string `json:"node name"` - Stage string `json:"stage"` - Sent string `json:"sent"` - Ipaddr string `json:"ipaddr"` - Lastseen int64 `json:"last seen"` - } - - // all status is a map with one key (nodes) - // and maps of [nodeName]NodeStatus underneath. - type allStatus struct { - Nodes map[string]*nodeStatusInternal `json:"nodes"` - } - - controller := warewulfconf.Get() - - if controller.Ipaddr == "" { - err = fmt.Errorf("the Warewulf Server IP Address is not properly configured") - wwlog.Error(fmt.Sprintf("%v", err.Error())) - return - } - - statusURL := fmt.Sprintf("http://%s:%d/status", controller.Ipaddr, controller.Warewulf.Port) - wwlog.Verbose("Connecting to: %s", statusURL) - - resp, err := http.Get(statusURL) - if err != nil { - wwlog.Error("Could not connect to Warewulf server: %s", err) - return - } - defer func() { _ = resp.Body.Close() }() - - decoder := json.NewDecoder(resp.Body) - var wwNodeStatus allStatus - - err = decoder.Decode(&wwNodeStatus) - if err != nil { - wwlog.Error("Could not decode JSON: %s", err) - return - } - - // Translate struct and filter. - nodeStatusResponse = &wwapiv1.NodeStatusResponse{} - - if len(nodeNames) == 0 { - for _, v := range wwNodeStatus.Nodes { - nodeStatusResponse.NodeStatus = append(nodeStatusResponse.NodeStatus, - &wwapiv1.NodeStatus{ - NodeName: v.NodeName, - Stage: v.Stage, - Sent: v.Sent, - Ipaddr: v.Ipaddr, - Lastseen: v.Lastseen, - }) - } - } else { - nodeList := hostlist.Expand(nodeNames) - for _, v := range wwNodeStatus.Nodes { - for j := 0; j < len(nodeList); j++ { - if v.NodeName == nodeList[j] { - nodeStatusResponse.NodeStatus = append(nodeStatusResponse.NodeStatus, - &wwapiv1.NodeStatus{ - NodeName: v.NodeName, - Stage: v.Stage, - Sent: v.Sent, - Ipaddr: v.Ipaddr, - Lastseen: v.Lastseen, - }) - break - } - } - } - } - return -} diff --git a/internal/pkg/node/update.go b/internal/pkg/node/update.go new file mode 100644 index 00000000..0b194096 --- /dev/null +++ b/internal/pkg/node/update.go @@ -0,0 +1,88 @@ +package node + +import ( + "reflect" +) + +// UpdateFrom copies fields from src to dst, but only those fields whose +// corresponding cobra flag (identified by the "lopt" struct tag) reports +// as changed. This replaces the old pattern of marshaling to YAML (to strip +// zero values via omitempty) and then merging with mergo. +// +// The changed function should typically be cmd.Flags().Changed. +func (dst *Node) UpdateFrom(src *Node, changed func(string) bool) { + recursiveUpdateFrom(reflect.ValueOf(dst).Elem(), reflect.ValueOf(src).Elem(), changed) +} + +// UpdateFrom copies fields from src to dst for profiles. +func (dst *Profile) UpdateFrom(src *Profile, changed func(string) bool) { + recursiveUpdateFrom(reflect.ValueOf(dst).Elem(), reflect.ValueOf(src).Elem(), changed) +} + +func recursiveUpdateFrom(dst, src reflect.Value, changed func(string) bool) { + srcType := src.Type() + + for i := 0; i < src.NumField(); i++ { + field := srcType.Field(i) + srcField := src.Field(i) + dstField := dst.Field(i) + + if !field.IsExported() { + continue + } + + if lopt := field.Tag.Get("lopt"); lopt != "" && field.Tag.Get("comment") != "" { + // Leaf field with a cobra flag — copy if the flag was changed + if changed(lopt) { + dstField.Set(srcField) + } + } else if field.Anonymous { + // Embedded struct (e.g., Profile in Node) + recursiveUpdateFrom(dstField, srcField, changed) + } else if field.Type.Kind() == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct { + // Pointer-to-struct (e.g., *IpmiConf, *KernelConf) + if srcField.IsNil() { + continue + } + if dstField.IsNil() { + dstField.Set(reflect.New(field.Type.Elem())) + } + recursiveUpdateFrom(dstField.Elem(), srcField.Elem(), changed) + } else if field.Type.Kind() == reflect.Struct { + // Direct struct + recursiveUpdateFrom(dstField, srcField, changed) + } else if field.Type.Kind() == reflect.Map { + // Map fields (e.g., NetDevs, Disks, FileSystems) + switch field.Type.Elem().Kind() { + case reflect.String, reflect.Interface: + // Tags map[string]string — handled separately by tag add/del operations + continue + case reflect.Pointer: + // map[string]*Struct — update entries from src into dst + if srcField.IsNil() { + continue + } + if dstField.IsNil() { + dstField.Set(reflect.MakeMap(field.Type)) + } + for _, key := range srcField.MapKeys() { + srcEntry := srcField.MapIndex(key) + if srcEntry.IsNil() { + continue + } + dstEntry := dstField.MapIndex(key) + if !dstEntry.IsValid() || dstEntry.IsNil() { + // Create new entry in dst + dstEntry = reflect.New(field.Type.Elem().Elem()) + dstField.SetMapIndex(key, dstEntry) + } + // Map values are not addressable, so we need to work with a copy + tmpDst := reflect.New(field.Type.Elem().Elem()).Elem() + tmpDst.Set(dstEntry.Elem()) + recursiveUpdateFrom(tmpDst, srcEntry.Elem(), changed) + dstField.SetMapIndex(key, tmpDst.Addr()) + } + } + } + } +}