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

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,16 +22,7 @@ const (
clusterRefresh = time.Duration(15 * time.Second) clusterRefresh = time.Duration(15 * time.Second)
) )
type ( type resourceView struct {
details interface {
tview.Primitive
setTitle(string)
clear()
setActions(keyActions)
update(resource.Properties)
}
resourceView struct {
*tview.Pages *tview.Pages
app *appView app *appView
@ -51,8 +42,8 @@ type (
suspended bool suspended bool
nsListAccess bool nsListAccess bool
path *string path *string
context context.Context
} }
)
func newResourceView(title string, app *appView, list resource.List) resourceViewer { func newResourceView(title string, app *appView, list resource.List) resourceViewer {
v := resourceView{ v := resourceView{
@ -78,7 +69,7 @@ func newResourceView(title string, app *appView, list resource.List) resourceVie
// Init watches all running pods in given namespace // Init watches all running pods in given namespace
func (v *resourceView) init(ctx context.Context, ns string) { func (v *resourceView) init(ctx context.Context, ns string) {
v.selectedItem, v.selectedNS = noSelection, ns v.context, v.selectedItem, v.selectedNS = ctx, noSelection, ns
colorer := defaultColorer colorer := defaultColorer
if v.colorerFn != nil { if v.colorerFn != nil {
@ -91,7 +82,7 @@ func (v *resourceView) init(ctx context.Context, ns string) {
nn, err := k8s.NewNamespace(v.app.conn()).List(resource.AllNamespaces) nn, err := k8s.NewNamespace(v.app.conn()).List(resource.AllNamespaces)
if err != nil { if err != nil {
log.Warn().Err(err).Msg("List namespaces") log.Warn().Err(err).Msg("List namespaces")
v.app.flash(flashErr, err.Error()) v.app.flash().errf("Unable to list namespaces %s", err)
} }
if v.list.Namespaced() && !v.list.AllNamespaces() { if v.list.Namespaced() && !v.list.AllNamespaces() {
@ -232,17 +223,17 @@ func (v *resourceView) enterCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.getTV().filterCmd(evt) == nil { if v.getTV().filterCmd(evt) == nil {
return nil return nil
} }
if v.enterFn != nil { if v.enterFn != nil {
v.enterFn(v.app, v.list.GetNamespace(), v.list.GetName(), v.selectedItem) v.enterFn(v.app, v.list.GetNamespace(), v.list.GetName(), v.selectedItem)
} else { } else {
v.defaultEnter(v.list.GetNamespace(), v.list.GetName(), v.selectedItem) v.defaultEnter(v.list.GetNamespace(), v.list.GetName(), v.selectedItem)
} }
return nil return nil
} }
func (v *resourceView) refreshCmd(*tcell.EventKey) *tcell.EventKey { func (v *resourceView) refreshCmd(*tcell.EventKey) *tcell.EventKey {
v.app.flash(flashInfo, "Refreshing...") v.app.flash().info("Refreshing...")
v.refresh() v.refresh()
return nil return nil
} }
@ -261,9 +252,9 @@ func (v *resourceView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
v.showModal(fmt.Sprintf("Delete %s %s?", v.list.GetName(), sel), func(_ int, button string) { v.showModal(fmt.Sprintf("Delete %s %s?", v.list.GetName(), sel), func(_ int, button string) {
if button == "OK" { if button == "OK" {
v.getTV().setDeleted() v.getTV().setDeleted()
v.app.flash(flashInfo, fmt.Sprintf("Deleting %s %s", v.list.GetName(), sel)) v.app.flash().infof("Deleting %s %s", v.list.GetName(), sel)
if err := v.list.Resource().Delete(sel); err != nil { if err := v.list.Resource().Delete(sel); err != nil {
v.app.flash(flashErr, "Boom!", err.Error()) v.app.flash().errf("Delete failed with %s", err)
} else { } else {
v.refresh() v.refresh()
} }
@ -290,10 +281,9 @@ func (v *resourceView) dismissModal() {
} }
func (v *resourceView) defaultEnter(ns, resource, selection string) { func (v *resourceView) defaultEnter(ns, resource, selection string) {
log.Debug().Msgf("Title %s", v.title)
yaml, err := v.list.Resource().Describe(v.title, selection, v.app.flags) yaml, err := v.list.Resource().Describe(v.title, selection, v.app.flags)
if err != nil { if err != nil {
v.app.flash(flashErr, err.Error()) v.app.flash().errf("Describe command failed %s", err)
log.Warn().Msgf("Describe %v", err.Error()) log.Warn().Msgf("Describe %v", err.Error())
return return
} }
@ -328,7 +318,7 @@ func (v *resourceView) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
sel := v.getSelectedItem() sel := v.getSelectedItem()
raw, err := v.list.Resource().Marshal(sel) raw, err := v.list.Resource().Marshal(sel)
if err != nil { if err != nil {
v.app.flash(flashErr, "Unable to marshal resource", err.Error()) v.app.flash().errf("Unable to marshal resource %s", err)
log.Error().Err(err) log.Error().Err(err)
return evt return evt
} }
@ -375,7 +365,7 @@ func (v *resourceView) doSwitchNamespace(ns string) {
ns = resource.AllNamespace ns = resource.AllNamespace
} }
v.selectedNS = ns v.selectedNS = ns
v.app.flash(flashInfo, fmt.Sprintf("Viewing `%s namespace...", ns)) v.app.flash().infof("Viewing `%s namespace...", ns)
v.list.SetNamespace(v.selectedNS) v.list.SetNamespace(v.selectedNS)
v.refresh() v.refresh()
@ -392,14 +382,14 @@ func (v *resourceView) refresh() {
return return
} }
v.refreshActions()
if v.list.Namespaced() { if v.list.Namespaced() {
v.list.SetNamespace(v.selectedNS) v.list.SetNamespace(v.selectedNS)
} }
v.refreshActions()
if err := v.list.Reconcile(v.app.informer, v.path); err != nil { if err := v.list.Reconcile(v.app.informer, v.path); err != nil {
log.Error().Err(err).Msg("Reconciliation failed") log.Error().Err(err).Msg("Reconciliation failed")
v.app.flash(flashErr, err.Error()) v.app.flash().errf("Reconciliation failed %s", err)
} }
data := v.list.Data() data := v.list.Data()
if v.decorateFn != nil { if v.decorateFn != nil {
@ -413,7 +403,6 @@ func (v *resourceView) getTV() *tableView {
if tv, ok := v.GetPrimitive(v.list.GetName()).(*tableView); ok { if tv, ok := v.GetPrimitive(v.list.GetName()).(*tableView); ok {
return tv return tv
} }
return nil return nil
} }
@ -424,19 +413,14 @@ func (v *resourceView) selectItem(r, c int) {
return return
} }
col0 := strings.TrimSpace(t.GetCell(r, 0).Text)
switch v.list.GetNamespace() { switch v.list.GetNamespace() {
case resource.NotNamespaced: case resource.NotNamespaced:
v.selectedItem = strings.TrimSpace(t.GetCell(r, 0).Text) v.selectedItem = col0
case resource.AllNamespaces: case resource.AllNamespaces:
v.selectedItem = path.Join( v.selectedItem = path.Join(col0, strings.TrimSpace(t.GetCell(r, 1).Text))
strings.TrimSpace(t.GetCell(r, 0).Text),
strings.TrimSpace(t.GetCell(r, 1).Text),
)
default: default:
v.selectedItem = path.Join( v.selectedItem = path.Join(v.selectedNS, col0)
v.selectedNS,
strings.TrimSpace(t.GetCell(r, 0).Text),
)
} }
} }

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,10 +55,14 @@ type (
) )
func newTableView(app *appView, title string) *tableView { func newTableView(app *appView, title string) *tableView {
v := tableView{app: app, Table: tview.NewTable(), sortCol: sortColumn{0, 0, true}} v := tableView{
{ app: app,
v.baseTitle = title Table: tview.NewTable(),
v.actions = make(keyActions) sortCol: sortColumn{0, 0, true},
actions: make(keyActions),
baseTitle: title,
cmdBuff: newCmdBuff('/'),
}
v.SetFixed(1, 0) v.SetFixed(1, 0)
v.SetBorder(true) v.SetBorder(true)
v.SetBackgroundColor(config.AsColor(app.styles.Style.Table.BgColor)) v.SetBackgroundColor(config.AsColor(app.styles.Style.Table.BgColor))
@ -67,7 +70,6 @@ func newTableView(app *appView, title string) *tableView {
v.SetBorderFocusColor(config.AsColor(app.styles.Style.Border.FocusColor)) v.SetBorderFocusColor(config.AsColor(app.styles.Style.Border.FocusColor))
v.SetBorderAttributes(tcell.AttrBold) v.SetBorderAttributes(tcell.AttrBold)
v.SetBorderPadding(0, 0, 1, 1) v.SetBorderPadding(0, 0, 1, 1)
v.cmdBuff = newCmdBuff('/')
v.cmdBuff.addListener(app.cmdView) v.cmdBuff.addListener(app.cmdView)
v.cmdBuff.reset() v.cmdBuff.reset()
v.SetSelectable(true, false) v.SetSelectable(true, false)
@ -78,7 +80,6 @@ func newTableView(app *appView, title string) *tableView {
) )
v.SetInputCapture(v.keyboard) v.SetInputCapture(v.keyboard)
v.bindKeys() v.bindKeys()
}
return &v return &v
} }
@ -95,10 +96,6 @@ func (v *tableView) bindKeys() {
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false) v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false) v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false) v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[KeyG] = newKeyAction("Top", v.app.puntCmd, false)
v.actions[KeyShiftG] = newKeyAction("Bottom", v.app.puntCmd, false)
v.actions[KeyB] = newKeyAction("Down", v.pageDownCmd, false)
v.actions[KeyF] = newKeyAction("Up", v.pageUpCmd, false)
} }
func (v *tableView) clearSelection() { func (v *tableView) clearSelection() {
@ -136,18 +133,6 @@ func (v *tableView) setSelection() {
} }
} }
func (v *tableView) pageUpCmd(evt *tcell.EventKey) *tcell.EventKey {
v.PageUp()
return nil
}
func (v *tableView) pageDownCmd(evt *tcell.EventKey) *tcell.EventKey {
v.PageDown()
return nil
}
func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey { func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.cmdBuff.isActive() { if v.cmdBuff.isActive() {
v.cmdBuff.setActive(false) v.cmdBuff.setActive(false)
@ -168,7 +153,7 @@ func (v *tableView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
func (v *tableView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { func (v *tableView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.cmdBuff.empty() { if !v.cmdBuff.empty() {
v.app.flash(flashInfo, "Clearing filter...") v.app.flash().info("Clearing filter...")
} }
v.cmdBuff.reset() v.cmdBuff.reset()
v.refresh() v.refresh()
@ -216,7 +201,7 @@ func (v *tableView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt return evt
} }
v.app.flash(flashInfo, "Filter mode activated.") v.app.flash().info("Filter mode activated.")
v.cmdBuff.reset() v.cmdBuff.reset()
v.cmdBuff.setActive(true) v.cmdBuff.setActive(true)
@ -274,7 +259,7 @@ func (v *tableView) filtered() resource.TableData {
rx, err := regexp.Compile(`(?i)` + v.cmdBuff.String()) rx, err := regexp.Compile(`(?i)` + v.cmdBuff.String())
if err != nil { if err != nil {
v.app.flash(flashErr, "Invalid filter expression") v.app.flash().err(errors.New("Invalid filter expression"))
v.cmdBuff.clear() v.cmdBuff.clear()
return v.data return v.data
} }
@ -392,21 +377,12 @@ func (v *tableView) addHeaderCell(col int, name string, fg, bg tcell.Color) {
} }
func (v *tableView) addBodyCell(header string, row, col int, field, delta string, color tcell.Color, pads maxyPad) { func (v *tableView) addBodyCell(header string, row, col int, field, delta string, color tcell.Color, pads maxyPad) {
const colPadding = 3
if header == "AGE" {
dur, err := time.ParseDuration(field)
if err == nil {
field = duration.HumanDuration(dur)
}
}
field += deltas(delta, field) field += deltas(delta, field)
align := tview.AlignLeft align := tview.AlignLeft
if crx.MatchString(header) || mrx.MatchString(header) { if crx.MatchString(header) || mrx.MatchString(header) {
align = tview.AlignRight align = tview.AlignRight
} else if isASCII(field) { } else if isASCII(field) {
field = pad(field, pads[col]+colPadding) field = pad(field, pads[col])
} }
c := tview.NewTableCell(field) c := tview.NewTableCell(field)
@ -414,7 +390,6 @@ func (v *tableView) addBodyCell(header string, row, col int, field, delta string
c.SetExpansion(1) c.SetExpansion(1)
c.SetAlign(align) c.SetAlign(align)
c.SetTextColor(color) c.SetTextColor(color)
c.SetMaxWidth(pads[col] + colPadding)
} }
v.SetCell(row, col, c) v.SetCell(row, col, c)
} }
@ -449,25 +424,17 @@ func (v *tableView) resetTitle() {
} }
switch v.currentNS { switch v.currentNS {
case resource.NotNamespaced, "*": case resource.NotNamespaced, "*":
fmat := strings.Replace(titleFmt, "[fg:bg", "["+v.app.styles.Style.Title.FgColor+":"+v.app.styles.Style.Title.BgColor, -1) title = skinTitle(fmt.Sprintf(titleFmt, v.baseTitle, rc), v.app.styles.Style)
fmat = strings.Replace(fmat, "[count", "["+v.app.styles.Style.Title.CounterColor, 1)
title = fmt.Sprintf(fmat, v.baseTitle, rc)
default: default:
ns := v.currentNS ns := v.currentNS
if ns == resource.AllNamespaces { if ns == resource.AllNamespaces {
ns = resource.AllNamespace ns = resource.AllNamespace
} }
fmat := strings.Replace(nsTitleFmt, "[fg:bg", "["+v.app.styles.Style.Title.FgColor+":"+v.app.styles.Style.Title.BgColor, -1) title = skinTitle(fmt.Sprintf(nsTitleFmt, v.baseTitle, ns, rc), v.app.styles.Style)
fmat = strings.Replace(fmat, "[hilite", "["+v.app.styles.Style.Title.HighlightColor, 1)
fmat = strings.Replace(fmat, "[count", "["+v.app.styles.Style.Title.CounterColor, 1)
title = fmt.Sprintf(fmat, v.baseTitle, ns, rc)
} }
if !v.cmdBuff.isActive() && !v.cmdBuff.empty() { if !v.cmdBuff.isActive() && !v.cmdBuff.empty() {
fmat := strings.Replace(searchFmt, "[fg:bg", "["+v.app.styles.Style.Title.FgColor+":"+v.app.styles.Style.Title.BgColor, -1) title += skinTitle(fmt.Sprintf(searchFmt, v.cmdBuff), v.app.styles.Style)
fmat = strings.Replace(fmat, ":bg:", ":"+v.app.styles.Style.Title.BgColor+":", -1)
fmat = strings.Replace(fmat, "[filter", "["+v.app.styles.Style.Title.FilterColor, 1)
title += fmt.Sprintf(fmat, v.cmdBuff)
} }
v.SetTitle(title) v.SetTitle(title)
} }
@ -475,6 +442,16 @@ func (v *tableView) resetTitle() {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Event listeners... // Event listeners...
func skinTitle(fmat string, style *config.Style) string {
fmat = strings.Replace(fmat, "[fg:bg", "["+style.Title.FgColor+":"+style.Title.BgColor, -1)
fmat = strings.Replace(fmat, "[hilite", "["+style.Title.HighlightColor, 1)
fmat = strings.Replace(fmat, "[key", "["+style.Menu.NumKeyColor, 1)
fmat = strings.Replace(fmat, "[filter", "["+style.Title.FilterColor, 1)
fmat = strings.Replace(fmat, "[count", "["+style.Title.CounterColor, 1)
fmat = strings.Replace(fmat, ":bg:", ":"+style.Title.BgColor+":", -1)
return fmat
}
func (v *tableView) changed(s string) {} func (v *tableView) changed(s string) {}
func (v *tableView) active(b bool) { func (v *tableView) active(b bool) {

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