github.com/oam-dev/kubevela@v1.9.11/references/cli/top/view/application_topology_view.go (about)

     1  /*
     2  Copyright 2022 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package view
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"time"
    23  
    24  	"github.com/bluele/gcache"
    25  	"github.com/gdamore/tcell/v2"
    26  	"github.com/rivo/tview"
    27  	"sigs.k8s.io/controller-runtime/pkg/client"
    28  
    29  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    30  	"github.com/oam-dev/kubevela/pkg/velaql/providers/query/types"
    31  	"github.com/oam-dev/kubevela/references/cli/top/component"
    32  	"github.com/oam-dev/kubevela/references/cli/top/model"
    33  	clicommon "github.com/oam-dev/kubevela/references/common"
    34  )
    35  
    36  // TopologyView display the resource topology of application
    37  type TopologyView struct {
    38  	*tview.Grid
    39  	app                      *App
    40  	actions                  model.KeyActions
    41  	ctx                      context.Context
    42  	focusTopology            bool
    43  	formatter                *component.TopologyTreeNodeFormatter
    44  	cache                    gcache.Cache // lru cache with expired time
    45  	metricsInstance          *tview.Table
    46  	appTopologyInstance      *TopologyTree
    47  	resourceTopologyInstance *TopologyTree
    48  	cancelFunc               func() // auto refresh cancel function
    49  }
    50  
    51  type cacheView struct {
    52  	appTopologyInstance      *TopologyTree
    53  	resourceTopologyInstance *TopologyTree
    54  }
    55  
    56  // TopologyTree is the abstract of topology tree
    57  type TopologyTree struct {
    58  	*tview.TreeView
    59  }
    60  
    61  const (
    62  	numberOfCacheView = 10
    63  	// cache expire time
    64  	expireTime = 10
    65  	// request timeout time
    66  	topologyReqTimeout = 5
    67  )
    68  
    69  var (
    70  	topologyViewInstance = new(TopologyView)
    71  )
    72  
    73  // NewTopologyView return a new topology view
    74  func NewTopologyView(ctx context.Context, app *App) model.View {
    75  	topologyViewInstance.app = app
    76  	topologyViewInstance.ctx = ctx
    77  
    78  	if topologyViewInstance.Grid == nil {
    79  		topologyViewInstance.Grid = tview.NewGrid()
    80  		topologyViewInstance.actions = make(model.KeyActions)
    81  		topologyViewInstance.formatter = component.NewTopologyTreeNodeFormatter(app.config.Theme)
    82  		topologyViewInstance.cache = gcache.New(numberOfCacheView).LRU().Expiration(expireTime * time.Second).Build()
    83  		topologyViewInstance.metricsInstance = tview.NewTable()
    84  		topologyViewInstance.appTopologyInstance = new(TopologyTree)
    85  		topologyViewInstance.resourceTopologyInstance = new(TopologyTree)
    86  
    87  		topologyViewInstance.Init()
    88  	}
    89  	return topologyViewInstance
    90  }
    91  
    92  // Init the topology view
    93  func (v *TopologyView) Init() {
    94  	v.metricsInstance.SetFixed(2, 6)
    95  	v.metricsInstance.SetBorder(true).SetBorderColor(v.app.config.Theme.Border.Table.Color())
    96  
    97  	title := fmt.Sprintf("[ %s ]", v.Name())
    98  	v.SetRows(0).SetColumns(-1, -1)
    99  	v.SetBorder(true)
   100  	v.SetBorderAttributes(tcell.AttrItalic)
   101  	v.SetBorderColor(v.app.config.Theme.Border.Table.Color())
   102  	v.SetTitle(title)
   103  	v.SetTitleColor(v.app.config.Theme.Table.Title.Color())
   104  	v.bindKeys()
   105  	v.SetInputCapture(v.keyboard)
   106  }
   107  
   108  // Start the topology view
   109  func (v *TopologyView) Start() {
   110  	v.Update(func() {})
   111  	v.AutoRefresh(v.Update)
   112  }
   113  
   114  // Stop the topology view
   115  func (v *TopologyView) Stop() {
   116  	v.Grid.Clear()
   117  	v.cancelFunc()
   118  }
   119  
   120  // Hint return the menu hints of topology view
   121  func (v *TopologyView) Hint() []model.MenuHint {
   122  	return v.actions.Hint()
   123  }
   124  
   125  // Name return the name of topology view
   126  func (v *TopologyView) Name() string {
   127  	return "Topology"
   128  }
   129  
   130  // Update the topology view
   131  func (v *TopologyView) Update(timeoutCancel func()) {
   132  	appName := v.ctx.Value(&model.CtxKeyAppName).(string)
   133  	namespace := v.ctx.Value(&model.CtxKeyNamespace).(string)
   134  	key := fmt.Sprintf("%s-%s", appName, namespace)
   135  
   136  	value, err := v.cache.Get(key)
   137  
   138  	generateTopology := func() {
   139  		v.resourceTopologyInstance = v.NewResourceTopologyView()
   140  		v.appTopologyInstance = v.NewAppTopologyView()
   141  		// add new topology view to cache
   142  		_ = v.cache.Set(key, &cacheView{
   143  			resourceTopologyInstance: v.resourceTopologyInstance,
   144  			appTopologyInstance:      v.appTopologyInstance,
   145  		})
   146  	}
   147  
   148  	if err != nil {
   149  		generateTopology()
   150  	} else {
   151  		view, ok := value.(*cacheView)
   152  		if ok {
   153  			v.appTopologyInstance = view.appTopologyInstance
   154  			v.resourceTopologyInstance = view.resourceTopologyInstance
   155  		} else {
   156  			generateTopology()
   157  		}
   158  	}
   159  	v.updateMetrics(appName, namespace)
   160  
   161  	v.Grid.AddItem(v.metricsInstance, 0, 0, 1, 2, 0, 0, false)
   162  	v.Grid.AddItem(v.appTopologyInstance, 1, 0, 7, 1, 0, 0, true)
   163  	v.Grid.AddItem(v.resourceTopologyInstance, 1, 1, 7, 1, 0, 0, true)
   164  
   165  	// reset focus
   166  	if v.focusTopology {
   167  		v.app.SetFocus(v.resourceTopologyInstance)
   168  	} else {
   169  		v.app.SetFocus(v.appTopologyInstance)
   170  	}
   171  	// ctx done
   172  	timeoutCancel()
   173  }
   174  
   175  func (v *TopologyView) keyboard(event *tcell.EventKey) *tcell.EventKey {
   176  	key := event.Key()
   177  	if key == tcell.KeyUp || key == tcell.KeyDown {
   178  		return event
   179  	}
   180  	if a, ok := v.actions[component.StandardizeKey(event)]; ok {
   181  		return a.Action(event)
   182  	}
   183  	return event
   184  }
   185  
   186  func (v *TopologyView) bindKeys() {
   187  	v.actions.Delete([]tcell.Key{tcell.KeyEnter})
   188  	v.actions.Add(model.KeyActions{
   189  		component.KeyQ:    model.KeyAction{Description: "Back", Action: v.app.Back, Visible: true, Shared: true},
   190  		component.KeyHelp: model.KeyAction{Description: "Help", Action: v.app.helpView, Visible: true, Shared: true},
   191  		tcell.KeyTAB:      model.KeyAction{Description: "Switch", Action: v.switchTopology, Visible: true, Shared: true},
   192  	})
   193  }
   194  
   195  func (v *TopologyView) updateMetrics(appName, namespace string) {
   196  	app := new(v1beta1.Application)
   197  	err := v.app.client.Get(context.Background(), client.ObjectKey{
   198  		Name:      appName,
   199  		Namespace: namespace,
   200  	}, app)
   201  	if err != nil {
   202  		return
   203  	}
   204  
   205  	metrics, err := clicommon.GetApplicationMetrics(v.app.client, v.app.config.RestConfig, app)
   206  	if err != nil {
   207  		return
   208  	}
   209  
   210  	format := "%10s : %10d"
   211  	cell := tview.NewTableCell(fmt.Sprintf(format, "Node",
   212  		metrics.ResourceNum.Node)).SetAlign(tview.AlignLeft).SetExpansion(3)
   213  	v.metricsInstance.SetCell(0, 0, cell)
   214  	cell = tview.NewTableCell(fmt.Sprintf(format, "Cluster", metrics.ResourceNum.Cluster)).SetAlign(tview.AlignLeft).SetExpansion(3)
   215  	v.metricsInstance.SetCell(1, 0, cell)
   216  
   217  	cell = tview.NewTableCell(fmt.Sprintf(format, "Pod", metrics.ResourceNum.Pod)).SetAlign(tview.AlignLeft).SetExpansion(3)
   218  	v.metricsInstance.SetCell(0, 1, cell)
   219  	cell = tview.NewTableCell(fmt.Sprintf(format, "Container", metrics.ResourceNum.Container)).SetAlign(tview.AlignLeft).SetExpansion(3)
   220  	v.metricsInstance.SetCell(1, 1, cell)
   221  
   222  	format = "%20s : %10s"
   223  	cell = tview.NewTableCell(fmt.Sprintf(format, "Managed Resource", fmt.Sprintf("%d", metrics.ResourceNum.Subresource))).SetAlign(tview.AlignLeft).SetExpansion(3)
   224  	v.metricsInstance.SetCell(0, 2, cell)
   225  	cell = tview.NewTableCell(fmt.Sprintf(format, "Storage", fmt.Sprintf("%dGi", metrics.Metrics.Storage/(1024*1024*1024)))).SetAlign(tview.AlignLeft).SetExpansion(3)
   226  	v.metricsInstance.SetCell(1, 2, cell)
   227  
   228  	format = "%10s : %10s"
   229  	cell = tview.NewTableCell(fmt.Sprintf(format, "CPU", fmt.Sprintf("%dm", metrics.Metrics.CPUUsage))).SetAlign(tview.AlignLeft).SetExpansion(3)
   230  	v.metricsInstance.SetCell(0, 3, cell)
   231  	cell = tview.NewTableCell(fmt.Sprintf(format, "Memory", fmt.Sprintf("%dMi", metrics.Metrics.MemoryUsage/(1024*1024)))).SetAlign(tview.AlignLeft).SetExpansion(3)
   232  	v.metricsInstance.SetCell(1, 3, cell)
   233  
   234  	format = "%20s : %10s"
   235  	cell = tview.NewTableCell(fmt.Sprintf(format, "CPU Limit", fmt.Sprintf("%dm", metrics.Metrics.CPULimit))).SetAlign(tview.AlignLeft).SetExpansion(3)
   236  	v.metricsInstance.SetCell(0, 4, cell)
   237  	cell = tview.NewTableCell(fmt.Sprintf(format, "Memory Limit", fmt.Sprintf("%dMi", metrics.Metrics.MemoryLimit/(1024*1024)))).SetAlign(tview.AlignLeft).SetExpansion(3)
   238  	v.metricsInstance.SetCell(1, 4, cell)
   239  
   240  	cell = tview.NewTableCell(fmt.Sprintf(format, "CPU Request", fmt.Sprintf("%dm", metrics.Metrics.CPURequest))).SetAlign(tview.AlignLeft).SetExpansion(3)
   241  	v.metricsInstance.SetCell(0, 5, cell)
   242  	cell = tview.NewTableCell(fmt.Sprintf(format, "Memory Request", fmt.Sprintf("%dMi", metrics.Metrics.MemoryRequest/(1024*1024)))).SetAlign(tview.AlignLeft).SetExpansion(3)
   243  	v.metricsInstance.SetCell(1, 5, cell)
   244  }
   245  
   246  // NewResourceTopologyView return a new resource topology view
   247  func (v *TopologyView) NewResourceTopologyView() *TopologyTree {
   248  	newTopology := new(TopologyTree)
   249  	appName := v.ctx.Value(&model.CtxKeyAppName).(string)
   250  	namespace := v.ctx.Value(&model.CtxKeyNamespace).(string)
   251  
   252  	newTopology.TreeView = tview.NewTreeView()
   253  	newTopology.SetGraphics(true)
   254  	newTopology.SetGraphicsColor(v.app.config.Theme.Topology.Line.Color())
   255  	newTopology.SetBorder(true)
   256  	newTopology.SetBorderColor(v.app.config.Theme.Border.Table.Color())
   257  	newTopology.SetTitle(fmt.Sprintf("[ %s ]", "Resource"))
   258  	newTopology.SetTitleColor(v.app.config.Theme.Table.Title.Color())
   259  
   260  	root := tview.NewTreeNode(v.formatter.EmojiFormat(fmt.Sprintf("%s (%s)", appName, namespace), "app")).SetSelectable(true)
   261  	newTopology.SetRoot(root)
   262  
   263  	resourceTree, err := model.ApplicationResourceTopology(v.app.client, appName, namespace)
   264  	if err == nil {
   265  		for _, resource := range resourceTree {
   266  			root.AddChild(v.buildTopology(resource.ResourceTree))
   267  		}
   268  	}
   269  	return newTopology
   270  }
   271  
   272  // NewAppTopologyView return a new app topology view
   273  func (v *TopologyView) NewAppTopologyView() *TopologyTree {
   274  	newTopology := new(TopologyTree)
   275  	appName := v.ctx.Value(&model.CtxKeyAppName).(string)
   276  	namespace := v.ctx.Value(&model.CtxKeyNamespace).(string)
   277  
   278  	newTopology.TreeView = tview.NewTreeView()
   279  	newTopology.SetGraphics(true)
   280  	newTopology.SetGraphicsColor(v.app.config.Theme.Topology.Line.Color())
   281  	newTopology.SetBorder(true)
   282  	newTopology.SetBorderColor(v.app.config.Theme.Border.Table.Color())
   283  	newTopology.SetTitle(fmt.Sprintf("[ %s ]", "App"))
   284  	newTopology.SetTitleColor(v.app.config.Theme.Table.Title.Color())
   285  
   286  	root := tview.NewTreeNode(v.formatter.EmojiFormat(fmt.Sprintf("%s (%s)", appName, namespace), "app")).SetSelectable(true)
   287  
   288  	newTopology.SetRoot(root)
   289  
   290  	app, err := model.LoadApplication(v.app.client, appName, namespace)
   291  	if err != nil {
   292  		return newTopology
   293  	}
   294  	// workflow
   295  	if app.Status.Workflow != nil {
   296  		workflowNode := tview.NewTreeNode(v.formatter.EmojiFormat("WorkFlow", "workflow")).SetSelectable(true)
   297  		root.AddChild(workflowNode)
   298  		for _, step := range app.Status.Workflow.Steps {
   299  			stepNode := tview.NewTreeNode(component.WorkflowStepFormat(step.Name, step.Phase))
   300  			for _, subStep := range step.SubStepsStatus {
   301  				subStepNode := tview.NewTreeNode(subStep.Name)
   302  				stepNode.AddChild(subStepNode)
   303  			}
   304  			workflowNode.AddChild(stepNode)
   305  		}
   306  	}
   307  
   308  	// component
   309  	componentTitleNode := tview.NewTreeNode(v.formatter.EmojiFormat("Component", "component")).SetSelectable(true)
   310  	root.AddChild(componentTitleNode)
   311  	for _, c := range app.Spec.Components {
   312  		cNode := tview.NewTreeNode(c.Name)
   313  		attrNode := tview.NewTreeNode("Attributes")
   314  		attrNode.AddChild(tview.NewTreeNode(fmt.Sprintf("Type: %s", c.Type)))
   315  		cNode.AddChild(attrNode)
   316  
   317  		if len(c.Traits) > 0 {
   318  			traitTitleNode := tview.NewTreeNode(v.formatter.EmojiFormat("Trait", "trait")).SetSelectable(true)
   319  			cNode.AddChild(traitTitleNode)
   320  			for _, trait := range c.Traits {
   321  				traitNode := tview.NewTreeNode(trait.Type)
   322  				traitTitleNode.AddChild(traitNode)
   323  			}
   324  		}
   325  
   326  		componentTitleNode.AddChild(cNode)
   327  	}
   328  
   329  	// policy
   330  	policyNode := tview.NewTreeNode(v.formatter.EmojiFormat("Policy", "policy")).SetSelectable(true)
   331  	root.AddChild(policyNode)
   332  	for _, policy := range app.Spec.Policies {
   333  		policyNode.AddChild(tview.NewTreeNode(policy.Name))
   334  	}
   335  	return newTopology
   336  }
   337  
   338  func (v *TopologyView) switchTopology(_ *tcell.EventKey) *tcell.EventKey {
   339  	if v.focusTopology {
   340  		v.app.SetFocus(v.appTopologyInstance)
   341  	} else {
   342  		v.app.SetFocus(v.resourceTopologyInstance)
   343  	}
   344  	v.focusTopology = !v.focusTopology
   345  	return nil
   346  }
   347  
   348  func (v *TopologyView) buildTopology(node *types.ResourceTreeNode) *tview.TreeNode {
   349  	if node == nil {
   350  		return tview.NewTreeNode("?")
   351  	}
   352  	rootNode := tview.NewTreeNode(v.formatter.EmojiFormat(node.Name, node.Kind)).SetSelectable(true)
   353  
   354  	attrNode := tview.NewTreeNode("Attributes")
   355  	attrNode.AddChild(tview.NewTreeNode(fmt.Sprintf("Kind: %s", v.formatter.ColorizeKind(node.Kind))))
   356  	attrNode.AddChild(tview.NewTreeNode(fmt.Sprintf("API Version: %s", node.APIVersion)))
   357  	attrNode.AddChild(tview.NewTreeNode(fmt.Sprintf("Namespace: %s", node.Namespace)))
   358  	attrNode.AddChild(tview.NewTreeNode(fmt.Sprintf("Cluster: %s", node.Cluster)))
   359  	attrNode.AddChild(tview.NewTreeNode(fmt.Sprintf("Status: %s", v.formatter.ColorizeStatus(node.HealthStatus.Status))))
   360  
   361  	rootNode.AddChild(attrNode)
   362  	if len(node.LeafNodes) > 0 {
   363  		subNode := tview.NewTreeNode("Sub Resource")
   364  		rootNode.AddChild(subNode)
   365  
   366  		for _, sub := range node.LeafNodes {
   367  			subNode.AddChild(v.buildTopology(sub))
   368  		}
   369  	}
   370  
   371  	return rootNode
   372  }
   373  
   374  // Refresh the topology  view
   375  func (v *TopologyView) Refresh(clear bool, update func(timeoutCancel func())) {
   376  	if clear {
   377  		v.Grid.Clear()
   378  	}
   379  
   380  	updateWithTimeout := func() {
   381  		ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*topologyReqTimeout)
   382  		defer cancelFunc()
   383  		go update(cancelFunc)
   384  
   385  		select {
   386  		case <-time.After(time.Second * topologyReqTimeout): // timeout
   387  		case <-ctx.Done(): // success
   388  		}
   389  	}
   390  
   391  	v.app.QueueUpdateDraw(updateWithTimeout)
   392  }
   393  
   394  // AutoRefresh will refresh the view in every RefreshDelay delay
   395  func (v *TopologyView) AutoRefresh(update func(timeoutCancel func())) {
   396  	var ctx context.Context
   397  	ctx, v.cancelFunc = context.WithCancel(context.Background())
   398  	go func() {
   399  		for {
   400  			time.Sleep(RefreshDelay * time.Second)
   401  			select {
   402  			case <-ctx.Done():
   403  				return
   404  			default:
   405  				v.Refresh(true, update)
   406  			}
   407  		}
   408  	}()
   409  }