Utku Ozdemir 2dd0964c5f
refactor: use resource watches on dashboard
Instead of doing excessive get/list requests, do a watch per node in an infinite retry.

Additionally, refactor the dashboard code to make the various data listener namings more consistent and reorganize the packages.

Closes siderolabs/talos#6960.

Signed-off-by: Utku Ozdemir <utku.ozdemir@siderolabs.com>
2023-03-17 23:06:35 +01:00

228 lines
5.2 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 components
import (
"net/netip"
"strings"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/rivo/tview"
"github.com/siderolabs/gen/maps"
"github.com/siderolabs/gen/slices"
"github.com/siderolabs/talos/internal/pkg/dashboard/resourcedata"
"github.com/siderolabs/talos/pkg/machinery/resources/cluster"
"github.com/siderolabs/talos/pkg/machinery/resources/network"
)
type networkInfoData struct {
addresses string
gateway string
resolvers string
timeservers string
routeStatusMap map[resource.ID]*network.RouteStatus
memberMap map[resource.ID]*cluster.Member
}
// NetworkInfo represents the network info widget.
type NetworkInfo struct {
tview.TextView
selectedNode string
nodeMap map[string]*networkInfoData
}
// NewNetworkInfo initializes NetworkInfo.
func NewNetworkInfo() *NetworkInfo {
component := &NetworkInfo{
TextView: *tview.NewTextView(),
nodeMap: make(map[string]*networkInfoData),
}
component.SetDynamicColors(true).
SetText(noData).
SetBorderPadding(1, 0, 1, 0)
return component
}
// OnNodeSelect implements the NodeSelectListener interface.
func (widget *NetworkInfo) OnNodeSelect(node string) {
if node != widget.selectedNode {
widget.selectedNode = node
widget.redraw()
}
}
// OnResourceDataChange implements the ResourceDataListener interface.
func (widget *NetworkInfo) OnResourceDataChange(data resourcedata.Data) {
widget.updateNodeData(data)
if data.Node == widget.selectedNode {
widget.redraw()
}
}
func (widget *NetworkInfo) updateNodeData(data resourcedata.Data) {
nodeData := widget.getOrCreateNodeData(data.Node)
switch res := data.Resource.(type) {
case *network.ResolverStatus:
if data.Deleted {
nodeData.resolvers = notAvailable
} else {
nodeData.resolvers = widget.resolvers(res)
}
case *network.TimeServerStatus:
if data.Deleted {
nodeData.timeservers = notAvailable
} else {
nodeData.timeservers = widget.timeservers(res)
}
case *network.RouteStatus:
if data.Deleted {
delete(nodeData.routeStatusMap, res.Metadata().ID())
} else {
nodeData.routeStatusMap[res.Metadata().ID()] = res
}
nodeData.gateway = widget.gateway(maps.Values(nodeData.routeStatusMap))
case *cluster.Member:
if data.Deleted {
delete(nodeData.memberMap, res.Metadata().ID())
} else {
nodeData.memberMap[res.Metadata().ID()] = res
}
nodeData.addresses = widget.addresses(data.Node, maps.Values(nodeData.memberMap))
}
}
func (widget *NetworkInfo) getOrCreateNodeData(node string) *networkInfoData {
data, ok := widget.nodeMap[node]
if !ok {
data = &networkInfoData{
addresses: notAvailable,
gateway: notAvailable,
resolvers: notAvailable,
timeservers: notAvailable,
routeStatusMap: make(map[resource.ID]*network.RouteStatus),
memberMap: make(map[resource.ID]*cluster.Member),
}
widget.nodeMap[node] = data
}
return data
}
func (widget *NetworkInfo) redraw() {
data := widget.getOrCreateNodeData(widget.selectedNode)
fields := fieldGroup{
fields: []field{
{
Name: "IP",
Value: data.addresses,
},
{
Name: "GW",
Value: data.gateway,
},
// TODO: enable when implemented
// {
// Name: "OUTBOUND",
// Value: data.outbound,
// },
{
Name: "DNS",
Value: data.resolvers,
},
{
Name: "NTP",
Value: data.timeservers,
},
},
}
widget.SetText(fields.String())
}
func (widget *NetworkInfo) addresses(node string, members []*cluster.Member) string {
var currentMember *cluster.Member
for _, member := range members {
for _, address := range member.TypedSpec().Addresses {
if address.String() == node {
currentMember = member
break
}
}
}
if currentMember == nil {
return notAvailable
}
ipStrs := slices.Map(currentMember.TypedSpec().Addresses, func(t netip.Addr) string {
return t.String()
})
return strings.Join(ipStrs, ", ")
}
func (widget *NetworkInfo) gateway(statuses []*network.RouteStatus) string {
resultV4 := notAvailable
resultV6 := notAvailable
priorityV4 := uint32(0)
priorityV6 := uint32(0)
for _, status := range statuses {
gateway := status.TypedSpec().Gateway
if !gateway.IsValid() {
continue
}
if gateway.Is4() && status.TypedSpec().Priority > priorityV4 {
resultV4 = gateway.String()
priorityV4 = status.TypedSpec().Priority
} else if gateway.Is6() && status.TypedSpec().Priority > priorityV6 {
resultV6 = gateway.String()
priorityV6 = status.TypedSpec().Priority
}
}
if resultV4 == notAvailable {
return resultV6
}
return resultV4
}
func (widget *NetworkInfo) resolvers(status *network.ResolverStatus) string {
strs := slices.Map(status.TypedSpec().DNSServers, func(t netip.Addr) string {
return t.String()
})
if len(strs) == 0 {
return none
}
return strings.Join(strs, ", ")
}
func (widget *NetworkInfo) timeservers(status *network.TimeServerStatus) string {
if len(status.TypedSpec().NTPServers) == 0 {
return none
}
return strings.Join(status.TypedSpec().NTPServers, ", ")
}