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 }