mirror of
https://github.com/siderolabs/talos.git
synced 2025-10-26 14:01:39 +01:00
Add a new resource, `SiderolinkStatus`, which combines the following info: - The Siderolink API endpoint without the query parameters or fragments (potentially sensitive info due to the join token) - The status of the Siderolink connection This resource is not set as sensitive, so it can be retrieved by the users with `os:operator` role (e.g., using `talosctl dashboard` through Omni). Make use of this resource in the dashboard to display the status of the Siderolink connection. Additionally, rework the status columns in the dashboard to: - Display a Linux terminal compatible "tick" or a "cross" prefix for statuses in addition to the red/green color coding. - Move and combine some statuses to save rows and make them more even. Closes siderolabs/talos#8643. Signed-off-by: Utku Ozdemir <utku.ozdemir@siderolabs.com>
274 lines
6.2 KiB
Go
274 lines
6.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"
|
||
"sort"
|
||
"strings"
|
||
|
||
"github.com/cosi-project/runtime/pkg/resource"
|
||
"github.com/rivo/tview"
|
||
"github.com/siderolabs/gen/maps"
|
||
"github.com/siderolabs/gen/xslices"
|
||
|
||
"github.com/siderolabs/talos/internal/pkg/dashboard/resourcedata"
|
||
"github.com/siderolabs/talos/pkg/machinery/resources/k8s"
|
||
"github.com/siderolabs/talos/pkg/machinery/resources/network"
|
||
)
|
||
|
||
var (
|
||
zeroPrefix = netip.Prefix{}
|
||
routedNoK8sID = network.FilteredNodeAddressID(network.NodeAddressRoutedID, k8s.NodeAddressFilterNoK8s)
|
||
)
|
||
|
||
type networkInfoData struct {
|
||
hostname string
|
||
gateway string
|
||
connectivity string
|
||
resolvers string
|
||
timeservers string
|
||
|
||
addresses string
|
||
nodeAddressRouted *network.NodeAddress
|
||
nodeAddressRoutedNoK8s *network.NodeAddress
|
||
|
||
routeStatusMap map[resource.ID]*network.RouteStatus
|
||
}
|
||
|
||
// 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()
|
||
}
|
||
}
|
||
|
||
//nolint:gocyclo
|
||
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.Status:
|
||
if data.Deleted {
|
||
nodeData.connectivity = notAvailable
|
||
} else {
|
||
nodeData.connectivity = widget.connectivity(res)
|
||
}
|
||
case *network.HostnameStatus:
|
||
if data.Deleted {
|
||
nodeData.hostname = notAvailable
|
||
} else {
|
||
nodeData.hostname = res.TypedSpec().Hostname
|
||
}
|
||
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 *network.NodeAddress:
|
||
widget.setAddresses(data, res)
|
||
}
|
||
}
|
||
|
||
func (widget *NetworkInfo) getOrCreateNodeData(node string) *networkInfoData {
|
||
data, ok := widget.nodeMap[node]
|
||
if !ok {
|
||
data = &networkInfoData{
|
||
hostname: notAvailable,
|
||
addresses: notAvailable,
|
||
gateway: notAvailable,
|
||
connectivity: notAvailable,
|
||
resolvers: notAvailable,
|
||
timeservers: notAvailable,
|
||
routeStatusMap: make(map[resource.ID]*network.RouteStatus),
|
||
}
|
||
|
||
widget.nodeMap[node] = data
|
||
}
|
||
|
||
return data
|
||
}
|
||
|
||
func (widget *NetworkInfo) redraw() {
|
||
data := widget.getOrCreateNodeData(widget.selectedNode)
|
||
|
||
fields := fieldGroup{
|
||
fields: []field{
|
||
{
|
||
Name: "HOST",
|
||
Value: data.hostname,
|
||
},
|
||
{
|
||
Name: "IP",
|
||
Value: data.addresses,
|
||
},
|
||
{
|
||
Name: "GW",
|
||
Value: data.gateway,
|
||
},
|
||
{
|
||
Name: "CONNECTIVITY",
|
||
Value: data.connectivity,
|
||
},
|
||
{
|
||
Name: "DNS",
|
||
Value: data.resolvers,
|
||
},
|
||
{
|
||
Name: "NTP",
|
||
Value: data.timeservers,
|
||
},
|
||
},
|
||
}
|
||
|
||
widget.SetText(fields.String())
|
||
}
|
||
|
||
func (widget *NetworkInfo) setAddresses(data resourcedata.Data, nodeAddress *network.NodeAddress) {
|
||
nodeData := widget.getOrCreateNodeData(data.Node)
|
||
|
||
switch nodeAddress.Metadata().ID() {
|
||
case network.NodeAddressRoutedID:
|
||
if data.Deleted {
|
||
nodeData.nodeAddressRouted = nil
|
||
} else {
|
||
nodeData.nodeAddressRouted = nodeAddress
|
||
}
|
||
case routedNoK8sID:
|
||
if data.Deleted {
|
||
nodeData.nodeAddressRoutedNoK8s = nil
|
||
} else {
|
||
nodeData.nodeAddressRoutedNoK8s = nodeAddress
|
||
}
|
||
}
|
||
|
||
formatIPs := func(res *network.NodeAddress) string {
|
||
if res == nil {
|
||
return notAvailable
|
||
}
|
||
|
||
strs := xslices.Map(res.TypedSpec().Addresses, func(prefix netip.Prefix) string {
|
||
return prefix.String()
|
||
})
|
||
|
||
sort.Strings(strs)
|
||
|
||
return strings.Join(strs, ", ")
|
||
}
|
||
|
||
// if "routed-no-k8s" is available, use it
|
||
if nodeData.nodeAddressRoutedNoK8s != nil {
|
||
nodeData.addresses = formatIPs(nodeData.nodeAddressRoutedNoK8s)
|
||
|
||
return
|
||
}
|
||
|
||
// fallback to "routed"
|
||
nodeData.addresses = formatIPs(nodeData.nodeAddressRouted)
|
||
}
|
||
|
||
func (widget *NetworkInfo) gateway(statuses []*network.RouteStatus) string {
|
||
var gatewaysV4, gatewaysV6 []string
|
||
|
||
for _, status := range statuses {
|
||
gateway := status.TypedSpec().Gateway
|
||
if !gateway.IsValid() ||
|
||
status.TypedSpec().Destination != zeroPrefix {
|
||
continue
|
||
}
|
||
|
||
if gateway.Is4() {
|
||
gatewaysV4 = append(gatewaysV4, gateway.String())
|
||
} else {
|
||
gatewaysV6 = append(gatewaysV6, gateway.String())
|
||
}
|
||
}
|
||
|
||
if len(gatewaysV4) == 0 && len(gatewaysV6) == 0 {
|
||
return notAvailable
|
||
}
|
||
|
||
sort.Strings(gatewaysV4)
|
||
sort.Strings(gatewaysV6)
|
||
|
||
return strings.Join(append(gatewaysV4, gatewaysV6...), ", ")
|
||
}
|
||
|
||
func (widget *NetworkInfo) resolvers(status *network.ResolverStatus) string {
|
||
strs := xslices.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, ", ")
|
||
}
|
||
|
||
func (widget *NetworkInfo) connectivity(status *network.Status) string {
|
||
if status.TypedSpec().ConnectivityReady {
|
||
return "[green]√ OK[-]"
|
||
}
|
||
|
||
return "[red]× FAILED[-]"
|
||
}
|