add port-forwards + some nav improv + bugz kill

mine
derailed 2019-05-28 16:29:27 -06:00
parent 247cc634be
commit 8b6898dea7
57 changed files with 1865 additions and 335 deletions

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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 (

View File

@ -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)
} }

View File

@ -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.

View File

@ -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
}

View File

@ -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]
}

View File

@ -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:

View File

@ -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 {

View File

@ -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
} }

View File

@ -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),
) )

View File

@ -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),
) )

View File

@ -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",
} }
} }

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

318
internal/views/bench.go Normal file
View File

@ -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)
}

View File

@ -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])
})
}
}

View File

@ -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
}

View File

@ -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
} }
return c
}
func containerColorer(ns string, r *resource.RowEvent) tcell.Color {
c := defaultColorer(ns, r)
readyCol := 2 readyCol := 2
if len(ns) != 0 { if strings.TrimSpace(r.Fields[readyCol]) == "false" {
readyCol = 1 c = errColor
} }
tokens := strings.Split(strings.TrimSpace(r.Fields[readyCol]), "/")
if len(tokens) == 2 && (tokens[0] == "0" || tokens[0] != tokens[1]) { stateCol := readyCol + 1
if strings.TrimSpace(r.Fields[statusCol]) != "Completed" { switch strings.TrimSpace(r.Fields[stateCol]) {
c = errColor case "ContainerCreating", "PodInitializing":
} return addColor
case "Terminating", "Initialized":
return highlightColor
case "Completed":
return completedColor
case "Running":
default:
c = errColor
} }
return c return c

View File

@ -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
} }

View File

@ -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

View File

@ -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)

View File

@ -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
} }

View File

@ -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)
} }

View File

@ -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)

View File

@ -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)

View File

@ -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)
} }
}) })
} }

View File

@ -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()

291
internal/views/forward.go Normal file
View File

@ -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)
}

View File

@ -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"},
} }

View File

@ -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 {

View File

@ -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)

View File

@ -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

83
internal/views/logo.go Normal file
View File

@ -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
}

View File

@ -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)

View File

@ -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()
} }

View File

@ -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++

View File

@ -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},
}, },
} }

View File

@ -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)
} }

View File

@ -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, " "))
})
}
}

View File

@ -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),
} }
} }

View File

@ -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),
} }

View File

@ -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",

View File

@ -22,37 +22,28 @@ const (
clusterRefresh = time.Duration(15 * time.Second) clusterRefresh = time.Duration(15 * time.Second)
) )
type ( type resourceView struct {
details interface { *tview.Pages
tview.Primitive
setTitle(string)
clear()
setActions(keyActions)
update(resource.Properties)
}
resourceView struct { app *appView
*tview.Pages title string
selectedItem string
app *appView selectedRow int
title string namespaces map[int]string
selectedItem string selectedNS string
selectedRow int list resource.List
namespaces map[int]string enterFn enterFn
selectedNS string extraActionsFn func(keyActions)
list resource.List selectedFn func() string
enterFn enterFn decorateFn decorateFn
extraActionsFn func(keyActions) colorerFn colorerFn
selectedFn func() string actions keyActions
decorateFn decorateFn mx sync.Mutex
colorerFn colorerFn suspended bool
actions keyActions nsListAccess bool
mx sync.Mutex path *string
suspended bool context context.Context
nsListAccess bool }
path *string
}
)
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),
)
} }
} }

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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

View File

@ -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)

84
internal/views/svc.go Normal file
View File

@ -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)
}

View File

@ -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,29 +55,31 @@ 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},
v.SetFixed(1, 0) actions: make(keyActions),
v.SetBorder(true) baseTitle: title,
v.SetBackgroundColor(config.AsColor(app.styles.Style.Table.BgColor)) cmdBuff: newCmdBuff('/'),
v.SetBorderColor(config.AsColor(app.styles.Style.Table.FgColor))
v.SetBorderFocusColor(config.AsColor(app.styles.Style.Border.FocusColor))
v.SetBorderAttributes(tcell.AttrBold)
v.SetBorderPadding(0, 0, 1, 1)
v.cmdBuff = newCmdBuff('/')
v.cmdBuff.addListener(app.cmdView)
v.cmdBuff.reset()
v.SetSelectable(true, false)
v.SetSelectedStyle(
tcell.ColorBlack,
config.AsColor(app.styles.Style.Table.CursorColor),
tcell.AttrBold,
)
v.SetInputCapture(v.keyboard)
v.bindKeys()
} }
v.SetFixed(1, 0)
v.SetBorder(true)
v.SetBackgroundColor(config.AsColor(app.styles.Style.Table.BgColor))
v.SetBorderColor(config.AsColor(app.styles.Style.Table.FgColor))
v.SetBorderFocusColor(config.AsColor(app.styles.Style.Border.FocusColor))
v.SetBorderAttributes(tcell.AttrBold)
v.SetBorderPadding(0, 0, 1, 1)
v.cmdBuff.addListener(app.cmdView)
v.cmdBuff.reset()
v.SetSelectable(true, false)
v.SetSelectedStyle(
tcell.ColorBlack,
config.AsColor(app.styles.Style.Table.CursorColor),
tcell.AttrBold,
)
v.SetInputCapture(v.keyboard)
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) {

View File

@ -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
} }

View File

@ -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
} }