300 lines
5.7 KiB
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++
|
|
}
|
|
}
|