Merge branch 'health'
commit
9e5da1ef45
|
|
@ -14,3 +14,4 @@ gen.sh
|
||||||
*.log
|
*.log
|
||||||
*~
|
*~
|
||||||
faas
|
faas
|
||||||
|
demos
|
||||||
|
|
@ -16,6 +16,7 @@ builds:
|
||||||
- 386
|
- 386
|
||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
|
- armhf
|
||||||
goarm:
|
goarm:
|
||||||
- 6
|
- 6
|
||||||
- 7
|
- 7
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 731 KiB |
|
|
@ -0,0 +1,185 @@
|
||||||
|
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
|
||||||
|
|
||||||
|
# Release v0.16.0
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!
|
||||||
|
|
||||||
|
Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
|
||||||
|
|
||||||
|
On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_doc.png" align="center"/>
|
||||||
|
|
||||||
|
This is one of these drops that may make you wonder if you'll go from zero to hero or likely the reverse?? Will see how this goes... Please proceed with caution on this one as there could very well be much distrubances in the force...
|
||||||
|
|
||||||
|
Lots of code churns so could have totally hose some stuff, but like my GranPappy used to say `can't cook without making a mess!`
|
||||||
|
|
||||||
|
## Going Wide?
|
||||||
|
|
||||||
|
In this drop, we've enabled a new shortcut namely `wide` as `Ctrl-w`. On table views, you will be able to see more information about the resources such as labels or others depending on the viewed resource. This mnemonic works as a toggle so you can `narrow` the view by hitting it again.
|
||||||
|
|
||||||
|
## Zoom, Zoom, Zoom!
|
||||||
|
|
||||||
|
While viewing some resources that may contain errors, sorting on columns may not achieve the results you're seeking ie `show me all resources in an error state`. We've added a new option to achieve just that aka `zoom errors` as `ctrl-z`. This works as a toggle and will unveil resources that are need of some TLC on your part ;)
|
||||||
|
|
||||||
|
## Does Your Cluster Have A Pulse 💓?
|
||||||
|
|
||||||
|
In this drop, we're introducing a brand new view aka `K9s Pulses` 💓. This is a summary view listing the most sailient resources in your clusters and their current states. This view tracks two main metrics ie Ok and Toast on a 5sec beat. This view affords cluster activity and failure rates. BTW this is the zero to hero deal 🙀 Hopefully you'll dig it as this was much work to put together and I personally think it's the `ducks nuts`... If you like, please give me some luving on social or via GH sponsors as batteries are running low...
|
||||||
|
|
||||||
|
To active, enter command mode by typing in `:pulse` aliases are `pu`, `pulses` or `hz`
|
||||||
|
To navigate thru the various pulses, you can use `tab`/`backtab` or use the menu index (just like namespaces selectors). Once on a pulse view, you can press `enter` to see the associated resource table view. Pressing `esc` will nav you back.
|
||||||
|
|
||||||
|
As I've may have mentioned before, my front-end/UX FU is weak, so I've also added a way for you to skin the charts via skins yaml to your own liking. Please see the skin section below for an example on how to skin the pulses dials. BONUS you should be able to skin K9s live! How cool is that 😻?
|
||||||
|
|
||||||
|
NOTE: Pulses are very much experimental and could totally bomb on your clusters! So please thread carefully and please do report (kindly!) back.
|
||||||
|
|
||||||
|
## BReaking Bad!
|
||||||
|
|
||||||
|
In this drop I've broken a few things (that I know off...), here is the list as I can recall...
|
||||||
|
|
||||||
|
1. Toggle header aka `my red headed step child`. Key moved (again!) now `Ctrl-e`
|
||||||
|
2. Skin yaml layout CHANGED! Moved table and xray sections under views and added charts section.
|
||||||
|
|
||||||
|
## Skins Updates!
|
||||||
|
|
||||||
|
The skin file format CHANGE! If you are running skins with K9s, please make sure to update your skin file. If not K9s could bomb coming up!
|
||||||
|
|
||||||
|
NOTE: I don't think I'll get around to update all the contributed skins in this repo `skins` dir. If you're looking for a way to help out and are UI inclined, please take a peek and make them cool!
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# my_cluster_skin.yml
|
||||||
|
# Styles...
|
||||||
|
foreground: &foreground "#f8f8f2"
|
||||||
|
background: &background "#282a36"
|
||||||
|
current_line: ¤t_line "#44475a"
|
||||||
|
selection: &selection "#44475a"
|
||||||
|
comment: &comment "#6272a4"
|
||||||
|
cyan: &cyan "#8be9fd"
|
||||||
|
green: &green "#50fa7b"
|
||||||
|
orange: &orange "#ffb86c"
|
||||||
|
pink: &pink "#ff79c6"
|
||||||
|
purple: &purple "#bd93f9"
|
||||||
|
red: &red "#ff5555"
|
||||||
|
yellow: &yellow "#f1fa8c"
|
||||||
|
|
||||||
|
# Skin...
|
||||||
|
k9s:
|
||||||
|
# General K9s styles
|
||||||
|
body:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
logoColor: *purple
|
||||||
|
# ClusterInfoView styles.
|
||||||
|
info:
|
||||||
|
fgColor: *pink
|
||||||
|
sectionColor: *foreground
|
||||||
|
frame:
|
||||||
|
# Borders styles.
|
||||||
|
border:
|
||||||
|
fgColor: *selection
|
||||||
|
focusColor: *current_line
|
||||||
|
menu:
|
||||||
|
fgColor: *foreground
|
||||||
|
keyColor: *pink
|
||||||
|
# Used for favorite namespaces
|
||||||
|
numKeyColor: *purple
|
||||||
|
# CrumbView attributes for history navigation.
|
||||||
|
crumbs:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *current_line
|
||||||
|
activeColor: *current_line
|
||||||
|
# Resource status and update styles
|
||||||
|
status:
|
||||||
|
newColor: *cyan
|
||||||
|
modifyColor: *purple
|
||||||
|
addColor: *green
|
||||||
|
errorColor: *red
|
||||||
|
highlightcolor: *orange
|
||||||
|
killColor: *comment
|
||||||
|
completedColor: *comment
|
||||||
|
# Border title styles.
|
||||||
|
title:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *current_line
|
||||||
|
highlightColor: *orange
|
||||||
|
counterColor: *purple
|
||||||
|
filterColor: *pink
|
||||||
|
views:
|
||||||
|
charts:
|
||||||
|
bgColor: *background
|
||||||
|
dialBgColor: "#0A2239"
|
||||||
|
chartBgColor: "#0A2239"
|
||||||
|
defaultDialColors:
|
||||||
|
- "#1E3888"
|
||||||
|
- "#820101"
|
||||||
|
defaultChartColors:
|
||||||
|
- "#1E3888"
|
||||||
|
- "#820101"
|
||||||
|
resourceColors:
|
||||||
|
batch/v1/jobs:
|
||||||
|
- "#5D737E"
|
||||||
|
- "#820101"
|
||||||
|
v1/persistentvolumes:
|
||||||
|
- "#3E554A"
|
||||||
|
- "#820101"
|
||||||
|
cpu:
|
||||||
|
- "#6EA4BF"
|
||||||
|
- "#820101"
|
||||||
|
mem:
|
||||||
|
- "#17505B"
|
||||||
|
- "#820101"
|
||||||
|
v1/events:
|
||||||
|
- "#073B3A"
|
||||||
|
- "#820101"
|
||||||
|
v1/pods:
|
||||||
|
- "#487FFF"
|
||||||
|
- "#820101"
|
||||||
|
# TableView attributes.
|
||||||
|
table:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
cursorColor: *current_line
|
||||||
|
# Header row styles.
|
||||||
|
header:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
sorterColor: *cyan
|
||||||
|
# Xray view attributes.
|
||||||
|
xray:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
cursorColor: *current_line
|
||||||
|
graphicColor: *purple
|
||||||
|
showIcons: true
|
||||||
|
# YAML info styles.
|
||||||
|
yaml:
|
||||||
|
keyColor: *pink
|
||||||
|
colonColor: *purple
|
||||||
|
valueColor: *foreground
|
||||||
|
# Logs styles.
|
||||||
|
logs:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resolved Bugs/Features/PRs
|
||||||
|
|
||||||
|
- [Issue #557](https://github.com/derailed/k9s/issues/557)
|
||||||
|
- [Issue #555](https://github.com/derailed/k9s/issues/555)
|
||||||
|
- [Issue #554](https://github.com/derailed/k9s/issues/554)
|
||||||
|
- [Issue #553](https://github.com/derailed/k9s/issues/553)
|
||||||
|
- [Issue #552](https://github.com/derailed/k9s/issues/552)
|
||||||
|
- [Issue #551](https://github.com/derailed/k9s/issues/551)
|
||||||
|
- [Issue #549](https://github.com/derailed/k9s/issues/549) A start with pulses...
|
||||||
|
- [Issue #540](https://github.com/derailed/k9s/issues/540)
|
||||||
|
- [Issue #421](https://github.com/derailed/k9s/issues/421)
|
||||||
|
- [Issue #351](https://github.com/derailed/k9s/issues/351) Solved by Pulses?
|
||||||
|
- [Issue #25](https://github.com/derailed/k9s/issues/25) Pulses? Oldie but goodie!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
4
go.mod
4
go.mod
|
|
@ -43,14 +43,14 @@ require (
|
||||||
github.com/gdamore/tcell v1.3.0
|
github.com/gdamore/tcell v1.3.0
|
||||||
github.com/ghodss/yaml v1.0.0
|
github.com/ghodss/yaml v1.0.0
|
||||||
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
|
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.5
|
github.com/mattn/go-runewidth v0.0.5
|
||||||
github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec
|
github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec
|
||||||
github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c
|
github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c
|
||||||
github.com/openfaas/faas-provider v0.15.0
|
github.com/openfaas/faas-provider v0.15.0
|
||||||
github.com/petergtz/pegomock v2.6.0+incompatible
|
github.com/petergtz/pegomock v2.6.0+incompatible
|
||||||
github.com/rakyll/hey v0.1.2
|
github.com/rakyll/hey v0.1.2
|
||||||
github.com/rs/zerolog v1.17.2
|
github.com/rivo/tview v0.0.0-20191018115645-bacbf5155bc1
|
||||||
|
github.com/rs/zerolog v1.18.0
|
||||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||||
github.com/sahilm/fuzzy v0.1.0
|
github.com/sahilm/fuzzy v0.1.0
|
||||||
github.com/spf13/cobra v0.0.5
|
github.com/spf13/cobra v0.0.5
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -483,6 +483,7 @@ github.com/mohae/deepcopy v0.0.0-20170603005431-491d3605edfb/go.mod h1:TaXosZuwd
|
||||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
|
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
|
||||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/mrunalp/fileutils v0.0.0-20160930181131-4ee1cc9a8058/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0=
|
github.com/mrunalp/fileutils v0.0.0-20160930181131-4ee1cc9a8058/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0=
|
||||||
|
github.com/mum4k/termdash v0.10.0/go.mod h1:l3tO+lJi9LZqXRq7cu7h5/8rDIK3AzelSuq2v/KncxI=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d h1:7PxY7LVfSZm7PEeBTyK1rj1gABdCO2mbri6GKO1cMDs=
|
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d h1:7PxY7LVfSZm7PEeBTyK1rj1gABdCO2mbri6GKO1cMDs=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU=
|
github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU=
|
||||||
|
|
@ -565,6 +566,7 @@ github.com/quobyte/api v0.1.2/go.mod h1:jL7lIHrmqQ7yh05OJ+eEEdHr0u/kmT1Ff9iHd+4H
|
||||||
github.com/rakyll/hey v0.1.2 h1:XlGaKcBdmXJaPImiTnE+TGLDUWQ2toYuHCwdrylLjmg=
|
github.com/rakyll/hey v0.1.2 h1:XlGaKcBdmXJaPImiTnE+TGLDUWQ2toYuHCwdrylLjmg=
|
||||||
github.com/rakyll/hey v0.1.2/go.mod h1:S5M+++KwbmxA7w68S92B5NdWiCB+cIhITaMUkq9W608=
|
github.com/rakyll/hey v0.1.2/go.mod h1:S5M+++KwbmxA7w68S92B5NdWiCB+cIhITaMUkq9W608=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
|
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
|
||||||
|
github.com/rivo/tview v0.0.0-20191018115645-bacbf5155bc1 h1:s9Lw4phBWkuQJUd+msaBMxP3utLvrFaBQV9jNgG55r0=
|
||||||
github.com/rivo/tview v0.0.0-20191018115645-bacbf5155bc1/go.mod h1:+rKjP5+h9HMwWRpAfhIkkQ9KE3m3Nz5rwn7YtUpwgqk=
|
github.com/rivo/tview v0.0.0-20191018115645-bacbf5155bc1/go.mod h1:+rKjP5+h9HMwWRpAfhIkkQ9KE3m3Nz5rwn7YtUpwgqk=
|
||||||
github.com/rivo/uniseg v0.0.0-20190513083848-b9f5b9457d44/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.0.0-20190513083848-b9f5b9457d44/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
||||||
|
|
@ -575,6 +577,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||||
github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo=
|
github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo=
|
||||||
github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
|
github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
|
||||||
|
github.com/rs/zerolog v1.18.0 h1:CbAm3kP2Tptby1i9sYy2MGRg0uxIN9cyDb59Ys7W8z8=
|
||||||
|
github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
|
||||||
github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto=
|
github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto=
|
||||||
github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package client
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
@ -177,13 +178,31 @@ func (c *Config) ClusterNames() ([]string, error) {
|
||||||
|
|
||||||
// CurrentGroupNames retrieves the active group names.
|
// CurrentGroupNames retrieves the active group names.
|
||||||
func (c *Config) CurrentGroupNames() ([]string, error) {
|
func (c *Config) CurrentGroupNames() ([]string, error) {
|
||||||
if c.flags.ImpersonateGroup != nil && len(*c.flags.ImpersonateGroup) != 0 {
|
if areSet(c.flags.ImpersonateGroup) {
|
||||||
return *c.flags.ImpersonateGroup, nil
|
return *c.flags.ImpersonateGroup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return []string{}, errors.New("unable to locate current group")
|
return []string{}, errors.New("unable to locate current group")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImpersonateGroups retrieves the active groupsif set on the CLI.
|
||||||
|
func (c *Config) ImpersonateGroups() (string, error) {
|
||||||
|
if areSet(c.flags.ImpersonateGroup) {
|
||||||
|
return strings.Join(*c.flags.ImpersonateGroup, ","), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("no groups set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImpersonateUser retrieves the active user name if set on the CLI.
|
||||||
|
func (c *Config) ImpersonateUser() (string, error) {
|
||||||
|
if isSet(c.flags.Impersonate) {
|
||||||
|
return *c.flags.Impersonate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("no user set")
|
||||||
|
}
|
||||||
|
|
||||||
// CurrentUserName retrieves the active user name.
|
// CurrentUserName retrieves the active user name.
|
||||||
func (c *Config) CurrentUserName() (string, error) {
|
func (c *Config) CurrentUserName() (string, error) {
|
||||||
if isSet(c.flags.Impersonate) {
|
if isSet(c.flags.Impersonate) {
|
||||||
|
|
@ -311,3 +330,7 @@ func (c *Config) ensureConfig() {
|
||||||
func isSet(s *string) bool {
|
func isSet(s *string) bool {
|
||||||
return s != nil && len(*s) != 0
|
return s != nil && len(*s) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func areSet(s *[]string) bool {
|
||||||
|
return s != nil && len(*s) != 0
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,46 @@ package client
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/cache"
|
||||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
mxCacheSize = 100
|
||||||
|
mxCacheExpiry = 1 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
var MetricsDial *MetricsServer
|
||||||
|
|
||||||
|
func DialMetrics(c Connection) *MetricsServer {
|
||||||
|
if MetricsDial == nil {
|
||||||
|
MetricsDial = NewMetricsServer(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MetricsDial
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResetMetrics() {
|
||||||
|
MetricsDial = nil
|
||||||
|
}
|
||||||
|
|
||||||
// MetricsServer serves cluster metrics for nodes and pods.
|
// MetricsServer serves cluster metrics for nodes and pods.
|
||||||
type MetricsServer struct {
|
type MetricsServer struct {
|
||||||
Connection
|
Connection
|
||||||
|
|
||||||
|
cache *cache.LRUExpireCache
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMetricsServer return a metric server instance.
|
// NewMetricsServer return a metric server instance.
|
||||||
func NewMetricsServer(c Connection) *MetricsServer {
|
func NewMetricsServer(c Connection) *MetricsServer {
|
||||||
return &MetricsServer{Connection: c}
|
return &MetricsServer{
|
||||||
|
Connection: c,
|
||||||
|
cache: cache.NewLRUExpireCache(mxCacheSize),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodesMetrics retrieves metrics for a given set of nodes.
|
// NodesMetrics retrieves metrics for a given set of nodes.
|
||||||
|
|
@ -28,15 +54,15 @@ func (m *MetricsServer) NodesMetrics(nodes *v1.NodeList, metrics *mv1beta1.NodeM
|
||||||
for _, no := range nodes.Items {
|
for _, no := range nodes.Items {
|
||||||
mmx[no.Name] = NodeMetrics{
|
mmx[no.Name] = NodeMetrics{
|
||||||
AvailCPU: no.Status.Allocatable.Cpu().MilliValue(),
|
AvailCPU: no.Status.Allocatable.Cpu().MilliValue(),
|
||||||
AvailMEM: toMB(no.Status.Allocatable.Memory().Value()),
|
AvailMEM: ToMB(no.Status.Allocatable.Memory().Value()),
|
||||||
TotalCPU: no.Status.Capacity.Cpu().MilliValue(),
|
TotalCPU: no.Status.Capacity.Cpu().MilliValue(),
|
||||||
TotalMEM: toMB(no.Status.Capacity.Memory().Value()),
|
TotalMEM: ToMB(no.Status.Capacity.Memory().Value()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, c := range metrics.Items {
|
for _, c := range metrics.Items {
|
||||||
if mx, ok := mmx[c.Name]; ok {
|
if mx, ok := mmx[c.Name]; ok {
|
||||||
mx.CurrentCPU = c.Usage.Cpu().MilliValue()
|
mx.CurrentCPU = c.Usage.Cpu().MilliValue()
|
||||||
mx.CurrentMEM = toMB(c.Usage.Memory().Value())
|
mx.CurrentMEM = ToMB(c.Usage.Memory().Value())
|
||||||
mmx[c.Name] = mx
|
mmx[c.Name] = mx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -51,13 +77,13 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
|
||||||
for _, no := range nos.Items {
|
for _, no := range nos.Items {
|
||||||
nodeMetrics[no.Name] = NodeMetrics{
|
nodeMetrics[no.Name] = NodeMetrics{
|
||||||
AvailCPU: no.Status.Allocatable.Cpu().MilliValue(),
|
AvailCPU: no.Status.Allocatable.Cpu().MilliValue(),
|
||||||
AvailMEM: toMB(no.Status.Allocatable.Memory().Value()),
|
AvailMEM: ToMB(no.Status.Allocatable.Memory().Value()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, mx := range nmx.Items {
|
for _, mx := range nmx.Items {
|
||||||
if m, ok := nodeMetrics[mx.Name]; ok {
|
if m, ok := nodeMetrics[mx.Name]; ok {
|
||||||
m.CurrentCPU = mx.Usage.Cpu().MilliValue()
|
m.CurrentCPU = mx.Usage.Cpu().MilliValue()
|
||||||
m.CurrentMEM = toMB(mx.Usage.Memory().Value())
|
m.CurrentMEM = ToMB(mx.Usage.Memory().Value())
|
||||||
nodeMetrics[mx.Name] = m
|
nodeMetrics[mx.Name] = m
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -74,86 +100,121 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchNodesMetrics return all metrics for pods in a given namespace.
|
func (m *MetricsServer) checkAccess(ns, gvr, msg string) error {
|
||||||
func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
|
|
||||||
var mx mv1beta1.NodeMetricsList
|
|
||||||
if !m.HasMetrics() {
|
if !m.HasMetrics() {
|
||||||
return &mx, fmt.Errorf("No metrics-server detected on cluster")
|
return fmt.Errorf("No metrics-server detected on cluster")
|
||||||
}
|
}
|
||||||
|
|
||||||
auth, err := m.CanI("", "metrics.k8s.io/v1beta1/nodes", ListAccess)
|
auth, err := m.CanI(ns, gvr, ListAccess)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &mx, err
|
return err
|
||||||
}
|
}
|
||||||
if !auth {
|
if !auth {
|
||||||
return &mx, fmt.Errorf("user is not authorized to list node metrics")
|
return fmt.Errorf(msg)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchNodesMetrics return all metrics for nodes.
|
||||||
|
func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
|
||||||
|
const msg = "user is not authorized to list node metrics"
|
||||||
|
|
||||||
|
mx := new(mv1beta1.NodeMetricsList)
|
||||||
|
if err := m.checkAccess("", "metrics.k8s.io/v1beta1/nodes", msg); err != nil {
|
||||||
|
return mx, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = "nodes"
|
||||||
|
if entry, ok := m.cache.Get(key); ok && entry != nil {
|
||||||
|
mxList, ok := entry.(*mv1beta1.NodeMetricsList)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("expected nodemetricslist but got %T", entry)
|
||||||
|
}
|
||||||
|
return mxList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := m.MXDial()
|
client, err := m.MXDial()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &mx, err
|
return mx, err
|
||||||
}
|
}
|
||||||
return client.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{})
|
mxList, err := client.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return mx, err
|
||||||
|
}
|
||||||
|
m.cache.Add(key, mxList, mxCacheExpiry)
|
||||||
|
|
||||||
|
return mxList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchPodsMetrics return all metrics for pods in a given namespace.
|
// FetchPodsMetrics return all metrics for pods in a given namespace.
|
||||||
func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, error) {
|
func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, error) {
|
||||||
var mx mv1beta1.PodMetricsList
|
mx := new(mv1beta1.PodMetricsList)
|
||||||
if m.Connection == nil {
|
const msg = "user is not authorized to list pods metrics"
|
||||||
return &mx, fmt.Errorf("no client connection")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !m.HasMetrics() {
|
|
||||||
return &mx, fmt.Errorf("No metrics-server detected on cluster")
|
|
||||||
}
|
|
||||||
if ns == NamespaceAll {
|
if ns == NamespaceAll {
|
||||||
ns = AllNamespaces
|
ns = AllNamespaces
|
||||||
}
|
}
|
||||||
|
if err := m.checkAccess(ns, "metrics.k8s.io/v1beta1/pods", msg); err != nil {
|
||||||
auth, err := m.CanI(ns, "metrics.k8s.io/v1beta1/pods", ListAccess)
|
return mx, err
|
||||||
if err != nil {
|
|
||||||
return &mx, err
|
|
||||||
}
|
}
|
||||||
if !auth {
|
|
||||||
return &mx, fmt.Errorf("user is not authorized to list pods metrics")
|
key := FQN(ns, "pods")
|
||||||
|
if entry, ok := m.cache.Get(key); ok {
|
||||||
|
mxList, ok := entry.(*mv1beta1.PodMetricsList)
|
||||||
|
if !ok {
|
||||||
|
return mx, fmt.Errorf("expected podmetricslist but got %T", entry)
|
||||||
|
}
|
||||||
|
return mxList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := m.MXDial()
|
client, err := m.MXDial()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &mx, err
|
return mx, err
|
||||||
}
|
}
|
||||||
|
mxList, err := client.MetricsV1beta1().PodMetricses(ns).List(metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return mx, err
|
||||||
|
}
|
||||||
|
m.cache.Add(key, mxList, mxCacheExpiry)
|
||||||
|
|
||||||
return client.MetricsV1beta1().PodMetricses(ns).List(metav1.ListOptions{})
|
return mxList, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchPodMetrics return all metrics for pods in a given namespace.
|
// FetchPodMetrics return all metrics for pods in a given namespace.
|
||||||
func (m *MetricsServer) FetchPodMetrics(fqn string) (*mv1beta1.PodMetrics, error) {
|
func (m *MetricsServer) FetchPodMetrics(fqn string) (*mv1beta1.PodMetrics, error) {
|
||||||
var mx mv1beta1.PodMetrics
|
var mx *mv1beta1.PodMetrics
|
||||||
if m.Connection == nil {
|
const msg = "user is not authorized to list pod metrics"
|
||||||
return &mx, fmt.Errorf("no client connection")
|
|
||||||
}
|
|
||||||
if !m.HasMetrics() {
|
|
||||||
return &mx, fmt.Errorf("No metrics-server detected on cluster")
|
|
||||||
}
|
|
||||||
|
|
||||||
ns, n := Namespaced(fqn)
|
ns, n := Namespaced(fqn)
|
||||||
if ns == NamespaceAll {
|
if ns == NamespaceAll {
|
||||||
ns = AllNamespaces
|
ns = AllNamespaces
|
||||||
}
|
}
|
||||||
auth, err := m.CanI(ns, "metrics.k8s.io/v1beta1/pods", GetAccess)
|
if err := m.checkAccess(ns, "metrics.k8s.io/v1beta1/pods", msg); err != nil {
|
||||||
if err != nil {
|
return mx, err
|
||||||
return &mx, err
|
|
||||||
}
|
}
|
||||||
if !auth {
|
|
||||||
return &mx, fmt.Errorf("user is not authorized to list pod metrics")
|
var key = FQN(ns, "pods")
|
||||||
|
if entry, ok := m.cache.Get(key); ok {
|
||||||
|
if list, ok := entry.(*mv1beta1.PodMetricsList); ok && list != nil {
|
||||||
|
for _, m := range list.Items {
|
||||||
|
if FQN(m.Namespace, m.Name) == fqn {
|
||||||
|
return &m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := m.MXDial()
|
client, err := m.MXDial()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &mx, err
|
return mx, err
|
||||||
}
|
}
|
||||||
|
mx, err = client.MetricsV1beta1().PodMetricses(ns).Get(n, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return mx, err
|
||||||
|
}
|
||||||
|
m.cache.Add(key, mx, mxCacheExpiry)
|
||||||
|
|
||||||
return client.MetricsV1beta1().PodMetricses(ns).Get(n, metav1.GetOptions{})
|
return mx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PodsMetrics retrieves metrics for all pods in a given namespace.
|
// PodsMetrics retrieves metrics for all pods in a given namespace.
|
||||||
|
|
@ -167,7 +228,7 @@ func (m *MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetri
|
||||||
var mx PodMetrics
|
var mx PodMetrics
|
||||||
for _, c := range p.Containers {
|
for _, c := range p.Containers {
|
||||||
mx.CurrentCPU += c.Usage.Cpu().MilliValue()
|
mx.CurrentCPU += c.Usage.Cpu().MilliValue()
|
||||||
mx.CurrentMEM += toMB(c.Usage.Memory().Value())
|
mx.CurrentMEM += ToMB(c.Usage.Memory().Value())
|
||||||
}
|
}
|
||||||
mmx[p.Namespace+"/"+p.Name] = mx
|
mmx[p.Namespace+"/"+p.Name] = mx
|
||||||
}
|
}
|
||||||
|
|
@ -178,8 +239,8 @@ func (m *MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetri
|
||||||
|
|
||||||
const megaByte = 1024 * 1024
|
const megaByte = 1024 * 1024
|
||||||
|
|
||||||
// toMB converts bytes to megabytes.
|
// ToMB converts bytes to megabytes.
|
||||||
func toMB(v int64) float64 {
|
func ToMB(v int64) float64 {
|
||||||
return float64(v) / megaByte
|
return float64(v) / megaByte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ const (
|
||||||
|
|
||||||
// ClusterScope designates a resource is not namespaced.
|
// ClusterScope designates a resource is not namespaced.
|
||||||
ClusterScope = "-"
|
ClusterScope = "-"
|
||||||
|
|
||||||
|
// NotNamespaced designates a non resource namespace.
|
||||||
|
NotNamespaced = "*"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
||||||
|
|
@ -31,65 +31,6 @@ func NewAliases() *Aliases {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Aliases) loadDefaults() {
|
|
||||||
const (
|
|
||||||
contexts = "contexts"
|
|
||||||
portFwds = "portforwards"
|
|
||||||
benchmarks = "benchmarks"
|
|
||||||
dumps = "screendumps"
|
|
||||||
groups = "groups"
|
|
||||||
users = "users"
|
|
||||||
)
|
|
||||||
|
|
||||||
a.mx.Lock()
|
|
||||||
defer a.mx.Unlock()
|
|
||||||
|
|
||||||
a.Alias["dp"] = "apps/v1/deployments"
|
|
||||||
a.Alias["sec"] = "v1/secrets"
|
|
||||||
a.Alias["jo"] = "batch/v1/jobs"
|
|
||||||
a.Alias["cr"] = "rbac.authorization.k8s.io/v1/clusterroles"
|
|
||||||
a.Alias["crb"] = "rbac.authorization.k8s.io/v1/clusterrolebindings"
|
|
||||||
a.Alias["ro"] = "rbac.authorization.k8s.io/v1/roles"
|
|
||||||
a.Alias["rb"] = "rbac.authorization.k8s.io/v1/rolebindings"
|
|
||||||
a.Alias["np"] = "networking.k8s.io/v1/networkpolicies"
|
|
||||||
{
|
|
||||||
a.Alias["ctx"] = contexts
|
|
||||||
a.Alias[contexts] = contexts
|
|
||||||
a.Alias["context"] = contexts
|
|
||||||
}
|
|
||||||
{
|
|
||||||
a.Alias["usr"] = users
|
|
||||||
a.Alias[users] = users
|
|
||||||
a.Alias["user"] = users
|
|
||||||
}
|
|
||||||
{
|
|
||||||
a.Alias["grp"] = groups
|
|
||||||
a.Alias["group"] = groups
|
|
||||||
a.Alias[groups] = groups
|
|
||||||
}
|
|
||||||
{
|
|
||||||
a.Alias["pf"] = portFwds
|
|
||||||
a.Alias[portFwds] = portFwds
|
|
||||||
a.Alias["portforward"] = portFwds
|
|
||||||
}
|
|
||||||
{
|
|
||||||
a.Alias["be"] = benchmarks
|
|
||||||
a.Alias["benchmark"] = benchmarks
|
|
||||||
a.Alias[benchmarks] = benchmarks
|
|
||||||
}
|
|
||||||
{
|
|
||||||
a.Alias["sd"] = dumps
|
|
||||||
a.Alias["screendump"] = dumps
|
|
||||||
a.Alias[dumps] = dumps
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load K9s aliases.
|
|
||||||
func (a *Aliases) Load() error {
|
|
||||||
a.loadDefaults()
|
|
||||||
return a.LoadAliases(K9sAlias)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShortNames return all shortnames.
|
// ShortNames return all shortnames.
|
||||||
func (a *Aliases) ShortNames() ShortNames {
|
func (a *Aliases) ShortNames() ShortNames {
|
||||||
a.mx.RLock()
|
a.mx.RLock()
|
||||||
|
|
@ -139,8 +80,14 @@ func (a *Aliases) Define(gvr string, aliases ...string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadAliases loads alias from a given file.
|
// Load K9s aliases.
|
||||||
func (a *Aliases) LoadAliases(path string) error {
|
func (a *Aliases) Load() error {
|
||||||
|
a.loadDefaultAliases()
|
||||||
|
return a.LoadFileAliases(K9sAlias)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFileAliases loads alias from a given file.
|
||||||
|
func (a *Aliases) LoadFileAliases(path string) error {
|
||||||
f, err := ioutil.ReadFile(path)
|
f, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Err(err).Msgf("No custom aliases found")
|
log.Debug().Err(err).Msgf("No custom aliases found")
|
||||||
|
|
@ -161,6 +108,63 @@ func (a *Aliases) LoadAliases(path string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Aliases) loadDefaultAliases() {
|
||||||
|
a.mx.Lock()
|
||||||
|
defer a.mx.Unlock()
|
||||||
|
|
||||||
|
a.Alias["dp"] = "apps/v1/deployments"
|
||||||
|
a.Alias["sec"] = "v1/secrets"
|
||||||
|
a.Alias["jo"] = "batch/v1/jobs"
|
||||||
|
a.Alias["cr"] = "rbac.authorization.k8s.io/v1/clusterroles"
|
||||||
|
a.Alias["crb"] = "rbac.authorization.k8s.io/v1/clusterrolebindings"
|
||||||
|
a.Alias["ro"] = "rbac.authorization.k8s.io/v1/roles"
|
||||||
|
a.Alias["rb"] = "rbac.authorization.k8s.io/v1/rolebindings"
|
||||||
|
a.Alias["np"] = "networking.k8s.io/v1/networkpolicies"
|
||||||
|
|
||||||
|
const contexts = "contexts"
|
||||||
|
{
|
||||||
|
a.Alias["ctx"] = contexts
|
||||||
|
a.Alias[contexts] = contexts
|
||||||
|
a.Alias["context"] = contexts
|
||||||
|
}
|
||||||
|
const users = "users"
|
||||||
|
{
|
||||||
|
a.Alias["usr"] = users
|
||||||
|
a.Alias[users] = users
|
||||||
|
a.Alias["user"] = users
|
||||||
|
}
|
||||||
|
const groups = "groups"
|
||||||
|
{
|
||||||
|
a.Alias["grp"] = groups
|
||||||
|
a.Alias["group"] = groups
|
||||||
|
a.Alias[groups] = groups
|
||||||
|
}
|
||||||
|
const portFwds = "portforwards"
|
||||||
|
{
|
||||||
|
a.Alias["pf"] = portFwds
|
||||||
|
a.Alias[portFwds] = portFwds
|
||||||
|
a.Alias["portforward"] = portFwds
|
||||||
|
}
|
||||||
|
const benchmarks = "benchmarks"
|
||||||
|
{
|
||||||
|
a.Alias["be"] = benchmarks
|
||||||
|
a.Alias["benchmark"] = benchmarks
|
||||||
|
a.Alias[benchmarks] = benchmarks
|
||||||
|
}
|
||||||
|
const dumps = "screendumps"
|
||||||
|
{
|
||||||
|
a.Alias["sd"] = dumps
|
||||||
|
a.Alias["screendump"] = dumps
|
||||||
|
a.Alias[dumps] = dumps
|
||||||
|
}
|
||||||
|
const pulses = "pulses"
|
||||||
|
{
|
||||||
|
a.Alias["hz"] = pulses
|
||||||
|
a.Alias["pu"] = pulses
|
||||||
|
a.Alias["pulse"] = pulses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save alias to disk.
|
// Save alias to disk.
|
||||||
func (a *Aliases) Save() error {
|
func (a *Aliases) Save() error {
|
||||||
log.Debug().Msg("[Config] Saving Aliases...")
|
log.Debug().Msg("[Config] Saving Aliases...")
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ func TestAliasDefine(t *testing.T) {
|
||||||
func TestAliasesLoad(t *testing.T) {
|
func TestAliasesLoad(t *testing.T) {
|
||||||
a := config.NewAliases()
|
a := config.NewAliases()
|
||||||
|
|
||||||
assert.Nil(t, a.LoadAliases("testdata/alias.yml"))
|
assert.Nil(t, a.LoadFileAliases("testdata/alias.yml"))
|
||||||
assert.Equal(t, 2, len(a.Alias))
|
assert.Equal(t, 2, len(a.Alias))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,6 +82,6 @@ func TestAliasesSave(t *testing.T) {
|
||||||
a.Alias["blee"] = "duh"
|
a.Alias["blee"] = "duh"
|
||||||
|
|
||||||
assert.Nil(t, a.SaveAliases("/tmp/a.yml"))
|
assert.Nil(t, a.SaveAliases("/tmp/a.yml"))
|
||||||
assert.Nil(t, a.LoadAliases("/tmp/a.yml"))
|
assert.Nil(t, a.LoadFileAliases("/tmp/a.yml"))
|
||||||
assert.Equal(t, 2, len(a.Alias))
|
assert.Equal(t, 2, len(a.Alias))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,31 @@ type StyleListener interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
// Color represents a color.
|
||||||
|
Color string
|
||||||
|
|
||||||
|
// Colors tracks multiple colors.
|
||||||
|
Colors []Color
|
||||||
|
|
||||||
// Styles tracks K9s styling options.
|
// Styles tracks K9s styling options.
|
||||||
Styles struct {
|
Styles struct {
|
||||||
K9s Style `yaml:"k9s"`
|
K9s Style `yaml:"k9s"`
|
||||||
listeners []StyleListener
|
listeners []StyleListener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Style tracks K9s styles.
|
||||||
|
Style struct {
|
||||||
|
Body Body `yaml:"body"`
|
||||||
|
Frame Frame `yaml:"frame"`
|
||||||
|
Info Info `yaml:"info"`
|
||||||
|
Views Views `yaml:"views"`
|
||||||
|
}
|
||||||
|
|
||||||
// Body tracks body styles.
|
// Body tracks body styles.
|
||||||
Body struct {
|
Body struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
BgColor string `yaml:"bgColor"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
LogoColor string `yaml:"logoColor"`
|
LogoColor Color `yaml:"logoColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Frame tracks frame styles.
|
// Frame tracks frame styles.
|
||||||
|
|
@ -45,120 +59,171 @@ type (
|
||||||
|
|
||||||
// Views tracks individual view styles.
|
// Views tracks individual view styles.
|
||||||
Views struct {
|
Views struct {
|
||||||
Yaml Yaml `yaml:"yaml"`
|
Table Table `yaml:"table"`
|
||||||
Log Log `yaml:"logs"`
|
Xray Xray `yaml:"xray"`
|
||||||
|
Charts Charts `yaml:"charts"`
|
||||||
|
Yaml Yaml `yaml:"yaml"`
|
||||||
|
Log Log `yaml:"logs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status tracks resource status styles.
|
// Status tracks resource status styles.
|
||||||
Status struct {
|
Status struct {
|
||||||
NewColor string `yaml:"newColor"`
|
NewColor Color `yaml:"newColor"`
|
||||||
ModifyColor string `yaml:"modifyColor"`
|
ModifyColor Color `yaml:"modifyColor"`
|
||||||
AddColor string `yaml:"addColor"`
|
AddColor Color `yaml:"addColor"`
|
||||||
ErrorColor string `yaml:"errorColor"`
|
ErrorColor Color `yaml:"errorColor"`
|
||||||
HighlightColor string `yaml:"highlightColor"`
|
HighlightColor Color `yaml:"highlightColor"`
|
||||||
KillColor string `yaml:"killColor"`
|
KillColor Color `yaml:"killColor"`
|
||||||
CompletedColor string `yaml:"completedColor"`
|
CompletedColor Color `yaml:"completedColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log tracks Log styles.
|
// Log tracks Log styles.
|
||||||
Log struct {
|
Log struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
BgColor string `yaml:"bgColor"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yaml tracks yaml styles.
|
// Yaml tracks yaml styles.
|
||||||
Yaml struct {
|
Yaml struct {
|
||||||
KeyColor string `yaml:"keyColor"`
|
KeyColor Color `yaml:"keyColor"`
|
||||||
ValueColor string `yaml:"valueColor"`
|
ValueColor Color `yaml:"valueColor"`
|
||||||
ColonColor string `yaml:"colonColor"`
|
ColonColor Color `yaml:"colonColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Title tracks title styles.
|
// Title tracks title styles.
|
||||||
Title struct {
|
Title struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
BgColor string `yaml:"bgColor"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
HighlightColor string `yaml:"highlightColor"`
|
HighlightColor Color `yaml:"highlightColor"`
|
||||||
CounterColor string `yaml:"counterColor"`
|
CounterColor Color `yaml:"counterColor"`
|
||||||
FilterColor string `yaml:"filterColor"`
|
FilterColor Color `yaml:"filterColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info tracks info styles.
|
// Info tracks info styles.
|
||||||
Info struct {
|
Info struct {
|
||||||
SectionColor string `yaml:"sectionColor"`
|
SectionColor Color `yaml:"sectionColor"`
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Border tracks border styles.
|
// ColorBorder tracks border styles.
|
||||||
Border struct {
|
Border struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
FocusColor string `yaml:"focusColor"`
|
FocusColor Color `yaml:"focusColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crumb tracks crumbs styles.
|
// Crumb tracks crumbs styles.
|
||||||
Crumb struct {
|
Crumb struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
BgColor string `yaml:"bgColor"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
ActiveColor string `yaml:"activeColor"`
|
ActiveColor Color `yaml:"activeColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table tracks table styles.
|
// Table tracks table styles.
|
||||||
Table struct {
|
Table struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
BgColor string `yaml:"bgColor"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
CursorColor string `yaml:"cursorColor"`
|
CursorColor Color `yaml:"cursorColor"`
|
||||||
MarkColor string `yaml:"markColor"`
|
MarkColor Color `yaml:"markColor"`
|
||||||
Header TableHeader `yaml:"header"`
|
Header TableHeader `yaml:"header"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableHeader tracks table header styles.
|
// TableHeader tracks table header styles.
|
||||||
TableHeader struct {
|
TableHeader struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
BgColor string `yaml:"bgColor"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
SorterColor string `yaml:"sorterColor"`
|
SorterColor Color `yaml:"sorterColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Xray tracks xray styles.
|
// Xray tracks xray styles.
|
||||||
Xray struct {
|
Xray struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
BgColor string `yaml:"bgColor"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
CursorColor string `yaml:"cursorColor"`
|
CursorColor Color `yaml:"cursorColor"`
|
||||||
GraphicColor string `yaml:"graphicColor"`
|
GraphicColor Color `yaml:"graphicColor"`
|
||||||
ShowIcons bool `yaml:"showIcons"`
|
ShowIcons bool `yaml:"showIcons"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menu tracks menu styles.
|
// Menu tracks menu styles.
|
||||||
Menu struct {
|
Menu struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
KeyColor string `yaml:"keyColor"`
|
KeyColor Color `yaml:"keyColor"`
|
||||||
NumKeyColor string `yaml:"numKeyColor"`
|
NumKeyColor Color `yaml:"numKeyColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Style tracks K9s styles.
|
// Charts tracks charts styles.
|
||||||
Style struct {
|
Charts struct {
|
||||||
Body Body `yaml:"body"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
Frame Frame `yaml:"frame"`
|
DialBgColor Color `yaml:"dialBgColor"`
|
||||||
Info Info `yaml:"info"`
|
ChartBgColor Color `yaml:"chartBgColor"`
|
||||||
Table Table `yaml:"table"`
|
DefaultDialColors Colors `yaml:"defaultDialColors"`
|
||||||
Xray Xray `yaml:"xray"`
|
DefaultChartColors Colors `yaml:"defaultChartColors"`
|
||||||
Views Views `yaml:"views"`
|
ResourceColors map[string]Colors `yaml:"resourceColors"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultColor represents a default color.
|
||||||
|
DefaultColor Color = "default"
|
||||||
|
|
||||||
|
// TransparentColor represents the terminal bg color.
|
||||||
|
TransparentColor Color = "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewColor returns a new color.
|
||||||
|
func NewColor(c string) Color {
|
||||||
|
return Color(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns color as string.
|
||||||
|
func (c Color) String() string {
|
||||||
|
return string(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsColor returns a view color.
|
||||||
|
func (c Color) Color() tcell.Color {
|
||||||
|
if c == DefaultColor {
|
||||||
|
return tcell.ColorDefault
|
||||||
|
}
|
||||||
|
if color, ok := tcell.ColorNames[c.String()]; ok {
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
return tcell.GetColor(c.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsColors converts series string colors to colors.
|
||||||
|
func (c Colors) Colors() []tcell.Color {
|
||||||
|
cc := make([]tcell.Color, 0, len(c))
|
||||||
|
for _, color := range c {
|
||||||
|
cc = append(cc, color.Color())
|
||||||
|
}
|
||||||
|
return cc
|
||||||
|
}
|
||||||
|
|
||||||
func newStyle() Style {
|
func newStyle() Style {
|
||||||
return Style{
|
return Style{
|
||||||
Body: newBody(),
|
Body: newBody(),
|
||||||
Frame: newFrame(),
|
Frame: newFrame(),
|
||||||
Info: newInfo(),
|
Info: newInfo(),
|
||||||
Table: newTable(),
|
|
||||||
Views: newViews(),
|
Views: newViews(),
|
||||||
Xray: newXray(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newCharts() Charts {
|
||||||
|
return Charts{
|
||||||
|
BgColor: "default",
|
||||||
|
DialBgColor: "default",
|
||||||
|
ChartBgColor: "default",
|
||||||
|
DefaultDialColors: Colors{Color("palegreen"), Color("orangered")},
|
||||||
|
DefaultChartColors: Colors{Color("palegreen"), Color("orangered")},
|
||||||
|
}
|
||||||
|
}
|
||||||
func newViews() Views {
|
func newViews() Views {
|
||||||
return Views{
|
return Views{
|
||||||
Yaml: newYaml(),
|
Table: newTable(),
|
||||||
Log: newLog(),
|
Xray: newXray(),
|
||||||
|
Charts: newCharts(),
|
||||||
|
Yaml: newYaml(),
|
||||||
|
Log: newLog(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,7 +253,7 @@ func newStatus() Status {
|
||||||
ErrorColor: "orangered",
|
ErrorColor: "orangered",
|
||||||
HighlightColor: "aqua",
|
HighlightColor: "aqua",
|
||||||
KillColor: "mediumpurple",
|
KillColor: "mediumpurple",
|
||||||
CompletedColor: "gray",
|
CompletedColor: "lightgray",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -292,6 +357,11 @@ func NewStyles() *Styles {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset resets styles.
|
||||||
|
func (s *Styles) Reset() {
|
||||||
|
s.K9s = newStyle()
|
||||||
|
}
|
||||||
|
|
||||||
// DefaultSkin loads the default skin
|
// DefaultSkin loads the default skin
|
||||||
func (s *Styles) DefaultSkin() {
|
func (s *Styles) DefaultSkin() {
|
||||||
s.K9s = newStyle()
|
s.K9s = newStyle()
|
||||||
|
|
@ -299,12 +369,12 @@ func (s *Styles) DefaultSkin() {
|
||||||
|
|
||||||
// FgColor returns the foreground color.
|
// FgColor returns the foreground color.
|
||||||
func (s *Styles) FgColor() tcell.Color {
|
func (s *Styles) FgColor() tcell.Color {
|
||||||
return AsColor(s.Body().FgColor)
|
return s.Body().FgColor.Color()
|
||||||
}
|
}
|
||||||
|
|
||||||
// BgColor returns the background color.
|
// BgColor returns the background color.
|
||||||
func (s *Styles) BgColor() tcell.Color {
|
func (s *Styles) BgColor() tcell.Color {
|
||||||
return AsColor(s.Body().BgColor)
|
return s.Body().BgColor.Color()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddListener registers a new listener.
|
// AddListener registers a new listener.
|
||||||
|
|
@ -353,14 +423,19 @@ func (s *Styles) Title() Title {
|
||||||
return s.Frame().Title
|
return s.Frame().Title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Charts returns charts styles.
|
||||||
|
func (s *Styles) Charts() Charts {
|
||||||
|
return s.K9s.Views.Charts
|
||||||
|
}
|
||||||
|
|
||||||
// Table returns table styles.
|
// Table returns table styles.
|
||||||
func (s *Styles) Table() Table {
|
func (s *Styles) Table() Table {
|
||||||
return s.K9s.Table
|
return s.K9s.Views.Table
|
||||||
}
|
}
|
||||||
|
|
||||||
// Xray returns xray styles.
|
// Xray returns xray styles.
|
||||||
func (s *Styles) Xray() Xray {
|
func (s *Styles) Xray() Xray {
|
||||||
return s.K9s.Xray
|
return s.K9s.Views.Xray
|
||||||
}
|
}
|
||||||
|
|
||||||
// Views returns views styles.
|
// Views returns views styles.
|
||||||
|
|
@ -388,19 +463,7 @@ func (s *Styles) Update() {
|
||||||
tview.Styles.PrimitiveBackgroundColor = s.BgColor()
|
tview.Styles.PrimitiveBackgroundColor = s.BgColor()
|
||||||
tview.Styles.ContrastBackgroundColor = s.BgColor()
|
tview.Styles.ContrastBackgroundColor = s.BgColor()
|
||||||
tview.Styles.PrimaryTextColor = s.FgColor()
|
tview.Styles.PrimaryTextColor = s.FgColor()
|
||||||
tview.Styles.BorderColor = AsColor(s.K9s.Frame.Border.FgColor)
|
tview.Styles.BorderColor = s.K9s.Frame.Border.FgColor.Color()
|
||||||
tview.Styles.FocusColor = AsColor(s.K9s.Frame.Border.FocusColor)
|
tview.Styles.FocusColor = s.K9s.Frame.Border.FocusColor.Color()
|
||||||
s.fireStylesChanged()
|
s.fireStylesChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsColor checks color index, if match return color otherwise pink it is.
|
|
||||||
func AsColor(c string) tcell.Color {
|
|
||||||
if c == "default" {
|
|
||||||
return tcell.ColorDefault
|
|
||||||
}
|
|
||||||
if color, ok := tcell.ColorNames[c]; ok {
|
|
||||||
return color
|
|
||||||
}
|
|
||||||
|
|
||||||
return tcell.GetColor(c)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAsColor(t *testing.T) {
|
func TestColor(t *testing.T) {
|
||||||
uu := map[string]tcell.Color{
|
uu := map[string]tcell.Color{
|
||||||
"blah": tcell.ColorDefault,
|
"blah": tcell.ColorDefault,
|
||||||
"blue": tcell.ColorBlue,
|
"blue": tcell.ColorBlue,
|
||||||
|
|
@ -20,7 +20,7 @@ func TestAsColor(t *testing.T) {
|
||||||
for k := range uu {
|
for k := range uu {
|
||||||
c, u := k, uu[k]
|
c, u := k, uu[k]
|
||||||
t.Run(k, func(t *testing.T) {
|
t.Run(k, func(t *testing.T) {
|
||||||
assert.Equal(t, u, config.AsColor(c))
|
assert.Equal(t, u, config.NewColor(c).Color())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -30,9 +30,9 @@ func TestSkinNone(t *testing.T) {
|
||||||
assert.Nil(t, s.Load("testdata/empty_skin.yml"))
|
assert.Nil(t, s.Load("testdata/empty_skin.yml"))
|
||||||
s.Update()
|
s.Update()
|
||||||
|
|
||||||
assert.Equal(t, "cadetblue", s.Body().FgColor)
|
assert.Equal(t, "cadetblue", s.Body().FgColor.String())
|
||||||
assert.Equal(t, "black", s.Body().BgColor)
|
assert.Equal(t, "black", s.Body().BgColor.String())
|
||||||
assert.Equal(t, "black", s.Table().BgColor)
|
assert.Equal(t, "black", s.Table().BgColor.String())
|
||||||
assert.Equal(t, tcell.ColorCadetBlue, s.FgColor())
|
assert.Equal(t, tcell.ColorCadetBlue, s.FgColor())
|
||||||
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
||||||
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor)
|
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor)
|
||||||
|
|
@ -43,9 +43,9 @@ func TestSkin(t *testing.T) {
|
||||||
assert.Nil(t, s.Load("testdata/black_and_wtf.yml"))
|
assert.Nil(t, s.Load("testdata/black_and_wtf.yml"))
|
||||||
s.Update()
|
s.Update()
|
||||||
|
|
||||||
assert.Equal(t, "white", s.Body().FgColor)
|
assert.Equal(t, "white", s.Body().FgColor.String())
|
||||||
assert.Equal(t, "black", s.Body().BgColor)
|
assert.Equal(t, "black", s.Body().BgColor.String())
|
||||||
assert.Equal(t, "black", s.Table().BgColor)
|
assert.Equal(t, "black", s.Table().BgColor.String())
|
||||||
assert.Equal(t, tcell.ColorWhite, s.FgColor())
|
assert.Equal(t, tcell.ColorWhite, s.FgColor())
|
||||||
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
||||||
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor)
|
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor)
|
||||||
|
|
|
||||||
|
|
@ -33,23 +33,21 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("no context path for %q", c.gvr)
|
return nil, fmt.Errorf("no context path for %q", c.gvr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
pmx *mv1beta1.PodMetrics
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
||||||
|
if pmx, err = client.DialMetrics(c.Client()).FetchPodMetrics(fqn); err != nil {
|
||||||
|
log.Warn().Err(err).Msgf("No metrics found for pod %q", fqn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
po, err := c.fetchPod(fqn)
|
po, err := c.fetchPod(fqn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var pmx *mv1beta1.PodMetrics
|
|
||||||
if c.Client().HasMetrics() {
|
|
||||||
mx := client.NewMetricsServer(c.Client())
|
|
||||||
if c.Client() != nil {
|
|
||||||
var err error
|
|
||||||
pmx, err = mx.FetchPodMetrics(fqn)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msgf("No metrics found for pod %q", fqn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res := make([]runtime.Object, 0, len(po.Spec.InitContainers)+len(po.Spec.Containers))
|
res := make([]runtime.Object, 0, len(po.Spec.InitContainers)+len(po.Spec.Containers))
|
||||||
for _, co := range po.Spec.InitContainers {
|
for _, co := range po.Spec.InitContainers {
|
||||||
res = append(res, makeContainerRes(co, po, pmx, true))
|
res = append(res, makeContainerRes(co, po, pmx, true))
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,11 @@ type Deployment struct {
|
||||||
Resource
|
Resource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsHappy check for happy deployments.
|
||||||
|
func (d *Deployment) IsHappy(dp appsv1.Deployment) bool {
|
||||||
|
return dp.Status.Replicas == dp.Status.AvailableReplicas
|
||||||
|
}
|
||||||
|
|
||||||
// Scale a Deployment.
|
// Scale a Deployment.
|
||||||
func (d *Deployment) Scale(path string, replicas int32) error {
|
func (d *Deployment) Scale(path string, replicas int32) error {
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,11 @@ type DaemonSet struct {
|
||||||
Resource
|
Resource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsHappy check for happy deployments.
|
||||||
|
func (d *DaemonSet) IsHappy(ds appsv1.DaemonSet) bool {
|
||||||
|
return ds.Status.DesiredNumberScheduled == ds.Status.CurrentNumberScheduled
|
||||||
|
}
|
||||||
|
|
||||||
// Restart a DaemonSet rollout.
|
// Restart a DaemonSet rollout.
|
||||||
func (d *DaemonSet) Restart(path string) error {
|
func (d *DaemonSet) Restart(path string) error {
|
||||||
ds, err := d.GetInstance(path)
|
ds, err := d.GetInstance(path)
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,14 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||||
log.Warn().Msgf("No label selector found in context")
|
log.Warn().Msgf("No label selector found in context")
|
||||||
}
|
}
|
||||||
|
|
||||||
mx := client.NewMetricsServer(n.Client())
|
var (
|
||||||
nmx, err := mx.FetchNodesMetrics()
|
nmx *mv1beta1.NodeMetricsList
|
||||||
if err != nil {
|
err error
|
||||||
log.Warn().Err(err).Msgf("No node metrics")
|
)
|
||||||
|
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
||||||
|
if nmx, err = client.DialMetrics(n.Client()).FetchNodesMetrics(); err != nil {
|
||||||
|
log.Warn().Err(err).Msgf("No node metrics")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nn, err := FetchNodes(n.Factory, labels)
|
nn, err := FetchNodes(n.Factory, labels)
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,16 @@ type Pod struct {
|
||||||
Resource
|
Resource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsHappy check for happy deployments.
|
||||||
|
func (p *Pod) IsHappy(po v1.Pod) bool {
|
||||||
|
for _, c := range po.Status.Conditions {
|
||||||
|
if c.Status == v1.ConditionFalse {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Get returns a resource instance if found, else an error.
|
// Get returns a resource instance if found, else an error.
|
||||||
func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
|
func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
|
||||||
o, err := p.Resource.Get(ctx, path)
|
o, err := p.Resource.Get(ctx, path)
|
||||||
|
|
@ -50,11 +60,11 @@ func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
|
||||||
return nil, fmt.Errorf("expecting *unstructured.Unstructured but got `%T", o)
|
return nil, fmt.Errorf("expecting *unstructured.Unstructured but got `%T", o)
|
||||||
}
|
}
|
||||||
|
|
||||||
// No Deal!
|
var pmx *mv1beta1.PodMetrics
|
||||||
mx := client.NewMetricsServer(p.Client())
|
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
||||||
pmx, err := mx.FetchPodMetrics(path)
|
if pmx, err = client.DialMetrics(p.Client()).FetchPodMetrics(path); err != nil {
|
||||||
if err != nil {
|
log.Warn().Err(err).Msgf("No pod metrics")
|
||||||
log.Warn().Err(err).Msgf("No pods metrics")
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &render.PodWithMetrics{Raw: u, MX: pmx}, nil
|
return &render.PodWithMetrics{Raw: u, MX: pmx}, nil
|
||||||
|
|
@ -77,10 +87,11 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||||
return oo, err
|
return oo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mx := client.NewMetricsServer(p.Client())
|
var pmx *mv1beta1.PodMetricsList
|
||||||
pmx, err := mx.FetchPodsMetrics(ns)
|
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
||||||
if err != nil {
|
if pmx, err = client.DialMetrics(p.Client()).FetchPodsMetrics(ns); err != nil {
|
||||||
log.Warn().Err(err).Msgf("No pods metrics")
|
log.Warn().Err(err).Msgf("No pods metrics")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var res []runtime.Object
|
var res []runtime.Object
|
||||||
|
|
@ -128,7 +139,7 @@ func (p *Pod) Containers(path string, includeInit bool) ([]string, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cc := []string{}
|
cc := make([]string, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers))
|
||||||
for _, c := range pod.Spec.Containers {
|
for _, c := range pod.Spec.Containers {
|
||||||
cc = append(cc, c.Name)
|
cc = append(cc, c.Name)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Pulse struct {
|
||||||
|
NonResource
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Pulse) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||||
|
return nil, fmt.Errorf("NYI")
|
||||||
|
}
|
||||||
|
|
@ -142,6 +142,13 @@ func loadNonResource(m ResourceMetas) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadK9s(m ResourceMetas) {
|
func loadK9s(m ResourceMetas) {
|
||||||
|
m[client.NewGVR("pulses")] = metav1.APIResource{
|
||||||
|
Name: "pulses",
|
||||||
|
Kind: "Pulse",
|
||||||
|
SingularName: "pulses",
|
||||||
|
ShortNames: []string{"hz", "pu"},
|
||||||
|
Categories: []string{"k9s"},
|
||||||
|
}
|
||||||
m[client.NewGVR("xrays")] = metav1.APIResource{
|
m[client.NewGVR("xrays")] = metav1.APIResource{
|
||||||
Name: "xray",
|
Name: "xray",
|
||||||
Kind: "XRays",
|
Kind: "XRays",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReplicaSet represents a replicaset K8s resource.
|
||||||
|
type ReplicaSet struct {
|
||||||
|
Resource
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsHappy check for happy deployments.
|
||||||
|
func (d *ReplicaSet) IsHappy(rs appsv1.ReplicaSet) bool {
|
||||||
|
if rs.Status.Replicas == 0 && rs.Status.Replicas != rs.Status.ReadyReplicas {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if rs.Status.Replicas != 0 && rs.Status.Replicas != rs.Status.ReadyReplicas {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,11 @@ type StatefulSet struct {
|
||||||
Resource
|
Resource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsHappy check for happy sts.
|
||||||
|
func (s *StatefulSet) IsHappy(sts appsv1.StatefulSet) bool {
|
||||||
|
return sts.Status.Replicas == sts.Status.ReadyReplicas
|
||||||
|
}
|
||||||
|
|
||||||
// Scale a StatefulSet.
|
// Scale a StatefulSet.
|
||||||
func (s *StatefulSet) Scale(path string, replicas int32) error {
|
func (s *StatefulSet) Scale(path string, replicas int32) error {
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check tracks resource health.
|
||||||
|
type Check struct {
|
||||||
|
Counts
|
||||||
|
|
||||||
|
GVR string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks represents a collection of health checks.
|
||||||
|
type Checks []*Check
|
||||||
|
|
||||||
|
// NewCheck returns a new health check.
|
||||||
|
func NewCheck(gvr string) *Check {
|
||||||
|
return &Check{
|
||||||
|
GVR: gvr,
|
||||||
|
Counts: make(Counts),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets a health metric.
|
||||||
|
func (c *Check) Set(l Level, v int) {
|
||||||
|
c.Counts[l] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inc increments a health metric.
|
||||||
|
func (c *Check) Inc(l Level) {
|
||||||
|
c.Counts[l]++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total stores a metric total.
|
||||||
|
func (c *Check) Total(n int) {
|
||||||
|
c.Counts[Corpus] = n
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tally retrieves a given health metric.
|
||||||
|
func (c *Check) Tally(l Level) int {
|
||||||
|
return c.Counts[l]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectKind returns a schema object.
|
||||||
|
func (Check) GetObjectKind() schema.ObjectKind {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject returns a container copy.
|
||||||
|
func (c Check) DeepCopyObject() runtime.Object {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package health_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/health"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheck(t *testing.T) {
|
||||||
|
var cc health.Checks
|
||||||
|
|
||||||
|
c := health.NewCheck("test")
|
||||||
|
n := 0
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
c.Inc(health.OK)
|
||||||
|
cc = append(cc, c)
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
c.Total(n)
|
||||||
|
|
||||||
|
assert.Equal(t, 10, len(cc))
|
||||||
|
assert.Equal(t, 10, c.Tally(health.Corpus))
|
||||||
|
assert.Equal(t, 10, c.Tally(health.OK))
|
||||||
|
assert.Equal(t, 0, c.Tally(health.Toast))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package health
|
||||||
|
|
||||||
|
// Level tracks health count categories.
|
||||||
|
type Level int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Unknown represents no health level.
|
||||||
|
Unknown Level = 1 << iota
|
||||||
|
|
||||||
|
// Corpus tracks total health.
|
||||||
|
Corpus
|
||||||
|
|
||||||
|
// OK tracks healhy.
|
||||||
|
OK
|
||||||
|
|
||||||
|
// Warn tracks health warnings.
|
||||||
|
Warn
|
||||||
|
|
||||||
|
// Toast tracks unhealties.
|
||||||
|
Toast
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message represents a health message.
|
||||||
|
type Message struct {
|
||||||
|
Level Level
|
||||||
|
Message string
|
||||||
|
GVR string
|
||||||
|
FQN string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages tracks a collection of messages.
|
||||||
|
type Messages []Message
|
||||||
|
|
||||||
|
// Counts tracks health counts by category.
|
||||||
|
type Counts map[Level]int
|
||||||
|
|
||||||
|
// Vital tracks a resource vitals.
|
||||||
|
type Vital struct {
|
||||||
|
Resource string
|
||||||
|
Total, OK, Toast int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vitals tracks a collection of resource health.
|
||||||
|
type Vitals []Vital
|
||||||
|
|
@ -25,4 +25,6 @@ const (
|
||||||
KeyApp ContextKey = "app"
|
KeyApp ContextKey = "app"
|
||||||
KeyStyles ContextKey = "styles"
|
KeyStyles ContextKey = "styles"
|
||||||
KeyMetrics ContextKey = "metrics"
|
KeyMetrics ContextKey = "metrics"
|
||||||
|
KeyToast ContextKey = "toast"
|
||||||
|
KeyWithMetrics ContextKey = "withMetrics"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ type (
|
||||||
func NewCluster(f dao.Factory) *Cluster {
|
func NewCluster(f dao.Factory) *Cluster {
|
||||||
return &Cluster{
|
return &Cluster{
|
||||||
factory: f,
|
factory: f,
|
||||||
mx: client.NewMetricsServer(f.Client()),
|
mx: client.DialMetrics(f.Client()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultFlashDelay sets the flash clear delay.
|
||||||
|
DefaultFlashDelay = 3 * time.Second
|
||||||
|
|
||||||
|
// FlashInfo represents an info message.
|
||||||
|
FlashInfo FlashLevel = iota
|
||||||
|
// FlashWarn represents an warning message.
|
||||||
|
FlashWarn
|
||||||
|
// FlashErr represents an error message.
|
||||||
|
FlashErr
|
||||||
|
)
|
||||||
|
|
||||||
|
type LevelMessage struct {
|
||||||
|
Level FlashLevel
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClearMessage() LevelMessage {
|
||||||
|
return LevelMessage{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LevelMessage) IsClear() bool {
|
||||||
|
return l.Text == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlashLevel represents flash message severity.
|
||||||
|
type FlashLevel int
|
||||||
|
|
||||||
|
// FlashChan represents a flash event channel.
|
||||||
|
type FlashChan chan LevelMessage
|
||||||
|
|
||||||
|
// FlashListener represents a text model listener.
|
||||||
|
type FlashListener interface {
|
||||||
|
// FlashChanged notifies the model changed.
|
||||||
|
FlashChanged(FlashLevel, string)
|
||||||
|
|
||||||
|
// FlashCleared notifies when the filter changed.
|
||||||
|
FlashCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flash represents a flash message model.
|
||||||
|
type Flash struct {
|
||||||
|
msg LevelMessage
|
||||||
|
cancel context.CancelFunc
|
||||||
|
delay time.Duration
|
||||||
|
msgChan chan LevelMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFlash(dur time.Duration) *Flash {
|
||||||
|
return &Flash{
|
||||||
|
delay: dur,
|
||||||
|
msgChan: make(FlashChan, 3),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel returns the flash channel.
|
||||||
|
func (f *Flash) Channel() FlashChan {
|
||||||
|
return f.msgChan
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info displays an info flash message.
|
||||||
|
func (f *Flash) Info(msg string) {
|
||||||
|
f.SetMessage(FlashInfo, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infof displays a formatted info flash message.
|
||||||
|
func (f *Flash) Infof(fmat string, args ...interface{}) {
|
||||||
|
f.Info(fmt.Sprintf(fmat, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn displays a warning flash message.
|
||||||
|
func (f *Flash) Warn(msg string) {
|
||||||
|
log.Warn().Msg(msg)
|
||||||
|
f.SetMessage(FlashWarn, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnf displays a formatted warning flash message.
|
||||||
|
func (f *Flash) Warnf(fmat string, args ...interface{}) {
|
||||||
|
f.Warn(fmt.Sprintf(fmat, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err displays an error flash message.
|
||||||
|
func (f *Flash) Err(err error) {
|
||||||
|
log.Error().Msg(err.Error())
|
||||||
|
f.SetMessage(FlashErr, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errf displays a formatted error flash message.
|
||||||
|
func (f *Flash) Errf(fmat string, args ...interface{}) {
|
||||||
|
var err error
|
||||||
|
for _, a := range args {
|
||||||
|
switch e := a.(type) {
|
||||||
|
case error:
|
||||||
|
err = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Error().Err(err).Msgf(fmat, args...)
|
||||||
|
f.SetMessage(FlashErr, fmt.Sprintf(fmat, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear clears the flash message.
|
||||||
|
func (f *Flash) Clear() {
|
||||||
|
f.fireCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMessage sets the flash level message.
|
||||||
|
func (f *Flash) SetMessage(level FlashLevel, msg string) {
|
||||||
|
if f.cancel != nil {
|
||||||
|
f.cancel()
|
||||||
|
f.cancel = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f.setLevelMessage(LevelMessage{Level: level, Text: msg})
|
||||||
|
f.fireFlashChanged()
|
||||||
|
|
||||||
|
var ctx context.Context
|
||||||
|
ctx, f.cancel = context.WithCancel(context.Background())
|
||||||
|
go f.refresh(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Flash) refresh(ctx context.Context) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(f.delay):
|
||||||
|
f.fireCleared()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Flash) setLevelMessage(msg LevelMessage) {
|
||||||
|
f.msg = msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Flash) fireFlashChanged() {
|
||||||
|
f.msgChan <- f.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Flash) fireCleared() {
|
||||||
|
f.msgChan <- newClearMessage()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
package model_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/model"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFlash(t *testing.T) {
|
||||||
|
const delay = 1 * time.Millisecond
|
||||||
|
|
||||||
|
uu := map[string]struct {
|
||||||
|
level model.FlashLevel
|
||||||
|
e string
|
||||||
|
}{
|
||||||
|
"info": {level: model.FlashInfo, e: "blee"},
|
||||||
|
"warn": {level: model.FlashWarn, e: "blee"},
|
||||||
|
"err": {level: model.FlashErr, e: "blee"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
f := model.NewFlash(delay)
|
||||||
|
v := newFlash()
|
||||||
|
go v.listen(f.Channel())
|
||||||
|
|
||||||
|
switch u.level {
|
||||||
|
case model.FlashInfo:
|
||||||
|
f.Info(u.e)
|
||||||
|
case model.FlashWarn:
|
||||||
|
f.Warn(u.e)
|
||||||
|
case model.FlashErr:
|
||||||
|
f.Err(errors.New(u.e))
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(2 * delay)
|
||||||
|
s, c, l, m := v.getMetrics()
|
||||||
|
assert.Equal(t, 1, s)
|
||||||
|
assert.Equal(t, u.level, l)
|
||||||
|
assert.Equal(t, u.e, m)
|
||||||
|
assert.Equal(t, 1, c)
|
||||||
|
|
||||||
|
close(f.Channel())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFlashBurst(t *testing.T) {
|
||||||
|
const delay = 1 * time.Millisecond
|
||||||
|
|
||||||
|
f := model.NewFlash(delay)
|
||||||
|
v := newFlash()
|
||||||
|
go v.listen(f.Channel())
|
||||||
|
|
||||||
|
count := 5
|
||||||
|
for i := 1; i <= count; i++ {
|
||||||
|
f.Info(fmt.Sprintf("test-%d", i))
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(2 * delay)
|
||||||
|
s, c, l, m := v.getMetrics()
|
||||||
|
assert.Equal(t, count, s)
|
||||||
|
assert.Equal(t, model.FlashInfo, l)
|
||||||
|
assert.Equal(t, fmt.Sprintf("test-%d", count), m)
|
||||||
|
assert.Equal(t, 1, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
type flash struct {
|
||||||
|
set, clear int
|
||||||
|
level model.FlashLevel
|
||||||
|
msg string
|
||||||
|
mx sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFlash() *flash {
|
||||||
|
return &flash{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *flash) getMetrics() (int, int, model.FlashLevel, string) {
|
||||||
|
f.mx.RLock()
|
||||||
|
defer f.mx.RUnlock()
|
||||||
|
return f.set, f.clear, f.level, f.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *flash) listen(c model.FlashChan) {
|
||||||
|
for m := range c {
|
||||||
|
f.mx.Lock()
|
||||||
|
{
|
||||||
|
if m.IsClear() {
|
||||||
|
f.clear++
|
||||||
|
} else {
|
||||||
|
f.set++
|
||||||
|
f.level, f.msg = m.Level, m.Text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.mx.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal"
|
||||||
|
"github.com/derailed/k9s/internal/dao"
|
||||||
|
"github.com/derailed/k9s/internal/health"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultRefreshRate = 5 * time.Second
|
||||||
|
|
||||||
|
// PulseListener represents a health model listener.
|
||||||
|
type PulseListener interface {
|
||||||
|
// PulseChanged notifies the model data changed.
|
||||||
|
PulseChanged(*health.Check)
|
||||||
|
|
||||||
|
// TreeFailed notifies the health check failed.
|
||||||
|
PulseFailed(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pulse tracks multiple resources health.
|
||||||
|
type Pulse struct {
|
||||||
|
gvr string
|
||||||
|
namespace string
|
||||||
|
inUpdate int32
|
||||||
|
listeners []PulseListener
|
||||||
|
refreshRate time.Duration
|
||||||
|
health *PulseHealth
|
||||||
|
data health.Checks
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPulse(gvr string) *Pulse {
|
||||||
|
return &Pulse{
|
||||||
|
gvr: gvr,
|
||||||
|
refreshRate: defaultRefreshRate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pulse) Watch(ctx context.Context) {
|
||||||
|
p.Refresh(ctx)
|
||||||
|
go p.updater(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pulse) updater(ctx context.Context) {
|
||||||
|
defer log.Debug().Msgf("Pulse canceled -- %q", p.gvr)
|
||||||
|
|
||||||
|
rate := initRefreshRate
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(rate):
|
||||||
|
rate = p.refreshRate
|
||||||
|
p.refresh(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh update the model now.
|
||||||
|
func (p *Pulse) Refresh(ctx context.Context) {
|
||||||
|
for _, d := range p.data {
|
||||||
|
p.firePulseChanged(d)
|
||||||
|
}
|
||||||
|
p.refresh(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pulse) refresh(ctx context.Context) {
|
||||||
|
if !atomic.CompareAndSwapInt32(&p.inUpdate, 0, 1) {
|
||||||
|
log.Debug().Msgf("Dropping update...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer atomic.StoreInt32(&p.inUpdate, 0)
|
||||||
|
|
||||||
|
if err := p.reconcile(ctx); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Reconcile failed")
|
||||||
|
p.firePulseFailed(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pulse) list(ctx context.Context) ([]runtime.Object, error) {
|
||||||
|
f, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
|
||||||
|
}
|
||||||
|
if p.health == nil {
|
||||||
|
p.health = NewPulseHealth(f)
|
||||||
|
}
|
||||||
|
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
||||||
|
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
|
||||||
|
return p.health.List(ctx, p.namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pulse) reconcile(ctx context.Context) error {
|
||||||
|
oo, err := p.list(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.data = health.Checks{}
|
||||||
|
for _, o := range oo {
|
||||||
|
c, ok := o.(*health.Check)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Expecting health check but got %T", o)
|
||||||
|
}
|
||||||
|
p.data = append(p.data, c)
|
||||||
|
p.firePulseChanged(c)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNamespace returns the model namespace.
|
||||||
|
func (p *Pulse) GetNamespace() string {
|
||||||
|
return p.namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNamespace sets up model namespace.
|
||||||
|
func (p *Pulse) SetNamespace(ns string) {
|
||||||
|
p.namespace = ns
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddListener adds a listener.
|
||||||
|
func (p *Pulse) AddListener(l PulseListener) {
|
||||||
|
p.listeners = append(p.listeners, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveListener delete a listener.
|
||||||
|
func (p *Pulse) RemoveListener(l PulseListener) {
|
||||||
|
victim := -1
|
||||||
|
for i, lis := range p.listeners {
|
||||||
|
if lis == l {
|
||||||
|
victim = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if victim >= 0 {
|
||||||
|
p.listeners = append(p.listeners[:victim], p.listeners[victim+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pulse) firePulseChanged(check *health.Check) {
|
||||||
|
for _, l := range p.listeners {
|
||||||
|
l.PulseChanged(check)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pulse) firePulseFailed(err error) {
|
||||||
|
for _, l := range p.listeners {
|
||||||
|
l.PulseFailed(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/client"
|
||||||
|
"github.com/derailed/k9s/internal/dao"
|
||||||
|
"github.com/derailed/k9s/internal/health"
|
||||||
|
"github.com/derailed/k9s/internal/render"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PulseHealth struct {
|
||||||
|
factory dao.Factory
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPulseHealth(f dao.Factory) *PulseHealth {
|
||||||
|
return &PulseHealth{
|
||||||
|
factory: f,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PulseHealth) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||||
|
defer func(t time.Time) {
|
||||||
|
log.Debug().Msgf("PulseHealthCheck %v", time.Since(t))
|
||||||
|
}(time.Now())
|
||||||
|
|
||||||
|
gvrs := []string{
|
||||||
|
"v1/pods",
|
||||||
|
"v1/events",
|
||||||
|
"apps/v1/replicasets",
|
||||||
|
"apps/v1/deployments",
|
||||||
|
"apps/v1/statefulsets",
|
||||||
|
"apps/v1/daemonsets",
|
||||||
|
"batch/v1/jobs",
|
||||||
|
"v1/persistentvolumes",
|
||||||
|
}
|
||||||
|
|
||||||
|
hh := make([]runtime.Object, 0, 10)
|
||||||
|
for _, gvr := range gvrs {
|
||||||
|
c, err := h.check(ctx, ns, gvr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hh = append(hh, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
mm, err := h.checkMetrics()
|
||||||
|
if err != nil {
|
||||||
|
return hh, nil
|
||||||
|
}
|
||||||
|
for _, m := range mm {
|
||||||
|
hh = append(hh, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PulseHealth) checkMetrics() (health.Checks, error) {
|
||||||
|
dial := client.DialMetrics(h.factory.Client())
|
||||||
|
nmx, err := dial.FetchNodesMetrics()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Fetching metrics")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cpu, mem float64
|
||||||
|
for _, mx := range nmx.Items {
|
||||||
|
cpu += float64(mx.Usage.Cpu().MilliValue())
|
||||||
|
mem += client.ToMB(mx.Usage.Memory().Value())
|
||||||
|
}
|
||||||
|
c1 := health.NewCheck("cpu")
|
||||||
|
c1.Set(health.OK, int(math.Round(cpu)))
|
||||||
|
c2 := health.NewCheck("mem")
|
||||||
|
c2.Set(health.OK, int(math.Round(mem)))
|
||||||
|
|
||||||
|
return health.Checks{c1, c2}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check, error) {
|
||||||
|
meta, ok := Registry[gvr]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("No meta for %q", gvr)
|
||||||
|
}
|
||||||
|
if meta.DAO == nil {
|
||||||
|
meta.DAO = &dao.Resource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.DAO.Init(h.factory, client.NewGVR(gvr))
|
||||||
|
oo, err := meta.DAO.List(ctx, ns)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c := health.NewCheck(gvr)
|
||||||
|
c.Total(len(oo))
|
||||||
|
rr, re := make(render.Rows, len(oo)), meta.Renderer
|
||||||
|
for i, o := range oo {
|
||||||
|
if err := re.Render(o, ns, &rr[i]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !render.Happy(ns, rr[i]) {
|
||||||
|
c.Inc(health.Toast)
|
||||||
|
} else {
|
||||||
|
c.Inc(health.OK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,9 @@ var Registry = map[string]ResourceMeta{
|
||||||
DAO: &dao.Chart{},
|
DAO: &dao.Chart{},
|
||||||
Renderer: &render.Chart{},
|
Renderer: &render.Chart{},
|
||||||
},
|
},
|
||||||
|
"pulses": {
|
||||||
|
DAO: &dao.Pulse{},
|
||||||
|
},
|
||||||
"openfaas": {
|
"openfaas": {
|
||||||
DAO: &dao.OpenFaas{},
|
DAO: &dao.OpenFaas{},
|
||||||
Renderer: &render.OpenFaas{},
|
Renderer: &render.OpenFaas{},
|
||||||
|
|
|
||||||
|
|
@ -108,8 +108,8 @@ func (s *Stack) Pop() (Component, bool) {
|
||||||
var c Component
|
var c Component
|
||||||
s.mx.Lock()
|
s.mx.Lock()
|
||||||
{
|
{
|
||||||
c = s.components[s.size()]
|
c = s.components[len(s.components)-1]
|
||||||
s.components = s.components[:s.size()]
|
s.components = s.components[:len(s.components)-1]
|
||||||
}
|
}
|
||||||
s.mx.Unlock()
|
s.mx.Unlock()
|
||||||
s.notify(StackPop, c)
|
s.notify(StackPop, c)
|
||||||
|
|
@ -163,11 +163,7 @@ func (s *Stack) Top() Component {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.components[s.size()]
|
return s.components[len(s.components)-1]
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stack) size() int {
|
|
||||||
return len(s.components) - 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stack) notify(a StackAction, c Component) {
|
func (s *Stack) notify(a StackAction, c Component) {
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,7 @@ func (t *Table) SetRefreshRate(d time.Duration) {
|
||||||
|
|
||||||
// ClusterWide checks if resource is scope for all namespaces.
|
// ClusterWide checks if resource is scope for all namespaces.
|
||||||
func (t *Table) ClusterWide() bool {
|
func (t *Table) ClusterWide() bool {
|
||||||
|
log.Debug().Msgf("CLUSTER-WIDE %q", t.namespace)
|
||||||
return client.IsClusterWide(t.namespace)
|
return client.IsClusterWide(t.namespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,6 +220,7 @@ func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, err
|
||||||
if client.IsClusterScoped(t.namespace) {
|
if client.IsClusterScoped(t.namespace) {
|
||||||
ns = client.AllNamespaces
|
ns = client.AllNamespaces
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.List(ctx, ns)
|
return a.List(ctx, ns)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,11 @@ func TestTableReconcile(t *testing.T) {
|
||||||
f.rows = []runtime.Object{load(t, "p1")}
|
f.rows = []runtime.Object{load(t, "p1")}
|
||||||
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
||||||
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
||||||
|
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
|
||||||
err := ta.reconcile(ctx)
|
err := ta.reconcile(ctx)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
data := ta.Peek()
|
data := ta.Peek()
|
||||||
assert.Equal(t, 15, len(data.Header))
|
assert.Equal(t, 17, len(data.Header))
|
||||||
assert.Equal(t, 1, len(data.RowEvents))
|
assert.Equal(t, 1, len(data.RowEvents))
|
||||||
assert.Equal(t, client.NamespaceAll, data.Namespace)
|
assert.Equal(t, client.NamespaceAll, data.Namespace)
|
||||||
}
|
}
|
||||||
|
|
@ -55,6 +56,7 @@ func TestTableGet(t *testing.T) {
|
||||||
f := makeFactory()
|
f := makeFactory()
|
||||||
f.rows = []runtime.Object{load(t, "p1")}
|
f.rows = []runtime.Object{load(t, "p1")}
|
||||||
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
||||||
|
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
|
||||||
row, err := ta.Get(ctx, "fred")
|
row, err := ta.Get(ctx, "fred")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.NotNil(t, row)
|
assert.NotNil(t, row)
|
||||||
|
|
@ -104,7 +106,7 @@ func TestTableHydrate(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, hydrate("blee", oo, rr, render.Pod{}))
|
assert.Nil(t, hydrate("blee", oo, rr, render.Pod{}))
|
||||||
assert.Equal(t, 1, len(rr))
|
assert.Equal(t, 1, len(rr))
|
||||||
assert.Equal(t, 14, len(rr[0].Fields))
|
assert.Equal(t, 16, len(rr[0].Fields))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTableGenericHydrate(t *testing.T) {
|
func TestTableGenericHydrate(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,10 @@ func TestTableRefresh(t *testing.T) {
|
||||||
f.rows = []runtime.Object{mustLoad("p1")}
|
f.rows = []runtime.Object{mustLoad("p1")}
|
||||||
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
||||||
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
||||||
|
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
|
||||||
ta.Refresh(ctx)
|
ta.Refresh(ctx)
|
||||||
data := ta.Peek()
|
data := ta.Peek()
|
||||||
assert.Equal(t, 15, len(data.Header))
|
assert.Equal(t, 17, len(data.Header))
|
||||||
assert.Equal(t, 1, len(data.RowEvents))
|
assert.Equal(t, 1, len(data.RowEvents))
|
||||||
assert.Equal(t, client.NamespaceAll, data.Namespace)
|
assert.Equal(t, client.NamespaceAll, data.Namespace)
|
||||||
assert.Equal(t, 1, l.count)
|
assert.Equal(t, 1, l.count)
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,7 @@ func (t *Tree) reconcile(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
if t.root == nil || t.root.Diff(root) {
|
if t.root == nil || t.root.Diff(root) {
|
||||||
t.root = root
|
t.root = root
|
||||||
t.fireTreeTreeChanged(t.root)
|
t.fireTreeChanged(t.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -251,7 +251,7 @@ func (t *Tree) resourceMeta() ResourceMeta {
|
||||||
return meta
|
return meta
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tree) fireTreeTreeChanged(root *xray.TreeNode) {
|
func (t *Tree) fireTreeChanged(root *xray.TreeNode) {
|
||||||
for _, l := range t.listeners {
|
for _, l := range t.listeners {
|
||||||
l.TreeChanged(root)
|
l.TreeChanged(root)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -8,6 +9,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
@ -28,11 +30,10 @@ var (
|
||||||
type Benchmark struct{}
|
type Benchmark struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Benchmark) ColorerFunc() ColorerFunc {
|
func (b Benchmark) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, re RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
c := tcell.ColorPaleGreen
|
c := tcell.ColorPaleGreen
|
||||||
statusCol := 2
|
if !Happy(ns, re.Row) {
|
||||||
if strings.TrimSpace(re.Row.Fields[statusCol]) != "pass" {
|
|
||||||
c = ErrColor
|
c = ErrColor
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
|
|
@ -50,6 +51,7 @@ func (Benchmark) Header(ns string) HeaderRow {
|
||||||
Header{Name: "2XX", Align: tview.AlignRight},
|
Header{Name: "2XX", Align: tview.AlignRight},
|
||||||
Header{Name: "4XX/5XX", Align: tview.AlignRight},
|
Header{Name: "4XX/5XX", Align: tview.AlignRight},
|
||||||
Header{Name: "REPORT"},
|
Header{Name: "REPORT"},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,6 +74,24 @@ func (b Benchmark) Render(o interface{}, ns string, r *Row) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
b.augmentRow(r.Fields, data)
|
b.augmentRow(r.Fields, data)
|
||||||
|
r.Fields[8] = asStatus(b.diagnose(ns, r.Fields))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Happy returns true if resoure is happy, false otherwise
|
||||||
|
func (Benchmark) diagnose(ns string, ff Fields) error {
|
||||||
|
statusCol := 3
|
||||||
|
if !client.IsAllNamespaces(ns) {
|
||||||
|
statusCol--
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ff) < statusCol {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if ff[statusCol] != "pass" {
|
||||||
|
return errors.New("failed benchmark")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +107,7 @@ func (Benchmark) readFile(file string) (string, error) {
|
||||||
return string(data), nil
|
return string(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Benchmark) initRow(row Fields, f os.FileInfo) error {
|
func (b Benchmark) initRow(row Fields, f os.FileInfo) error {
|
||||||
tokens := strings.Split(f.Name(), "_")
|
tokens := strings.Split(f.Name(), "_")
|
||||||
if len(tokens) < 2 {
|
if len(tokens) < 2 {
|
||||||
return fmt.Errorf("Invalid file name %s", f.Name())
|
return fmt.Errorf("Invalid file name %s", f.Name())
|
||||||
|
|
@ -95,7 +115,7 @@ func (Benchmark) initRow(row Fields, f os.FileInfo) error {
|
||||||
row[0] = tokens[0]
|
row[0] = tokens[0]
|
||||||
row[1] = tokens[1]
|
row[1] = tokens[1]
|
||||||
row[7] = f.Name()
|
row[7] = f.Name()
|
||||||
row[8] = timeToAge(f.ModTime())
|
row[9] = timeToAge(f.ModTime())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ type Chart struct{}
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Chart) ColorerFunc() ColorerFunc {
|
func (Chart) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, re RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
|
if !Happy(ns, re.Row) {
|
||||||
|
return ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
return tcell.ColorMediumSpringGreen
|
return tcell.ColorMediumSpringGreen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -35,6 +39,7 @@ func (Chart) Header(ns string) HeaderRow {
|
||||||
Header{Name: "STATUS"},
|
Header{Name: "STATUS"},
|
||||||
Header{Name: "CHART"},
|
Header{Name: "CHART"},
|
||||||
Header{Name: "APP VERSION"},
|
Header{Name: "APP VERSION"},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -57,12 +62,21 @@ func (c Chart) Render(o interface{}, ns string, r *Row) error {
|
||||||
h.Release.Info.Status.String(),
|
h.Release.Info.Status.String(),
|
||||||
h.Release.Chart.Metadata.Name+"-"+h.Release.Chart.Metadata.Version,
|
h.Release.Chart.Metadata.Name+"-"+h.Release.Chart.Metadata.Version,
|
||||||
h.Release.Chart.Metadata.AppVersion,
|
h.Release.Chart.Metadata.AppVersion,
|
||||||
|
asStatus(c.diagnose(h.Release.Info.Status.String())),
|
||||||
toAge(metav1.Time{Time: h.Release.Info.LastDeployed.Time}),
|
toAge(metav1.Time{Time: h.Release.Info.LastDeployed.Time}),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Chart) diagnose(s string) error {
|
||||||
|
if s != "deployed" {
|
||||||
|
return fmt.Errorf("chart is in an invalid state")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -36,18 +37,18 @@ type ContainerWithMetrics interface {
|
||||||
// Container renders a K8s Container to screen.
|
// Container renders a K8s Container to screen.
|
||||||
type Container struct{}
|
type Container struct{}
|
||||||
|
|
||||||
|
const readyCol = 2
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Container) ColorerFunc() ColorerFunc {
|
func (c Container) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
color := DefaultColorer(ns, re)
|
||||||
|
|
||||||
readyCol := 2
|
if !Happy(ns, re.Row) {
|
||||||
if strings.TrimSpace(r.Row.Fields[readyCol]) == "false" {
|
color = ErrColor
|
||||||
c = ErrColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stateCol := readyCol + 1
|
stateCol := readyCol + 1
|
||||||
switch strings.TrimSpace(r.Row.Fields[stateCol]) {
|
switch strings.TrimSpace(re.Row.Fields[stateCol]) {
|
||||||
case ContainerCreating, PodInitializing:
|
case ContainerCreating, PodInitializing:
|
||||||
return AddColor
|
return AddColor
|
||||||
case Terminating, Initialized:
|
case Terminating, Initialized:
|
||||||
|
|
@ -56,10 +57,10 @@ func (Container) ColorerFunc() ColorerFunc {
|
||||||
return CompletedColor
|
return CompletedColor
|
||||||
case Running:
|
case Running:
|
||||||
default:
|
default:
|
||||||
c = ErrColor
|
color = ErrColor
|
||||||
}
|
}
|
||||||
|
|
||||||
return c
|
return color
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,6 +81,7 @@ func (Container) Header(ns string) HeaderRow {
|
||||||
Header{Name: "%CPU/L", Align: tview.AlignRight},
|
Header{Name: "%CPU/L", Align: tview.AlignRight},
|
||||||
Header{Name: "%MEM/L", Align: tview.AlignRight},
|
Header{Name: "%MEM/L", Align: tview.AlignRight},
|
||||||
Header{Name: "PORTS"},
|
Header{Name: "PORTS"},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,12 +116,25 @@ func (c Container) Render(o interface{}, name string, r *Row) error {
|
||||||
limit.cpu,
|
limit.cpu,
|
||||||
limit.mem,
|
limit.mem,
|
||||||
toStrPorts(co.Container.Ports),
|
toStrPorts(co.Container.Ports),
|
||||||
|
asStatus(c.diagnose(state, ready)),
|
||||||
toAge(co.Age),
|
toAge(co.Age),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Happy returns true if resoure is happy, false otherwise
|
||||||
|
func (Container) diagnose(state, ready string) error {
|
||||||
|
if state == "Completed" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ready == "false" {
|
||||||
|
return errors.New("container is not ready")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ func TestContainer(t *testing.T) {
|
||||||
"50",
|
"50",
|
||||||
"20",
|
"20",
|
||||||
"",
|
"",
|
||||||
|
"container is not ready",
|
||||||
},
|
},
|
||||||
r.Fields[:len(r.Fields)-1],
|
r.Fields[:len(r.Fields)-1],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ func (ClusterRole) ColorerFunc() ColorerFunc {
|
||||||
func (ClusterRole) Header(string) HeaderRow {
|
func (ClusterRole) Header(string) HeaderRow {
|
||||||
return HeaderRow{
|
return HeaderRow{
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -40,6 +41,7 @@ func (ClusterRole) Render(o interface{}, ns string, r *Row) error {
|
||||||
r.ID = client.FQN("-", cr.ObjectMeta.Name)
|
r.ID = client.FQN("-", cr.ObjectMeta.Name)
|
||||||
r.Fields = Fields{
|
r.Fields = Fields{
|
||||||
cr.Name,
|
cr.Name,
|
||||||
|
mapToStr(cr.Labels),
|
||||||
toAge(cr.ObjectMeta.CreationTimestamp),
|
toAge(cr.ObjectMeta.CreationTimestamp),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,9 @@ func (ClusterRoleBinding) Header(string) HeaderRow {
|
||||||
return HeaderRow{
|
return HeaderRow{
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "CLUSTERROLE"},
|
Header{Name: "CLUSTERROLE"},
|
||||||
Header{Name: "KIND"},
|
Header{Name: "SUBJECT-KIND"},
|
||||||
Header{Name: "SUBJECTS"},
|
Header{Name: "SUBJECTS"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +49,7 @@ func (ClusterRoleBinding) Render(o interface{}, ns string, r *Row) error {
|
||||||
crb.RoleRef.Name,
|
crb.RoleRef.Name,
|
||||||
kind,
|
kind,
|
||||||
ss,
|
ss,
|
||||||
|
mapToStr(crb.Labels),
|
||||||
toAge(crb.ObjectMeta.CreationTimestamp),
|
toAge(crb.ObjectMeta.CreationTimestamp),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestClusterRoleBindingRender(t *testing.T) {
|
||||||
c.Render(load(t, "crb"), "-", &r)
|
c.Render(load(t, "crb"), "-", &r)
|
||||||
|
|
||||||
assert.Equal(t, "-/blee", r.ID)
|
assert.Equal(t, "-/blee", r.ID)
|
||||||
assert.Equal(t, render.Fields{"blee", "blee", "USR", "fernand"}, r.Fields[:4])
|
assert.Equal(t, render.Fields{"blee", "blee", "User", "fernand"}, r.Fields[:4])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ func (CustomResourceDefinition) ColorerFunc() ColorerFunc {
|
||||||
func (CustomResourceDefinition) Header(string) HeaderRow {
|
func (CustomResourceDefinition) Header(string) HeaderRow {
|
||||||
return HeaderRow{
|
return HeaderRow{
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -45,6 +46,7 @@ func (CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error {
|
||||||
r.ID = client.FQN(client.ClusterScope, extractMetaField(meta, "name"))
|
r.ID = client.FQN(client.ClusterScope, extractMetaField(meta, "name"))
|
||||||
r.Fields = Fields{
|
r.Fields = Fields{
|
||||||
extractMetaField(meta, "name"),
|
extractMetaField(meta, "name"),
|
||||||
|
mapToIfc(meta["labels"]),
|
||||||
toAge(metav1.Time{Time: t}),
|
toAge(metav1.Time{Time: t}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,12 @@ package render
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
|
batchv1 "k8s.io/api/batch/v1"
|
||||||
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
@ -31,6 +34,11 @@ func (CronJob) Header(ns string) HeaderRow {
|
||||||
Header{Name: "SUSPEND"},
|
Header{Name: "SUSPEND"},
|
||||||
Header{Name: "ACTIVE"},
|
Header{Name: "ACTIVE"},
|
||||||
Header{Name: "LAST_SCHEDULE"},
|
Header{Name: "LAST_SCHEDULE"},
|
||||||
|
Header{Name: "SELECTOR", Wide: true},
|
||||||
|
Header{Name: "CONTAINERS", Wide: true},
|
||||||
|
Header{Name: "IMAGES", Wide: true},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -63,8 +71,64 @@ func (c CronJob) Render(o interface{}, ns string, r *Row) error {
|
||||||
boolPtrToStr(cj.Spec.Suspend),
|
boolPtrToStr(cj.Spec.Suspend),
|
||||||
strconv.Itoa(len(cj.Status.Active)),
|
strconv.Itoa(len(cj.Status.Active)),
|
||||||
lastScheduled,
|
lastScheduled,
|
||||||
|
jobSelector(cj.Spec.JobTemplate.Spec),
|
||||||
|
podContainerNames(cj.Spec.JobTemplate.Spec.Template.Spec, true),
|
||||||
|
podImageNames(cj.Spec.JobTemplate.Spec.Template.Spec, true),
|
||||||
|
mapToStr(cj.Labels),
|
||||||
|
"",
|
||||||
toAge(cj.ObjectMeta.CreationTimestamp),
|
toAge(cj.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
|
||||||
|
func jobSelector(spec batchv1.JobSpec) string {
|
||||||
|
if spec.Selector == nil {
|
||||||
|
return MissingValue
|
||||||
|
}
|
||||||
|
if len(spec.Selector.MatchLabels) > 0 {
|
||||||
|
return mapToStr(spec.Selector.MatchLabels)
|
||||||
|
}
|
||||||
|
if len(spec.Selector.MatchExpressions) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
ss := make([]string, 0, len(spec.Selector.MatchExpressions))
|
||||||
|
for _, e := range spec.Selector.MatchExpressions {
|
||||||
|
ss = append(ss, e.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(ss, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func podContainerNames(spec v1.PodSpec, includeInit bool) string {
|
||||||
|
cc := make([]string, 0, len(spec.Containers)+len(spec.InitContainers))
|
||||||
|
|
||||||
|
if includeInit {
|
||||||
|
for _, c := range spec.InitContainers {
|
||||||
|
cc = append(cc, c.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, c := range spec.Containers {
|
||||||
|
cc = append(cc, c.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(cc, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func podImageNames(spec v1.PodSpec, includeInit bool) string {
|
||||||
|
cc := make([]string, 0, len(spec.Containers)+len(spec.InitContainers))
|
||||||
|
|
||||||
|
if includeInit {
|
||||||
|
for _, c := range spec.InitContainers {
|
||||||
|
cc = append(cc, c.Image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, c := range spec.Containers {
|
||||||
|
cc = append(cc, c.Image)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(cc, ",")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package render
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
|
@ -17,19 +16,13 @@ import (
|
||||||
type Deployment struct{}
|
type Deployment struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Deployment) ColorerFunc() ColorerFunc {
|
func (d Deployment) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, r)
|
||||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
if !Happy(ns, r.Row) {
|
||||||
readyCol := 2
|
|
||||||
if !client.IsAllNamespaces(ns) {
|
|
||||||
readyCol--
|
|
||||||
}
|
|
||||||
tokens := strings.Split(r.Row.Fields[readyCol], "/")
|
|
||||||
if tokens[0] != tokens[1] {
|
|
||||||
return ErrColor
|
return ErrColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,6 +42,9 @@ func (Deployment) Header(ns string) HeaderRow {
|
||||||
Header{Name: "READY"},
|
Header{Name: "READY"},
|
||||||
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
||||||
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||||
|
Header{Name: "READY", Align: tview.AlignRight},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -73,11 +69,21 @@ func (d Deployment) Render(o interface{}, ns string, r *Row) error {
|
||||||
}
|
}
|
||||||
r.Fields = append(r.Fields,
|
r.Fields = append(r.Fields,
|
||||||
dp.Name,
|
dp.Name,
|
||||||
strconv.Itoa(int(dp.Status.AvailableReplicas))+"/"+strconv.Itoa(int(*dp.Spec.Replicas)),
|
strconv.Itoa(int(dp.Status.AvailableReplicas))+"/"+strconv.Itoa(int(dp.Status.Replicas)),
|
||||||
strconv.Itoa(int(dp.Status.UpdatedReplicas)),
|
strconv.Itoa(int(dp.Status.UpdatedReplicas)),
|
||||||
strconv.Itoa(int(dp.Status.AvailableReplicas)),
|
strconv.Itoa(int(dp.Status.AvailableReplicas)),
|
||||||
|
strconv.Itoa(int(dp.Status.ReadyReplicas)),
|
||||||
|
mapToStr(dp.Labels),
|
||||||
|
asStatus(d.diagnose(dp.Status.Replicas, dp.Status.AvailableReplicas)),
|
||||||
toAge(dp.ObjectMeta.CreationTimestamp),
|
toAge(dp.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (Deployment) diagnose(d, r int32) error {
|
||||||
|
if d != r {
|
||||||
|
return fmt.Errorf("desiring %d replicas got %d available", d, r)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package render
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
|
@ -17,18 +16,14 @@ import (
|
||||||
type DaemonSet struct{}
|
type DaemonSet struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (DaemonSet) ColorerFunc() ColorerFunc {
|
func (d DaemonSet) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, r)
|
||||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
desiredCol := 2
|
if !Happy(ns, r.Row) {
|
||||||
if !client.IsAllNamespaces(ns) {
|
|
||||||
desiredCol = 1
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(r.Row.Fields[desiredCol]) != strings.TrimSpace(r.Row.Fields[desiredCol+2]) {
|
|
||||||
return ErrColor
|
return ErrColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,6 +45,8 @@ func (DaemonSet) Header(ns string) HeaderRow {
|
||||||
Header{Name: "READY", Align: tview.AlignRight},
|
Header{Name: "READY", Align: tview.AlignRight},
|
||||||
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
||||||
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -78,8 +75,18 @@ func (d DaemonSet) Render(o interface{}, ns string, r *Row) error {
|
||||||
strconv.Itoa(int(ds.Status.NumberReady)),
|
strconv.Itoa(int(ds.Status.NumberReady)),
|
||||||
strconv.Itoa(int(ds.Status.UpdatedNumberScheduled)),
|
strconv.Itoa(int(ds.Status.UpdatedNumberScheduled)),
|
||||||
strconv.Itoa(int(ds.Status.NumberAvailable)),
|
strconv.Itoa(int(ds.Status.NumberAvailable)),
|
||||||
|
mapToStr(ds.Labels),
|
||||||
|
asStatus(d.diagnose(ds.Status.DesiredNumberScheduled, ds.Status.NumberReady)),
|
||||||
toAge(ds.ObjectMeta.CreationTimestamp),
|
toAge(ds.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Happy returns true if resoure is happy, false otherwise
|
||||||
|
func (DaemonSet) diagnose(d, r int32) error {
|
||||||
|
if d != r {
|
||||||
|
return fmt.Errorf("desiring %d replicas but %d ready", d, r)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -17,19 +18,20 @@ import (
|
||||||
type Event struct{}
|
type Event struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Event) ColorerFunc() ColorerFunc {
|
func (e Event) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, r)
|
||||||
|
|
||||||
|
if !Happy(ns, r.Row) {
|
||||||
|
return ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
markCol := 3
|
markCol := 3
|
||||||
if !client.IsAllNamespaces(ns) {
|
if !client.IsAllNamespaces(ns) {
|
||||||
markCol = 2
|
markCol = 2
|
||||||
}
|
}
|
||||||
switch strings.TrimSpace(r.Row.Fields[markCol]) {
|
if strings.TrimSpace(r.Row.Fields[markCol]) == "Killing" {
|
||||||
case "Failed":
|
return KillColor
|
||||||
c = ErrColor
|
|
||||||
case "Killing":
|
|
||||||
c = KillColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
|
|
@ -45,10 +47,12 @@ func (Event) Header(ns string) HeaderRow {
|
||||||
|
|
||||||
return append(h,
|
return append(h,
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
|
Header{Name: "TYPE"},
|
||||||
Header{Name: "REASON"},
|
Header{Name: "REASON"},
|
||||||
Header{Name: "SOURCE"},
|
Header{Name: "SOURCE"},
|
||||||
Header{Name: "COUNT", Align: tview.AlignRight},
|
Header{Name: "COUNT", Align: tview.AlignRight},
|
||||||
Header{Name: "MESSAGE"},
|
Header{Name: "MESSAGE", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -72,15 +76,27 @@ func (e Event) Render(o interface{}, ns string, r *Row) error {
|
||||||
}
|
}
|
||||||
r.Fields = append(r.Fields,
|
r.Fields = append(r.Fields,
|
||||||
asRef(ev.InvolvedObject),
|
asRef(ev.InvolvedObject),
|
||||||
|
ev.Type,
|
||||||
ev.Reason,
|
ev.Reason,
|
||||||
ev.Source.Component,
|
ev.Source.Component,
|
||||||
strconv.Itoa(int(ev.Count)),
|
strconv.Itoa(int(ev.Count)),
|
||||||
ev.Message,
|
ev.Message,
|
||||||
|
asStatus(e.diagnose(ev.Type)),
|
||||||
toAge(ev.LastTimestamp))
|
toAge(ev.LastTimestamp))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Happy returns true if resoure is happy, false otherwise
|
||||||
|
func (Event) diagnose(kind string) error {
|
||||||
|
if kind != "Normal" {
|
||||||
|
return errors.New("failed event")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
func asRef(r v1.ObjectReference) string {
|
func asRef(r v1.ObjectReference) string {
|
||||||
return strings.ToLower(r.Kind) + ":" + r.Name
|
return strings.ToLower(r.Kind) + ":" + r.Name
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,17 @@ func TestEventRender(t *testing.T) {
|
||||||
c.Render(load(t, "ev"), "", &r)
|
c.Render(load(t, "ev"), "", &r)
|
||||||
|
|
||||||
assert.Equal(t, "default/hello-1567197780-mn4mv.15bfce150bd764dd", r.ID)
|
assert.Equal(t, "default/hello-1567197780-mn4mv.15bfce150bd764dd", r.ID)
|
||||||
assert.Equal(t, render.Fields{"default", "pod:hello-1567197780-mn4mv", "Pulled", "kubelet", "1", `Successfully pulled image "blang/busybox-bash"`}, r.Fields[:6])
|
assert.Equal(t, render.Fields{"default", "pod:hello-1567197780-mn4mv", "Normal", "Pulled", "kubelet", "1", `Successfully pulled image "blang/busybox-bash"`}, r.Fields[:7])
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkEventRender(b *testing.B) {
|
||||||
|
ev := load(b, "ev")
|
||||||
|
var re render.Event
|
||||||
|
r := render.NewRow(7)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = re.Render(&ev, "", &r)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,11 @@ type Generic struct {
|
||||||
ageIndex int
|
ageIndex int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Happy returns true if resoure is happy, false otherwise
|
||||||
|
func (Generic) Happy(ns string, r Row) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// SetTable sets the tabular resource.
|
// SetTable sets the tabular resource.
|
||||||
func (g *Generic) SetTable(t *metav1beta1.Table) {
|
func (g *Generic) SetTable(t *metav1beta1.Table) {
|
||||||
g.table = t
|
g.table = t
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,12 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/util/duration"
|
"k8s.io/apimachinery/pkg/util/duration"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Happy returns true if resoure is happy, false otherwise
|
||||||
|
func Happy(ns string, r Row) bool {
|
||||||
|
validCol := r.Len() - 2
|
||||||
|
return strings.TrimSpace(r.Fields[validCol]) == ""
|
||||||
|
}
|
||||||
|
|
||||||
const megaByte = 1024 * 1024
|
const megaByte = 1024 * 1024
|
||||||
|
|
||||||
// ToMB converts bytes to megabytes.
|
// ToMB converts bytes to megabytes.
|
||||||
|
|
@ -20,6 +26,13 @@ func ToMB(v int64) float64 {
|
||||||
return float64(v) / megaByte
|
return float64(v) / megaByte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func asStatus(err error) string {
|
||||||
|
if err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
func asSelector(s *metav1.LabelSelector) string {
|
func asSelector(s *metav1.LabelSelector) string {
|
||||||
sel, err := metav1.LabelSelectorAsSelector(s)
|
sel, err := metav1.LabelSelectorAsSelector(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -84,7 +97,7 @@ func join(a []string, sep string) string {
|
||||||
|
|
||||||
var buff strings.Builder
|
var buff strings.Builder
|
||||||
buff.Grow(n)
|
buff.Grow(n)
|
||||||
buff.WriteString(a[0])
|
buff.WriteString(b[0])
|
||||||
for _, s := range b[1:] {
|
for _, s := range b[1:] {
|
||||||
buff.WriteString(sep)
|
buff.WriteString(sep)
|
||||||
buff.WriteString(s)
|
buff.WriteString(s)
|
||||||
|
|
@ -163,7 +176,40 @@ func mapToStr(m map[string]string) (s string) {
|
||||||
for i, k := range kk {
|
for i, k := range kk {
|
||||||
s += k + "=" + m[k]
|
s += k + "=" + m[k]
|
||||||
if i < len(kk)-1 {
|
if i < len(kk)-1 {
|
||||||
s += ","
|
s += " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapToIfc(m interface{}) (s string) {
|
||||||
|
if m == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
mm, ok := m.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if len(mm) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
kk := make([]string, 0, len(mm))
|
||||||
|
for k := range mm {
|
||||||
|
kk = append(kk, k)
|
||||||
|
}
|
||||||
|
sort.Strings(kk)
|
||||||
|
|
||||||
|
for i, k := range kk {
|
||||||
|
str, ok := mm[k].(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s += k + "=" + str
|
||||||
|
if i < len(kk)-1 {
|
||||||
|
s += " "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,10 +82,11 @@ func TestJoin(t *testing.T) {
|
||||||
i []string
|
i []string
|
||||||
e string
|
e string
|
||||||
}{
|
}{
|
||||||
"zero": {[]string{}, ""},
|
"zero": {[]string{}, ""},
|
||||||
"std": {[]string{"a", "b", "c"}, "a,b,c"},
|
"std": {[]string{"a", "b", "c"}, "a,b,c"},
|
||||||
"blank": {[]string{"", "", ""}, ""},
|
"blank": {[]string{"", "", ""}, ""},
|
||||||
"sparse": {[]string{"a", "", "c"}, "a,c"},
|
"sparse": {[]string{"a", "", "c"}, "a,c"},
|
||||||
|
"withBlank": {[]string{"", "a", "c"}, "a,c"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for k := range uu {
|
for k := range uu {
|
||||||
|
|
@ -304,7 +305,7 @@ func TestMapToStr(t *testing.T) {
|
||||||
i map[string]string
|
i map[string]string
|
||||||
e string
|
e string
|
||||||
}{
|
}{
|
||||||
{map[string]string{"blee": "duh", "aa": "bb"}, "aa=bb,blee=duh"},
|
{map[string]string{"blee": "duh", "aa": "bb"}, "aa=bb blee=duh"},
|
||||||
{map[string]string{}, ""},
|
{map[string]string{}, ""},
|
||||||
}
|
}
|
||||||
for _, u := range uu {
|
for _, u := range uu {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ func (HorizontalPodAutoscaler) Header(ns string) HeaderRow {
|
||||||
Header{Name: "MINPODS", Align: tview.AlignRight},
|
Header{Name: "MINPODS", Align: tview.AlignRight},
|
||||||
Header{Name: "MAXPODS", Align: tview.AlignRight},
|
Header{Name: "MAXPODS", Align: tview.AlignRight},
|
||||||
Header{Name: "REPLICAS", Align: tview.AlignRight},
|
Header{Name: "REPLICAS", Align: tview.AlignRight},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -80,6 +81,7 @@ func (h HorizontalPodAutoscaler) renderV1(raw *unstructured.Unstructured, ns str
|
||||||
strconv.Itoa(int(*hpa.Spec.MinReplicas)),
|
strconv.Itoa(int(*hpa.Spec.MinReplicas)),
|
||||||
strconv.Itoa(int(hpa.Spec.MaxReplicas)),
|
strconv.Itoa(int(hpa.Spec.MaxReplicas)),
|
||||||
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
||||||
|
"",
|
||||||
toAge(hpa.ObjectMeta.CreationTimestamp),
|
toAge(hpa.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -106,6 +108,7 @@ func (h HorizontalPodAutoscaler) renderV2b1(raw *unstructured.Unstructured, ns s
|
||||||
strconv.Itoa(int(*hpa.Spec.MinReplicas)),
|
strconv.Itoa(int(*hpa.Spec.MinReplicas)),
|
||||||
strconv.Itoa(int(hpa.Spec.MaxReplicas)),
|
strconv.Itoa(int(hpa.Spec.MaxReplicas)),
|
||||||
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
||||||
|
"",
|
||||||
toAge(hpa.ObjectMeta.CreationTimestamp),
|
toAge(hpa.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -132,6 +135,7 @@ func (h HorizontalPodAutoscaler) renderV2b2(raw *unstructured.Unstructured, ns s
|
||||||
strconv.Itoa(int(*hpa.Spec.MinReplicas)),
|
strconv.Itoa(int(*hpa.Spec.MinReplicas)),
|
||||||
strconv.Itoa(int(hpa.Spec.MaxReplicas)),
|
strconv.Itoa(int(hpa.Spec.MaxReplicas)),
|
||||||
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
||||||
|
"",
|
||||||
toAge(hpa.ObjectMeta.CreationTimestamp),
|
toAge(hpa.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ func (Ingress) Header(ns string) HeaderRow {
|
||||||
Header{Name: "HOSTS"},
|
Header{Name: "HOSTS"},
|
||||||
Header{Name: "ADDRESS"},
|
Header{Name: "ADDRESS"},
|
||||||
Header{Name: "PORT"},
|
Header{Name: "PORT"},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -57,6 +58,7 @@ func (i Ingress) Render(o interface{}, ns string, r *Row) error {
|
||||||
toHosts(ing.Spec.Rules),
|
toHosts(ing.Spec.Rules),
|
||||||
toAddress(ing.Status.LoadBalancer),
|
toAddress(ing.Status.LoadBalancer),
|
||||||
toTLSPorts(ing.Spec.TLS),
|
toTLSPorts(ing.Spec.TLS),
|
||||||
|
"",
|
||||||
toAge(ing.ObjectMeta.CreationTimestamp),
|
toAge(ing.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
batchv1 "k8s.io/api/batch/v1"
|
batchv1 "k8s.io/api/batch/v1"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/duration"
|
"k8s.io/apimachinery/pkg/util/duration"
|
||||||
|
|
@ -33,8 +34,10 @@ func (Job) Header(ns string) HeaderRow {
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "COMPLETIONS"},
|
Header{Name: "COMPLETIONS"},
|
||||||
Header{Name: "DURATION"},
|
Header{Name: "DURATION"},
|
||||||
Header{Name: "CONTAINERS"},
|
Header{Name: "SELECTOR", Wide: true},
|
||||||
Header{Name: "IMAGES"},
|
Header{Name: "CONTAINERS", Wide: true},
|
||||||
|
Header{Name: "IMAGES", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -50,6 +53,7 @@ func (j Job) Render(o interface{}, ns string, r *Row) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
ready := toCompletion(job.Spec, job.Status)
|
||||||
|
|
||||||
r.ID = client.MetaFQN(job.ObjectMeta)
|
r.ID = client.MetaFQN(job.ObjectMeta)
|
||||||
r.Fields = make(Fields, 0, len(j.Header(ns)))
|
r.Fields = make(Fields, 0, len(j.Header(ns)))
|
||||||
|
|
@ -59,16 +63,29 @@ func (j Job) Render(o interface{}, ns string, r *Row) error {
|
||||||
cc, ii := toContainers(job.Spec.Template.Spec)
|
cc, ii := toContainers(job.Spec.Template.Spec)
|
||||||
r.Fields = append(r.Fields,
|
r.Fields = append(r.Fields,
|
||||||
job.Name,
|
job.Name,
|
||||||
toCompletion(job.Spec, job.Status),
|
ready,
|
||||||
toDuration(job.Status),
|
toDuration(job.Status),
|
||||||
|
jobSelector(job.Spec),
|
||||||
cc,
|
cc,
|
||||||
ii,
|
ii,
|
||||||
|
asStatus(j.diagnose(ready, job.Status.CompletionTime)),
|
||||||
toAge(job.ObjectMeta.CreationTimestamp),
|
toAge(job.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (Job) diagnose(ready string, completed *metav1.Time) error {
|
||||||
|
if completed == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tokens := strings.Split(ready, "/")
|
||||||
|
if tokens[0] != tokens[1] {
|
||||||
|
return fmt.Errorf("expecting %s completion got %s", tokens[1], tokens[0])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestJobRender(t *testing.T) {
|
||||||
c.Render(load(t, "job"), "", &r)
|
c.Render(load(t, "job"), "", &r)
|
||||||
|
|
||||||
assert.Equal(t, "default/hello-1567179180", r.ID)
|
assert.Equal(t, "default/hello-1567179180", r.ID)
|
||||||
assert.Equal(t, render.Fields{"default", "hello-1567179180", "1/1", "8s", "c1", "blang/busybox-bash"}, r.Fields[:6])
|
assert.Equal(t, render.Fields{"default", "hello-1567179180", "1/1", "8s", "controller-uid=7473e6d0-cb3b-11e9-990f-42010a800218", "c1", "blang/busybox-bash"}, r.Fields[:7])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
@ -22,8 +25,16 @@ const (
|
||||||
type Node struct{}
|
type Node struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Node) ColorerFunc() ColorerFunc {
|
func (n Node) ColorerFunc() ColorerFunc {
|
||||||
return DefaultColorer
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
|
c := DefaultColorer(ns, r)
|
||||||
|
if !Happy(ns, r.Row) {
|
||||||
|
return ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header row.
|
// Header returns a header row.
|
||||||
|
|
@ -31,17 +42,19 @@ func (Node) Header(_ string) HeaderRow {
|
||||||
return HeaderRow{
|
return HeaderRow{
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "STATUS"},
|
Header{Name: "STATUS"},
|
||||||
Header{Name: "ROLE"},
|
Header{Name: "ROLE", Wide: true},
|
||||||
Header{Name: "VERSION"},
|
Header{Name: "VERSION", Wide: true},
|
||||||
Header{Name: "KERNEL"},
|
Header{Name: "KERNEL", Wide: true},
|
||||||
Header{Name: "INTERNAL-IP"},
|
Header{Name: "INTERNAL-IP", Wide: true},
|
||||||
Header{Name: "EXTERNAL-IP"},
|
Header{Name: "EXTERNAL-IP", Wide: true},
|
||||||
Header{Name: "CPU", Align: tview.AlignRight},
|
Header{Name: "CPU", Align: tview.AlignRight},
|
||||||
Header{Name: "MEM", Align: tview.AlignRight},
|
Header{Name: "MEM", Align: tview.AlignRight},
|
||||||
Header{Name: "%CPU", Align: tview.AlignRight},
|
Header{Name: "%CPU", Align: tview.AlignRight},
|
||||||
Header{Name: "%MEM", Align: tview.AlignRight},
|
Header{Name: "%MEM", Align: tview.AlignRight},
|
||||||
Header{Name: "ACPU", Align: tview.AlignRight},
|
Header{Name: "ACPU", Align: tview.AlignRight},
|
||||||
Header{Name: "AMEM", Align: tview.AlignRight},
|
Header{Name: "AMEM", Align: tview.AlignRight},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -69,17 +82,19 @@ func (n Node) Render(o interface{}, ns string, r *Row) error {
|
||||||
|
|
||||||
c, a, p := gatherNodeMX(&no, oo.MX)
|
c, a, p := gatherNodeMX(&no, oo.MX)
|
||||||
|
|
||||||
sta := make([]string, 10)
|
statuses := make(sort.StringSlice, 10)
|
||||||
status(no.Status, no.Spec.Unschedulable, sta)
|
status(no.Status, no.Spec.Unschedulable, statuses)
|
||||||
ro := make([]string, 10)
|
sort.Sort(statuses)
|
||||||
nodeRoles(&no, ro)
|
roles := make(sort.StringSlice, 10)
|
||||||
|
nodeRoles(&no, roles)
|
||||||
|
sort.Sort(roles)
|
||||||
|
|
||||||
r.ID = client.FQN("", na)
|
r.ID = client.FQN("", na)
|
||||||
r.Fields = make(Fields, 0, len(n.Header(ns)))
|
r.Fields = make(Fields, 0, len(n.Header(ns)))
|
||||||
r.Fields = append(r.Fields,
|
r.Fields = append(r.Fields,
|
||||||
no.Name,
|
no.Name,
|
||||||
join(sta, ","),
|
join(statuses, ","),
|
||||||
join(ro, ","),
|
join(roles, ","),
|
||||||
no.Status.NodeInfo.KubeletVersion,
|
no.Status.NodeInfo.KubeletVersion,
|
||||||
no.Status.NodeInfo.KernelVersion,
|
no.Status.NodeInfo.KernelVersion,
|
||||||
iIP,
|
iIP,
|
||||||
|
|
@ -90,12 +105,27 @@ func (n Node) Render(o interface{}, ns string, r *Row) error {
|
||||||
p.mem,
|
p.mem,
|
||||||
a.cpu,
|
a.cpu,
|
||||||
a.mem,
|
a.mem,
|
||||||
|
mapToStr(no.Labels),
|
||||||
|
asStatus(n.diagnose(statuses)),
|
||||||
toAge(no.ObjectMeta.CreationTimestamp),
|
toAge(no.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (Node) diagnose(ss []string) error {
|
||||||
|
if len(ss) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, s := range ss {
|
||||||
|
if s == "Ready" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("node is not ready")
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
|
@ -156,6 +186,9 @@ func nodeRoles(node *v1.Node, res []string) {
|
||||||
res[index] = v
|
res[index] = v
|
||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
|
if index >= len(res) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if empty(res) {
|
if empty(res) {
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,14 @@ func (NetworkPolicy) Header(ns string) HeaderRow {
|
||||||
|
|
||||||
return append(h,
|
return append(h,
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "ING-SELECTOR"},
|
Header{Name: "ING-SELECTOR", Wide: true},
|
||||||
Header{Name: "ING-PORTS"},
|
Header{Name: "ING-PORTS"},
|
||||||
Header{Name: "ING-BLOCK"},
|
Header{Name: "ING-BLOCK"},
|
||||||
Header{Name: "EGR-SELECTOR"},
|
Header{Name: "EGR-SELECTOR", Wide: true},
|
||||||
Header{Name: "EGR-PORTS"},
|
Header{Name: "EGR-PORTS"},
|
||||||
Header{Name: "EGR-BLOCK"},
|
Header{Name: "EGR-BLOCK"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -66,6 +68,8 @@ func (n NetworkPolicy) Render(o interface{}, ns string, r *Row) error {
|
||||||
es,
|
es,
|
||||||
ep,
|
ep,
|
||||||
eb,
|
eb,
|
||||||
|
mapToStr(np.Labels),
|
||||||
|
"",
|
||||||
toAge(np.ObjectMeta.CreationTimestamp),
|
toAge(np.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -15,7 +16,7 @@ import (
|
||||||
type Namespace struct{}
|
type Namespace struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Namespace) ColorerFunc() ColorerFunc {
|
func (n Namespace) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, r)
|
||||||
if r.Kind == EventAdd {
|
if r.Kind == EventAdd {
|
||||||
|
|
@ -25,9 +26,8 @@ func (Namespace) ColorerFunc() ColorerFunc {
|
||||||
if r.Kind == EventUpdate {
|
if r.Kind == EventUpdate {
|
||||||
c = StdColor
|
c = StdColor
|
||||||
}
|
}
|
||||||
switch strings.TrimSpace(r.Row.Fields[1]) {
|
if !Happy(ns, r.Row) {
|
||||||
case "Inactive", Terminating:
|
return ErrColor
|
||||||
c = ErrColor
|
|
||||||
}
|
}
|
||||||
if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") {
|
if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") {
|
||||||
c = HighlightColor
|
c = HighlightColor
|
||||||
|
|
@ -42,12 +42,14 @@ func (Namespace) Header(string) HeaderRow {
|
||||||
return HeaderRow{
|
return HeaderRow{
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "STATUS"},
|
Header{Name: "STATUS"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render renders a K8s resource to screen.
|
// Render renders a K8s resource to screen.
|
||||||
func (Namespace) Render(o interface{}, _ string, r *Row) error {
|
func (n Namespace) Render(o interface{}, _ string, r *Row) error {
|
||||||
raw, ok := o.(*unstructured.Unstructured)
|
raw, ok := o.(*unstructured.Unstructured)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("Expected Namespace, but got %T", o)
|
return fmt.Errorf("Expected Namespace, but got %T", o)
|
||||||
|
|
@ -62,8 +64,17 @@ func (Namespace) Render(o interface{}, _ string, r *Row) error {
|
||||||
r.Fields = Fields{
|
r.Fields = Fields{
|
||||||
ns.Name,
|
ns.Name,
|
||||||
string(ns.Status.Phase),
|
string(ns.Status.Phase),
|
||||||
|
mapToStr(ns.Labels),
|
||||||
|
asStatus(n.diagnose(ns.Status.Phase)),
|
||||||
toAge(ns.ObjectMeta.CreationTimestamp),
|
toAge(ns.ObjectMeta.CreationTimestamp),
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (Namespace) diagnose(phase v1.NamespacePhase) error {
|
||||||
|
if phase != v1.NamespaceActive && phase != v1.NamespaceTerminating {
|
||||||
|
return errors.New("namespace not ready")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -23,8 +24,12 @@ const (
|
||||||
type OpenFaas struct{}
|
type OpenFaas struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (OpenFaas) ColorerFunc() ColorerFunc {
|
func (o OpenFaas) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, re RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
|
if !Happy(ns, re.Row) {
|
||||||
|
return ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
return tcell.ColorPaleTurquoise
|
return tcell.ColorPaleTurquoise
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -44,6 +49,7 @@ func (OpenFaas) Header(ns string) HeaderRow {
|
||||||
Header{Name: "INVOCATIONS", Align: tview.AlignRight},
|
Header{Name: "INVOCATIONS", Align: tview.AlignRight},
|
||||||
Header{Name: "REPLICAS", Align: tview.AlignRight},
|
Header{Name: "REPLICAS", Align: tview.AlignRight},
|
||||||
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -77,12 +83,21 @@ func (f OpenFaas) Render(o interface{}, ns string, r *Row) error {
|
||||||
strconv.Itoa(int(fn.Function.InvocationCount)),
|
strconv.Itoa(int(fn.Function.InvocationCount)),
|
||||||
strconv.Itoa(int(fn.Function.Replicas)),
|
strconv.Itoa(int(fn.Function.Replicas)),
|
||||||
strconv.Itoa(int(fn.Function.AvailableReplicas)),
|
strconv.Itoa(int(fn.Function.AvailableReplicas)),
|
||||||
|
asStatus(f.diagnose(status)),
|
||||||
toAge(metav1.Time{Time: time.Now()}),
|
toAge(metav1.Time{Time: time.Now()}),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (OpenFaas) diagnose(status string) error {
|
||||||
|
if status != "Ready" {
|
||||||
|
return errors.New("function not ready")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package render
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
|
@ -18,24 +17,19 @@ import (
|
||||||
type PodDisruptionBudget struct{}
|
type PodDisruptionBudget struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (PodDisruptionBudget) ColorerFunc() ColorerFunc {
|
func (p PodDisruptionBudget) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, re)
|
||||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
markCol := 5
|
if !Happy(ns, re.Row) {
|
||||||
if !client.IsAllNamespaces(ns) {
|
|
||||||
markCol--
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) {
|
|
||||||
return ErrColor
|
return ErrColor
|
||||||
}
|
}
|
||||||
|
|
||||||
return StdColor
|
return StdColor
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header row.
|
// Header returns a header row.
|
||||||
|
|
@ -53,6 +47,8 @@ func (PodDisruptionBudget) Header(ns string) HeaderRow {
|
||||||
Header{Name: "CURRENT", Align: tview.AlignRight},
|
Header{Name: "CURRENT", Align: tview.AlignRight},
|
||||||
Header{Name: "DESIRED", Align: tview.AlignRight},
|
Header{Name: "DESIRED", Align: tview.AlignRight},
|
||||||
Header{Name: "EXPECTED", Align: tview.AlignRight},
|
Header{Name: "EXPECTED", Align: tview.AlignRight},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -82,12 +78,21 @@ func (p PodDisruptionBudget) Render(o interface{}, ns string, r *Row) error {
|
||||||
strconv.Itoa(int(pdb.Status.CurrentHealthy)),
|
strconv.Itoa(int(pdb.Status.CurrentHealthy)),
|
||||||
strconv.Itoa(int(pdb.Status.DesiredHealthy)),
|
strconv.Itoa(int(pdb.Status.DesiredHealthy)),
|
||||||
strconv.Itoa(int(pdb.Status.ExpectedPods)),
|
strconv.Itoa(int(pdb.Status.ExpectedPods)),
|
||||||
|
mapToStr(pdb.Labels),
|
||||||
|
asStatus(p.diagnose(pdb.Spec.MinAvailable.IntVal, pdb.Status.CurrentHealthy)),
|
||||||
toAge(pdb.ObjectMeta.CreationTimestamp),
|
toAge(pdb.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (PodDisruptionBudget) diagnose(min, healthy int32) error {
|
||||||
|
if min > healthy {
|
||||||
|
return fmt.Errorf("expected %d but got %d", min, healthy)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
func numbToStr(n *intstr.IntOrString) string {
|
func numbToStr(n *intstr.IntOrString) string {
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,11 @@ func (p Pod) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, re RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, re)
|
c := DefaultColorer(ns, re)
|
||||||
|
|
||||||
readyCol := 2
|
statusCol := 4
|
||||||
if !client.IsAllNamespaces(ns) {
|
if !client.IsAllNamespaces(ns) {
|
||||||
readyCol--
|
statusCol--
|
||||||
}
|
}
|
||||||
statusCol := readyCol + 1
|
status := strings.TrimSpace(re.Row.Fields[statusCol])
|
||||||
|
|
||||||
ready, status := strings.TrimSpace(re.Row.Fields[readyCol]), strings.TrimSpace(re.Row.Fields[statusCol])
|
|
||||||
c = p.checkReadyCol(ready, status, c)
|
|
||||||
|
|
||||||
switch status {
|
switch status {
|
||||||
case ContainerCreating, PodInitializing:
|
case ContainerCreating, PodInitializing:
|
||||||
c = AddColor
|
c = AddColor
|
||||||
|
|
@ -42,28 +38,19 @@ func (p Pod) ColorerFunc() ColorerFunc {
|
||||||
case Completed:
|
case Completed:
|
||||||
c = CompletedColor
|
c = CompletedColor
|
||||||
case Running:
|
case Running:
|
||||||
|
c = StdColor
|
||||||
case Terminating:
|
case Terminating:
|
||||||
c = KillColor
|
c = KillColor
|
||||||
default:
|
default:
|
||||||
c = ErrColor
|
if !Happy(ns, re.Row) {
|
||||||
|
c = ErrColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Pod) checkReadyCol(readyCol, statusCol string, c tcell.Color) tcell.Color {
|
|
||||||
if statusCol == "Completed" {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens := strings.Split(readyCol, "/")
|
|
||||||
if len(tokens) == 2 && (tokens[0] == "0" || tokens[0] != tokens[1]) {
|
|
||||||
return ErrColor
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header returns a header row.
|
// Header returns a header row.
|
||||||
func (Pod) Header(ns string) HeaderRow {
|
func (Pod) Header(ns string) HeaderRow {
|
||||||
var h HeaderRow
|
var h HeaderRow
|
||||||
|
|
@ -74,17 +61,19 @@ func (Pod) Header(ns string) HeaderRow {
|
||||||
return append(h,
|
return append(h,
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "READY"},
|
Header{Name: "READY"},
|
||||||
Header{Name: "STATUS"},
|
|
||||||
Header{Name: "RS", Align: tview.AlignRight},
|
Header{Name: "RS", Align: tview.AlignRight},
|
||||||
|
Header{Name: "STATUS"},
|
||||||
Header{Name: "CPU", Align: tview.AlignRight},
|
Header{Name: "CPU", Align: tview.AlignRight},
|
||||||
Header{Name: "MEM", Align: tview.AlignRight},
|
Header{Name: "MEM", Align: tview.AlignRight},
|
||||||
Header{Name: "%CPU/R", Align: tview.AlignRight},
|
Header{Name: "%CPU/R", Align: tview.AlignRight},
|
||||||
Header{Name: "%MEM/R", Align: tview.AlignRight},
|
Header{Name: "%MEM/R", Align: tview.AlignRight},
|
||||||
Header{Name: "%CPU/L", Align: tview.AlignRight},
|
Header{Name: "%CPU/L", Align: tview.AlignRight},
|
||||||
Header{Name: "%MEM/L", Align: tview.AlignRight},
|
Header{Name: "%MEM/L", Align: tview.AlignRight},
|
||||||
Header{Name: "IP"},
|
Header{Name: "IP", Wide: true},
|
||||||
Header{Name: "NODE"},
|
Header{Name: "NODE", Wide: true},
|
||||||
Header{Name: "QOS"},
|
Header{Name: "QOS", Wide: true},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +94,7 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
||||||
ss := po.Status.ContainerStatuses
|
ss := po.Status.ContainerStatuses
|
||||||
cr, _, rc := p.Statuses(ss)
|
cr, _, rc := p.Statuses(ss)
|
||||||
c, perc := p.gatherPodMX(&po, pwm.MX)
|
c, perc := p.gatherPodMX(&po, pwm.MX)
|
||||||
|
phase := p.Phase(&po)
|
||||||
r.ID = client.MetaFQN(po.ObjectMeta)
|
r.ID = client.MetaFQN(po.ObjectMeta)
|
||||||
r.Fields = make(Fields, 0, len(p.Header(ns)))
|
r.Fields = make(Fields, 0, len(p.Header(ns)))
|
||||||
if client.IsAllNamespaces(ns) {
|
if client.IsAllNamespaces(ns) {
|
||||||
|
|
@ -114,8 +103,8 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
||||||
r.Fields = append(r.Fields,
|
r.Fields = append(r.Fields,
|
||||||
po.ObjectMeta.Name,
|
po.ObjectMeta.Name,
|
||||||
strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)),
|
strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)),
|
||||||
p.Phase(&po),
|
|
||||||
strconv.Itoa(rc),
|
strconv.Itoa(rc),
|
||||||
|
phase,
|
||||||
c.cpu,
|
c.cpu,
|
||||||
c.mem,
|
c.mem,
|
||||||
perc.cpu,
|
perc.cpu,
|
||||||
|
|
@ -125,12 +114,25 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
||||||
na(po.Status.PodIP),
|
na(po.Status.PodIP),
|
||||||
na(po.Spec.NodeName),
|
na(po.Spec.NodeName),
|
||||||
p.mapQOS(po.Status.QOSClass),
|
p.mapQOS(po.Status.QOSClass),
|
||||||
|
mapToStr(po.Labels),
|
||||||
|
asStatus(p.diagnose(phase, cr, len(ss))),
|
||||||
toAge(po.ObjectMeta.CreationTimestamp),
|
toAge(po.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p Pod) diagnose(phase string, cr, ct int) error {
|
||||||
|
if phase == "Completed" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cr != ct {
|
||||||
|
return fmt.Errorf("container ready check failed: %d of %d", cr, ct)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,12 @@ type (
|
||||||
|
|
||||||
func TestPodColorer(t *testing.T) {
|
func TestPodColorer(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
nsRow = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "Running"}}
|
nsRow = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "0", "Running"}}
|
||||||
toastNS = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "Boom"}}
|
toastNS = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "0", "Boom"}}
|
||||||
notReadyNS = render.Row{Fields: render.Fields{"blee", "fred", "0/1", "Boom"}}
|
notReadyNS = render.Row{Fields: render.Fields{"blee", "fred", "0/1", "0", "Boom"}}
|
||||||
row = render.Row{Fields: render.Fields{"fred", "1/1", "Running"}}
|
row = render.Row{Fields: render.Fields{"fred", "1/1", "0", "Running"}}
|
||||||
toast = render.Row{Fields: render.Fields{"fred", "1/1", "Boom"}}
|
toast = render.Row{Fields: render.Fields{"fred", "1/1", "0", "Boom"}}
|
||||||
notReady = render.Row{Fields: render.Fields{"fred", "0/1", "Boom"}}
|
notReady = render.Row{Fields: render.Fields{"fred", "0/1", "0", "Boom"}}
|
||||||
)
|
)
|
||||||
|
|
||||||
uu := colorerUCs{
|
uu := colorerUCs{
|
||||||
|
|
@ -70,7 +70,7 @@ func TestPodRender(t *testing.T) {
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "default/nginx", r.ID)
|
assert.Equal(t, "default/nginx", r.ID)
|
||||||
e := render.Fields{"default", "nginx", "1/1", "Running", "0", "10", "10", "10", "14", "0", "5", "172.17.0.6", "minikube", "BE"}
|
e := render.Fields{"default", "nginx", "1/1", "0", "Running", "10", "10", "10", "14", "0", "5", "172.17.0.6", "minikube", "BE"}
|
||||||
assert.Equal(t, e, r.Fields[:14])
|
assert.Equal(t, e, r.Fields[:14])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,7 +101,7 @@ func TestPodInitRender(t *testing.T) {
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "default/nginx", r.ID)
|
assert.Equal(t, "default/nginx", r.ID)
|
||||||
e := render.Fields{"default", "nginx", "1/1", "Init:0/1", "0", "10", "10", "10", "14", "0", "5", "172.17.0.6", "minikube", "BE"}
|
e := render.Fields{"default", "nginx", "1/1", "0", "Init:0/1", "10", "10", "10", "14", "0", "5", "172.17.0.6", "minikube", "BE"}
|
||||||
assert.Equal(t, e, r.Fields[:14])
|
assert.Equal(t, e, r.Fields[:14])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ func rbacVerbHeader() HeaderRow {
|
||||||
Header{Name: "PATCH "},
|
Header{Name: "PATCH "},
|
||||||
Header{Name: "UPDATE"},
|
Header{Name: "UPDATE"},
|
||||||
Header{Name: "DELETE"},
|
Header{Name: "DELETE"},
|
||||||
Header{Name: "DLIST "},
|
Header{Name: "DEL-LIST "},
|
||||||
Header{Name: "EXTRAS"},
|
Header{Name: "EXTRAS", Wide: true},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +41,10 @@ func (Policy) Header(ns string) HeaderRow {
|
||||||
Header{Name: "API GROUP"},
|
Header{Name: "API GROUP"},
|
||||||
Header{Name: "BINDING"},
|
Header{Name: "BINDING"},
|
||||||
}
|
}
|
||||||
return append(h, rbacVerbHeader()...)
|
h = append(h, rbacVerbHeader()...)
|
||||||
|
h = append(h, Header{Name: "VALID", Wide: true})
|
||||||
|
|
||||||
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render renders a K8s resource to screen.
|
// Render renders a K8s resource to screen.
|
||||||
|
|
@ -52,8 +55,14 @@ func (Policy) Render(o interface{}, gvr string, r *Row) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
r.ID = client.FQN(p.Namespace, p.Resource)
|
r.ID = client.FQN(p.Namespace, p.Resource)
|
||||||
r.Fields = append(r.Fields, p.Namespace, cleanseResource(p.Resource), p.Group, p.Binding)
|
r.Fields = append(r.Fields,
|
||||||
|
p.Namespace,
|
||||||
|
cleanseResource(p.Resource),
|
||||||
|
p.Group,
|
||||||
|
p.Binding,
|
||||||
|
)
|
||||||
r.Fields = append(r.Fields, asVerbs(p.Verbs)...)
|
r.Fields = append(r.Fields, asVerbs(p.Verbs)...)
|
||||||
|
r.Fields = append(r.Fields, "")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,5 +37,6 @@ func TestPolicyRender(t *testing.T) {
|
||||||
"[orangered::b] 𐄂 [::]",
|
"[orangered::b] 𐄂 [::]",
|
||||||
"[orangered::b] 𐄂 [::]",
|
"[orangered::b] 𐄂 [::]",
|
||||||
"",
|
"",
|
||||||
|
"",
|
||||||
}, r.Fields)
|
}, r.Fields)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ func TestPortForwardRender(t *testing.T) {
|
||||||
"http://0.0.0.0:p1/",
|
"http://0.0.0.0:p1/",
|
||||||
"1",
|
"1",
|
||||||
"1",
|
"1",
|
||||||
|
"",
|
||||||
"2m",
|
"2m",
|
||||||
}, r.Fields)
|
}, r.Fields)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ func (PortForward) Header(ns string) HeaderRow {
|
||||||
Header{Name: "URL"},
|
Header{Name: "URL"},
|
||||||
Header{Name: "C"},
|
Header{Name: "C"},
|
||||||
Header{Name: "N"},
|
Header{Name: "N"},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +72,7 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error {
|
||||||
UrlFor(pf.Config.Host, pf.Config.Path, ports[0]),
|
UrlFor(pf.Config.Host, pf.Config.Path, ports[0]),
|
||||||
asNum(pf.Config.C),
|
asNum(pf.Config.C),
|
||||||
asNum(pf.Config.N),
|
asNum(pf.Config.N),
|
||||||
|
"",
|
||||||
pf.Age(),
|
pf.Age(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,26 +16,26 @@ import (
|
||||||
type PersistentVolume struct{}
|
type PersistentVolume struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (PersistentVolume) ColorerFunc() ColorerFunc {
|
func (p PersistentVolume) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, re)
|
||||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
status := strings.TrimSpace(r.Row.Fields[4])
|
if !Happy(ns, re.Row) {
|
||||||
switch status {
|
return ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.TrimSpace(re.Row.Fields[4]) {
|
||||||
case "Bound":
|
case "Bound":
|
||||||
c = StdColor
|
c = StdColor
|
||||||
case "Available":
|
case "Available":
|
||||||
c = tcell.ColorYellow
|
c = tcell.ColorYellow
|
||||||
default:
|
|
||||||
c = ErrColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header rbw.
|
// Header returns a header rbw.
|
||||||
|
|
@ -49,6 +49,9 @@ func (PersistentVolume) Header(string) HeaderRow {
|
||||||
Header{Name: "CLAIM"},
|
Header{Name: "CLAIM"},
|
||||||
Header{Name: "STORAGECLASS"},
|
Header{Name: "STORAGECLASS"},
|
||||||
Header{Name: "REASON"},
|
Header{Name: "REASON"},
|
||||||
|
Header{Name: "VOLUMEMODE", Wide: true},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,12 +93,30 @@ func (p PersistentVolume) Render(o interface{}, ns string, r *Row) error {
|
||||||
claim,
|
claim,
|
||||||
class,
|
class,
|
||||||
pv.Status.Reason,
|
pv.Status.Reason,
|
||||||
|
p.volumeMode(pv.Spec.VolumeMode),
|
||||||
|
mapToStr(pv.Labels),
|
||||||
|
asStatus(p.diagnose(string(phase))),
|
||||||
toAge(pv.ObjectMeta.CreationTimestamp),
|
toAge(pv.ObjectMeta.CreationTimestamp),
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (PersistentVolume) diagnose(r string) error {
|
||||||
|
if r != "Bound" && r != "Available" {
|
||||||
|
return fmt.Errorf("unexpected status %s", r)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PersistentVolume) volumeMode(m *v1.PersistentVolumeMode) string {
|
||||||
|
if m == nil {
|
||||||
|
return MissingValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(*m)
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
|
|
@ -15,19 +14,14 @@ import (
|
||||||
type PersistentVolumeClaim struct{}
|
type PersistentVolumeClaim struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (PersistentVolumeClaim) ColorerFunc() ColorerFunc {
|
func (p PersistentVolumeClaim) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, re)
|
||||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
if !Happy(ns, re.Row) {
|
||||||
markCol := 2
|
return ErrColor
|
||||||
if !client.IsAllNamespaces(ns) {
|
|
||||||
markCol--
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(r.Row.Fields[markCol]) != "Bound" {
|
|
||||||
c = ErrColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
|
|
@ -49,6 +43,8 @@ func (PersistentVolumeClaim) Header(ns string) HeaderRow {
|
||||||
Header{Name: "CAPACITY"},
|
Header{Name: "CAPACITY"},
|
||||||
Header{Name: "ACCESS MODES"},
|
Header{Name: "ACCESS MODES"},
|
||||||
Header{Name: "STORAGECLASS"},
|
Header{Name: "STORAGECLASS"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -96,8 +92,17 @@ func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *Row) error {
|
||||||
capacity,
|
capacity,
|
||||||
accessModes,
|
accessModes,
|
||||||
class,
|
class,
|
||||||
|
mapToStr(pvc.Labels),
|
||||||
|
asStatus(p.diagnose(string(phase))),
|
||||||
toAge(pvc.ObjectMeta.CreationTimestamp),
|
toAge(pvc.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (PersistentVolumeClaim) diagnose(r string) error {
|
||||||
|
if r != "Bound" && r != "Available" {
|
||||||
|
return fmt.Errorf("unexpected status %s", r)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,10 @@ func (Rbac) Header(ns string) HeaderRow {
|
||||||
Header{Name: "API GROUP"},
|
Header{Name: "API GROUP"},
|
||||||
}
|
}
|
||||||
|
|
||||||
return append(h, rbacVerbHeader()...)
|
h = append(h, rbacVerbHeader()...)
|
||||||
|
h = append(h, Header{Name: "VALID", Wide: true})
|
||||||
|
|
||||||
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render renders a K8s resource to screen.
|
// Render renders a K8s resource to screen.
|
||||||
|
|
@ -57,8 +60,12 @@ func (Rbac) Render(o interface{}, gvr string, r *Row) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
r.ID = p.Resource
|
r.ID = p.Resource
|
||||||
r.Fields = append(r.Fields, cleanseResource(p.Resource), p.Group)
|
r.Fields = append(r.Fields,
|
||||||
|
cleanseResource(p.Resource),
|
||||||
|
p.Group,
|
||||||
|
)
|
||||||
r.Fields = append(r.Fields, asVerbs(p.Verbs)...)
|
r.Fields = append(r.Fields, asVerbs(p.Verbs)...)
|
||||||
|
r.Fields = append(r.Fields, "")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ func (Role) Header(ns string) HeaderRow {
|
||||||
|
|
||||||
return append(h,
|
return append(h,
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -49,6 +51,8 @@ func (r Role) Render(o interface{}, ns string, row *Row) error {
|
||||||
}
|
}
|
||||||
row.Fields = append(row.Fields,
|
row.Fields = append(row.Fields,
|
||||||
ro.Name,
|
ro.Name,
|
||||||
|
mapToStr(ro.Labels),
|
||||||
|
"",
|
||||||
toAge(ro.ObjectMeta.CreationTimestamp),
|
toAge(ro.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ func (RoleBinding) Header(ns string) HeaderRow {
|
||||||
Header{Name: "ROLE"},
|
Header{Name: "ROLE"},
|
||||||
Header{Name: "KIND"},
|
Header{Name: "KIND"},
|
||||||
Header{Name: "SUBJECTS"},
|
Header{Name: "SUBJECTS"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -58,6 +60,8 @@ func (r RoleBinding) Render(o interface{}, ns string, row *Row) error {
|
||||||
rb.RoleRef.Name,
|
rb.RoleRef.Name,
|
||||||
kind,
|
kind,
|
||||||
ss,
|
ss,
|
||||||
|
mapToStr(rb.Labels),
|
||||||
|
"",
|
||||||
toAge(rb.ObjectMeta.CreationTimestamp),
|
toAge(rb.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -87,11 +91,11 @@ func toSubjectAlias(s string) string {
|
||||||
|
|
||||||
switch s {
|
switch s {
|
||||||
case rbacv1.UserKind:
|
case rbacv1.UserKind:
|
||||||
return "USR"
|
return "User"
|
||||||
case rbacv1.GroupKind:
|
case rbacv1.GroupKind:
|
||||||
return "GRP"
|
return "Group"
|
||||||
case rbacv1.ServiceAccountKind:
|
case rbacv1.ServiceAccountKind:
|
||||||
return "SA"
|
return "SvcAcct"
|
||||||
default:
|
default:
|
||||||
return strings.ToUpper(s)
|
return strings.ToUpper(s)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestRoleBindingRender(t *testing.T) {
|
||||||
c.Render(load(t, "rb"), "", &r)
|
c.Render(load(t, "rb"), "", &r)
|
||||||
|
|
||||||
assert.Equal(t, "default/blee", r.ID)
|
assert.Equal(t, "default/blee", r.ID)
|
||||||
assert.Equal(t, render.Fields{"default", "blee", "blee", "SA", "fernand"}, r.Fields[:5])
|
assert.Equal(t, render.Fields{"default", "blee", "blee", "SvcAcct", "fernand"}, r.Fields[:5])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,11 @@ func (r Row) Clone() Row {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Len returns the length of the row.
|
||||||
|
func (r Row) Len() int {
|
||||||
|
return len(r.Fields)
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
// Rows represents a collection of rows.
|
// Rows represents a collection of rows.
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ func (rr RowEvents) Sort(ns string, col int, asc bool) {
|
||||||
func toAgeDuration(dur string) string {
|
func toAgeDuration(dur string) string {
|
||||||
d, err := time.ParseDuration(dur)
|
d, err := time.ParseDuration(dur)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "n/a"
|
return dur
|
||||||
}
|
}
|
||||||
return duration.HumanDuration(d)
|
return duration.HumanDuration(d)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ type Header struct {
|
||||||
Name string
|
Name string
|
||||||
Align int
|
Align int
|
||||||
Decorator DecoratorFunc
|
Decorator DecoratorFunc
|
||||||
|
Hide bool
|
||||||
|
Wide bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone copies a header.
|
// Clone copies a header.
|
||||||
|
|
@ -56,13 +58,7 @@ func (hh HeaderRow) Columns() []string {
|
||||||
|
|
||||||
// HasAge returns true if table has an age column.
|
// HasAge returns true if table has an age column.
|
||||||
func (hh HeaderRow) HasAge() bool {
|
func (hh HeaderRow) HasAge() bool {
|
||||||
for _, r := range hh {
|
return hh.IndexOf(ageCol) != -1
|
||||||
if r.Name == ageCol {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AgeCol checks if given column index is the age column.
|
// AgeCol checks if given column index is the age column.
|
||||||
|
|
@ -72,3 +68,18 @@ func (hh HeaderRow) AgeCol(col int) bool {
|
||||||
}
|
}
|
||||||
return col == len(hh)-1
|
return col == len(hh)-1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidColIndex returns the valid col index or -1 if none.
|
||||||
|
func (hh HeaderRow) ValidColIndex() int {
|
||||||
|
return hh.IndexOf("VALID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndeOf returns the col index or -1 if none.
|
||||||
|
func (hh HeaderRow) IndexOf(c string) int {
|
||||||
|
for i, h := range hh {
|
||||||
|
if h.Name == c {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package render
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
|
@ -17,24 +16,19 @@ import (
|
||||||
type ReplicaSet struct{}
|
type ReplicaSet struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (ReplicaSet) ColorerFunc() ColorerFunc {
|
func (r ReplicaSet) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, re)
|
||||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
markCol := 2
|
if !Happy(ns, re.Row) {
|
||||||
if !client.IsAllNamespaces(ns) {
|
|
||||||
markCol--
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) {
|
|
||||||
return ErrColor
|
return ErrColor
|
||||||
}
|
}
|
||||||
|
|
||||||
return StdColor
|
return StdColor
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header row.
|
// Header returns a header row.
|
||||||
|
|
@ -49,6 +43,8 @@ func (ReplicaSet) Header(ns string) HeaderRow {
|
||||||
Header{Name: "DESIRED", Align: tview.AlignRight},
|
Header{Name: "DESIRED", Align: tview.AlignRight},
|
||||||
Header{Name: "CURRENT", Align: tview.AlignRight},
|
Header{Name: "CURRENT", Align: tview.AlignRight},
|
||||||
Header{Name: "READY", Align: tview.AlignRight},
|
Header{Name: "READY", Align: tview.AlignRight},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -75,8 +71,21 @@ func (s ReplicaSet) Render(o interface{}, ns string, r *Row) error {
|
||||||
strconv.Itoa(int(*rs.Spec.Replicas)),
|
strconv.Itoa(int(*rs.Spec.Replicas)),
|
||||||
strconv.Itoa(int(rs.Status.Replicas)),
|
strconv.Itoa(int(rs.Status.Replicas)),
|
||||||
strconv.Itoa(int(rs.Status.ReadyReplicas)),
|
strconv.Itoa(int(rs.Status.ReadyReplicas)),
|
||||||
|
mapToStr(rs.Labels),
|
||||||
|
asStatus(s.diagnose(rs)),
|
||||||
toAge(rs.ObjectMeta.CreationTimestamp),
|
toAge(rs.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s ReplicaSet) diagnose(rs appsv1.ReplicaSet) error {
|
||||||
|
if rs.Status.Replicas != rs.Status.ReadyReplicas {
|
||||||
|
if rs.Status.Replicas == 0 {
|
||||||
|
return fmt.Errorf("did not phase down correctly expecting 0 replicas but got %d", rs.Status.ReadyReplicas)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("mismatch desired(%d) vs ready(%d)", rs.Status.Replicas, rs.Status.ReadyReplicas)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ func (ServiceAccount) Header(ns string) HeaderRow {
|
||||||
return append(h,
|
return append(h,
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "SECRET"},
|
Header{Name: "SECRET"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -52,6 +54,8 @@ func (s ServiceAccount) Render(o interface{}, ns string, r *Row) error {
|
||||||
r.Fields = append(r.Fields,
|
r.Fields = append(r.Fields,
|
||||||
sa.Name,
|
sa.Name,
|
||||||
strconv.Itoa(len(sa.Secrets)),
|
strconv.Itoa(len(sa.Secrets)),
|
||||||
|
mapToStr(sa.Labels),
|
||||||
|
"",
|
||||||
toAge(sa.ObjectMeta.CreationTimestamp),
|
toAge(sa.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ func (StorageClass) Header(ns string) HeaderRow {
|
||||||
return HeaderRow{
|
return HeaderRow{
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "PROVISIONER"},
|
Header{Name: "PROVISIONER"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -42,6 +44,8 @@ func (StorageClass) Render(o interface{}, ns string, r *Row) error {
|
||||||
r.Fields = Fields{
|
r.Fields = Fields{
|
||||||
sc.Name,
|
sc.Name,
|
||||||
string(sc.Provisioner),
|
string(sc.Provisioner),
|
||||||
|
mapToStr(sc.Labels),
|
||||||
|
"",
|
||||||
toAge(sc.ObjectMeta.CreationTimestamp),
|
toAge(sc.ObjectMeta.CreationTimestamp),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ var AgeDecorator = func(a string) string {
|
||||||
func (ScreenDump) Header(ns string) HeaderRow {
|
func (ScreenDump) Header(ns string) HeaderRow {
|
||||||
return HeaderRow{
|
return HeaderRow{
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
|
Header{Name: "DIR"},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -47,6 +49,8 @@ func (b ScreenDump) Render(o interface{}, ns string, r *Row) error {
|
||||||
r.ID = filepath.Join(f.Dir, f.File.Name())
|
r.ID = filepath.Join(f.Dir, f.File.Name())
|
||||||
r.Fields = Fields{
|
r.Fields = Fields{
|
||||||
f.File.Name(),
|
f.File.Name(),
|
||||||
|
f.Dir,
|
||||||
|
"",
|
||||||
timeToAge(f.File.ModTime()),
|
timeToAge(f.File.ModTime()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ func TestScreenDumpRender(t *testing.T) {
|
||||||
assert.Equal(t, "fred/blee/bob", r.ID)
|
assert.Equal(t, "fred/blee/bob", r.ID)
|
||||||
assert.Equal(t, render.Fields{
|
assert.Equal(t, render.Fields{
|
||||||
"bob",
|
"bob",
|
||||||
|
"fred/blee",
|
||||||
|
"",
|
||||||
}, r.Fields[:len(r.Fields)-1])
|
}, r.Fields[:len(r.Fields)-1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package render
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
|
|
@ -16,20 +15,13 @@ import (
|
||||||
type StatefulSet struct{}
|
type StatefulSet struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (StatefulSet) ColorerFunc() ColorerFunc {
|
func (s StatefulSet) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, re)
|
||||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
if !Happy(ns, re.Row) {
|
||||||
readyCol := 2
|
|
||||||
if !client.IsAllNamespaces(ns) {
|
|
||||||
readyCol--
|
|
||||||
}
|
|
||||||
tokens := strings.Split(strings.TrimSpace(r.Row.Fields[readyCol]), "/")
|
|
||||||
curr, des := tokens[0], tokens[1]
|
|
||||||
if curr != des {
|
|
||||||
return ErrColor
|
return ErrColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,8 +39,12 @@ func (StatefulSet) Header(ns string) HeaderRow {
|
||||||
return append(h,
|
return append(h,
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "READY"},
|
Header{Name: "READY"},
|
||||||
Header{Name: "SELECTOR"},
|
Header{Name: "SELECTOR", Wide: true},
|
||||||
Header{Name: "SERVICE"},
|
Header{Name: "SERVICE"},
|
||||||
|
Header{Name: "CONTAINERS", Wide: true},
|
||||||
|
Header{Name: "IMAGES", Wide: true},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -72,11 +68,22 @@ func (s StatefulSet) Render(o interface{}, ns string, r *Row) error {
|
||||||
}
|
}
|
||||||
r.Fields = append(r.Fields,
|
r.Fields = append(r.Fields,
|
||||||
sts.Name,
|
sts.Name,
|
||||||
strconv.Itoa(int(sts.Status.Replicas))+"/"+strconv.Itoa(int(*sts.Spec.Replicas)),
|
strconv.Itoa(int(sts.Status.ReadyReplicas))+"/"+strconv.Itoa(int(sts.Status.Replicas)),
|
||||||
asSelector(sts.Spec.Selector),
|
asSelector(sts.Spec.Selector),
|
||||||
na(sts.Spec.ServiceName),
|
na(sts.Spec.ServiceName),
|
||||||
|
podContainerNames(sts.Spec.Template.Spec, true),
|
||||||
|
podImageNames(sts.Spec.Template.Spec, true),
|
||||||
|
mapToStr(sts.Labels),
|
||||||
|
asStatus(s.diagnose(sts.Status.Replicas, sts.Status.ReadyReplicas)),
|
||||||
toAge(sts.ObjectMeta.CreationTimestamp),
|
toAge(sts.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (StatefulSet) diagnose(d, r int32) error {
|
||||||
|
if d != r {
|
||||||
|
return fmt.Errorf("desiring %d replicas got %d available", d, r)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestStatefulSetRender(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, c.Render(load(t, "sts"), "", &r))
|
assert.Nil(t, c.Render(load(t, "sts"), "", &r))
|
||||||
assert.Equal(t, "default/nginx-sts", r.ID)
|
assert.Equal(t, "default/nginx-sts", r.ID)
|
||||||
assert.Equal(t, render.Fields{"default", "nginx-sts", "4/4", "app=nginx-sts", "nginx-sts"}, r.Fields[:len(r.Fields)-1])
|
assert.Equal(t, render.Fields{"default", "nginx-sts", "4/4", "app=nginx-sts", "nginx-sts", "nginx", "k8s.gcr.io/nginx-slim:0.8", "app=nginx-sts", ""}, r.Fields[:len(r.Fields)-1])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,11 @@ import (
|
||||||
// Subject renders a rbac to screen.
|
// Subject renders a rbac to screen.
|
||||||
type Subject struct{}
|
type Subject struct{}
|
||||||
|
|
||||||
|
// Happy returns true if resoure is happy, false otherwise
|
||||||
|
func (Subject) Happy(_ string, _ Row) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Subject) ColorerFunc() ColorerFunc {
|
func (Subject) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, re RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
|
|
@ -24,6 +29,7 @@ func (Subject) Header(ns string) HeaderRow {
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "KIND"},
|
Header{Name: "KIND"},
|
||||||
Header{Name: "FIRST LOCATION"},
|
Header{Name: "FIRST LOCATION"},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,6 +46,7 @@ func (s Subject) Render(o interface{}, ns string, r *Row) error {
|
||||||
res.Name,
|
res.Name,
|
||||||
res.Kind,
|
res.Kind,
|
||||||
res.FirstLocation,
|
res.FirstLocation,
|
||||||
|
"",
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,10 @@ func (Service) Header(ns string) HeaderRow {
|
||||||
Header{Name: "TYPE"},
|
Header{Name: "TYPE"},
|
||||||
Header{Name: "CLUSTER-IP"},
|
Header{Name: "CLUSTER-IP"},
|
||||||
Header{Name: "EXTERNAL-IP"},
|
Header{Name: "EXTERNAL-IP"},
|
||||||
Header{Name: "SELECTOR"},
|
Header{Name: "SELECTOR", Wide: true},
|
||||||
Header{Name: "PORTS"},
|
Header{Name: "PORTS", Wide: true},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -58,19 +60,32 @@ func (s Service) Render(o interface{}, ns string, r *Row) error {
|
||||||
r.Fields = append(r.Fields,
|
r.Fields = append(r.Fields,
|
||||||
svc.ObjectMeta.Name,
|
svc.ObjectMeta.Name,
|
||||||
string(svc.Spec.Type),
|
string(svc.Spec.Type),
|
||||||
svc.Spec.ClusterIP,
|
toIP(svc.Spec.ClusterIP),
|
||||||
toIPs(svc.Spec.Type, getSvcExtIPS(&svc)),
|
toIPs(svc.Spec.Type, getSvcExtIPS(&svc)),
|
||||||
mapToStr(svc.Spec.Selector),
|
mapToStr(svc.Spec.Selector),
|
||||||
toPorts(svc.Spec.Ports),
|
toPorts(svc.Spec.Ports),
|
||||||
|
mapToStr(svc.Labels),
|
||||||
|
asStatus(s.diagnose()),
|
||||||
toAge(svc.ObjectMeta.CreationTimestamp),
|
toAge(svc.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (Service) diagnose() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
func toIP(ip string) string {
|
||||||
|
if ip == "" || ip == "None" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
func getSvcExtIPS(svc *v1.Service) []string {
|
func getSvcExtIPS(svc *v1.Service) []string {
|
||||||
results := []string{}
|
results := []string{}
|
||||||
|
|
||||||
|
|
@ -116,7 +131,7 @@ func toIPs(svcType v1.ServiceType, ips []string) string {
|
||||||
if svcType == v1.ServiceTypeLoadBalancer {
|
if svcType == v1.ServiceTypeLoadBalancer {
|
||||||
return "<pending>"
|
return "<pending>"
|
||||||
}
|
}
|
||||||
return MissingValue
|
return ""
|
||||||
}
|
}
|
||||||
sort.Strings(ips)
|
sort.Strings(ips)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestServiceRender(t *testing.T) {
|
||||||
c.Render(load(t, "svc"), "", &r)
|
c.Render(load(t, "svc"), "", &r)
|
||||||
|
|
||||||
assert.Equal(t, "default/dictionary1", r.ID)
|
assert.Equal(t, "default/dictionary1", r.ID)
|
||||||
assert.Equal(t, render.Fields{"default", "dictionary1", "ClusterIP", "10.47.248.116", "<none>", "app=dictionary1", "http:4001►0"}, r.Fields[:7])
|
assert.Equal(t, render.Fields{"default", "dictionary1", "ClusterIP", "10.47.248.116", "", "app=dictionary1", "http:4001►0"}, r.Fields[:7])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
package tchart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/derailed/tview"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
okColor, faultColor = tcell.ColorPaleGreen, tcell.ColorOrangeRed
|
||||||
|
okColorName, faultColorName = "palegreen", "orangered"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Component represents a graphic component.
|
||||||
|
type Component struct {
|
||||||
|
*tview.Box
|
||||||
|
|
||||||
|
bgColor, noColor tcell.Color
|
||||||
|
seriesColors []tcell.Color
|
||||||
|
dimmed tcell.Style
|
||||||
|
id, legend string
|
||||||
|
blur func(tcell.Key)
|
||||||
|
mx sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewComponent returns a new component.
|
||||||
|
func NewComponent(id string) *Component {
|
||||||
|
return &Component{
|
||||||
|
Box: tview.NewBox(),
|
||||||
|
id: id,
|
||||||
|
noColor: tcell.ColorDefault,
|
||||||
|
seriesColors: []tcell.Color{tview.Styles.PrimaryTextColor, tview.Styles.FocusColor},
|
||||||
|
dimmed: tcell.StyleDefault.Background(tview.Styles.PrimitiveBackgroundColor).Foreground(tcell.ColorGray).Dim(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBackgroundColor sets the graph bg color.
|
||||||
|
func (c *Component) SetBackgroundColor(color tcell.Color) {
|
||||||
|
c.Box.SetBackgroundColor(color)
|
||||||
|
c.bgColor = color
|
||||||
|
c.dimmed = c.dimmed.Background(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the component ID.
|
||||||
|
func (c *Component) ID() string {
|
||||||
|
return c.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLegend sets the component legend.
|
||||||
|
func (c *Component) SetLegend(l string) {
|
||||||
|
c.mx.Lock()
|
||||||
|
defer c.mx.Unlock()
|
||||||
|
c.legend = l
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputHandler returns the handler for this primitive.
|
||||||
|
func (c *Component) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
|
||||||
|
return c.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
|
||||||
|
switch key := event.Key(); key {
|
||||||
|
case tcell.KeyEnter:
|
||||||
|
log.Debug().Msgf("YO %s ENTER!!", c.id)
|
||||||
|
case tcell.KeyBacktab, tcell.KeyTab:
|
||||||
|
log.Debug().Msgf("YO %s TAB!!", c.id)
|
||||||
|
if c.blur != nil {
|
||||||
|
c.blur(key)
|
||||||
|
}
|
||||||
|
setFocus(c)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDial returns true if chart is a dial
|
||||||
|
func (c *Component) IsDial() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBlurFunc sets a callback fn when component gets out of focus.
|
||||||
|
func (c *Component) SetBlurFunc(handler func(key tcell.Key)) *Component {
|
||||||
|
c.blur = handler
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSeriesColors sets the component series colors.
|
||||||
|
func (c *Component) SetSeriesColors(cc ...tcell.Color) {
|
||||||
|
c.mx.Lock()
|
||||||
|
defer c.mx.Unlock()
|
||||||
|
c.seriesColors = cc
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSeriesColorNames returns series colors by name.
|
||||||
|
func (c *Component) GetSeriesColorNames() []string {
|
||||||
|
c.mx.RLock()
|
||||||
|
defer c.mx.RUnlock()
|
||||||
|
|
||||||
|
var nn []string
|
||||||
|
for _, color := range c.seriesColors {
|
||||||
|
for name, co := range tcell.ColorNames {
|
||||||
|
if co == color {
|
||||||
|
nn = append(nn, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(nn) < 2 {
|
||||||
|
nn = append(nn, okColorName, faultColorName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Component) colorForSeries() (tcell.Color, tcell.Color) {
|
||||||
|
c.mx.RLock()
|
||||||
|
defer c.mx.RUnlock()
|
||||||
|
if len(c.seriesColors) > 1 {
|
||||||
|
return c.seriesColors[0], c.seriesColors[1]
|
||||||
|
}
|
||||||
|
return okColor, faultColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Component) asRect() image.Rectangle {
|
||||||
|
x, y, width, height := c.GetInnerRect()
|
||||||
|
return image.Rectangle{
|
||||||
|
Min: image.Point{X: x, Y: y},
|
||||||
|
Max: image.Point{X: x + width, Y: y + height},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
package tchart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// var dots = []rune{' ', '⠂', '⠶', '⠿'}
|
||||||
|
var dots = []rune{' ', '⠂', '▤', '▥'}
|
||||||
|
|
||||||
|
// var dots = []rune{' ', '⠂', '▤', '▇'}
|
||||||
|
|
||||||
|
type Segment []int
|
||||||
|
|
||||||
|
type Segments []Segment
|
||||||
|
|
||||||
|
type Matrix [][]rune
|
||||||
|
|
||||||
|
type Orientation int
|
||||||
|
|
||||||
|
type DotMatrix struct {
|
||||||
|
row, col int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDotMatrix(row, col int) DotMatrix {
|
||||||
|
return DotMatrix{
|
||||||
|
row: row,
|
||||||
|
col: col,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DotMatrix) Print(n int) Matrix {
|
||||||
|
m := make(Matrix, d.row)
|
||||||
|
segs := asSegments(n)
|
||||||
|
for row := 0; row < d.row; row++ {
|
||||||
|
for col := 0; col < d.col; col++ {
|
||||||
|
m[row] = append(m[row], segs.CharFor(row, col))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func asSegments(n int) Segment {
|
||||||
|
switch n {
|
||||||
|
case 0:
|
||||||
|
return Segment{1, 1, 1, 0, 1, 1, 1}
|
||||||
|
case 1:
|
||||||
|
return Segment{0, 0, 1, 0, 0, 1, 0}
|
||||||
|
case 2:
|
||||||
|
return Segment{1, 0, 1, 1, 1, 0, 1}
|
||||||
|
case 3:
|
||||||
|
return Segment{1, 0, 1, 1, 0, 1, 1}
|
||||||
|
case 4:
|
||||||
|
return Segment{0, 1, 0, 1, 0, 1, 0}
|
||||||
|
case 5:
|
||||||
|
return Segment{1, 1, 0, 1, 0, 1, 1}
|
||||||
|
case 6:
|
||||||
|
return Segment{0, 1, 0, 1, 1, 1, 1}
|
||||||
|
case 7:
|
||||||
|
return Segment{1, 0, 1, 0, 0, 1, 0}
|
||||||
|
case 8:
|
||||||
|
return Segment{1, 1, 1, 1, 1, 1, 1}
|
||||||
|
case 9:
|
||||||
|
return Segment{1, 1, 1, 1, 0, 1, 0}
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("NYI %d", n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Segment) CharFor(row, col int) rune {
|
||||||
|
c := ' '
|
||||||
|
segs := ToSegments(row, col)
|
||||||
|
if segs == nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
for _, seg := range segs {
|
||||||
|
if s[seg] == 1 {
|
||||||
|
c = charForSeg(seg, row, col)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func charForSeg(seg, row, col int) rune {
|
||||||
|
switch seg {
|
||||||
|
case 0, 3, 6:
|
||||||
|
return dots[2]
|
||||||
|
}
|
||||||
|
if row == 0 && (col == 0 || col == 2) {
|
||||||
|
return dots[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
return dots[3]
|
||||||
|
}
|
||||||
|
|
||||||
|
var segs = map[int][][]int{
|
||||||
|
0: [][]int{[]int{1, 0}, []int{0}, []int{2, 0}},
|
||||||
|
1: [][]int{[]int{1}, nil, []int{2}},
|
||||||
|
2: [][]int{[]int{1, 3}, []int{3}, []int{2, 3}},
|
||||||
|
3: [][]int{[]int{4}, nil, []int{5}},
|
||||||
|
4: [][]int{[]int{4, 6}, []int{6}, []int{5, 6}},
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToSegments(row, col int) []int {
|
||||||
|
return segs[row][col]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
package tchart_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/tchart"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSegmentFor(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
r, c int
|
||||||
|
e []int
|
||||||
|
}{
|
||||||
|
"0x0": {r: 0, c: 0, e: []int{1, 0}},
|
||||||
|
"0x1": {r: 0, c: 1, e: []int{0}},
|
||||||
|
"0x2": {r: 0, c: 2, e: []int{2, 0}},
|
||||||
|
"1x0": {r: 1, c: 0, e: []int{1}},
|
||||||
|
"1x1": {r: 1, c: 1, e: nil},
|
||||||
|
"1x2": {r: 1, c: 2, e: []int{2}},
|
||||||
|
"2x0": {r: 2, c: 0, e: []int{1, 3}},
|
||||||
|
"2x1": {r: 2, c: 1, e: []int{3}},
|
||||||
|
"2x2": {r: 2, c: 2, e: []int{2, 3}},
|
||||||
|
"3x0": {r: 3, c: 0, e: []int{4}},
|
||||||
|
"3x1": {r: 3, c: 1, e: nil},
|
||||||
|
"3x2": {r: 3, c: 2, e: []int{5}},
|
||||||
|
"4x0": {r: 4, c: 0, e: []int{4, 6}},
|
||||||
|
"4x1": {r: 4, c: 1, e: []int{6}},
|
||||||
|
"4x2": {r: 4, c: 2, e: []int{5, 6}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
assert.Equal(t, u.e, tchart.ToSegments(u.r, u.c))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDial(t *testing.T) {
|
||||||
|
d := tchart.NewDotMatrix(5, 3)
|
||||||
|
for n := 0; n <= 9; n++ {
|
||||||
|
i := n
|
||||||
|
t.Run(strconv.Itoa(n), func(t *testing.T) {
|
||||||
|
assert.Equal(t, numbers[i], d.Print(i))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
const hChar, vChar = '▤', '▥'
|
||||||
|
|
||||||
|
var numbers = []tchart.Matrix{
|
||||||
|
[][]rune{
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
{vChar, ' ', vChar},
|
||||||
|
{vChar, ' ', vChar},
|
||||||
|
{vChar, ' ', vChar},
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
},
|
||||||
|
[][]rune{
|
||||||
|
{' ', ' ', hChar},
|
||||||
|
{' ', ' ', vChar},
|
||||||
|
{' ', ' ', vChar},
|
||||||
|
{' ', ' ', vChar},
|
||||||
|
{' ', ' ', vChar},
|
||||||
|
},
|
||||||
|
[][]rune{
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
{' ', ' ', vChar},
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
{vChar, ' ', ' '},
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
},
|
||||||
|
[][]rune{
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
{' ', ' ', vChar},
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
{' ', ' ', vChar},
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
},
|
||||||
|
[][]rune{
|
||||||
|
{hChar, ' ', ' '},
|
||||||
|
{vChar, ' ', ' '},
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
{' ', ' ', vChar},
|
||||||
|
{' ', ' ', vChar},
|
||||||
|
},
|
||||||
|
[][]rune{
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
{vChar, ' ', ' '},
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
{' ', ' ', vChar},
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
},
|
||||||
|
[][]rune{
|
||||||
|
{hChar, ' ', ' '},
|
||||||
|
{vChar, ' ', ' '},
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
{vChar, ' ', vChar},
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
},
|
||||||
|
[][]rune{
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
{' ', ' ', vChar},
|
||||||
|
{' ', ' ', vChar},
|
||||||
|
{' ', ' ', vChar},
|
||||||
|
{' ', ' ', vChar},
|
||||||
|
},
|
||||||
|
[][]rune{
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
{vChar, ' ', vChar},
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
{vChar, ' ', vChar},
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
},
|
||||||
|
[][]rune{
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
{vChar, ' ', vChar},
|
||||||
|
{hChar, hChar, hChar},
|
||||||
|
{' ', ' ', vChar},
|
||||||
|
{' ', ' ', vChar},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
package tchart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"github.com/derailed/tview"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DeltaSame delta = iota
|
||||||
|
DeltaMore
|
||||||
|
DeltaLess
|
||||||
|
|
||||||
|
gaugeFmt = "0%dd"
|
||||||
|
)
|
||||||
|
|
||||||
|
type delta int
|
||||||
|
|
||||||
|
// Gauge represents a gauge component.
|
||||||
|
type Gauge struct {
|
||||||
|
*Component
|
||||||
|
|
||||||
|
data Metric
|
||||||
|
deltaOk, deltaFault delta
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGauge returns a new gauge.
|
||||||
|
func NewGauge(id string) *Gauge {
|
||||||
|
return &Gauge{
|
||||||
|
Component: NewComponent(id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDial returns true if chart is a dial
|
||||||
|
func (g *Gauge) IsDial() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gauge) Add(m Metric) {
|
||||||
|
g.mx.Lock()
|
||||||
|
defer g.mx.Unlock()
|
||||||
|
|
||||||
|
g.deltaOk, g.deltaFault = computeDelta(g.data.OK, m.OK), computeDelta(g.data.Fault, m.Fault)
|
||||||
|
g.data = m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gauge) drawNum(sc tcell.Screen, ok bool, o image.Point, n int, dn delta, ns string, style tcell.Style) {
|
||||||
|
c1, _ := g.colorForSeries()
|
||||||
|
if ok {
|
||||||
|
o.X -= 1
|
||||||
|
style = style.Foreground(c1)
|
||||||
|
printDelta(sc, dn, o, style)
|
||||||
|
o.X += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
dm, sig := NewDotMatrix(5, 3), n == 0
|
||||||
|
if n == 0 {
|
||||||
|
style = g.dimmed
|
||||||
|
}
|
||||||
|
for i := 0; i < len(ns); i++ {
|
||||||
|
if ns[i] == '0' && !sig {
|
||||||
|
g.drawDial(sc, dm.Print(int(ns[i]-48)), o, g.dimmed)
|
||||||
|
} else {
|
||||||
|
sig = true
|
||||||
|
g.drawDial(sc, dm.Print(int(ns[i]-48)), o, style)
|
||||||
|
}
|
||||||
|
o.X += 5
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
printDelta(sc, dn, o, style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gauge) Draw(sc tcell.Screen) {
|
||||||
|
g.Component.Draw(sc)
|
||||||
|
|
||||||
|
g.mx.RLock()
|
||||||
|
defer g.mx.RUnlock()
|
||||||
|
|
||||||
|
rect := g.asRect()
|
||||||
|
mid := image.Point{X: rect.Min.X + rect.Dx()/2 - 2, Y: rect.Min.Y + rect.Dy()/2 - 2}
|
||||||
|
|
||||||
|
style := tcell.StyleDefault.Background(g.bgColor)
|
||||||
|
style = style.Foreground(tcell.ColorYellow)
|
||||||
|
sc.SetContent(mid.X+1, mid.Y+2, '⠔', nil, style)
|
||||||
|
|
||||||
|
var (
|
||||||
|
max = g.data.MaxDigits()
|
||||||
|
fmat = "%" + fmt.Sprintf(gaugeFmt, max)
|
||||||
|
o = image.Point{X: mid.X - 3, Y: mid.Y}
|
||||||
|
)
|
||||||
|
|
||||||
|
s1C, s2C := g.colorForSeries()
|
||||||
|
d1, d2 := fmt.Sprintf(fmat, g.data.OK), fmt.Sprintf(fmat, g.data.Fault)
|
||||||
|
o.X -= (len(d1) - 1) * 5
|
||||||
|
g.drawNum(sc, true, o, g.data.OK, g.deltaOk, d1, style.Foreground(s1C).Dim(false))
|
||||||
|
|
||||||
|
o.X = mid.X + 3
|
||||||
|
g.drawNum(sc, false, o, g.data.Fault, g.deltaFault, d2, style.Foreground(s2C).Dim(false))
|
||||||
|
|
||||||
|
if rect.Dx() > 0 && rect.Dy() > 0 && g.legend != "" {
|
||||||
|
legend := g.legend
|
||||||
|
if g.HasFocus() {
|
||||||
|
legend = "[:aqua]" + g.legend + "[::]"
|
||||||
|
}
|
||||||
|
tview.Print(sc, legend, rect.Min.X, rect.Max.Y, rect.Dx(), tview.AlignCenter, tcell.ColorWhite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gauge) drawDial(sc tcell.Screen, m Matrix, o image.Point, style tcell.Style) {
|
||||||
|
for r := 0; r < len(m); r++ {
|
||||||
|
for c := 0; c < len(m[r]); c++ {
|
||||||
|
dot := m[r][c]
|
||||||
|
if dot == dots[0] {
|
||||||
|
sc.SetContent(o.X+c, o.Y+r, dots[1], nil, g.dimmed)
|
||||||
|
} else {
|
||||||
|
sc.SetContent(o.X+c, o.Y+r, dot, nil, style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func computeDelta(d1, d2 int) delta {
|
||||||
|
if d2 == 0 {
|
||||||
|
return DeltaSame
|
||||||
|
}
|
||||||
|
|
||||||
|
d := d2 - d1
|
||||||
|
switch {
|
||||||
|
case d > 0:
|
||||||
|
return DeltaMore
|
||||||
|
case d < 0:
|
||||||
|
return DeltaLess
|
||||||
|
default:
|
||||||
|
return DeltaSame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printDelta(sc tcell.Screen, d delta, o image.Point, s tcell.Style) {
|
||||||
|
s = s.Dim(false)
|
||||||
|
switch d {
|
||||||
|
case DeltaLess:
|
||||||
|
sc.SetContent(o.X-1, o.Y+2, '↓', nil, s)
|
||||||
|
case DeltaMore:
|
||||||
|
sc.SetContent(o.X-1, o.Y+2, '↑', nil, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
package tchart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/derailed/tview"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
)
|
||||||
|
|
||||||
|
var sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
||||||
|
|
||||||
|
type block struct {
|
||||||
|
full int
|
||||||
|
partial rune
|
||||||
|
}
|
||||||
|
|
||||||
|
type blocks struct {
|
||||||
|
oks, errs block
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metric tracks a good and error rates.
|
||||||
|
type Metric struct {
|
||||||
|
OK, Fault int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max returns the max of the metric.
|
||||||
|
func (m Metric) MaxDigits() int {
|
||||||
|
max := int(math.Max(float64(m.OK), float64(m.Fault)))
|
||||||
|
s := fmt.Sprintf("%d", max)
|
||||||
|
return len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum returns the sum of the metrics.
|
||||||
|
func (m Metric) Sum() int {
|
||||||
|
return m.OK + m.Fault
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sparkline represents a sparkline component.
|
||||||
|
type SparkLine struct {
|
||||||
|
*Component
|
||||||
|
|
||||||
|
data []Metric
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSparkLine returns a new graph.
|
||||||
|
func NewSparkLine(id string) *SparkLine {
|
||||||
|
return &SparkLine{
|
||||||
|
Component: NewComponent(id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds a metric.
|
||||||
|
func (s *SparkLine) Add(m Metric) {
|
||||||
|
s.mx.Lock()
|
||||||
|
defer s.mx.Unlock()
|
||||||
|
s.data = append(s.data, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw draws the graph.
|
||||||
|
func (s *SparkLine) Draw(screen tcell.Screen) {
|
||||||
|
s.Component.Draw(screen)
|
||||||
|
|
||||||
|
s.mx.RLock()
|
||||||
|
defer s.mx.RUnlock()
|
||||||
|
|
||||||
|
if len(s.data) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pad := 1
|
||||||
|
if s.legend != "" {
|
||||||
|
pad++
|
||||||
|
}
|
||||||
|
|
||||||
|
rect := s.asRect()
|
||||||
|
s.cutSet(rect.Dx())
|
||||||
|
max := s.computeMax()
|
||||||
|
|
||||||
|
cX, idx := rect.Min.X+1, 0
|
||||||
|
if len(s.data)*2 < rect.Dx() {
|
||||||
|
cX = rect.Max.X - len(s.data)*2
|
||||||
|
} else {
|
||||||
|
idx = len(s.data) - rect.Dx()/2
|
||||||
|
}
|
||||||
|
|
||||||
|
scale := float64(len(sparks)) * float64((rect.Dy() - pad)) / float64(max)
|
||||||
|
c1, c2 := s.colorForSeries()
|
||||||
|
for _, d := range s.data[idx:] {
|
||||||
|
b := toBlocks(d, scale)
|
||||||
|
cY := rect.Max.Y - pad
|
||||||
|
s.drawBlock(screen, cX, cY, b.oks, c1)
|
||||||
|
s.drawBlock(screen, cX, cY, b.errs, c2)
|
||||||
|
cX += 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if rect.Dx() > 0 && rect.Dy() > 0 && s.legend != "" {
|
||||||
|
legend := s.legend
|
||||||
|
if s.HasFocus() {
|
||||||
|
legend = "[:aqua:]" + s.legend + "[::]"
|
||||||
|
}
|
||||||
|
tview.Print(screen, legend, rect.Min.X, rect.Max.Y, rect.Dx(), tview.AlignCenter, tcell.ColorWhite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SparkLine) drawBlock(screen tcell.Screen, x, y int, b block, c tcell.Color) {
|
||||||
|
style := tcell.StyleDefault.Foreground(c).Background(s.bgColor)
|
||||||
|
|
||||||
|
for i := 0; i < b.full; i++ {
|
||||||
|
screen.SetContent(x, y, sparks[len(sparks)-1], nil, style)
|
||||||
|
y--
|
||||||
|
}
|
||||||
|
if b.partial != 0 {
|
||||||
|
screen.SetContent(x, y, b.partial, nil, style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SparkLine) cutSet(width int) {
|
||||||
|
if width <= 0 || len(s.data) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.data) >= width*2 {
|
||||||
|
s.data = s.data[len(s.data)-width:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SparkLine) computeMax() int {
|
||||||
|
var max int
|
||||||
|
for _, d := range s.data {
|
||||||
|
if max < d.OK {
|
||||||
|
max = d.OK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
func toBlocks(m Metric, scale float64) blocks {
|
||||||
|
if m.Sum() <= 0 {
|
||||||
|
return blocks{}
|
||||||
|
}
|
||||||
|
return blocks{oks: makeBlocks(m.OK, false, scale), errs: makeBlocks(m.Fault, true, scale)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeBlocks(v int, isErr bool, scale float64) block {
|
||||||
|
scaled := int(math.Round(float64(v) * scale))
|
||||||
|
part, b := scaled%len(sparks), block{full: scaled / len(sparks)}
|
||||||
|
// Err might get scaled way down if so nudge.
|
||||||
|
if v > 0 && isErr && scaled == 0 {
|
||||||
|
part = 1
|
||||||
|
}
|
||||||
|
if part > 0 {
|
||||||
|
b.partial = sparks[part-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package ui
|
||||||
import (
|
import (
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
"github.com/derailed/k9s/internal/model"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
@ -14,6 +15,7 @@ type App struct {
|
||||||
Configurator
|
Configurator
|
||||||
|
|
||||||
Main *Pages
|
Main *Pages
|
||||||
|
flash *model.Flash
|
||||||
actions KeyActions
|
actions KeyActions
|
||||||
views map[string]tview.Primitive
|
views map[string]tview.Primitive
|
||||||
cmdBuff *CmdBuff
|
cmdBuff *CmdBuff
|
||||||
|
|
@ -25,6 +27,7 @@ func NewApp(context string) *App {
|
||||||
Application: tview.NewApplication(),
|
Application: tview.NewApplication(),
|
||||||
actions: make(KeyActions),
|
actions: make(KeyActions),
|
||||||
Main: NewPages(),
|
Main: NewPages(),
|
||||||
|
flash: model.NewFlash(model.DefaultFlashDelay),
|
||||||
cmdBuff: NewCmdBuff(':', CommandBuff),
|
cmdBuff: NewCmdBuff(':', CommandBuff),
|
||||||
}
|
}
|
||||||
a.ReloadStyles(context)
|
a.ReloadStyles(context)
|
||||||
|
|
@ -33,7 +36,6 @@ func NewApp(context string) *App {
|
||||||
"menu": NewMenu(a.Styles),
|
"menu": NewMenu(a.Styles),
|
||||||
"logo": NewLogo(a.Styles),
|
"logo": NewLogo(a.Styles),
|
||||||
"cmd": NewCommand(a.Styles),
|
"cmd": NewCommand(a.Styles),
|
||||||
"flash": NewFlash(&a, "Initializing..."),
|
|
||||||
"crumbs": NewCrumbs(a.Styles),
|
"crumbs": NewCrumbs(a.Styles),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,11 +241,6 @@ func (a *App) Logo() *Logo {
|
||||||
return a.views["logo"].(*Logo)
|
return a.views["logo"].(*Logo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flash returns app flash.
|
|
||||||
func (a *App) Flash() *Flash {
|
|
||||||
return a.views["flash"].(*Flash)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cmd returns app cmd.
|
// Cmd returns app cmd.
|
||||||
func (a *App) Cmd() *Command {
|
func (a *App) Cmd() *Command {
|
||||||
return a.views["cmd"].(*Command)
|
return a.views["cmd"].(*Command)
|
||||||
|
|
@ -254,6 +251,11 @@ func (a *App) Menu() *Menu {
|
||||||
return a.views["menu"].(*Menu)
|
return a.views["menu"].(*Menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flash returns a flash model.
|
||||||
|
func (a *App) Flash() *model.Flash {
|
||||||
|
return a.flash
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ func TestAppViews(t *testing.T) {
|
||||||
a := ui.NewApp("")
|
a := ui.NewApp("")
|
||||||
a.Init()
|
a.Init()
|
||||||
|
|
||||||
vv := []string{"crumbs", "logo", "cmd", "flash", "menu"}
|
vv := []string{"crumbs", "logo", "cmd", "menu"}
|
||||||
for i := range vv {
|
for i := range vv {
|
||||||
v := vv[i]
|
v := vv[i]
|
||||||
t.Run(v, func(t *testing.T) {
|
t.Run(v, func(t *testing.T) {
|
||||||
|
|
@ -68,7 +68,6 @@ func TestAppViews(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.NotNil(t, a.Crumbs())
|
assert.NotNil(t, a.Crumbs())
|
||||||
assert.NotNil(t, a.Flash())
|
|
||||||
assert.NotNil(t, a.Logo())
|
assert.NotNil(t, a.Logo())
|
||||||
assert.NotNil(t, a.Cmd())
|
assert.NotNil(t, a.Cmd())
|
||||||
assert.NotNil(t, a.Menu())
|
assert.NotNil(t, a.Menu())
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,8 @@ func (c *Configurator) RefreshStyles(context string) {
|
||||||
clusterSkins := filepath.Join(config.K9sHome, fmt.Sprintf("%s_skin.yml", context))
|
clusterSkins := filepath.Join(config.K9sHome, fmt.Sprintf("%s_skin.yml", context))
|
||||||
if c.Styles == nil {
|
if c.Styles == nil {
|
||||||
c.Styles = config.NewStyles()
|
c.Styles = config.NewStyles()
|
||||||
|
} else {
|
||||||
|
c.Styles.Reset()
|
||||||
}
|
}
|
||||||
if err := c.Styles.Load(clusterSkins); err != nil {
|
if err := c.Styles.Load(clusterSkins); err != nil {
|
||||||
log.Info().Msgf("No context specific skin file found -- %s", clusterSkins)
|
log.Info().Msgf("No context specific skin file found -- %s", clusterSkins)
|
||||||
|
|
@ -102,10 +104,10 @@ func (c *Configurator) updateStyles(f string) {
|
||||||
}
|
}
|
||||||
c.Styles.Update()
|
c.Styles.Update()
|
||||||
|
|
||||||
render.StdColor = config.AsColor(c.Styles.Frame().Status.NewColor)
|
render.StdColor = c.Styles.Frame().Status.NewColor.Color()
|
||||||
render.AddColor = config.AsColor(c.Styles.Frame().Status.AddColor)
|
render.AddColor = c.Styles.Frame().Status.AddColor.Color()
|
||||||
render.ModColor = config.AsColor(c.Styles.Frame().Status.ModifyColor)
|
render.ModColor = c.Styles.Frame().Status.ModifyColor.Color()
|
||||||
render.ErrColor = config.AsColor(c.Styles.Frame().Status.ErrorColor)
|
render.ErrColor = c.Styles.Frame().Status.ErrorColor.Color()
|
||||||
render.HighlightColor = config.AsColor(c.Styles.Frame().Status.HighlightColor)
|
render.HighlightColor = c.Styles.Frame().Status.HighlightColor.Color()
|
||||||
render.CompletedColor = config.AsColor(c.Styles.Frame().Status.CompletedColor)
|
render.CompletedColor = c.Styles.Frame().Status.CompletedColor.Color()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,67 +2,45 @@ package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/model"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// FlashInfo represents an info message.
|
emoHappy = "😎"
|
||||||
FlashInfo FlashLevel = iota
|
|
||||||
// FlashWarn represents an warning message.
|
|
||||||
FlashWarn
|
|
||||||
// FlashErr represents an error message.
|
|
||||||
FlashErr
|
|
||||||
// FlashFatal represents an fatal message.
|
|
||||||
FlashFatal
|
|
||||||
|
|
||||||
flashDelay = 3 * time.Second
|
|
||||||
|
|
||||||
emoDoh = "😗"
|
emoDoh = "😗"
|
||||||
emoRed = "😡"
|
emoRed = "😡"
|
||||||
emoDead = "💀"
|
|
||||||
emoHappy = "😎"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
// Flash represents a flash message indicator.
|
||||||
// FlashLevel represents flash message severity.
|
type Flash struct {
|
||||||
FlashLevel int
|
*tview.TextView
|
||||||
|
|
||||||
// Flash represents a flash message indicator.
|
app *App
|
||||||
Flash struct {
|
testMode bool
|
||||||
*tview.TextView
|
}
|
||||||
|
|
||||||
cancel context.CancelFunc
|
|
||||||
app *App
|
|
||||||
flushNow bool
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewFlash returns a new flash view.
|
// NewFlash returns a new flash view.
|
||||||
func NewFlash(app *App, m string) *Flash {
|
func NewFlash(app *App) *Flash {
|
||||||
f := Flash{
|
f := Flash{
|
||||||
app: app,
|
app: app,
|
||||||
TextView: tview.NewTextView(),
|
TextView: tview.NewTextView(),
|
||||||
}
|
}
|
||||||
f.SetTextColor(tcell.ColorAqua)
|
f.SetTextColor(tcell.ColorAqua)
|
||||||
f.SetTextAlign(tview.AlignLeft)
|
f.SetTextAlign(tview.AlignCenter)
|
||||||
f.SetBorderPadding(0, 0, 1, 1)
|
f.SetBorderPadding(0, 0, 1, 1)
|
||||||
f.SetText(m)
|
|
||||||
f.app.Styles.AddListener(&f)
|
f.app.Styles.AddListener(&f)
|
||||||
|
|
||||||
return &f
|
return &f
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMode for testing...
|
// SetTestMode for testing ONLY!
|
||||||
func (f *Flash) TestMode() {
|
func (f *Flash) SetTestMode(b bool) {
|
||||||
f.flushNow = true
|
f.testMode = b
|
||||||
}
|
}
|
||||||
|
|
||||||
// StylesChanged notifies listener the skin changed.
|
// StylesChanged notifies listener the skin changed.
|
||||||
|
|
@ -71,101 +49,53 @@ func (f *Flash) StylesChanged(s *config.Styles) {
|
||||||
f.SetTextColor(s.FgColor())
|
f.SetTextColor(s.FgColor())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info displays an info flash message.
|
func (f *Flash) Watch(ctx context.Context, c model.FlashChan) {
|
||||||
func (f *Flash) Info(msg string) {
|
defer log.Debug().Msgf("Flash Canceled!")
|
||||||
log.Info().Msg(msg)
|
for {
|
||||||
f.SetMessage(FlashInfo, msg)
|
select {
|
||||||
}
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
// Infof displays a formatted info flash message.
|
case msg := <-c:
|
||||||
func (f *Flash) Infof(fmat string, args ...interface{}) {
|
f.SetMessage(msg)
|
||||||
f.Info(fmt.Sprintf(fmat, args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn displays a warning flash message.
|
|
||||||
func (f *Flash) Warn(msg string) {
|
|
||||||
log.Warn().Msg(msg)
|
|
||||||
f.SetMessage(FlashWarn, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warnf displays a formatted warning flash message.
|
|
||||||
func (f *Flash) Warnf(fmat string, args ...interface{}) {
|
|
||||||
f.Warn(fmt.Sprintf(fmat, args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Err displays an error flash message.
|
|
||||||
func (f *Flash) Err(err error) {
|
|
||||||
log.Error().Msg(err.Error())
|
|
||||||
f.SetMessage(FlashErr, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Errf displays a formatted error flash message.
|
|
||||||
func (f *Flash) Errf(fmat string, args ...interface{}) {
|
|
||||||
var err error
|
|
||||||
for _, a := range args {
|
|
||||||
switch e := a.(type) {
|
|
||||||
case error:
|
|
||||||
err = e
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Error().Err(err).Msgf(fmat, args...)
|
|
||||||
f.SetMessage(FlashErr, fmt.Sprintf(fmat, args...))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetMessage sets flash message and level.
|
// SetMessage sets flash message and level.
|
||||||
func (f *Flash) SetMessage(level FlashLevel, msg ...string) {
|
func (f *Flash) SetMessage(m model.LevelMessage) {
|
||||||
if f.cancel != nil {
|
fn := func() {
|
||||||
f.cancel()
|
if m.Text == "" {
|
||||||
|
f.Clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.SetTextColor(flashColor(m.Level))
|
||||||
|
f.SetText(flashEmoji(m.Level) + " " + m.Text)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, width, _ := f.GetRect()
|
if f.testMode {
|
||||||
if width <= 15 {
|
fn()
|
||||||
width = 100
|
|
||||||
}
|
|
||||||
m := strings.Join(msg, " ")
|
|
||||||
if f.flushNow {
|
|
||||||
f.SetTextColor(flashColor(level))
|
|
||||||
f.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3))
|
|
||||||
} else {
|
} else {
|
||||||
f.app.QueueUpdateDraw(func() {
|
f.app.QueueUpdateDraw(fn)
|
||||||
f.SetTextColor(flashColor(level))
|
|
||||||
f.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var ctx context.Context
|
|
||||||
ctx, f.cancel = context.WithTimeout(context.Background(), flashDelay)
|
|
||||||
go f.refresh(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Flash) refresh(ctx context.Context) {
|
func flashEmoji(l model.FlashLevel) string {
|
||||||
<-ctx.Done()
|
|
||||||
f.app.QueueUpdateDraw(func() {
|
|
||||||
f.Clear()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func flashEmoji(l FlashLevel) string {
|
|
||||||
switch l {
|
switch l {
|
||||||
case FlashWarn:
|
case model.FlashWarn:
|
||||||
return emoDoh
|
return emoDoh
|
||||||
case FlashErr:
|
case model.FlashErr:
|
||||||
return emoRed
|
return emoRed
|
||||||
case FlashFatal:
|
|
||||||
return emoDead
|
|
||||||
default:
|
default:
|
||||||
return emoHappy
|
return emoHappy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func flashColor(l FlashLevel) tcell.Color {
|
func flashColor(l model.FlashLevel) tcell.Color {
|
||||||
switch l {
|
switch l {
|
||||||
case FlashWarn:
|
case model.FlashWarn:
|
||||||
return tcell.ColorOrange
|
return tcell.ColorOrange
|
||||||
case FlashErr:
|
case model.FlashErr:
|
||||||
return tcell.ColorOrangeRed
|
return tcell.ColorOrangeRed
|
||||||
case FlashFatal:
|
|
||||||
return tcell.ColorFuchsia
|
|
||||||
default:
|
default:
|
||||||
return tcell.ColorNavajoWhite
|
return tcell.ColorNavajoWhite
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,40 @@
|
||||||
package ui_test
|
package ui_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/model"
|
||||||
"github.com/derailed/k9s/internal/ui"
|
"github.com/derailed/k9s/internal/ui"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFlashInfo(t *testing.T) {
|
func TestFlash(t *testing.T) {
|
||||||
f := newFlash()
|
const delay = 1 * time.Millisecond
|
||||||
f.Info("Blee")
|
uu := map[string]struct {
|
||||||
|
l model.FlashLevel
|
||||||
|
i, e string
|
||||||
|
}{
|
||||||
|
"info": {l: model.FlashInfo, i: "hello", e: "😎 hello\n"},
|
||||||
|
"warn": {l: model.FlashWarn, i: "hello", e: "😗 hello\n"},
|
||||||
|
"err": {l: model.FlashErr, i: "hello", e: "😡 hello\n"},
|
||||||
|
}
|
||||||
|
|
||||||
assert.Equal(t, "😎 Blee\n", f.GetText(false))
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
f.Infof("Blee %s", "duh")
|
defer cancel()
|
||||||
assert.Equal(t, "😎 Blee duh\n", f.GetText(false))
|
|
||||||
}
|
a := ui.NewApp("test")
|
||||||
|
f := ui.NewFlash(a)
|
||||||
func TestFlashWarn(t *testing.T) {
|
f.SetTestMode(true)
|
||||||
f := newFlash()
|
go f.Watch(ctx, a.Flash().Channel())
|
||||||
f.Warn("Blee")
|
|
||||||
|
for k := range uu {
|
||||||
assert.Equal(t, "😗 Blee\n", f.GetText(false))
|
u := uu[k]
|
||||||
f.Warnf("Blee %s", "duh")
|
t.Run(k, func(t *testing.T) {
|
||||||
assert.Equal(t, "😗 Blee duh\n", f.GetText(false))
|
a.Flash().SetMessage(u.l, u.i)
|
||||||
}
|
time.Sleep(delay)
|
||||||
|
assert.Equal(t, u.e, f.GetText(false))
|
||||||
func TestFlashErr(t *testing.T) {
|
})
|
||||||
f := newFlash()
|
}
|
||||||
|
|
||||||
f.Err(errors.New("Blee"))
|
|
||||||
assert.Equal(t, "😡 Blee\n", f.GetText(false))
|
|
||||||
f.Errf("Blee %s", "duh")
|
|
||||||
assert.Equal(t, "😡 Blee duh\n", f.GetText(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// Helpers...
|
|
||||||
|
|
||||||
func newFlash() *ui.Flash {
|
|
||||||
f := ui.NewFlash(ui.NewApp(""), "YO!")
|
|
||||||
f.TestMode()
|
|
||||||
return f
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ func NewLogo(styles *config.Styles) *Logo {
|
||||||
}
|
}
|
||||||
l.SetDirection(tview.FlexRow)
|
l.SetDirection(tview.FlexRow)
|
||||||
l.AddItem(l.logo, 0, 6, false)
|
l.AddItem(l.logo, 0, 6, false)
|
||||||
l.AddItem(l.status, 0, 1, false)
|
l.AddItem(l.status, 1, 0, false)
|
||||||
l.refreshLogo(styles.Body().LogoColor)
|
l.refreshLogo(styles.Body().LogoColor)
|
||||||
l.SetBackgroundColor(styles.BgColor())
|
l.SetBackgroundColor(styles.BgColor())
|
||||||
styles.AddListener(&l)
|
styles.AddListener(&l)
|
||||||
|
|
@ -60,30 +60,30 @@ func (l *Logo) Reset() {
|
||||||
|
|
||||||
// Err displays a log error state.
|
// Err displays a log error state.
|
||||||
func (l *Logo) Err(msg string) {
|
func (l *Logo) Err(msg string) {
|
||||||
l.update(msg, "red")
|
l.update(msg, config.NewColor("red"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn displays a log warning state.
|
// Warn displays a log warning state.
|
||||||
func (l *Logo) Warn(msg string) {
|
func (l *Logo) Warn(msg string) {
|
||||||
l.update(msg, "mediumvioletred")
|
l.update(msg, config.NewColor("mediumvioletred"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info displays a log info state.
|
// Info displays a log info state.
|
||||||
func (l *Logo) Info(msg string) {
|
func (l *Logo) Info(msg string) {
|
||||||
l.update(msg, "green")
|
l.update(msg, config.NewColor("green"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logo) update(msg, c string) {
|
func (l *Logo) update(msg string, c config.Color) {
|
||||||
l.refreshStatus(msg, c)
|
l.refreshStatus(msg, c)
|
||||||
l.refreshLogo(c)
|
l.refreshLogo(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logo) refreshStatus(msg, c string) {
|
func (l *Logo) refreshStatus(msg string, c config.Color) {
|
||||||
l.status.SetBackgroundColor(config.AsColor(c))
|
l.status.SetBackgroundColor(c.Color())
|
||||||
l.status.SetText(fmt.Sprintf("[white::b]%s", msg))
|
l.status.SetText(fmt.Sprintf("[white::b]%s", msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logo) refreshLogo(c string) {
|
func (l *Logo) refreshLogo(c config.Color) {
|
||||||
l.logo.Clear()
|
l.logo.Clear()
|
||||||
for i, s := range LogoSmall {
|
for i, s := range LogoSmall {
|
||||||
fmt.Fprintf(l.logo, "[%s::b]%s", c, s)
|
fmt.Fprintf(l.logo, "[%s::b]%s", c, s)
|
||||||
|
|
|
||||||
|
|
@ -188,16 +188,16 @@ func toMnemonic(s string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatNSMenu(i int, name string, styles config.Frame) string {
|
func formatNSMenu(i int, name string, styles config.Frame) string {
|
||||||
fmat := strings.Replace(menuIndexFmt, "[key", "["+styles.Menu.NumKeyColor, 1)
|
fmat := strings.Replace(menuIndexFmt, "[key", "["+styles.Menu.NumKeyColor.String(), 1)
|
||||||
fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor+":", -1)
|
fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor.String()+":", -1)
|
||||||
fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor, 1)
|
fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor.String(), 1)
|
||||||
return fmt.Sprintf(fmat, i, name)
|
return fmt.Sprintf(fmat, i, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatPlainMenu(h model.MenuHint, size int, styles config.Frame) string {
|
func formatPlainMenu(h model.MenuHint, size int, styles config.Frame) string {
|
||||||
menuFmt := " [key:-:b]%-" + strconv.Itoa(size+2) + "s [fg:-:d]%s "
|
menuFmt := " [key:-:b]%-" + strconv.Itoa(size+2) + "s [fg:-:d]%s "
|
||||||
fmat := strings.Replace(menuFmt, "[key", "["+styles.Menu.KeyColor, 1)
|
fmat := strings.Replace(menuFmt, "[key", "["+styles.Menu.KeyColor.String(), 1)
|
||||||
fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor, 1)
|
fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor.String(), 1)
|
||||||
fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor+":", -1)
|
fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor.String()+":", -1)
|
||||||
return fmt.Sprintf(fmat, toMnemonic(h.Mnemonic), h.Description)
|
return fmt.Sprintf(fmat, toMnemonic(h.Mnemonic), h.Description)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue