k9s/internal/watch/no.go

300 lines
5.7 KiB
Go

package watch
import (
"fmt"
"strings"
"github.com/derailed/k9s/internal/k8s"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/watch"
wv1 "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/tools/cache"
)
const (
// NodeIndex marker for stored nodes.
NodeIndex string = "no"
nodeCols = 12
)
// Node tracks node activities.
type Node struct {
cache.SharedIndexInformer
client k8s.Connection
data RowEvents
mxData k8s.NodesMetrics
listener TableListenerFn
}
// NewNode returns a new node.
func NewNode(client k8s.Connection) *Node {
no := Node{
client: client,
data: RowEvents{},
mxData: k8s.NodesMetrics{},
}
if client == nil {
return &no
}
no.SharedIndexInformer = wv1.NewNodeInformer(
client.DialOrDie(),
0,
cache.Indexers{},
)
no.SharedIndexInformer.AddEventHandler(&no)
return &no
}
// List all nodes.
func (n *Node) List(_ string) (k8s.Collection, error) {
var res k8s.Collection
for _, o := range n.GetStore().List() {
res = append(res, o)
}
return res, nil
}
// Get retrieves a given node from store.
func (n *Node) Get(fqn string) (interface{}, error) {
o, ok, err := n.GetStore().GetByKey(fqn)
if err != nil {
return nil, err
}
if !ok {
return nil, fmt.Errorf("Node %s not found", fqn)
}
return o, nil
}
// SetListener registers event recipient.
func (n *Node) SetListener(_ string, cb TableListenerFn) {
n.listener = cb
n.fireChanged()
}
// UnsetListener unregister event recipient.
func (n *Node) UnsetListener(_ string) {
n.listener = nil
}
func (n *Node) fetchMetrics() {
if n.client == nil {
return
}
client := k8s.NewMetricsServer(n.client)
mx, err := client.FetchNodesMetrics()
if err != nil {
log.Error().Err(err).Msg("Node metrics failed")
return
}
client.NodesMetrics(n.GetStore().List(), mx, n.mxData)
}
// Data return current data.
func (n *Node) tableData() TableData {
return TableData{
Header: n.header(),
Rows: n.data,
Namespace: NotNamespaced,
}
}
func (n *Node) fireChanged() {
if cb := n.listener; cb != nil {
cb(n.tableData())
}
}
// OnAdd notify node added.
func (n *Node) OnAdd(obj interface{}) {
no := obj.(*v1.Node)
n.fetchMetrics()
ff := make(Row, nodeCols)
n.fields(no, ff)
fqn := MetaFQN(no.ObjectMeta)
log.Debug().Msgf("Node Added %s", fqn)
n.data[fqn] = &RowEvent{
Action: watch.Added,
Fields: ff,
Deltas: make(Row, len(ff)),
}
if n.HasSynced() {
n.fireChanged()
}
}
// OnUpdate notify node updated.
func (n *Node) OnUpdate(oldObj, newObj interface{}) {
ono, nno := oldObj.(*v1.Node), newObj.(*v1.Node)
k1 := MetaFQN(ono.ObjectMeta)
k2 := MetaFQN(nno.ObjectMeta)
ff := make(Row, nodeCols)
n.fields(nno, ff)
if re, ok := n.data[k1]; ok {
re.Action = watch.Modified
re.Deltas = re.Fields
re.Fields = ff
}
n.data[k2] = &RowEvent{
Action: watch.Added,
Fields: ff,
Deltas: make(Row, len(ff)),
}
n.fireChanged()
}
// OnDelete notify node was deleted.
func (n *Node) OnDelete(obj interface{}) {
po := obj.(*v1.Node)
key := MetaFQN(po.ObjectMeta)
log.Debug().Msgf("Node Delete %s", key)
delete(n.data, key)
n.fireChanged()
}
// Header returns resource header.
func (*Node) header() Row {
return Row{
"NAME",
"STATUS",
"ROLE",
"VERSION",
"KERNEL",
"INTERNAL-IP",
"EXTERNAL-IP",
"CPU",
"MEM",
"ACPU",
"AMEM",
"AGE",
}
}
// Fields returns displayable fields.
func (n *Node) fields(no *v1.Node, r Row) {
col := 0
r[col] = no.Name
col++
sta := make([]string, 10)
n.status(no.Status, no.Spec.Unschedulable, sta)
r[col] = join(sta, ",")
col++
ro := make([]string, 10)
n.nodeRoles(no, ro)
r[col] = join(ro, ",")
col++
r[col] = no.Status.NodeInfo.KubeletVersion
col++
r[col] = no.Status.NodeInfo.KernelVersion
col++
iIP, eIP := n.getIPs(no.Status.Addresses)
iIP, eIP = missing(iIP), missing(eIP)
r[col], r[col+1] = iIP, eIP
col += 2
fqn := MetaFQN(no.ObjectMeta)
n.fetchMetrics()
mx := n.mxData[fqn]
r[col] = withPerc(
ToMillicore(mx.CurrentCPU),
AsPerc(toPerc(float64(mx.CurrentCPU), float64(mx.AvailCPU))),
)
col++
r[col] = withPerc(
ToMi(mx.CurrentMEM),
AsPerc(toPerc(mx.CurrentMEM, mx.AvailMEM)),
)
col++
r[col] = ToMillicore(mx.AvailCPU)
col++
r[col] = ToMi(mx.AvailMEM)
col++
r[col] = toAge(no.ObjectMeta.CreationTimestamp)
}
// ----------------------------------------------------------------------------
// Helpers...
const (
labelNodeRolePrefix = "node-role.kubernetes.io/"
nodeLabelRole = "kubernetes.io/role"
)
func (*Node) nodeRoles(node *v1.Node, res []string) {
index := 0
for k, v := range node.Labels {
switch {
case strings.HasPrefix(k, labelNodeRolePrefix):
if role := strings.TrimPrefix(k, labelNodeRolePrefix); len(role) > 0 {
res[index] = role
index++
}
case k == nodeLabelRole && v != "":
res[index] = v
index++
}
}
if empty(res) {
res[index] = MissingValue
index++
}
}
func (*Node) getIPs(addrs []v1.NodeAddress) (iIP, eIP string) {
for _, a := range addrs {
switch a.Type {
case v1.NodeExternalIP:
eIP = a.Address
case v1.NodeInternalIP:
iIP = a.Address
}
}
return
}
func (*Node) status(status v1.NodeStatus, exempt bool, res []string) {
var index int
conditions := make(map[v1.NodeConditionType]*v1.NodeCondition)
for n := range status.Conditions {
cond := status.Conditions[n]
conditions[cond.Type] = &cond
}
validConditions := []v1.NodeConditionType{v1.NodeReady}
for _, validCondition := range validConditions {
condition, ok := conditions[validCondition]
if !ok {
continue
}
neg := ""
if condition.Status != v1.ConditionTrue {
neg = "Not"
}
res[index] = neg + string(condition.Type)
index++
}
if len(res) == 0 {
res[index] = "Unknown"
index++
}
if exempt {
res[index] = "SchedulingDisabled"
index++
}
}