checkpoint

mine
derailed 2020-03-23 08:01:19 -06:00
parent 3efb3f2596
commit dbbf68fab9
66 changed files with 1236 additions and 617 deletions

View File

@ -74,7 +74,7 @@ func run(cmd *cobra.Command, args []string) {
log.Error().Msg(string(debug.Stack())) log.Error().Msg(string(debug.Stack()))
printLogo(color.Red) printLogo(color.Red)
fmt.Printf("%s", color.Colorize("Boom!! ", color.Red)) fmt.Printf("%s", color.Colorize("Boom!! ", color.Red))
fmt.Println(color.Colorize(fmt.Sprintf("%v.", err), color.White)) fmt.Println(color.Colorize(fmt.Sprintf("%v.", err), color.LightGray))
} }
}() }()

View File

@ -41,7 +41,7 @@ func printVersion(short bool) {
func printTuple(fmat, section, value string, outputColor color.Paint) { func printTuple(fmat, section, value string, outputColor color.Paint) {
if outputColor != -1 { if outputColor != -1 {
fmt.Printf(fmat, color.Colorize(section+":", outputColor), color.Colorize(value, color.White)) fmt.Printf(fmat, color.Colorize(section+":", outputColor), color.Colorize(value, color.LightGray))
return return
} }
fmt.Printf(fmat, section, value) fmt.Printf(fmat, section, value)

2
go.mod
View File

@ -30,7 +30,7 @@ replace (
require ( require (
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/atotto/clipboard v0.1.2 github.com/atotto/clipboard v0.1.2
github.com/derailed/tview v0.3.7 github.com/derailed/tview v0.3.8
github.com/drone/envsubst v1.0.2 // indirect github.com/drone/envsubst v1.0.2 // indirect
github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect
github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect

4
go.sum
View File

@ -128,6 +128,8 @@ github.com/deislabs/oras v0.7.0 h1:RnDoFd3tQYODMiUqxgQ8JxlrlWL0/VMKIKRD01MmNYk=
github.com/deislabs/oras v0.7.0/go.mod h1:sqMKPG3tMyIX9xwXUBRLhZ24o+uT4y6jgBD2RzUTKDM= github.com/deislabs/oras v0.7.0/go.mod h1:sqMKPG3tMyIX9xwXUBRLhZ24o+uT4y6jgBD2RzUTKDM=
github.com/derailed/tview v0.3.7 h1:q0eYai9blR6wAWz/+lo2Knacl/Pnv9YSfI4aYme1aok= github.com/derailed/tview v0.3.7 h1:q0eYai9blR6wAWz/+lo2Knacl/Pnv9YSfI4aYme1aok=
github.com/derailed/tview v0.3.7/go.mod h1:GJ3k/TIzEE+sj1L09/usk6HrkjsdadSsb03eHgPbcII= github.com/derailed/tview v0.3.7/go.mod h1:GJ3k/TIzEE+sj1L09/usk6HrkjsdadSsb03eHgPbcII=
github.com/derailed/tview v0.3.8 h1:P5UmN8piZ8SbbvdPF2gnGd2dNaU6xLZtyYFCgrbrELQ=
github.com/derailed/tview v0.3.8/go.mod h1:GJ3k/TIzEE+sj1L09/usk6HrkjsdadSsb03eHgPbcII=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
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/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
@ -709,6 +711,8 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
helm.sh/helm v1.2.1 h1:Jrn7kKQqQ/hnFWZEX+9pMFvYqFexkzrBnGqYBmIph7c=
helm.sh/helm v2.16.3+incompatible h1:a7P7FSGTBdK6ZsAcWWZZQXPIdzkgybD8CWd/Dy+jwf4=
helm.sh/helm/v3 v3.0.2 h1:BggvLisIMrAc+Is5oAHVrlVxgwOOrMN8nddfQbm5gKo= helm.sh/helm/v3 v3.0.2 h1:BggvLisIMrAc+Is5oAHVrlVxgwOOrMN8nddfQbm5gKo=
helm.sh/helm/v3 v3.0.2/go.mod h1:KBxE6XWO57XSNA1PA9CvVLYRY0zWqYQTad84bNXp1lw= helm.sh/helm/v3 v3.0.2/go.mod h1:KBxE6XWO57XSNA1PA9CvVLYRY0zWqYQTad84bNXp1lw=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -143,7 +143,6 @@ func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) {
} }
// CheckConnectivity return true if api server is cool or false otherwise. // CheckConnectivity return true if api server is cool or false otherwise.
// BOZO!! No super sure about this approach either??
func (a *APIClient) CheckConnectivity() (status bool) { func (a *APIClient) CheckConnectivity() (status bool) {
defer func() { defer func() {
if !status { if !status {

View File

@ -4,20 +4,22 @@ import (
"fmt" "fmt"
) )
const ColorFmt = "\x1b[%dm%s\x1b[0m"
// Paint describes a terminal color. // Paint describes a terminal color.
type Paint int type Paint int
// Defines basic ANSI colors. // Defines basic ANSI colors.
const ( const (
Black Paint = iota + 30 Black Paint = iota + 30 // 30
Red Red // 31
Green Green // 32
Yellow Yellow // 33
Blue Blue // 34
Magenta Magenta // 35
Cyan Cyan // 36
White LightGray // 37
DarkGray = 90 DarkGray = 90
Bold = 1 Bold = 1
) )
@ -25,7 +27,7 @@ const (
// Colorize returns an ASCII colored string based on given color. // Colorize returns an ASCII colored string based on given color.
func Colorize(s string, c Paint) string { func Colorize(s string, c Paint) string {
if c == 0 { if c == 0 {
c = White return s
} }
return fmt.Sprintf("\x1b[%dm%s\x1b[0m", c, s) return fmt.Sprintf(ColorFmt, c, s)
} }

View File

@ -1,26 +1,27 @@
package color package color_test
import ( import (
"testing" "testing"
"github.com/derailed/k9s/internal/color"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestColorize(t *testing.T) { func TestColorize(t *testing.T) {
uu := map[string]struct { uu := map[string]struct {
s string s string
c Paint c color.Paint
e string e string
}{ }{
"white": {"blee", White, "\x1b[37mblee\x1b[0m"}, "white": {"blee", color.LightGray, "\x1b[37mblee\x1b[0m"},
"black": {"blee", Black, "\x1b[30mblee\x1b[0m"}, "black": {"blee", color.Black, "\x1b[30mblee\x1b[0m"},
"default": {"blee", 0, "\x1b[37mblee\x1b[0m"}, "default": {"blee", 0, "blee"},
} }
for k := range uu { for k := range uu {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, Colorize(u.s, u.c)) assert.Equal(t, u.e, color.Colorize(u.s, u.c))
}) })
} }
} }

View File

@ -31,6 +31,18 @@ func NewAliases() *Aliases {
} }
} }
// Keys returns all aliases keys.
func (a *Aliases) Keys() []string {
a.mx.RLock()
defer a.mx.RUnlock()
ss := make([]string, 0, len(a.Alias))
for k := range a.Alias {
ss = append(ss, k)
}
return ss
}
// ShortNames return all shortnames. // ShortNames return all shortnames.
func (a *Aliases) ShortNames() ShortNames { func (a *Aliases) ShortNames() ShortNames {
a.mx.RLock() a.mx.RLock()
@ -107,6 +119,13 @@ func (a *Aliases) LoadFileAliases(path string) error {
return nil return nil
} }
func (a *Aliases) declare(key string, aliases ...string) {
a.Alias[key] = key
for _, alias := range aliases {
a.Alias[alias] = key
}
}
func (a *Aliases) loadDefaultAliases() { func (a *Aliases) loadDefaultAliases() {
a.mx.Lock() a.mx.Lock()
defer a.mx.Unlock() defer a.mx.Unlock()
@ -120,49 +139,16 @@ func (a *Aliases) loadDefaultAliases() {
a.Alias["rb"] = "rbac.authorization.k8s.io/v1/rolebindings" a.Alias["rb"] = "rbac.authorization.k8s.io/v1/rolebindings"
a.Alias["np"] = "networking.k8s.io/v1/networkpolicies" a.Alias["np"] = "networking.k8s.io/v1/networkpolicies"
const contexts = "contexts" a.declare("help", "h", "?")
{ a.declare("quit", "q", "Q")
a.Alias["ctx"] = contexts a.declare("aliases", "alias", "a")
a.Alias[contexts] = contexts a.declare("contexts", "context", "ctx")
a.Alias["context"] = contexts a.declare("users", "user", "usr")
} a.declare("groups", "group", "grp")
const users = "users" a.declare("portforwards", "portforward", "pf")
{ a.declare("benchmarks", "benchmark", "be")
a.Alias["usr"] = users a.declare("screendumps", "screendump", "sd")
a.Alias[users] = users a.declare("pulses", "pulse", "pu", "hz")
a.Alias["user"] = users
}
const groups = "groups"
{
a.Alias["grp"] = groups
a.Alias["group"] = groups
a.Alias[groups] = groups
}
const portFwds = "portforwards"
{
a.Alias["pf"] = portFwds
a.Alias[portFwds] = portFwds
a.Alias["portforward"] = portFwds
}
const benchmarks = "benchmarks"
{
a.Alias["be"] = benchmarks
a.Alias["benchmark"] = benchmarks
a.Alias[benchmarks] = benchmarks
}
const dumps = "screendumps"
{
a.Alias["sd"] = dumps
a.Alias["screendump"] = dumps
a.Alias[dumps] = dumps
}
const pulses = "pulses"
{
a.Alias["hz"] = pulses
a.Alias["pu"] = pulses
a.Alias["pulse"] = pulses
a.Alias["pulses"] = pulses
}
} }
// Save alias to disk. // Save alias to disk.

View File

@ -97,7 +97,7 @@ func TestConfigLoad(t *testing.T) {
assert.Equal(t, 2, cfg.K9s.RefreshRate) assert.Equal(t, 2, cfg.K9s.RefreshRate)
assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize) assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize)
assert.Equal(t, 200, cfg.K9s.Logger.TailCount) assert.Equal(t, int64(200), cfg.K9s.Logger.TailCount)
assert.Equal(t, "minikube", cfg.K9s.CurrentContext) assert.Equal(t, "minikube", cfg.K9s.CurrentContext)
assert.Equal(t, "minikube", cfg.K9s.CurrentCluster) assert.Equal(t, "minikube", cfg.K9s.CurrentCluster)
assert.NotNil(t, cfg.K9s.Clusters) assert.NotNil(t, cfg.K9s.Clusters)
@ -266,6 +266,7 @@ var expectedConfig = `k9s:
logger: logger:
tail: 500 tail: 500
buffer: 800 buffer: 800
sinceSeconds: 300
currentContext: blee currentContext: blee
currentCluster: blee currentCluster: blee
fullScreenLogs: false fullScreenLogs: false
@ -314,7 +315,8 @@ var resetConfig = `k9s:
readOnly: false readOnly: false
logger: logger:
tail: 200 tail: 200
buffer: 2000 buffer: 1000
sinceSeconds: 300
currentContext: blee currentContext: blee
currentCluster: blee currentCluster: blee
fullScreenLogs: false fullScreenLogs: false

View File

@ -22,7 +22,7 @@ func TestK9sValidate(t *testing.T) {
c.Validate(mc, mk) c.Validate(mc, mk)
assert.Equal(t, 2, c.RefreshRate) assert.Equal(t, 2, c.RefreshRate)
assert.Equal(t, 50, c.Logger.TailCount) assert.Equal(t, int64(100), c.Logger.TailCount)
assert.Equal(t, 1_000, c.Logger.BufferSize) assert.Equal(t, 1_000, c.Logger.BufferSize)
assert.Equal(t, "ctx1", c.CurrentContext) assert.Equal(t, "ctx1", c.CurrentContext)
assert.Equal(t, "c1", c.CurrentCluster) assert.Equal(t, "c1", c.CurrentCluster)
@ -45,7 +45,7 @@ func TestK9sValidateBlank(t *testing.T) {
c.Validate(mc, mk) c.Validate(mc, mk)
assert.Equal(t, 2, c.RefreshRate) assert.Equal(t, 2, c.RefreshRate)
assert.Equal(t, 50, c.Logger.TailCount) assert.Equal(t, int64(100), c.Logger.TailCount)
assert.Equal(t, 1_000, c.Logger.BufferSize) assert.Equal(t, 1_000, c.Logger.BufferSize)
assert.Equal(t, "ctx1", c.CurrentContext) assert.Equal(t, "ctx1", c.CurrentContext)
assert.Equal(t, "c1", c.CurrentCluster) assert.Equal(t, "c1", c.CurrentCluster)

View File

@ -5,25 +5,29 @@ import (
) )
const ( const (
// DefaultLoggerTailCount tracks log tail size. // DefaultLoggerTailCount tracks default log tail size.
DefaultLoggerTailCount = 50 DefaultLoggerTailCount = 100
// DefaultLoggerBufferSize tracks the buffer size. // DefaultLoggerBufferSize tracks default view buffer size.
DefaultLoggerBufferSize = 1_000 DefaultLoggerBufferSize = 1_000
// MaxLogThreshold sets the max value for log size. // MaxLogThreshold sets the max value for log size.
MaxLogThreshold = 5_000 MaxLogThreshold = 1_000
// DefaultSinceSeconds tracks default log age.
DefaultSinceSeconds = 5 * 60 // 5mins
) )
// Logger tracks logger options // Logger tracks logger options
type Logger struct { type Logger struct {
TailCount int `yaml:"tail"` TailCount int64 `yaml:"tail"`
BufferSize int `yaml:"buffer"` BufferSize int `yaml:"buffer"`
SinceSeconds int64 `yaml:"sinceSeconds"`
} }
// NewLogger returns a new instance. // NewLogger returns a new instance.
func NewLogger() *Logger { func NewLogger() *Logger {
return &Logger{ return &Logger{
TailCount: DefaultLoggerTailCount, TailCount: DefaultLoggerTailCount,
BufferSize: DefaultLoggerBufferSize, BufferSize: DefaultLoggerBufferSize,
SinceSeconds: DefaultSinceSeconds,
} }
} }
@ -41,4 +45,7 @@ func (l *Logger) Validate(_ client.Connection, _ KubeSettings) {
if l.BufferSize > MaxLogThreshold { if l.BufferSize > MaxLogThreshold {
l.BufferSize = MaxLogThreshold l.BufferSize = MaxLogThreshold
} }
if l.SinceSeconds == 0 {
l.SinceSeconds = DefaultSinceSeconds
}
} }

View File

@ -11,7 +11,7 @@ func TestNewLogger(t *testing.T) {
l := config.NewLogger() l := config.NewLogger()
l.Validate(nil, nil) l.Validate(nil, nil)
assert.Equal(t, 50, l.TailCount) assert.Equal(t, int64(100), l.TailCount)
assert.Equal(t, 1_000, l.BufferSize) assert.Equal(t, 1_000, l.BufferSize)
} }
@ -19,6 +19,6 @@ func TestLoggerValidate(t *testing.T) {
var l config.Logger var l config.Logger
l.Validate(nil, nil) l.Validate(nil, nil)
assert.Equal(t, 50, l.TailCount) assert.Equal(t, int64(100), l.TailCount)
assert.Equal(t, 1_000, l.BufferSize) assert.Equal(t, 1_000, l.BufferSize)
} }

View File

@ -77,6 +77,13 @@ type (
// Log tracks Log styles. // Log tracks Log styles.
Log struct { Log struct {
FgColor Color `yaml:"fgColor"`
BgColor Color `yaml:"bgColor"`
Indicator LogIndicator `yaml:"indicator"`
}
// LogIndicator tracks log view indicator.
LogIndicator struct {
FgColor Color `yaml:"fgColor"` FgColor Color `yaml:"fgColor"`
BgColor Color `yaml:"bgColor"` BgColor Color `yaml:"bgColor"`
} }
@ -259,15 +266,21 @@ func newStatus() Status {
} }
} }
// NewLog returns a new log style.
func newLog() Log { func newLog() Log {
return Log{ return Log{
FgColor: "lightskyblue", FgColor: "lightskyblue",
BgColor: "black",
Indicator: newLogIndicator(),
}
}
func newLogIndicator() LogIndicator {
return LogIndicator{
FgColor: "dodgerblue",
BgColor: "black", BgColor: "black",
} }
} }
// NewYaml returns a new yaml style.
func newYaml() Yaml { func newYaml() Yaml {
return Yaml{ return Yaml{
KeyColor: "steelblue", KeyColor: "steelblue",
@ -276,7 +289,6 @@ func newYaml() Yaml {
} }
} }
// NewTitle returns a new title style.
func newTitle() Title { func newTitle() Title {
return Title{ return Title{
FgColor: "aqua", FgColor: "aqua",
@ -287,7 +299,6 @@ func newTitle() Title {
} }
} }
// NewInfo returns a new info style.
func newInfo() Info { func newInfo() Info {
return Info{ return Info{
SectionColor: "white", SectionColor: "white",
@ -295,7 +306,6 @@ func newInfo() Info {
} }
} }
// NewXray returns a new xray style.
func newXray() Xray { func newXray() Xray {
return Xray{ return Xray{
FgColor: "aqua", FgColor: "aqua",
@ -306,7 +316,6 @@ func newXray() Xray {
} }
} }
// NewTable returns a new table style.
func newTable() Table { func newTable() Table {
return Table{ return Table{
FgColor: "aqua", FgColor: "aqua",
@ -317,7 +326,6 @@ func newTable() Table {
} }
} }
// NewTableHeader returns a new table header style.
func newTableHeader() TableHeader { func newTableHeader() TableHeader {
return TableHeader{ return TableHeader{
FgColor: "white", FgColor: "white",
@ -326,7 +334,6 @@ func newTableHeader() TableHeader {
} }
} }
// NewCrumb returns a new crumbs style.
func newCrumb() Crumb { func newCrumb() Crumb {
return Crumb{ return Crumb{
FgColor: "black", FgColor: "black",
@ -335,7 +342,6 @@ func newCrumb() Crumb {
} }
} }
// NewBorder returns a new border style.
func newBorder() Border { func newBorder() Border {
return Border{ return Border{
FgColor: "dodgerblue", FgColor: "dodgerblue",
@ -343,7 +349,6 @@ func newBorder() Border {
} }
} }
// NewMenu returns a new menu style.
func newMenu() Menu { func newMenu() Menu {
return Menu{ return Menu{
FgColor: "white", FgColor: "white",
@ -464,6 +469,7 @@ func (s *Styles) Load(path string) error {
func (s *Styles) Update() { func (s *Styles) Update() {
tview.Styles.PrimitiveBackgroundColor = s.BgColor() tview.Styles.PrimitiveBackgroundColor = s.BgColor()
tview.Styles.ContrastBackgroundColor = s.BgColor() tview.Styles.ContrastBackgroundColor = s.BgColor()
tview.Styles.MoreContrastBackgroundColor = s.BgColor()
tview.Styles.PrimaryTextColor = s.FgColor() tview.Styles.PrimaryTextColor = s.FgColor()
tview.Styles.BorderColor = s.K9s.Frame.Border.FgColor.Color() tview.Styles.BorderColor = s.K9s.Frame.Border.FgColor.Color()
tview.Styles.FocusColor = s.K9s.Frame.Border.FocusColor.Color() tview.Styles.FocusColor = s.K9s.Frame.Border.FocusColor.Color()

View File

@ -2,7 +2,6 @@ package dao
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
@ -13,7 +12,6 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
restclient "k8s.io/client-go/rest"
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
) )
@ -60,37 +58,12 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error
} }
// TailLogs tails a given container logs // TailLogs tails a given container logs
func (c *Container) TailLogs(ctx context.Context, logChan chan<- []byte, opts LogOptions) error { func (c *Container) TailLogs(ctx context.Context, logChan LogChan, opts LogOptions) error {
fac, ok := ctx.Value(internal.KeyFactory).(Factory) log.Debug().Msgf("CONTAINER-LOGS")
if !ok { po := Pod{}
return errors.New("Expecting an informer") po.Init(c.Factory, client.NewGVR("v1/pods"))
}
o, err := fac.Get("v1/pods", opts.Path, true, labels.Everything())
if err != nil {
return err
}
var po v1.Pod return po.TailLogs(ctx, logChan, opts)
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil {
return err
}
return tailLogs(ctx, c, logChan, opts)
}
// Logs fetch container logs for a given pod and container.
func (c *Container) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) {
ns, _ := client.Namespaced(path)
auth, err := c.Client().CanI(ns, "v1/pods:log", client.GetAccess)
if err != nil {
return nil, err
}
if !auth {
return nil, fmt.Errorf("user is not authorized to view pod logs")
}
ns, n := client.Namespaced(path)
return c.Client().DialOrDie().CoreV1().Pods(ns).GetLogs(n, opts), nil
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View File

@ -80,7 +80,7 @@ func (d *Deployment) Restart(path string) error {
} }
// TailLogs tail logs for all pods represented by this Deployment. // TailLogs tail logs for all pods represented by this Deployment.
func (d *Deployment) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { func (d *Deployment) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
dp, err := d.Load(d.Factory, opts.Path) dp, err := d.Load(d.Factory, opts.Path)
if err != nil { if err != nil {
return err return err

View File

@ -61,7 +61,7 @@ func (d *DaemonSet) Restart(path string) error {
} }
// TailLogs tail logs for all pods represented by this DaemonSet. // TailLogs tail logs for all pods represented by this DaemonSet.
func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { func (d *DaemonSet) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
ds, err := d.GetInstance(opts.Path) ds, err := d.GetInstance(opts.Path)
if err != nil { if err != nil {
return err return err
@ -74,7 +74,7 @@ func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptio
return podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts) return podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts)
} }
func podLogs(ctx context.Context, c chan<- []byte, sel map[string]string, opts LogOptions) error { func podLogs(ctx context.Context, c LogChan, sel map[string]string, opts LogOptions) error {
f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
if !ok { if !ok {
return errors.New("expecting a context factory") return errors.New("expecting a context factory")
@ -89,14 +89,11 @@ func podLogs(ctx context.Context, c chan<- []byte, sel map[string]string, opts L
} }
ns, _ := client.Namespaced(opts.Path) ns, _ := client.Namespaced(opts.Path)
oo, err := f.List("v1/pods", ns, false, lsel) oo, err := f.List("v1/pods", ns, true, lsel)
if err != nil { if err != nil {
return err return err
} }
opts.MultiPods = true
if len(oo) > 1 {
opts.MultiPods = true
}
po := Pod{} po := Pod{}
po.Init(f, client.NewGVR("v1/pods")) po.Init(f, client.NewGVR("v1/pods"))

View File

@ -12,6 +12,14 @@ import (
"k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/printers"
) )
// IsFuzzySelector checks if filter is fuzzy or not.
func IsFuzzySelector(s string) bool {
if s == "" {
return false
}
return fuzzyRx.MatchString(s)
}
func toPerc(v1, v2 float64) float64 { func toPerc(v1, v2 float64) float64 {
if v2 == 0 { if v2 == 0 {
return 0 return 0

View File

@ -23,7 +23,7 @@ type Job struct {
} }
// TailLogs tail logs for all pods represented by this Job. // TailLogs tail logs for all pods represented by this Job.
func (j *Job) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { func (j *Job) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
o, err := j.Factory.Get(j.gvr.String(), opts.Path, true, labels.Everything()) o, err := j.Factory.Get(j.gvr.String(), opts.Path, true, labels.Everything())
if err != nil { if err != nil {
return err return err

141
internal/dao/log_item.go Normal file
View File

@ -0,0 +1,141 @@
package dao
import (
"bytes"
"fmt"
"regexp"
"strings"
"time"
"github.com/rs/zerolog/log"
"github.com/sahilm/fuzzy"
)
// LogChan represents a channel for logs.
type LogChan chan *LogItem
// LogItem represents a container log line.
type LogItem struct {
Pod, Container, Timestamp string
Bytes []byte
}
// NewLogItem returns a new item.
func NewLogItem(b []byte) *LogItem {
space := []byte(" ")
var l LogItem
cols := bytes.Split(b[:len(b)-1], space)
l.Timestamp = string(cols[0])
l.Bytes = bytes.Join(cols[1:], space)
return &l
}
// NewLogItemFromString returns a new item.
func NewLogItemFromString(s string) *LogItem {
l := LogItem{Bytes: []byte(s)}
l.Timestamp = time.Now().String()
return &l
}
// IsEmpty checks if the entry is empty.
func (l *LogItem) IsEmpty() bool {
return len(l.Bytes) == 0
}
// Render returns a log line as string.
func (l *LogItem) Render(showTime bool) []byte {
bb := make([]byte, 0, 100+len(l.Bytes))
if showTime {
bb = append(bb, fmt.Sprintf("%-30s ", l.Timestamp)...)
}
if l.Pod != "" {
bb = append(bb, l.Pod...)
bb = append(bb, ':')
bb = append(bb, l.Container...)
bb = append(bb, ' ')
} else if l.Container != "" {
bb = append(bb, l.Container...)
bb = append(bb, ' ')
}
bb = append(bb, l.Bytes...)
return bb
}
// ----------------------------------------------------------------------------
// LogItems represents a collection of log items.
type LogItems []*LogItem
// Lines returns a collection of log lines.
func (l LogItems) Lines() []string {
ll := make([]string, len(l))
for i, item := range l {
ll[i] = string(item.Render(false))
}
return ll
}
// Render returns logs as a collection of strings.
func (l LogItems) Render(showTime bool, ll [][]byte) {
for i, item := range l {
ll[i] = item.Render(showTime)
}
}
// DumpDebug for debuging
func (l LogItems) DumpDebug(m string) {
fmt.Println(m + strings.Repeat("-", 50))
for i, line := range l {
fmt.Println(i, string(line.Bytes))
}
}
// Filter filters out log items based on given filter.
func (l LogItems) Filter(q string) ([]int, error) {
if q == "" {
return nil, nil
}
if IsFuzzySelector(q) {
return l.fuzzyFilter(strings.TrimSpace(q[2:])), nil
}
indexes, err := l.filterLogs(q)
if err != nil {
log.Error().Err(err).Msgf("Logs filter failed")
return nil, err
}
return indexes, nil
}
var fuzzyRx = regexp.MustCompile(`\A\-f`)
func (l LogItems) fuzzyFilter(q string) []int {
q = strings.TrimSpace(q)
matches := make([]int, 0, len(l))
mm := fuzzy.Find(q, l.Lines())
for _, m := range mm {
matches = append(matches, m.Index)
}
return matches
}
func (l LogItems) filterLogs(q string) ([]int, error) {
rx, err := regexp.Compile(`(?i)` + q)
if err != nil {
return nil, err
}
matches := make([]int, 0, len(l))
for i, line := range l.Lines() {
if rx.MatchString(line) {
matches = append(matches, i)
}
}
return matches, nil
}

View File

@ -0,0 +1,200 @@
package dao_test
import (
"fmt"
"testing"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
)
func init() {
zerolog.SetGlobalLevel(zerolog.FatalLevel)
}
func TestLogItemsFilter(t *testing.T) {
uu := map[string]struct {
q string
opts dao.LogOptions
e []int
err error
}{
"empty": {
opts: dao.LogOptions{},
},
"pod-name": {
q: "blee",
opts: dao.LogOptions{
Path: "fred/blee",
Container: "c1",
},
e: []int{0, 1, 2},
},
"container-name": {
q: "c1",
opts: dao.LogOptions{
Path: "fred/blee",
Container: "c1",
},
e: []int{0, 1, 2},
},
"message": {
q: "zorg",
opts: dao.LogOptions{
Path: "fred/blee",
Container: "c1",
},
e: []int{2},
},
"fuzzy": {
q: "-f zorg",
opts: dao.LogOptions{
Path: "fred/blee",
Container: "c1",
},
e: []int{2},
},
}
for k := range uu {
u := uu[k]
ii := dao.LogItems{
dao.NewLogItem([]byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3..."))),
dao.NewLogItemFromString("Bumble bee tuna"),
dao.NewLogItemFromString("Jean Batiste Emmanuel Zorg"),
}
t.Run(k, func(t *testing.T) {
_, n := client.Namespaced(u.opts.Path)
for _, i := range ii {
i.Pod, i.Container = n, u.opts.Container
}
res, err := ii.Filter(u.q)
assert.Equal(t, u.err, err)
if err == nil {
assert.Equal(t, u.e, res)
}
})
}
}
func TestLogItemsRender(t *testing.T) {
uu := map[string]struct {
opts dao.LogOptions
e string
}{
"empty": {
opts: dao.LogOptions{},
e: "Testing 1,2,3...",
},
"container": {
opts: dao.LogOptions{
Container: "fred",
},
e: "fred Testing 1,2,3...",
},
"pod": {
opts: dao.LogOptions{
Path: "blee/fred",
Container: "blee",
},
e: "fred:blee Testing 1,2,3...",
},
"full": {
opts: dao.LogOptions{
Path: "blee/fred",
Container: "blee",
ShowTimestamp: true,
},
e: "2018-12-14T10:36:43.326972-07:00 fred:blee Testing 1,2,3...",
},
}
s := []byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3..."))
ii := dao.LogItems{dao.NewLogItem(s)}
for k := range uu {
u := uu[k]
_, n := client.Namespaced(u.opts.Path)
ii[0].Pod, ii[0].Container = n, u.opts.Container
t.Run(k, func(t *testing.T) {
res := make([][]byte, 1)
ii.Render(u.opts.ShowTimestamp, res)
assert.Equal(t, u.e, string(res[0]))
})
}
}
func TestLogItemEmpty(t *testing.T) {
uu := map[string]struct {
s string
e bool
}{
"empty": {s: "", e: true},
"full": {s: "Testing 1,2,3..."},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
i := dao.NewLogItemFromString(u.s)
assert.Equal(t, u.e, i.IsEmpty())
})
}
}
func TestLogItemRender(t *testing.T) {
uu := map[string]struct {
opts dao.LogOptions
e string
}{
"empty": {
opts: dao.LogOptions{},
e: "Testing 1,2,3...",
},
"container": {
opts: dao.LogOptions{
Container: "fred",
},
e: "fred Testing 1,2,3...",
},
"pod": {
opts: dao.LogOptions{
Path: "blee/fred",
Container: "blee",
},
e: "fred:blee Testing 1,2,3...",
},
"full": {
opts: dao.LogOptions{
Path: "blee/fred",
Container: "blee",
ShowTimestamp: true,
},
e: "2018-12-14T10:36:43.326972-07:00 fred:blee Testing 1,2,3...",
},
}
s := []byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3..."))
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
i := dao.NewLogItem(s)
_, n := client.Namespaced(u.opts.Path)
i.Pod, i.Container = n, u.opts.Container
assert.Equal(t, u.e, string(i.Render(u.opts.ShowTimestamp)))
})
}
}
func BenchmarkLogItemRender(b *testing.B) {
s := []byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3..."))
i := dao.NewLogItem(s)
i.Pod, i.Container = "fred", "blee"
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
i.Render(true)
}
}

View File

@ -2,9 +2,11 @@ package dao
import ( import (
"strings" "strings"
"time"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/color" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
// LogOptions represent logger options. // LogOptions represent logger options.
@ -12,11 +14,13 @@ type LogOptions struct {
Path string Path string
Container string Container string
Lines int64 Lines int64
Color color.Paint
Previous bool Previous bool
SingleContainer bool SingleContainer bool
MultiPods bool MultiPods bool
ShowTimestamp bool ShowTimestamp bool
SinceTime string
SinceSeconds int64
In, Out string
} }
// HasContainer checks if a container is present. // HasContainer checks if a container is present.
@ -24,6 +28,33 @@ func (o LogOptions) HasContainer() bool {
return o.Container != "" return o.Container != ""
} }
// ToPodLogOptions returns pod log options.
func (o LogOptions) ToPodLogOptions() *v1.PodLogOptions {
opts := v1.PodLogOptions{
Follow: true,
Timestamps: true,
Container: o.Container,
Previous: o.Previous,
TailLines: &o.Lines,
}
if o.SinceSeconds < 0 {
return &opts
}
if o.SinceSeconds != 0 {
opts.SinceSeconds = &o.SinceSeconds
return &opts
}
if o.SinceTime == "" {
return &opts
}
if t, err := time.Parse(time.RFC3339, o.SinceTime); err == nil {
opts.SinceTime = &metav1.Time{Time: t.Add(time.Second)}
}
return &opts
}
// FixedSizeName returns a normalize fixed size pod name if possible. // FixedSizeName returns a normalize fixed size pod name if possible.
func (o LogOptions) FixedSizeName() string { func (o LogOptions) FixedSizeName() string {
_, n := client.Namespaced(o.Path) _, n := client.Namespaced(o.Path)
@ -39,35 +70,21 @@ func (o LogOptions) FixedSizeName() string {
return Truncate(strings.Join(s, "-"), 15) + "-" + tokens[len(tokens)-1] return Truncate(strings.Join(s, "-"), 15) + "-" + tokens[len(tokens)-1]
} }
func colorize(c color.Paint, txt string) string {
if c == 0 {
return ""
}
return color.Colorize(txt, c)
}
// DecorateLog add a log header to display po/co information along with the log message. // DecorateLog add a log header to display po/co information along with the log message.
func (o LogOptions) DecorateLog(bytes []byte) []byte { func (o LogOptions) DecorateLog(bytes []byte) *LogItem {
item := NewLogItem(bytes)
if len(bytes) == 0 { if len(bytes) == 0 {
return bytes return item
} }
bytes = bytes[:len(bytes)-1]
_, n := client.Namespaced(o.Path)
var prefix []byte
if o.MultiPods { if o.MultiPods {
prefix = []byte(colorize(o.Color, n+":"+o.Container+" ")) _, pod := client.Namespaced(o.Path)
item.Pod, item.Container = pod, o.Container
} }
if !o.SingleContainer { if !o.SingleContainer {
prefix = []byte(colorize(o.Color, o.Container+" ")) item.Container = o.Container
} }
if len(prefix) == 0 { return item
return bytes
}
return append(prefix, bytes...)
} }

View File

@ -9,7 +9,6 @@ import (
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/color"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/watch" "github.com/derailed/k9s/internal/watch"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -171,14 +170,8 @@ func (p *Pod) GetInstance(fqn string) (*v1.Pod, error) {
} }
// TailLogs tails a given container logs // TailLogs tails a given container logs
func (p *Pod) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
if !opts.HasContainer() { log.Debug().Msgf("TAIL-LOGS for %q:%q", opts.Path, opts.Container)
return p.logs(ctx, c, opts)
}
return tailLogs(ctx, p, c, opts)
}
func (p *Pod) logs(ctx context.Context, c chan<- []byte, opts LogOptions) error {
fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
if !ok { if !ok {
return errors.New("Expecting an informer") return errors.New("Expecting an informer")
@ -192,41 +185,51 @@ func (p *Pod) logs(ctx context.Context, c chan<- []byte, opts LogOptions) error
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil { if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil {
return err return err
} }
opts.Color = asColor(po.Name)
rcos := loggableContainers(po.Status)
if opts.HasContainer() {
opts.SingleContainer = true
if !in(rcos, opts.Container) {
return fmt.Errorf("no logs found for container %s on %s", opts.Container, opts.Path)
}
if err := tailLogs(ctx, p, c, opts); err != nil {
log.Error().Err(err).Msgf("Getting logs for %s failed", opts.Container)
return err
}
return nil
}
if len(po.Spec.InitContainers)+len(po.Spec.Containers) == 1 { if len(po.Spec.InitContainers)+len(po.Spec.Containers) == 1 {
opts.SingleContainer = true opts.SingleContainer = true
} }
var tailed bool
for _, co := range po.Spec.InitContainers { for _, co := range po.Spec.InitContainers {
opts.Container = co.Name opts.Container = co.Name
if err := p.TailLogs(ctx, c, opts); err != nil { if err := p.TailLogs(ctx, c, opts); err != nil {
return err return err
} }
tailed = true
} }
rcos := loggableContainers(po.Status)
for _, co := range po.Spec.Containers { for _, co := range po.Spec.Containers {
if in(rcos, co.Name) { if in(rcos, co.Name) {
opts.Container = co.Name opts.Container = co.Name
if err := p.TailLogs(ctx, c, opts); err != nil { if err := tailLogs(ctx, p, c, opts); err != nil {
log.Error().Err(err).Msgf("Getting logs for %s failed", co.Name) log.Error().Err(err).Msgf("Getting logs for %s failed", co.Name)
return err return err
} }
tailed = true
} }
} }
if !tailed {
return fmt.Errorf("no loggable containers found for pod %s", opts.Path)
}
return nil return nil
} }
func tailLogs(ctx context.Context, logger Logger, c chan<- []byte, opts LogOptions) error { func tailLogs(ctx context.Context, logger Logger, c LogChan, opts LogOptions) error {
log.Debug().Msgf("Tailing logs for %q -- %q", opts.Path, opts.Container) log.Debug().Msgf("Tailing logs for %q:%q", opts.Path, opts.Container)
o := v1.PodLogOptions{ req, err := logger.Logs(opts.Path, opts.ToPodLogOptions())
Follow: true,
Timestamps: false,
Container: opts.Container,
Previous: opts.Previous,
TailLines: &opts.Lines,
}
req, err := logger.Logs(opts.Path, &o)
if err != nil { if err != nil {
return err return err
} }
@ -236,15 +239,15 @@ func tailLogs(ctx context.Context, logger Logger, c chan<- []byte, opts LogOptio
stream, err := req.Stream() stream, err := req.Stream()
if err != nil { if err != nil {
c <- opts.DecorateLog([]byte(err.Error() + "\n")) c <- opts.DecorateLog([]byte(err.Error() + "\n"))
log.Error().Err(err).Msgf("Log stream failed for `%s", opts.Path) log.Error().Err(err).Msgf("Unable to obtain log stream failed for `%s", opts.Path)
return fmt.Errorf("Unable to obtain log stream for %s", opts.Path) return err
} }
go readLogs(stream, c, opts) go readLogs(stream, c, opts)
return nil return nil
} }
func readLogs(stream io.ReadCloser, c chan<- []byte, opts LogOptions) { func readLogs(stream io.ReadCloser, c LogChan, opts LogOptions) {
defer func() { defer func() {
log.Debug().Msgf(">>> Closing stream `%s", opts.Path) log.Debug().Msgf(">>> Closing stream `%s", opts.Path)
if err := stream.Close(); err != nil { if err := stream.Close(); err != nil {
@ -258,11 +261,12 @@ func readLogs(stream io.ReadCloser, c chan<- []byte, opts LogOptions) {
if err != nil { if err != nil {
log.Warn().Err(err).Msg("Read error") log.Warn().Err(err).Msg("Read error")
if err == io.EOF { if err == io.EOF {
c <- opts.DecorateLog([]byte("<STREAM> closed\n")) log.Warn().Err(err).Msgf("stream closed")
c <- NewLogItemFromString("<STREAM> closed")
return return
} }
log.Error().Err(err).Msgf("stream reader failed") log.Error().Err(err).Msgf("stream reader failed")
c <- opts.DecorateLog([]byte("<STREAM> failed\n")) c <- NewLogItemFromString("<STREAM> failed")
return return
} }
c <- opts.DecorateLog(bytes) c <- opts.DecorateLog(bytes)
@ -331,19 +335,13 @@ func extractFQN(o runtime.Object) string {
func loggableContainers(s v1.PodStatus) []string { func loggableContainers(s v1.PodStatus) []string {
var rcos []string var rcos []string
for _, c := range s.ContainerStatuses { for _, c := range s.ContainerStatuses {
rcos = append(rcos, c.Name) if c.State.Waiting == nil {
rcos = append(rcos, c.Name)
}
} }
return rcos return rcos
} }
func asColor(n string) color.Paint {
var sum int
for _, r := range n {
sum += int(r)
}
return color.Paint(30 + 2 + sum%6)
}
// Check if string is in a string list. // Check if string is in a string list.
func in(ll []string, s string) bool { func in(ll []string, s string) bool {
for _, l := range ll { for _, l := range ll {

View File

@ -81,7 +81,7 @@ func (s *StatefulSet) Restart(path string) error {
} }
// TailLogs tail logs for all pods represented by this StatefulSet. // TailLogs tail logs for all pods represented by this StatefulSet.
func (s *StatefulSet) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { func (s *StatefulSet) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
sts, err := s.getStatefulSet(opts.Path) sts, err := s.getStatefulSet(opts.Path)
if err != nil { if err != nil {
return errors.New("expecting StatefulSet resource") return errors.New("expecting StatefulSet resource")

View File

@ -24,7 +24,7 @@ type Service struct {
} }
// TailLogs tail logs for all pods represented by this Service. // TailLogs tail logs for all pods represented by this Service.
func (s *Service) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { func (s *Service) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
svc, err := s.GetInstance(opts.Path) svc, err := s.GetInstance(opts.Path)
if err != nil { if err != nil {
return err return err

View File

@ -93,7 +93,7 @@ type NodeMaintainer interface {
// Loggable represents resources with logs. // Loggable represents resources with logs.
type Loggable interface { type Loggable interface {
// TaiLogs streams resource logs. // TaiLogs streams resource logs.
TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error TailLogs(ctx context.Context, c LogChan, opts LogOptions) error
} }
// Describer describes a resource. // Describer describes a resource.

View File

@ -1,37 +1,35 @@
package ui package model
const maxBuff = 10 const maxBuff = 10
const ( const (
// CommandBuff indicates a command buffer. // Command represents a command buffer.
CommandBuff BufferKind = 1 << iota Command BufferKind = 1 << iota
// FilterBuff indicates a search buffer. // Filter represents a filter buffer.
FilterBuff Filter
) )
type ( // BufferKind indicates a buffer type
// BufferKind indicates a buffer type type BufferKind int8
BufferKind int8
// BuffWatcher represents a command buffer listener. // BuffWatcher represents a command buffer listener.
BuffWatcher interface { type BuffWatcher interface {
// Changed indicates the buffer was changed. // Changed indicates the buffer was changed.
BufferChanged(s string) BufferChanged(s string)
// Active indicates the buff activity changed. // Active indicates the buff activity changed.
BufferActive(state bool, kind BufferKind) BufferActive(state bool, kind BufferKind)
} }
// CmdBuff represents user command input. // CmdBuff represents user command input.
CmdBuff struct { type CmdBuff struct {
buff []rune buff []rune
listeners []BuffWatcher listeners []BuffWatcher
hotKey rune hotKey rune
kind BufferKind kind BufferKind
sticky bool sticky bool
active bool active bool
} }
)
// NewCmdBuff returns a new command buffer. // NewCmdBuff returns a new command buffer.
func NewCmdBuff(key rune, kind BufferKind) *CmdBuff { func NewCmdBuff(key rune, kind BufferKind) *CmdBuff {

View File

@ -1,9 +1,9 @@
package ui_test package model_test
import ( import (
"testing" "testing"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/model"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -17,7 +17,7 @@ func (l *testListener) BufferChanged(s string) {
l.text = s l.text = s
} }
func (l *testListener) BufferActive(s bool, _ ui.BufferKind) { func (l *testListener) BufferActive(s bool, _ model.BufferKind) {
if s { if s {
l.act++ l.act++
return return
@ -26,7 +26,7 @@ func (l *testListener) BufferActive(s bool, _ ui.BufferKind) {
} }
func TestCmdBuffActivate(t *testing.T) { func TestCmdBuffActivate(t *testing.T) {
b, l := ui.NewCmdBuff('>', ui.CommandBuff), testListener{} b, l := model.NewCmdBuff('>', model.Command), testListener{}
b.AddListener(&l) b.AddListener(&l)
b.SetActive(true) b.SetActive(true)
@ -36,7 +36,7 @@ func TestCmdBuffActivate(t *testing.T) {
} }
func TestCmdBuffDeactivate(t *testing.T) { func TestCmdBuffDeactivate(t *testing.T) {
b, l := ui.NewCmdBuff('>', ui.CommandBuff), testListener{} b, l := model.NewCmdBuff('>', model.Command), testListener{}
b.AddListener(&l) b.AddListener(&l)
b.SetActive(false) b.SetActive(false)
@ -46,7 +46,7 @@ func TestCmdBuffDeactivate(t *testing.T) {
} }
func TestCmdBuffChanged(t *testing.T) { func TestCmdBuffChanged(t *testing.T) {
b, l := ui.NewCmdBuff('>', ui.CommandBuff), testListener{} b, l := model.NewCmdBuff('>', model.Command), testListener{}
b.AddListener(&l) b.AddListener(&l)
b.Add('b') b.Add('b')
@ -78,7 +78,7 @@ func TestCmdBuffChanged(t *testing.T) {
} }
func TestCmdBuffAdd(t *testing.T) { func TestCmdBuffAdd(t *testing.T) {
b := ui.NewCmdBuff('>', ui.CommandBuff) b := model.NewCmdBuff('>', model.Command)
uu := []struct { uu := []struct {
runes []rune runes []rune
@ -99,7 +99,7 @@ func TestCmdBuffAdd(t *testing.T) {
} }
func TestCmdBuffDel(t *testing.T) { func TestCmdBuffDel(t *testing.T) {
b := ui.NewCmdBuff('>', ui.CommandBuff) b := model.NewCmdBuff('>', model.Command)
uu := []struct { uu := []struct {
runes []rune runes []rune
@ -121,7 +121,7 @@ func TestCmdBuffDel(t *testing.T) {
} }
func TestCmdBuffEmpty(t *testing.T) { func TestCmdBuffEmpty(t *testing.T) {
b := ui.NewCmdBuff('>', ui.CommandBuff) b := model.NewCmdBuff('>', model.Command)
uu := []struct { uu := []struct {
runes []rune runes []rune

View File

@ -0,0 +1,61 @@
package model
import (
"sort"
)
// SuggestionListener listens for suggestions.
type SuggestionListener interface {
BuffWatcher
// SuggestionChanged notifies suggestion changes.
SuggestionChanged([]string)
}
// SuggestionFunc produces suggestions.
type SuggestionFunc func(s string) sort.StringSlice
// FishBuff represents a suggestion buffer.
type FishBuff struct {
*CmdBuff
suggestionFn SuggestionFunc
}
// NewFishBuffer returns a new command buffer.
func NewFishBuff(key rune, kind BufferKind) *FishBuff {
return &FishBuff{CmdBuff: NewCmdBuff(key, kind)}
}
// SetSuggestionFn sets up suggestions.
func (f *FishBuff) SetSuggestionFn(fn SuggestionFunc) {
f.suggestionFn = fn
}
// Delete removes the last character from the buffer.
func (f *FishBuff) Delete() {
f.CmdBuff.Delete()
if f.suggestionFn == nil {
return
}
cc := f.suggestionFn(string(f.buff))
f.fireSuggest(cc)
}
// Add adds a new charater to the buffer.
func (f *FishBuff) Add(r rune) {
f.CmdBuff.Add(r)
if f.suggestionFn == nil {
return
}
cc := f.suggestionFn(string(f.buff))
f.fireSuggest(cc)
}
func (f *FishBuff) fireSuggest(cc []string) {
for _, l := range f.listeners {
if s, ok := l.(SuggestionListener); ok {
s.SuggestionChanged(cc)
}
}
}

View File

@ -3,8 +3,6 @@ package model
import ( import (
"context" "context"
"fmt" "fmt"
"regexp"
"strings"
"sync" "sync"
"time" "time"
@ -13,13 +11,12 @@ import (
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/sahilm/fuzzy"
) )
// LogsListener represents a log model listener. // LogsListener represents a log model listener.
type LogsListener interface { type LogsListener interface {
// LogChanged notifies the model changed. // LogChanged notifies the model changed.
LogChanged([]string) LogChanged(dao.LogItems)
// LogCleanred indicates logs are cleared. // LogCleanred indicates logs are cleared.
LogCleared() LogCleared()
@ -30,18 +27,17 @@ type LogsListener interface {
// Log represents a resource logger. // Log represents a resource logger.
type Log struct { type Log struct {
factory dao.Factory factory dao.Factory
lines []string lines dao.LogItems
listeners []LogsListener listeners []LogsListener
gvr client.GVR gvr client.GVR
logOptions dao.LogOptions logOptions dao.LogOptions
cancelFn context.CancelFunc cancelFn context.CancelFunc
mx sync.RWMutex mx sync.RWMutex
filter string filter string
bufferSize int bufferSize int
lastSent int lastSent int
showTimestamp bool flushTimeout time.Duration
flushTimeout time.Duration
} }
// NewLog returns a new model. // NewLog returns a new model.
@ -54,9 +50,28 @@ func NewLog(gvr client.GVR, opts dao.LogOptions, flushTimeout time.Duration) *Lo
} }
} }
// LogOptions returns the current log options.
func (l *Log) LogOptions() dao.LogOptions {
return l.logOptions
}
// SinceSeconds returns since seconds option.
func (l *Log) SinceSeconds() int64 {
l.mx.RLock()
defer l.mx.RUnlock()
return l.logOptions.SinceSeconds
}
func (l *Log) SetLogOptions(opts dao.LogOptions) {
l.logOptions = opts
l.Restart()
}
// Configure sets logger configuration. // Configure sets logger configuration.
func (l *Log) Configure(opts *config.Logger) { func (l *Log) Configure(opts *config.Logger) {
l.bufferSize, l.logOptions.Lines = opts.BufferSize, int64(opts.TailCount) l.bufferSize = opts.BufferSize
l.logOptions.Lines = int64(opts.TailCount)
l.logOptions.SinceSeconds = opts.SinceSeconds
} }
// GetPath returns resource path. // GetPath returns resource path.
@ -74,22 +89,25 @@ func (l *Log) Init(f dao.Factory) {
func (l *Log) Clear() { func (l *Log) Clear() {
l.mx.Lock() l.mx.Lock()
{ {
l.lines, l.lastSent = []string{}, 0 l.lines, l.lastSent = dao.LogItems{}, 0
} }
l.mx.Unlock() l.mx.Unlock()
l.fireLogCleared() l.fireLogCleared()
} }
// ShowTimestamp toggles timestamp on logs. // Refresh refreshes the logs.
func (l *Log) ShowTimestamp(b bool) { func (l *Log) Refresh() {
l.mx.RLock()
defer l.mx.RUnlock()
l.showTimestamp = b
l.fireLogCleared() l.fireLogCleared()
l.fireLogChanged(l.lines) l.fireLogChanged(l.lines)
} }
// Restart restarts the logger.
func (l *Log) Restart() {
l.Clear()
l.Stop()
l.Start()
}
// Start initialize log tailer. // Start initialize log tailer.
func (l *Log) Start() { func (l *Log) Start() {
if err := l.load(); err != nil { if err := l.load(); err != nil {
@ -108,21 +126,21 @@ func (l *Log) Stop() {
} }
// Set sets the log lines (for testing only!) // Set sets the log lines (for testing only!)
func (l *Log) Set(lines []string) { func (l *Log) Set(items dao.LogItems) {
l.mx.Lock() l.mx.Lock()
defer l.mx.Unlock() defer l.mx.Unlock()
l.lines = items
l.lines = lines l.fireLogCleared()
l.fireLogChanged(lines) l.fireLogChanged(items)
} }
// ClearFilter resets the log filter if any. // ClearFilter resets the log filter if any.
func (l *Log) ClearFilter() { func (l *Log) ClearFilter() {
log.Debug().Msgf("CLEARED!!")
l.mx.RLock() l.mx.RLock()
defer l.mx.RUnlock() defer l.mx.RUnlock()
l.filter = "" l.filter = ""
l.fireLogCleared()
l.fireLogChanged(l.lines) l.fireLogChanged(l.lines)
} }
@ -131,14 +149,9 @@ func (l *Log) Filter(q string) error {
l.mx.RLock() l.mx.RLock()
defer l.mx.RUnlock() defer l.mx.RUnlock()
log.Debug().Msgf("FILTER!")
l.filter = q l.filter = q
filtered, err := applyFilter(l.filter, l.lines)
if err != nil {
return err
}
l.fireLogCleared() l.fireLogCleared()
l.fireLogChanged(filtered) l.fireLogBuffChanged(l.lines)
return nil return nil
} }
@ -148,7 +161,7 @@ func (l *Log) load() error {
ctx = context.WithValue(context.Background(), internal.KeyFactory, l.factory) ctx = context.WithValue(context.Background(), internal.KeyFactory, l.factory)
ctx, l.cancelFn = context.WithCancel(ctx) ctx, l.cancelFn = context.WithCancel(ctx)
c := make(chan []byte, 10) c := make(dao.LogChan, 10)
go l.updateLogs(ctx, c) go l.updateLogs(ctx, c)
accessor, err := dao.AccessorFor(l.factory, l.gvr) accessor, err := dao.AccessorFor(l.factory, l.gvr)
@ -160,6 +173,7 @@ func (l *Log) load() error {
return fmt.Errorf("Resource %s is not Loggable", l.gvr) return fmt.Errorf("Resource %s is not Loggable", l.gvr)
} }
if err := logger.TailLogs(ctx, c, l.logOptions); err != nil { if err := logger.TailLogs(ctx, c, l.logOptions); err != nil {
log.Error().Err(err).Msgf("Tail logs failed")
if l.cancelFn != nil { if l.cancelFn != nil {
l.cancelFn() l.cancelFn()
} }
@ -171,14 +185,15 @@ func (l *Log) load() error {
} }
// Append adds a log line. // Append adds a log line.
func (l *Log) Append(line string) { func (l *Log) Append(line *dao.LogItem) {
if line == "" { if line == nil || line.IsEmpty() {
return return
} }
l.mx.Lock() l.mx.Lock()
defer l.mx.Unlock() defer l.mx.Unlock()
l.logOptions.SinceTime = line.Timestamp
if l.lines == nil { if l.lines == nil {
l.fireLogCleared() l.fireLogCleared()
} }
@ -205,20 +220,20 @@ func (l *Log) Notify(timedOut bool) {
} }
} }
func (l *Log) updateLogs(ctx context.Context, c <-chan []byte) { func (l *Log) updateLogs(ctx context.Context, c dao.LogChan) {
defer func() { defer func() {
log.Debug().Msgf("updateLogs view bailing out!") log.Debug().Msgf("updateLogs view bailing out!")
}() }()
for { for {
select { select {
case bytes, ok := <-c: case item, ok := <-c:
if !ok { if !ok {
log.Debug().Msgf("Closed channel detected. Bailing out...") log.Debug().Msgf("Closed channel detected. Bailing out...")
l.Append(string(bytes)) l.Append(item)
l.Notify(false) l.Notify(false)
return return
} }
l.Append(string(bytes)) l.Append(item)
var overflow bool var overflow bool
l.mx.RLock() l.mx.RLock()
{ {
@ -256,11 +271,11 @@ func (l *Log) RemoveListener(listener LogsListener) {
} }
} }
func applyFilter(q string, lines []string) ([]string, error) { func applyFilter(q string, lines dao.LogItems) (dao.LogItems, error) {
if q == "" { if q == "" {
return lines, nil return lines, nil
} }
indexes, err := filter(q, lines) indexes, err := lines.Filter(q)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -272,7 +287,7 @@ func applyFilter(q string, lines []string) ([]string, error) {
if len(indexes) == 0 { if len(indexes) == 0 {
return nil, nil return nil, nil
} }
filtered := make([]string, 0, len(indexes)) filtered := make(dao.LogItems, 0, len(indexes))
for _, idx := range indexes { for _, idx := range indexes {
filtered = append(filtered, lines[idx]) filtered = append(filtered, lines[idx])
} }
@ -280,7 +295,7 @@ func applyFilter(q string, lines []string) ([]string, error) {
return filtered, nil return filtered, nil
} }
func (l *Log) fireLogBuffChanged(lines []string) { func (l *Log) fireLogBuffChanged(lines dao.LogItems) {
filtered, err := applyFilter(l.filter, lines) filtered, err := applyFilter(l.filter, lines)
if err != nil { if err != nil {
l.fireLogError(err) l.fireLogError(err)
@ -297,7 +312,7 @@ func (l *Log) fireLogError(err error) {
} }
} }
func (l *Log) fireLogChanged(lines []string) { func (l *Log) fireLogChanged(lines dao.LogItems) {
for _, lis := range l.listeners { for _, lis := range l.listeners {
lis.LogChanged(lines) lis.LogChanged(lines)
} }
@ -308,55 +323,3 @@ func (l *Log) fireLogCleared() {
lis.LogCleared() lis.LogCleared()
} }
} }
// ----------------------------------------------------------------------------
// Helpers...
var fuzzyRx = regexp.MustCompile(`\A\-f`)
func isFuzzySelector(s string) bool {
if s == "" {
return false
}
return fuzzyRx.MatchString(s)
}
func filter(q string, lines []string) ([]int, error) {
if q == "" {
return nil, nil
}
if isFuzzySelector(q) {
return fuzzyFilter(strings.TrimSpace(q[2:]), lines), nil
}
indexes, err := filterLogs(q, lines)
if err != nil {
log.Error().Err(err).Msgf("Logs filter failed")
return nil, err
}
return indexes, nil
}
func fuzzyFilter(q string, lines []string) []int {
matches := make([]int, 0, len(lines))
mm := fuzzy.Find(q, lines)
for _, m := range mm {
matches = append(matches, m.Index)
}
return matches
}
func filterLogs(q string, lines []string) ([]int, error) {
rx, err := regexp.Compile(`(?i)` + q)
if err != nil {
return nil, err
}
matches := make([]int, 0, len(lines))
for i, l := range lines {
if rx.MatchString(l) {
matches = append(matches, i)
}
}
return matches, nil
}

View File

@ -24,9 +24,9 @@ func TestLogFullBuffer(t *testing.T) {
v := newTestView() v := newTestView()
m.AddListener(v) m.AddListener(v)
data := make([]string, 0, 2*size) data := make(dao.LogItems, 0, 2*size)
for i := 0; i < 2*size; i++ { for i := 0; i < 2*size; i++ {
data = append(data, "line"+strconv.Itoa(i)) data = append(data, dao.NewLogItemFromString("line"+strconv.Itoa(i)))
m.Append(data[i]) m.Append(data[i])
} }
m.Notify(true) m.Notify(true)
@ -47,8 +47,8 @@ func TestLogFilter(t *testing.T) {
e: 2, e: 2,
}, },
"regexp": { "regexp": {
q: `\Apod-line-[1-3]{1}\z`, q: `pod-line-[1-3]{1}`,
e: 3, e: 4,
}, },
"fuzzy": { "fuzzy": {
q: `-f po-l1`, q: `-f po-l1`,
@ -67,21 +67,21 @@ func TestLogFilter(t *testing.T) {
m.AddListener(v) m.AddListener(v)
m.Filter(u.q) m.Filter(u.q)
var data []string var data dao.LogItems
for i := 0; i < size; i++ { for i := 0; i < size; i++ {
data = append(data, fmt.Sprintf("pod-line-%d", i+1)) data = append(data, dao.NewLogItemFromString(fmt.Sprintf("pod-line-%d", i+1)))
m.Append(data[i]) m.Append(data[i])
} }
m.Notify(true) m.Notify(true)
assert.Equal(t, 2, v.dataCalled) assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 2, v.clearCalled) assert.Equal(t, 2, v.clearCalled)
assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, v.errCalled)
assert.Equal(t, u.e, len(v.data)) assert.Equal(t, u.e, len(v.data))
m.ClearFilter() m.ClearFilter()
assert.Equal(t, 3, v.dataCalled) assert.Equal(t, 2, v.dataCalled)
assert.Equal(t, 2, v.clearCalled) assert.Equal(t, 3, v.clearCalled)
assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, v.errCalled)
assert.Equal(t, size, len(v.data)) assert.Equal(t, size, len(v.data))
}) })
@ -96,7 +96,7 @@ func TestLogStartStop(t *testing.T) {
m.AddListener(v) m.AddListener(v)
m.Start() m.Start()
data := []string{"line1", "line2"} data := dao.LogItems{dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2")}
for _, d := range data { for _, d := range data {
m.Append(d) m.Append(d)
} }
@ -118,7 +118,7 @@ func TestLogClear(t *testing.T) {
v := newTestView() v := newTestView()
m.AddListener(v) m.AddListener(v)
data := []string{"line1", "line2"} data := dao.LogItems{dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2")}
for _, d := range data { for _, d := range data {
m.Append(d) m.Append(d)
} }
@ -138,11 +138,11 @@ func TestLogBasic(t *testing.T) {
v := newTestView() v := newTestView()
m.AddListener(v) m.AddListener(v)
data := []string{"line1", "line2"} data := dao.LogItems{dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2")}
m.Set(data) m.Set(data)
assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 0, v.clearCalled) assert.Equal(t, 1, v.clearCalled)
assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, v.errCalled)
assert.Equal(t, data, v.data) assert.Equal(t, data, v.data)
} }
@ -153,21 +153,25 @@ func TestLogAppend(t *testing.T) {
v := newTestView() v := newTestView()
m.AddListener(v) m.AddListener(v)
m.Set([]string{"blah blah"}) items := dao.LogItems{dao.NewLogItemFromString("blah blah")}
assert.Equal(t, []string{"blah blah"}, v.data) m.Set(items)
assert.Equal(t, items, v.data)
data := []string{"line1", "line2"} data := dao.LogItems{
dao.NewLogItemFromString("line1"),
dao.NewLogItemFromString("line2"),
}
for _, d := range data { for _, d := range data {
m.Append(d) m.Append(d)
} }
assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, []string{"blah blah"}, v.data) assert.Equal(t, items, v.data)
m.Notify(true) m.Notify(true)
assert.Equal(t, 2, v.dataCalled) assert.Equal(t, 2, v.dataCalled)
assert.Equal(t, 0, v.clearCalled) assert.Equal(t, 1, v.clearCalled)
assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, v.errCalled)
assert.Equal(t, append([]string{"blah blah"}, data...), v.data) assert.Equal(t, append(items, data...), v.data)
} }
func TestLogTimedout(t *testing.T) { func TestLogTimedout(t *testing.T) {
@ -178,15 +182,20 @@ func TestLogTimedout(t *testing.T) {
m.AddListener(v) m.AddListener(v)
m.Filter("line1") m.Filter("line1")
data := []string{"line1", "line2", "line3", "line4"} data := dao.LogItems{
dao.NewLogItemFromString("line1"),
dao.NewLogItemFromString("line2"),
dao.NewLogItemFromString("line3"),
dao.NewLogItemFromString("line4"),
}
for _, d := range data { for _, d := range data {
m.Append(d) m.Append(d)
} }
m.Notify(true) m.Notify(true)
assert.Equal(t, 2, v.dataCalled) assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 2, v.clearCalled) assert.Equal(t, 2, v.clearCalled)
assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, v.errCalled)
assert.Equal(t, []string{"line1"}, v.data) assert.Equal(t, dao.LogItems{data[0]}, v.data)
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -203,7 +212,7 @@ func makeLogOpts(count int) dao.LogOptions {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
type testView struct { type testView struct {
data []string data dao.LogItems
dataCalled int dataCalled int
clearCalled int clearCalled int
errCalled int errCalled int
@ -213,13 +222,13 @@ func newTestView() *testView {
return &testView{} return &testView{}
} }
func (t *testView) LogChanged(d []string) { func (t *testView) LogChanged(d dao.LogItems) {
t.data = d t.data = d
t.dataCalled++ t.dataCalled++
} }
func (t *testView) LogCleared() { func (t *testView) LogCleared() {
t.clearCalled++ t.clearCalled++
t.data = []string{} t.data = dao.LogItems{}
} }
func (t *testView) LogFailed(err error) { func (t *testView) LogFailed(err error) {
fmt.Println("LogErr", err) fmt.Println("LogErr", err)

View File

@ -290,7 +290,6 @@ func (t *Table) getMeta(ctx context.Context) (ResourceMeta, error) {
func (t *Table) resourceMeta() ResourceMeta { func (t *Table) resourceMeta() ResourceMeta {
meta, ok := Registry[t.gvr.String()] meta, ok := Registry[t.gvr.String()]
if !ok { if !ok {
log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr)
meta = ResourceMeta{ meta = ResourceMeta{
DAO: &dao.Table{}, DAO: &dao.Table{},
Renderer: &render.Generic{}, Renderer: &render.Generic{},

View File

@ -4,6 +4,7 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/derailed/k9s/internal/dao"
"github.com/sahilm/fuzzy" "github.com/sahilm/fuzzy"
) )
@ -94,7 +95,7 @@ func (t *Text) filter(q string, lines []string) fuzzy.Matches {
if q == "" { if q == "" {
return nil return nil
} }
if isFuzzySelector(q) { if dao.IsFuzzySelector(q) {
return t.fuzzyFilter(strings.TrimSpace(q[2:]), lines) return t.fuzzyFilter(strings.TrimSpace(q[2:]), lines)
} }
return t.rxFilter(q, lines) return t.rxFilter(q, lines)

View File

@ -238,7 +238,6 @@ func (t *Tree) reconcile(ctx context.Context) error {
func (t *Tree) resourceMeta() ResourceMeta { func (t *Tree) resourceMeta() ResourceMeta {
meta, ok := Registry[t.gvr.String()] meta, ok := Registry[t.gvr.String()]
if !ok { if !ok {
log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr)
meta = ResourceMeta{ meta = ResourceMeta{
DAO: &dao.Table{}, DAO: &dao.Table{},
Renderer: &render.Generic{}, Renderer: &render.Generic{},

View File

@ -18,7 +18,7 @@ type App struct {
flash *model.Flash flash *model.Flash
actions KeyActions actions KeyActions
views map[string]tview.Primitive views map[string]tview.Primitive
cmdBuff *CmdBuff cmdBuff *model.FishBuff
} }
// NewApp returns a new app. // NewApp returns a new app.
@ -28,14 +28,14 @@ func NewApp(context string) *App {
actions: make(KeyActions), actions: make(KeyActions),
Main: NewPages(), Main: NewPages(),
flash: model.NewFlash(model.DefaultFlashDelay), flash: model.NewFlash(model.DefaultFlashDelay),
cmdBuff: NewCmdBuff(':', CommandBuff), cmdBuff: model.NewFishBuff(':', model.Command),
} }
a.ReloadStyles(context) a.ReloadStyles(context)
a.views = map[string]tview.Primitive{ a.views = map[string]tview.Primitive{
"menu": NewMenu(a.Styles), "menu": NewMenu(a.Styles),
"logo": NewLogo(a.Styles), "logo": NewLogo(a.Styles),
"cmd": NewCommand(a.Styles), "cmd": NewCommand(a.Styles, a.cmdBuff),
"crumbs": NewCrumbs(a.Styles), "crumbs": NewCrumbs(a.Styles),
} }
@ -56,7 +56,7 @@ func (a *App) Init() {
func (a *App) BufferChanged(s string) {} func (a *App) BufferChanged(s string) {}
// BufferActive indicates the buff activity changed. // BufferActive indicates the buff activity changed.
func (a *App) BufferActive(state bool, _ BufferKind) { func (a *App) BufferActive(state bool, _ model.BufferKind) {
flex, ok := a.Main.GetPrimitive("main").(*tview.Flex) flex, ok := a.Main.GetPrimitive("main").(*tview.Flex)
if !ok { if !ok {
return return
@ -70,6 +70,8 @@ func (a *App) BufferActive(state bool, _ BufferKind) {
a.Draw() a.Draw()
} }
func (a *App) SuggestionChanged(ss []string) {}
// StylesChanged notifies the skin changed. // StylesChanged notifies the skin changed.
func (a *App) StylesChanged(s *config.Styles) { func (a *App) StylesChanged(s *config.Styles) {
a.Main.SetBackgroundColor(s.BgColor()) a.Main.SetBackgroundColor(s.BgColor())
@ -129,7 +131,7 @@ func (a *App) GetCmd() string {
} }
// CmdBuff returns a cmd buffer. // CmdBuff returns a cmd buffer.
func (a *App) CmdBuff() *CmdBuff { func (a *App) CmdBuff() *model.FishBuff {
return a.cmdBuff return a.cmdBuff
} }
@ -190,6 +192,7 @@ func (a *App) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
a.cmdBuff.SetActive(true) a.cmdBuff.SetActive(true)
a.cmdBuff.Clear() a.cmdBuff.Clear()
a.SetFocus(a.Cmd())
return nil return nil
} }
@ -207,6 +210,7 @@ func (a *App) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
func (a *App) escapeCmd(evt *tcell.EventKey) *tcell.EventKey { func (a *App) escapeCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.cmdBuff.IsActive() { if a.cmdBuff.IsActive() {
a.cmdBuff.Reset() a.cmdBuff.Reset()
a.SetFocus(a.Main.GetPrimitive("main"))
} }
return evt return evt
} }

View File

@ -4,25 +4,29 @@ import (
"fmt" "fmt"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
const defaultPrompt = "%c> %s" const defaultPrompt = "%c> [::b]%s"
// Command captures users free from command input. // Command captures users free from command input.
type Command struct { type Command struct {
*tview.TextView *tview.TextView
activated bool activated bool
icon rune icon rune
text string text string
styles *config.Styles styles *config.Styles
model *model.FishBuff
suggestions []string
suggestionIndex int
} }
// NewCommand returns a new command view. // NewCommand returns a new command view.
func NewCommand(styles *config.Styles) *Command { func NewCommand(styles *config.Styles, m *model.FishBuff) *Command {
c := Command{styles: styles, TextView: tview.NewTextView()} c := Command{styles: styles, TextView: tview.NewTextView(), model: m}
c.SetWordWrap(true) c.SetWordWrap(true)
c.SetWrap(true) c.SetWrap(true)
c.SetDynamicColors(true) c.SetDynamicColors(true)
@ -31,10 +35,46 @@ func NewCommand(styles *config.Styles) *Command {
c.SetBackgroundColor(styles.BgColor()) c.SetBackgroundColor(styles.BgColor())
c.SetTextColor(styles.FgColor()) c.SetTextColor(styles.FgColor())
styles.AddListener(&c) styles.AddListener(&c)
c.SetInputCapture(c.keyboard)
return &c return &c
} }
func (c *Command) keyboard(evt *tcell.EventKey) *tcell.EventKey {
switch evt.Key() {
case tcell.KeyEnter, tcell.KeyCtrlE:
if c.suggestionIndex >= 0 {
c.model.Set(c.text + c.suggestions[c.suggestionIndex])
}
case tcell.KeyCtrlW:
c.model.Clear()
case tcell.KeyTab, tcell.KeyDown:
if c.text == "" || c.suggestionIndex < 0 {
return evt
}
c.suggestionIndex++
if c.suggestionIndex >= len(c.suggestions) {
c.suggestionIndex = 0
}
c.suggest(c.model.String(), c.suggestions[c.suggestionIndex])
case tcell.KeyBacktab, tcell.KeyUp:
if c.text == "" || c.suggestionIndex < 0 {
return evt
}
c.suggestionIndex--
if c.suggestionIndex < 0 {
c.suggestionIndex = len(c.suggestions) - 1
}
c.suggest(c.model.String(), c.suggestions[c.suggestionIndex])
case tcell.KeyRight, tcell.KeyCtrlF:
if c.suggestionIndex >= 0 {
c.model.Set(c.model.String() + c.suggestions[c.suggestionIndex])
c.suggestionIndex = -1
}
}
return evt
}
// StylesChanged notifies skin changed. // StylesChanged notifies skin changed.
func (c *Command) StylesChanged(s *config.Styles) { func (c *Command) StylesChanged(s *config.Styles) {
c.styles = s c.styles = s
@ -60,6 +100,11 @@ func (c *Command) update(s string) {
c.write(c.text) c.write(c.text)
} }
func (c *Command) suggest(text, suggestion string) {
c.Clear()
c.write(text + "[gray::-]" + suggestion)
}
func (c *Command) write(s string) { func (c *Command) write(s string) {
fmt.Fprintf(c, defaultPrompt, c.icon, s) fmt.Fprintf(c, defaultPrompt, c.icon, s)
} }
@ -67,19 +112,28 @@ func (c *Command) write(s string) {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Event Listener protocol... // Event Listener protocol...
// SuggestionChanged indicates the suggestions changed.
func (c *Command) SuggestionChanged(ss []string) {
c.suggestions, c.suggestionIndex = ss, 0
if ss == nil {
c.suggestionIndex = -1
return
}
fmt.Fprintf(c, "[gray::-]%s", ss[c.suggestionIndex])
}
// BufferChanged indicates the buffer was changed. // BufferChanged indicates the buffer was changed.
func (c *Command) BufferChanged(s string) { func (c *Command) BufferChanged(s string) {
c.update(s) c.update(s)
} }
// BufferActive indicates the buff activity changed. // BufferActive indicates the buff activity changed.
func (c *Command) BufferActive(f bool, k BufferKind) { func (c *Command) BufferActive(f bool, k model.BufferKind) {
if c.activated = f; f { if c.activated = f; f {
c.SetBorder(true) c.SetBorder(true)
c.SetTextColor(c.styles.FgColor()) c.SetTextColor(c.styles.FgColor())
c.SetBorderColor(colorFor(k)) c.SetBorderColor(colorFor(k))
c.icon = iconFor(k) c.icon = iconFor(k)
// c.reset()
c.activate() c.activate()
} else { } else {
c.SetBorder(false) c.SetBorder(false)
@ -88,18 +142,18 @@ func (c *Command) BufferActive(f bool, k BufferKind) {
} }
} }
func colorFor(k BufferKind) tcell.Color { func colorFor(k model.BufferKind) tcell.Color {
switch k { switch k {
case CommandBuff: case model.Command:
return tcell.ColorAqua return tcell.ColorAqua
default: default:
return tcell.ColorSeaGreen return tcell.ColorSeaGreen
} }
} }
func iconFor(k BufferKind) rune { func iconFor(k model.BufferKind) rune {
switch k { switch k {
case CommandBuff: case model.Command:
return '🐶' return '🐶'
default: default:
return '🐩' return '🐩'

View File

@ -4,41 +4,40 @@ import (
"testing" "testing"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestCmdNew(t *testing.T) { func TestCmdNew(t *testing.T) {
v := ui.NewCommand(config.NewStyles()) model := model.NewFishBuff(':', model.Command)
v := ui.NewCommand(config.NewStyles(), model)
buff := ui.NewCmdBuff(':', ui.CommandBuff) model.AddListener(v)
buff.AddListener(v) model.Set("blee")
buff.Set("blee")
assert.Equal(t, "\x00> blee\n", v.GetText(false)) assert.Equal(t, "\x00> [::b]blee\n", v.GetText(false))
} }
func TestCmdUpdate(t *testing.T) { func TestCmdUpdate(t *testing.T) {
v := ui.NewCommand(config.NewStyles()) model := model.NewFishBuff(':', model.Command)
v := ui.NewCommand(config.NewStyles(), model)
buff := ui.NewCmdBuff(':', ui.CommandBuff) model.AddListener(v)
buff.AddListener(v) model.Set("blee")
model.Add('!')
buff.Set("blee") assert.Equal(t, "\x00> [::b]blee!\n", v.GetText(false))
buff.Add('!')
assert.Equal(t, "\x00> blee!\n", v.GetText(false))
assert.False(t, v.InCmdMode()) assert.False(t, v.InCmdMode())
} }
func TestCmdMode(t *testing.T) { func TestCmdMode(t *testing.T) {
v := ui.NewCommand(config.NewStyles()) model := model.NewFishBuff(':', model.Command)
v := ui.NewCommand(config.NewStyles(), model)
buff := ui.NewCmdBuff(':', ui.CommandBuff) model.AddListener(v)
buff.AddListener(v)
for _, f := range []bool{false, true} { for _, f := range []bool{false, true} {
buff.SetActive(f) model.SetActive(f)
assert.Equal(t, f, v.InCmdMode()) assert.Equal(t, f, v.InCmdMode())
} }
} }

View File

@ -19,7 +19,7 @@ func initKeys() {
// Defines numeric keys for container actions // Defines numeric keys for container actions
const ( const (
Key0 int32 = iota + 48 Key0 tcell.Key = iota + 48
Key1 Key1
Key2 Key2
Key3 Key3
@ -33,16 +33,16 @@ const (
// Defines numeric keys for container actions // Defines numeric keys for container actions
const ( const (
KeyShift0 int32 = 41 KeyShift0 tcell.Key = 41
KeyShift1 int32 = 33 KeyShift1 tcell.Key = 33
KeyShift2 int32 = 64 KeyShift2 tcell.Key = 64
KeyShift3 int32 = 35 KeyShift3 tcell.Key = 35
KeyShift4 int32 = 36 KeyShift4 tcell.Key = 36
KeyShift5 int32 = 37 KeyShift5 tcell.Key = 37
KeyShift6 int32 = 94 KeyShift6 tcell.Key = 94
KeyShift7 int32 = 38 KeyShift7 tcell.Key = 38
KeyShift8 int32 = 42 KeyShift8 tcell.Key = 42
KeyShift9 int32 = 40 KeyShift9 tcell.Key = 40
) )
// Defines char keystrokes // Defines char keystrokes
@ -110,7 +110,7 @@ const (
) )
// NumKeys tracks number keys. // NumKeys tracks number keys.
var NumKeys = map[int]int32{ var NumKeys = map[int]tcell.Key{
0: Key0, 0: Key0,
1: Key1, 1: Key1,
2: Key2, 2: Key2,
@ -124,16 +124,16 @@ var NumKeys = map[int]int32{
} }
func initNumbKeys() { func initNumbKeys() {
tcell.KeyNames[tcell.Key(Key0)] = "0" tcell.KeyNames[Key0] = "0"
tcell.KeyNames[tcell.Key(Key1)] = "1" tcell.KeyNames[Key1] = "1"
tcell.KeyNames[tcell.Key(Key2)] = "2" tcell.KeyNames[Key2] = "2"
tcell.KeyNames[tcell.Key(Key3)] = "3" tcell.KeyNames[Key3] = "3"
tcell.KeyNames[tcell.Key(Key4)] = "4" tcell.KeyNames[Key4] = "4"
tcell.KeyNames[tcell.Key(Key5)] = "5" tcell.KeyNames[Key5] = "5"
tcell.KeyNames[tcell.Key(Key6)] = "6" tcell.KeyNames[Key6] = "6"
tcell.KeyNames[tcell.Key(Key7)] = "7" tcell.KeyNames[Key7] = "7"
tcell.KeyNames[tcell.Key(Key8)] = "8" tcell.KeyNames[Key8] = "8"
tcell.KeyNames[tcell.Key(Key9)] = "9" tcell.KeyNames[Key9] = "9"
} }
func initStdKeys() { func initStdKeys() {

View File

@ -6,7 +6,6 @@ import (
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -30,10 +29,10 @@ func TestActionHints(t *testing.T) {
}{ }{
"a": { "a": {
aa: ui.KeyActions{ aa: ui.KeyActions{
ui.KeyB: ui.NewKeyAction("bleeB", nil, true), ui.KeyB: ui.NewKeyAction("bleeB", nil, true),
ui.KeyA: ui.NewKeyAction("bleeA", nil, true), ui.KeyA: ui.NewKeyAction("bleeA", nil, true),
tcell.Key(ui.Key0): ui.NewKeyAction("zero", nil, true), ui.Key0: ui.NewKeyAction("zero", nil, true),
tcell.Key(ui.Key1): ui.NewKeyAction("one", nil, false), ui.Key1: ui.NewKeyAction("one", nil, false),
}, },
e: model.MenuHints{ e: model.MenuHints{
{Mnemonic: "0", Description: "zero", Visible: true}, {Mnemonic: "0", Description: "zero", Visible: true},

View File

@ -34,7 +34,7 @@ type Table struct {
actions KeyActions actions KeyActions
gvr client.GVR gvr client.GVR
Path string Path string
cmdBuff *CmdBuff cmdBuff *model.CmdBuff
styles *config.Styles styles *config.Styles
viewSetting *config.ViewSetting viewSetting *config.ViewSetting
sortCol SortColumn sortCol SortColumn
@ -56,7 +56,7 @@ func NewTable(gvr client.GVR) *Table {
}, },
gvr: gvr, gvr: gvr,
actions: make(KeyActions), actions: make(KeyActions),
cmdBuff: NewCmdBuff('/', FilterBuff), cmdBuff: model.NewCmdBuff('/', model.Filter),
sortCol: SortColumn{asc: true}, sortCol: SortColumn{asc: true},
} }
} }
@ -358,7 +358,7 @@ func (t *Table) filtered(data render.TableData) render.TableData {
q := t.cmdBuff.String() q := t.cmdBuff.String()
if IsFuzzySelector(q) { if IsFuzzySelector(q) {
return fuzzyFilter(q[2:], t.NameColIndex(), filtered) return fuzzyFilter(q[2:], filtered)
} }
filtered, err := rxFilter(t.cmdBuff.String(), filtered) filtered, err := rxFilter(t.cmdBuff.String(), filtered)
@ -371,7 +371,7 @@ func (t *Table) filtered(data render.TableData) render.TableData {
} }
// SearchBuff returns the associated command buffer. // SearchBuff returns the associated command buffer.
func (t *Table) SearchBuff() *CmdBuff { func (t *Table) SearchBuff() *model.CmdBuff {
return t.cmdBuff return t.cmdBuff
} }

View File

@ -149,8 +149,7 @@ func rxFilter(q string, data render.TableData) (render.TableData, error) {
Namespace: data.Namespace, Namespace: data.Namespace,
} }
for _, re := range data.RowEvents { for _, re := range data.RowEvents {
f := strings.Join(re.Row.Fields, " ") if rx.MatchString(re.Row.ID) {
if rx.MatchString(f) {
filtered.RowEvents = append(filtered.RowEvents, re) filtered.RowEvents = append(filtered.RowEvents, re)
} }
} }
@ -158,10 +157,11 @@ func rxFilter(q string, data render.TableData) (render.TableData, error) {
return filtered, nil return filtered, nil
} }
func fuzzyFilter(q string, index int, data render.TableData) render.TableData { func fuzzyFilter(q string, data render.TableData) render.TableData {
q = strings.TrimSpace(q)
var ss []string var ss []string
for _, re := range data.RowEvents { for _, re := range data.RowEvents {
ss = append(ss, re.Row.Fields[index]) ss = append(ss, re.Row.ID)
} }
filtered := render.TableData{ filtered := render.TableData{

View File

@ -17,7 +17,7 @@ type Tree struct {
actions KeyActions actions KeyActions
selectedItem string selectedItem string
cmdBuff *CmdBuff cmdBuff *model.CmdBuff
expandNodes bool expandNodes bool
Count int Count int
keyListener KeyListenerFunc keyListener KeyListenerFunc
@ -29,7 +29,7 @@ func NewTree() *Tree {
TreeView: tview.NewTreeView(), TreeView: tview.NewTreeView(),
expandNodes: true, expandNodes: true,
actions: make(KeyActions), actions: make(KeyActions),
cmdBuff: NewCmdBuff('/', FilterBuff), cmdBuff: model.NewCmdBuff('/', model.Filter),
} }
} }
@ -62,7 +62,7 @@ func (t *Tree) ExpandNodes() bool {
} }
// CmdBuff returns the filter command. // CmdBuff returns the filter command.
func (t *Tree) CmdBuff() *CmdBuff { func (t *Tree) CmdBuff() *model.CmdBuff {
return t.cmdBuff return t.cmdBuff
} }

View File

@ -63,7 +63,7 @@ type buffL struct {
func (b *buffL) BufferChanged(s string) { func (b *buffL) BufferChanged(s string) {
b.changed++ b.changed++
} }
func (b *buffL) BufferActive(state bool, kind ui.BufferKind) { func (b *buffL) BufferActive(state bool, kind model.BufferKind) {
b.active++ b.active++
} }

View File

@ -4,6 +4,8 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"sort"
"strings"
"sync/atomic" "sync/atomic"
"time" "time"
@ -96,6 +98,7 @@ func (a *App) Init(version string, rate int) error {
if err := a.command.Init(); err != nil { if err := a.command.Init(); err != nil {
return err return err
} }
a.CmdBuff().SetSuggestionFn(a.suggestCommand())
a.clusterInfo().Init() a.clusterInfo().Init()
@ -104,9 +107,9 @@ func (a *App) Init(version string, rate int) error {
main := tview.NewFlex().SetDirection(tview.FlexRow) main := tview.NewFlex().SetDirection(tview.FlexRow)
main.AddItem(a.statusIndicator(), 1, 1, false) main.AddItem(a.statusIndicator(), 1, 1, false)
main.AddItem(flash, 1, 1, false)
main.AddItem(a.Content, 0, 10, true) main.AddItem(a.Content, 0, 10, true)
main.AddItem(a.Crumbs(), 1, 1, false) main.AddItem(a.Crumbs(), 1, 1, false)
main.AddItem(flash, 1, 1, false)
a.Main.AddPage("main", main, true, false) a.Main.AddPage("main", main, true, false)
a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true) a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true)
@ -115,6 +118,29 @@ func (a *App) Init(version string, rate int) error {
return nil return nil
} }
func (a *App) suggestCommand() func(s string) (entries sort.StringSlice) {
return func(s string) (entries sort.StringSlice) {
if s == "" {
return
}
for _, k := range a.command.alias.Aliases.Keys() {
lok, los := strings.ToLower(k), strings.ToLower(s)
if lok == los {
continue
}
if strings.HasPrefix(lok, los) {
entries = append(entries, strings.Replace(k, los, "", 1))
}
}
if len(entries) == 0 {
entries = nil
}
entries.Sort()
return
}
}
func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey { func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey {
key := evt.Key() key := evt.Key()
if key == tcell.KeyRune { if key == tcell.KeyRune {
@ -442,6 +468,9 @@ func (a *App) helpCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
func (a *App) aliasCmd(evt *tcell.EventKey) *tcell.EventKey { func (a *App) aliasCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.CmdBuff().InCmdMode() {
return evt
}
if _, ok := a.Content.GetPrimitive("main").(*Alias); ok { if _, ok := a.Content.GetPrimitive("main").(*Alias); ok {
return evt return evt
} }

View File

@ -403,14 +403,14 @@ func (b *Browser) namespaceActions(aa ui.KeyActions) {
return return
} }
b.namespaces = make(map[int]string, config.MaxFavoritesNS) b.namespaces = make(map[int]string, config.MaxFavoritesNS)
aa[tcell.Key(ui.NumKeys[0])] = ui.NewKeyAction(client.NamespaceAll, b.switchNamespaceCmd, true) aa[ui.Key0] = ui.NewKeyAction(client.NamespaceAll, b.switchNamespaceCmd, true)
b.namespaces[0] = client.NamespaceAll b.namespaces[0] = client.NamespaceAll
index := 1 index := 1
for _, ns := range b.app.Config.FavNamespaces() { for _, ns := range b.app.Config.FavNamespaces() {
if ns == client.NamespaceAll { if ns == client.NamespaceAll {
continue continue
} }
aa[tcell.Key(ui.NumKeys[index])] = ui.NewKeyAction(ns, b.switchNamespaceCmd, true) aa[ui.NumKeys[index]] = ui.NewKeyAction(ns, b.switchNamespaceCmd, true)
b.namespaces[index] = ns b.namespaces[index] = ns
index++ index++
} }

View File

@ -7,7 +7,6 @@ import (
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
) )
// Chart represents a helm chart view. // Chart represents a helm chart view.
@ -36,18 +35,8 @@ func (c *Chart) chartContext(ctx context.Context) context.Context {
func (c *Chart) bindKeys(aa ui.KeyActions) { func (c *Chart) bindKeys(aa ui.KeyActions) {
aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)
aa.Add(ui.KeyActions{ aa.Add(ui.KeyActions{
ui.KeyB: ui.NewKeyAction("Blee", c.bleeCmd, true),
ui.KeyShiftN: ui.NewKeyAction("Sort Name", c.GetTable().SortColCmd(nameCol, true), false), ui.KeyShiftN: ui.NewKeyAction("Sort Name", c.GetTable().SortColCmd(nameCol, true), false),
ui.KeyShiftS: ui.NewKeyAction("Sort Status", c.GetTable().SortColCmd(statusCol, true), false), ui.KeyShiftS: ui.NewKeyAction("Sort Status", c.GetTable().SortColCmd(statusCol, true), false),
ui.KeyShiftA: ui.NewKeyAction("Sort Age", c.GetTable().SortColCmd(ageCol, true), false), ui.KeyShiftA: ui.NewKeyAction("Sort Age", c.GetTable().SortColCmd(ageCol, true), false),
}) })
} }
func (c *Chart) bleeCmd(evt *tcell.EventKey) *tcell.EventKey {
path := c.GetTable().GetSelectedItem()
if path == "" {
return nil
}
log.Debug().Msgf("BLEE CMD %q", path)
return nil
}

View File

@ -84,7 +84,6 @@ func (c *ClusterInfo) ClusterInfoUpdated(data model.ClusterMeta) {
// ClusterInfoChanged notifies the cluster meta was changed. // ClusterInfoChanged notifies the cluster meta was changed.
func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) { func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) {
// BOZO!!
c.app.QueueUpdate(func() { c.app.QueueUpdate(func() {
c.Clear() c.Clear()
c.layout() c.layout()

View File

@ -116,7 +116,6 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt return evt
} }
log.Debug().Msgf("CONTAINER-SEL %q", path)
if _, ok := c.App().factory.ForwarderFor(fwFQN(c.GetTable().Path, path)); ok { if _, ok := c.App().factory.ForwarderFor(fwFQN(c.GetTable().Path, path)); ok {
c.App().Flash().Err(fmt.Errorf("A PortForward already exist on container %s", c.GetTable().Path)) c.App().Flash().Err(fmt.Errorf("A PortForward already exist on container %s", c.GetTable().Path))
return nil return nil
@ -126,7 +125,6 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
if !ok { if !ok {
return nil return nil
} }
log.Debug().Msgf("CONTAINER-PORTS %#v", ports)
ShowPortForwards(c, c.GetTable().Path, ports, startFwdCB) ShowPortForwards(c, c.GetTable().Path, ports, startFwdCB)
return nil return nil

View File

@ -23,7 +23,7 @@ type Details struct {
actions ui.KeyActions actions ui.KeyActions
app *App app *App
title, subject string title, subject string
cmdBuff *ui.CmdBuff cmdBuff *model.CmdBuff
model *model.Text model *model.Text
currentRegion, maxRegions int currentRegion, maxRegions int
searchable bool searchable bool
@ -37,7 +37,7 @@ func NewDetails(app *App, title, subject string, searchable bool) *Details {
title: title, title: title,
subject: subject, subject: subject,
actions: make(ui.KeyActions), actions: make(ui.KeyActions),
cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff), cmdBuff: model.NewCmdBuff('/', model.Filter),
model: model.NewText(), model: model.NewText(),
searchable: searchable, searchable: searchable,
} }
@ -103,7 +103,7 @@ func (d *Details) TextFiltered(lines []string, matches fuzzy.Matches) {
func (d *Details) BufferChanged(s string) {} func (d *Details) BufferChanged(s string) {}
// BufferActive indicates the buff activity changed. // BufferActive indicates the buff activity changed.
func (d *Details) BufferActive(state bool, k ui.BufferKind) { func (d *Details) BufferActive(state bool, k model.BufferKind) {
d.app.BufferActive(state, k) d.app.BufferActive(state, k)
} }
@ -162,14 +162,12 @@ func (d *Details) StylesChanged(s *config.Styles) {
d.SetBackgroundColor(d.app.Styles.BgColor()) d.SetBackgroundColor(d.app.Styles.BgColor())
d.SetTextColor(d.app.Styles.FgColor()) d.SetTextColor(d.app.Styles.FgColor())
d.SetBorderFocusColor(d.app.Styles.Frame().Border.FocusColor.Color()) d.SetBorderFocusColor(d.app.Styles.Frame().Border.FocusColor.Color())
d.TextChanged(d.model.Peek()) d.TextChanged(d.model.Peek())
} }
// Update updates the view content. // Update updates the view content.
func (d *Details) Update(buff string) *Details { func (d *Details) Update(buff string) *Details {
d.model.SetText(buff) d.model.SetText(buff)
return d return d
} }
@ -293,6 +291,7 @@ func (d *Details) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
} else { } else {
d.app.Flash().Infof("Log %s saved successfully!", path) d.app.Flash().Infof("Log %s saved successfully!", path)
} }
return nil return nil
} }
@ -301,6 +300,7 @@ func (d *Details) cpCmd(evt *tcell.EventKey) *tcell.EventKey {
if err := clipboard.WriteAll(d.GetText(true)); err != nil { if err := clipboard.WriteAll(d.GetText(true)); err != nil {
d.app.Flash().Err(err) d.app.Flash().Err(err)
} }
return nil return nil
} }
@ -321,6 +321,5 @@ func (d *Details) updateTitle() {
search += fmt.Sprintf("[%d:%d]", d.currentRegion+1, d.maxRegions) search += fmt.Sprintf("[%d:%d]", d.currentRegion+1, d.maxRegions)
} }
fmat += fmt.Sprintf(ui.SearchFmt, search) fmat += fmt.Sprintf(ui.SearchFmt, search)
d.SetTitle(ui.SkinTitle(fmat, d.app.Styles.Frame())) d.SetTitle(ui.SkinTitle(fmat, d.app.Styles.Frame()))
} }

View File

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/color"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/model"
@ -21,10 +22,10 @@ import (
const ( const (
logTitle = "logs" logTitle = "logs"
logMessage = "[:orange:b]Waiting for logs...[::]" logMessage = "Waiting for logs..."
logCoFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:bg:-]) " logFmt = " Logs([hilite:bg:]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] "
logFmt = " Logs([fg:bg:]%s) " logCoFmt = " Logs([hilite:bg:]%s:[hilite:bg:b]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] "
flushTimeout = 200 * time.Millisecond flushTimeout = 100 * time.Millisecond
) )
// Log represents a generic log viewer. // Log represents a generic log viewer.
@ -35,9 +36,7 @@ type Log struct {
logs *Details logs *Details
indicator *LogIndicator indicator *LogIndicator
ansiWriter io.Writer ansiWriter io.Writer
cmdBuff *ui.CmdBuff
model *model.Log model *model.Log
counts int
} }
var _ model.Component = (*Log)(nil) var _ model.Component = (*Log)(nil)
@ -45,15 +44,18 @@ var _ model.Component = (*Log)(nil)
// NewLog returns a new viewer. // NewLog returns a new viewer.
func NewLog(gvr client.GVR, path, co string, prev bool) *Log { func NewLog(gvr client.GVR, path, co string, prev bool) *Log {
l := Log{ l := Log{
Flex: tview.NewFlex(), Flex: tview.NewFlex(),
cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff), model: model.NewLog(
model: model.NewLog(gvr, buildLogOpts(path, co, prev, true, config.DefaultLoggerTailCount), flushTimeout), gvr,
buildLogOpts(path, co, prev, true, config.DefaultLoggerTailCount),
flushTimeout,
),
} }
return &l return &l
} }
// Init initialiazes the viewer. // Init initializes the viewer.
func (l *Log) Init(ctx context.Context) (err error) { func (l *Log) Init(ctx context.Context) (err error) {
if l.app, err = extractApp(ctx); err != nil { if l.app, err = extractApp(ctx); err != nil {
return err return err
@ -61,7 +63,6 @@ func (l *Log) Init(ctx context.Context) (err error) {
l.model.Configure(l.app.Config.K9s.Logger) l.model.Configure(l.app.Config.K9s.Logger)
l.SetBorder(true) l.SetBorder(true)
l.SetBorderPadding(0, 0, 1, 1)
l.SetDirection(tview.FlexRow) l.SetDirection(tview.FlexRow)
l.indicator = NewLogIndicator(l.app.Config, l.app.Styles) l.indicator = NewLogIndicator(l.app.Config, l.app.Styles)
@ -72,14 +73,15 @@ func (l *Log) Init(ctx context.Context) (err error) {
if err = l.logs.Init(ctx); err != nil { if err = l.logs.Init(ctx); err != nil {
return err return err
} }
l.logs.SetBorderPadding(0, 0, 1, 1)
l.logs.SetText(logMessage) l.logs.SetText(logMessage)
l.logs.SetWrap(false) l.logs.SetWrap(false)
l.logs.SetMaxBuffer(l.app.Config.K9s.Logger.BufferSize) l.logs.SetMaxBuffer(l.app.Config.K9s.Logger.BufferSize)
l.logs.cmdBuff.AddListener(l)
l.ansiWriter = tview.ANSIWriter(l.logs, l.app.Styles.Views().Log.FgColor.String(), l.app.Styles.Views().Log.BgColor.String()) l.ansiWriter = tview.ANSIWriter(l.logs, l.app.Styles.Views().Log.FgColor.String(), l.app.Styles.Views().Log.BgColor.String())
l.AddItem(l.logs, 0, 1, true) l.AddItem(l.logs, 0, 1, true)
l.bindKeys() l.bindKeys()
l.logs.SetInputCapture(l.keyboard)
l.StylesChanged(l.app.Styles) l.StylesChanged(l.app.Styles)
l.app.Styles.AddListener(l) l.app.Styles.AddListener(l)
@ -89,15 +91,11 @@ func (l *Log) Init(ctx context.Context) (err error) {
l.model.AddListener(l) l.model.AddListener(l)
l.updateTitle() l.updateTitle()
l.cmdBuff.AddListener(l.app.Cmd())
l.cmdBuff.AddListener(l)
return nil return nil
} }
// LogCleared clears the logs. // LogCleared clears the logs.
func (l *Log) LogCleared() { func (l *Log) LogCleared() {
l.counts = 0
l.app.QueueUpdateDraw(func() { l.app.QueueUpdateDraw(func() {
l.logs.Clear() l.logs.Clear()
l.logs.ScrollTo(0, 0) l.logs.ScrollTo(0, 0)
@ -108,21 +106,30 @@ func (l *Log) LogCleared() {
func (l *Log) LogFailed(err error) { func (l *Log) LogFailed(err error) {
l.app.QueueUpdateDraw(func() { l.app.QueueUpdateDraw(func() {
l.app.Flash().Err(err) l.app.Flash().Err(err)
if l.logs.GetText(true) == logMessage {
l.logs.Clear()
}
l.write(color.Colorize(err.Error(), color.Red))
}) })
} }
// LogChanged updates the logs. // LogChanged updates the logs.
func (l *Log) LogChanged(lines []string) { func (l *Log) LogChanged(lines dao.LogItems) {
l.app.QueueUpdateDraw(func() { l.app.QueueUpdateDraw(func() {
l.Flush(lines) l.Flush(lines)
}) })
} }
// BufferChanged indicates the buffer was changed. // BufferChanged indicates the buffer was changed.
func (l *Log) BufferChanged(s string) {} func (l *Log) BufferChanged(s string) {
if err := l.model.Filter(l.logs.cmdBuff.String()); err != nil {
l.app.Flash().Err(err)
}
l.updateTitle()
}
// BufferActive indicates the buff activity changed. // BufferActive indicates the buff activity changed.
func (l *Log) BufferActive(state bool, k ui.BufferKind) { func (l *Log) BufferActive(state bool, k model.BufferKind) {
l.app.BufferActive(state, k) l.app.BufferActive(state, k)
} }
@ -151,7 +158,6 @@ func (l *Log) ExtraHints() map[string]string {
// Start runs the component. // Start runs the component.
func (l *Log) Start() { func (l *Log) Start() {
l.model.Start() l.model.Start()
l.app.SetFocus(l)
} }
// Stop terminates the component. // Stop terminates the component.
@ -159,8 +165,8 @@ func (l *Log) Stop() {
l.model.Stop() l.model.Stop()
l.model.RemoveListener(l) l.model.RemoveListener(l)
l.app.Styles.RemoveListener(l) l.app.Styles.RemoveListener(l)
l.cmdBuff.RemoveListener(l) l.logs.cmdBuff.RemoveListener(l)
l.cmdBuff.RemoveListener(l.app.Cmd()) l.logs.cmdBuff.RemoveListener(l.app.Cmd())
} }
// Name returns the component name. // Name returns the component name.
@ -168,43 +174,33 @@ func (l *Log) Name() string { return logTitle }
func (l *Log) bindKeys() { func (l *Log) bindKeys() {
l.logs.Actions().Set(ui.KeyActions{ l.logs.Actions().Set(ui.KeyActions{
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false), ui.Key0: ui.NewKeyAction("all", l.sinceCmd(-1), true),
tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, true), ui.Key1: ui.NewKeyAction("1m", l.sinceCmd(60), true),
ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true), ui.Key2: ui.NewKeyAction("5m", l.sinceCmd(5*60), true),
ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.ToggleAutoScrollCmd, true), ui.Key3: ui.NewKeyAction("15m", l.sinceCmd(15*60), true),
ui.KeyF: ui.NewKeyAction("FullScreen", l.fullScreenCmd, true), ui.Key4: ui.NewKeyAction("30m", l.sinceCmd(30*60), true),
ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.textWrapCmd, true), ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true),
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true), tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false),
ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", l.activateCmd, false), ui.KeyA: ui.NewKeyAction("Apply", l.applyCmd, true),
tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", l.resetCmd, false), ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true),
tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", l.eraseCmd, false), ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true),
tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", l.eraseCmd, false), ui.KeyF: ui.NewKeyAction("FullScreen", l.toggleFullScreenCmd, true),
tcell.KeyDelete: ui.NewSharedKeyAction("Erase", l.eraseCmd, false), ui.KeyT: ui.NewKeyAction("Toggle Timestamp", l.toggleTimestampCmd, true),
ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.toggleTextWrapCmd, true),
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true),
}) })
} }
func (l *Log) keyboard(evt *tcell.EventKey) *tcell.EventKey { func (l *Log) SendStrokes(s string) {
key := evt.Key() for _, r := range s {
if key == tcell.KeyUp || key == tcell.KeyDown { l.logs.keyboard(tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone))
return evt
}
if key == tcell.KeyRune {
if l.cmdBuff.IsActive() {
l.cmdBuff.Add(evt.Rune())
if err := l.model.Filter(l.cmdBuff.String()); err != nil {
l.app.Flash().Err(err)
}
l.updateTitle()
return nil
}
key = extractKey(evt)
} }
}
if a, ok := l.logs.Actions()[key]; ok { func (l *Log) SendKeys(kk ...tcell.Key) {
return a.Action(evt) for _, k := range kk {
l.logs.keyboard(tcell.NewEventKey(k, ' ', tcell.ModNone))
} }
return evt
} }
// Indicator returns the scroll mode viewer. // Indicator returns the scroll mode viewer.
@ -213,19 +209,26 @@ func (l *Log) Indicator() *LogIndicator {
} }
func (l *Log) updateTitle() { func (l *Log) updateTitle() {
var fmat string sinceSeconds, since := l.model.SinceSeconds(), "all"
if sinceSeconds > 0 && sinceSeconds < 60*60 {
since = fmt.Sprintf("%dm", sinceSeconds/60)
}
if sinceSeconds >= 60*60 {
since = fmt.Sprintf("%dh", sinceSeconds/(60*60))
}
var title string
path, co := l.model.GetPath(), l.model.GetContainer() path, co := l.model.GetPath(), l.model.GetContainer()
if co == "" { if co == "" {
fmat = ui.SkinTitle(fmt.Sprintf(logFmt, path), l.app.Styles.Frame()) title = ui.SkinTitle(fmt.Sprintf(logFmt, path, since), l.app.Styles.Frame())
} else { } else {
fmat = ui.SkinTitle(fmt.Sprintf(logCoFmt, path, co), l.app.Styles.Frame()) title = ui.SkinTitle(fmt.Sprintf(logCoFmt, path, co, since), l.app.Styles.Frame())
} }
buff := l.cmdBuff.String() buff := l.logs.cmdBuff.String()
if buff != "" { if buff != "" {
fmat += ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), l.app.Styles.Frame()) title += ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), l.app.Styles.Frame())
} }
l.SetTitle(fmat) l.SetTitle(title)
} }
// Logs returns the log viewer. // Logs returns the log viewer.
@ -238,43 +241,52 @@ func (l *Log) write(lines string) {
} }
// Flush write logs to viewer. // Flush write logs to viewer.
func (l *Log) Flush(lines []string) { func (l *Log) Flush(lines dao.LogItems) {
l.write(strings.Join(lines, "\n")) defer func(t time.Time) {
l.indicator.Refresh() log.Debug().Msgf("FLUSH %d--%v", len(lines), time.Since(t))
}(time.Now())
showTime := l.Indicator().showTime
ll := make([]string, len(lines))
for i, line := range lines {
ll[i] = string(line.Render(showTime))
}
l.write(strings.Join(ll, "\n"))
l.logs.ScrollToEnd() l.logs.ScrollToEnd()
l.indicator.Refresh()
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Actions()... // Actions()...
func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey { func (l *Log) sinceCmd(a int) func(evt *tcell.EventKey) *tcell.EventKey {
if !l.cmdBuff.IsActive() { return func(evt *tcell.EventKey) *tcell.EventKey {
return evt opts := l.model.LogOptions()
opts.SinceSeconds = int64(a)
l.model.SetLogOptions(opts)
l.updateTitle()
return nil
} }
l.cmdBuff.SetActive(false)
if err := l.model.Filter(l.cmdBuff.String()); err != nil {
l.app.Flash().Err(err)
}
l.updateTitle()
return nil
} }
func (l *Log) activateCmd(evt *tcell.EventKey) *tcell.EventKey { func (l *Log) applyCmd(evt *tcell.EventKey) *tcell.EventKey {
if l.app.InCmdMode() { if l.app.InCmdMode() {
return evt return evt
} }
l.cmdBuff.SetActive(true) ShowLogs(l.app, "blee", l.filterLogs)
return nil return nil
} }
func (l *Log) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { func (l *Log) filterLogs(path string, opts dao.LogOptions) {
if !l.cmdBuff.IsActive() { }
return nil
func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
if !l.logs.cmdBuff.IsActive() {
return evt
} }
l.cmdBuff.Delete() l.logs.cmdBuff.SetActive(false)
if err := l.model.Filter(l.cmdBuff.String()); err != nil { if err := l.model.Filter(l.logs.cmdBuff.String()); err != nil {
l.app.Flash().Err(err) l.app.Flash().Err(err)
} }
l.updateTitle() l.updateTitle()
@ -282,22 +294,6 @@ func (l *Log) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil return nil
} }
func (l *Log) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !l.cmdBuff.InCmdMode() {
l.cmdBuff.Reset()
return l.app.PrevCmd(evt)
}
if l.cmdBuff.String() != "" {
l.model.ClearFilter()
}
l.cmdBuff.SetActive(false)
l.cmdBuff.Reset()
l.updateTitle()
return nil
}
// SaveCmd dumps the logs to file. // SaveCmd dumps the logs to file.
func (l *Log) SaveCmd(evt *tcell.EventKey) *tcell.EventKey { func (l *Log) SaveCmd(evt *tcell.EventKey) *tcell.EventKey {
if path, err := saveData(l.app.Config.K9s.CurrentCluster, l.model.GetPath(), l.logs.GetText(true)); err != nil { if path, err := saveData(l.app.Config.K9s.CurrentCluster, l.model.GetPath(), l.logs.GetText(true)); err != nil {
@ -346,14 +342,33 @@ func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey {
return nil return nil
} }
func (l *Log) textWrapCmd(*tcell.EventKey) *tcell.EventKey { func (l *Log) toggleTimestampCmd(evt *tcell.EventKey) *tcell.EventKey {
if l.app.InCmdMode() {
return evt
}
l.indicator.ToggleTimestamp()
l.model.Refresh()
return nil
}
func (l *Log) toggleTextWrapCmd(evt *tcell.EventKey) *tcell.EventKey {
if l.app.InCmdMode() {
return evt
}
l.indicator.ToggleTextWrap() l.indicator.ToggleTextWrap()
l.logs.SetWrap(l.indicator.textWrap) l.logs.SetWrap(l.indicator.textWrap)
return nil return nil
} }
// ToggleAutoScrollCmd toggles autoscroll status. // ToggleAutoScrollCmd toggles autoscroll status.
func (l *Log) ToggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey { func (l *Log) toggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
if l.app.InCmdMode() {
return evt
}
l.indicator.ToggleAutoScroll() l.indicator.ToggleAutoScroll()
if l.indicator.AutoScroll() { if l.indicator.AutoScroll() {
l.model.Start() l.model.Start()
@ -363,34 +378,23 @@ func (l *Log) ToggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil return nil
} }
func (l *Log) fullScreenCmd(*tcell.EventKey) *tcell.EventKey { func (l *Log) toggleFullScreenCmd(evt *tcell.EventKey) *tcell.EventKey {
if l.app.InCmdMode() {
return evt
}
l.indicator.ToggleFullScreen() l.indicator.ToggleFullScreen()
l.goFullScreen() l.goFullScreen()
return nil return nil
} }
func (l *Log) goFullScreen() { func (l *Log) goFullScreen() {
sidePadding := 1
if l.indicator.FullScreen() {
sidePadding = 0
}
l.SetFullScreen(l.indicator.FullScreen()) l.SetFullScreen(l.indicator.FullScreen())
l.Box.SetBorder(!l.indicator.FullScreen()) l.Box.SetBorder(!l.indicator.FullScreen())
l.Flex.SetBorderPadding(0, 0, sidePadding, sidePadding)
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
// AsKey converts rune to keyboard key.,
func extractKey(evt *tcell.EventKey) tcell.Key {
key := tcell.Key(evt.Rune())
if evt.Modifiers() == tcell.ModAlt {
key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers()))
}
return key
}
func buildLogOpts(path, co string, prevLogs, showTime bool, tailLineCount int) dao.LogOptions { func buildLogOpts(path, co string, prevLogs, showTime bool, tailLineCount int) dao.LogOptions {
return dao.LogOptions{ return dao.LogOptions{
Path: path, Path: path,

View File

@ -0,0 +1,80 @@
package view
import (
"fmt"
"strconv"
"time"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview"
)
const logKey = "logs"
// LogCB represents a log callback function.
type LogCB func(path string, opts dao.LogOptions)
// ShowLogs pops a port forwarding configuration dialog.
func ShowLogs(a *App, path string, applyFn LogCB) {
styles := a.Styles
f := tview.NewForm()
f.SetItemPadding(0)
f.SetButtonsAlign(tview.AlignCenter).
SetButtonBackgroundColor(styles.BgColor()).
SetButtonTextColor(styles.FgColor()).
SetLabelColor(styles.K9s.Info.FgColor.Color()).
SetFieldTextColor(styles.K9s.Info.SectionColor.Color())
secs, start, in, out, container := "5", time.Now().String(), "", "", ""
f.AddInputField("Container:", container, 0, nil, func(v string) {
container = v
})
f.AddInputField("Since Seconds:", secs, 0, nil, func(v string) {
secs = v
})
f.AddInputField("Since Time:", start, 0, nil, func(v string) {
start = v
})
f.AddInputField("Filter In:", in, 0, nil, func(v string) {
in = v
})
f.AddInputField("Filter Out:", out, 0, nil, func(v string) {
out = v
})
pages := a.Content.Pages
f.AddButton("Apply", func() {
s, _ := strconv.Atoi(secs)
opts := dao.LogOptions{
SinceTime: start,
SinceSeconds: int64(s),
In: in,
Out: out,
}
applyFn(path, opts)
})
f.AddButton("Dismiss", func() {
DismissLogs(a, pages)
})
modal := tview.NewModalForm(fmt.Sprintf("<Configure Logs for %s>", path), f)
modal.SetDoneFunc(func(_ int, b string) {
DismissLogs(a, pages)
})
pages.AddPage(logKey, modal, false, true)
pages.ShowPage(logKey)
a.SetFocus(pages.GetPrimitive(logKey))
}
// DismissLogs dismiss the dialog.
func DismissLogs(a *App, p *ui.Pages) {
p.RemovePage(logKey)
a.SetFocus(p.CurrentPage().Item)
}
// ----------------------------------------------------------------------------
// Helpers...

View File

@ -27,13 +27,20 @@ func NewLogIndicator(cfg *config.Config, styles *config.Styles) *LogIndicator {
scrollStatus: 1, scrollStatus: 1,
fullScreen: cfg.K9s.FullScreenLogs, fullScreen: cfg.K9s.FullScreenLogs,
} }
l.SetBackgroundColor(styles.Views().Log.BgColor.Color()) l.StylesChanged(styles)
l.SetTextAlign(tview.AlignRight) styles.AddListener(&l)
l.SetTextAlign(tview.AlignCenter)
l.SetDynamicColors(true) l.SetDynamicColors(true)
return &l return &l
} }
// StylesChanged notifies listener the skin changed.
func (l *LogIndicator) StylesChanged(styles *config.Styles) {
l.SetBackgroundColor(styles.K9s.Views.Log.Indicator.BgColor.Color())
l.SetTextColor(styles.K9s.Views.Log.Indicator.FgColor.Color())
}
// AutoScroll reports the current scrolling status. // AutoScroll reports the current scrolling status.
func (l *LogIndicator) AutoScroll() bool { func (l *LogIndicator) AutoScroll() bool {
return atomic.LoadInt32(&l.scrollStatus) == 1 return atomic.LoadInt32(&l.scrollStatus) == 1
@ -86,8 +93,7 @@ func (l *LogIndicator) Refresh() {
l.Clear() l.Clear()
l.update("Autoscroll: " + l.onOff(l.AutoScroll())) l.update("Autoscroll: " + l.onOff(l.AutoScroll()))
l.update("FullScreen: " + l.onOff(l.fullScreen)) l.update("FullScreen: " + l.onOff(l.fullScreen))
// BOZO!! log timestamp l.update("Timestamps: " + l.onOff(l.showTime))
// l.update("Timestamp: " + l.onOff(l.showTime))
l.update("Wrap: " + l.onOff(l.textWrap)) l.update("Wrap: " + l.onOff(l.textWrap))
} }
@ -99,6 +105,5 @@ func (l *LogIndicator) onOff(b bool) string {
} }
func (l *LogIndicator) update(status string) { func (l *LogIndicator) update(status string) {
fg, bg := l.styles.Frame().Crumb.FgColor, l.styles.Frame().Crumb.ActiveColor fmt.Fprintf(l, "[::b]%-20s", status)
fmt.Fprintf(l, "[%s:%s:b] %-15s ", fg, bg, status)
} }

View File

@ -13,5 +13,5 @@ func TestLogIndicatorRefresh(t *testing.T) {
v := view.NewLogIndicator(config.NewConfig(nil), defaults) v := view.NewLogIndicator(config.NewConfig(nil), defaults)
v.Refresh() v.Refresh()
assert.Equal(t, "[black:orange:b] Autoscroll: On [black:orange:b] FullScreen: Off [black:orange:b] Wrap: Off \n", v.GetText(false)) assert.Equal(t, "[::b]Autoscroll: On [::b]FullScreen: Off [::b]Timestamps: Off [::b]Wrap: Off \n", v.GetText(false))
} }

View File

@ -0,0 +1,110 @@
package view
import (
"fmt"
"testing"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui"
"github.com/stretchr/testify/assert"
)
func TestLogAutoScroll(t *testing.T) {
v := NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
v.Init(makeContext())
v.GetModel().Set(dao.LogItems{dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo")})
v.GetModel().Notify(true)
assert.Equal(t, 14, len(v.Hints()))
v.toggleAutoScrollCmd(nil)
assert.Equal(t, "Autoscroll: Off FullScreen: Off Timestamps: Off Wrap: Off ", v.Indicator().GetText(true))
}
func TestLogViewNav(t *testing.T) {
v := NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
v.Init(makeContext())
var buff dao.LogItems
for i := 0; i < 100; i++ {
buff = append(buff, dao.NewLogItemFromString(fmt.Sprintf("line-%d\n", i)))
}
v.GetModel().Set(buff)
v.toggleAutoScrollCmd(nil)
r, _ := v.Logs().GetScrollOffset()
assert.Equal(t, 0, r)
}
func TestLogViewClear(t *testing.T) {
v := NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
v.Init(makeContext())
v.toggleAutoScrollCmd(nil)
v.Logs().SetText("blee\nblah")
v.Logs().Clear()
assert.Equal(t, "", v.Logs().GetText(true))
}
func TestLogTimestamp(t *testing.T) {
l := NewLog(client.NewGVR("test"), "fred/blee", "c1", false)
l.Init(makeContext())
buff := dao.LogItems{
&dao.LogItem{
Pod: "fred/blee",
Container: "c1",
Timestamp: "ttt",
Bytes: []byte("Testing 1, 2, 3"),
},
}
var list logList
l.GetModel().AddListener(&list)
l.GetModel().Set(buff)
l.SendKeys(ui.KeyT)
l.Logs().Clear()
l.Flush(buff)
assert.Equal(t, fmt.Sprintf("%-30s %s", "ttt", "fred/blee:c1 Testing 1, 2, 3\n"), l.Logs().GetText(true))
assert.Equal(t, 2, list.change)
assert.Equal(t, 2, list.clear)
assert.Equal(t, 0, list.fail)
}
func TestLogFilter(t *testing.T) {
l := NewLog(client.NewGVR("test"), "fred/blee", "c1", false)
l.Init(makeContext())
buff := dao.LogItems{
dao.NewLogItemFromString("duh"),
dao.NewLogItemFromString("zorg"),
}
var list logList
l.GetModel().AddListener(&list)
l.GetModel().Set(buff)
l.SendKeys(ui.KeySlash)
l.SendStrokes("zorg")
assert.Equal(t, "zorg", list.lines)
assert.Equal(t, 5, list.change)
assert.Equal(t, 5, list.clear)
assert.Equal(t, 0, list.fail)
}
// ----------------------------------------------------------------------------
// Helpers...
type logList struct {
change, clear, fail int
lines string
}
func (l *logList) LogChanged(ii dao.LogItems) {
l.change++
l.lines = ""
for _, i := range ii {
l.lines += string(i.Render(false))
}
}
func (l *logList) LogCleared() { l.clear++ }
func (l *logList) LogFailed(error) { l.fail++ }

View File

@ -9,6 +9,7 @@ import (
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/view" "github.com/derailed/k9s/internal/view"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -28,24 +29,12 @@ func TestLogAnsi(t *testing.T) {
assert.Equal(t, s+"\n", v.GetText(false)) assert.Equal(t, s+"\n", v.GetText(false))
} }
func TestLogAutoScroll(t *testing.T) {
v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
v.Init(makeContext())
v.GetModel().Set([]string{"blee", "bozo"})
v.GetModel().Notify(true)
assert.Equal(t, 6, len(v.Hints()))
v.ToggleAutoScrollCmd(nil)
assert.Equal(t, " Autoscroll: Off FullScreen: Off Wrap: Off ", v.Indicator().GetText(true))
}
func TestLogViewSave(t *testing.T) { func TestLogViewSave(t *testing.T) {
v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false) v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
v.Init(makeContext()) v.Init(makeContext())
app := makeApp() app := makeApp()
v.Flush([]string{"blee", "bozo"}) v.Flush(dao.LogItems{dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo")})
config.K9sDumpDir = "/tmp" config.K9sDumpDir = "/tmp"
dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster) dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster)
c1, _ := ioutil.ReadDir(dir) c1, _ := ioutil.ReadDir(dir)
@ -54,31 +43,6 @@ func TestLogViewSave(t *testing.T) {
assert.Equal(t, len(c2), len(c1)+1) assert.Equal(t, len(c2), len(c1)+1)
} }
func TestLogViewNav(t *testing.T) {
v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
v.Init(makeContext())
var buff []string
for i := 0; i < 100; i++ {
buff = append(buff, fmt.Sprintf("line-%d\n", i))
}
v.GetModel().Set(buff)
v.ToggleAutoScrollCmd(nil)
r, _ := v.Logs().GetScrollOffset()
assert.Equal(t, 0, r)
}
func TestLogViewClear(t *testing.T) {
v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
v.Init(makeContext())
v.ToggleAutoScrollCmd(nil)
v.Logs().SetText("blee\nblah")
v.Logs().Clear()
assert.Equal(t, "", v.Logs().GetText(true))
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...

View File

@ -11,11 +11,11 @@ import (
const portForwardKey = "portforward" const portForwardKey = "portforward"
// PortForwardFunc represents a port-forward callback function. // PortForwardCB represents a port-forward callback function.
type PortForwardFunc func(v ResourceViewer, path, co string, mapper client.PortTunnel) type PortForwardCB func(v ResourceViewer, path, co string, mapper client.PortTunnel)
// ShowPortForwards pops a port forwarding configuration dialog. // ShowPortForwards pops a port forwarding configuration dialog.
func ShowPortForwards(v ResourceViewer, path string, ports []string, okFn PortForwardFunc) { func ShowPortForwards(v ResourceViewer, path string, ports []string, okFn PortForwardCB) {
styles := v.App().Styles styles := v.App().Styles
f := tview.NewForm() f := tview.NewForm()

View File

@ -123,7 +123,7 @@ func startFwdCB(v ResourceViewer, path, co string, t client.PortTunnel) {
go runForward(v, pf, fwd) go runForward(v, pf, fwd)
} }
func showFwdDialog(v ResourceViewer, path string, cb PortForwardFunc) error { func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error {
mm, err := fetchPodPorts(v.App().factory, path) mm, err := fetchPodPorts(v.App().factory, path)
if err != nil { if err != nil {
return nil return nil

View File

@ -7,6 +7,7 @@ import (
"github.com/atotto/clipboard" "github.com/atotto/clipboard"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -132,7 +133,7 @@ func (t *Table) SetExtraActionsFn(BoostActionsFunc) {}
func (t *Table) BufferChanged(s string) {} func (t *Table) BufferChanged(s string) {}
// BufferActive indicates the buff activity changed. // BufferActive indicates the buff activity changed.
func (t *Table) BufferActive(state bool, k ui.BufferKind) { func (t *Table) BufferActive(state bool, k model.BufferKind) {
t.app.BufferActive(state, k) t.app.BufferActive(state, k)
} }

View File

@ -68,7 +68,7 @@ func TestTableViewFilter(t *testing.T) {
v.SearchBuff().SetActive(true) v.SearchBuff().SetActive(true)
v.SearchBuff().Set("blee") v.SearchBuff().Set("blee")
v.Refresh() v.Refresh()
assert.Equal(t, 2, v.GetRowCount()) assert.Equal(t, 1, v.GetRowCount())
} }
func TestTableViewSort(t *testing.T) { func TestTableViewSort(t *testing.T) {

View File

@ -576,7 +576,7 @@ func (x *Xray) Refresh() {
func (x *Xray) BufferChanged(s string) {} func (x *Xray) BufferChanged(s string) {}
// BufferActive indicates the buff activity changed. // BufferActive indicates the buff activity changed.
func (x *Xray) BufferActive(state bool, k ui.BufferKind) { func (x *Xray) BufferActive(state bool, k model.BufferKind) {
x.app.BufferActive(state, k) x.app.BufferActive(state, k)
} }

View File

@ -25,7 +25,6 @@ const (
) )
func colorizeYAML(style config.Yaml, raw string) string { func colorizeYAML(style config.Yaml, raw string) string {
// lines := strings.Split(raw, "\n")
lines := strings.Split(tview.Escape(raw), "\n") lines := strings.Split(tview.Escape(raw), "\n")
fullFmt := strings.Replace(yamlFullFmt, "[key", "["+style.KeyColor.String(), 1) fullFmt := strings.Replace(yamlFullFmt, "[key", "["+style.KeyColor.String(), 1)

View File

@ -15,7 +15,7 @@ import (
const ( const (
defaultResync = 10 * time.Minute defaultResync = 10 * time.Minute
defaultWaitTime = 500 * time.Millisecond defaultWaitTime = 250 * time.Millisecond
) )
// Factory tracks various resource informers. // Factory tracks various resource informers.

View File

@ -68,6 +68,9 @@ k9s:
logs: logs:
fgColor: *ghost fgColor: *ghost
bgColor: *bg bgColor: *bg
indicator:
fgColor: *ghost
bgColor: *bg
charts: charts:
bgColor: default bgColor: default
defaultDialColors: defaultDialColors:

View File

@ -90,3 +90,6 @@ k9s:
logs: logs:
fgColor: *foreground fgColor: *foreground
bgColor: *background bgColor: *background
indicator:
fgColor: *foreground
bgColor: *purple

View File

@ -84,3 +84,6 @@ k9s:
logs: logs:
fgColor: *dark fgColor: *dark
bgColor: *bg bgColor: *bg
indicator:
fgColor: *dark
bgColor: *bg

View File

@ -64,3 +64,6 @@ k9s:
logs: logs:
fgColor: white fgColor: white
bgColor: "#282a36" bgColor: "#282a36"
indicator:
fgColor: white
bgColor: "#282a36"

View File

@ -63,3 +63,6 @@ k9s:
logs: logs:
fgColor: white fgColor: white
bgColor: black bgColor: black
indicator:
fgColor: dodgerblue
bgColor: black