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()))
printLogo(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) {
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
}
fmt.Printf(fmat, section, value)

2
go.mod
View File

@ -30,7 +30,7 @@ replace (
require (
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
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/elazarl/goproxy 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/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.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/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
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.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
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/go.mod h1:KBxE6XWO57XSNA1PA9CvVLYRY0zWqYQTad84bNXp1lw=
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.
// BOZO!! No super sure about this approach either??
func (a *APIClient) CheckConnectivity() (status bool) {
defer func() {
if !status {

View File

@ -4,20 +4,22 @@ import (
"fmt"
)
const ColorFmt = "\x1b[%dm%s\x1b[0m"
// Paint describes a terminal color.
type Paint int
// Defines basic ANSI colors.
const (
Black Paint = iota + 30
Red
Green
Yellow
Blue
Magenta
Cyan
White
DarkGray = 90
Black Paint = iota + 30 // 30
Red // 31
Green // 32
Yellow // 33
Blue // 34
Magenta // 35
Cyan // 36
LightGray // 37
DarkGray = 90
Bold = 1
)
@ -25,7 +27,7 @@ const (
// Colorize returns an ASCII colored string based on given color.
func Colorize(s string, c Paint) string {
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 (
"testing"
"github.com/derailed/k9s/internal/color"
"github.com/stretchr/testify/assert"
)
func TestColorize(t *testing.T) {
uu := map[string]struct {
s string
c Paint
c color.Paint
e string
}{
"white": {"blee", White, "\x1b[37mblee\x1b[0m"},
"black": {"blee", Black, "\x1b[30mblee\x1b[0m"},
"default": {"blee", 0, "\x1b[37mblee\x1b[0m"},
"white": {"blee", color.LightGray, "\x1b[37mblee\x1b[0m"},
"black": {"blee", color.Black, "\x1b[30mblee\x1b[0m"},
"default": {"blee", 0, "blee"},
}
for k := range uu {
u := uu[k]
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.
func (a *Aliases) ShortNames() ShortNames {
a.mx.RLock()
@ -107,6 +119,13 @@ func (a *Aliases) LoadFileAliases(path string) error {
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() {
a.mx.Lock()
defer a.mx.Unlock()
@ -120,49 +139,16 @@ func (a *Aliases) loadDefaultAliases() {
a.Alias["rb"] = "rbac.authorization.k8s.io/v1/rolebindings"
a.Alias["np"] = "networking.k8s.io/v1/networkpolicies"
const contexts = "contexts"
{
a.Alias["ctx"] = contexts
a.Alias[contexts] = contexts
a.Alias["context"] = contexts
}
const users = "users"
{
a.Alias["usr"] = users
a.Alias[users] = users
a.Alias["user"] = users
}
const groups = "groups"
{
a.Alias["grp"] = groups
a.Alias["group"] = groups
a.Alias[groups] = groups
}
const portFwds = "portforwards"
{
a.Alias["pf"] = portFwds
a.Alias[portFwds] = portFwds
a.Alias["portforward"] = portFwds
}
const benchmarks = "benchmarks"
{
a.Alias["be"] = benchmarks
a.Alias["benchmark"] = benchmarks
a.Alias[benchmarks] = benchmarks
}
const dumps = "screendumps"
{
a.Alias["sd"] = dumps
a.Alias["screendump"] = dumps
a.Alias[dumps] = dumps
}
const pulses = "pulses"
{
a.Alias["hz"] = pulses
a.Alias["pu"] = pulses
a.Alias["pulse"] = pulses
a.Alias["pulses"] = pulses
}
a.declare("help", "h", "?")
a.declare("quit", "q", "Q")
a.declare("aliases", "alias", "a")
a.declare("contexts", "context", "ctx")
a.declare("users", "user", "usr")
a.declare("groups", "group", "grp")
a.declare("portforwards", "portforward", "pf")
a.declare("benchmarks", "benchmark", "be")
a.declare("screendumps", "screendump", "sd")
a.declare("pulses", "pulse", "pu", "hz")
}
// 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, 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.CurrentCluster)
assert.NotNil(t, cfg.K9s.Clusters)
@ -266,6 +266,7 @@ var expectedConfig = `k9s:
logger:
tail: 500
buffer: 800
sinceSeconds: 300
currentContext: blee
currentCluster: blee
fullScreenLogs: false
@ -314,7 +315,8 @@ var resetConfig = `k9s:
readOnly: false
logger:
tail: 200
buffer: 2000
buffer: 1000
sinceSeconds: 300
currentContext: blee
currentCluster: blee
fullScreenLogs: false

View File

@ -22,7 +22,7 @@ func TestK9sValidate(t *testing.T) {
c.Validate(mc, mk)
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, "ctx1", c.CurrentContext)
assert.Equal(t, "c1", c.CurrentCluster)
@ -45,7 +45,7 @@ func TestK9sValidateBlank(t *testing.T) {
c.Validate(mc, mk)
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, "ctx1", c.CurrentContext)
assert.Equal(t, "c1", c.CurrentCluster)

View File

@ -5,25 +5,29 @@ import (
)
const (
// DefaultLoggerTailCount tracks log tail size.
DefaultLoggerTailCount = 50
// DefaultLoggerBufferSize tracks the buffer size.
// DefaultLoggerTailCount tracks default log tail size.
DefaultLoggerTailCount = 100
// DefaultLoggerBufferSize tracks default view buffer size.
DefaultLoggerBufferSize = 1_000
// 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
type Logger struct {
TailCount int `yaml:"tail"`
BufferSize int `yaml:"buffer"`
TailCount int64 `yaml:"tail"`
BufferSize int `yaml:"buffer"`
SinceSeconds int64 `yaml:"sinceSeconds"`
}
// NewLogger returns a new instance.
func NewLogger() *Logger {
return &Logger{
TailCount: DefaultLoggerTailCount,
BufferSize: DefaultLoggerBufferSize,
TailCount: DefaultLoggerTailCount,
BufferSize: DefaultLoggerBufferSize,
SinceSeconds: DefaultSinceSeconds,
}
}
@ -41,4 +45,7 @@ func (l *Logger) Validate(_ client.Connection, _ KubeSettings) {
if 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.Validate(nil, nil)
assert.Equal(t, 50, l.TailCount)
assert.Equal(t, int64(100), l.TailCount)
assert.Equal(t, 1_000, l.BufferSize)
}
@ -19,6 +19,6 @@ func TestLoggerValidate(t *testing.T) {
var l config.Logger
l.Validate(nil, nil)
assert.Equal(t, 50, l.TailCount)
assert.Equal(t, int64(100), l.TailCount)
assert.Equal(t, 1_000, l.BufferSize)
}

View File

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

View File

@ -2,7 +2,6 @@ package dao
import (
"context"
"errors"
"fmt"
"github.com/derailed/k9s/internal"
@ -13,7 +12,6 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
restclient "k8s.io/client-go/rest"
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
func (c *Container) TailLogs(ctx context.Context, logChan chan<- []byte, opts LogOptions) error {
fac, ok := ctx.Value(internal.KeyFactory).(Factory)
if !ok {
return errors.New("Expecting an informer")
}
o, err := fac.Get("v1/pods", opts.Path, true, labels.Everything())
if err != nil {
return err
}
func (c *Container) TailLogs(ctx context.Context, logChan LogChan, opts LogOptions) error {
log.Debug().Msgf("CONTAINER-LOGS")
po := Pod{}
po.Init(c.Factory, client.NewGVR("v1/pods"))
var po v1.Pod
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
return po.TailLogs(ctx, logChan, opts)
}
// ----------------------------------------------------------------------------

View File

@ -80,7 +80,7 @@ func (d *Deployment) Restart(path string) error {
}
// 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)
if err != nil {
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.
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)
if err != nil {
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)
}
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)
if !ok {
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)
oo, err := f.List("v1/pods", ns, false, lsel)
oo, err := f.List("v1/pods", ns, true, lsel)
if err != nil {
return err
}
if len(oo) > 1 {
opts.MultiPods = true
}
opts.MultiPods = true
po := Pod{}
po.Init(f, client.NewGVR("v1/pods"))

View File

@ -12,6 +12,14 @@ import (
"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 {
if v2 == 0 {
return 0

View File

@ -23,7 +23,7 @@ type Job struct {
}
// 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())
if err != nil {
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 (
"strings"
"time"
"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.
@ -12,11 +14,13 @@ type LogOptions struct {
Path string
Container string
Lines int64
Color color.Paint
Previous bool
SingleContainer bool
MultiPods bool
ShowTimestamp bool
SinceTime string
SinceSeconds int64
In, Out string
}
// HasContainer checks if a container is present.
@ -24,6 +28,33 @@ func (o LogOptions) HasContainer() bool {
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.
func (o LogOptions) FixedSizeName() string {
_, n := client.Namespaced(o.Path)
@ -39,35 +70,21 @@ func (o LogOptions) FixedSizeName() string {
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.
func (o LogOptions) DecorateLog(bytes []byte) []byte {
func (o LogOptions) DecorateLog(bytes []byte) *LogItem {
item := NewLogItem(bytes)
if len(bytes) == 0 {
return bytes
return item
}
bytes = bytes[:len(bytes)-1]
_, n := client.Namespaced(o.Path)
var prefix []byte
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 {
prefix = []byte(colorize(o.Color, o.Container+" "))
item.Container = o.Container
}
if len(prefix) == 0 {
return bytes
}
return append(prefix, bytes...)
return item
}

View File

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

View File

@ -93,7 +93,7 @@ type NodeMaintainer interface {
// Loggable represents resources with logs.
type Loggable interface {
// 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.

View File

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

View File

@ -1,9 +1,9 @@
package ui_test
package model_test
import (
"testing"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/model"
"github.com/stretchr/testify/assert"
)
@ -17,7 +17,7 @@ func (l *testListener) BufferChanged(s string) {
l.text = s
}
func (l *testListener) BufferActive(s bool, _ ui.BufferKind) {
func (l *testListener) BufferActive(s bool, _ model.BufferKind) {
if s {
l.act++
return
@ -26,7 +26,7 @@ func (l *testListener) BufferActive(s bool, _ ui.BufferKind) {
}
func TestCmdBuffActivate(t *testing.T) {
b, l := ui.NewCmdBuff('>', ui.CommandBuff), testListener{}
b, l := model.NewCmdBuff('>', model.Command), testListener{}
b.AddListener(&l)
b.SetActive(true)
@ -36,7 +36,7 @@ func TestCmdBuffActivate(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.SetActive(false)
@ -46,7 +46,7 @@ func TestCmdBuffDeactivate(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.Add('b')
@ -78,7 +78,7 @@ func TestCmdBuffChanged(t *testing.T) {
}
func TestCmdBuffAdd(t *testing.T) {
b := ui.NewCmdBuff('>', ui.CommandBuff)
b := model.NewCmdBuff('>', model.Command)
uu := []struct {
runes []rune
@ -99,7 +99,7 @@ func TestCmdBuffAdd(t *testing.T) {
}
func TestCmdBuffDel(t *testing.T) {
b := ui.NewCmdBuff('>', ui.CommandBuff)
b := model.NewCmdBuff('>', model.Command)
uu := []struct {
runes []rune
@ -121,7 +121,7 @@ func TestCmdBuffDel(t *testing.T) {
}
func TestCmdBuffEmpty(t *testing.T) {
b := ui.NewCmdBuff('>', ui.CommandBuff)
b := model.NewCmdBuff('>', model.Command)
uu := []struct {
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 (
"context"
"fmt"
"regexp"
"strings"
"sync"
"time"
@ -13,13 +11,12 @@ import (
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"github.com/rs/zerolog/log"
"github.com/sahilm/fuzzy"
)
// LogsListener represents a log model listener.
type LogsListener interface {
// LogChanged notifies the model changed.
LogChanged([]string)
LogChanged(dao.LogItems)
// LogCleanred indicates logs are cleared.
LogCleared()
@ -30,18 +27,17 @@ type LogsListener interface {
// Log represents a resource logger.
type Log struct {
factory dao.Factory
lines []string
listeners []LogsListener
gvr client.GVR
logOptions dao.LogOptions
cancelFn context.CancelFunc
mx sync.RWMutex
filter string
bufferSize int
lastSent int
showTimestamp bool
flushTimeout time.Duration
factory dao.Factory
lines dao.LogItems
listeners []LogsListener
gvr client.GVR
logOptions dao.LogOptions
cancelFn context.CancelFunc
mx sync.RWMutex
filter string
bufferSize int
lastSent int
flushTimeout time.Duration
}
// 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.
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.
@ -74,22 +89,25 @@ func (l *Log) Init(f dao.Factory) {
func (l *Log) Clear() {
l.mx.Lock()
{
l.lines, l.lastSent = []string{}, 0
l.lines, l.lastSent = dao.LogItems{}, 0
}
l.mx.Unlock()
l.fireLogCleared()
}
// ShowTimestamp toggles timestamp on logs.
func (l *Log) ShowTimestamp(b bool) {
l.mx.RLock()
defer l.mx.RUnlock()
l.showTimestamp = b
// Refresh refreshes the logs.
func (l *Log) Refresh() {
l.fireLogCleared()
l.fireLogChanged(l.lines)
}
// Restart restarts the logger.
func (l *Log) Restart() {
l.Clear()
l.Stop()
l.Start()
}
// Start initialize log tailer.
func (l *Log) Start() {
if err := l.load(); err != nil {
@ -108,21 +126,21 @@ func (l *Log) Stop() {
}
// Set sets the log lines (for testing only!)
func (l *Log) Set(lines []string) {
func (l *Log) Set(items dao.LogItems) {
l.mx.Lock()
defer l.mx.Unlock()
l.lines = lines
l.fireLogChanged(lines)
l.lines = items
l.fireLogCleared()
l.fireLogChanged(items)
}
// ClearFilter resets the log filter if any.
func (l *Log) ClearFilter() {
log.Debug().Msgf("CLEARED!!")
l.mx.RLock()
defer l.mx.RUnlock()
l.filter = ""
l.fireLogCleared()
l.fireLogChanged(l.lines)
}
@ -131,14 +149,9 @@ func (l *Log) Filter(q string) error {
l.mx.RLock()
defer l.mx.RUnlock()
log.Debug().Msgf("FILTER!")
l.filter = q
filtered, err := applyFilter(l.filter, l.lines)
if err != nil {
return err
}
l.fireLogCleared()
l.fireLogChanged(filtered)
l.fireLogBuffChanged(l.lines)
return nil
}
@ -148,7 +161,7 @@ func (l *Log) load() error {
ctx = context.WithValue(context.Background(), internal.KeyFactory, l.factory)
ctx, l.cancelFn = context.WithCancel(ctx)
c := make(chan []byte, 10)
c := make(dao.LogChan, 10)
go l.updateLogs(ctx, c)
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)
}
if err := logger.TailLogs(ctx, c, l.logOptions); err != nil {
log.Error().Err(err).Msgf("Tail logs failed")
if l.cancelFn != nil {
l.cancelFn()
}
@ -171,14 +185,15 @@ func (l *Log) load() error {
}
// Append adds a log line.
func (l *Log) Append(line string) {
if line == "" {
func (l *Log) Append(line *dao.LogItem) {
if line == nil || line.IsEmpty() {
return
}
l.mx.Lock()
defer l.mx.Unlock()
l.logOptions.SinceTime = line.Timestamp
if l.lines == nil {
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() {
log.Debug().Msgf("updateLogs view bailing out!")
}()
for {
select {
case bytes, ok := <-c:
case item, ok := <-c:
if !ok {
log.Debug().Msgf("Closed channel detected. Bailing out...")
l.Append(string(bytes))
l.Append(item)
l.Notify(false)
return
}
l.Append(string(bytes))
l.Append(item)
var overflow bool
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 == "" {
return lines, nil
}
indexes, err := filter(q, lines)
indexes, err := lines.Filter(q)
if err != nil {
return nil, err
}
@ -272,7 +287,7 @@ func applyFilter(q string, lines []string) ([]string, error) {
if len(indexes) == 0 {
return nil, nil
}
filtered := make([]string, 0, len(indexes))
filtered := make(dao.LogItems, 0, len(indexes))
for _, idx := range indexes {
filtered = append(filtered, lines[idx])
}
@ -280,7 +295,7 @@ func applyFilter(q string, lines []string) ([]string, error) {
return filtered, nil
}
func (l *Log) fireLogBuffChanged(lines []string) {
func (l *Log) fireLogBuffChanged(lines dao.LogItems) {
filtered, err := applyFilter(l.filter, lines)
if err != nil {
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 {
lis.LogChanged(lines)
}
@ -308,55 +323,3 @@ func (l *Log) fireLogCleared() {
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()
m.AddListener(v)
data := make([]string, 0, 2*size)
data := make(dao.LogItems, 0, 2*size)
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.Notify(true)
@ -47,8 +47,8 @@ func TestLogFilter(t *testing.T) {
e: 2,
},
"regexp": {
q: `\Apod-line-[1-3]{1}\z`,
e: 3,
q: `pod-line-[1-3]{1}`,
e: 4,
},
"fuzzy": {
q: `-f po-l1`,
@ -67,21 +67,21 @@ func TestLogFilter(t *testing.T) {
m.AddListener(v)
m.Filter(u.q)
var data []string
var data dao.LogItems
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.Notify(true)
assert.Equal(t, 2, v.dataCalled)
assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 2, v.clearCalled)
assert.Equal(t, 0, v.errCalled)
assert.Equal(t, u.e, len(v.data))
m.ClearFilter()
assert.Equal(t, 3, v.dataCalled)
assert.Equal(t, 2, v.clearCalled)
assert.Equal(t, 2, v.dataCalled)
assert.Equal(t, 3, v.clearCalled)
assert.Equal(t, 0, v.errCalled)
assert.Equal(t, size, len(v.data))
})
@ -96,7 +96,7 @@ func TestLogStartStop(t *testing.T) {
m.AddListener(v)
m.Start()
data := []string{"line1", "line2"}
data := dao.LogItems{dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2")}
for _, d := range data {
m.Append(d)
}
@ -118,7 +118,7 @@ func TestLogClear(t *testing.T) {
v := newTestView()
m.AddListener(v)
data := []string{"line1", "line2"}
data := dao.LogItems{dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2")}
for _, d := range data {
m.Append(d)
}
@ -138,11 +138,11 @@ func TestLogBasic(t *testing.T) {
v := newTestView()
m.AddListener(v)
data := []string{"line1", "line2"}
data := dao.LogItems{dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2")}
m.Set(data)
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, data, v.data)
}
@ -153,21 +153,25 @@ func TestLogAppend(t *testing.T) {
v := newTestView()
m.AddListener(v)
m.Set([]string{"blah blah"})
assert.Equal(t, []string{"blah blah"}, v.data)
items := dao.LogItems{dao.NewLogItemFromString("blah blah")}
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 {
m.Append(d)
}
assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, []string{"blah blah"}, v.data)
assert.Equal(t, items, v.data)
m.Notify(true)
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, append([]string{"blah blah"}, data...), v.data)
assert.Equal(t, append(items, data...), v.data)
}
func TestLogTimedout(t *testing.T) {
@ -178,15 +182,20 @@ func TestLogTimedout(t *testing.T) {
m.AddListener(v)
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 {
m.Append(d)
}
m.Notify(true)
assert.Equal(t, 2, v.dataCalled)
assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 2, v.clearCalled)
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 {
data []string
data dao.LogItems
dataCalled int
clearCalled int
errCalled int
@ -213,13 +222,13 @@ func newTestView() *testView {
return &testView{}
}
func (t *testView) LogChanged(d []string) {
func (t *testView) LogChanged(d dao.LogItems) {
t.data = d
t.dataCalled++
}
func (t *testView) LogCleared() {
t.clearCalled++
t.data = []string{}
t.data = dao.LogItems{}
}
func (t *testView) LogFailed(err error) {
fmt.Println("LogErr", err)

View File

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

View File

@ -4,6 +4,7 @@ import (
"regexp"
"strings"
"github.com/derailed/k9s/internal/dao"
"github.com/sahilm/fuzzy"
)
@ -94,7 +95,7 @@ func (t *Text) filter(q string, lines []string) fuzzy.Matches {
if q == "" {
return nil
}
if isFuzzySelector(q) {
if dao.IsFuzzySelector(q) {
return t.fuzzyFilter(strings.TrimSpace(q[2:]), 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 {
meta, ok := Registry[t.gvr.String()]
if !ok {
log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr)
meta = ResourceMeta{
DAO: &dao.Table{},
Renderer: &render.Generic{},

View File

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

View File

@ -4,25 +4,29 @@ import (
"fmt"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
)
const defaultPrompt = "%c> %s"
const defaultPrompt = "%c> [::b]%s"
// Command captures users free from command input.
type Command struct {
*tview.TextView
activated bool
icon rune
text string
styles *config.Styles
activated bool
icon rune
text string
styles *config.Styles
model *model.FishBuff
suggestions []string
suggestionIndex int
}
// NewCommand returns a new command view.
func NewCommand(styles *config.Styles) *Command {
c := Command{styles: styles, TextView: tview.NewTextView()}
func NewCommand(styles *config.Styles, m *model.FishBuff) *Command {
c := Command{styles: styles, TextView: tview.NewTextView(), model: m}
c.SetWordWrap(true)
c.SetWrap(true)
c.SetDynamicColors(true)
@ -31,10 +35,46 @@ func NewCommand(styles *config.Styles) *Command {
c.SetBackgroundColor(styles.BgColor())
c.SetTextColor(styles.FgColor())
styles.AddListener(&c)
c.SetInputCapture(c.keyboard)
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.
func (c *Command) StylesChanged(s *config.Styles) {
c.styles = s
@ -60,6 +100,11 @@ func (c *Command) update(s string) {
c.write(c.text)
}
func (c *Command) suggest(text, suggestion string) {
c.Clear()
c.write(text + "[gray::-]" + suggestion)
}
func (c *Command) write(s string) {
fmt.Fprintf(c, defaultPrompt, c.icon, s)
}
@ -67,19 +112,28 @@ func (c *Command) write(s string) {
// ----------------------------------------------------------------------------
// 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.
func (c *Command) BufferChanged(s string) {
c.update(s)
}
// 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 {
c.SetBorder(true)
c.SetTextColor(c.styles.FgColor())
c.SetBorderColor(colorFor(k))
c.icon = iconFor(k)
// c.reset()
c.activate()
} else {
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 {
case CommandBuff:
case model.Command:
return tcell.ColorAqua
default:
return tcell.ColorSeaGreen
}
}
func iconFor(k BufferKind) rune {
func iconFor(k model.BufferKind) rune {
switch k {
case CommandBuff:
case model.Command:
return '🐶'
default:
return '🐩'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ type Tree struct {
actions KeyActions
selectedItem string
cmdBuff *CmdBuff
cmdBuff *model.CmdBuff
expandNodes bool
Count int
keyListener KeyListenerFunc
@ -29,7 +29,7 @@ func NewTree() *Tree {
TreeView: tview.NewTreeView(),
expandNodes: true,
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.
func (t *Tree) CmdBuff() *CmdBuff {
func (t *Tree) CmdBuff() *model.CmdBuff {
return t.cmdBuff
}

View File

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

View File

@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"sort"
"strings"
"sync/atomic"
"time"
@ -96,6 +98,7 @@ func (a *App) Init(version string, rate int) error {
if err := a.command.Init(); err != nil {
return err
}
a.CmdBuff().SetSuggestionFn(a.suggestCommand())
a.clusterInfo().Init()
@ -104,9 +107,9 @@ func (a *App) Init(version string, rate int) error {
main := tview.NewFlex().SetDirection(tview.FlexRow)
main.AddItem(a.statusIndicator(), 1, 1, false)
main.AddItem(flash, 1, 1, false)
main.AddItem(a.Content, 0, 10, true)
main.AddItem(a.Crumbs(), 1, 1, false)
main.AddItem(flash, 1, 1, false)
a.Main.AddPage("main", main, true, false)
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
}
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 {
key := evt.Key()
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 {
if a.CmdBuff().InCmdMode() {
return evt
}
if _, ok := a.Content.GetPrimitive("main").(*Alias); ok {
return evt
}

View File

@ -403,14 +403,14 @@ func (b *Browser) namespaceActions(aa ui.KeyActions) {
return
}
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
index := 1
for _, ns := range b.app.Config.FavNamespaces() {
if ns == client.NamespaceAll {
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
index++
}

View File

@ -7,7 +7,6 @@ import (
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
)
// 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) {
aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)
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.KeyShiftS: ui.NewKeyAction("Sort Status", c.GetTable().SortColCmd(statusCol, 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.
func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) {
// BOZO!!
c.app.QueueUpdate(func() {
c.Clear()
c.layout()

View File

@ -116,7 +116,6 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt
}
log.Debug().Msgf("CONTAINER-SEL %q", path)
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))
return nil
@ -126,7 +125,6 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
if !ok {
return nil
}
log.Debug().Msgf("CONTAINER-PORTS %#v", ports)
ShowPortForwards(c, c.GetTable().Path, ports, startFwdCB)
return nil

View File

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

View File

@ -10,6 +10,7 @@ import (
"time"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/color"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
@ -21,10 +22,10 @@ import (
const (
logTitle = "logs"
logMessage = "[:orange:b]Waiting for logs...[::]"
logCoFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:bg:-]) "
logFmt = " Logs([fg:bg:]%s) "
flushTimeout = 200 * time.Millisecond
logMessage = "Waiting for logs..."
logFmt = " Logs([hilite:bg:]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] "
logCoFmt = " Logs([hilite:bg:]%s:[hilite:bg:b]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] "
flushTimeout = 100 * time.Millisecond
)
// Log represents a generic log viewer.
@ -35,9 +36,7 @@ type Log struct {
logs *Details
indicator *LogIndicator
ansiWriter io.Writer
cmdBuff *ui.CmdBuff
model *model.Log
counts int
}
var _ model.Component = (*Log)(nil)
@ -45,15 +44,18 @@ var _ model.Component = (*Log)(nil)
// NewLog returns a new viewer.
func NewLog(gvr client.GVR, path, co string, prev bool) *Log {
l := Log{
Flex: tview.NewFlex(),
cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff),
model: model.NewLog(gvr, buildLogOpts(path, co, prev, true, config.DefaultLoggerTailCount), flushTimeout),
Flex: tview.NewFlex(),
model: model.NewLog(
gvr,
buildLogOpts(path, co, prev, true, config.DefaultLoggerTailCount),
flushTimeout,
),
}
return &l
}
// Init initialiazes the viewer.
// Init initializes the viewer.
func (l *Log) Init(ctx context.Context) (err error) {
if l.app, err = extractApp(ctx); err != nil {
return err
@ -61,7 +63,6 @@ func (l *Log) Init(ctx context.Context) (err error) {
l.model.Configure(l.app.Config.K9s.Logger)
l.SetBorder(true)
l.SetBorderPadding(0, 0, 1, 1)
l.SetDirection(tview.FlexRow)
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 {
return err
}
l.logs.SetBorderPadding(0, 0, 1, 1)
l.logs.SetText(logMessage)
l.logs.SetWrap(false)
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.AddItem(l.logs, 0, 1, true)
l.bindKeys()
l.logs.SetInputCapture(l.keyboard)
l.StylesChanged(l.app.Styles)
l.app.Styles.AddListener(l)
@ -89,15 +91,11 @@ func (l *Log) Init(ctx context.Context) (err error) {
l.model.AddListener(l)
l.updateTitle()
l.cmdBuff.AddListener(l.app.Cmd())
l.cmdBuff.AddListener(l)
return nil
}
// LogCleared clears the logs.
func (l *Log) LogCleared() {
l.counts = 0
l.app.QueueUpdateDraw(func() {
l.logs.Clear()
l.logs.ScrollTo(0, 0)
@ -108,21 +106,30 @@ func (l *Log) LogCleared() {
func (l *Log) LogFailed(err error) {
l.app.QueueUpdateDraw(func() {
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.
func (l *Log) LogChanged(lines []string) {
func (l *Log) LogChanged(lines dao.LogItems) {
l.app.QueueUpdateDraw(func() {
l.Flush(lines)
})
}
// 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.
func (l *Log) BufferActive(state bool, k ui.BufferKind) {
func (l *Log) BufferActive(state bool, k model.BufferKind) {
l.app.BufferActive(state, k)
}
@ -151,7 +158,6 @@ func (l *Log) ExtraHints() map[string]string {
// Start runs the component.
func (l *Log) Start() {
l.model.Start()
l.app.SetFocus(l)
}
// Stop terminates the component.
@ -159,8 +165,8 @@ func (l *Log) Stop() {
l.model.Stop()
l.model.RemoveListener(l)
l.app.Styles.RemoveListener(l)
l.cmdBuff.RemoveListener(l)
l.cmdBuff.RemoveListener(l.app.Cmd())
l.logs.cmdBuff.RemoveListener(l)
l.logs.cmdBuff.RemoveListener(l.app.Cmd())
}
// Name returns the component name.
@ -168,43 +174,33 @@ func (l *Log) Name() string { return logTitle }
func (l *Log) bindKeys() {
l.logs.Actions().Set(ui.KeyActions{
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false),
tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, true),
ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true),
ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.ToggleAutoScrollCmd, true),
ui.KeyF: ui.NewKeyAction("FullScreen", l.fullScreenCmd, true),
ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.textWrapCmd, true),
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true),
ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", l.activateCmd, false),
tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", l.resetCmd, false),
tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", l.eraseCmd, false),
tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", l.eraseCmd, false),
tcell.KeyDelete: ui.NewSharedKeyAction("Erase", l.eraseCmd, false),
ui.Key0: ui.NewKeyAction("all", l.sinceCmd(-1), true),
ui.Key1: ui.NewKeyAction("1m", l.sinceCmd(60), true),
ui.Key2: ui.NewKeyAction("5m", l.sinceCmd(5*60), true),
ui.Key3: ui.NewKeyAction("15m", l.sinceCmd(15*60), true),
ui.Key4: ui.NewKeyAction("30m", l.sinceCmd(30*60), true),
ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true),
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false),
ui.KeyA: ui.NewKeyAction("Apply", l.applyCmd, true),
ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true),
ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true),
ui.KeyF: ui.NewKeyAction("FullScreen", l.toggleFullScreenCmd, true),
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 {
key := evt.Key()
if key == tcell.KeyUp || key == tcell.KeyDown {
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)
func (l *Log) SendStrokes(s string) {
for _, r := range s {
l.logs.keyboard(tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone))
}
}
if a, ok := l.logs.Actions()[key]; ok {
return a.Action(evt)
func (l *Log) SendKeys(kk ...tcell.Key) {
for _, k := range kk {
l.logs.keyboard(tcell.NewEventKey(k, ' ', tcell.ModNone))
}
return evt
}
// Indicator returns the scroll mode viewer.
@ -213,19 +209,26 @@ func (l *Log) Indicator() *LogIndicator {
}
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()
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 {
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 != "" {
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.
@ -238,43 +241,52 @@ func (l *Log) write(lines string) {
}
// Flush write logs to viewer.
func (l *Log) Flush(lines []string) {
l.write(strings.Join(lines, "\n"))
l.indicator.Refresh()
func (l *Log) Flush(lines dao.LogItems) {
defer func(t time.Time) {
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.indicator.Refresh()
}
// ----------------------------------------------------------------------------
// Actions()...
func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
if !l.cmdBuff.IsActive() {
return evt
func (l *Log) sinceCmd(a int) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
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() {
return evt
}
l.cmdBuff.SetActive(true)
ShowLogs(l.app, "blee", l.filterLogs)
return nil
}
func (l *Log) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
if !l.cmdBuff.IsActive() {
return nil
func (l *Log) filterLogs(path string, opts dao.LogOptions) {
}
func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
if !l.logs.cmdBuff.IsActive() {
return evt
}
l.cmdBuff.Delete()
if err := l.model.Filter(l.cmdBuff.String()); err != nil {
l.logs.cmdBuff.SetActive(false)
if err := l.model.Filter(l.logs.cmdBuff.String()); err != nil {
l.app.Flash().Err(err)
}
l.updateTitle()
@ -282,22 +294,6 @@ func (l *Log) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
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.
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 {
@ -346,14 +342,33 @@ func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey {
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.logs.SetWrap(l.indicator.textWrap)
return nil
}
// 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()
if l.indicator.AutoScroll() {
l.model.Start()
@ -363,34 +378,23 @@ func (l *Log) ToggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
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.goFullScreen()
return nil
}
func (l *Log) goFullScreen() {
sidePadding := 1
if l.indicator.FullScreen() {
sidePadding = 0
}
l.SetFullScreen(l.indicator.FullScreen())
l.Box.SetBorder(!l.indicator.FullScreen())
l.Flex.SetBorderPadding(0, 0, sidePadding, sidePadding)
}
// ----------------------------------------------------------------------------
// 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 {
return dao.LogOptions{
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,
fullScreen: cfg.K9s.FullScreenLogs,
}
l.SetBackgroundColor(styles.Views().Log.BgColor.Color())
l.SetTextAlign(tview.AlignRight)
l.StylesChanged(styles)
styles.AddListener(&l)
l.SetTextAlign(tview.AlignCenter)
l.SetDynamicColors(true)
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.
func (l *LogIndicator) AutoScroll() bool {
return atomic.LoadInt32(&l.scrollStatus) == 1
@ -86,8 +93,7 @@ func (l *LogIndicator) Refresh() {
l.Clear()
l.update("Autoscroll: " + l.onOff(l.AutoScroll()))
l.update("FullScreen: " + l.onOff(l.fullScreen))
// BOZO!! log timestamp
// l.update("Timestamp: " + l.onOff(l.showTime))
l.update("Timestamps: " + l.onOff(l.showTime))
l.update("Wrap: " + l.onOff(l.textWrap))
}
@ -99,6 +105,5 @@ func (l *LogIndicator) onOff(b bool) string {
}
func (l *LogIndicator) update(status string) {
fg, bg := l.styles.Frame().Crumb.FgColor, l.styles.Frame().Crumb.ActiveColor
fmt.Fprintf(l, "[%s:%s:b] %-15s ", fg, bg, status)
fmt.Fprintf(l, "[::b]%-20s", status)
}

View File

@ -13,5 +13,5 @@ func TestLogIndicatorRefresh(t *testing.T) {
v := view.NewLogIndicator(config.NewConfig(nil), defaults)
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/config"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/view"
"github.com/derailed/tview"
"github.com/stretchr/testify/assert"
@ -28,24 +29,12 @@ func TestLogAnsi(t *testing.T) {
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) {
v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
v.Init(makeContext())
app := makeApp()
v.Flush([]string{"blee", "bozo"})
v.Flush(dao.LogItems{dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo")})
config.K9sDumpDir = "/tmp"
dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster)
c1, _ := ioutil.ReadDir(dir)
@ -54,31 +43,6 @@ func TestLogViewSave(t *testing.T) {
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...

View File

@ -11,11 +11,11 @@ import (
const portForwardKey = "portforward"
// PortForwardFunc represents a port-forward callback function.
type PortForwardFunc func(v ResourceViewer, path, co string, mapper client.PortTunnel)
// PortForwardCB represents a port-forward callback function.
type PortForwardCB func(v ResourceViewer, path, co string, mapper client.PortTunnel)
// 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
f := tview.NewForm()

View File

@ -123,7 +123,7 @@ func startFwdCB(v ResourceViewer, path, co string, t client.PortTunnel) {
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)
if err != nil {
return nil

View File

@ -7,6 +7,7 @@ import (
"github.com/atotto/clipboard"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
@ -132,7 +133,7 @@ func (t *Table) SetExtraActionsFn(BoostActionsFunc) {}
func (t *Table) BufferChanged(s string) {}
// 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)
}

View File

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

View File

@ -576,7 +576,7 @@ func (x *Xray) Refresh() {
func (x *Xray) BufferChanged(s string) {}
// 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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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