add port-forwards + some nav improv + bugz kill
parent
247cc634be
commit
8b6898dea7
|
|
@ -315,6 +315,10 @@ k9s:
|
||||||
keyColor: steelblue
|
keyColor: steelblue
|
||||||
colonColor: blue
|
colonColor: blue
|
||||||
valueColor: royalblue
|
valueColor: royalblue
|
||||||
|
# Logs styles.
|
||||||
|
yaml:
|
||||||
|
fgColor: white
|
||||||
|
bgColor: black
|
||||||
```
|
```
|
||||||
|
|
||||||
Available color names are defined below:
|
Available color names are defined below:
|
||||||
|
|
|
||||||
3
go.mod
3
go.mod
|
|
@ -3,6 +3,7 @@ module github.com/derailed/k9s
|
||||||
go 1.12
|
go 1.12
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
|
github.com/derailed/tview => /Users/fernand/go_wk/derailed/src/github.com/derailed/tview
|
||||||
k8s.io/api => k8s.io/api v0.0.0-20190222213804-5cb15d344471
|
k8s.io/api => k8s.io/api v0.0.0-20190222213804-5cb15d344471
|
||||||
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.0.0-20190325193600-475668423e9f
|
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.0.0-20190325193600-475668423e9f
|
||||||
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628
|
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628
|
||||||
|
|
@ -15,6 +16,7 @@ replace (
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-autorest/autorest v0.1.0 // indirect
|
github.com/Azure/go-autorest/autorest v0.1.0 // indirect
|
||||||
github.com/derailed/tview v0.1.6
|
github.com/derailed/tview v0.1.6
|
||||||
|
github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect
|
||||||
github.com/evanphx/json-patch v4.1.0+incompatible // indirect
|
github.com/evanphx/json-patch v4.1.0+incompatible // indirect
|
||||||
github.com/fatih/camelcase v1.0.0 // indirect
|
github.com/fatih/camelcase v1.0.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.4.7
|
github.com/fsnotify/fsnotify v1.4.7
|
||||||
|
|
@ -35,6 +37,7 @@ require (
|
||||||
github.com/onsi/gomega v1.5.0 // indirect
|
github.com/onsi/gomega v1.5.0 // indirect
|
||||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||||
github.com/petergtz/pegomock v0.0.0-20181206220228-b113d17a7e81
|
github.com/petergtz/pegomock v0.0.0-20181206220228-b113d17a7e81
|
||||||
|
github.com/rakyll/hey v0.1.2
|
||||||
github.com/rs/zerolog v1.14.3
|
github.com/rs/zerolog v1.14.3
|
||||||
github.com/spf13/cobra v0.0.3
|
github.com/spf13/cobra v0.0.3
|
||||||
github.com/spf13/pflag v1.0.3 // indirect
|
github.com/spf13/pflag v1.0.3 // indirect
|
||||||
|
|
|
||||||
5
go.sum
5
go.sum
|
|
@ -51,6 +51,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c h1:ZfSZ3P3BedhKGUhzj7BQlPSU4OvT6tfOKe3DVHzOA7s=
|
||||||
|
github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||||
|
|
@ -211,6 +213,8 @@ github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
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/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
|
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
|
||||||
github.com/rivo/tview v0.0.0-20190213202703-b373355e9db4/go.mod h1:J4W+hErFfITUbyFAEXizpmkuxX7ZN56dopxHB4XQhMw=
|
github.com/rivo/tview v0.0.0-20190213202703-b373355e9db4/go.mod h1:J4W+hErFfITUbyFAEXizpmkuxX7ZN56dopxHB4XQhMw=
|
||||||
|
|
@ -267,6 +271,7 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73r
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ var (
|
||||||
// K9sConfigFile represents K9s config file location.
|
// K9sConfigFile represents K9s config file location.
|
||||||
K9sConfigFile = filepath.Join(K9sHome, "config.yml")
|
K9sConfigFile = filepath.Join(K9sHome, "config.yml")
|
||||||
// K9sLogs represents K9s log.
|
// K9sLogs represents K9s log.
|
||||||
K9sLogs = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-%s.log", mustK9sUser()))
|
K9sLogs = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-%s.log", MustK9sUser()))
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,8 @@ func mustK9sHome() string {
|
||||||
return usr.HomeDir
|
return usr.HomeDir
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustK9sUser() string {
|
// MustK9sUser establishes current user identity or fail.
|
||||||
|
func MustK9sUser() string {
|
||||||
usr, err := user.Current()
|
usr, err := user.Current()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
@ -57,7 +58,7 @@ func mustK9sUser() string {
|
||||||
func EnsurePath(path string, mod os.FileMode) {
|
func EnsurePath(path string, mod os.FileMode) {
|
||||||
dir := filepath.Dir(path)
|
dir := filepath.Dir(path)
|
||||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
if err = os.Mkdir(dir, mod); err != nil {
|
if err = os.MkdirAll(dir, mod); err != nil {
|
||||||
log.Error().Msgf("Unable to create K9s home config dir: %v", err)
|
log.Error().Msgf("Unable to create K9s home config dir: %v", err)
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ type (
|
||||||
Status *Status `yaml:"status"`
|
Status *Status `yaml:"status"`
|
||||||
Title *Title `yaml:"title"`
|
Title *Title `yaml:"title"`
|
||||||
Yaml *Yaml `yaml:"yaml"`
|
Yaml *Yaml `yaml:"yaml"`
|
||||||
|
Log *Log `yaml:"logs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status tracks resource status styles.
|
// Status tracks resource status styles.
|
||||||
|
|
@ -47,6 +48,12 @@ type (
|
||||||
CompletedColor string `yaml:"completedColor"`
|
CompletedColor string `yaml:"completedColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log tracks Log styles.
|
||||||
|
Log struct {
|
||||||
|
FgColor string `yaml:"fgColor"`
|
||||||
|
BgColor string `yaml:"bgColor"`
|
||||||
|
}
|
||||||
|
|
||||||
// Yaml tracks yaml styles.
|
// Yaml tracks yaml styles.
|
||||||
Yaml struct {
|
Yaml struct {
|
||||||
KeyColor string `yaml:"keyColor"`
|
KeyColor string `yaml:"keyColor"`
|
||||||
|
|
@ -118,6 +125,7 @@ func newStyle() *Style {
|
||||||
Status: newStatus(),
|
Status: newStatus(),
|
||||||
Title: newTitle(),
|
Title: newTitle(),
|
||||||
Yaml: newYaml(),
|
Yaml: newYaml(),
|
||||||
|
Log: newLog(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,6 +141,14 @@ func newStatus() *Status {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewLog returns a new log style.
|
||||||
|
func newLog() *Log {
|
||||||
|
return &Log{
|
||||||
|
FgColor: "lightskyblue",
|
||||||
|
BgColor: "black",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewYaml returns a new yaml style.
|
// NewYaml returns a new yaml style.
|
||||||
func newYaml() *Yaml {
|
func newYaml() *Yaml {
|
||||||
return &Yaml{
|
return &Yaml{
|
||||||
|
|
@ -251,6 +267,10 @@ func (s *Styles) ensure() {
|
||||||
if s.Style.Yaml == nil {
|
if s.Style.Yaml == nil {
|
||||||
s.Style.Yaml = newYaml()
|
s.Style.Yaml = newYaml()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.Style.Log == nil {
|
||||||
|
s.Style.Log = newLog()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FgColor returns the foreground color.
|
// FgColor returns the foreground color.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package k8s
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const megaByte = 1024 * 1024
|
const megaByte = 1024 * 1024
|
||||||
|
|
@ -17,3 +19,9 @@ func toPerc(v1, v2 float64) float64 {
|
||||||
}
|
}
|
||||||
return math.Round((v1 / v2) * 100)
|
return math.Round((v1 / v2) * 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func namespaced(n string) (string, string) {
|
||||||
|
ns, po := path.Split(n)
|
||||||
|
|
||||||
|
return strings.Trim(ns, "/"), po
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
"k8s.io/client-go/tools/portforward"
|
||||||
|
"k8s.io/client-go/transport/spdy"
|
||||||
|
"k8s.io/kubernetes/pkg/kubectl/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const localhost = "localhost"
|
||||||
|
|
||||||
|
// PortForward tracks a port forward stream.
|
||||||
|
type PortForward struct {
|
||||||
|
Connection
|
||||||
|
genericclioptions.IOStreams
|
||||||
|
|
||||||
|
stopChan, readyChan chan struct{}
|
||||||
|
logger *zerolog.Logger
|
||||||
|
active bool
|
||||||
|
path string
|
||||||
|
ports []string
|
||||||
|
age time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPortForward returns a new port forward streamer.
|
||||||
|
func NewPortForward(c Connection, l *zerolog.Logger) *PortForward {
|
||||||
|
return &PortForward{
|
||||||
|
Connection: c,
|
||||||
|
logger: l,
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
readyChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Age returns the port forward age.
|
||||||
|
func (p *PortForward) Age() string {
|
||||||
|
return time.Since(p.age).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active returns the forward status.
|
||||||
|
func (p *PortForward) Active() bool {
|
||||||
|
return p.active
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PortForward) SetActive(b bool) {
|
||||||
|
p.active = b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ports returns the forwarded ports mappings.
|
||||||
|
func (p *PortForward) Ports() []string {
|
||||||
|
return p.ports
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path returns the pod resource path.
|
||||||
|
func (p *PortForward) Path() string {
|
||||||
|
return p.path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop terminates a port forard
|
||||||
|
func (p *PortForward) Stop() {
|
||||||
|
p.logger.Debug().Msgf("<<< Stopping port forward %q %v", p.path, p.ports)
|
||||||
|
p.active = false
|
||||||
|
close(p.stopChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start initiates a port foward session for a given pod and ports.
|
||||||
|
func (p *PortForward) Start(path string, ports []string) (*portforward.PortForwarder, error) {
|
||||||
|
p.path, p.ports, p.age = path, ports, time.Now()
|
||||||
|
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
pod, err := p.DialOrDie().CoreV1().Pods(ns).Get(n, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pod.Status.Phase != v1.PodRunning {
|
||||||
|
return nil, fmt.Errorf("unable to forward port because pod is not running. Current status=%v", pod.Status.Phase)
|
||||||
|
}
|
||||||
|
|
||||||
|
rcfg := p.RestConfigOrDie()
|
||||||
|
rcfg.GroupVersion = &schema.GroupVersion{Group: "", Version: "v1"}
|
||||||
|
rcfg.APIPath = "/api"
|
||||||
|
codecs, _ := codecs()
|
||||||
|
rcfg.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: codecs}
|
||||||
|
clt, err := rest.RESTClientFor(rcfg)
|
||||||
|
if err != nil {
|
||||||
|
p.logger.Debug().Msgf("Boom! %#v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req := clt.Post().
|
||||||
|
Resource("pods").
|
||||||
|
Namespace(ns).
|
||||||
|
Name(n).
|
||||||
|
SubResource("portforward")
|
||||||
|
|
||||||
|
return p.forwardPorts("POST", req.URL(), ports)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PortForward) forwardPorts(method string, url *url.URL, ports []string) (*portforward.PortForwarder, error) {
|
||||||
|
cfg, err := p.Config().RESTConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
transport, upgrader, err := spdy.RoundTripperFor(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url)
|
||||||
|
addrs := []string{localhost}
|
||||||
|
return portforward.NewOnAddresses(dialer, addrs, ports, p.stopChan, p.readyChan, p.Out, p.ErrOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func codecs() (serializer.CodecFactory, runtime.ParameterCodec) {
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
gv := schema.GroupVersion{Group: "", Version: "v1"}
|
||||||
|
metav1.AddToGroupVersion(scheme, gv)
|
||||||
|
scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
|
||||||
|
scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
|
||||||
|
|
||||||
|
return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
func svcPortToTargetPort(ports []string, svc v1.Service, pod v1.Pod) ([]string, error) {
|
||||||
|
var translated []string
|
||||||
|
for _, port := range ports {
|
||||||
|
localPort, remotePort := splitPort(port)
|
||||||
|
portnum, err := strconv.Atoi(remotePort)
|
||||||
|
if err != nil {
|
||||||
|
svcPort, err := util.LookupServicePortNumberByName(svc, remotePort)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
portnum = int(svcPort)
|
||||||
|
if localPort == remotePort {
|
||||||
|
localPort = strconv.Itoa(portnum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
containerPort, err := util.LookupContainerPortNumberByServicePort(svc, pod, int32(portnum))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if int32(portnum) != containerPort {
|
||||||
|
port = fmt.Sprintf("%s:%d", localPort, containerPort)
|
||||||
|
}
|
||||||
|
translated = append(translated, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
return translated, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitPort(port string) (local, remote string) {
|
||||||
|
parts := strings.Split(port, ":")
|
||||||
|
if len(parts) == 2 {
|
||||||
|
return parts[0], parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts[0], parts[0]
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -158,6 +159,7 @@ func (*Container) Header(ns string) Row {
|
||||||
"MEM",
|
"MEM",
|
||||||
"%CPU",
|
"%CPU",
|
||||||
"%MEM",
|
"%MEM",
|
||||||
|
"PORTS",
|
||||||
"AGE",
|
"AGE",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -223,6 +225,7 @@ func (r *Container) Fields(ns string) Row {
|
||||||
smem,
|
smem,
|
||||||
pcpu,
|
pcpu,
|
||||||
pmem,
|
pmem,
|
||||||
|
toStrPorts(i.Ports),
|
||||||
toAge(r.pod.CreationTimestamp),
|
toAge(r.pod.CreationTimestamp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -230,6 +233,21 @@ func (r *Container) Fields(ns string) Row {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
func toStrPorts(pp []v1.ContainerPort) string {
|
||||||
|
ports := make([]string, len(pp))
|
||||||
|
for i, p := range pp {
|
||||||
|
if len(p.Name) > 0 {
|
||||||
|
ports[i] = p.Name + ":"
|
||||||
|
}
|
||||||
|
ports[i] += strconv.Itoa(int(p.ContainerPort))
|
||||||
|
if p.Protocol != "TCP" {
|
||||||
|
ports[i] += "╱" + string(p.Protocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(ports, ",")
|
||||||
|
}
|
||||||
|
|
||||||
func toState(s v1.ContainerState) string {
|
func toState(s v1.ContainerState) string {
|
||||||
switch {
|
switch {
|
||||||
case s.Waiting != nil:
|
case s.Waiting != nil:
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,13 @@ const (
|
||||||
NAValue = "n/a"
|
NAValue = "n/a"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func fqn(ns, n string) string {
|
||||||
|
if ns == "" {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
return ns + "/" + n
|
||||||
|
}
|
||||||
|
|
||||||
func empty(s []string) bool {
|
func empty(s []string) bool {
|
||||||
for _, v := range s {
|
for _, v := range s {
|
||||||
if len(v) != 0 {
|
if len(v) != 0 {
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,7 @@ func metaFQN(m metav1.ObjectMeta) string {
|
||||||
return m.Name
|
return m.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.Namespace + "/" + m.Name
|
return fqn(m.Namespace, m.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *list) fetchFromStore(m *wa.Meta, ns string) (Columnars, error) {
|
func (l *list) fetchFromStore(m *wa.Meta, ns string) (Columnars, error) {
|
||||||
|
|
@ -239,7 +239,6 @@ func (l *list) fetchFromStore(m *wa.Meta, ns string) (Columnars, error) {
|
||||||
LabelSelector: l.resource.GetLabelSelector(),
|
LabelSelector: l.resource.GetLabelSelector(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Msgf(">>>>>> DOH! %#v", err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -238,8 +238,8 @@ func (r *Pod) Fields(ns string) Row {
|
||||||
cmem,
|
cmem,
|
||||||
pcpu,
|
pcpu,
|
||||||
pmem,
|
pmem,
|
||||||
i.Status.PodIP,
|
na(i.Status.PodIP),
|
||||||
i.Spec.NodeName,
|
na(i.Spec.NodeName),
|
||||||
r.mapQOS(i.Status.QOSClass),
|
r.mapQOS(i.Status.QOSClass),
|
||||||
toAge(i.ObjectMeta.CreationTimestamp),
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,8 @@ func (*Service) Header(ns string) Row {
|
||||||
"TYPE",
|
"TYPE",
|
||||||
"CLUSTER-IP",
|
"CLUSTER-IP",
|
||||||
"EXTERNAL-IP",
|
"EXTERNAL-IP",
|
||||||
"PORT(S)",
|
"SELECTOR",
|
||||||
|
"PORTS",
|
||||||
"AGE",
|
"AGE",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -99,6 +100,7 @@ func (r *Service) Fields(ns string) Row {
|
||||||
string(i.Spec.Type),
|
string(i.Spec.Type),
|
||||||
i.Spec.ClusterIP,
|
i.Spec.ClusterIP,
|
||||||
r.toIPs(i.Spec.Type, r.getSvcExtIPS(i)),
|
r.toIPs(i.Spec.Type, r.getSvcExtIPS(i)),
|
||||||
|
mapToStr(i.Spec.Selector),
|
||||||
r.toPorts(i.Spec.Ports),
|
r.toPorts(i.Spec.Ports),
|
||||||
toAge(i.ObjectMeta.CreationTimestamp),
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ func TestSvcFields(t *testing.T) {
|
||||||
"ClusterIP",
|
"ClusterIP",
|
||||||
"1.1.1.1",
|
"1.1.1.1",
|
||||||
"2.2.2.2",
|
"2.2.2.2",
|
||||||
|
"fred=blee",
|
||||||
"http:90►0",
|
"http:90►0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -98,7 +99,7 @@ func TestSVCListData(t *testing.T) {
|
||||||
assert.Equal(t, 1, len(td.Rows))
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
assert.Equal(t, "blee", l.GetNamespace())
|
assert.Equal(t, "blee", l.GetNamespace())
|
||||||
row := td.Rows["blee/fred"]
|
row := td.Rows["blee/fred"]
|
||||||
assert.Equal(t, 6, len(row.Deltas))
|
assert.Equal(t, 7, len(row.Deltas))
|
||||||
for _, d := range row.Deltas {
|
for _, d := range row.Deltas {
|
||||||
assert.Equal(t, "", d)
|
assert.Equal(t, "", d)
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +142,8 @@ func svcHeader() resource.Row {
|
||||||
"TYPE",
|
"TYPE",
|
||||||
"CLUSTER-IP",
|
"CLUSTER-IP",
|
||||||
"EXTERNAL-IP",
|
"EXTERNAL-IP",
|
||||||
"PORT(S)",
|
"SELECTOR",
|
||||||
|
"PORTS",
|
||||||
"AGE",
|
"AGE",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
aliasTitle = "Aliases"
|
aliasTitle = "Aliases"
|
||||||
aliasTitleFmt = " [aqua::b]%s[[aqua::b]%d[aqua::-]][aqua::-] "
|
aliasTitleFmt = " [aqua::b]%s([fuchsia::b]%d[fuchsia::-])[aqua::-] "
|
||||||
)
|
)
|
||||||
|
|
||||||
type aliasView struct {
|
type aliasView struct {
|
||||||
|
|
@ -24,6 +24,7 @@ type aliasView struct {
|
||||||
func newAliasView(app *appView) *aliasView {
|
func newAliasView(app *appView) *aliasView {
|
||||||
v := aliasView{tableView: newTableView(app, aliasTitle)}
|
v := aliasView{tableView: newTableView(app, aliasTitle)}
|
||||||
{
|
{
|
||||||
|
v.SetBorderFocusColor(tcell.ColorFuchsia)
|
||||||
v.SetSelectedStyle(tcell.ColorWhite, tcell.ColorFuchsia, tcell.AttrNone)
|
v.SetSelectedStyle(tcell.ColorWhite, tcell.ColorFuchsia, tcell.AttrNone)
|
||||||
v.colorerFn = aliasColorer
|
v.colorerFn = aliasColorer
|
||||||
v.current = app.content.GetPrimitive("main").(igniter)
|
v.current = app.content.GetPrimitive("main").(igniter)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
|
@ -13,6 +12,7 @@ import (
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/client-go/tools/portforward"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -23,6 +23,15 @@ const (
|
||||||
type (
|
type (
|
||||||
focusHandler func(tview.Primitive)
|
focusHandler func(tview.Primitive)
|
||||||
|
|
||||||
|
forwarder interface {
|
||||||
|
Start(path string, ports []string) (*portforward.PortForwarder, error)
|
||||||
|
Stop()
|
||||||
|
Path() string
|
||||||
|
Ports() []string
|
||||||
|
Active() bool
|
||||||
|
Age() string
|
||||||
|
}
|
||||||
|
|
||||||
igniter interface {
|
igniter interface {
|
||||||
tview.Primitive
|
tview.Primitive
|
||||||
|
|
||||||
|
|
@ -56,31 +65,38 @@ type (
|
||||||
content *tview.Pages
|
content *tview.Pages
|
||||||
flashView *flashView
|
flashView *flashView
|
||||||
crumbsView *crumbsView
|
crumbsView *crumbsView
|
||||||
|
logoView *logoView
|
||||||
menuView *menuView
|
menuView *menuView
|
||||||
clusterInfoView *clusterInfoView
|
clusterInfoView *clusterInfoView
|
||||||
|
cmdView *cmdView
|
||||||
command *command
|
command *command
|
||||||
focusGroup []tview.Primitive
|
focusGroup []tview.Primitive
|
||||||
focusCurrent int
|
focusCurrent int
|
||||||
focusChanged focusHandler
|
focusChanged focusHandler
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
cancelSkin context.CancelFunc
|
||||||
cmdBuff *cmdBuff
|
cmdBuff *cmdBuff
|
||||||
cmdView *cmdView
|
|
||||||
actions keyActions
|
actions keyActions
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
informer *watch.Meta
|
informer *watch.Meta
|
||||||
|
forwarders []forwarder
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewApp returns a K9s app instance.
|
// NewApp returns a K9s app instance.
|
||||||
func NewApp(cfg *config.Config) *appView {
|
func NewApp(cfg *config.Config) *appView {
|
||||||
v := appView{Application: tview.NewApplication(), config: cfg}
|
v := appView{
|
||||||
|
Application: tview.NewApplication(),
|
||||||
|
config: cfg,
|
||||||
|
pages: tview.NewPages(),
|
||||||
|
actions: make(keyActions),
|
||||||
|
content: tview.NewPages(),
|
||||||
|
cmdBuff: newCmdBuff(':'),
|
||||||
|
}
|
||||||
{
|
{
|
||||||
v.refreshStyles()
|
v.refreshStyles()
|
||||||
v.pages = tview.NewPages()
|
|
||||||
v.actions = make(keyActions)
|
|
||||||
v.menuView = newMenuView(&v)
|
v.menuView = newMenuView(&v)
|
||||||
v.content = tview.NewPages()
|
v.logoView = newLogoView(&v)
|
||||||
v.cmdBuff = newCmdBuff(':')
|
|
||||||
v.cmdView = newCmdView(&v, '🐶')
|
v.cmdView = newCmdView(&v, '🐶')
|
||||||
v.command = newCommand(&v)
|
v.command = newCommand(&v)
|
||||||
v.flashView = newFlashView(&v, "Initializing...")
|
v.flashView = newFlashView(&v, "Initializing...")
|
||||||
|
|
@ -116,7 +132,7 @@ func (a *appView) Init(v string, rate int, flags *genericclioptions.ConfigFlags)
|
||||||
header.SetDirection(tview.FlexColumn)
|
header.SetDirection(tview.FlexColumn)
|
||||||
header.AddItem(a.clusterInfoView, 35, 1, false)
|
header.AddItem(a.clusterInfoView, 35, 1, false)
|
||||||
header.AddItem(a.menuView, 0, 1, false)
|
header.AddItem(a.menuView, 0, 1, false)
|
||||||
header.AddItem(a.logoView(), 26, 1, false)
|
header.AddItem(a.logoView, 26, 1, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
main := tview.NewFlex()
|
main := tview.NewFlex()
|
||||||
|
|
@ -154,6 +170,18 @@ func (a *appView) bailOut() {
|
||||||
log.Debug().Msg("<<<< Stopping Watcher")
|
log.Debug().Msg("<<<< Stopping Watcher")
|
||||||
close(a.stopCh)
|
close(a.stopCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.cancel != nil {
|
||||||
|
a.cancel()
|
||||||
|
}
|
||||||
|
if a.cancelSkin != nil {
|
||||||
|
a.cancelSkin()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range a.forwarders {
|
||||||
|
f.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
a.Stop()
|
a.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,10 +189,10 @@ func (a *appView) conn() k8s.Connection {
|
||||||
return a.config.GetConnection()
|
return a.config.GetConnection()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *appView) stylesUpdater() (*fsnotify.Watcher, error) {
|
func (a *appView) stylesUpdater(ctx context.Context) error {
|
||||||
w, err := fsnotify.NewWatcher()
|
w, err := fsnotify.NewWatcher()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return w, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
|
@ -177,27 +205,26 @@ func (a *appView) stylesUpdater() (*fsnotify.Watcher, error) {
|
||||||
})
|
})
|
||||||
case err := <-w.Errors:
|
case err := <-w.Errors:
|
||||||
log.Info().Err(err).Msg("Skin watcher failed")
|
log.Info().Err(err).Msg("Skin watcher failed")
|
||||||
|
return
|
||||||
|
case <-ctx.Done():
|
||||||
|
w.Close()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := w.Add(config.K9sStylesFile); err != nil {
|
return w.Add(config.K9sStylesFile)
|
||||||
return w, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return w, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run starts the application loop
|
// Run starts the application loop
|
||||||
func (a *appView) Run() {
|
func (a *appView) Run() {
|
||||||
// Only enable updater while in dev mode.
|
// Only enable skin updater while in dev mode.
|
||||||
if a.version == devMode {
|
if a.version == devMode {
|
||||||
w, err := a.stylesUpdater()
|
var ctx context.Context
|
||||||
defer func() {
|
ctx, a.cancelSkin = context.WithCancel(context.Background())
|
||||||
if err != nil {
|
if err := a.stylesUpdater(ctx); err != nil {
|
||||||
w.Close()
|
log.Error().Err(err).Msg("Unable to track skin changes")
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
|
@ -213,6 +240,23 @@ func (a *appView) Run() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *appView) statusReset() {
|
||||||
|
a.logoView.reset()
|
||||||
|
a.Draw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *appView) status(l flashLevel, msg string) {
|
||||||
|
switch l {
|
||||||
|
case flashWarn:
|
||||||
|
a.logoView.warn(msg)
|
||||||
|
case flashInfo:
|
||||||
|
a.logoView.info(msg)
|
||||||
|
default:
|
||||||
|
a.logoView.reset()
|
||||||
|
}
|
||||||
|
a.Draw()
|
||||||
|
}
|
||||||
|
|
||||||
func (a *appView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
func (a *appView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
key := evt.Key()
|
key := evt.Key()
|
||||||
if key == tcell.KeyRune {
|
if key == tcell.KeyRune {
|
||||||
|
|
@ -287,7 +331,7 @@ func (a *appView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
if a.cmdView.inCmdMode() {
|
if a.cmdView.inCmdMode() {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
a.flash(flashInfo, "Command mode activated.")
|
a.flashView.info("Command mode activated.")
|
||||||
log.Debug().Msg("Entering command mode...")
|
log.Debug().Msg("Entering command mode...")
|
||||||
a.cmdBuff.setActive(true)
|
a.cmdBuff.setActive(true)
|
||||||
a.cmdBuff.clear()
|
a.cmdBuff.clear()
|
||||||
|
|
@ -298,7 +342,8 @@ func (a *appView) quitCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
if a.cmdMode() {
|
if a.cmdMode() {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
a.Stop()
|
a.bailOut()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,10 +359,20 @@ func (a *appView) aliasCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
if a.cmdView.inCmdMode() {
|
if a.cmdView.inCmdMode() {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
a.inject(newAliasView(a))
|
a.inject(newAliasView(a))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *appView) fwdCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if a.cmdView.inCmdMode() {
|
||||||
|
return evt
|
||||||
|
}
|
||||||
|
|
||||||
|
a.inject(newForwardView(a))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *appView) noopCmd(*tcell.EventKey) *tcell.EventKey {
|
func (a *appView) noopCmd(*tcell.EventKey) *tcell.EventKey {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -327,10 +382,14 @@ func (a *appView) puntCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *appView) gotoResource(res string, record bool) bool {
|
func (a *appView) gotoResource(res string, record bool) bool {
|
||||||
|
if a.cancel != nil {
|
||||||
|
a.cancel()
|
||||||
|
}
|
||||||
valid := a.command.run(res)
|
valid := a.command.run(res)
|
||||||
if valid && record {
|
if valid && record {
|
||||||
a.command.pushCmd(res)
|
a.command.pushCmd(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
return valid
|
return valid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -360,30 +419,14 @@ func (a *appView) cmdMode() bool {
|
||||||
return a.cmdView.inCmdMode()
|
return a.cmdView.inCmdMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *appView) flash(level flashLevel, m ...string) {
|
func (a *appView) flash() *flashView {
|
||||||
a.flashView.setMessage(level, m...)
|
return a.flashView
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *appView) setHints(h hints) {
|
func (a *appView) setHints(h hints) {
|
||||||
a.menuView.populateMenu(h)
|
a.menuView.populateMenu(h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *appView) logoView() tview.Primitive {
|
|
||||||
v := tview.NewTextView()
|
|
||||||
{
|
|
||||||
v.SetWordWrap(false)
|
|
||||||
v.SetWrap(false)
|
|
||||||
v.SetDynamicColors(true)
|
|
||||||
for i, s := range LogoSmall {
|
|
||||||
fmt.Fprintf(v, "[%s::b]%s", a.styles.Style.LogoColor, s)
|
|
||||||
if i+1 < len(LogoSmall) {
|
|
||||||
fmt.Fprintf(v, "\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *appView) fireFocusChanged(p tview.Primitive) {
|
func (a *appView) fireFocusChanged(p tview.Primitive) {
|
||||||
if a.focusChanged != nil {
|
if a.focusChanged != nil {
|
||||||
a.focusChanged(p)
|
a.focusChanged(p)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
Total: 3.3544 secs
|
||||||
|
Slowest: 0.1031 secs
|
||||||
|
Fastest: 0.0310 secs
|
||||||
|
Average: 0.0335 secs
|
||||||
|
Requests/sec: 29.8116
|
||||||
|
|
||||||
|
Total data: 61200 bytes
|
||||||
|
Size/request: 612 bytes
|
||||||
|
|
||||||
|
Response time histogram:
|
||||||
|
0.031 [1] |
|
||||||
|
0.038 [92] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
|
||||||
|
0.045 [6] |■■■
|
||||||
|
0.053 [0] |
|
||||||
|
0.060 [0] |
|
||||||
|
0.067 [0] |
|
||||||
|
0.074 [0] |
|
||||||
|
0.081 [0] |
|
||||||
|
0.089 [0] |
|
||||||
|
0.096 [0] |
|
||||||
|
0.103 [1] |
|
||||||
|
|
||||||
|
|
||||||
|
Latency distribution:
|
||||||
|
10% in 0.0314 secs
|
||||||
|
25% in 0.0317 secs
|
||||||
|
50% in 0.0320 secs
|
||||||
|
75% in 0.0327 secs
|
||||||
|
90% in 0.0369 secs
|
||||||
|
95% in 0.0394 secs
|
||||||
|
99% in 0.1031 secs
|
||||||
|
|
||||||
|
Details (average, fastest, slowest):
|
||||||
|
DNS+dialup: 0.0001 secs, 0.0310 secs, 0.1031 secs
|
||||||
|
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0049 secs
|
||||||
|
req write: 0.0000 secs, 0.0000 secs, 0.0001 secs
|
||||||
|
resp wait: 0.0330 secs, 0.0305 secs, 0.0973 secs
|
||||||
|
resp read: 0.0005 secs, 0.0000 secs, 0.0039 secs
|
||||||
|
|
||||||
|
Status code distribution:
|
||||||
|
[200] 100 responses
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
Summary:
|
||||||
|
Total: 3.3544 secs
|
||||||
|
Slowest: 0.1031 secs
|
||||||
|
Fastest: 0.0310 secs
|
||||||
|
Average: 0.0335 secs
|
||||||
|
Requests/sec: 29.8116
|
||||||
|
|
||||||
|
Total data: 61200 bytes
|
||||||
|
Size/request: 612 bytes
|
||||||
|
|
||||||
|
Response time histogram:
|
||||||
|
0.031 [1] |
|
||||||
|
0.038 [92] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
|
||||||
|
0.045 [6] |■■■
|
||||||
|
0.053 [0] |
|
||||||
|
0.060 [0] |
|
||||||
|
0.067 [0] |
|
||||||
|
0.074 [0] |
|
||||||
|
0.081 [0] |
|
||||||
|
0.089 [0] |
|
||||||
|
0.096 [0] |
|
||||||
|
0.103 [1] |
|
||||||
|
|
||||||
|
|
||||||
|
Latency distribution:
|
||||||
|
10% in 0.0314 secs
|
||||||
|
25% in 0.0317 secs
|
||||||
|
50% in 0.0320 secs
|
||||||
|
75% in 0.0327 secs
|
||||||
|
90% in 0.0369 secs
|
||||||
|
95% in 0.0394 secs
|
||||||
|
99% in 0.1031 secs
|
||||||
|
|
||||||
|
Details (average, fastest, slowest):
|
||||||
|
DNS+dialup: 0.0001 secs, 0.0310 secs, 0.1031 secs
|
||||||
|
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0049 secs
|
||||||
|
req write: 0.0000 secs, 0.0000 secs, 0.0001 secs
|
||||||
|
resp wait: 0.0330 secs, 0.0305 secs, 0.0973 secs
|
||||||
|
resp read: 0.0005 secs, 0.0000 secs, 0.0039 secs
|
||||||
|
|
||||||
|
Status code distribution:
|
||||||
|
[200] 100 responses
|
||||||
|
[404] 2 responses
|
||||||
|
[500] 10 responses
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
Total: 2.3688 secs
|
||||||
|
Slowest: 0.0000 secs
|
||||||
|
Fastest: 0.0000 secs
|
||||||
|
Average: NaN secs
|
||||||
|
Requests/sec: 35.4606
|
||||||
|
|
||||||
|
|
||||||
|
Response time histogram:
|
||||||
|
|
||||||
|
|
||||||
|
Latency distribution:
|
||||||
|
|
||||||
|
Details (average, fastest, slowest):
|
||||||
|
DNS+dialup: NaN secs, 0.0000 secs, 0.0000 secs
|
||||||
|
DNS-lookup: NaN secs, 0.0000 secs, 0.0000 secs
|
||||||
|
req write: NaN secs, 0.0000 secs, 0.0000 secs
|
||||||
|
resp wait: NaN secs, 0.0000 secs, 0.0000 secs
|
||||||
|
resp read: NaN secs, 0.0000 secs, 0.0000 secs
|
||||||
|
|
||||||
|
Status code distribution:
|
||||||
|
|
||||||
|
Error distribution:
|
||||||
|
[84] Get http://localhost:8081: dial tcp [::1]:8081: connect: connection refused
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
Total: 3.3544 secs
|
||||||
|
Slowest: 0.1031 secs
|
||||||
|
Fastest: 0.0310 secs
|
||||||
|
Average: 0.0335 secs
|
||||||
|
Requests/sec: 29.8116
|
||||||
|
|
||||||
|
Total data: 61200 bytes
|
||||||
|
Size/request: 612 bytes
|
||||||
|
|
||||||
|
Response time histogram:
|
||||||
|
0.031 [1] |
|
||||||
|
0.038 [92] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
|
||||||
|
0.045 [6] |■■■
|
||||||
|
0.053 [0] |
|
||||||
|
0.060 [0] |
|
||||||
|
0.067 [0] |
|
||||||
|
0.074 [0] |
|
||||||
|
0.081 [0] |
|
||||||
|
0.089 [0] |
|
||||||
|
0.096 [0] |
|
||||||
|
0.103 [1] |
|
||||||
|
|
||||||
|
|
||||||
|
Latency distribution:
|
||||||
|
10% in 0.0314 secs
|
||||||
|
25% in 0.0317 secs
|
||||||
|
50% in 0.0320 secs
|
||||||
|
75% in 0.0327 secs
|
||||||
|
90% in 0.0369 secs
|
||||||
|
95% in 0.0394 secs
|
||||||
|
99% in 0.1031 secs
|
||||||
|
|
||||||
|
Details (average, fastest, slowest):
|
||||||
|
DNS+dialup: 0.0001 secs, 0.0310 secs, 0.1031 secs
|
||||||
|
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0049 secs
|
||||||
|
req write: 0.0000 secs, 0.0000 secs, 0.0001 secs
|
||||||
|
resp wait: 0.0330 secs, 0.0305 secs, 0.0973 secs
|
||||||
|
resp read: 0.0005 secs, 0.0000 secs, 0.0039 secs
|
||||||
|
|
||||||
|
Status code distribution:
|
||||||
|
[200] 100 responses
|
||||||
|
[204] 50 responses
|
||||||
|
[202] 10 responses
|
||||||
|
|
@ -0,0 +1,318 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/resource"
|
||||||
|
"github.com/derailed/tview"
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
benchTitle = "Benchmarks"
|
||||||
|
benchTitleFmt = " [seagreen::b]%s([fuchsia::b]%d[fuchsia::-])[seagreen::-] "
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
totalRx = regexp.MustCompile(`Total:\s+([0-9.]+)\ssecs`)
|
||||||
|
reqRx = regexp.MustCompile(`Requests/sec:\s+([0-9.]+)`)
|
||||||
|
okRx = regexp.MustCompile(`\[2\d{2}\]\s+(\d+)\s+responses`)
|
||||||
|
errRx = regexp.MustCompile(`\[[4-5]\d{2}\]\s+(\d+)\s+responses`)
|
||||||
|
toastRx = regexp.MustCompile(`Error distribution`)
|
||||||
|
benchHeader = resource.Row{"NAMESPACE", "NAME", "STATUS", "TIME", "REQ/S", "2XX", "4XX/5XX", "REPORT", "AGE"}
|
||||||
|
)
|
||||||
|
|
||||||
|
type benchView struct {
|
||||||
|
*tview.Pages
|
||||||
|
|
||||||
|
app *appView
|
||||||
|
current igniter
|
||||||
|
cancel context.CancelFunc
|
||||||
|
selectedItem string
|
||||||
|
selectedRow int
|
||||||
|
actions keyActions
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBenchView(app *appView) *benchView {
|
||||||
|
v := benchView{
|
||||||
|
Pages: tview.NewPages(),
|
||||||
|
actions: make(keyActions),
|
||||||
|
app: app,
|
||||||
|
current: app.content.GetPrimitive("main").(igniter),
|
||||||
|
}
|
||||||
|
|
||||||
|
tv := newTableView(app, benchTitle)
|
||||||
|
{
|
||||||
|
tv.SetSelectionChangedFunc(v.selChanged)
|
||||||
|
tv.SetBorderFocusColor(tcell.ColorSeaGreen)
|
||||||
|
tv.SetSelectedStyle(tcell.ColorWhite, tcell.ColorSeaGreen, tcell.AttrNone)
|
||||||
|
tv.colorerFn = benchColorer
|
||||||
|
tv.currentNS = ""
|
||||||
|
}
|
||||||
|
v.AddPage("table", tv, true, true)
|
||||||
|
|
||||||
|
details := newDetailsView(app, v.backCmd)
|
||||||
|
v.AddPage("details", details, true, false)
|
||||||
|
|
||||||
|
v.registerActions()
|
||||||
|
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init the view.
|
||||||
|
func (v *benchView) init(ctx context.Context, _ string) {
|
||||||
|
if err := v.watchBenchDir(ctx); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Benchdir watch failed!")
|
||||||
|
v.app.flash().errf("Unable to watch benchmarks directory %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tv := v.getTV()
|
||||||
|
v.refresh()
|
||||||
|
tv.sortCol.index, tv.sortCol.asc = tv.nameColIndex()+7, true
|
||||||
|
tv.refresh()
|
||||||
|
v.app.SetFocus(tv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *benchView) refresh() {
|
||||||
|
tv := v.getTV()
|
||||||
|
tv.update(v.hydrate())
|
||||||
|
tv.resetTitle()
|
||||||
|
v.selChanged(v.selectedRow, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *benchView) getTV() *tableView {
|
||||||
|
if vu, ok := v.GetPrimitive("table").(*tableView); ok {
|
||||||
|
return vu
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *benchView) getDetails() *detailsView {
|
||||||
|
if vu, ok := v.GetPrimitive("details").(*detailsView); ok {
|
||||||
|
return vu
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *benchView) registerActions() {
|
||||||
|
v.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false)
|
||||||
|
v.actions[tcell.KeyEnter] = newKeyAction("Enter", v.enterCmd, false)
|
||||||
|
v.actions[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd, false)
|
||||||
|
|
||||||
|
vu := v.getTV()
|
||||||
|
vu.setActions(v.actions)
|
||||||
|
v.app.setHints(vu.hints())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *benchView) getTitle() string {
|
||||||
|
return benchTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *benchView) selChanged(r, c int) {
|
||||||
|
tv := v.getTV()
|
||||||
|
if r == 0 || tv.GetCell(r, 0) == nil {
|
||||||
|
v.selectedItem = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v.selectedRow = r
|
||||||
|
v.selectedItem = strings.TrimSpace(tv.GetCell(r, 7).Text)
|
||||||
|
v.getTV().cmdBuff.setActive(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *benchView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
tv := v.getTV()
|
||||||
|
tv.sortCol.index, tv.sortCol.asc = tv.nameColIndex()+col, asc
|
||||||
|
tv.refresh()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *benchView) enterCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if v.getTV().cmdBuff.isActive() {
|
||||||
|
return v.getTV().filterCmd(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
sel := v.selectedItem
|
||||||
|
if sel == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile(filepath.Join(K9sBenchDir, sel))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Read failed")
|
||||||
|
v.app.flash().errf("Unable to load bench file %s", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Debug().Msgf("Bench %v", string(data))
|
||||||
|
vu := v.getDetails()
|
||||||
|
vu.Clear()
|
||||||
|
fmt.Fprintln(vu, string(data))
|
||||||
|
{
|
||||||
|
vu.setCategory("Bench")
|
||||||
|
vu.setTitle(sel)
|
||||||
|
vu.SetTextColor(tcell.ColorAqua)
|
||||||
|
vu.ScrollToBeginning()
|
||||||
|
}
|
||||||
|
v.SwitchToPage("details")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *benchView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
sel := v.selectedItem
|
||||||
|
if sel == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
showModal(v.Pages, fmt.Sprintf("Deleting `%s are you sure?", sel), "table", func() {
|
||||||
|
if err := os.Remove(filepath.Join(K9sBenchDir, sel)); err != nil {
|
||||||
|
v.app.flash().errf("Unable to delete file %s", err)
|
||||||
|
log.Error().Err(err).Msg("Delete failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v.refresh()
|
||||||
|
v.app.flash().infof("Benchmark %s deleted!", sel)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *benchView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if v.cancel != nil {
|
||||||
|
v.cancel()
|
||||||
|
}
|
||||||
|
v.SwitchToPage("table")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *benchView) hints() hints {
|
||||||
|
return v.CurrentPage().Item.(hinter).hints()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *benchView) hydrate() resource.TableData {
|
||||||
|
cmds := helpCmds(v.app.conn())
|
||||||
|
|
||||||
|
data := resource.TableData{
|
||||||
|
Header: benchHeader,
|
||||||
|
Rows: make(resource.RowEvents, len(cmds)),
|
||||||
|
Namespace: resource.AllNamespaces,
|
||||||
|
}
|
||||||
|
|
||||||
|
ff, err := ioutil.ReadDir(K9sBenchDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Reading bench dir")
|
||||||
|
v.app.flash().errf("Unable to read bench directory %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range ff {
|
||||||
|
bench, err := ioutil.ReadFile(filepath.Join(K9sBenchDir, f.Name()))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tokens := strings.Split(f.Name(), "_")
|
||||||
|
fields := resource.Row{0: tokens[0], 1: tokens[1], 7: f.Name(), 8: time.Since(f.ModTime()).String()}
|
||||||
|
augmentRow(fields, string(bench))
|
||||||
|
data.Rows[f.Name()] = &resource.RowEvent{
|
||||||
|
Action: resource.New,
|
||||||
|
Fields: fields,
|
||||||
|
Deltas: fields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func augmentRow(fields resource.Row, data string) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
col := 2
|
||||||
|
fields[col] = "pass"
|
||||||
|
mf := toastRx.FindAllStringSubmatch(data, 1)
|
||||||
|
if len(mf) > 0 {
|
||||||
|
fields[col] = "fail"
|
||||||
|
}
|
||||||
|
col++
|
||||||
|
|
||||||
|
mt := totalRx.FindAllStringSubmatch(data, 1)
|
||||||
|
if len(mt) > 0 {
|
||||||
|
fields[col] = mt[0][1]
|
||||||
|
}
|
||||||
|
col++
|
||||||
|
|
||||||
|
mr := reqRx.FindAllStringSubmatch(data, 1)
|
||||||
|
if len(mr) > 0 {
|
||||||
|
fields[col] = mr[0][1]
|
||||||
|
}
|
||||||
|
col++
|
||||||
|
|
||||||
|
ms := okRx.FindAllStringSubmatch(data, -1)
|
||||||
|
fields[col] = "0"
|
||||||
|
if len(ms) > 0 {
|
||||||
|
var sum int
|
||||||
|
for _, m := range ms {
|
||||||
|
if m, err := strconv.Atoi(string(m[1])); err == nil {
|
||||||
|
sum += m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fields[col] = strconv.Itoa(sum)
|
||||||
|
}
|
||||||
|
col++
|
||||||
|
|
||||||
|
me := errRx.FindAllStringSubmatch(data, -1)
|
||||||
|
fields[col] = "0"
|
||||||
|
if len(me) > 0 {
|
||||||
|
var sum int
|
||||||
|
for _, m := range me {
|
||||||
|
if m, err := strconv.Atoi(string(m[1])); err == nil {
|
||||||
|
sum += m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fields[col] = strconv.Itoa(sum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *benchView) resetTitle() {
|
||||||
|
v.SetTitle(fmt.Sprintf(benchTitleFmt, benchTitle, v.getTV().GetRowCount()-1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *benchView) watchBenchDir(ctx context.Context) error {
|
||||||
|
w, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case evt := <-w.Events:
|
||||||
|
log.Debug().Msgf("Bench event %#v", evt)
|
||||||
|
v.app.QueueUpdateDraw(func() {
|
||||||
|
v.refresh()
|
||||||
|
})
|
||||||
|
case err := <-w.Errors:
|
||||||
|
log.Info().Err(err).Msg("Skin watcher failed")
|
||||||
|
return
|
||||||
|
case <-ctx.Done():
|
||||||
|
w.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return w.Add(K9sBenchDir)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/resource"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAugmentRow(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
file string
|
||||||
|
e resource.Row
|
||||||
|
}{
|
||||||
|
"cool": {
|
||||||
|
"assets/b1.txt",
|
||||||
|
resource.Row{"pass", "3.3544", "29.8116", "100", "0"},
|
||||||
|
},
|
||||||
|
"2XX": {
|
||||||
|
"assets/b4.txt",
|
||||||
|
resource.Row{"pass", "3.3544", "29.8116", "160", "0"},
|
||||||
|
},
|
||||||
|
"4XX/5XX": {
|
||||||
|
"assets/b2.txt",
|
||||||
|
resource.Row{"pass", "3.3544", "29.8116", "100", "12"},
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"assets/b3.txt",
|
||||||
|
resource.Row{"fail", "2.3688", "35.4606", "0", "0"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, u := range uu {
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
data, err := ioutil.ReadFile(u.file)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
fields := make(resource.Row, 8)
|
||||||
|
augmentRow(fields, string(data))
|
||||||
|
assert.Equal(t, u.e, fields[2:7])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
"github.com/rakyll/hey/requester"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const benchFmat = "%s_%s_%d.txt"
|
||||||
|
|
||||||
|
// K9sBenchDir directory to store K9s benchmark files.
|
||||||
|
var K9sBenchDir = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-bench-%s", config.MustK9sUser()))
|
||||||
|
|
||||||
|
type (
|
||||||
|
benchmark struct {
|
||||||
|
canceled bool
|
||||||
|
config benchConfig
|
||||||
|
worker *requester.Work
|
||||||
|
}
|
||||||
|
|
||||||
|
benchConfig struct {
|
||||||
|
Method, Path, URL string
|
||||||
|
C, N int
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func newBenchmark(cfg benchConfig) (*benchmark, error) {
|
||||||
|
b := benchmark{config: cfg}
|
||||||
|
return &b, b.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *benchmark) init() error {
|
||||||
|
req, err := http.NewRequest(b.config.Method, b.config.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.worker = &requester.Work{
|
||||||
|
Request: req,
|
||||||
|
N: b.config.N,
|
||||||
|
C: b.config.C,
|
||||||
|
Output: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *benchmark) annuled() bool {
|
||||||
|
return b.canceled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *benchmark) cancel() {
|
||||||
|
b.canceled = true
|
||||||
|
b.worker.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *benchmark) run(done func()) {
|
||||||
|
buff := new(bytes.Buffer)
|
||||||
|
b.worker.Writer = buff
|
||||||
|
b.worker.Run()
|
||||||
|
if !b.canceled {
|
||||||
|
if err := b.save(buff); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Saving benchmark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *benchmark) save(r io.Reader) error {
|
||||||
|
if err := os.MkdirAll(K9sBenchDir, 0777); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ns, n := namespaced(b.config.Path)
|
||||||
|
file := filepath.Join(K9sBenchDir, fmt.Sprintf(benchFmat, ns, n, time.Now().UnixNano()))
|
||||||
|
f, err := os.Create(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
bb, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Write(bb)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -29,24 +29,45 @@ func defaultColorer(ns string, r *resource.RowEvent) tcell.Color {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func forwardColorer(string, *resource.RowEvent) tcell.Color {
|
||||||
|
return tcell.ColorSkyblue
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchColorer(ns string, r *resource.RowEvent) tcell.Color {
|
||||||
|
c := tcell.ColorPaleGreen
|
||||||
|
|
||||||
|
statusCol := 2
|
||||||
|
if strings.TrimSpace(r.Fields[statusCol]) != "pass" {
|
||||||
|
c = errColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
func aliasColorer(string, *resource.RowEvent) tcell.Color {
|
func aliasColorer(string, *resource.RowEvent) tcell.Color {
|
||||||
return tcell.ColorFuchsia
|
return tcell.ColorFuchsia
|
||||||
}
|
}
|
||||||
|
|
||||||
func rbacColorer(ns string, r *resource.RowEvent) tcell.Color {
|
func rbacColorer(ns string, r *resource.RowEvent) tcell.Color {
|
||||||
c := defaultColorer(ns, r)
|
return defaultColorer(ns, r)
|
||||||
|
|
||||||
// return tcell.ColorDarkOliveGreen
|
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func podColorer(ns string, r *resource.RowEvent) tcell.Color {
|
func podColorer(ns string, r *resource.RowEvent) tcell.Color {
|
||||||
c := defaultColorer(ns, r)
|
c := defaultColorer(ns, r)
|
||||||
|
|
||||||
statusCol := 3
|
readyCol := 2
|
||||||
if len(ns) != 0 {
|
if len(ns) != 0 {
|
||||||
statusCol = 2
|
readyCol = 1
|
||||||
}
|
}
|
||||||
|
statusCol := readyCol + 1
|
||||||
|
|
||||||
|
tokens := strings.Split(strings.TrimSpace(r.Fields[readyCol]), "/")
|
||||||
|
if len(tokens) == 2 && (tokens[0] == "0" || tokens[0] != tokens[1]) {
|
||||||
|
if strings.TrimSpace(r.Fields[statusCol]) != "Completed" {
|
||||||
|
c = errColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch strings.TrimSpace(r.Fields[statusCol]) {
|
switch strings.TrimSpace(r.Fields[statusCol]) {
|
||||||
case "ContainerCreating", "PodInitializing":
|
case "ContainerCreating", "PodInitializing":
|
||||||
return addColor
|
return addColor
|
||||||
|
|
@ -59,15 +80,28 @@ func podColorer(ns string, r *resource.RowEvent) tcell.Color {
|
||||||
c = errColor
|
c = errColor
|
||||||
}
|
}
|
||||||
|
|
||||||
readyCol := 2
|
return c
|
||||||
if len(ns) != 0 {
|
|
||||||
readyCol = 1
|
|
||||||
}
|
}
|
||||||
tokens := strings.Split(strings.TrimSpace(r.Fields[readyCol]), "/")
|
|
||||||
if len(tokens) == 2 && (tokens[0] == "0" || tokens[0] != tokens[1]) {
|
func containerColorer(ns string, r *resource.RowEvent) tcell.Color {
|
||||||
if strings.TrimSpace(r.Fields[statusCol]) != "Completed" {
|
c := defaultColorer(ns, r)
|
||||||
|
|
||||||
|
readyCol := 2
|
||||||
|
if strings.TrimSpace(r.Fields[readyCol]) == "false" {
|
||||||
c = errColor
|
c = errColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stateCol := readyCol + 1
|
||||||
|
switch strings.TrimSpace(r.Fields[stateCol]) {
|
||||||
|
case "ContainerCreating", "PodInitializing":
|
||||||
|
return addColor
|
||||||
|
case "Terminating", "Initialized":
|
||||||
|
return highlightColor
|
||||||
|
case "Completed":
|
||||||
|
return completedColor
|
||||||
|
case "Running":
|
||||||
|
default:
|
||||||
|
c = errColor
|
||||||
}
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package views
|
package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/resource"
|
"github.com/derailed/k9s/internal/resource"
|
||||||
|
|
@ -58,6 +57,12 @@ func (c *command) run(cmd string) bool {
|
||||||
case cmd == "?", cmd == "help":
|
case cmd == "?", cmd == "help":
|
||||||
c.app.inject(newHelpView(c.app))
|
c.app.inject(newHelpView(c.app))
|
||||||
return true
|
return true
|
||||||
|
case cmd == "pf":
|
||||||
|
c.app.inject(newForwardView(c.app))
|
||||||
|
return true
|
||||||
|
case cmd == "be":
|
||||||
|
c.app.inject(newBenchView(c.app))
|
||||||
|
return true
|
||||||
case cmd == "alias":
|
case cmd == "alias":
|
||||||
c.app.inject(newAliasView(c.app))
|
c.app.inject(newAliasView(c.app))
|
||||||
return true
|
return true
|
||||||
|
|
@ -84,7 +89,7 @@ func (c *command) run(cmd string) bool {
|
||||||
v.setDecorateFn(res.decorateFn)
|
v.setDecorateFn(res.decorateFn)
|
||||||
}
|
}
|
||||||
const fmat = "Viewing resource %s..."
|
const fmat = "Viewing resource %s..."
|
||||||
c.app.flash(flashInfo, fmt.Sprintf(fmat, res.title))
|
c.app.flash().infof(fmat, res.title)
|
||||||
log.Debug().Msgf("Running command %s", cmd)
|
log.Debug().Msgf("Running command %s", cmd)
|
||||||
c.exec(cmd, v)
|
c.exec(cmd, v)
|
||||||
return true
|
return true
|
||||||
|
|
@ -93,7 +98,7 @@ func (c *command) run(cmd string) bool {
|
||||||
|
|
||||||
res, ok := allCRDs(c.app.conn())[cmd]
|
res, ok := allCRDs(c.app.conn())[cmd]
|
||||||
if !ok {
|
if !ok {
|
||||||
c.app.flash(flashWarn, fmt.Sprintf("Huh? `%s` command not found", cmd))
|
c.app.flash().warnf("Huh? `%s` command not found", cmd)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,6 +113,7 @@ func (c *command) run(cmd string) bool {
|
||||||
)
|
)
|
||||||
v.setColorerFn(defaultColorer)
|
v.setColorerFn(defaultColorer)
|
||||||
c.exec(cmd, v)
|
c.exec(cmd, v)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
package views
|
package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
"github.com/derailed/k9s/internal/resource"
|
"github.com/derailed/k9s/internal/resource"
|
||||||
|
"github.com/derailed/tview"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"k8s.io/client-go/tools/portforward"
|
||||||
)
|
)
|
||||||
|
|
||||||
type containerView struct {
|
type containerView struct {
|
||||||
|
|
@ -18,6 +24,7 @@ func newContainerView(t string, app *appView, list resource.List, path string, e
|
||||||
{
|
{
|
||||||
v.path = &path
|
v.path = &path
|
||||||
v.extraActionsFn = v.extraActions
|
v.extraActionsFn = v.extraActions
|
||||||
|
v.colorerFn = containerColorer
|
||||||
v.current = app.content.GetPrimitive("main").(igniter)
|
v.current = app.content.GetPrimitive("main").(igniter)
|
||||||
v.exitFn = exitFn
|
v.exitFn = exitFn
|
||||||
}
|
}
|
||||||
|
|
@ -52,6 +59,12 @@ func (v *containerView) logsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
if !v.rowSelected() {
|
if !v.rowSelected() {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cell := v.getTV().GetCell(v.selectedRow, 3)
|
||||||
|
if cell != nil && strings.TrimSpace(cell.Text) != "Running" {
|
||||||
|
v.app.flash().err(errors.New("No logs for a non running container"))
|
||||||
|
return evt
|
||||||
|
}
|
||||||
v.showLogs(v.selectedItem, v.list.GetName(), v, false)
|
v.showLogs(v.selectedItem, v.list.GetName(), v, false)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -76,38 +89,28 @@ func (v *containerView) shellCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
if !v.rowSelected() {
|
if !v.rowSelected() {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
log.Debug().Msgf("Selected %s", v.selectedItem)
|
|
||||||
v.shellIn(*v.path, v.selectedItem)
|
v.suspend()
|
||||||
|
{
|
||||||
|
shellIn(v.app, *v.path, v.selectedItem)
|
||||||
|
}
|
||||||
|
v.resume()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *containerView) shellIn(path, co string) {
|
|
||||||
ns, po := namespaced(path)
|
|
||||||
args := make([]string, 0, 12)
|
|
||||||
args = append(args, "exec", "-it")
|
|
||||||
args = append(args, "--context", v.app.config.K9s.CurrentContext)
|
|
||||||
args = append(args, "-n", ns)
|
|
||||||
args = append(args, po)
|
|
||||||
if len(co) != 0 {
|
|
||||||
args = append(args, "-c", co)
|
|
||||||
}
|
|
||||||
args = append(args, "--", "sh")
|
|
||||||
log.Debug().Msgf("Shell args %v", args)
|
|
||||||
runK(true, v.app, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *containerView) extraActions(aa keyActions) {
|
func (v *containerView) extraActions(aa keyActions) {
|
||||||
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
|
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
|
||||||
aa[KeyShiftL] = newKeyAction("Prev Logs", v.prevLogsCmd, true)
|
aa[KeyShiftF] = newKeyAction("PortFwd", v.portFwdCmd, true)
|
||||||
|
aa[KeyShiftL] = newKeyAction("Logs Previous", v.prevLogsCmd, true)
|
||||||
aa[KeyS] = newKeyAction("Shell", v.shellCmd, true)
|
aa[KeyS] = newKeyAction("Shell", v.shellCmd, true)
|
||||||
aa[tcell.KeyEscape] = newKeyAction("Back", v.backCmd, false)
|
aa[tcell.KeyEscape] = newKeyAction("Back", v.backCmd, false)
|
||||||
aa[KeyP] = newKeyAction("Previous", v.backCmd, false)
|
aa[KeyP] = newKeyAction("Previous", v.backCmd, false)
|
||||||
aa[tcell.KeyEnter] = newKeyAction("View Logs", v.logsCmd, false)
|
aa[tcell.KeyEnter] = newKeyAction("View Logs", v.logsCmd, false)
|
||||||
aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(6, false), true)
|
aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(6, false), true)
|
||||||
aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(7, false), true)
|
aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(7, false), true)
|
||||||
aa[KeyAltC] = newKeyAction("Sort %CPU", v.sortColCmd(8, false), true)
|
aa[KeyAltC] = newKeyAction("Sort CPU%", v.sortColCmd(8, false), true)
|
||||||
aa[KeyAltM] = newKeyAction("Sort %MEM", v.sortColCmd(9, false), true)
|
aa[KeyAltM] = newKeyAction("Sort MEM%", v.sortColCmd(9, false), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *containerView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
func (v *containerView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
@ -120,8 +123,87 @@ func (v *containerView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *containerView) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if !v.rowSelected() {
|
||||||
|
return evt
|
||||||
|
}
|
||||||
|
|
||||||
|
cell := v.getTV().GetCell(v.selectedRow, 10)
|
||||||
|
ports := strings.Split(cell.Text, ",")
|
||||||
|
if len(ports) == 0 {
|
||||||
|
v.app.flash().err(errors.New("No ports to foward to"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
port := strings.TrimSpace(ports[0])
|
||||||
|
if port == "" {
|
||||||
|
v.app.flash().err(errors.New("No ports to foward to"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
f := tview.NewForm()
|
||||||
|
f.SetItemPadding(0)
|
||||||
|
f.SetButtonsAlign(tview.AlignCenter).
|
||||||
|
SetButtonBackgroundColor(tview.Styles.PrimitiveBackgroundColor).
|
||||||
|
SetButtonTextColor(tview.Styles.PrimaryTextColor).
|
||||||
|
SetLabelColor(tcell.ColorAqua).
|
||||||
|
SetFieldTextColor(tcell.ColorOrange)
|
||||||
|
|
||||||
|
f1, f2 := port, port
|
||||||
|
f.AddInputField("Pod Port:", f1, 10, nil, func(changed string) {
|
||||||
|
f1 = changed
|
||||||
|
})
|
||||||
|
f.AddInputField("Local Port:", f2, 10, nil, func(changed string) {
|
||||||
|
f2 = changed
|
||||||
|
})
|
||||||
|
|
||||||
|
f.AddButton("OK", func() {
|
||||||
|
pf := k8s.NewPortForward(v.app.conn(), &log.Logger)
|
||||||
|
ports := []string{f2 + ":" + f1}
|
||||||
|
fw, err := pf.Start(*v.path, ports)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Fort Forward")
|
||||||
|
v.app.flash().errf("PortForward failed! %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf(">>> Starting port forward %q %v", *v.path, ports)
|
||||||
|
go func(f *portforward.PortForwarder) {
|
||||||
|
v.app.QueueUpdate(func() {
|
||||||
|
v.app.forwarders = append(v.app.forwarders, pf)
|
||||||
|
v.app.flash().infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0])
|
||||||
|
v.app.gotoResource("pf", true)
|
||||||
|
})
|
||||||
|
pf.SetActive(true)
|
||||||
|
if err := f.ForwardPorts(); err != nil {
|
||||||
|
v.app.forwarders = v.app.forwarders[:len(v.app.forwarders)-1]
|
||||||
|
v.app.QueueUpdate(func() {
|
||||||
|
pf.SetActive(false)
|
||||||
|
log.Error().Err(err).Msg("Port forward failed")
|
||||||
|
v.app.flash().errf("PortForward failed %s", err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}(fw)
|
||||||
|
})
|
||||||
|
f.AddButton("Cancel", func() {
|
||||||
|
v.app.flash().info("Canceled!!")
|
||||||
|
v.dismissModal()
|
||||||
|
})
|
||||||
|
|
||||||
|
modal := tview.NewModalForm("<PortForward>", f)
|
||||||
|
modal.SetDoneFunc(func(_ int, b string) {
|
||||||
|
v.dismissModal()
|
||||||
|
})
|
||||||
|
v.AddPage("dialog", modal, false, false)
|
||||||
|
v.ShowPage("dialog")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *containerView) dismissModal() {
|
||||||
|
v.RemovePage("dialog")
|
||||||
|
v.switchPage(v.list.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
func (v *containerView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (v *containerView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
// v.app.inject(v.current)
|
|
||||||
v.exitFn()
|
v.exitFn()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ func (v *contextView) useCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
if err := v.useContext(v.selectedItem); err != nil {
|
if err := v.useContext(v.selectedItem); err != nil {
|
||||||
v.app.flash(flashWarn, err.Error())
|
v.app.flash().err(err)
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,14 +57,9 @@ func (v *contextView) useContext(name string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
v.app.startInformer()
|
v.app.startInformer()
|
||||||
// // Update cluster info on context switch.
|
|
||||||
// v.app.QueueUpdateDraw(func() {
|
|
||||||
// v.app.clusterInfoView.refresh()
|
|
||||||
// })
|
|
||||||
|
|
||||||
v.app.config.Reset()
|
v.app.config.Reset()
|
||||||
v.app.config.Save()
|
v.app.config.Save()
|
||||||
v.app.flash(flashInfo, "Switching context to", ctx)
|
v.app.flash().infof("Switching context to %s", ctx)
|
||||||
v.refresh()
|
v.refresh()
|
||||||
if tv, ok := v.GetPrimitive("ctx").(*tableView); ok {
|
if tv, ok := v.GetPrimitive("ctx").(*tableView); ok {
|
||||||
tv.Select(0, 0)
|
tv.Select(0, 0)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
package views
|
package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/resource"
|
"github.com/derailed/k9s/internal/resource"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
)
|
)
|
||||||
|
|
@ -26,11 +24,11 @@ func (v *cronJobView) trigger(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
v.app.flash(flashInfo, fmt.Sprintf("Triggering %s %s", v.list.GetName(), v.selectedItem))
|
|
||||||
if err := v.list.Resource().(resource.Runner).Run(v.selectedItem); err != nil {
|
if err := v.list.Resource().(resource.Runner).Run(v.selectedItem); err != nil {
|
||||||
v.app.flash(flashErr, "Boom!", err.Error())
|
v.app.flash().errf("Cronjob trigger failed %v", err)
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
v.app.flash().infof("Triggering %s %s", v.list.GetName(), v.selectedItem)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ func (v *detailsView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
||||||
func (v *detailsView) searchCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (v *detailsView) searchCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
if v.cmdBuff.isActive() && !v.cmdBuff.empty() {
|
if v.cmdBuff.isActive() && !v.cmdBuff.empty() {
|
||||||
v.app.flash(flashInfo, fmt.Sprintf("Searching for %s...", v.cmdBuff))
|
v.app.flash().infof("Searching for %s...", v.cmdBuff)
|
||||||
v.search(evt)
|
v.search(evt)
|
||||||
highlights := v.GetHighlights()
|
highlights := v.GetHighlights()
|
||||||
if len(highlights) > 0 {
|
if len(highlights) > 0 {
|
||||||
|
|
@ -135,15 +135,15 @@ func (v *detailsView) search(evt *tcell.EventKey) {
|
||||||
v.SetText(v.decorateLines(v.GetText(false), v.cmdBuff.String()))
|
v.SetText(v.decorateLines(v.GetText(false), v.cmdBuff.String()))
|
||||||
|
|
||||||
if v.cmdBuff.empty() {
|
if v.cmdBuff.empty() {
|
||||||
v.app.flash(flashWarn, "Clearing out search query...")
|
v.app.flash().info("Clearing out search query...")
|
||||||
v.refreshTitle()
|
v.refreshTitle()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if v.numSelections == 0 {
|
if v.numSelections == 0 {
|
||||||
v.app.flash(flashWarn, "No matches found!")
|
v.app.flash().warn("No matches found!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
v.app.flash(flashWarn, fmt.Sprintf("Found <%d> matches! <tab>/<TAB> for next/previous", v.numSelections))
|
v.app.flash().infof("Found <%d> matches! <tab>/<TAB> for next/previous", v.numSelections)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *detailsView) nextCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (v *detailsView) nextCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
@ -154,7 +154,7 @@ func (v *detailsView) nextCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
index, _ := strconv.Atoi(highlights[0])
|
index, _ := strconv.Atoi(highlights[0])
|
||||||
index = (index + 1) % v.numSelections
|
index = (index + 1) % v.numSelections
|
||||||
if index+1 == v.numSelections {
|
if index+1 == v.numSelections {
|
||||||
v.app.flash(flashInfo, "Search hit BOTTOM, continuing at TOP")
|
v.app.flash().info("Search hit BOTTOM, continuing at TOP")
|
||||||
}
|
}
|
||||||
v.Highlight(strconv.Itoa(index)).ScrollToHighlight()
|
v.Highlight(strconv.Itoa(index)).ScrollToHighlight()
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -168,7 +168,7 @@ func (v *detailsView) prevCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
index, _ := strconv.Atoi(highlights[0])
|
index, _ := strconv.Atoi(highlights[0])
|
||||||
index = (index - 1 + v.numSelections) % v.numSelections
|
index = (index - 1 + v.numSelections) % v.numSelections
|
||||||
if index == 0 {
|
if index == 0 {
|
||||||
v.app.flash(flashInfo, "Search hit TOP, continuing at BOTTOM")
|
v.app.flash().info("Search hit TOP, continuing at BOTTOM")
|
||||||
}
|
}
|
||||||
v.Highlight(strconv.Itoa(index)).ScrollToHighlight()
|
v.Highlight(strconv.Itoa(index)).ScrollToHighlight()
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -196,13 +196,9 @@ func (v *detailsView) refreshTitle() {
|
||||||
func (v *detailsView) setTitle(t string) {
|
func (v *detailsView) setTitle(t string) {
|
||||||
v.title = t
|
v.title = t
|
||||||
|
|
||||||
fmat := strings.Replace(detailsTitleFmt, "[fg:bg", "["+v.app.styles.Style.Title.FgColor+":"+v.app.styles.Style.Title.BgColor, -1)
|
title := skinTitle(fmt.Sprintf(detailsTitleFmt, v.category, t), v.app.styles.Style)
|
||||||
fmat = strings.Replace(fmat, "[hilite", "["+v.app.styles.Style.Title.CounterColor, 1)
|
|
||||||
title := fmt.Sprintf(fmat, v.category, t)
|
|
||||||
if !v.cmdBuff.empty() {
|
if !v.cmdBuff.empty() {
|
||||||
fmat := strings.Replace(searchFmt, "[fg:bg", "["+v.app.styles.Style.Title.FgColor+":"+v.app.styles.Style.Title.BgColor, -1)
|
title += skinTitle(fmt.Sprintf(searchFmt, v.cmdBuff.String()), v.app.styles.Style)
|
||||||
fmat = strings.Replace(fmat, "[filter", "["+v.app.styles.Style.Title.FilterColor, 1)
|
|
||||||
title += fmt.Sprintf(fmat, v.cmdBuff.String())
|
|
||||||
}
|
}
|
||||||
v.SetTitle(title)
|
v.SetTitle(title)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ func (v *deployView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
dep, err := d.Get(ns, n)
|
dep, err := d.Get(ns, n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Fetching Deployment %s", v.selectedItem)
|
log.Error().Err(err).Msgf("Fetching Deployment %s", v.selectedItem)
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash().err(err)
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
dp := dep.(*v1.Deployment)
|
dp := dep.(*v1.Deployment)
|
||||||
|
|
@ -57,7 +57,7 @@ func (v *deployView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
sel, err := metav1.LabelSelectorAsSelector(dp.Spec.Selector)
|
sel, err := metav1.LabelSelectorAsSelector(dp.Spec.Selector)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Converting selector for Deployment %s", v.selectedItem)
|
log.Error().Err(err).Msgf("Converting selector for Deployment %s", v.selectedItem)
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash().err(err)
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
showPods(v.app, ns, "Deployment", v.selectedItem, sel.String(), "", v.backCmd)
|
showPods(v.app, ns, "Deployment", v.selectedItem, sel.String(), "", v.backCmd)
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ func (v *daemonSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
dset, err := d.Get(ns, n)
|
dset, err := d.Get(ns, n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Fetching DeaemonSet %s", v.selectedItem)
|
log.Error().Err(err).Msgf("Fetching DeaemonSet %s", v.selectedItem)
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash().err(err)
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
ds := dset.(*extv1beta1.DaemonSet)
|
ds := dset.(*extv1beta1.DaemonSet)
|
||||||
|
|
@ -57,7 +57,7 @@ func (v *daemonSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
sel, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector)
|
sel, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Converting selector for DaemonSet %s", v.selectedItem)
|
log.Error().Err(err).Msgf("Converting selector for DaemonSet %s", v.selectedItem)
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash().err(err)
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
showPods(v.app, "", "DaemonSet", v.selectedItem, sel.String(), "", v.backCmd)
|
showPods(v.app, "", "DaemonSet", v.selectedItem, sel.String(), "", v.backCmd)
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ func runK(clear bool, app *appView, args ...string) bool {
|
||||||
}
|
}
|
||||||
if err := execute(clear, bin, args...); err != nil {
|
if err := execute(clear, bin, args...); err != nil {
|
||||||
log.Error().Msgf("Command exited: %T %v %v", err, err, args)
|
log.Error().Msgf("Command exited: %T %v %v", err, err, args)
|
||||||
app.flash(flashErr, "Command exited:", err.Error())
|
app.flash().errf("Command exited: %v", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -44,6 +45,30 @@ func newFlashView(app *appView, m string) *flashView {
|
||||||
return &f
|
return &f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *flashView) info(msg string) {
|
||||||
|
v.setMessage(flashInfo, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *flashView) infof(fmat string, args ...interface{}) {
|
||||||
|
v.info(fmt.Sprintf(fmat, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *flashView) warn(msg string) {
|
||||||
|
v.setMessage(flashWarn, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *flashView) warnf(fmat string, args ...interface{}) {
|
||||||
|
v.warn(fmt.Sprintf(fmat, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *flashView) err(err error) {
|
||||||
|
v.setMessage(flashErr, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *flashView) errf(fmat string, args ...interface{}) {
|
||||||
|
v.setMessage(flashErr, fmt.Sprintf(fmat, args...))
|
||||||
|
}
|
||||||
|
|
||||||
func (v *flashView) setMessage(level flashLevel, msg ...string) {
|
func (v *flashView) setMessage(level flashLevel, msg ...string) {
|
||||||
if v.cancel != nil {
|
if v.cancel != nil {
|
||||||
v.cancel()
|
v.cancel()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,291 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/resource"
|
||||||
|
"github.com/derailed/tview"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
forwardTitle = "Port Forwards"
|
||||||
|
forwardTitleFmt = " [aqua::b]%s([fuchsia::b]%d[fuchsia::-])[aqua::-] "
|
||||||
|
)
|
||||||
|
|
||||||
|
type forwardView struct {
|
||||||
|
*tview.Pages
|
||||||
|
|
||||||
|
app *appView
|
||||||
|
current igniter
|
||||||
|
cancel context.CancelFunc
|
||||||
|
bench *benchmark
|
||||||
|
}
|
||||||
|
|
||||||
|
func newForwardView(app *appView) *forwardView {
|
||||||
|
v := forwardView{
|
||||||
|
Pages: tview.NewPages(),
|
||||||
|
app: app,
|
||||||
|
}
|
||||||
|
|
||||||
|
tv := newTableView(app, forwardTitle)
|
||||||
|
tv.SetBorderFocusColor(tcell.ColorDodgerBlue)
|
||||||
|
tv.SetSelectedStyle(tcell.ColorWhite, tcell.ColorDodgerBlue, tcell.AttrNone)
|
||||||
|
tv.colorerFn = forwardColorer
|
||||||
|
tv.currentNS = ""
|
||||||
|
v.AddPage("table", tv, true, true)
|
||||||
|
|
||||||
|
v.current = app.content.GetPrimitive("main").(igniter)
|
||||||
|
v.registerActions()
|
||||||
|
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init the view.
|
||||||
|
func (v *forwardView) init(context.Context, string) {
|
||||||
|
tv := v.getTV()
|
||||||
|
v.refresh()
|
||||||
|
tv.sortCol.index, tv.sortCol.asc = tv.nameColIndex()+4, true
|
||||||
|
tv.refresh()
|
||||||
|
tv.Select(1, 0)
|
||||||
|
v.app.SetFocus(tv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *forwardView) getTV() *tableView {
|
||||||
|
if vu, ok := v.GetPrimitive("table").(*tableView); ok {
|
||||||
|
return vu
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *forwardView) refresh() {
|
||||||
|
tv := v.getTV()
|
||||||
|
tv.update(v.hydrate())
|
||||||
|
v.app.SetFocus(tv)
|
||||||
|
tv.resetTitle()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *forwardView) registerActions() {
|
||||||
|
tv := v.getTV()
|
||||||
|
tv.actions[tcell.KeyCtrlB] = newKeyAction("Bench", v.benchCmd, true)
|
||||||
|
tv.actions[KeyAltB] = newKeyAction("Bench Stop", v.benchStopCmd, true)
|
||||||
|
tv.actions[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd, true)
|
||||||
|
tv.actions[KeySlash] = newKeyAction("Filter", tv.activateCmd, false)
|
||||||
|
tv.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false)
|
||||||
|
tv.actions[KeyShiftP] = newKeyAction("Sort Ports", v.sortColCmd(2, true), true)
|
||||||
|
tv.actions[KeyShiftU] = newKeyAction("Sort URL", v.sortColCmd(4, true), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *forwardView) getTitle() string {
|
||||||
|
return forwardTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *forwardView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
tv := v.getTV()
|
||||||
|
tv.sortCol.index, tv.sortCol.asc = tv.nameColIndex()+col, asc
|
||||||
|
v.refresh()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *forwardView) benchStopCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if v.bench != nil {
|
||||||
|
log.Debug().Msg(">>> Benchmark canceled!!")
|
||||||
|
v.app.flash().info("Benchmark canceled!")
|
||||||
|
v.bench.cancel()
|
||||||
|
v.bench = nil
|
||||||
|
}
|
||||||
|
v.app.statusReset()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *forwardView) benchCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
sel := v.getSelectedItem()
|
||||||
|
if sel == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.bench != nil {
|
||||||
|
v.app.flash().err(errors.New("Only one benchmark allowed at a time"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tv := v.getTV()
|
||||||
|
r, _ := tv.GetSelection()
|
||||||
|
url := strings.TrimSpace(tv.GetCell(r, 4).Text)
|
||||||
|
log.Debug().Msgf("Go Routines before %d", runtime.NumGoroutine())
|
||||||
|
cfg := benchConfig{
|
||||||
|
Method: "GET",
|
||||||
|
Path: sel,
|
||||||
|
URL: url,
|
||||||
|
C: 1,
|
||||||
|
N: 200,
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if v.bench, err = newBenchmark(cfg); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Bench failed!")
|
||||||
|
v.app.flash().errf("Bench failed %v", err)
|
||||||
|
v.app.statusReset()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
v.app.status(flashWarn, "Starting Benchmark...")
|
||||||
|
log.Debug().Msg("Bench starting...")
|
||||||
|
go v.bench.run(func() {
|
||||||
|
log.Debug().Msg("Bench Completed!")
|
||||||
|
v.app.QueueUpdate(func() {
|
||||||
|
v.bench = nil
|
||||||
|
v.app.flash().infof("Benchmark for %s is done!", sel)
|
||||||
|
v.app.status(flashInfo, "Benchmark Completed!")
|
||||||
|
go func() {
|
||||||
|
<-time.After(2 * time.Second)
|
||||||
|
v.app.QueueUpdate(func() {
|
||||||
|
v.app.statusReset()
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
log.Debug().Msgf("Go Routines after %d", runtime.NumGoroutine())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *forwardView) getSelectedItem() string {
|
||||||
|
tv := v.getTV()
|
||||||
|
r, _ := tv.GetSelection()
|
||||||
|
if r == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return fqn(strings.TrimSpace(tv.GetCell(r, 0).Text), strings.TrimSpace(tv.GetCell(r, 1).Text))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *forwardView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
tv := v.getTV()
|
||||||
|
if !tv.cmdBuff.empty() {
|
||||||
|
tv.cmdBuff.reset()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sel := v.getSelectedItem()
|
||||||
|
if sel == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
showModal(v.Pages, fmt.Sprintf("Deleting `%s are you sure?", sel), "table", func() {
|
||||||
|
index := -1
|
||||||
|
for i, f := range v.app.forwarders {
|
||||||
|
if sel == f.Path() {
|
||||||
|
index = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if index == -1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if index == 0 && len(v.app.forwarders) == 1 {
|
||||||
|
v.app.forwarders = []forwarder{}
|
||||||
|
} else {
|
||||||
|
v.app.forwarders = append(v.app.forwarders[:index], v.app.forwarders[index+1:]...)
|
||||||
|
}
|
||||||
|
v.getTV().update(v.hydrate())
|
||||||
|
v.app.flash().infof("PortForward %s deleted!", sel)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *forwardView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if v.cancel != nil {
|
||||||
|
v.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
tv := v.getTV()
|
||||||
|
if tv.cmdBuff.isActive() {
|
||||||
|
tv.cmdBuff.reset()
|
||||||
|
} else {
|
||||||
|
v.app.inject(v.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *forwardView) runCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
tv := v.getTV()
|
||||||
|
r, _ := tv.GetSelection()
|
||||||
|
if r > 0 {
|
||||||
|
v.app.gotoResource(strings.TrimSpace(tv.GetCell(r, 0).Text), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *forwardView) hints() hints {
|
||||||
|
return v.getTV().actions.toHints()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *forwardView) hydrate() resource.TableData {
|
||||||
|
cmds := helpCmds(v.app.conn())
|
||||||
|
|
||||||
|
data := resource.TableData{
|
||||||
|
Header: resource.Row{"NAMESPACE", "NAME", "PORTS", "ACTIVE", "URL", "AGE"},
|
||||||
|
Rows: make(resource.RowEvents, len(cmds)),
|
||||||
|
Namespace: resource.AllNamespaces,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range v.app.forwarders {
|
||||||
|
ports := strings.Split(f.Ports()[0], ":")
|
||||||
|
ns, n := namespaced(f.Path())
|
||||||
|
fields := resource.Row{
|
||||||
|
ns,
|
||||||
|
n,
|
||||||
|
strings.Join(f.Ports(), ","),
|
||||||
|
fmt.Sprintf("%t", f.Active()),
|
||||||
|
"http://localhost" + ":" + ports[0],
|
||||||
|
f.Age(),
|
||||||
|
}
|
||||||
|
data.Rows[f.Path()] = &resource.RowEvent{
|
||||||
|
Action: resource.New,
|
||||||
|
Fields: fields,
|
||||||
|
Deltas: fields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *forwardView) resetTitle() {
|
||||||
|
v.SetTitle(fmt.Sprintf(forwardTitleFmt, forwardTitle, v.getTV().GetRowCount()-1))
|
||||||
|
}
|
||||||
|
|
||||||
|
const genericPrompt = "prompt"
|
||||||
|
|
||||||
|
func showModal(pv *tview.Pages, msg, back string, ok func()) {
|
||||||
|
m := tview.NewModal().
|
||||||
|
AddButtons([]string{"Cancel", "OK"}).
|
||||||
|
SetTextColor(tcell.ColorFuchsia).
|
||||||
|
SetText(msg).
|
||||||
|
SetDoneFunc(func(_ int, b string) {
|
||||||
|
if b == "OK" {
|
||||||
|
ok()
|
||||||
|
}
|
||||||
|
dismissModal(pv, back)
|
||||||
|
})
|
||||||
|
m.SetTitle("<Confirm>")
|
||||||
|
pv.AddPage(genericPrompt, m, false, false)
|
||||||
|
pv.ShowPage(genericPrompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismissModal(pv *tview.Pages, page string) {
|
||||||
|
pv.RemovePage(genericPrompt)
|
||||||
|
pv.SwitchToPage(page)
|
||||||
|
}
|
||||||
|
|
@ -82,10 +82,10 @@ func (v *helpView) init(_ context.Context, _ string) {
|
||||||
navigation := []helpItem{
|
navigation := []helpItem{
|
||||||
{"g", "Goto Top"},
|
{"g", "Goto Top"},
|
||||||
{"G", "Goto Bottom"},
|
{"G", "Goto Bottom"},
|
||||||
{"b", "Page Down"},
|
{"Ctrl-b", "Page Down"},
|
||||||
{"f", "Page Up"},
|
{"Ctrl-f", "Page Up"},
|
||||||
{"l", "Left"},
|
{"h", "Left"},
|
||||||
{"h", "Right"},
|
{"l", "Right"},
|
||||||
{"k", "Up"},
|
{"k", "Up"},
|
||||||
{"j", "Down"},
|
{"j", "Down"},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,13 @@ const (
|
||||||
minusSign = "↓"
|
minusSign = "↓"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func fqn(ns, n string) string {
|
||||||
|
if ns == "" {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
return ns + "/" + n
|
||||||
|
}
|
||||||
|
|
||||||
func deltas(o, n string) string {
|
func deltas(o, n string) string {
|
||||||
o, n = strings.TrimSpace(o), strings.TrimSpace(n)
|
o, n = strings.TrimSpace(o), strings.TrimSpace(n)
|
||||||
if o == "" || o == res.NAValue {
|
if o == "" || o == res.NAValue {
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ func (v *jobView) viewLogs(previous bool) bool {
|
||||||
|
|
||||||
cc, err := fetchContainers(v.list, v.selectedItem, true)
|
cc, err := fetchContainers(v.list, v.selectedItem, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash().err(err)
|
||||||
log.Error().Err(err).Msgf("Unable to fetch containers for %s", v.selectedItem)
|
log.Error().Err(err).Msgf("Unable to fetch containers for %s", v.selectedItem)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -104,7 +104,7 @@ func (v *jobView) showLogs(path, co, view string, parent loggable, prev bool) {
|
||||||
|
|
||||||
func (v *jobView) extraActions(aa keyActions) {
|
func (v *jobView) extraActions(aa keyActions) {
|
||||||
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
|
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
|
||||||
aa[KeyShiftL] = newKeyAction("Prev Logs", v.prevLogsCmd, true)
|
aa[KeyShiftL] = newKeyAction("Logs Previous", v.prevLogsCmd, true)
|
||||||
aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true)
|
aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,7 +118,7 @@ func (v *jobView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
job, err := j.Get(ns, n)
|
job, err := j.Get(ns, n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Fetching Job %s", v.selectedItem)
|
log.Error().Err(err).Msgf("Fetching Job %s", v.selectedItem)
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash().err(err)
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
jo := job.(*batchv1.Job)
|
jo := job.(*batchv1.Job)
|
||||||
|
|
@ -126,7 +126,7 @@ func (v *jobView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
sel, err := metav1.LabelSelectorAsSelector(jo.Spec.Selector)
|
sel, err := metav1.LabelSelectorAsSelector(jo.Spec.Selector)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Converting selector for Job %s", v.selectedItem)
|
log.Error().Err(err).Msgf("Converting selector for Job %s", v.selectedItem)
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash().err(err)
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
showPods(v.app, "", "Job", v.selectedItem, sel.String(), "", v.backCmd)
|
showPods(v.app, "", "Job", v.selectedItem, sel.String(), "", v.backCmd)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
@ -27,12 +28,15 @@ func newLogView(title string, parent masterView) *logView {
|
||||||
v.autoScroll = true
|
v.autoScroll = true
|
||||||
v.parent = parent
|
v.parent = parent
|
||||||
v.SetBorder(true)
|
v.SetBorder(true)
|
||||||
|
v.SetBackgroundColor(config.AsColor(parent.appView().styles.Style.Log.BgColor))
|
||||||
v.SetBorderPadding(0, 0, 1, 1)
|
v.SetBorderPadding(0, 0, 1, 1)
|
||||||
v.logs = newDetailsView(parent.appView(), parent.backFn())
|
v.logs = newDetailsView(parent.appView(), parent.backFn())
|
||||||
{
|
{
|
||||||
v.logs.SetBorder(false)
|
v.logs.SetBorder(false)
|
||||||
v.logs.setCategory("Logs")
|
v.logs.setCategory("Logs")
|
||||||
v.logs.SetDynamicColors(true)
|
v.logs.SetDynamicColors(true)
|
||||||
|
v.logs.SetTextColor(config.AsColor(parent.appView().styles.Style.Log.FgColor))
|
||||||
|
v.logs.SetBackgroundColor(config.AsColor(parent.appView().styles.Style.Log.BgColor))
|
||||||
v.logs.SetWrap(true)
|
v.logs.SetWrap(true)
|
||||||
v.logs.SetMaxBuffer(parent.appView().config.K9s.LogBufferSize)
|
v.logs.SetMaxBuffer(parent.appView().config.K9s.LogBufferSize)
|
||||||
}
|
}
|
||||||
|
|
@ -110,11 +114,11 @@ func (v *logView) update() {
|
||||||
func (v *logView) toggleScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (v *logView) toggleScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
v.autoScroll = !v.autoScroll
|
v.autoScroll = !v.autoScroll
|
||||||
if v.autoScroll {
|
if v.autoScroll {
|
||||||
v.app.flash(flashInfo, "Autoscroll is on.")
|
v.app.flash().info("Autoscroll is on.")
|
||||||
v.logs.ScrollToEnd()
|
v.logs.ScrollToEnd()
|
||||||
} else {
|
} else {
|
||||||
v.logs.PageUp()
|
v.logs.PageUp()
|
||||||
v.app.flash(flashInfo, "Autoscroll is off.")
|
v.app.flash().info("Autoscroll is off.")
|
||||||
}
|
}
|
||||||
v.update()
|
v.update()
|
||||||
|
|
||||||
|
|
@ -126,33 +130,33 @@ func (v *logView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *logView) topCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (v *logView) topCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
v.app.flash(flashInfo, "Top of logs...")
|
v.app.flash().info("Top of logs...")
|
||||||
v.logs.ScrollToBeginning()
|
v.logs.ScrollToBeginning()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *logView) bottomCmd(*tcell.EventKey) *tcell.EventKey {
|
func (v *logView) bottomCmd(*tcell.EventKey) *tcell.EventKey {
|
||||||
v.app.flash(flashInfo, "Bottom of logs...")
|
v.app.flash().info("Bottom of logs...")
|
||||||
v.logs.ScrollToEnd()
|
v.logs.ScrollToEnd()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *logView) pageUpCmd(*tcell.EventKey) *tcell.EventKey {
|
func (v *logView) pageUpCmd(*tcell.EventKey) *tcell.EventKey {
|
||||||
if v.logs.PageUp() {
|
if v.logs.PageUp() {
|
||||||
v.app.flash(flashInfo, "Reached Top ...")
|
v.app.flash().info("Reached Top ...")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *logView) pageDownCmd(*tcell.EventKey) *tcell.EventKey {
|
func (v *logView) pageDownCmd(*tcell.EventKey) *tcell.EventKey {
|
||||||
if v.logs.PageDown() {
|
if v.logs.PageDown() {
|
||||||
v.app.flash(flashInfo, "Reached Bottom ...")
|
v.app.flash().info("Reached Bottom ...")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *logView) clearCmd(*tcell.EventKey) *tcell.EventKey {
|
func (v *logView) clearCmd(*tcell.EventKey) *tcell.EventKey {
|
||||||
v.app.flash(flashInfo, "Clearing logs...")
|
v.app.flash().info("Clearing logs...")
|
||||||
v.logs.Clear()
|
v.logs.Clear()
|
||||||
v.logs.ScrollTo(0, 0)
|
v.logs.ScrollTo(0, 0)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
"github.com/derailed/tview"
|
||||||
|
)
|
||||||
|
|
||||||
|
type logoView struct {
|
||||||
|
*tview.Flex
|
||||||
|
logo, status *tview.TextView
|
||||||
|
app *appView
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLogoView(app *appView) *logoView {
|
||||||
|
v := logoView{
|
||||||
|
Flex: tview.NewFlex(),
|
||||||
|
logo: logo(),
|
||||||
|
status: status(),
|
||||||
|
app: app,
|
||||||
|
}
|
||||||
|
v.SetDirection(tview.FlexRow)
|
||||||
|
v.AddItem(v.logo, 0, 6, false)
|
||||||
|
v.AddItem(v.status, 0, 1, false)
|
||||||
|
v.refreshLogo(app.styles.Style.LogoColor)
|
||||||
|
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *logoView) reset() {
|
||||||
|
v.status.Clear()
|
||||||
|
v.status.SetBackgroundColor(v.app.styles.BgColor())
|
||||||
|
v.refreshLogo(v.app.styles.Style.LogoColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *logoView) warn(msg string) {
|
||||||
|
v.update(msg, "red")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *logoView) info(msg string) {
|
||||||
|
v.update(msg, "green")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *logoView) update(msg, c string) {
|
||||||
|
v.refreshStatus(msg, c)
|
||||||
|
v.refreshLogo(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *logoView) refreshStatus(msg, c string) {
|
||||||
|
v.status.SetBackgroundColor(config.AsColor(c))
|
||||||
|
v.status.SetText(fmt.Sprintf("[white::b]%s", msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *logoView) refreshLogo(c string) {
|
||||||
|
v.logo.Clear()
|
||||||
|
for i, s := range LogoSmall {
|
||||||
|
fmt.Fprintf(v.logo, "[%s::b]%s", c, s)
|
||||||
|
if i+1 < len(LogoSmall) {
|
||||||
|
fmt.Fprintf(v.logo, "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func logo() *tview.TextView {
|
||||||
|
v := tview.NewTextView()
|
||||||
|
v.SetWordWrap(false)
|
||||||
|
v.SetWrap(false)
|
||||||
|
v.SetTextAlign(tview.AlignLeft)
|
||||||
|
v.SetDynamicColors(true)
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func status() *tview.TextView {
|
||||||
|
v := tview.NewTextView()
|
||||||
|
v.SetWordWrap(false)
|
||||||
|
v.SetWrap(false)
|
||||||
|
v.SetTextAlign(tview.AlignCenter)
|
||||||
|
v.SetDynamicColors(true)
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,6 @@ package views
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/resource"
|
"github.com/derailed/k9s/internal/resource"
|
||||||
|
|
@ -18,6 +17,8 @@ const (
|
||||||
maxCleanse = 100
|
maxCleanse = 100
|
||||||
logBuffSize = 100
|
logBuffSize = 100
|
||||||
flushTimeout = 200 * time.Millisecond
|
flushTimeout = 200 * time.Millisecond
|
||||||
|
|
||||||
|
logFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:bg:-]) "
|
||||||
)
|
)
|
||||||
|
|
||||||
type masterView interface {
|
type masterView interface {
|
||||||
|
|
@ -104,7 +105,7 @@ func (v *logsView) load(i int) {
|
||||||
}
|
}
|
||||||
v.SwitchToPage(v.containers[i])
|
v.SwitchToPage(v.containers[i])
|
||||||
if err := v.doLoad(v.parent.getSelection(), v.containers[i]); err != nil {
|
if err := v.doLoad(v.parent.getSelection(), v.containers[i]); err != nil {
|
||||||
v.parent.appView().flash(flashErr, err.Error())
|
v.parent.appView().flash().err(err)
|
||||||
l := v.CurrentPage().Item.(*logView)
|
l := v.CurrentPage().Item.(*logView)
|
||||||
l.logLine("😂 Doh! No logs are available at this time. Check again later on...")
|
l.logLine("😂 Doh! No logs are available at this time. Check again later on...")
|
||||||
return
|
return
|
||||||
|
|
@ -118,10 +119,7 @@ func (v *logsView) doLoad(path, co string) error {
|
||||||
maxBuff := int64(v.parent.appView().config.K9s.LogRequestSize)
|
maxBuff := int64(v.parent.appView().config.K9s.LogRequestSize)
|
||||||
l := v.CurrentPage().Item.(*logView)
|
l := v.CurrentPage().Item.(*logView)
|
||||||
l.logs.Clear()
|
l.logs.Clear()
|
||||||
const logFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:-:-]) "
|
fmat := skinTitle(fmt.Sprintf(logFmt, path, co), v.parent.appView().styles.Style)
|
||||||
fmat := fmt.Sprintf(logFmt, path, co)
|
|
||||||
fmat = strings.Replace(fmat, "[fg:bg", "["+v.parent.appView().styles.Style.Title.FgColor+":"+v.parent.appView().styles.Style.Title.BgColor, -1)
|
|
||||||
fmat = strings.Replace(fmat, "[hilite", "["+v.parent.appView().styles.Style.Title.HighlightColor, 1)
|
|
||||||
l.SetTitle(fmat)
|
l.SetTitle(fmat)
|
||||||
|
|
||||||
c := make(chan string, 10)
|
c := make(chan string, 10)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package views
|
package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
|
@ -60,9 +59,9 @@ func (v *namespaceView) useNsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
||||||
func (v *namespaceView) useNamespace(name string) {
|
func (v *namespaceView) useNamespace(name string) {
|
||||||
if err := v.app.config.SetActiveNamespace(name); err != nil {
|
if err := v.app.config.SetActiveNamespace(name); err != nil {
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash().err(err)
|
||||||
} else {
|
} else {
|
||||||
v.app.flash(flashInfo, fmt.Sprintf("Namespace %s is now active!", name))
|
v.app.flash().infof("Namespace %s is now active!", name)
|
||||||
}
|
}
|
||||||
v.app.config.Save()
|
v.app.config.Save()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,18 @@ package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/resource"
|
"github.com/derailed/k9s/internal/resource"
|
||||||
|
"k8s.io/apimachinery/pkg/util/duration"
|
||||||
)
|
)
|
||||||
|
|
||||||
type maxyPad []int
|
type maxyPad []int
|
||||||
|
|
||||||
func computeMaxColumns(pads maxyPad, sortCol int, table resource.TableData) {
|
func computeMaxColumns(pads maxyPad, sortCol int, table resource.TableData) {
|
||||||
|
const colPadding = 1
|
||||||
|
|
||||||
for index, h := range table.Header {
|
for index, h := range table.Header {
|
||||||
pads[index] = len(h)
|
pads[index] = len(h)
|
||||||
if index == sortCol {
|
if index == sortCol {
|
||||||
|
|
@ -18,10 +22,20 @@ func computeMaxColumns(pads maxyPad, sortCol int, table resource.TableData) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var row int
|
var row int
|
||||||
for _, rev := range table.Rows {
|
for k, rev := range table.Rows {
|
||||||
|
ageIndex := len(rev.Fields) - 1
|
||||||
for index, field := range rev.Fields {
|
for index, field := range rev.Fields {
|
||||||
if len(field) > pads[index] {
|
// Date field comes out as timestamp.
|
||||||
pads[index] = len([]rune(field))
|
if index == ageIndex {
|
||||||
|
dur, err := time.ParseDuration(field)
|
||||||
|
if err == nil {
|
||||||
|
field = duration.HumanDuration(dur)
|
||||||
|
}
|
||||||
|
table.Rows[k].Fields[index] = field
|
||||||
|
}
|
||||||
|
width := len(field) + colPadding
|
||||||
|
if width > pads[index] {
|
||||||
|
pads[index] = width
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
row++
|
row++
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ func TestMaxColumn(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
maxyPad{5, 5},
|
maxyPad{6, 6},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resource.TableData{
|
resource.TableData{
|
||||||
|
|
@ -33,7 +33,7 @@ func TestMaxColumn(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
1,
|
1,
|
||||||
maxyPad{5, 5},
|
maxyPad{6, 6},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resource.TableData{
|
resource.TableData{
|
||||||
|
|
@ -44,7 +44,7 @@ func TestMaxColumn(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
maxyPad{28, 5},
|
maxyPad{32, 6},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package views
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/k8s"
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
"github.com/derailed/k9s/internal/resource"
|
"github.com/derailed/k9s/internal/resource"
|
||||||
|
|
@ -56,22 +55,18 @@ func (v *podView) listContainers(app *appView, _, res, sel string) {
|
||||||
po, err := v.app.informer.Get(watch.PodIndex, sel, metav1.GetOptions{})
|
po, err := v.app.informer.Get(watch.PodIndex, sel, metav1.GetOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Unable to retrieve pod %s", sel)
|
log.Error().Err(err).Msgf("Unable to retrieve pod %s", sel)
|
||||||
app.flash(flashErr, err.Error())
|
app.flash().errf("Unable to retrieve pods %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pod := po.(*v1.Pod)
|
pod := po.(*v1.Pod)
|
||||||
mx := k8s.NewMetricsServer(app.conn())
|
mx := k8s.NewMetricsServer(app.conn())
|
||||||
list := resource.NewContainerList(app.conn(), mx, pod)
|
list := resource.NewContainerList(app.conn(), mx, pod)
|
||||||
log.Debug().Msgf(">>>> Got pod %s", pod.Name)
|
title := skinTitle(fmt.Sprintf(containerFmt, "Containers", sel), v.app.styles.Style)
|
||||||
|
|
||||||
fmat := strings.Replace(containerFmt, "[fg:bg", "["+v.app.styles.Style.Title.FgColor+":"+v.app.styles.Style.Title.BgColor, -1)
|
|
||||||
fmat = strings.Replace(fmat, "[hilite", "["+v.app.styles.Style.Title.CounterColor, 1)
|
|
||||||
title := fmt.Sprintf(fmat, "Containers", sel)
|
|
||||||
|
|
||||||
v.suspend()
|
v.suspend()
|
||||||
cv := newContainerView(title, app, list, namespacedName(pod.Namespace, pod.Name), v.exitFn)
|
cv := newContainerView(title, app, list, fqn(pod.Namespace, pod.Name), v.exitFn)
|
||||||
v.AddPage("containers", cv, true, true)
|
v.AddPage("containers", cv, true, true)
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(v.context)
|
||||||
v.cancel = cancel
|
v.cancel = cancel
|
||||||
cv.init(ctx, pod.Namespace)
|
cv.init(ctx, pod.Namespace)
|
||||||
}
|
}
|
||||||
|
|
@ -125,7 +120,7 @@ func (v *podView) viewLogs(prev bool) bool {
|
||||||
}
|
}
|
||||||
cc, err := fetchContainers(v.list, v.selectedItem, true)
|
cc, err := fetchContainers(v.list, v.selectedItem, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash().errf("Unable to retrieve containers %s", err)
|
||||||
log.Error().Err(err)
|
log.Error().Err(err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +150,7 @@ func (v *podView) shellCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
}
|
}
|
||||||
cc, err := fetchContainers(v.list, v.selectedItem, false)
|
cc, err := fetchContainers(v.list, v.selectedItem, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash().errf("Unable to retrieve containers %s", err)
|
||||||
log.Error().Msgf("Error fetching containers %v", err)
|
log.Error().Msgf("Error fetching containers %v", err)
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
@ -176,33 +171,46 @@ func (v *podView) shellCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
func (v *podView) shellIn(path, co string) {
|
func (v *podView) shellIn(path, co string) {
|
||||||
v.suspend()
|
v.suspend()
|
||||||
{
|
{
|
||||||
ns, po := namespaced(path)
|
shellIn(v.app, path, co)
|
||||||
args := make([]string, 0, 12)
|
|
||||||
args = append(args, "exec", "-it")
|
|
||||||
args = append(args, "--context", v.app.config.K9s.CurrentContext)
|
|
||||||
args = append(args, "-n", ns)
|
|
||||||
args = append(args, po)
|
|
||||||
if len(co) != 0 {
|
|
||||||
args = append(args, "-c", co)
|
|
||||||
}
|
|
||||||
args = append(args, "--", "sh")
|
|
||||||
log.Debug().Msgf("Shell args %v", args)
|
|
||||||
runK(true, v.app, args...)
|
|
||||||
}
|
}
|
||||||
v.resume()
|
v.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shellIn(a *appView, path, co string) {
|
||||||
|
args := computeShellArgs(path, co, a.config.K9s.CurrentContext, a.conn().Config().Flags().KubeConfig)
|
||||||
|
log.Debug().Msgf("Shell args %v", args)
|
||||||
|
runK(true, a, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeShellArgs(path, co, context string, cfg *string) []string {
|
||||||
|
a := make([]string, 0, 15)
|
||||||
|
a = append(a, "exec", "-it")
|
||||||
|
a = append(a, "--context", context)
|
||||||
|
ns, po := namespaced(path)
|
||||||
|
a = append(a, "-n", ns)
|
||||||
|
a = append(a, po)
|
||||||
|
if cfg != nil && *cfg != "" {
|
||||||
|
a = append(a, "--kubeconfig", *cfg)
|
||||||
|
}
|
||||||
|
if co != "" {
|
||||||
|
a = append(a, "-c", co)
|
||||||
|
}
|
||||||
|
a = append(a, "--", "sh")
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
func (v *podView) extraActions(aa keyActions) {
|
func (v *podView) extraActions(aa keyActions) {
|
||||||
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
|
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
|
||||||
aa[KeyShiftL] = newKeyAction("Prev Logs", v.prevLogsCmd, true)
|
aa[KeyShiftL] = newKeyAction("Logs Previous", v.prevLogsCmd, true)
|
||||||
aa[KeyS] = newKeyAction("Shell", v.shellCmd, true)
|
aa[KeyS] = newKeyAction("Shell", v.shellCmd, true)
|
||||||
aa[KeyShiftR] = newKeyAction("Sort Ready", v.sortColCmd(1, false), true)
|
aa[KeyShiftR] = newKeyAction("Sort Ready", v.sortColCmd(1, false), true)
|
||||||
aa[KeyShiftS] = newKeyAction("Sort Status", v.sortColCmd(2, true), true)
|
aa[KeyShiftS] = newKeyAction("Sort Status", v.sortColCmd(2, true), true)
|
||||||
aa[KeyShiftT] = newKeyAction("Sort Restart", v.sortColCmd(3, false), true)
|
aa[KeyShiftT] = newKeyAction("Sort Restart", v.sortColCmd(3, false), true)
|
||||||
aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(4, false), true)
|
aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(4, false), true)
|
||||||
aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(5, false), true)
|
aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(5, false), true)
|
||||||
aa[KeyAltC] = newKeyAction("Sort %CPU", v.sortColCmd(6, false), true)
|
aa[KeyAltC] = newKeyAction("Sort CPU%", v.sortColCmd(6, false), true)
|
||||||
aa[KeyAltM] = newKeyAction("Sort %MEM", v.sortColCmd(7, false), true)
|
aa[KeyAltM] = newKeyAction("Sort MEM%", v.sortColCmd(7, false), true)
|
||||||
aa[KeyShiftO] = newKeyAction("Sort Node", v.sortColCmd(8, true), true)
|
aa[KeyShiftO] = newKeyAction("Sort Node", v.sortColCmd(8, true), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestComputeShellArgs(t *testing.T) {
|
||||||
|
config, empty := "coolConfig", ""
|
||||||
|
uu := map[string]struct {
|
||||||
|
path, co, context string
|
||||||
|
cfg *string
|
||||||
|
e string
|
||||||
|
}{
|
||||||
|
"config": {
|
||||||
|
"fred/blee",
|
||||||
|
"c1",
|
||||||
|
"ctx1",
|
||||||
|
&config,
|
||||||
|
"exec -it --context ctx1 -n fred blee --kubeconfig coolConfig -c c1 -- sh",
|
||||||
|
},
|
||||||
|
"noconfig": {
|
||||||
|
"fred/blee",
|
||||||
|
"c1",
|
||||||
|
"ctx1",
|
||||||
|
nil,
|
||||||
|
"exec -it --context ctx1 -n fred blee -c c1 -- sh",
|
||||||
|
},
|
||||||
|
"emptyConfig": {
|
||||||
|
"fred/blee",
|
||||||
|
"c1",
|
||||||
|
"ctx1",
|
||||||
|
&empty,
|
||||||
|
"exec -it --context ctx1 -n fred blee -c c1 -- sh",
|
||||||
|
},
|
||||||
|
"singleContainer": {
|
||||||
|
"fred/blee",
|
||||||
|
"",
|
||||||
|
"ctx1",
|
||||||
|
&empty,
|
||||||
|
"exec -it --context ctx1 -n fred blee -- sh",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, u := range uu {
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
args := computeShellArgs(u.path, u.co, u.context, u.cfg)
|
||||||
|
|
||||||
|
assert.Equal(t, u.e, strings.Join(args, " "))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -223,10 +223,6 @@ func (v *policyView) namespacePolicies() (resource.RowEvents, []error) {
|
||||||
return evts, errs
|
return evts, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
func namespacedName(ns, n string) string {
|
|
||||||
return ns + "/" + n
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *policyView) parseRules(ns, binding string, rules []rbacv1.PolicyRule) resource.RowEvents {
|
func (v *policyView) parseRules(ns, binding string, rules []rbacv1.PolicyRule) resource.RowEvents {
|
||||||
m := make(resource.RowEvents, len(rules))
|
m := make(resource.RowEvents, len(rules))
|
||||||
for _, r := range rules {
|
for _, r := range rules {
|
||||||
|
|
@ -237,12 +233,12 @@ func (v *policyView) parseRules(ns, binding string, rules []rbacv1.PolicyRule) r
|
||||||
k = res + "." + grp
|
k = res + "." + grp
|
||||||
}
|
}
|
||||||
for _, na := range r.ResourceNames {
|
for _, na := range r.ResourceNames {
|
||||||
n := k + "/" + na
|
n := fqn(k, na)
|
||||||
m[namespacedName(ns, n)] = &resource.RowEvent{
|
m[fqn(ns, n)] = &resource.RowEvent{
|
||||||
Fields: v.prepRow(ns, n, grp, binding, r.Verbs),
|
Fields: v.prepRow(ns, n, grp, binding, r.Verbs),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m[namespacedName(ns, k)] = &resource.RowEvent{
|
m[fqn(ns, k)] = &resource.RowEvent{
|
||||||
Fields: v.prepRow(ns, k, grp, binding, r.Verbs),
|
Fields: v.prepRow(ns, k, grp, binding, r.Verbs),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -251,7 +247,7 @@ func (v *policyView) parseRules(ns, binding string, rules []rbacv1.PolicyRule) r
|
||||||
if nres[0] != '/' {
|
if nres[0] != '/' {
|
||||||
nres = "/" + nres
|
nres = "/" + nres
|
||||||
}
|
}
|
||||||
m[namespacedName(ns, nres)] = &resource.RowEvent{
|
m[fqn(ns, nres)] = &resource.RowEvent{
|
||||||
Fields: v.prepRow(ns, nres, resource.NAValue, binding, r.Verbs),
|
Fields: v.prepRow(ns, nres, resource.NAValue, binding, r.Verbs),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,11 +126,7 @@ func (v *rbacView) bindKeys() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *rbacView) getTitle() string {
|
func (v *rbacView) getTitle() string {
|
||||||
fmat := strings.Replace(rbacTitleFmt, "[fg", "["+v.app.styles.Style.Title.FgColor, -1)
|
return skinTitle(fmt.Sprintf(rbacTitleFmt, rbacTitle, v.roleName), v.app.styles.Style)
|
||||||
fmat = strings.Replace(fmat, ":bg:", ":"+v.app.styles.Style.Title.BgColor+":", -1)
|
|
||||||
fmat = strings.Replace(fmat, "[hilite", "["+v.app.styles.Style.Title.HighlightColor, 1)
|
|
||||||
|
|
||||||
return fmt.Sprintf(fmat, rbacTitle, v.roleName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *rbacView) hints() hints {
|
func (v *rbacView) hints() hints {
|
||||||
|
|
@ -244,7 +240,7 @@ func (v *rbacView) parseRules(rules []rbacv1.PolicyRule) resource.RowEvents {
|
||||||
k = res + "." + grp
|
k = res + "." + grp
|
||||||
}
|
}
|
||||||
for _, na := range r.ResourceNames {
|
for _, na := range r.ResourceNames {
|
||||||
n := k + "/" + na
|
n := fqn(k, na)
|
||||||
m[n] = &resource.RowEvent{
|
m[n] = &resource.RowEvent{
|
||||||
Fields: prepRow(n, grp, r.Verbs),
|
Fields: prepRow(n, grp, r.Verbs),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ func showRBAC(app *appView, ns, resource, selection string) {
|
||||||
func showClusterRole(app *appView, ns, resource, selection string) {
|
func showClusterRole(app *appView, ns, resource, selection string) {
|
||||||
crb, err := app.conn().DialOrDie().Rbac().ClusterRoleBindings().Get(selection, metav1.GetOptions{})
|
crb, err := app.conn().DialOrDie().Rbac().ClusterRoleBindings().Get(selection, metav1.GetOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.flash(flashErr, "Unable to retrieve crb", selection)
|
app.flash().errf("Unable to retrieve clusterrolebindings for %s", selection)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.inject(newRBACView(app, ns, crb.RoleRef.Name, clusterRole))
|
app.inject(newRBACView(app, ns, crb.RoleRef.Name, clusterRole))
|
||||||
|
|
@ -96,10 +96,10 @@ func showRole(app *appView, _, resource, selection string) {
|
||||||
ns, n := namespaced(selection)
|
ns, n := namespaced(selection)
|
||||||
rb, err := app.conn().DialOrDie().Rbac().RoleBindings(ns).Get(n, metav1.GetOptions{})
|
rb, err := app.conn().DialOrDie().Rbac().RoleBindings(ns).Get(n, metav1.GetOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.flash(flashErr, "Unable to retrieve rb", selection)
|
app.flash().errf("Unable to retrieve rolebindings for %s", selection)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.inject(newRBACView(app, ns, namespacedName(ns, rb.RoleRef.Name), role))
|
app.inject(newRBACView(app, ns, fqn(ns, rb.RoleRef.Name), role))
|
||||||
}
|
}
|
||||||
|
|
||||||
func showSAPolicy(app *appView, _, _, selection string) {
|
func showSAPolicy(app *appView, _, _, selection string) {
|
||||||
|
|
@ -280,7 +280,7 @@ func resourceViews(c k8s.Connection) map[string]resCmd {
|
||||||
"svc": {
|
"svc": {
|
||||||
title: "Services",
|
title: "Services",
|
||||||
api: "",
|
api: "",
|
||||||
viewFn: newResourceView,
|
viewFn: newSvcView,
|
||||||
listFn: resource.NewServiceList,
|
listFn: resource.NewServiceList,
|
||||||
},
|
},
|
||||||
"usr": {
|
"usr": {
|
||||||
|
|
@ -307,7 +307,6 @@ func resourceViews(c k8s.Connection) map[string]resCmd {
|
||||||
|
|
||||||
switch rev {
|
switch rev {
|
||||||
case "v1":
|
case "v1":
|
||||||
log.Debug().Msg("Using HPA V1!")
|
|
||||||
cmds["hpa"] = resCmd{
|
cmds["hpa"] = resCmd{
|
||||||
title: "HorizontalPodAutoscalers",
|
title: "HorizontalPodAutoscalers",
|
||||||
api: "autoscaling",
|
api: "autoscaling",
|
||||||
|
|
@ -315,7 +314,6 @@ func resourceViews(c k8s.Connection) map[string]resCmd {
|
||||||
listFn: resource.NewHorizontalPodAutoscalerV1List,
|
listFn: resource.NewHorizontalPodAutoscalerV1List,
|
||||||
}
|
}
|
||||||
case "v2beta1":
|
case "v2beta1":
|
||||||
log.Debug().Msg("Using HPA V2Beta1!")
|
|
||||||
cmds["hpa"] = resCmd{
|
cmds["hpa"] = resCmd{
|
||||||
title: "HorizontalPodAutoscalers",
|
title: "HorizontalPodAutoscalers",
|
||||||
api: "autoscaling",
|
api: "autoscaling",
|
||||||
|
|
@ -323,7 +321,6 @@ func resourceViews(c k8s.Connection) map[string]resCmd {
|
||||||
listFn: resource.NewHorizontalPodAutoscalerV2Beta1List,
|
listFn: resource.NewHorizontalPodAutoscalerV2Beta1List,
|
||||||
}
|
}
|
||||||
case "v2beta2":
|
case "v2beta2":
|
||||||
log.Debug().Msg("Using HPA V2Beta2!")
|
|
||||||
cmds["hpa"] = resCmd{
|
cmds["hpa"] = resCmd{
|
||||||
title: "HorizontalPodAutoscalers",
|
title: "HorizontalPodAutoscalers",
|
||||||
api: "autoscaling",
|
api: "autoscaling",
|
||||||
|
|
|
||||||
|
|
@ -22,16 +22,7 @@ const (
|
||||||
clusterRefresh = time.Duration(15 * time.Second)
|
clusterRefresh = time.Duration(15 * time.Second)
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type resourceView struct {
|
||||||
details interface {
|
|
||||||
tview.Primitive
|
|
||||||
setTitle(string)
|
|
||||||
clear()
|
|
||||||
setActions(keyActions)
|
|
||||||
update(resource.Properties)
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceView struct {
|
|
||||||
*tview.Pages
|
*tview.Pages
|
||||||
|
|
||||||
app *appView
|
app *appView
|
||||||
|
|
@ -51,8 +42,8 @@ type (
|
||||||
suspended bool
|
suspended bool
|
||||||
nsListAccess bool
|
nsListAccess bool
|
||||||
path *string
|
path *string
|
||||||
|
context context.Context
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
func newResourceView(title string, app *appView, list resource.List) resourceViewer {
|
func newResourceView(title string, app *appView, list resource.List) resourceViewer {
|
||||||
v := resourceView{
|
v := resourceView{
|
||||||
|
|
@ -78,7 +69,7 @@ func newResourceView(title string, app *appView, list resource.List) resourceVie
|
||||||
|
|
||||||
// Init watches all running pods in given namespace
|
// Init watches all running pods in given namespace
|
||||||
func (v *resourceView) init(ctx context.Context, ns string) {
|
func (v *resourceView) init(ctx context.Context, ns string) {
|
||||||
v.selectedItem, v.selectedNS = noSelection, ns
|
v.context, v.selectedItem, v.selectedNS = ctx, noSelection, ns
|
||||||
|
|
||||||
colorer := defaultColorer
|
colorer := defaultColorer
|
||||||
if v.colorerFn != nil {
|
if v.colorerFn != nil {
|
||||||
|
|
@ -91,7 +82,7 @@ func (v *resourceView) init(ctx context.Context, ns string) {
|
||||||
nn, err := k8s.NewNamespace(v.app.conn()).List(resource.AllNamespaces)
|
nn, err := k8s.NewNamespace(v.app.conn()).List(resource.AllNamespaces)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("List namespaces")
|
log.Warn().Err(err).Msg("List namespaces")
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash().errf("Unable to list namespaces %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.list.Namespaced() && !v.list.AllNamespaces() {
|
if v.list.Namespaced() && !v.list.AllNamespaces() {
|
||||||
|
|
@ -232,17 +223,17 @@ func (v *resourceView) enterCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
if v.getTV().filterCmd(evt) == nil {
|
if v.getTV().filterCmd(evt) == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.enterFn != nil {
|
if v.enterFn != nil {
|
||||||
v.enterFn(v.app, v.list.GetNamespace(), v.list.GetName(), v.selectedItem)
|
v.enterFn(v.app, v.list.GetNamespace(), v.list.GetName(), v.selectedItem)
|
||||||
} else {
|
} else {
|
||||||
v.defaultEnter(v.list.GetNamespace(), v.list.GetName(), v.selectedItem)
|
v.defaultEnter(v.list.GetNamespace(), v.list.GetName(), v.selectedItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *resourceView) refreshCmd(*tcell.EventKey) *tcell.EventKey {
|
func (v *resourceView) refreshCmd(*tcell.EventKey) *tcell.EventKey {
|
||||||
v.app.flash(flashInfo, "Refreshing...")
|
v.app.flash().info("Refreshing...")
|
||||||
v.refresh()
|
v.refresh()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -261,9 +252,9 @@ func (v *resourceView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
v.showModal(fmt.Sprintf("Delete %s %s?", v.list.GetName(), sel), func(_ int, button string) {
|
v.showModal(fmt.Sprintf("Delete %s %s?", v.list.GetName(), sel), func(_ int, button string) {
|
||||||
if button == "OK" {
|
if button == "OK" {
|
||||||
v.getTV().setDeleted()
|
v.getTV().setDeleted()
|
||||||
v.app.flash(flashInfo, fmt.Sprintf("Deleting %s %s", v.list.GetName(), sel))
|
v.app.flash().infof("Deleting %s %s", v.list.GetName(), sel)
|
||||||
if err := v.list.Resource().Delete(sel); err != nil {
|
if err := v.list.Resource().Delete(sel); err != nil {
|
||||||
v.app.flash(flashErr, "Boom!", err.Error())
|
v.app.flash().errf("Delete failed with %s", err)
|
||||||
} else {
|
} else {
|
||||||
v.refresh()
|
v.refresh()
|
||||||
}
|
}
|
||||||
|
|
@ -290,10 +281,9 @@ func (v *resourceView) dismissModal() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *resourceView) defaultEnter(ns, resource, selection string) {
|
func (v *resourceView) defaultEnter(ns, resource, selection string) {
|
||||||
log.Debug().Msgf("Title %s", v.title)
|
|
||||||
yaml, err := v.list.Resource().Describe(v.title, selection, v.app.flags)
|
yaml, err := v.list.Resource().Describe(v.title, selection, v.app.flags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash().errf("Describe command failed %s", err)
|
||||||
log.Warn().Msgf("Describe %v", err.Error())
|
log.Warn().Msgf("Describe %v", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -328,7 +318,7 @@ func (v *resourceView) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
sel := v.getSelectedItem()
|
sel := v.getSelectedItem()
|
||||||
raw, err := v.list.Resource().Marshal(sel)
|
raw, err := v.list.Resource().Marshal(sel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.app.flash(flashErr, "Unable to marshal resource", err.Error())
|
v.app.flash().errf("Unable to marshal resource %s", err)
|
||||||
log.Error().Err(err)
|
log.Error().Err(err)
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
@ -375,7 +365,7 @@ func (v *resourceView) doSwitchNamespace(ns string) {
|
||||||
ns = resource.AllNamespace
|
ns = resource.AllNamespace
|
||||||
}
|
}
|
||||||
v.selectedNS = ns
|
v.selectedNS = ns
|
||||||
v.app.flash(flashInfo, fmt.Sprintf("Viewing `%s namespace...", ns))
|
v.app.flash().infof("Viewing `%s namespace...", ns)
|
||||||
v.list.SetNamespace(v.selectedNS)
|
v.list.SetNamespace(v.selectedNS)
|
||||||
|
|
||||||
v.refresh()
|
v.refresh()
|
||||||
|
|
@ -392,14 +382,14 @@ func (v *resourceView) refresh() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v.refreshActions()
|
||||||
|
|
||||||
if v.list.Namespaced() {
|
if v.list.Namespaced() {
|
||||||
v.list.SetNamespace(v.selectedNS)
|
v.list.SetNamespace(v.selectedNS)
|
||||||
}
|
}
|
||||||
|
|
||||||
v.refreshActions()
|
|
||||||
if err := v.list.Reconcile(v.app.informer, v.path); err != nil {
|
if err := v.list.Reconcile(v.app.informer, v.path); err != nil {
|
||||||
log.Error().Err(err).Msg("Reconciliation failed")
|
log.Error().Err(err).Msg("Reconciliation failed")
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash().errf("Reconciliation failed %s", err)
|
||||||
}
|
}
|
||||||
data := v.list.Data()
|
data := v.list.Data()
|
||||||
if v.decorateFn != nil {
|
if v.decorateFn != nil {
|
||||||
|
|
@ -413,7 +403,6 @@ func (v *resourceView) getTV() *tableView {
|
||||||
if tv, ok := v.GetPrimitive(v.list.GetName()).(*tableView); ok {
|
if tv, ok := v.GetPrimitive(v.list.GetName()).(*tableView); ok {
|
||||||
return tv
|
return tv
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,19 +413,14 @@ func (v *resourceView) selectItem(r, c int) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
col0 := strings.TrimSpace(t.GetCell(r, 0).Text)
|
||||||
switch v.list.GetNamespace() {
|
switch v.list.GetNamespace() {
|
||||||
case resource.NotNamespaced:
|
case resource.NotNamespaced:
|
||||||
v.selectedItem = strings.TrimSpace(t.GetCell(r, 0).Text)
|
v.selectedItem = col0
|
||||||
case resource.AllNamespaces:
|
case resource.AllNamespaces:
|
||||||
v.selectedItem = path.Join(
|
v.selectedItem = path.Join(col0, strings.TrimSpace(t.GetCell(r, 1).Text))
|
||||||
strings.TrimSpace(t.GetCell(r, 0).Text),
|
|
||||||
strings.TrimSpace(t.GetCell(r, 1).Text),
|
|
||||||
)
|
|
||||||
default:
|
default:
|
||||||
v.selectedItem = path.Join(
|
v.selectedItem = path.Join(v.selectedNS, col0)
|
||||||
v.selectedNS,
|
|
||||||
strings.TrimSpace(t.GetCell(r, 0).Text),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ func (v *replicaSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
r, err := rset.Get(ns, n)
|
r, err := rset.Get(ns, n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Fetching ReplicaSet %s", v.selectedItem)
|
log.Error().Err(err).Msgf("Fetching ReplicaSet %s", v.selectedItem)
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash().errf("Replicaset failed %s", err)
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
rs := r.(*v1.ReplicaSet)
|
rs := r.(*v1.ReplicaSet)
|
||||||
|
|
@ -65,7 +65,7 @@ func (v *replicaSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
sel, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector)
|
sel, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Converting selector for ReplicaSet %s", v.selectedItem)
|
log.Error().Err(err).Msgf("Converting selector for ReplicaSet %s", v.selectedItem)
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash().errf("Selector failed %s", err)
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
showPods(v.app, "", "ReplicaSet", v.selectedItem, sel.String(), "", v.backCmd)
|
showPods(v.app, "", "ReplicaSet", v.selectedItem, sel.String(), "", v.backCmd)
|
||||||
|
|
@ -86,7 +86,7 @@ func (v *replicaSetView) rollbackCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
||||||
v.showModal(fmt.Sprintf("Rollback %s %s?", v.list.GetName(), v.selectedItem), func(_ int, button string) {
|
v.showModal(fmt.Sprintf("Rollback %s %s?", v.list.GetName(), v.selectedItem), func(_ int, button string) {
|
||||||
if button == "OK" {
|
if button == "OK" {
|
||||||
v.app.flash(flashInfo, fmt.Sprintf("Rolling back %s %s", v.list.GetName(), v.selectedItem))
|
v.app.flash().infof("Rolling back %s %s", v.list.GetName(), v.selectedItem)
|
||||||
rollback(v.app, v.selectedItem)
|
rollback(v.app, v.selectedItem)
|
||||||
v.refresh()
|
v.refresh()
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +102,7 @@ func rollback(app *appView, selectedItem string) bool {
|
||||||
r, err := rset.Get(ns, n)
|
r, err := rset.Get(ns, n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Fetching ReplicaSet %s", selectedItem)
|
log.Error().Err(err).Msgf("Fetching ReplicaSet %s", selectedItem)
|
||||||
app.flash(flashErr, err.Error())
|
app.flash().errf("Failed retrieving replicaset %s", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
rs := r.(*v1.ReplicaSet)
|
rs := r.(*v1.ReplicaSet)
|
||||||
|
|
@ -115,13 +115,13 @@ func rollback(app *appView, selectedItem string) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ctrlName == "" || ctrlKind == "" || ctrlAPI == "" {
|
if ctrlName == "" || ctrlKind == "" || ctrlAPI == "" {
|
||||||
app.flash(flashErr, "Unable to find controller for ReplicaSet %s", selectedItem)
|
app.flash().errf("Unable to find controller for ReplicaSet %s", selectedItem)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
revision := rs.ObjectMeta.Annotations["deployment.kubernetes.io/revision"]
|
revision := rs.ObjectMeta.Annotations["deployment.kubernetes.io/revision"]
|
||||||
if rs.Status.Replicas != 0 {
|
if rs.Status.Replicas != 0 {
|
||||||
app.flash(flashWarn, "Can not rollback the current replica!")
|
app.flash().warn("Can not rollback the current replica!")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,7 +129,7 @@ func rollback(app *appView, selectedItem string) bool {
|
||||||
dep, err := dpr.Get(ns, ctrlName)
|
dep, err := dpr.Get(ns, ctrlName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Fetching Deployment %s", selectedItem)
|
log.Error().Err(err).Msgf("Fetching Deployment %s", selectedItem)
|
||||||
app.flash(flashErr, err.Error())
|
app.flash().errf("Unable to retrieve deployments %s", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
dp := dep.(*appsv1.Deployment)
|
dp := dep.(*appsv1.Deployment)
|
||||||
|
|
@ -157,7 +157,7 @@ func rollback(app *appView, selectedItem string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
log.Debug().Msgf("Version %s %s", revision, res)
|
log.Debug().Msgf("Version %s %s", revision, res)
|
||||||
app.flash(flashInfo, fmt.Sprintf("Version %s %s", revision, res))
|
app.flash().infof("Version %s %s", revision, res)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ func (v *secretView) decodeCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
ns, n := namespaced(sel)
|
ns, n := namespaced(sel)
|
||||||
sec, err := v.app.conn().DialOrDie().CoreV1().Secrets(ns).Get(n, metav1.GetOptions{})
|
sec, err := v.app.conn().DialOrDie().CoreV1().Secrets(ns).Get(n, metav1.GetOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.app.flash(flashErr, "Unable to retrieve secret", sel)
|
v.app.flash().errf("Unable to retrieve secret %s", err)
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,7 +46,7 @@ func (v *secretView) decodeCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
}
|
}
|
||||||
raw, err := yaml.Marshal(d)
|
raw, err := yaml.Marshal(d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.app.flash(flashErr, "Error decoding secret for `", sel)
|
v.app.flash().errf("Error decoding secret %s", err)
|
||||||
log.Error().Err(err).Msgf("Marshal error getting secret %s", sel)
|
log.Error().Err(err).Msgf("Marshal error getting secret %s", sel)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package views
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -15,9 +16,8 @@ type statusView struct {
|
||||||
func newStatusView(app *appView) *statusView {
|
func newStatusView(app *appView) *statusView {
|
||||||
v := statusView{app: app, TextView: tview.NewTextView()}
|
v := statusView{app: app, TextView: tview.NewTextView()}
|
||||||
{
|
{
|
||||||
v.SetBackgroundColor(app.styles.BgColor())
|
v.SetBackgroundColor(config.AsColor(app.styles.Style.Log.BgColor))
|
||||||
v.SetTextAlign(tview.AlignRight)
|
v.SetTextAlign(tview.AlignRight)
|
||||||
// v.SetBorderPadding(0, 0, 1, 1)
|
|
||||||
v.SetDynamicColors(true)
|
v.SetDynamicColors(true)
|
||||||
}
|
}
|
||||||
return &v
|
return &v
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ func (v *statefulSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
s, err := d.Get(ns, n)
|
s, err := d.Get(ns, n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Fetching StatefulSet %s", v.selectedItem)
|
log.Error().Err(err).Msgf("Fetching StatefulSet %s", v.selectedItem)
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash().errf("Unable to fetch statefulset %s", err)
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
sts := s.(*v1.StatefulSet)
|
sts := s.(*v1.StatefulSet)
|
||||||
|
|
@ -57,7 +57,7 @@ func (v *statefulSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
sel, err := metav1.LabelSelectorAsSelector(sts.Spec.Selector)
|
sel, err := metav1.LabelSelectorAsSelector(sts.Spec.Selector)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Converting selector for StatefulSet %s", v.selectedItem)
|
log.Error().Err(err).Msgf("Converting selector for StatefulSet %s", v.selectedItem)
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash().errf("Selector failed %s", err)
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
showPods(v.app, "", "StatefulSet", v.selectedItem, sel.String(), "", v.backCmd)
|
showPods(v.app, "", "StatefulSet", v.selectedItem, sel.String(), "", v.backCmd)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
|
"github.com/derailed/k9s/internal/resource"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type svcView struct {
|
||||||
|
*resourceView
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSvcView(t string, app *appView, list resource.List) resourceViewer {
|
||||||
|
v := svcView{newResourceView(t, app, list).(*resourceView)}
|
||||||
|
{
|
||||||
|
v.extraActionsFn = v.extraActions
|
||||||
|
v.switchPage("svc")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *svcView) extraActions(aa keyActions) {
|
||||||
|
aa[KeyShiftT] = newKeyAction("Sort Type", v.sortColCmd(1, false), true)
|
||||||
|
aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *svcView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
t := v.getTV()
|
||||||
|
t.sortCol.index, t.sortCol.asc = t.nameColIndex()+col, asc
|
||||||
|
t.refresh()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *svcView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if !v.rowSelected() {
|
||||||
|
return evt
|
||||||
|
}
|
||||||
|
|
||||||
|
s := k8s.NewService(v.app.conn())
|
||||||
|
ns, n := namespaced(v.selectedItem)
|
||||||
|
res, err := s.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Fetch service %s", v.selectedItem)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
svc := res.(*v1.Service)
|
||||||
|
|
||||||
|
v.showSvcPods(ns, svc.Spec.Selector, v.backCmd)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *svcView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
v.app.inject(v)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *svcView) showSvcPods(ns string, sel map[string]string, b actionHandler) {
|
||||||
|
var s []string
|
||||||
|
for k, v := range sel {
|
||||||
|
s = append(s, fmt.Sprintf("%s=%s", k, v))
|
||||||
|
}
|
||||||
|
list := resource.NewPodList(v.app.conn(), ns)
|
||||||
|
list.SetLabelSelector(strings.Join(s, ","))
|
||||||
|
|
||||||
|
pv := newPodView("Pods", v.app, list)
|
||||||
|
pv.setColorerFn(podColorer)
|
||||||
|
pv.setExtraActionsFn(func(aa keyActions) {
|
||||||
|
aa[tcell.KeyEsc] = newKeyAction("Back", b, true)
|
||||||
|
})
|
||||||
|
// Reset active namespace to all.
|
||||||
|
v.app.config.SetActiveNamespace(ns)
|
||||||
|
v.app.inject(pv)
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
package views
|
package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/k9s/internal/resource"
|
"github.com/derailed/k9s/internal/resource"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"k8s.io/apimachinery/pkg/util/duration"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -56,10 +55,14 @@ type (
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTableView(app *appView, title string) *tableView {
|
func newTableView(app *appView, title string) *tableView {
|
||||||
v := tableView{app: app, Table: tview.NewTable(), sortCol: sortColumn{0, 0, true}}
|
v := tableView{
|
||||||
{
|
app: app,
|
||||||
v.baseTitle = title
|
Table: tview.NewTable(),
|
||||||
v.actions = make(keyActions)
|
sortCol: sortColumn{0, 0, true},
|
||||||
|
actions: make(keyActions),
|
||||||
|
baseTitle: title,
|
||||||
|
cmdBuff: newCmdBuff('/'),
|
||||||
|
}
|
||||||
v.SetFixed(1, 0)
|
v.SetFixed(1, 0)
|
||||||
v.SetBorder(true)
|
v.SetBorder(true)
|
||||||
v.SetBackgroundColor(config.AsColor(app.styles.Style.Table.BgColor))
|
v.SetBackgroundColor(config.AsColor(app.styles.Style.Table.BgColor))
|
||||||
|
|
@ -67,7 +70,6 @@ func newTableView(app *appView, title string) *tableView {
|
||||||
v.SetBorderFocusColor(config.AsColor(app.styles.Style.Border.FocusColor))
|
v.SetBorderFocusColor(config.AsColor(app.styles.Style.Border.FocusColor))
|
||||||
v.SetBorderAttributes(tcell.AttrBold)
|
v.SetBorderAttributes(tcell.AttrBold)
|
||||||
v.SetBorderPadding(0, 0, 1, 1)
|
v.SetBorderPadding(0, 0, 1, 1)
|
||||||
v.cmdBuff = newCmdBuff('/')
|
|
||||||
v.cmdBuff.addListener(app.cmdView)
|
v.cmdBuff.addListener(app.cmdView)
|
||||||
v.cmdBuff.reset()
|
v.cmdBuff.reset()
|
||||||
v.SetSelectable(true, false)
|
v.SetSelectable(true, false)
|
||||||
|
|
@ -78,7 +80,6 @@ func newTableView(app *appView, title string) *tableView {
|
||||||
)
|
)
|
||||||
v.SetInputCapture(v.keyboard)
|
v.SetInputCapture(v.keyboard)
|
||||||
v.bindKeys()
|
v.bindKeys()
|
||||||
}
|
|
||||||
|
|
||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|
@ -95,10 +96,6 @@ func (v *tableView) bindKeys() {
|
||||||
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false)
|
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false)
|
||||||
v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false)
|
v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false)
|
||||||
v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false)
|
v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false)
|
||||||
v.actions[KeyG] = newKeyAction("Top", v.app.puntCmd, false)
|
|
||||||
v.actions[KeyShiftG] = newKeyAction("Bottom", v.app.puntCmd, false)
|
|
||||||
v.actions[KeyB] = newKeyAction("Down", v.pageDownCmd, false)
|
|
||||||
v.actions[KeyF] = newKeyAction("Up", v.pageUpCmd, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *tableView) clearSelection() {
|
func (v *tableView) clearSelection() {
|
||||||
|
|
@ -136,18 +133,6 @@ func (v *tableView) setSelection() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *tableView) pageUpCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|
||||||
v.PageUp()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *tableView) pageDownCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|
||||||
v.PageDown()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
if v.cmdBuff.isActive() {
|
if v.cmdBuff.isActive() {
|
||||||
v.cmdBuff.setActive(false)
|
v.cmdBuff.setActive(false)
|
||||||
|
|
@ -168,7 +153,7 @@ func (v *tableView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
||||||
func (v *tableView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (v *tableView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
if !v.cmdBuff.empty() {
|
if !v.cmdBuff.empty() {
|
||||||
v.app.flash(flashInfo, "Clearing filter...")
|
v.app.flash().info("Clearing filter...")
|
||||||
}
|
}
|
||||||
v.cmdBuff.reset()
|
v.cmdBuff.reset()
|
||||||
v.refresh()
|
v.refresh()
|
||||||
|
|
@ -216,7 +201,7 @@ func (v *tableView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
v.app.flash(flashInfo, "Filter mode activated.")
|
v.app.flash().info("Filter mode activated.")
|
||||||
v.cmdBuff.reset()
|
v.cmdBuff.reset()
|
||||||
v.cmdBuff.setActive(true)
|
v.cmdBuff.setActive(true)
|
||||||
|
|
||||||
|
|
@ -274,7 +259,7 @@ func (v *tableView) filtered() resource.TableData {
|
||||||
|
|
||||||
rx, err := regexp.Compile(`(?i)` + v.cmdBuff.String())
|
rx, err := regexp.Compile(`(?i)` + v.cmdBuff.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.app.flash(flashErr, "Invalid filter expression")
|
v.app.flash().err(errors.New("Invalid filter expression"))
|
||||||
v.cmdBuff.clear()
|
v.cmdBuff.clear()
|
||||||
return v.data
|
return v.data
|
||||||
}
|
}
|
||||||
|
|
@ -392,21 +377,12 @@ func (v *tableView) addHeaderCell(col int, name string, fg, bg tcell.Color) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *tableView) addBodyCell(header string, row, col int, field, delta string, color tcell.Color, pads maxyPad) {
|
func (v *tableView) addBodyCell(header string, row, col int, field, delta string, color tcell.Color, pads maxyPad) {
|
||||||
const colPadding = 3
|
|
||||||
|
|
||||||
if header == "AGE" {
|
|
||||||
dur, err := time.ParseDuration(field)
|
|
||||||
if err == nil {
|
|
||||||
field = duration.HumanDuration(dur)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
field += deltas(delta, field)
|
field += deltas(delta, field)
|
||||||
align := tview.AlignLeft
|
align := tview.AlignLeft
|
||||||
if crx.MatchString(header) || mrx.MatchString(header) {
|
if crx.MatchString(header) || mrx.MatchString(header) {
|
||||||
align = tview.AlignRight
|
align = tview.AlignRight
|
||||||
} else if isASCII(field) {
|
} else if isASCII(field) {
|
||||||
field = pad(field, pads[col]+colPadding)
|
field = pad(field, pads[col])
|
||||||
}
|
}
|
||||||
|
|
||||||
c := tview.NewTableCell(field)
|
c := tview.NewTableCell(field)
|
||||||
|
|
@ -414,7 +390,6 @@ func (v *tableView) addBodyCell(header string, row, col int, field, delta string
|
||||||
c.SetExpansion(1)
|
c.SetExpansion(1)
|
||||||
c.SetAlign(align)
|
c.SetAlign(align)
|
||||||
c.SetTextColor(color)
|
c.SetTextColor(color)
|
||||||
c.SetMaxWidth(pads[col] + colPadding)
|
|
||||||
}
|
}
|
||||||
v.SetCell(row, col, c)
|
v.SetCell(row, col, c)
|
||||||
}
|
}
|
||||||
|
|
@ -449,25 +424,17 @@ func (v *tableView) resetTitle() {
|
||||||
}
|
}
|
||||||
switch v.currentNS {
|
switch v.currentNS {
|
||||||
case resource.NotNamespaced, "*":
|
case resource.NotNamespaced, "*":
|
||||||
fmat := strings.Replace(titleFmt, "[fg:bg", "["+v.app.styles.Style.Title.FgColor+":"+v.app.styles.Style.Title.BgColor, -1)
|
title = skinTitle(fmt.Sprintf(titleFmt, v.baseTitle, rc), v.app.styles.Style)
|
||||||
fmat = strings.Replace(fmat, "[count", "["+v.app.styles.Style.Title.CounterColor, 1)
|
|
||||||
title = fmt.Sprintf(fmat, v.baseTitle, rc)
|
|
||||||
default:
|
default:
|
||||||
ns := v.currentNS
|
ns := v.currentNS
|
||||||
if ns == resource.AllNamespaces {
|
if ns == resource.AllNamespaces {
|
||||||
ns = resource.AllNamespace
|
ns = resource.AllNamespace
|
||||||
}
|
}
|
||||||
fmat := strings.Replace(nsTitleFmt, "[fg:bg", "["+v.app.styles.Style.Title.FgColor+":"+v.app.styles.Style.Title.BgColor, -1)
|
title = skinTitle(fmt.Sprintf(nsTitleFmt, v.baseTitle, ns, rc), v.app.styles.Style)
|
||||||
fmat = strings.Replace(fmat, "[hilite", "["+v.app.styles.Style.Title.HighlightColor, 1)
|
|
||||||
fmat = strings.Replace(fmat, "[count", "["+v.app.styles.Style.Title.CounterColor, 1)
|
|
||||||
title = fmt.Sprintf(fmat, v.baseTitle, ns, rc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !v.cmdBuff.isActive() && !v.cmdBuff.empty() {
|
if !v.cmdBuff.isActive() && !v.cmdBuff.empty() {
|
||||||
fmat := strings.Replace(searchFmt, "[fg:bg", "["+v.app.styles.Style.Title.FgColor+":"+v.app.styles.Style.Title.BgColor, -1)
|
title += skinTitle(fmt.Sprintf(searchFmt, v.cmdBuff), v.app.styles.Style)
|
||||||
fmat = strings.Replace(fmat, ":bg:", ":"+v.app.styles.Style.Title.BgColor+":", -1)
|
|
||||||
fmat = strings.Replace(fmat, "[filter", "["+v.app.styles.Style.Title.FilterColor, 1)
|
|
||||||
title += fmt.Sprintf(fmat, v.cmdBuff)
|
|
||||||
}
|
}
|
||||||
v.SetTitle(title)
|
v.SetTitle(title)
|
||||||
}
|
}
|
||||||
|
|
@ -475,6 +442,16 @@ func (v *tableView) resetTitle() {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Event listeners...
|
// Event listeners...
|
||||||
|
|
||||||
|
func skinTitle(fmat string, style *config.Style) string {
|
||||||
|
fmat = strings.Replace(fmat, "[fg:bg", "["+style.Title.FgColor+":"+style.Title.BgColor, -1)
|
||||||
|
fmat = strings.Replace(fmat, "[hilite", "["+style.Title.HighlightColor, 1)
|
||||||
|
fmat = strings.Replace(fmat, "[key", "["+style.Menu.NumKeyColor, 1)
|
||||||
|
fmat = strings.Replace(fmat, "[filter", "["+style.Title.FilterColor, 1)
|
||||||
|
fmat = strings.Replace(fmat, "[count", "["+style.Title.CounterColor, 1)
|
||||||
|
fmat = strings.Replace(fmat, ":bg:", ":"+style.Title.BgColor+":", -1)
|
||||||
|
return fmat
|
||||||
|
}
|
||||||
|
|
||||||
func (v *tableView) changed(s string) {}
|
func (v *tableView) changed(s string) {}
|
||||||
|
|
||||||
func (v *tableView) active(b bool) {
|
func (v *tableView) active(b bool) {
|
||||||
|
|
|
||||||
|
|
@ -160,10 +160,7 @@ func (n *nodeMxWatcher) update(list *mv1beta1.NodeMetricsList, notify bool) {
|
||||||
kind = watch.Modified
|
kind = watch.Modified
|
||||||
}
|
}
|
||||||
if notify {
|
if notify {
|
||||||
n.eventChan <- watch.Event{
|
n.eventChan <- watch.Event{Type: kind, Object: v}
|
||||||
Type: kind,
|
|
||||||
Object: v,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
n.cache[k] = v
|
n.cache[k] = v
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,10 +164,7 @@ func (p *podMxWatcher) update(list *mv1beta1.PodMetricsList, notify bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if notify {
|
if notify {
|
||||||
p.eventChan <- watch.Event{
|
p.eventChan <- watch.Event{Type: kind, Object: v}
|
||||||
Type: kind,
|
|
||||||
Object: v,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
p.cache[k] = v
|
p.cache[k] = v
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue