Merge branch 'master' into attach
commit
184ea664eb
|
|
@ -17,3 +17,4 @@ pod1.go
|
|||
.project
|
||||
faas
|
||||
.settings/*
|
||||
demos
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ builds:
|
|||
- 386
|
||||
- amd64
|
||||
- arm64
|
||||
- arm
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
ldflags:
|
||||
- -s -w -X github.com/derailed/k9s/cmd.version={{.Version}} -X github.com/derailed/k9s/cmd.commit={{.Commit}} -X github.com/derailed/k9s/cmd.date={{.Date}}
|
||||
|
|
|
|||
25
README.md
25
README.md
|
|
@ -99,8 +99,9 @@ K9s is available on Linux, macOS and Windows platforms.
|
|||
|
||||
---
|
||||
|
||||
## Demo Video
|
||||
## Demo Videos/Recordings
|
||||
|
||||
* [K9s Pulses](https://asciinema.org/a/UbXKPal6IWpTaVAjBBFmizcGN)
|
||||
* [K9s v0.15.1](https://youtu.be/7Fx4XQ2ftpM)
|
||||
* [K9s v0.13.0](https://www.youtube.com/watch?v=qaeR2iK7U0o&t=15s)
|
||||
* [K9s v0.9.0](https://www.youtube.com/watch?v=bxKfqumjW4I)
|
||||
|
|
@ -141,7 +142,7 @@ K9s uses aliases to navigate most K8s resources.
|
|||
| `:`ns`<ENTER>` | To view and switch to another Kubernetes namespace | `:`+`ns`+`<ENTER>` |
|
||||
| `:screendump`, `:sd` | To view all saved resources | |
|
||||
| `Ctrl-d` | To delete a resource (TAB and ENTER to confirm) | |
|
||||
| `Ctrl-k` | To delete a resource (no confirmation dialog) | |
|
||||
| `Ctrl-k` | To kill a resource (no confirmation dialog!) | |
|
||||
| `:q`, `Ctrl-c` | To bail out of K9s | |
|
||||
|
||||
---
|
||||
|
|
@ -505,17 +506,17 @@ k9s:
|
|||
highlightColor: skyblue
|
||||
counterColor: slateblue
|
||||
filterColor: slategray
|
||||
# TableView attributes.
|
||||
table:
|
||||
fgColor: blue
|
||||
bgColor: darkblue
|
||||
cursorColor: aqua
|
||||
# Header row styles.
|
||||
header:
|
||||
fgColor: white
|
||||
bgColor: darkblue
|
||||
sorterColor: orange
|
||||
views:
|
||||
# TableView attributes.
|
||||
table:
|
||||
fgColor: blue
|
||||
bgColor: darkblue
|
||||
cursorColor: aqua
|
||||
# Header row styles.
|
||||
header:
|
||||
fgColor: white
|
||||
bgColor: darkblue
|
||||
sorterColor: orange
|
||||
# YAML info styles.
|
||||
yaml:
|
||||
keyColor: steelblue
|
||||
|
|
|
|||
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)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
|
||||
|
||||
# Release v0.16.1
|
||||
|
||||
## 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)
|
||||
|
||||
---
|
||||
|
||||
Maintenance Release!
|
||||
|
||||
## Resolved Bugs/Features/PRs
|
||||
|
||||
- [Issue #561](https://github.com/derailed/k9s/issues/561)
|
||||
|
||||
---
|
||||
|
||||
<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)
|
||||
6
go.mod
6
go.mod
|
|
@ -34,7 +34,7 @@ require (
|
|||
github.com/alexellis/go-execute v0.0.0-20200124154445-8697e4e28c5e // indirect
|
||||
github.com/alexellis/hmac v0.0.0-20180624211220-5c52ab81c0de // indirect
|
||||
github.com/atotto/clipboard v0.1.2
|
||||
github.com/derailed/tview v0.3.5
|
||||
github.com/derailed/tview v0.3.6
|
||||
github.com/drone/envsubst v1.0.2 // indirect
|
||||
github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect
|
||||
github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect
|
||||
|
|
@ -43,14 +43,14 @@ require (
|
|||
github.com/gdamore/tcell v1.3.0
|
||||
github.com/ghodss/yaml v1.0.0
|
||||
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/openfaas/faas v0.0.0-20200207215241-6afae214e3ec
|
||||
github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c
|
||||
github.com/openfaas/faas-provider v0.15.0
|
||||
github.com/petergtz/pegomock v2.6.0+incompatible
|
||||
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/sahilm/fuzzy v0.1.0
|
||||
github.com/spf13/cobra v0.0.5
|
||||
|
|
|
|||
6
go.sum
6
go.sum
|
|
@ -157,6 +157,8 @@ github.com/derailed/tview v0.3.4 h1:PnF64fLqm48LEjC/XwOS7JufDgFuuPYx85YVt5t3rwE=
|
|||
github.com/derailed/tview v0.3.4/go.mod h1:yApPszFU62FoaGkf7swy2nIdV/h7Nid3dhMSVy6+OFI=
|
||||
github.com/derailed/tview v0.3.5 h1:1vKqcJIiZtLAs5moX9c38+BbBSYhPgFq0ZndnVNVNFc=
|
||||
github.com/derailed/tview v0.3.5/go.mod h1:yApPszFU62FoaGkf7swy2nIdV/h7Nid3dhMSVy6+OFI=
|
||||
github.com/derailed/tview v0.3.6 h1:9PyX6Nu1vs9mCVfvV2q2fwT/dZta0dBGr4ZPjCF1KnU=
|
||||
github.com/derailed/tview v0.3.6/go.mod h1:GJ3k/TIzEE+sj1L09/usk6HrkjsdadSsb03eHgPbcII=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
|
|
@ -483,6 +485,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/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
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/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU=
|
||||
|
|
@ -565,6 +568,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/go.mod h1:S5M+++KwbmxA7w68S92B5NdWiCB+cIhITaMUkq9W608=
|
||||
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/uniseg v0.0.0-20190513083848-b9f5b9457d44/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
||||
|
|
@ -575,6 +579,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/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.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/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package client
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
|
@ -177,13 +178,31 @@ func (c *Config) ClusterNames() ([]string, error) {
|
|||
|
||||
// CurrentGroupNames retrieves the active group names.
|
||||
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 []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.
|
||||
func (c *Config) CurrentUserName() (string, error) {
|
||||
if isSet(c.flags.Impersonate) {
|
||||
|
|
@ -311,3 +330,7 @@ func (c *Config) ensureConfig() {
|
|||
func isSet(s *string) bool {
|
||||
return s != nil && len(*s) != 0
|
||||
}
|
||||
|
||||
func areSet(s *[]string) bool {
|
||||
return s != nil && len(*s) != 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,20 +3,49 @@ package client
|
|||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/cache"
|
||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||
)
|
||||
|
||||
const (
|
||||
mxCacheSize = 100
|
||||
mxCacheExpiry = 1 * time.Minute
|
||||
)
|
||||
|
||||
// MetricsDial tracks global metric server handle.
|
||||
var MetricsDial *MetricsServer
|
||||
|
||||
// DialMetrics dials the metrics server.
|
||||
func DialMetrics(c Connection) *MetricsServer {
|
||||
if MetricsDial == nil {
|
||||
MetricsDial = NewMetricsServer(c)
|
||||
}
|
||||
|
||||
return MetricsDial
|
||||
}
|
||||
|
||||
// ResetMetrics resets the metric server handle.
|
||||
func ResetMetrics() {
|
||||
MetricsDial = nil
|
||||
}
|
||||
|
||||
// MetricsServer serves cluster metrics for nodes and pods.
|
||||
type MetricsServer struct {
|
||||
Connection
|
||||
|
||||
cache *cache.LRUExpireCache
|
||||
}
|
||||
|
||||
// NewMetricsServer return a metric server instance.
|
||||
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.
|
||||
|
|
@ -28,15 +57,15 @@ func (m *MetricsServer) NodesMetrics(nodes *v1.NodeList, metrics *mv1beta1.NodeM
|
|||
for _, no := range nodes.Items {
|
||||
mmx[no.Name] = NodeMetrics{
|
||||
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(),
|
||||
TotalMEM: toMB(no.Status.Capacity.Memory().Value()),
|
||||
TotalMEM: ToMB(no.Status.Capacity.Memory().Value()),
|
||||
}
|
||||
}
|
||||
for _, c := range metrics.Items {
|
||||
if mx, ok := mmx[c.Name]; ok {
|
||||
mx.CurrentCPU = c.Usage.Cpu().MilliValue()
|
||||
mx.CurrentMEM = toMB(c.Usage.Memory().Value())
|
||||
mx.CurrentMEM = ToMB(c.Usage.Memory().Value())
|
||||
mmx[c.Name] = mx
|
||||
}
|
||||
}
|
||||
|
|
@ -51,13 +80,13 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
|
|||
for _, no := range nos.Items {
|
||||
nodeMetrics[no.Name] = NodeMetrics{
|
||||
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 {
|
||||
if m, ok := nodeMetrics[mx.Name]; ok {
|
||||
m.CurrentCPU = mx.Usage.Cpu().MilliValue()
|
||||
m.CurrentMEM = toMB(mx.Usage.Memory().Value())
|
||||
m.CurrentMEM = ToMB(mx.Usage.Memory().Value())
|
||||
nodeMetrics[mx.Name] = m
|
||||
}
|
||||
}
|
||||
|
|
@ -74,86 +103,121 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
|
|||
return nil
|
||||
}
|
||||
|
||||
// FetchNodesMetrics return all metrics for pods in a given namespace.
|
||||
func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
|
||||
var mx mv1beta1.NodeMetricsList
|
||||
func (m *MetricsServer) checkAccess(ns, gvr, msg string) error {
|
||||
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 {
|
||||
return &mx, err
|
||||
return err
|
||||
}
|
||||
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()
|
||||
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.
|
||||
func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, error) {
|
||||
var mx mv1beta1.PodMetricsList
|
||||
if m.Connection == nil {
|
||||
return &mx, fmt.Errorf("no client connection")
|
||||
}
|
||||
mx := new(mv1beta1.PodMetricsList)
|
||||
const msg = "user is not authorized to list pods metrics"
|
||||
|
||||
if !m.HasMetrics() {
|
||||
return &mx, fmt.Errorf("No metrics-server detected on cluster")
|
||||
}
|
||||
if ns == NamespaceAll {
|
||||
ns = AllNamespaces
|
||||
}
|
||||
|
||||
auth, err := m.CanI(ns, "metrics.k8s.io/v1beta1/pods", ListAccess)
|
||||
if err != nil {
|
||||
return &mx, err
|
||||
if err := m.checkAccess(ns, "metrics.k8s.io/v1beta1/pods", msg); 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()
|
||||
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.
|
||||
func (m *MetricsServer) FetchPodMetrics(fqn string) (*mv1beta1.PodMetrics, error) {
|
||||
var mx mv1beta1.PodMetrics
|
||||
if m.Connection == nil {
|
||||
return &mx, fmt.Errorf("no client connection")
|
||||
}
|
||||
if !m.HasMetrics() {
|
||||
return &mx, fmt.Errorf("No metrics-server detected on cluster")
|
||||
}
|
||||
var mx *mv1beta1.PodMetrics
|
||||
const msg = "user is not authorized to list pod metrics"
|
||||
|
||||
ns, n := Namespaced(fqn)
|
||||
if ns == NamespaceAll {
|
||||
ns = AllNamespaces
|
||||
}
|
||||
auth, err := m.CanI(ns, "metrics.k8s.io/v1beta1/pods", GetAccess)
|
||||
if err != nil {
|
||||
return &mx, err
|
||||
if err := m.checkAccess(ns, "metrics.k8s.io/v1beta1/pods", msg); err != nil {
|
||||
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()
|
||||
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.
|
||||
|
|
@ -167,7 +231,7 @@ func (m *MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetri
|
|||
var mx PodMetrics
|
||||
for _, c := range p.Containers {
|
||||
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
|
||||
}
|
||||
|
|
@ -178,8 +242,8 @@ func (m *MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetri
|
|||
|
||||
const megaByte = 1024 * 1024
|
||||
|
||||
// toMB converts bytes to megabytes.
|
||||
func toMB(v int64) float64 {
|
||||
// ToMB converts bytes to megabytes.
|
||||
func ToMB(v int64) float64 {
|
||||
return float64(v) / megaByte
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ const (
|
|||
|
||||
// ClusterScope designates a resource is not namespaced.
|
||||
ClusterScope = "-"
|
||||
|
||||
// NotNamespaced designates a non resource namespace.
|
||||
NotNamespaced = "*"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package config
|
|||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
|
|
@ -87,6 +88,13 @@ func (a *Aliases) loadDefaults() {
|
|||
// Load K9s aliases.
|
||||
func (a *Aliases) Load() error {
|
||||
a.loadDefaults()
|
||||
|
||||
_, err := os.Stat(K9sAlias)
|
||||
if os.IsNotExist(err) {
|
||||
log.Debug().Err(err).Msgf("No custom aliases found")
|
||||
return nil
|
||||
}
|
||||
|
||||
return a.LoadAliases(K9sAlias)
|
||||
}
|
||||
|
||||
|
|
@ -139,8 +147,14 @@ func (a *Aliases) Define(gvr string, aliases ...string) {
|
|||
}
|
||||
}
|
||||
|
||||
// LoadAliases loads alias from a given file.
|
||||
func (a *Aliases) LoadAliases(path string) error {
|
||||
// Load K9s aliases.
|
||||
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)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msgf("No custom aliases found")
|
||||
|
|
@ -161,6 +175,63 @@ func (a *Aliases) LoadAliases(path string) error {
|
|||
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.
|
||||
func (a *Aliases) Save() error {
|
||||
log.Debug().Msg("[Config] Saving Aliases...")
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ func TestAliasDefine(t *testing.T) {
|
|||
func TestAliasesLoad(t *testing.T) {
|
||||
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))
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +82,6 @@ func TestAliasesSave(t *testing.T) {
|
|||
a.Alias["blee"] = "duh"
|
||||
|
||||
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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,17 +21,31 @@ type StyleListener interface {
|
|||
}
|
||||
|
||||
type (
|
||||
// Color represents a color.
|
||||
Color string
|
||||
|
||||
// Colors tracks multiple colors.
|
||||
Colors []Color
|
||||
|
||||
// Styles tracks K9s styling options.
|
||||
Styles struct {
|
||||
K9s Style `yaml:"k9s"`
|
||||
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 struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
BgColor string `yaml:"bgColor"`
|
||||
LogoColor string `yaml:"logoColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
LogoColor Color `yaml:"logoColor"`
|
||||
}
|
||||
|
||||
// Frame tracks frame styles.
|
||||
|
|
@ -45,120 +59,171 @@ type (
|
|||
|
||||
// Views tracks individual view styles.
|
||||
Views struct {
|
||||
Yaml Yaml `yaml:"yaml"`
|
||||
Log Log `yaml:"logs"`
|
||||
Table Table `yaml:"table"`
|
||||
Xray Xray `yaml:"xray"`
|
||||
Charts Charts `yaml:"charts"`
|
||||
Yaml Yaml `yaml:"yaml"`
|
||||
Log Log `yaml:"logs"`
|
||||
}
|
||||
|
||||
// Status tracks resource status styles.
|
||||
Status struct {
|
||||
NewColor string `yaml:"newColor"`
|
||||
ModifyColor string `yaml:"modifyColor"`
|
||||
AddColor string `yaml:"addColor"`
|
||||
ErrorColor string `yaml:"errorColor"`
|
||||
HighlightColor string `yaml:"highlightColor"`
|
||||
KillColor string `yaml:"killColor"`
|
||||
CompletedColor string `yaml:"completedColor"`
|
||||
NewColor Color `yaml:"newColor"`
|
||||
ModifyColor Color `yaml:"modifyColor"`
|
||||
AddColor Color `yaml:"addColor"`
|
||||
ErrorColor Color `yaml:"errorColor"`
|
||||
HighlightColor Color `yaml:"highlightColor"`
|
||||
KillColor Color `yaml:"killColor"`
|
||||
CompletedColor Color `yaml:"completedColor"`
|
||||
}
|
||||
|
||||
// Log tracks Log styles.
|
||||
Log struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
BgColor string `yaml:"bgColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
}
|
||||
|
||||
// Yaml tracks yaml styles.
|
||||
Yaml struct {
|
||||
KeyColor string `yaml:"keyColor"`
|
||||
ValueColor string `yaml:"valueColor"`
|
||||
ColonColor string `yaml:"colonColor"`
|
||||
KeyColor Color `yaml:"keyColor"`
|
||||
ValueColor Color `yaml:"valueColor"`
|
||||
ColonColor Color `yaml:"colonColor"`
|
||||
}
|
||||
|
||||
// Title tracks title styles.
|
||||
Title struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
BgColor string `yaml:"bgColor"`
|
||||
HighlightColor string `yaml:"highlightColor"`
|
||||
CounterColor string `yaml:"counterColor"`
|
||||
FilterColor string `yaml:"filterColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
HighlightColor Color `yaml:"highlightColor"`
|
||||
CounterColor Color `yaml:"counterColor"`
|
||||
FilterColor Color `yaml:"filterColor"`
|
||||
}
|
||||
|
||||
// Info tracks info styles.
|
||||
Info struct {
|
||||
SectionColor string `yaml:"sectionColor"`
|
||||
FgColor string `yaml:"fgColor"`
|
||||
SectionColor Color `yaml:"sectionColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
}
|
||||
|
||||
// Border tracks border styles.
|
||||
Border struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
FocusColor string `yaml:"focusColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
FocusColor Color `yaml:"focusColor"`
|
||||
}
|
||||
|
||||
// Crumb tracks crumbs styles.
|
||||
Crumb struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
BgColor string `yaml:"bgColor"`
|
||||
ActiveColor string `yaml:"activeColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
ActiveColor Color `yaml:"activeColor"`
|
||||
}
|
||||
|
||||
// Table tracks table styles.
|
||||
Table struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
BgColor string `yaml:"bgColor"`
|
||||
CursorColor string `yaml:"cursorColor"`
|
||||
MarkColor string `yaml:"markColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
CursorColor Color `yaml:"cursorColor"`
|
||||
MarkColor Color `yaml:"markColor"`
|
||||
Header TableHeader `yaml:"header"`
|
||||
}
|
||||
|
||||
// TableHeader tracks table header styles.
|
||||
TableHeader struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
BgColor string `yaml:"bgColor"`
|
||||
SorterColor string `yaml:"sorterColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
SorterColor Color `yaml:"sorterColor"`
|
||||
}
|
||||
|
||||
// Xray tracks xray styles.
|
||||
Xray struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
BgColor string `yaml:"bgColor"`
|
||||
CursorColor string `yaml:"cursorColor"`
|
||||
GraphicColor string `yaml:"graphicColor"`
|
||||
ShowIcons bool `yaml:"showIcons"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
CursorColor Color `yaml:"cursorColor"`
|
||||
GraphicColor Color `yaml:"graphicColor"`
|
||||
ShowIcons bool `yaml:"showIcons"`
|
||||
}
|
||||
|
||||
// Menu tracks menu styles.
|
||||
Menu struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
KeyColor string `yaml:"keyColor"`
|
||||
NumKeyColor string `yaml:"numKeyColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
KeyColor Color `yaml:"keyColor"`
|
||||
NumKeyColor Color `yaml:"numKeyColor"`
|
||||
}
|
||||
|
||||
// Style tracks K9s styles.
|
||||
Style struct {
|
||||
Body Body `yaml:"body"`
|
||||
Frame Frame `yaml:"frame"`
|
||||
Info Info `yaml:"info"`
|
||||
Table Table `yaml:"table"`
|
||||
Xray Xray `yaml:"xray"`
|
||||
Views Views `yaml:"views"`
|
||||
// Charts tracks charts styles.
|
||||
Charts struct {
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
DialBgColor Color `yaml:"dialBgColor"`
|
||||
ChartBgColor Color `yaml:"chartBgColor"`
|
||||
DefaultDialColors Colors `yaml:"defaultDialColors"`
|
||||
DefaultChartColors Colors `yaml:"defaultChartColors"`
|
||||
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)
|
||||
}
|
||||
|
||||
// Color 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())
|
||||
}
|
||||
|
||||
// Colors 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 {
|
||||
return Style{
|
||||
Body: newBody(),
|
||||
Frame: newFrame(),
|
||||
Info: newInfo(),
|
||||
Table: newTable(),
|
||||
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 {
|
||||
return Views{
|
||||
Yaml: newYaml(),
|
||||
Log: newLog(),
|
||||
Table: newTable(),
|
||||
Xray: newXray(),
|
||||
Charts: newCharts(),
|
||||
Yaml: newYaml(),
|
||||
Log: newLog(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +253,7 @@ func newStatus() Status {
|
|||
ErrorColor: "orangered",
|
||||
HighlightColor: "aqua",
|
||||
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
|
||||
func (s *Styles) DefaultSkin() {
|
||||
s.K9s = newStyle()
|
||||
|
|
@ -299,12 +369,12 @@ func (s *Styles) DefaultSkin() {
|
|||
|
||||
// FgColor returns the foreground color.
|
||||
func (s *Styles) FgColor() tcell.Color {
|
||||
return AsColor(s.Body().FgColor)
|
||||
return s.Body().FgColor.Color()
|
||||
}
|
||||
|
||||
// BgColor returns the background color.
|
||||
func (s *Styles) BgColor() tcell.Color {
|
||||
return AsColor(s.Body().BgColor)
|
||||
return s.Body().BgColor.Color()
|
||||
}
|
||||
|
||||
// AddListener registers a new listener.
|
||||
|
|
@ -353,14 +423,19 @@ func (s *Styles) Title() Title {
|
|||
return s.Frame().Title
|
||||
}
|
||||
|
||||
// Charts returns charts styles.
|
||||
func (s *Styles) Charts() Charts {
|
||||
return s.K9s.Views.Charts
|
||||
}
|
||||
|
||||
// Table returns table styles.
|
||||
func (s *Styles) Table() Table {
|
||||
return s.K9s.Table
|
||||
return s.K9s.Views.Table
|
||||
}
|
||||
|
||||
// Xray returns xray styles.
|
||||
func (s *Styles) Xray() Xray {
|
||||
return s.K9s.Xray
|
||||
return s.K9s.Views.Xray
|
||||
}
|
||||
|
||||
// Views returns views styles.
|
||||
|
|
@ -388,19 +463,7 @@ func (s *Styles) Update() {
|
|||
tview.Styles.PrimitiveBackgroundColor = s.BgColor()
|
||||
tview.Styles.ContrastBackgroundColor = s.BgColor()
|
||||
tview.Styles.PrimaryTextColor = s.FgColor()
|
||||
tview.Styles.BorderColor = AsColor(s.K9s.Frame.Border.FgColor)
|
||||
tview.Styles.FocusColor = AsColor(s.K9s.Frame.Border.FocusColor)
|
||||
tview.Styles.BorderColor = s.K9s.Frame.Border.FgColor.Color()
|
||||
tview.Styles.FocusColor = s.K9s.Frame.Border.FocusColor.Color()
|
||||
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"
|
||||
)
|
||||
|
||||
func TestAsColor(t *testing.T) {
|
||||
func TestColor(t *testing.T) {
|
||||
uu := map[string]tcell.Color{
|
||||
"blah": tcell.ColorDefault,
|
||||
"blue": tcell.ColorBlue,
|
||||
|
|
@ -20,7 +20,7 @@ func TestAsColor(t *testing.T) {
|
|||
for k := range uu {
|
||||
c, u := k, uu[k]
|
||||
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"))
|
||||
s.Update()
|
||||
|
||||
assert.Equal(t, "cadetblue", s.Body().FgColor)
|
||||
assert.Equal(t, "black", s.Body().BgColor)
|
||||
assert.Equal(t, "black", s.Table().BgColor)
|
||||
assert.Equal(t, "cadetblue", s.Body().FgColor.String())
|
||||
assert.Equal(t, "black", s.Body().BgColor.String())
|
||||
assert.Equal(t, "black", s.Table().BgColor.String())
|
||||
assert.Equal(t, tcell.ColorCadetBlue, s.FgColor())
|
||||
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
||||
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"))
|
||||
s.Update()
|
||||
|
||||
assert.Equal(t, "white", s.Body().FgColor)
|
||||
assert.Equal(t, "black", s.Body().BgColor)
|
||||
assert.Equal(t, "black", s.Table().BgColor)
|
||||
assert.Equal(t, "white", s.Body().FgColor.String())
|
||||
assert.Equal(t, "black", s.Body().BgColor.String())
|
||||
assert.Equal(t, "black", s.Table().BgColor.String())
|
||||
assert.Equal(t, tcell.ColorWhite, s.FgColor())
|
||||
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
||||
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 {
|
||||
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)
|
||||
if err != nil {
|
||||
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))
|
||||
for _, co := range po.Spec.InitContainers {
|
||||
res = append(res, makeContainerRes(co, po, pmx, true))
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ type Deployment struct {
|
|||
Resource
|
||||
}
|
||||
|
||||
// IsHappy check for happy deployments.
|
||||
func (d *Deployment) IsHappy(dp appsv1.Deployment) bool {
|
||||
return dp.Status.Replicas == dp.Status.AvailableReplicas
|
||||
}
|
||||
|
||||
// Scale a Deployment.
|
||||
func (d *Deployment) Scale(path string, replicas int32) error {
|
||||
ns, n := client.Namespaced(path)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ type DaemonSet struct {
|
|||
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.
|
||||
func (d *DaemonSet) Restart(path string) error {
|
||||
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")
|
||||
}
|
||||
|
||||
mx := client.NewMetricsServer(n.Client())
|
||||
nmx, err := mx.FetchNodesMetrics()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("No node metrics")
|
||||
var (
|
||||
nmx *mv1beta1.NodeMetricsList
|
||||
err error
|
||||
)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,16 @@ type Pod struct {
|
|||
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.
|
||||
func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
// No Deal!
|
||||
mx := client.NewMetricsServer(p.Client())
|
||||
pmx, err := mx.FetchPodMetrics(path)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("No pods metrics")
|
||||
var pmx *mv1beta1.PodMetrics
|
||||
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
||||
if pmx, err = client.DialMetrics(p.Client()).FetchPodMetrics(path); err != nil {
|
||||
log.Warn().Err(err).Msgf("No pod metrics")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
mx := client.NewMetricsServer(p.Client())
|
||||
pmx, err := mx.FetchPodsMetrics(ns)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("No pods metrics")
|
||||
var pmx *mv1beta1.PodMetricsList
|
||||
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
||||
if pmx, err = client.DialMetrics(p.Client()).FetchPodsMetrics(ns); err != nil {
|
||||
log.Warn().Err(err).Msgf("No pods metrics")
|
||||
}
|
||||
}
|
||||
|
||||
var res []runtime.Object
|
||||
|
|
@ -128,7 +139,7 @@ func (p *Pod) Containers(path string, includeInit bool) ([]string, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
cc := []string{}
|
||||
cc := make([]string, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers))
|
||||
for _, c := range pod.Spec.Containers {
|
||||
cc = append(cc, c.Name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// Pulse tracks pulses.
|
||||
type Pulse struct {
|
||||
NonResource
|
||||
}
|
||||
|
||||
// List lists out pulses.
|
||||
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) {
|
||||
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{
|
||||
Name: "xray",
|
||||
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
|
||||
}
|
||||
|
||||
// IsHappy check for happy sts.
|
||||
func (s *StatefulSet) IsHappy(sts appsv1.StatefulSet) bool {
|
||||
return sts.Status.Replicas == sts.Status.ReadyReplicas
|
||||
}
|
||||
|
||||
// Scale a StatefulSet.
|
||||
func (s *StatefulSet) Scale(path string, replicas int32) error {
|
||||
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"
|
||||
KeyStyles ContextKey = "styles"
|
||||
KeyMetrics ContextKey = "metrics"
|
||||
KeyToast ContextKey = "toast"
|
||||
KeyWithMetrics ContextKey = "withMetrics"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ type (
|
|||
func NewCluster(f dao.Factory) *Cluster {
|
||||
return &Cluster{
|
||||
factory: f,
|
||||
mx: client.NewMetricsServer(f.Client()),
|
||||
mx: client.DialMetrics(f.Client()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
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
|
||||
)
|
||||
|
||||
// LevelMessage tracks an message and severity.
|
||||
type LevelMessage struct {
|
||||
Level FlashLevel
|
||||
Text string
|
||||
}
|
||||
|
||||
func newClearMessage() LevelMessage {
|
||||
return LevelMessage{}
|
||||
}
|
||||
|
||||
// IsClear returns true if message is empty.
|
||||
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
|
||||
}
|
||||
|
||||
// NewFlash returns a new instance.
|
||||
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,101 @@
|
|||
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(5 * delay)
|
||||
s, _, l, m := v.getMetrics()
|
||||
assert.Equal(t, 1, s)
|
||||
assert.Equal(t, u.level, l)
|
||||
assert.Equal(t, u.e, m)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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, _, l, m := v.getMetrics()
|
||||
assert.Equal(t, count, s)
|
||||
assert.Equal(t, model.FlashInfo, l)
|
||||
assert.Equal(t, fmt.Sprintf("test-%d", count), m)
|
||||
}
|
||||
|
||||
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,160 @@
|
|||
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
|
||||
}
|
||||
|
||||
// NewPulse returns a new pulse.
|
||||
func NewPulse(gvr string) *Pulse {
|
||||
return &Pulse{
|
||||
gvr: gvr,
|
||||
refreshRate: defaultRefreshRate,
|
||||
}
|
||||
}
|
||||
|
||||
// Watch monitors pulses.
|
||||
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,117 @@
|
|||
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"
|
||||
)
|
||||
|
||||
// PulseHealth tracks resources health.
|
||||
type PulseHealth struct {
|
||||
factory dao.Factory
|
||||
}
|
||||
|
||||
// NewPulseHealth returns a new instance.
|
||||
func NewPulseHealth(f dao.Factory) *PulseHealth {
|
||||
return &PulseHealth{
|
||||
factory: f,
|
||||
}
|
||||
}
|
||||
|
||||
// List returns a canned collection of resources health.
|
||||
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{},
|
||||
Renderer: &render.Chart{},
|
||||
},
|
||||
"pulses": {
|
||||
DAO: &dao.Pulse{},
|
||||
},
|
||||
"openfaas": {
|
||||
DAO: &dao.OpenFaas{},
|
||||
Renderer: &render.OpenFaas{},
|
||||
|
|
|
|||
|
|
@ -108,8 +108,8 @@ func (s *Stack) Pop() (Component, bool) {
|
|||
var c Component
|
||||
s.mx.Lock()
|
||||
{
|
||||
c = s.components[s.size()]
|
||||
s.components = s.components[:s.size()]
|
||||
c = s.components[len(s.components)-1]
|
||||
s.components = s.components[:len(s.components)-1]
|
||||
}
|
||||
s.mx.Unlock()
|
||||
s.notify(StackPop, c)
|
||||
|
|
@ -163,11 +163,7 @@ func (s *Stack) Top() Component {
|
|||
return nil
|
||||
}
|
||||
|
||||
return s.components[s.size()]
|
||||
}
|
||||
|
||||
func (s *Stack) size() int {
|
||||
return len(s.components) - 1
|
||||
return s.components[len(s.components)-1]
|
||||
}
|
||||
|
||||
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.
|
||||
func (t *Table) ClusterWide() bool {
|
||||
log.Debug().Msgf("CLUSTER-WIDE %q", 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) {
|
||||
ns = client.AllNamespaces
|
||||
}
|
||||
|
||||
return a.List(ctx, ns)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,10 +29,11 @@ func TestTableReconcile(t *testing.T) {
|
|||
f.rows = []runtime.Object{load(t, "p1")}
|
||||
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
||||
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
||||
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
|
||||
err := ta.reconcile(ctx)
|
||||
assert.Nil(t, err)
|
||||
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, client.NamespaceAll, data.Namespace)
|
||||
}
|
||||
|
|
@ -55,6 +56,7 @@ func TestTableGet(t *testing.T) {
|
|||
f := makeFactory()
|
||||
f.rows = []runtime.Object{load(t, "p1")}
|
||||
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
||||
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
|
||||
row, err := ta.Get(ctx, "fred")
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, row)
|
||||
|
|
@ -104,7 +106,7 @@ func TestTableHydrate(t *testing.T) {
|
|||
|
||||
assert.Nil(t, hydrate("blee", oo, rr, render.Pod{}))
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -30,9 +30,10 @@ func TestTableRefresh(t *testing.T) {
|
|||
f.rows = []runtime.Object{mustLoad("p1")}
|
||||
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
||||
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
||||
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
|
||||
ta.Refresh(ctx)
|
||||
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, client.NamespaceAll, data.Namespace)
|
||||
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) {
|
||||
t.root = root
|
||||
t.fireTreeTreeChanged(t.root)
|
||||
t.fireTreeChanged(t.root)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -251,7 +251,7 @@ func (t *Tree) resourceMeta() ResourceMeta {
|
|||
return meta
|
||||
}
|
||||
|
||||
func (t *Tree) fireTreeTreeChanged(root *xray.TreeNode) {
|
||||
func (t *Tree) fireTreeChanged(root *xray.TreeNode) {
|
||||
for _, l := range t.listeners {
|
||||
l.TreeChanged(root)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
|
@ -8,6 +9,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
"golang.org/x/text/language"
|
||||
|
|
@ -28,11 +30,10 @@ var (
|
|||
type Benchmark struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Benchmark) ColorerFunc() ColorerFunc {
|
||||
func (b Benchmark) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := tcell.ColorPaleGreen
|
||||
statusCol := 2
|
||||
if strings.TrimSpace(re.Row.Fields[statusCol]) != "pass" {
|
||||
if !Happy(ns, re.Row) {
|
||||
c = ErrColor
|
||||
}
|
||||
return c
|
||||
|
|
@ -50,6 +51,7 @@ func (Benchmark) Header(ns string) HeaderRow {
|
|||
Header{Name: "2XX", Align: tview.AlignRight},
|
||||
Header{Name: "4XX/5XX", Align: tview.AlignRight},
|
||||
Header{Name: "REPORT"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
|
@ -72,6 +74,24 @@ func (b Benchmark) Render(o interface{}, ns string, r *Row) error {
|
|||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
@ -87,7 +107,7 @@ func (Benchmark) readFile(file string) (string, error) {
|
|||
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(), "_")
|
||||
if len(tokens) < 2 {
|
||||
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[1] = tokens[1]
|
||||
row[7] = f.Name()
|
||||
row[8] = timeToAge(f.ModTime())
|
||||
row[9] = timeToAge(f.ModTime())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ type Chart struct{}
|
|||
// ColorerFunc colors a resource row.
|
||||
func (Chart) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return tcell.ColorMediumSpringGreen
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +39,7 @@ func (Chart) Header(ns string) HeaderRow {
|
|||
Header{Name: "STATUS"},
|
||||
Header{Name: "CHART"},
|
||||
Header{Name: "APP VERSION"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
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.Chart.Metadata.Name+"-"+h.Release.Chart.Metadata.Version,
|
||||
h.Release.Chart.Metadata.AppVersion,
|
||||
asStatus(c.diagnose(h.Release.Info.Status.String())),
|
||||
toAge(metav1.Time{Time: h.Release.Info.LastDeployed.Time}),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Chart) diagnose(s string) error {
|
||||
if s != "deployed" {
|
||||
return fmt.Errorf("chart is in an invalid state")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -36,18 +37,18 @@ type ContainerWithMetrics interface {
|
|||
// Container renders a K8s Container to screen.
|
||||
type Container struct{}
|
||||
|
||||
const readyCol = 2
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Container) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
func (c Container) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
color := DefaultColorer(ns, re)
|
||||
|
||||
readyCol := 2
|
||||
if strings.TrimSpace(r.Row.Fields[readyCol]) == "false" {
|
||||
c = ErrColor
|
||||
if !Happy(ns, re.Row) {
|
||||
color = ErrColor
|
||||
}
|
||||
|
||||
stateCol := readyCol + 1
|
||||
switch strings.TrimSpace(r.Row.Fields[stateCol]) {
|
||||
switch strings.TrimSpace(re.Row.Fields[stateCol]) {
|
||||
case ContainerCreating, PodInitializing:
|
||||
return AddColor
|
||||
case Terminating, Initialized:
|
||||
|
|
@ -56,10 +57,10 @@ func (Container) ColorerFunc() ColorerFunc {
|
|||
return CompletedColor
|
||||
case Running:
|
||||
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: "%MEM/L", Align: tview.AlignRight},
|
||||
Header{Name: "PORTS"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
|
@ -114,12 +116,25 @@ func (c Container) Render(o interface{}, name string, r *Row) error {
|
|||
limit.cpu,
|
||||
limit.mem,
|
||||
toStrPorts(co.Container.Ports),
|
||||
asStatus(c.diagnose(state, ready)),
|
||||
toAge(co.Age),
|
||||
)
|
||||
|
||||
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...
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ func TestContainer(t *testing.T) {
|
|||
"50",
|
||||
"20",
|
||||
"",
|
||||
"container is not ready",
|
||||
},
|
||||
r.Fields[:len(r.Fields)-1],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ func (ClusterRole) ColorerFunc() ColorerFunc {
|
|||
func (ClusterRole) Header(string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
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.Fields = Fields{
|
||||
cr.Name,
|
||||
mapToStr(cr.Labels),
|
||||
toAge(cr.ObjectMeta.CreationTimestamp),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,8 +22,9 @@ func (ClusterRoleBinding) Header(string) HeaderRow {
|
|||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "CLUSTERROLE"},
|
||||
Header{Name: "KIND"},
|
||||
Header{Name: "SUBJECT-KIND"},
|
||||
Header{Name: "SUBJECTS"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
|
@ -48,6 +49,7 @@ func (ClusterRoleBinding) Render(o interface{}, ns string, r *Row) error {
|
|||
crb.RoleRef.Name,
|
||||
kind,
|
||||
ss,
|
||||
mapToStr(crb.Labels),
|
||||
toAge(crb.ObjectMeta.CreationTimestamp),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestClusterRoleBindingRender(t *testing.T) {
|
|||
c.Render(load(t, "crb"), "-", &r)
|
||||
|
||||
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 {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
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.Fields = Fields{
|
||||
extractMetaField(meta, "name"),
|
||||
mapToIfc(meta["labels"]),
|
||||
toAge(metav1.Time{Time: t}),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ package render
|
|||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
|
@ -31,6 +34,11 @@ func (CronJob) Header(ns string) HeaderRow {
|
|||
Header{Name: "SUSPEND"},
|
||||
Header{Name: "ACTIVE"},
|
||||
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},
|
||||
)
|
||||
}
|
||||
|
|
@ -63,8 +71,64 @@ func (c CronJob) Render(o interface{}, ns string, r *Row) error {
|
|||
boolPtrToStr(cj.Spec.Suspend),
|
||||
strconv.Itoa(len(cj.Status.Active)),
|
||||
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),
|
||||
)
|
||||
|
||||
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 (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
|
|
@ -17,19 +16,13 @@ import (
|
|||
type Deployment struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Deployment) ColorerFunc() ColorerFunc {
|
||||
func (d Deployment) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
readyCol := 2
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
readyCol--
|
||||
}
|
||||
tokens := strings.Split(r.Row.Fields[readyCol], "/")
|
||||
if tokens[0] != tokens[1] {
|
||||
if !Happy(ns, r.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
|
|
@ -49,6 +42,9 @@ func (Deployment) Header(ns string) HeaderRow {
|
|||
Header{Name: "READY"},
|
||||
Header{Name: "UP-TO-DATE", 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},
|
||||
)
|
||||
}
|
||||
|
|
@ -73,11 +69,21 @@ func (d Deployment) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
r.Fields = append(r.Fields,
|
||||
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.AvailableReplicas)),
|
||||
strconv.Itoa(int(dp.Status.ReadyReplicas)),
|
||||
mapToStr(dp.Labels),
|
||||
asStatus(d.diagnose(dp.Status.Replicas, dp.Status.AvailableReplicas)),
|
||||
toAge(dp.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
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 (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
|
|
@ -17,18 +16,14 @@ import (
|
|||
type DaemonSet struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (DaemonSet) ColorerFunc() ColorerFunc {
|
||||
func (d DaemonSet) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
desiredCol := 2
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
desiredCol = 1
|
||||
}
|
||||
if strings.TrimSpace(r.Row.Fields[desiredCol]) != strings.TrimSpace(r.Row.Fields[desiredCol+2]) {
|
||||
if !Happy(ns, r.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
|
|
@ -50,6 +45,8 @@ func (DaemonSet) Header(ns string) HeaderRow {
|
|||
Header{Name: "READY", Align: tview.AlignRight},
|
||||
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
||||
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
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.UpdatedNumberScheduled)),
|
||||
strconv.Itoa(int(ds.Status.NumberAvailable)),
|
||||
mapToStr(ds.Labels),
|
||||
asStatus(d.diagnose(ds.Status.DesiredNumberScheduled, ds.Status.NumberReady)),
|
||||
toAge(ds.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -17,19 +18,20 @@ import (
|
|||
type Event struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Event) ColorerFunc() ColorerFunc {
|
||||
func (e Event) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
|
||||
if !Happy(ns, r.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
markCol := 3
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
markCol = 2
|
||||
}
|
||||
switch strings.TrimSpace(r.Row.Fields[markCol]) {
|
||||
case "Failed":
|
||||
c = ErrColor
|
||||
case "Killing":
|
||||
c = KillColor
|
||||
if strings.TrimSpace(r.Row.Fields[markCol]) == "Killing" {
|
||||
return KillColor
|
||||
}
|
||||
|
||||
return c
|
||||
|
|
@ -45,10 +47,12 @@ func (Event) Header(ns string) HeaderRow {
|
|||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "TYPE"},
|
||||
Header{Name: "REASON"},
|
||||
Header{Name: "SOURCE"},
|
||||
Header{Name: "COUNT", Align: tview.AlignRight},
|
||||
Header{Name: "MESSAGE"},
|
||||
Header{Name: "MESSAGE", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
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,
|
||||
asRef(ev.InvolvedObject),
|
||||
ev.Type,
|
||||
ev.Reason,
|
||||
ev.Source.Component,
|
||||
strconv.Itoa(int(ev.Count)),
|
||||
ev.Message,
|
||||
asStatus(e.diagnose(ev.Type)),
|
||||
toAge(ev.LastTimestamp))
|
||||
|
||||
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 {
|
||||
return strings.ToLower(r.Kind) + ":" + r.Name
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,17 @@ func TestEventRender(t *testing.T) {
|
|||
c.Render(load(t, "ev"), "", &r)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Happy returns true if resoure is happy, false otherwise
|
||||
func (Generic) Happy(ns string, r Row) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// SetTable sets the tabular resource.
|
||||
func (g *Generic) SetTable(t *metav1beta1.Table) {
|
||||
g.table = t
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ import (
|
|||
"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
|
||||
|
||||
// ToMB converts bytes to megabytes.
|
||||
|
|
@ -20,6 +26,13 @@ func ToMB(v int64) float64 {
|
|||
return float64(v) / megaByte
|
||||
}
|
||||
|
||||
func asStatus(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
func asSelector(s *metav1.LabelSelector) string {
|
||||
sel, err := metav1.LabelSelectorAsSelector(s)
|
||||
if err != nil {
|
||||
|
|
@ -84,7 +97,7 @@ func join(a []string, sep string) string {
|
|||
|
||||
var buff strings.Builder
|
||||
buff.Grow(n)
|
||||
buff.WriteString(a[0])
|
||||
buff.WriteString(b[0])
|
||||
for _, s := range b[1:] {
|
||||
buff.WriteString(sep)
|
||||
buff.WriteString(s)
|
||||
|
|
@ -163,7 +176,40 @@ func mapToStr(m map[string]string) (s string) {
|
|||
for i, k := range kk {
|
||||
s += k + "=" + m[k]
|
||||
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
|
||||
e string
|
||||
}{
|
||||
"zero": {[]string{}, ""},
|
||||
"std": {[]string{"a", "b", "c"}, "a,b,c"},
|
||||
"blank": {[]string{"", "", ""}, ""},
|
||||
"sparse": {[]string{"a", "", "c"}, "a,c"},
|
||||
"zero": {[]string{}, ""},
|
||||
"std": {[]string{"a", "b", "c"}, "a,b,c"},
|
||||
"blank": {[]string{"", "", ""}, ""},
|
||||
"sparse": {[]string{"a", "", "c"}, "a,c"},
|
||||
"withBlank": {[]string{"", "a", "c"}, "a,c"},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
|
|
@ -304,7 +305,7 @@ func TestMapToStr(t *testing.T) {
|
|||
i map[string]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{}, ""},
|
||||
}
|
||||
for _, u := range uu {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ func (HorizontalPodAutoscaler) Header(ns string) HeaderRow {
|
|||
Header{Name: "MINPODS", Align: tview.AlignRight},
|
||||
Header{Name: "MAXPODS", Align: tview.AlignRight},
|
||||
Header{Name: "REPLICAS", Align: tview.AlignRight},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
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.MaxReplicas)),
|
||||
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
||||
"",
|
||||
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.MaxReplicas)),
|
||||
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
||||
"",
|
||||
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.MaxReplicas)),
|
||||
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
||||
"",
|
||||
toAge(hpa.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ func (Ingress) Header(ns string) HeaderRow {
|
|||
Header{Name: "HOSTS"},
|
||||
Header{Name: "ADDRESS"},
|
||||
Header{Name: "PORT"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -57,6 +58,7 @@ func (i Ingress) Render(o interface{}, ns string, r *Row) error {
|
|||
toHosts(ing.Spec.Rules),
|
||||
toAddress(ing.Status.LoadBalancer),
|
||||
toTLSPorts(ing.Spec.TLS),
|
||||
"",
|
||||
toAge(ing.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/derailed/k9s/internal/client"
|
||||
batchv1 "k8s.io/api/batch/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/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/duration"
|
||||
|
|
@ -33,8 +34,10 @@ func (Job) Header(ns string) HeaderRow {
|
|||
Header{Name: "NAME"},
|
||||
Header{Name: "COMPLETIONS"},
|
||||
Header{Name: "DURATION"},
|
||||
Header{Name: "CONTAINERS"},
|
||||
Header{Name: "IMAGES"},
|
||||
Header{Name: "SELECTOR", Wide: true},
|
||||
Header{Name: "CONTAINERS", Wide: true},
|
||||
Header{Name: "IMAGES", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -50,6 +53,7 @@ func (j Job) Render(o interface{}, ns string, r *Row) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ready := toCompletion(job.Spec, job.Status)
|
||||
|
||||
r.ID = client.MetaFQN(job.ObjectMeta)
|
||||
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)
|
||||
r.Fields = append(r.Fields,
|
||||
job.Name,
|
||||
toCompletion(job.Spec, job.Status),
|
||||
ready,
|
||||
toDuration(job.Status),
|
||||
jobSelector(job.Spec),
|
||||
cc,
|
||||
ii,
|
||||
asStatus(j.diagnose(ready, job.Status.CompletionTime)),
|
||||
toAge(job.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
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...
|
||||
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestJobRender(t *testing.T) {
|
|||
c.Render(load(t, "job"), "", &r)
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
|
@ -22,8 +25,16 @@ const (
|
|||
type Node struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Node) ColorerFunc() ColorerFunc {
|
||||
return DefaultColorer
|
||||
func (n Node) ColorerFunc() ColorerFunc {
|
||||
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.
|
||||
|
|
@ -31,17 +42,19 @@ func (Node) Header(_ string) HeaderRow {
|
|||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "STATUS"},
|
||||
Header{Name: "ROLE"},
|
||||
Header{Name: "VERSION"},
|
||||
Header{Name: "KERNEL"},
|
||||
Header{Name: "INTERNAL-IP"},
|
||||
Header{Name: "EXTERNAL-IP"},
|
||||
Header{Name: "ROLE", Wide: true},
|
||||
Header{Name: "VERSION", Wide: true},
|
||||
Header{Name: "KERNEL", Wide: true},
|
||||
Header{Name: "INTERNAL-IP", Wide: true},
|
||||
Header{Name: "EXTERNAL-IP", Wide: true},
|
||||
Header{Name: "CPU", Align: tview.AlignRight},
|
||||
Header{Name: "MEM", Align: tview.AlignRight},
|
||||
Header{Name: "%CPU", Align: tview.AlignRight},
|
||||
Header{Name: "%MEM", Align: tview.AlignRight},
|
||||
Header{Name: "ACPU", Align: tview.AlignRight},
|
||||
Header{Name: "AMEM", Align: tview.AlignRight},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
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)
|
||||
|
||||
sta := make([]string, 10)
|
||||
status(no.Status, no.Spec.Unschedulable, sta)
|
||||
ro := make([]string, 10)
|
||||
nodeRoles(&no, ro)
|
||||
statuses := make(sort.StringSlice, 10)
|
||||
status(no.Status, no.Spec.Unschedulable, statuses)
|
||||
sort.Sort(statuses)
|
||||
roles := make(sort.StringSlice, 10)
|
||||
nodeRoles(&no, roles)
|
||||
sort.Sort(roles)
|
||||
|
||||
r.ID = client.FQN("", na)
|
||||
r.Fields = make(Fields, 0, len(n.Header(ns)))
|
||||
r.Fields = append(r.Fields,
|
||||
no.Name,
|
||||
join(sta, ","),
|
||||
join(ro, ","),
|
||||
join(statuses, ","),
|
||||
join(roles, ","),
|
||||
no.Status.NodeInfo.KubeletVersion,
|
||||
no.Status.NodeInfo.KernelVersion,
|
||||
iIP,
|
||||
|
|
@ -90,12 +105,27 @@ func (n Node) Render(o interface{}, ns string, r *Row) error {
|
|||
p.mem,
|
||||
a.cpu,
|
||||
a.mem,
|
||||
mapToStr(no.Labels),
|
||||
asStatus(n.diagnose(statuses)),
|
||||
toAge(no.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
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...
|
||||
|
||||
|
|
@ -156,6 +186,9 @@ func nodeRoles(node *v1.Node, res []string) {
|
|||
res[index] = v
|
||||
index++
|
||||
}
|
||||
if index >= len(res) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if empty(res) {
|
||||
|
|
|
|||
|
|
@ -28,12 +28,14 @@ func (NetworkPolicy) Header(ns string) HeaderRow {
|
|||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "ING-SELECTOR"},
|
||||
Header{Name: "ING-SELECTOR", Wide: true},
|
||||
Header{Name: "ING-PORTS"},
|
||||
Header{Name: "ING-BLOCK"},
|
||||
Header{Name: "EGR-SELECTOR"},
|
||||
Header{Name: "EGR-SELECTOR", Wide: true},
|
||||
Header{Name: "EGR-PORTS"},
|
||||
Header{Name: "EGR-BLOCK"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -66,6 +68,8 @@ func (n NetworkPolicy) Render(o interface{}, ns string, r *Row) error {
|
|||
es,
|
||||
ep,
|
||||
eb,
|
||||
mapToStr(np.Labels),
|
||||
"",
|
||||
toAge(np.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
|
|
@ -15,7 +16,7 @@ import (
|
|||
type Namespace struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Namespace) ColorerFunc() ColorerFunc {
|
||||
func (n Namespace) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd {
|
||||
|
|
@ -25,9 +26,8 @@ func (Namespace) ColorerFunc() ColorerFunc {
|
|||
if r.Kind == EventUpdate {
|
||||
c = StdColor
|
||||
}
|
||||
switch strings.TrimSpace(r.Row.Fields[1]) {
|
||||
case "Inactive", Terminating:
|
||||
c = ErrColor
|
||||
if !Happy(ns, r.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") {
|
||||
c = HighlightColor
|
||||
|
|
@ -42,12 +42,14 @@ func (Namespace) Header(string) HeaderRow {
|
|||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "STATUS"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
if !ok {
|
||||
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{
|
||||
ns.Name,
|
||||
string(ns.Status.Phase),
|
||||
mapToStr(ns.Labels),
|
||||
asStatus(n.diagnose(ns.Status.Phase)),
|
||||
toAge(ns.ObjectMeta.CreationTimestamp),
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
|
@ -23,8 +24,12 @@ const (
|
|||
type OpenFaas struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (OpenFaas) ColorerFunc() ColorerFunc {
|
||||
func (o OpenFaas) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return tcell.ColorPaleTurquoise
|
||||
}
|
||||
}
|
||||
|
|
@ -44,13 +49,14 @@ func (OpenFaas) Header(ns string) HeaderRow {
|
|||
Header{Name: "INVOCATIONS", Align: tview.AlignRight},
|
||||
Header{Name: "REPLICAS", Align: tview.AlignRight},
|
||||
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a chart to screen.
|
||||
func (f OpenFaas) Render(o interface{}, ns string, r *Row) error {
|
||||
fn, ok := o.(OpenFaasRes)
|
||||
func (o OpenFaas) Render(i interface{}, ns string, r *Row) error {
|
||||
fn, ok := i.(OpenFaasRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected OpenFaasRes, but got %T", o)
|
||||
}
|
||||
|
|
@ -65,7 +71,7 @@ func (f OpenFaas) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
|
||||
r.ID = client.FQN(fn.Function.Namespace, fn.Function.Name)
|
||||
r.Fields = make(Fields, 0, len(f.Header(ns)))
|
||||
r.Fields = make(Fields, 0, len(o.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, fn.Function.Namespace)
|
||||
}
|
||||
|
|
@ -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.Replicas)),
|
||||
strconv.Itoa(int(fn.Function.AvailableReplicas)),
|
||||
asStatus(o.diagnose(status)),
|
||||
toAge(metav1.Time{Time: time.Now()}),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (OpenFaas) diagnose(status string) error {
|
||||
if status != "Ready" {
|
||||
return errors.New("function not ready")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package render
|
|||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
|
|
@ -18,24 +17,19 @@ import (
|
|||
type PodDisruptionBudget struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (PodDisruptionBudget) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
func (p PodDisruptionBudget) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
markCol := 5
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
markCol--
|
||||
}
|
||||
if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) {
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return StdColor
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
|
|
@ -53,6 +47,8 @@ func (PodDisruptionBudget) Header(ns string) HeaderRow {
|
|||
Header{Name: "CURRENT", Align: tview.AlignRight},
|
||||
Header{Name: "DESIRED", Align: tview.AlignRight},
|
||||
Header{Name: "EXPECTED", Align: tview.AlignRight},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
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.DesiredHealthy)),
|
||||
strconv.Itoa(int(pdb.Status.ExpectedPods)),
|
||||
mapToStr(pdb.Labels),
|
||||
asStatus(p.diagnose(pdb.Spec.MinAvailable.IntVal, pdb.Status.CurrentHealthy)),
|
||||
toAge(pdb.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
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...
|
||||
|
||||
func numbToStr(n *intstr.IntOrString) string {
|
||||
|
|
|
|||
|
|
@ -25,15 +25,11 @@ func (p Pod) ColorerFunc() ColorerFunc {
|
|||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
|
||||
readyCol := 2
|
||||
statusCol := 4
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
readyCol--
|
||||
statusCol--
|
||||
}
|
||||
statusCol := readyCol + 1
|
||||
|
||||
ready, status := strings.TrimSpace(re.Row.Fields[readyCol]), strings.TrimSpace(re.Row.Fields[statusCol])
|
||||
c = p.checkReadyCol(ready, status, c)
|
||||
|
||||
status := strings.TrimSpace(re.Row.Fields[statusCol])
|
||||
switch status {
|
||||
case ContainerCreating, PodInitializing:
|
||||
c = AddColor
|
||||
|
|
@ -42,28 +38,19 @@ func (p Pod) ColorerFunc() ColorerFunc {
|
|||
case Completed:
|
||||
c = CompletedColor
|
||||
case Running:
|
||||
c = StdColor
|
||||
case Terminating:
|
||||
c = KillColor
|
||||
default:
|
||||
c = ErrColor
|
||||
if !Happy(ns, re.Row) {
|
||||
c = ErrColor
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
func (Pod) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
|
|
@ -74,17 +61,19 @@ func (Pod) Header(ns string) HeaderRow {
|
|||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "READY"},
|
||||
Header{Name: "STATUS"},
|
||||
Header{Name: "RS", Align: tview.AlignRight},
|
||||
Header{Name: "STATUS"},
|
||||
Header{Name: "CPU", Align: tview.AlignRight},
|
||||
Header{Name: "MEM", Align: tview.AlignRight},
|
||||
Header{Name: "%CPU/R", Align: tview.AlignRight},
|
||||
Header{Name: "%MEM/R", Align: tview.AlignRight},
|
||||
Header{Name: "%CPU/L", Align: tview.AlignRight},
|
||||
Header{Name: "%MEM/L", Align: tview.AlignRight},
|
||||
Header{Name: "IP"},
|
||||
Header{Name: "NODE"},
|
||||
Header{Name: "QOS"},
|
||||
Header{Name: "IP", Wide: true},
|
||||
Header{Name: "NODE", Wide: true},
|
||||
Header{Name: "QOS", Wide: true},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -105,7 +94,7 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
|||
ss := po.Status.ContainerStatuses
|
||||
cr, _, rc := p.Statuses(ss)
|
||||
c, perc := p.gatherPodMX(&po, pwm.MX)
|
||||
|
||||
phase := p.Phase(&po)
|
||||
r.ID = client.MetaFQN(po.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(p.Header(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,
|
||||
po.ObjectMeta.Name,
|
||||
strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)),
|
||||
p.Phase(&po),
|
||||
strconv.Itoa(rc),
|
||||
phase,
|
||||
c.cpu,
|
||||
c.mem,
|
||||
perc.cpu,
|
||||
|
|
@ -125,12 +114,25 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
|||
na(po.Status.PodIP),
|
||||
na(po.Spec.NodeName),
|
||||
p.mapQOS(po.Status.QOSClass),
|
||||
mapToStr(po.Labels),
|
||||
asStatus(p.diagnose(phase, cr, len(ss))),
|
||||
toAge(po.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
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...
|
||||
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@ type (
|
|||
|
||||
func TestPodColorer(t *testing.T) {
|
||||
var (
|
||||
nsRow = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "Running"}}
|
||||
toastNS = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "Boom"}}
|
||||
notReadyNS = render.Row{Fields: render.Fields{"blee", "fred", "0/1", "Boom"}}
|
||||
row = render.Row{Fields: render.Fields{"fred", "1/1", "Running"}}
|
||||
toast = render.Row{Fields: render.Fields{"fred", "1/1", "Boom"}}
|
||||
notReady = render.Row{Fields: render.Fields{"fred", "0/1", "Boom"}}
|
||||
nsRow = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "0", "Running"}}
|
||||
toastNS = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "0", "Boom"}}
|
||||
notReadyNS = render.Row{Fields: render.Fields{"blee", "fred", "0/1", "0", "Boom"}}
|
||||
row = render.Row{Fields: render.Fields{"fred", "1/1", "0", "Running"}}
|
||||
toast = render.Row{Fields: render.Fields{"fred", "1/1", "0", "Boom"}}
|
||||
notReady = render.Row{Fields: render.Fields{"fred", "0/1", "0", "Boom"}}
|
||||
)
|
||||
|
||||
uu := colorerUCs{
|
||||
|
|
@ -70,7 +70,7 @@ func TestPodRender(t *testing.T) {
|
|||
assert.Nil(t, err)
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ func TestPodInitRender(t *testing.T) {
|
|||
assert.Nil(t, err)
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ func rbacVerbHeader() HeaderRow {
|
|||
Header{Name: "PATCH "},
|
||||
Header{Name: "UPDATE"},
|
||||
Header{Name: "DELETE"},
|
||||
Header{Name: "DLIST "},
|
||||
Header{Name: "EXTRAS"},
|
||||
Header{Name: "DEL-LIST "},
|
||||
Header{Name: "EXTRAS", Wide: true},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +41,10 @@ func (Policy) Header(ns string) HeaderRow {
|
|||
Header{Name: "API GROUP"},
|
||||
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.
|
||||
|
|
@ -52,8 +55,14 @@ func (Policy) Render(o interface{}, gvr string, r *Row) error {
|
|||
}
|
||||
|
||||
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, "")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,5 +37,6 @@ func TestPolicyRender(t *testing.T) {
|
|||
"[orangered::b] 𐄂 [::]",
|
||||
"[orangered::b] 𐄂 [::]",
|
||||
"",
|
||||
"",
|
||||
}, r.Fields)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ func TestPortForwardRender(t *testing.T) {
|
|||
"http://0.0.0.0:p1/",
|
||||
"1",
|
||||
"1",
|
||||
"",
|
||||
"2m",
|
||||
}, r.Fields)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ func (PortForward) Header(ns string) HeaderRow {
|
|||
Header{Name: "URL"},
|
||||
Header{Name: "C"},
|
||||
Header{Name: "N"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
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]),
|
||||
asNum(pf.Config.C),
|
||||
asNum(pf.Config.N),
|
||||
"",
|
||||
pf.Age(),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,26 +16,26 @@ import (
|
|||
type PersistentVolume struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (PersistentVolume) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
func (p PersistentVolume) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
status := strings.TrimSpace(r.Row.Fields[4])
|
||||
switch status {
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
switch strings.TrimSpace(re.Row.Fields[4]) {
|
||||
case "Bound":
|
||||
c = StdColor
|
||||
case "Available":
|
||||
c = tcell.ColorYellow
|
||||
default:
|
||||
c = ErrColor
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Header returns a header rbw.
|
||||
|
|
@ -49,6 +49,9 @@ func (PersistentVolume) Header(string) HeaderRow {
|
|||
Header{Name: "CLAIM"},
|
||||
Header{Name: "STORAGECLASS"},
|
||||
Header{Name: "REASON"},
|
||||
Header{Name: "VOLUMEMODE", Wide: true},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
|
@ -90,12 +93,30 @@ func (p PersistentVolume) Render(o interface{}, ns string, r *Row) error {
|
|||
claim,
|
||||
class,
|
||||
pv.Status.Reason,
|
||||
p.volumeMode(pv.Spec.VolumeMode),
|
||||
mapToStr(pv.Labels),
|
||||
asStatus(p.diagnose(string(phase))),
|
||||
toAge(pv.ObjectMeta.CreationTimestamp),
|
||||
}
|
||||
|
||||
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...
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package render
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/gdamore/tcell"
|
||||
|
|
@ -15,19 +14,14 @@ import (
|
|||
type PersistentVolumeClaim struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (PersistentVolumeClaim) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
func (p PersistentVolumeClaim) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
markCol := 2
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
markCol--
|
||||
}
|
||||
if strings.TrimSpace(r.Row.Fields[markCol]) != "Bound" {
|
||||
c = ErrColor
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return c
|
||||
|
|
@ -49,6 +43,8 @@ func (PersistentVolumeClaim) Header(ns string) HeaderRow {
|
|||
Header{Name: "CAPACITY"},
|
||||
Header{Name: "ACCESS MODES"},
|
||||
Header{Name: "STORAGECLASS"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -96,8 +92,17 @@ func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *Row) error {
|
|||
capacity,
|
||||
accessModes,
|
||||
class,
|
||||
mapToStr(pvc.Labels),
|
||||
asStatus(p.diagnose(string(phase))),
|
||||
toAge(pvc.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
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"},
|
||||
}
|
||||
|
||||
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.
|
||||
|
|
@ -57,8 +60,12 @@ func (Rbac) Render(o interface{}, gvr string, r *Row) error {
|
|||
}
|
||||
|
||||
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, "")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ func (Role) Header(ns string) HeaderRow {
|
|||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
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,
|
||||
ro.Name,
|
||||
mapToStr(ro.Labels),
|
||||
"",
|
||||
toAge(ro.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ func (RoleBinding) Header(ns string) HeaderRow {
|
|||
Header{Name: "ROLE"},
|
||||
Header{Name: "KIND"},
|
||||
Header{Name: "SUBJECTS"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -58,6 +60,8 @@ func (r RoleBinding) Render(o interface{}, ns string, row *Row) error {
|
|||
rb.RoleRef.Name,
|
||||
kind,
|
||||
ss,
|
||||
mapToStr(rb.Labels),
|
||||
"",
|
||||
toAge(rb.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
|
|
@ -87,11 +91,11 @@ func toSubjectAlias(s string) string {
|
|||
|
||||
switch s {
|
||||
case rbacv1.UserKind:
|
||||
return "USR"
|
||||
return "User"
|
||||
case rbacv1.GroupKind:
|
||||
return "GRP"
|
||||
return "Group"
|
||||
case rbacv1.ServiceAccountKind:
|
||||
return "SA"
|
||||
return "SvcAcct"
|
||||
default:
|
||||
return strings.ToUpper(s)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestRoleBindingRender(t *testing.T) {
|
|||
c.Render(load(t, "rb"), "", &r)
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ func (rr RowEvents) Sort(ns string, col int, asc bool) {
|
|||
func toAgeDuration(dur string) string {
|
||||
d, err := time.ParseDuration(dur)
|
||||
if err != nil {
|
||||
return "n/a"
|
||||
return dur
|
||||
}
|
||||
return duration.HumanDuration(d)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ type Header struct {
|
|||
Name string
|
||||
Align int
|
||||
Decorator DecoratorFunc
|
||||
Hide bool
|
||||
Wide bool
|
||||
}
|
||||
|
||||
// Clone copies a header.
|
||||
|
|
@ -56,13 +58,7 @@ func (hh HeaderRow) Columns() []string {
|
|||
|
||||
// HasAge returns true if table has an age column.
|
||||
func (hh HeaderRow) HasAge() bool {
|
||||
for _, r := range hh {
|
||||
if r.Name == ageCol {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return hh.IndexOf(ageCol) != -1
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// ValidColIndex returns the valid col index or -1 if none.
|
||||
func (hh HeaderRow) ValidColIndex() int {
|
||||
return hh.IndexOf("VALID")
|
||||
}
|
||||
|
||||
// IndexOf 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 (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
|
|
@ -17,24 +16,19 @@ import (
|
|||
type ReplicaSet struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (ReplicaSet) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
func (r ReplicaSet) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
markCol := 2
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
markCol--
|
||||
}
|
||||
if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) {
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return StdColor
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
|
|
@ -49,12 +43,14 @@ func (ReplicaSet) Header(ns string) HeaderRow {
|
|||
Header{Name: "DESIRED", Align: tview.AlignRight},
|
||||
Header{Name: "CURRENT", Align: tview.AlignRight},
|
||||
Header{Name: "READY", Align: tview.AlignRight},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
func (s ReplicaSet) Render(o interface{}, ns string, r *Row) error {
|
||||
func (r ReplicaSet) Render(o interface{}, ns string, row *Row) error {
|
||||
raw, ok := o.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return fmt.Errorf("Expected ReplicaSet, but got %T", o)
|
||||
|
|
@ -65,18 +61,31 @@ func (s ReplicaSet) Render(o interface{}, ns string, r *Row) error {
|
|||
return err
|
||||
}
|
||||
|
||||
r.ID = client.MetaFQN(rs.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(s.Header(ns)))
|
||||
row.ID = client.MetaFQN(rs.ObjectMeta)
|
||||
row.Fields = make(Fields, 0, len(r.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, rs.Namespace)
|
||||
row.Fields = append(row.Fields, rs.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
row.Fields = append(row.Fields,
|
||||
rs.Name,
|
||||
strconv.Itoa(int(*rs.Spec.Replicas)),
|
||||
strconv.Itoa(int(rs.Status.Replicas)),
|
||||
strconv.Itoa(int(rs.Status.ReadyReplicas)),
|
||||
mapToStr(rs.Labels),
|
||||
asStatus(r.diagnose(rs)),
|
||||
toAge(rs.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (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,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "SECRET"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
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,
|
||||
sa.Name,
|
||||
strconv.Itoa(len(sa.Secrets)),
|
||||
mapToStr(sa.Labels),
|
||||
"",
|
||||
toAge(sa.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ func (StorageClass) Header(ns string) HeaderRow {
|
|||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "PROVISIONER"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +44,8 @@ func (StorageClass) Render(o interface{}, ns string, r *Row) error {
|
|||
r.Fields = Fields{
|
||||
sc.Name,
|
||||
string(sc.Provisioner),
|
||||
mapToStr(sc.Labels),
|
||||
"",
|
||||
toAge(sc.ObjectMeta.CreationTimestamp),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ var AgeDecorator = func(a string) string {
|
|||
func (ScreenDump) Header(ns string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "DIR"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
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.Fields = Fields{
|
||||
f.File.Name(),
|
||||
f.Dir,
|
||||
"",
|
||||
timeToAge(f.File.ModTime()),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ func TestScreenDumpRender(t *testing.T) {
|
|||
assert.Equal(t, "fred/blee/bob", r.ID)
|
||||
assert.Equal(t, render.Fields{
|
||||
"bob",
|
||||
"fred/blee",
|
||||
"",
|
||||
}, r.Fields[:len(r.Fields)-1])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package render
|
|||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/gdamore/tcell"
|
||||
|
|
@ -16,20 +15,13 @@ import (
|
|||
type StatefulSet struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (StatefulSet) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
func (s StatefulSet) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
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 {
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
|
|
@ -47,8 +39,12 @@ func (StatefulSet) Header(ns string) HeaderRow {
|
|||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "READY"},
|
||||
Header{Name: "SELECTOR"},
|
||||
Header{Name: "SELECTOR", Wide: true},
|
||||
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},
|
||||
)
|
||||
}
|
||||
|
|
@ -72,11 +68,22 @@ func (s StatefulSet) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
r.Fields = append(r.Fields,
|
||||
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),
|
||||
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),
|
||||
)
|
||||
|
||||
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.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.
|
||||
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.
|
||||
func (Subject) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
|
|
@ -24,6 +29,7 @@ func (Subject) Header(ns string) HeaderRow {
|
|||
Header{Name: "NAME"},
|
||||
Header{Name: "KIND"},
|
||||
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.Kind,
|
||||
res.FirstLocation,
|
||||
"",
|
||||
)
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -32,8 +32,10 @@ func (Service) Header(ns string) HeaderRow {
|
|||
Header{Name: "TYPE"},
|
||||
Header{Name: "CLUSTER-IP"},
|
||||
Header{Name: "EXTERNAL-IP"},
|
||||
Header{Name: "SELECTOR"},
|
||||
Header{Name: "PORTS"},
|
||||
Header{Name: "SELECTOR", Wide: true},
|
||||
Header{Name: "PORTS", Wide: true},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
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,
|
||||
svc.ObjectMeta.Name,
|
||||
string(svc.Spec.Type),
|
||||
svc.Spec.ClusterIP,
|
||||
toIP(svc.Spec.ClusterIP),
|
||||
toIPs(svc.Spec.Type, getSvcExtIPS(&svc)),
|
||||
mapToStr(svc.Spec.Selector),
|
||||
toPorts(svc.Spec.Ports),
|
||||
mapToStr(svc.Labels),
|
||||
asStatus(s.diagnose()),
|
||||
toAge(svc.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Service) diagnose() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func toIP(ip string) string {
|
||||
if ip == "" || ip == "None" {
|
||||
return ""
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
func getSvcExtIPS(svc *v1.Service) []string {
|
||||
results := []string{}
|
||||
|
||||
|
|
@ -116,7 +131,7 @@ func toIPs(svcType v1.ServiceType, ips []string) string {
|
|||
if svcType == v1.ServiceTypeLoadBalancer {
|
||||
return "<pending>"
|
||||
}
|
||||
return MissingValue
|
||||
return ""
|
||||
}
|
||||
sort.Strings(ips)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestServiceRender(t *testing.T) {
|
|||
c.Render(load(t, "svc"), "", &r)
|
||||
|
||||
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,112 @@
|
|||
package tchart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var dots = []rune{' ', '⠂', '▤', '▥'}
|
||||
|
||||
// Segment represents a dial segment.
|
||||
type Segment []int
|
||||
|
||||
// Segments represents a collection of segments.
|
||||
type Segments []Segment
|
||||
|
||||
// Matrix represents a number dial.
|
||||
type Matrix [][]rune
|
||||
|
||||
// Orientation tracks char orientations.
|
||||
type Orientation int
|
||||
|
||||
// DotMatrix tracks a char matrix.
|
||||
type DotMatrix struct {
|
||||
row, col int
|
||||
}
|
||||
|
||||
// NewDotMatrix returns a new matrix.
|
||||
func NewDotMatrix(row, col int) DotMatrix {
|
||||
return DotMatrix{
|
||||
row: row,
|
||||
col: col,
|
||||
}
|
||||
}
|
||||
|
||||
// Print prints the matrix.
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// CharFor return a char based on row/col.
|
||||
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: {{1, 0}, {0}, {2, 0}},
|
||||
1: {{1}, nil, {2}},
|
||||
2: {{1, 3}, {3}, {2, 3}},
|
||||
3: {{4}, nil, {5}},
|
||||
4: {{4, 6}, {6}, {5, 6}},
|
||||
}
|
||||
|
||||
// ToSegments return path segments.
|
||||
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,159 @@
|
|||
package tchart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
const (
|
||||
// DeltaSame represents no difference.
|
||||
DeltaSame delta = iota
|
||||
|
||||
// DeltaMore represents a higher value.
|
||||
DeltaMore
|
||||
|
||||
// DeltaLess represents a lower value.
|
||||
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
|
||||
}
|
||||
|
||||
// Add adds a new metric.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw draws the primitive.
|
||||
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
|
||||
}
|
||||
|
||||
// MaxDigits 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 (
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
|
@ -14,6 +15,7 @@ type App struct {
|
|||
Configurator
|
||||
|
||||
Main *Pages
|
||||
flash *model.Flash
|
||||
actions KeyActions
|
||||
views map[string]tview.Primitive
|
||||
cmdBuff *CmdBuff
|
||||
|
|
@ -25,6 +27,7 @@ func NewApp(context string) *App {
|
|||
Application: tview.NewApplication(),
|
||||
actions: make(KeyActions),
|
||||
Main: NewPages(),
|
||||
flash: model.NewFlash(model.DefaultFlashDelay),
|
||||
cmdBuff: NewCmdBuff(':', CommandBuff),
|
||||
}
|
||||
a.ReloadStyles(context)
|
||||
|
|
@ -33,7 +36,6 @@ func NewApp(context string) *App {
|
|||
"menu": NewMenu(a.Styles),
|
||||
"logo": NewLogo(a.Styles),
|
||||
"cmd": NewCommand(a.Styles),
|
||||
"flash": NewFlash(&a, "Initializing..."),
|
||||
"crumbs": NewCrumbs(a.Styles),
|
||||
}
|
||||
|
||||
|
|
@ -239,11 +241,6 @@ func (a *App) 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.
|
||||
func (a *App) Cmd() *Command {
|
||||
return a.views["cmd"].(*Command)
|
||||
|
|
@ -254,6 +251,11 @@ func (a *App) Menu() *Menu {
|
|||
return a.views["menu"].(*Menu)
|
||||
}
|
||||
|
||||
// Flash returns a flash model.
|
||||
func (a *App) Flash() *model.Flash {
|
||||
return a.flash
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ func TestAppViews(t *testing.T) {
|
|||
a := ui.NewApp("")
|
||||
a.Init()
|
||||
|
||||
vv := []string{"crumbs", "logo", "cmd", "flash", "menu"}
|
||||
vv := []string{"crumbs", "logo", "cmd", "menu"}
|
||||
for i := range vv {
|
||||
v := vv[i]
|
||||
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.Flash())
|
||||
assert.NotNil(t, a.Logo())
|
||||
assert.NotNil(t, a.Cmd())
|
||||
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))
|
||||
if c.Styles == nil {
|
||||
c.Styles = config.NewStyles()
|
||||
} else {
|
||||
c.Styles.Reset()
|
||||
}
|
||||
if err := c.Styles.Load(clusterSkins); err != nil {
|
||||
log.Info().Msgf("No context specific skin file found -- %s", clusterSkins)
|
||||
|
|
@ -102,10 +104,10 @@ func (c *Configurator) updateStyles(f string) {
|
|||
}
|
||||
c.Styles.Update()
|
||||
|
||||
render.StdColor = config.AsColor(c.Styles.Frame().Status.NewColor)
|
||||
render.AddColor = config.AsColor(c.Styles.Frame().Status.AddColor)
|
||||
render.ModColor = config.AsColor(c.Styles.Frame().Status.ModifyColor)
|
||||
render.ErrColor = config.AsColor(c.Styles.Frame().Status.ErrorColor)
|
||||
render.HighlightColor = config.AsColor(c.Styles.Frame().Status.HighlightColor)
|
||||
render.CompletedColor = config.AsColor(c.Styles.Frame().Status.CompletedColor)
|
||||
render.StdColor = c.Styles.Frame().Status.NewColor.Color()
|
||||
render.AddColor = c.Styles.Frame().Status.AddColor.Color()
|
||||
render.ModColor = c.Styles.Frame().Status.ModifyColor.Color()
|
||||
render.ErrColor = c.Styles.Frame().Status.ErrorColor.Color()
|
||||
render.HighlightColor = c.Styles.Frame().Status.HighlightColor.Color()
|
||||
render.CompletedColor = c.Styles.Frame().Status.CompletedColor.Color()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,67 +2,45 @@ package ui
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// FlashInfo represents an info message.
|
||||
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
|
||||
|
||||
emoHappy = "😎"
|
||||
emoDoh = "😗"
|
||||
emoRed = "😡"
|
||||
emoDead = "💀"
|
||||
emoHappy = "😎"
|
||||
)
|
||||
|
||||
type (
|
||||
// FlashLevel represents flash message severity.
|
||||
FlashLevel int
|
||||
// Flash represents a flash message indicator.
|
||||
type Flash struct {
|
||||
*tview.TextView
|
||||
|
||||
// Flash represents a flash message indicator.
|
||||
Flash struct {
|
||||
*tview.TextView
|
||||
|
||||
cancel context.CancelFunc
|
||||
app *App
|
||||
flushNow bool
|
||||
}
|
||||
)
|
||||
app *App
|
||||
testMode bool
|
||||
}
|
||||
|
||||
// NewFlash returns a new flash view.
|
||||
func NewFlash(app *App, m string) *Flash {
|
||||
func NewFlash(app *App) *Flash {
|
||||
f := Flash{
|
||||
app: app,
|
||||
TextView: tview.NewTextView(),
|
||||
}
|
||||
f.SetTextColor(tcell.ColorAqua)
|
||||
f.SetTextAlign(tview.AlignLeft)
|
||||
f.SetTextAlign(tview.AlignCenter)
|
||||
f.SetBorderPadding(0, 0, 1, 1)
|
||||
f.SetText(m)
|
||||
f.app.Styles.AddListener(&f)
|
||||
|
||||
return &f
|
||||
}
|
||||
|
||||
// TestMode for testing...
|
||||
func (f *Flash) TestMode() {
|
||||
f.flushNow = true
|
||||
// SetTestMode for testing ONLY!
|
||||
func (f *Flash) SetTestMode(b bool) {
|
||||
f.testMode = b
|
||||
}
|
||||
|
||||
// StylesChanged notifies listener the skin changed.
|
||||
|
|
@ -71,101 +49,54 @@ func (f *Flash) StylesChanged(s *config.Styles) {
|
|||
f.SetTextColor(s.FgColor())
|
||||
}
|
||||
|
||||
// Info displays an info flash message.
|
||||
func (f *Flash) Info(msg string) {
|
||||
log.Info().Msg(msg)
|
||||
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
|
||||
// Watch watches for flash changes.
|
||||
func (f *Flash) Watch(ctx context.Context, c model.FlashChan) {
|
||||
defer log.Debug().Msgf("Flash Canceled!")
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case msg := <-c:
|
||||
f.SetMessage(msg)
|
||||
}
|
||||
}
|
||||
log.Error().Err(err).Msgf(fmat, args...)
|
||||
f.SetMessage(FlashErr, fmt.Sprintf(fmat, args...))
|
||||
}
|
||||
|
||||
// SetMessage sets flash message and level.
|
||||
func (f *Flash) SetMessage(level FlashLevel, msg ...string) {
|
||||
if f.cancel != nil {
|
||||
f.cancel()
|
||||
func (f *Flash) SetMessage(m model.LevelMessage) {
|
||||
fn := func() {
|
||||
if m.Text == "" {
|
||||
f.Clear()
|
||||
return
|
||||
}
|
||||
f.SetTextColor(flashColor(m.Level))
|
||||
f.SetText(flashEmoji(m.Level) + " " + m.Text)
|
||||
}
|
||||
|
||||
_, _, width, _ := f.GetRect()
|
||||
if width <= 15 {
|
||||
width = 100
|
||||
}
|
||||
m := strings.Join(msg, " ")
|
||||
if f.flushNow {
|
||||
f.SetTextColor(flashColor(level))
|
||||
f.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3))
|
||||
if f.testMode {
|
||||
fn()
|
||||
} else {
|
||||
f.app.QueueUpdateDraw(func() {
|
||||
f.SetTextColor(flashColor(level))
|
||||
f.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3))
|
||||
})
|
||||
f.app.QueueUpdateDraw(fn)
|
||||
}
|
||||
|
||||
var ctx context.Context
|
||||
ctx, f.cancel = context.WithTimeout(context.Background(), flashDelay)
|
||||
go f.refresh(ctx)
|
||||
}
|
||||
|
||||
func (f *Flash) refresh(ctx context.Context) {
|
||||
<-ctx.Done()
|
||||
f.app.QueueUpdateDraw(func() {
|
||||
f.Clear()
|
||||
})
|
||||
}
|
||||
|
||||
func flashEmoji(l FlashLevel) string {
|
||||
func flashEmoji(l model.FlashLevel) string {
|
||||
switch l {
|
||||
case FlashWarn:
|
||||
case model.FlashWarn:
|
||||
return emoDoh
|
||||
case FlashErr:
|
||||
case model.FlashErr:
|
||||
return emoRed
|
||||
case FlashFatal:
|
||||
return emoDead
|
||||
default:
|
||||
return emoHappy
|
||||
}
|
||||
}
|
||||
|
||||
func flashColor(l FlashLevel) tcell.Color {
|
||||
func flashColor(l model.FlashLevel) tcell.Color {
|
||||
switch l {
|
||||
case FlashWarn:
|
||||
case model.FlashWarn:
|
||||
return tcell.ColorOrange
|
||||
case FlashErr:
|
||||
case model.FlashErr:
|
||||
return tcell.ColorOrangeRed
|
||||
case FlashFatal:
|
||||
return tcell.ColorFuchsia
|
||||
default:
|
||||
return tcell.ColorNavajoWhite
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,40 @@
|
|||
package ui_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFlashInfo(t *testing.T) {
|
||||
f := newFlash()
|
||||
f.Info("Blee")
|
||||
func TestFlash(t *testing.T) {
|
||||
const delay = 1 * time.Millisecond
|
||||
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))
|
||||
f.Infof("Blee %s", "duh")
|
||||
assert.Equal(t, "😎 Blee duh\n", f.GetText(false))
|
||||
}
|
||||
|
||||
func TestFlashWarn(t *testing.T) {
|
||||
f := newFlash()
|
||||
f.Warn("Blee")
|
||||
|
||||
assert.Equal(t, "😗 Blee\n", f.GetText(false))
|
||||
f.Warnf("Blee %s", "duh")
|
||||
assert.Equal(t, "😗 Blee duh\n", 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
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
a := ui.NewApp("test")
|
||||
f := ui.NewFlash(a)
|
||||
f.SetTestMode(true)
|
||||
go f.Watch(ctx, a.Flash().Channel())
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
a.Flash().SetMessage(u.l, u.i)
|
||||
time.Sleep(delay)
|
||||
assert.Equal(t, u.e, f.GetText(false))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue