checkpoint

mine
derailed 2020-10-26 16:49:28 -06:00
parent ca51ce547b
commit ffc31f856a
96 changed files with 759 additions and 304 deletions

View File

@ -0,0 +1,59 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.23.0
## Notes
Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!
If you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorhip program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)
---
## ♫ Sound Behind The Release ♭
I figured, why not share one of the tunes I was spinning when powering thru teh bugs? Might as well share the pain/pleasure while viewing this release notes!
[On An Island - David Gilmour With Crosby&Nash](https://www.youtube.com/watch?v=kEa__0wtIRo)
## A Word From Our Sponsors...
First off, I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!
* [Martin Kemp](https://github.com/MartiUK)
Contrarily to popular belief, OSS is not free! We've now reached ~9k stars and 300k downloads! As you all know, this project is not pimped out by a big company with deep pockets and a large team. K9s is complex and does demand a lot of my time. So if this tool is useful to you and part of your daily lifecycle, please contribute! Your contribution whether financial, PRs, issues or shout-outs on social/blogs are crucial to keep K9s growing and powerful for all of us. Don't let OSS by individual contributors become an oxymoron!
## Best Effort... Not!
In this drop, we've added 2 new columns to the Pod/Container views namely `CPU(R:L)` and `MEM(R:L)`. These represents the current request/limit resources specified at either the pod or container level. While in Pod view, you will need to use the wide command `Ctrl-W` to see the resources set at the pod level or you can use K9s column customization feature to volunteer them by default.
## Container Images
You have now the ability to tweak your container images for experimentation, using the new SetImage binding aka `i`. This feature is available for standalone pods, deployments, sts and ds. With a resource selected, pressing `i` will provision an edit dialog listing all init/container images.
NOTE! This is a one shot commands applied directly against your cluster and won't survive a new resource deployment.
Big `ATTA Boy!` in effect to [Antoine Méausoone](https://github.com/Ameausoone) for putting forth the effort to make this feature available to all of us!!
## Resolved Issues/Features
* [Issue #906](https://github.com/derailed/k9s/issues/906) Print resources in pod view
* [Issue #900](https://github.com/derailed/k9s/issues/900) Support sort by pending status
* [Issue #895](https://github.com/derailed/k9s/issues/895) Wrong highlight position when filtering logs
* [Issue #892](https://github.com/derailed/k9s/issues/892) tacit kustomize & kpt support
* [Issue #889](https://github.com/derailed/k9s/issues/889) Disable read only config via command line flag
*
* [Issue #886](https://github.com/derailed/k9s/issues/886) Full screen mode or remove borders in YAML view for easy copy/paste
* [Issue #887](https://github.com/derailed/k9s/issues/887) Ability to call out a separate program to parse/filter logs
* [Issue #884](https://github.com/derailed/k9s/issues/884) Refresh for describe & yaml view
* [Issue #883](https://github.com/derailed/k9s/issues/883) View logs quickly scrolls through entire logs when initially loading
* [Issue #875](https://github.com/derailed/k9s/issues/875) Lazy filter
* [Issue #848](https://github.com/derailed/k9s/issues/848) Support an inverse operator on filtered search
## Resolved PRs
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -26,7 +26,7 @@ const (
var _ config.KubeSettings = (*client.Config)(nil) var _ config.KubeSettings = (*client.Config)(nil)
var ( var (
version, commit, date = "dev", "dev", "n/a" version, commit, date = "dev", "dev", client.NA
k9sFlags *config.Flags k9sFlags *config.Flags
k8sFlags *genericclioptions.ConfigFlags k8sFlags *genericclioptions.ConfigFlags
demoMode = new(bool) demoMode = new(bool)
@ -215,7 +215,7 @@ func initK9sFlags() {
k9sFlags.ReadOnly, k9sFlags.ReadOnly,
"readonly", "readonly",
false, false,
"Disable all commands that modify the cluster", "Toggles readOnly mode by overriding configuration setting",
) )
} }

View File

@ -249,11 +249,12 @@ func (m *MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetri
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
const megaByte = 1024 * 1024 // MegaByte represents a megabyte.
const MegaByte = 1024 * 1024
// ToMB converts bytes to megabytes. // ToMB converts bytes to megabytes.
func ToMB(v int64) int64 { func ToMB(v int64) int64 {
return v / megaByte return v / MegaByte
} }
// ToPercentage computes percentage. // ToPercentage computes percentage.

View File

@ -29,14 +29,13 @@ func TestToPercentage(t *testing.T) {
} }
func TestToMB(t *testing.T) { func TestToMB(t *testing.T) {
const mb = 1024 * 1024
uu := []struct { uu := []struct {
v int64 v int64
e int64 e int64
}{ }{
{0, 0}, {0, 0},
{2 * mb, 2}, {2 * client.MegaByte, 2},
{10 * mb, 10}, {10 * client.MegaByte, 10},
} }
for _, u := range uu { for _, u := range uu {

View File

@ -34,8 +34,8 @@ func Colorize(s string, c Paint) string {
} }
// ANSIColorize colors a string. // ANSIColorize colors a string.
func ANSIColorize(s string, c int) string { func ANSIColorize(text string, color int) string {
return "\033[38;5;" + strconv.Itoa(c) + "m" + s + "\033[0m" return "\033[38;5;" + strconv.Itoa(color) + "m" + text + "\033[0m"
} }
// Highlight colorize bytes at given indices. // Highlight colorize bytes at given indices.

View File

@ -73,9 +73,9 @@ func (k *K9s) GetRefreshRate() int {
} }
// GetReadOnly returns the readonly setting. // GetReadOnly returns the readonly setting.
func (k *K9s) GetReadOnly() bool { func (k *K9s) IsReadOnly() bool {
readOnly := k.ReadOnly readOnly := k.ReadOnly
if k.manualReadOnly != nil && *k.manualReadOnly { if k.manualReadOnly != nil {
readOnly = *k.manualReadOnly readOnly = *k.manualReadOnly
} }
return readOnly return readOnly

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"

View File

@ -80,7 +80,7 @@ var (
) )
// Render returns a log line as string. // Render returns a log line as string.
func (l *LogItem) Render(c int, showTime bool) []byte { func (l *LogItem) Render(paint int, showTime bool) []byte {
bb := make([]byte, 0, 200) bb := make([]byte, 0, 200)
if showTime { if showTime {
t := l.Timestamp t := l.Timestamp
@ -92,11 +92,11 @@ func (l *LogItem) Render(c int, showTime bool) []byte {
} }
if l.Pod != "" { if l.Pod != "" {
bb = append(bb, color.ANSIColorize(l.Pod, c)...) bb = append(bb, color.ANSIColorize(l.Pod, paint)...)
bb = append(bb, ':') bb = append(bb, ':')
} }
if !l.SingleContainer && l.Container != "" { if !l.SingleContainer && l.Container != "" {
bb = append(bb, color.ANSIColorize(l.Container, c)...) bb = append(bb, color.ANSIColorize(l.Container, paint)...)
bb = append(bb, ' ') bb = append(bb, ' ')
} }
@ -122,10 +122,20 @@ func colorFor(n string) int {
type LogItems []*LogItem type LogItems []*LogItem
// Lines returns a collection of log lines. // Lines returns a collection of log lines.
func (l LogItems) Lines() []string { func (l LogItems) Lines(showTime bool) [][]byte {
ll := make([][]byte, len(l))
for i, item := range l {
ll[i] = item.Render(0, showTime)
}
return ll
}
// StrLines returns a collection of log lines.
func (l LogItems) StrLines(showTime bool) []string {
ll := make([]string, len(l)) ll := make([]string, len(l))
for i, item := range l { for i, item := range l {
ll[i] = string(item.Render(0, false)) ll[i] = string(item.Render(0, showTime))
} }
return ll return ll
@ -154,15 +164,15 @@ func (l LogItems) DumpDebug(m string) {
} }
// Filter filters out log items based on given filter. // Filter filters out log items based on given filter.
func (l LogItems) Filter(q string) ([]int, [][]int, error) { func (l LogItems) Filter(q string, showTime bool) ([]int, [][]int, error) {
if q == "" { if q == "" {
return nil, nil, nil return nil, nil, nil
} }
if IsFuzzySelector(q) { if IsFuzzySelector(q) {
mm, ii := l.fuzzyFilter(strings.TrimSpace(q[2:])) mm, ii := l.fuzzyFilter(strings.TrimSpace(q[2:]), showTime)
return mm, ii, nil return mm, ii, nil
} }
matches, indices, err := l.filterLogs(q) matches, indices, err := l.filterLogs(q, showTime)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("Logs filter failed") log.Error().Err(err).Msgf("Logs filter failed")
return nil, nil, err return nil, nil, err
@ -172,10 +182,10 @@ func (l LogItems) Filter(q string) ([]int, [][]int, error) {
var fuzzyRx = regexp.MustCompile(`\A\-f`) var fuzzyRx = regexp.MustCompile(`\A\-f`)
func (l LogItems) fuzzyFilter(q string) ([]int, [][]int) { func (l LogItems) fuzzyFilter(q string, showTime bool) ([]int, [][]int) {
q = strings.TrimSpace(q) q = strings.TrimSpace(q)
matches, indices := make([]int, 0, len(l)), make([][]int, 0, 10) matches, indices := make([]int, 0, len(l)), make([][]int, 0, 10)
mm := fuzzy.Find(q, l.Lines()) mm := fuzzy.Find(q, l.StrLines(showTime))
for _, m := range mm { for _, m := range mm {
matches = append(matches, m.Index) matches = append(matches, m.Index)
indices = append(indices, m.MatchedIndexes) indices = append(indices, m.MatchedIndexes)
@ -184,14 +194,14 @@ func (l LogItems) fuzzyFilter(q string) ([]int, [][]int) {
return matches, indices return matches, indices
} }
func (l LogItems) filterLogs(q string) ([]int, [][]int, error) { func (l LogItems) filterLogs(q string, showTime bool) ([]int, [][]int, error) {
rx, err := regexp.Compile(`(?i)` + q) rx, err := regexp.Compile(`(?i)` + q)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
matches, indices := make([]int, 0, len(l)), make([][]int, 0, 10) matches, indices := make([]int, 0, len(l)), make([][]int, 0, 10)
for i, line := range l.Lines() { for i, line := range l.Lines(showTime) {
if locs := rx.FindStringIndex(line); locs != nil { if locs := rx.FindIndex(line); locs != nil {
matches = append(matches, i) matches = append(matches, i)
ii := make([]int, 0, 10) ii := make([]int, 0, 10)
for i := 0; i < len(locs); i += 2 { for i := 0; i < len(locs); i += 2 {

View File

@ -70,7 +70,7 @@ func TestLogItemsFilter(t *testing.T) {
for _, i := range ii { for _, i := range ii {
i.Pod, i.Container = n, u.opts.Container i.Pod, i.Container = n, u.opts.Container
} }
res, _, err := ii.Filter(u.q) res, _, err := ii.Filter(u.q, false)
assert.Equal(t, u.err, err) assert.Equal(t, u.err, err)
if err == nil { if err == nil {
assert.Equal(t, u.e, res) assert.Equal(t, u.e, res)

View File

@ -205,7 +205,6 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
var tailed bool var tailed bool
for _, co := range po.Spec.InitContainers { for _, co := range po.Spec.InitContainers {
log.Debug().Msgf("Tailing INIT-CO %q", co.Name)
opts.Container = co.Name opts.Container = co.Name
if err := tailLogs(ctx, p, c, opts); err != nil { if err := tailLogs(ctx, p, c, opts); err != nil {
return err return err
@ -213,7 +212,6 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
tailed = true tailed = true
} }
for _, co := range po.Spec.Containers { for _, co := range po.Spec.Containers {
log.Debug().Msgf("Tailing CO %q", co.Name)
opts.Container = co.Name opts.Container = co.Name
if err := tailLogs(ctx, p, c, opts); err != nil { if err := tailLogs(ctx, p, c, opts); err != nil {
return err return err
@ -221,7 +219,6 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
tailed = true tailed = true
} }
for _, co := range po.Spec.EphemeralContainers { for _, co := range po.Spec.EphemeralContainers {
log.Debug().Msgf("Tailing EPH-CO %q", co.Name)
opts.Container = co.Name opts.Container = co.Name
if err := tailLogs(ctx, p, c, opts); err != nil { if err := tailLogs(ctx, p, c, opts); err != nil {
return err return err
@ -236,7 +233,7 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
return nil return nil
} }
// ScanSA scans for serviceaccount refs. // ScanSA scans for ServiceAccount refs.
func (p *Pod) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) { func (p *Pod) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn) ns, n := client.Namespaced(fqn)
oo, err := p.Factory.List(p.GVR(), ns, wait, labels.Everything()) oo, err := p.Factory.List(p.GVR(), ns, wait, labels.Everything())
@ -334,12 +331,10 @@ func tailLogs(ctx context.Context, logger Logger, c LogChan, opts LogOptions) er
) )
done: done:
for r := 0; r < logRetryCount; r++ { for r := 0; r < logRetryCount; r++ {
log.Debug().Msgf("Retry logs %d", r)
req, err = logger.Logs(opts.Path, opts.ToPodLogOptions()) req, err = logger.Logs(opts.Path, opts.ToPodLogOptions())
if err == nil { if err == nil {
// This call will block if nothing is in the stream!! // This call will block if nothing is in the stream!!
if stream, err = req.Stream(ctx); err == nil { if stream, err = req.Stream(ctx); err == nil {
log.Debug().Msgf("Reading logs")
go readLogs(stream, c, opts) go readLogs(stream, c, opts)
break break
} else { } else {
@ -426,18 +421,18 @@ func extractFQN(o runtime.Object) string {
u, ok := o.(*unstructured.Unstructured) u, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
log.Error().Err(fmt.Errorf("expecting unstructured but got %T", o)) log.Error().Err(fmt.Errorf("expecting unstructured but got %T", o))
return "na" return client.NA
} }
m, ok := u.Object["metadata"].(map[string]interface{}) m, ok := u.Object["metadata"].(map[string]interface{})
if !ok { if !ok {
log.Error().Err(fmt.Errorf("expecting interface map for metadata but got %T", u.Object["metadata"])) log.Error().Err(fmt.Errorf("expecting interface map for metadata but got %T", u.Object["metadata"]))
return "na" return client.NA
} }
n, ok := m["name"].(string) n, ok := m["name"].(string)
if !ok { if !ok {
log.Error().Err(fmt.Errorf("expecting interface map for name but got %T", m["name"])) log.Error().Err(fmt.Errorf("expecting interface map for name but got %T", m["name"]))
return "na" return client.NA
} }
ns, ok := m["namespace"].(string) ns, ok := m["namespace"].(string)

View File

@ -174,7 +174,7 @@ func parseRules(ns, binding string, rules []rbacv1.PolicyRule) render.Policies {
if nres[0] != '/' { if nres[0] != '/' {
nres = "/" + nres nres = "/" + nres
} }
pp = pp.Upsert(render.NewPolicyRes(ns, binding, nres, "n/a", rule.Verbs)) pp = pp.Upsert(render.NewPolicyRes(ns, binding, nres, client.NA, rule.Verbs))
} }
} }

View File

@ -45,7 +45,7 @@ func NewCluster(f dao.Factory) *Cluster {
func (c *Cluster) Version() string { func (c *Cluster) Version() string {
info, err := c.factory.Client().ServerVersion() info, err := c.factory.Client().ServerVersion()
if err != nil { if err != nil {
return NA return client.NA
} }
return info.GitVersion return info.GitVersion
@ -55,7 +55,7 @@ func (c *Cluster) Version() string {
func (c *Cluster) ContextName() string { func (c *Cluster) ContextName() string {
n, err := c.factory.Client().Config().CurrentContextName() n, err := c.factory.Client().Config().CurrentContextName()
if err != nil { if err != nil {
return NA return client.NA
} }
return n return n
} }
@ -64,7 +64,7 @@ func (c *Cluster) ContextName() string {
func (c *Cluster) ClusterName() string { func (c *Cluster) ClusterName() string {
n, err := c.factory.Client().Config().CurrentClusterName() n, err := c.factory.Client().Config().CurrentClusterName()
if err != nil { if err != nil {
return NA return client.NA
} }
return n return n
} }
@ -73,7 +73,7 @@ func (c *Cluster) ClusterName() string {
func (c *Cluster) UserName() string { func (c *Cluster) UserName() string {
n, err := c.factory.Client().Config().CurrentUserName() n, err := c.factory.Client().Config().CurrentUserName()
if err != nil { if err != nil {
return NA return client.NA
} }
return n return n
} }

View File

@ -16,9 +16,6 @@ type ClusterInfoListener interface {
ClusterInfoUpdated(ClusterMeta) ClusterInfoUpdated(ClusterMeta)
} }
// NA indicates data is missing at this time.
const NA = "n/a"
// ClusterMeta represents cluster meta data. // ClusterMeta represents cluster meta data.
type ClusterMeta struct { type ClusterMeta struct {
Context, Cluster string Context, Cluster string
@ -30,11 +27,11 @@ type ClusterMeta struct {
// NewClusterMeta returns a new instance. // NewClusterMeta returns a new instance.
func NewClusterMeta() ClusterMeta { func NewClusterMeta() ClusterMeta {
return ClusterMeta{ return ClusterMeta{
Context: NA, Context: client.NA,
Cluster: NA, Cluster: client.NA,
User: NA, User: client.NA,
K9sVer: NA, K9sVer: client.NA,
K8sVer: NA, K8sVer: client.NA,
Cpu: 0, Cpu: 0,
Mem: 0, Mem: 0,
Ephemeral: 0, Ephemeral: 0,

View File

@ -1,25 +1,37 @@
package model package model
const maxBuff = 10 import (
"context"
"time"
)
const ( const (
maxBuff = 10
keyEntryDelay = 300 * time.Millisecond
// CommandBuffer represents a command buffer. // CommandBuffer represents a command buffer.
CommandBuffer BufferKind = 1 << iota CommandBuffer BufferKind = 1 << iota
// FilterBuffer represents a filter buffer. // FilterBuffer represents a filter buffer.
FilterBuffer FilterBuffer
) )
// BufferKind indicates a buffer type type (
type BufferKind int8 // BufferKind indicates a buffer type
BufferKind int8
// BuffWatcher represents a command buffer listener. // BuffWatcher represents a command buffer listener.
type BuffWatcher interface { BuffWatcher interface {
// Changed indicates the buffer was changed. // BufferCompleted indicates input was accepted.
BufferChanged(s string) BufferCompleted(s string)
// Active indicates the buff activity changed. // BufferChanged indicates the buffer was changed.
BufferActive(state bool, kind BufferKind) BufferChanged(s string)
}
// BufferActive indicates the buff activity changed.
BufferActive(state bool, kind BufferKind)
}
)
// CmdBuff represents user command input. // CmdBuff represents user command input.
type CmdBuff struct { type CmdBuff struct {
@ -28,6 +40,7 @@ type CmdBuff struct {
hotKey rune hotKey rune
kind BufferKind kind BufferKind
active bool active bool
cancel context.CancelFunc
} }
// NewCmdBuff returns a new command buffer. // NewCmdBuff returns a new command buffer.
@ -64,13 +77,24 @@ func (c *CmdBuff) GetText() string {
// SetText initializes the buffer with a command. // SetText initializes the buffer with a command.
func (c *CmdBuff) SetText(cmd string) { func (c *CmdBuff) SetText(cmd string) {
c.buff = []rune(cmd) c.buff = []rune(cmd)
c.fireBufferChanged() c.fireBufferCompleted()
} }
// Add adds a new character to the buffer. // Add adds a new character to the buffer.
func (c *CmdBuff) Add(r rune) { func (c *CmdBuff) Add(r rune) {
c.buff = append(c.buff, r) c.buff = append(c.buff, r)
c.fireBufferChanged() c.fireBufferChanged()
if c.cancel != nil {
return
}
var ctx context.Context
ctx, c.cancel = context.WithTimeout(context.Background(), keyEntryDelay)
go func() {
<-ctx.Done()
c.fireBufferCompleted()
c.cancel = nil
}()
} }
// Delete removes the last character from the buffer. // Delete removes the last character from the buffer.
@ -80,13 +104,25 @@ func (c *CmdBuff) Delete() {
} }
c.buff = c.buff[:len(c.buff)-1] c.buff = c.buff[:len(c.buff)-1]
c.fireBufferChanged() c.fireBufferChanged()
if c.cancel != nil {
return
}
var ctx context.Context
ctx, c.cancel = context.WithTimeout(context.Background(), 800*time.Millisecond)
go func() {
<-ctx.Done()
c.fireBufferCompleted()
c.cancel = nil
}()
} }
// ClearText clears out command buffer. // ClearText clears out command buffer.
func (c *CmdBuff) ClearText(fire bool) { func (c *CmdBuff) ClearText(fire bool) {
c.buff = make([]rune, 0, maxBuff) c.buff = make([]rune, 0, maxBuff)
if fire { if fire {
c.fireBufferChanged() c.fireBufferCompleted()
} }
} }
@ -94,6 +130,7 @@ func (c *CmdBuff) ClearText(fire bool) {
func (c *CmdBuff) Reset() { func (c *CmdBuff) Reset() {
c.ClearText(true) c.ClearText(true)
c.SetActive(false) c.SetActive(false)
c.fireBufferCompleted()
} }
// Empty returns true if no cmd, false otherwise. // Empty returns true if no cmd, false otherwise.
@ -125,6 +162,12 @@ func (c *CmdBuff) RemoveListener(l BuffWatcher) {
c.listeners = append(c.listeners[:victim], c.listeners[victim+1:]...) c.listeners = append(c.listeners[:victim], c.listeners[victim+1:]...)
} }
func (c *CmdBuff) fireBufferCompleted() {
for _, l := range c.listeners {
l.BufferCompleted(c.GetText())
}
}
func (c *CmdBuff) fireBufferChanged() { func (c *CmdBuff) fireBufferChanged() {
for _, l := range c.listeners { for _, l := range c.listeners {
l.BufferChanged(c.GetText()) l.BufferChanged(c.GetText())

View File

@ -17,6 +17,10 @@ func (l *testListener) BufferChanged(s string) {
l.text = s l.text = s
} }
func (l *testListener) BufferCompleted(s string) {
l.text = s
}
func (l *testListener) BufferActive(s bool, _ model.BufferKind) { func (l *testListener) BufferActive(s bool, _ model.BufferKind) {
if s { if s {
l.act++ l.act++

View File

@ -79,6 +79,9 @@ func (m *mockSuggestionListener) BufferChanged(s string) {
m.buff++ m.buff++
} }
func (m *mockSuggestionListener) BufferCompleted(s string) {
}
func (m *mockSuggestionListener) BufferActive(state bool, kind model.BufferKind) { func (m *mockSuggestionListener) BufferActive(state bool, kind model.BufferKind) {
m.active = state m.active = state
} }

View File

@ -17,7 +17,7 @@ import (
// 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(dao.LogItems) LogChanged([][]byte)
// LogCleanred indicates logs are cleared. // LogCleanred indicates logs are cleared.
LogCleared() LogCleared()
@ -80,7 +80,7 @@ func (l *Log) SetLogOptions(opts dao.LogOptions) {
// Configure sets logger configuration. // Configure sets logger configuration.
func (l *Log) Configure(opts *config.Logger) { func (l *Log) Configure(opts *config.Logger) {
l.logOptions.Lines = int64(opts.BufferSize) l.logOptions.Lines = int64(opts.TailCount)
l.logOptions.SinceSeconds = opts.SinceSeconds l.logOptions.SinceSeconds = opts.SinceSeconds
} }
@ -113,7 +113,9 @@ func (l *Log) Clear() {
// Refresh refreshes the logs. // Refresh refreshes the logs.
func (l *Log) Refresh() { func (l *Log) Refresh() {
l.fireLogCleared() l.fireLogCleared()
l.fireLogChanged(l.lines) ll := make([][]byte, len(l.lines))
l.lines.Render(l.logOptions.ShowTimestamp, ll)
l.fireLogChanged(ll)
} }
// Restart restarts the logger. // Restart restarts the logger.
@ -149,7 +151,9 @@ func (l *Log) Set(items dao.LogItems) {
l.mx.Unlock() l.mx.Unlock()
l.fireLogCleared() l.fireLogCleared()
l.fireLogChanged(items) ll := make([][]byte, len(l.lines))
l.lines.Render(l.logOptions.ShowTimestamp, ll)
l.fireLogChanged(ll)
} }
// ClearFilter resets the log filter if any. // ClearFilter resets the log filter if any.
@ -161,7 +165,9 @@ func (l *Log) ClearFilter() {
l.mx.Unlock() l.mx.Unlock()
l.fireLogCleared() l.fireLogCleared()
l.fireLogChanged(l.lines) ll := make([][]byte, len(l.lines))
l.lines.Render(l.logOptions.ShowTimestamp, ll)
l.fireLogChanged(ll)
} }
// Filter filters the model using either fuzzy or regexp. // Filter filters the model using either fuzzy or regexp.
@ -169,6 +175,13 @@ func (l *Log) Filter(q string) {
l.mx.Lock() l.mx.Lock()
defer l.mx.Unlock() defer l.mx.Unlock()
if len(q) == 0 {
l.filter, l.filtering = "", false
l.fireLogCleared()
l.fireLogBuffChanged(l.lines)
return
}
l.filter = q l.filter = q
if l.filtering { if l.filtering {
return return
@ -308,45 +321,48 @@ func (l *Log) RemoveListener(listener LogsListener) {
} }
} }
func applyFilter(q string, lines dao.LogItems) (dao.LogItems, error) { func (l *Log) applyFilter(q string) ([][]byte, error) {
if q == "" { if q == "" {
return lines, nil return nil, nil
} }
matches, indices, err := lines.Filter(q) matches, indices, err := l.lines.Filter(q, l.logOptions.ShowTimestamp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// No filter! // No filter!
if matches == nil { if matches == nil {
return lines, nil ll := make([][]byte, len(l.lines))
l.lines.Render(l.logOptions.ShowTimestamp, ll)
return ll, nil
} }
// Blank filter // Blank filter
if len(matches) == 0 { if len(matches) == 0 {
return nil, nil return nil, nil
} }
filtered := make(dao.LogItems, 0, len(matches)) filtered := make([][]byte, 0, len(matches))
lines := l.lines.Lines(l.logOptions.ShowTimestamp)
for i, idx := range matches { for i, idx := range matches {
item := lines[idx].Clone() filtered = append(filtered, color.Highlight(lines[idx], indices[i], 209))
item.Bytes = color.Highlight(item.Bytes, indices[i], 209)
filtered = append(filtered, item)
} }
return filtered, nil return filtered, nil
} }
func (l *Log) fireLogBuffChanged(lines dao.LogItems) { func (l *Log) fireLogBuffChanged(lines dao.LogItems) {
filtered := lines ll := make([][]byte, len(l.lines))
if l.filter != "" { if l.filter == "" {
var err error l.lines.Render(l.logOptions.ShowTimestamp, ll)
filtered, err = applyFilter(l.filter, lines) } else {
ff, err := l.applyFilter(l.filter)
if err != nil { if err != nil {
l.fireLogError(err) l.fireLogError(err)
return return
} }
ll = ff
} }
if len(filtered) > 0 { if len(ll) > 0 {
l.fireLogChanged(filtered) l.fireLogChanged(ll)
} }
} }
@ -356,7 +372,7 @@ func (l *Log) fireLogError(err error) {
} }
} }
func (l *Log) fireLogChanged(lines dao.LogItems) { func (l *Log) fireLogChanged(lines [][]byte) {
for _, lis := range l.listeners { for _, lis := range l.listeners {
lis.LogChanged(lines) lis.LogChanged(lines)
} }

View File

@ -72,8 +72,8 @@ func newMockLogView() *mockLogView {
return &mockLogView{} return &mockLogView{}
} }
func (t *mockLogView) LogChanged(d dao.LogItems) { func (t *mockLogView) LogChanged(ll [][]byte) {
t.count += len(d.Lines()) t.count += len(ll)
} }
func (t *mockLogView) LogCleared() {} func (t *mockLogView) LogCleared() {}
func (t *mockLogView) LogFailed(err error) {} func (t *mockLogView) LogFailed(err error) {}

View File

@ -34,7 +34,7 @@ func TestLogFullBuffer(t *testing.T) {
assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 1, v.clearCalled)
assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, v.errCalled)
assert.Equal(t, data[4:], v.data) assert.Equal(t, data[4:].Lines(false), v.data)
} }
func TestLogFilter(t *testing.T) { func TestLogFilter(t *testing.T) {
@ -144,7 +144,7 @@ func TestLogBasic(t *testing.T) {
assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 1, 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.Lines(false), v.data)
} }
func TestLogAppend(t *testing.T) { func TestLogAppend(t *testing.T) {
@ -153,9 +153,11 @@ func TestLogAppend(t *testing.T) {
v := newTestView() v := newTestView()
m.AddListener(v) m.AddListener(v)
items := dao.LogItems{dao.NewLogItemFromString("blah blah")} items := dao.LogItems{
dao.NewLogItemFromString("blah blah"),
}
m.Set(items) m.Set(items)
assert.Equal(t, items, v.data) assert.Equal(t, items.Lines(false), v.data)
data := dao.LogItems{ data := dao.LogItems{
dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line1"),
@ -165,13 +167,13 @@ func TestLogAppend(t *testing.T) {
m.Append(d) m.Append(d)
} }
assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, items, v.data) assert.Equal(t, items.Lines(false), v.data)
m.Notify() m.Notify()
assert.Equal(t, 2, v.dataCalled) assert.Equal(t, 2, v.dataCalled)
assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 1, v.clearCalled)
assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, v.errCalled)
assert.Equal(t, append(items, data...), v.data) assert.Equal(t, append(items, data...).Lines(false), v.data)
} }
func TestLogTimedout(t *testing.T) { func TestLogTimedout(t *testing.T) {
@ -196,7 +198,7 @@ func TestLogTimedout(t *testing.T) {
assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 1, v.clearCalled)
assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, v.errCalled)
const e = "\x1b[38;5;209ml\x1b[0m\x1b[38;5;209mi\x1b[0m\x1b[38;5;209mn\x1b[0m\x1b[38;5;209me\x1b[0m\x1b[38;5;209m1\x1b[0m" const e = "\x1b[38;5;209ml\x1b[0m\x1b[38;5;209mi\x1b[0m\x1b[38;5;209mn\x1b[0m\x1b[38;5;209me\x1b[0m\x1b[38;5;209m1\x1b[0m"
assert.Equal(t, e, string(v.data[0].Bytes)) assert.Equal(t, e, string(v.data[0]))
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -213,7 +215,7 @@ func makeLogOpts(count int) dao.LogOptions {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
type testView struct { type testView struct {
data dao.LogItems data [][]byte
dataCalled int dataCalled int
clearCalled int clearCalled int
errCalled int errCalled int
@ -223,13 +225,13 @@ func newTestView() *testView {
return &testView{} return &testView{}
} }
func (t *testView) LogChanged(d dao.LogItems) { func (t *testView) LogChanged(ll [][]byte) {
t.data = d t.data = ll
t.dataCalled++ t.dataCalled++
} }
func (t *testView) LogCleared() { func (t *testView) LogCleared() {
t.clearCalled++ t.clearCalled++
t.data = dao.LogItems{} t.data = nil
} }
func (t *testView) LogFailed(err error) { func (t *testView) LogFailed(err error) {
fmt.Println("LogErr", err) fmt.Println("LogErr", err)

View File

@ -33,7 +33,7 @@ func TestTableReconcile(t *testing.T) {
err := ta.reconcile(ctx) err := ta.reconcile(ctx)
assert.Nil(t, err) assert.Nil(t, err)
data := ta.Peek() data := ta.Peek()
assert.Equal(t, 18, len(data.Header)) assert.Equal(t, 20, len(data.Header))
assert.Equal(t, 1, len(data.RowEvents)) assert.Equal(t, 1, len(data.RowEvents))
assert.Equal(t, client.NamespaceAll, data.Namespace) assert.Equal(t, client.NamespaceAll, data.Namespace)
} }
@ -106,7 +106,7 @@ func TestTableHydrate(t *testing.T) {
assert.Nil(t, hydrate("blee", oo, rr, render.Pod{})) assert.Nil(t, hydrate("blee", oo, rr, render.Pod{}))
assert.Equal(t, 1, len(rr)) assert.Equal(t, 1, len(rr))
assert.Equal(t, 18, len(rr[0].Fields)) assert.Equal(t, 20, len(rr[0].Fields))
} }
func TestTableGenericHydrate(t *testing.T) { func TestTableGenericHydrate(t *testing.T) {

View File

@ -33,7 +33,7 @@ func TestTableRefresh(t *testing.T) {
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false) ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
ta.Refresh(ctx) ta.Refresh(ctx)
data := ta.Peek() data := ta.Peek()
assert.Equal(t, 18, len(data.Header)) assert.Equal(t, 20, len(data.Header))
assert.Equal(t, 1, len(data.RowEvents)) assert.Equal(t, 1, len(data.RowEvents))
assert.Equal(t, client.NamespaceAll, data.Namespace) assert.Equal(t, client.NamespaceAll, data.Namespace)
assert.Equal(t, 1, l.count) assert.Equal(t, 1, l.count)

View File

@ -76,6 +76,8 @@ func (Container) Header(ns string) Header {
HeaderColumn{Name: "INIT"}, HeaderColumn{Name: "INIT"},
HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight}, HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight},
HeaderColumn{Name: "PROBES(L:R)"}, HeaderColumn{Name: "PROBES(L:R)"},
HeaderColumn{Name: "CPU(R:L)", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "MEM(R:L)", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true}, HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true},
@ -95,7 +97,7 @@ func (c Container) Render(o interface{}, name string, r *Row) error {
return fmt.Errorf("Expected ContainerRes, but got %T", o) return fmt.Errorf("Expected ContainerRes, but got %T", o)
} }
cur, perc, limit := gatherMetrics(co.Container, co.MX) cur, perc, limit, res := gatherMetrics(co.Container, co.MX)
ready, state, restarts := "false", MissingValue, "0" ready, state, restarts := "false", MissingValue, "0"
if co.Status != nil { if co.Status != nil {
ready, state, restarts = boolToStr(co.Status.Ready), ToContainerState(co.Status.State), strconv.Itoa(int(co.Status.RestartCount)) ready, state, restarts = boolToStr(co.Status.Ready), ToContainerState(co.Status.State), strconv.Itoa(int(co.Status.RestartCount))
@ -111,6 +113,8 @@ func (c Container) Render(o interface{}, name string, r *Row) error {
boolToStr(co.IsInit), boolToStr(co.IsInit),
restarts, restarts,
probe(co.Container.LivenessProbe) + ":" + probe(co.Container.ReadinessProbe), probe(co.Container.LivenessProbe) + ":" + probe(co.Container.ReadinessProbe),
ToResourcesMc(res),
ToResourcesMi(res),
cur.cpu, cur.cpu,
cur.mem, cur.mem,
perc.cpu, perc.cpu,
@ -140,8 +144,24 @@ func (Container) diagnose(state, ready string) error {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, p, l metric) { func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, p, l metric, r resources) {
c, p, l = noMetric(), noMetric(), noMetric() c, p, l = noMetric(), noMetric(), noMetric()
r = make(resources, 4)
rcpu, rmem := containerResources(*co)
lcpu, lmem := containerLimits(*co)
if rcpu != nil {
r[requestCPU] = rcpu
}
if rmem != nil {
r[requestMEM] = rmem
}
if lcpu != nil {
r[limitCPU] = lcpu
}
if lmem != nil {
r[limitMEM] = lmem
}
if mx == nil { if mx == nil {
return return
} }
@ -149,11 +169,10 @@ func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, p, l met
cpu := mx.Usage.Cpu().MilliValue() cpu := mx.Usage.Cpu().MilliValue()
mem := client.ToMB(mx.Usage.Memory().Value()) mem := client.ToMB(mx.Usage.Memory().Value())
c = metric{ c = metric{
cpu: ToMillicore(cpu), cpu: ToMc(cpu),
mem: ToMi(mem), mem: ToMi(mem),
} }
rcpu, rmem := containerResources(*co)
if rcpu != nil { if rcpu != nil {
p.cpu = client.ToPercentageStr(cpu, rcpu.MilliValue()) p.cpu = client.ToPercentageStr(cpu, rcpu.MilliValue())
} }
@ -161,7 +180,6 @@ func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, p, l met
p.mem = client.ToPercentageStr(mem, client.ToMB(rmem.Value())) p.mem = client.ToPercentageStr(mem, client.ToMB(rmem.Value()))
} }
lcpu, lmem := containerLimits(*co)
if lcpu != nil { if lcpu != nil {
l.cpu = client.ToPercentageStr(cpu, lcpu.MilliValue()) l.cpu = client.ToPercentageStr(cpu, lcpu.MilliValue())
} }

View File

@ -35,8 +35,10 @@ func TestContainer(t *testing.T) {
"false", "false",
"0", "0",
"off:off", "off:off",
"10", "20m/20m",
"20", "100Mi/100Mi",
"10m",
"20Mi",
"50", "50",
"20", "20",
"50", "50",

View File

@ -57,13 +57,13 @@ func extractMetaField(m map[string]interface{}, field string) string {
f, ok := m[field] f, ok := m[field]
if !ok { if !ok {
log.Error().Err(fmt.Errorf("failed to extract field from meta %s", field)) log.Error().Err(fmt.Errorf("failed to extract field from meta %s", field))
return "n/a" return NAValue
} }
fs, ok := f.(string) fs, ok := f.(string)
if !ok { if !ok {
log.Error().Err(fmt.Errorf("failed to extract string from field %s", field)) log.Error().Err(fmt.Errorf("failed to extract string from field %s", field))
return "n/a" return NAValue
} }
return fs return fs

View File

@ -7,6 +7,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/tview" "github.com/derailed/tview"
runewidth "github.com/mattn/go-runewidth" runewidth "github.com/mattn/go-runewidth"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -63,13 +64,6 @@ func Happy(ns string, h Header, r Row) bool {
return strings.TrimSpace(r.Fields[validCol]) == "" return strings.TrimSpace(r.Fields[validCol]) == ""
} }
// const megaByte = 1024 * 1024
// // ToMB converts bytes to megabytes.
// func ToMB(v int64) float64 {
// return float64(v) / megaByte
// }
func asStatus(err error) string { func asStatus(err error) string {
if err == nil { if err == nil {
return "" return ""
@ -257,14 +251,64 @@ func mapToIfc(m interface{}) (s string) {
return return
} }
// ToMillicore shows cpu reading for human. // ToResourcesMi prints out request:limit mem resources.
func ToMillicore(v int64) string { func ToResourcesMi(res resources) string {
return strconv.Itoa(int(v)) var v1, v2 int64
if v, ok := res[requestMEM]; ok && v != nil {
v1 = v.MilliValue()
}
if v, ok := res[limitMEM]; ok && v != nil {
v2 = v.MilliValue()
}
if v1 == 0 && v2 == 0 {
return NAValue
}
return bytesToMb(v1) + ":" + bytesToMb(v2)
} }
// ToMi shows mem reading for human. func toMc(v int64) string {
if v == 0 {
return NAValue
}
p := message.NewPrinter(language.English)
return p.Sprintf("%dm", v)
}
func bytesToMb(v int64) string {
if v == 0 {
return NAValue
}
p := message.NewPrinter(language.English)
return p.Sprintf("%dMi", v/(client.MegaByte*1_000))
}
// ToResourcesMc prints out request:limit cpu resources.
func ToResourcesMc(res resources) string {
var v1, v2 int64
if v, ok := res[requestCPU]; ok && v != nil {
v1 = v.MilliValue()
}
if v, ok := res[limitCPU]; ok && v != nil {
v2 = v.MilliValue()
}
if v1 == 0 && v2 == 0 {
return NAValue
}
return toMc(v1) + ":" + toMc(v2)
}
// ToMc returns a the millicore unit.
func ToMc(v int64) string {
p := message.NewPrinter(language.English)
return p.Sprintf("%dm", v)
}
// ToMi returns the megabytes unit.
func ToMi(v int64) string { func ToMi(v int64) string {
return strconv.Itoa(int(v)) p := message.NewPrinter(language.English)
return p.Sprintf("%dMi", v)
} }
func boolPtrToStr(b *bool) string { func boolPtrToStr(b *bool) string {

View File

@ -361,18 +361,18 @@ func BenchmarkMapToStr(b *testing.B) {
} }
} }
func TestToMillicore(t *testing.T) { func TestToMc(t *testing.T) {
uu := []struct { uu := []struct {
v int64 v int64
e string e string
}{ }{
{0, "0"}, {0, "0m"},
{2, "2"}, {2, "2m"},
{1000, "1000"}, {1000, "1,000m"},
} }
for _, u := range uu { for _, u := range uu {
assert.Equal(t, u.e, ToMillicore(u.v)) assert.Equal(t, u.e, ToMc(u.v))
} }
} }
@ -381,9 +381,9 @@ func TestToMi(t *testing.T) {
v int64 v int64
e string e string
}{ }{
{0, "0"}, {0, "0Mi"},
{2, "2"}, {2, "2Mi"},
{1000, "1000"}, {1000, "1,000Mi"},
} }
for _, u := range uu { for _, u := range uu {

View File

@ -159,13 +159,13 @@ func gatherNodeMX(no *v1.Node, mx *mv1beta1.NodeMetrics) (c metric, a metric, p
cpu, mem := mx.Usage.Cpu().MilliValue(), client.ToMB(mx.Usage.Memory().Value()) cpu, mem := mx.Usage.Cpu().MilliValue(), client.ToMB(mx.Usage.Memory().Value())
c = metric{ c = metric{
cpu: ToMillicore(cpu), cpu: ToMc(cpu),
mem: ToMi(mem), mem: ToMi(mem),
} }
acpu, amem := no.Status.Allocatable.Cpu().MilliValue(), client.ToMB(no.Status.Allocatable.Memory().Value()) acpu, amem := no.Status.Allocatable.Cpu().MilliValue(), client.ToMB(no.Status.Allocatable.Memory().Value())
a = metric{ a = metric{
cpu: ToMillicore(acpu), cpu: ToMc(acpu),
mem: ToMi(amem), mem: ToMi(amem),
} }

View File

@ -21,7 +21,7 @@ func TestNodeRender(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "minikube", r.ID) assert.Equal(t, "minikube", r.ID)
e := render.Fields{"minikube", "Ready", "master", "v1.15.2", "4.15.0", "192.168.64.107", "<none>", "0", "10", "10", "0", "0", "4000", "7874"} e := render.Fields{"minikube", "Ready", "master", "v1.15.2", "4.15.0", "192.168.64.107", "<none>", "0", "10m", "10Mi", "0", "0", "4,000m", "7,874Mi"}
assert.Equal(t, e, r.Fields[:14]) assert.Equal(t, e, r.Fields[:14])
} }

View File

@ -13,5 +13,5 @@ func TestPodDisruptionBudgetRender(t *testing.T) {
c.Render(load(t, "pdb"), "", &r) c.Render(load(t, "pdb"), "", &r)
assert.Equal(t, "default/fred", r.ID) assert.Equal(t, "default/fred", r.ID)
assert.Equal(t, render.Fields{"default", "fred", "2", "n/a", "0", "0", "2", "0"}, r.Fields[:8]) assert.Equal(t, render.Fields{"default", "fred", "2", render.NAValue, "0", "0", "2", "0"}, r.Fields[:8])
} }

View File

@ -63,6 +63,8 @@ func (Pod) Header(ns string) Header {
HeaderColumn{Name: "READY"}, HeaderColumn{Name: "READY"},
HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight}, HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight},
HeaderColumn{Name: "STATUS"}, HeaderColumn{Name: "STATUS"},
HeaderColumn{Name: "CPU(R:L)", Align: tview.AlignRight, MX: true, Wide: true},
HeaderColumn{Name: "MEM(R:L)", Align: tview.AlignRight, MX: true, Wide: true},
HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true}, HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true},
@ -92,7 +94,7 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
ss := po.Status.ContainerStatuses ss := po.Status.ContainerStatuses
cr, _, rc := p.Statuses(ss) cr, _, rc := p.Statuses(ss)
c, perc := p.gatherPodMX(&po, pwm.MX) c, perc, res := p.gatherPodMX(&po, pwm.MX)
phase := p.Phase(&po) phase := p.Phase(&po)
r.ID = client.MetaFQN(po.ObjectMeta) r.ID = client.MetaFQN(po.ObjectMeta)
r.Fields = Fields{ r.Fields = Fields{
@ -102,6 +104,8 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
strconv.Itoa(cr) + "/" + strconv.Itoa(len(ss)), strconv.Itoa(cr) + "/" + strconv.Itoa(len(ss)),
strconv.Itoa(rc), strconv.Itoa(rc),
phase, phase,
ToResourcesMc(res),
ToResourcesMi(res),
c.cpu, c.cpu,
c.mem, c.mem,
perc.cpu, perc.cpu,
@ -149,7 +153,19 @@ func (p *PodWithMetrics) DeepCopyObject() runtime.Object {
return p return p
} }
func (*Pod) gatherPodMX(pod *v1.Pod, mx *mv1beta1.PodMetrics) (c, p metric) { const (
requestCPU qualifiedResource = "rcpu"
requestMEM = "rmem"
limitCPU = "lcpu"
limitMEM = "lmem"
)
type (
qualifiedResource string
resources map[qualifiedResource]*resource.Quantity
)
func (*Pod) gatherPodMX(pod *v1.Pod, mx *mv1beta1.PodMetrics) (c, p metric, r resources) {
c, p = noMetric(), noMetric() c, p = noMetric(), noMetric()
if mx == nil { if mx == nil {
return return
@ -161,12 +177,15 @@ func (*Pod) gatherPodMX(pod *v1.Pod, mx *mv1beta1.PodMetrics) (c, p metric) {
} }
cpu, mem := currentRes(mx) cpu, mem := currentRes(mx)
c = metric{ c = metric{
cpu: ToMillicore(cpu.MilliValue()), cpu: ToMc(cpu.MilliValue()),
mem: ToMi(client.ToMB(mem.Value())), mem: ToMi(client.ToMB(mem.Value())),
} }
rc, rm := resourceRequests(pod.Spec.Containers) rc, rm := podRequests(pod.Spec)
lc, lm := resourceLimits(pod.Spec.Containers) lc, lm := podLimits(pod.Spec)
r = make(resources, 4)
r[requestCPU], r[requestMEM] = rc, rm
r[limitCPU], r[limitMEM] = lc, lm
p = metric{ p = metric{
cpu: client.ToPercentageStr(cpu.MilliValue(), rc.MilliValue()), cpu: client.ToPercentageStr(cpu.MilliValue(), rc.MilliValue()),
mem: client.ToPercentageStr(client.ToMB(mem.Value()), client.ToMB(rm.Value())), mem: client.ToPercentageStr(client.ToMB(mem.Value()), client.ToMB(rm.Value())),
@ -197,7 +216,8 @@ func containerLimits(co v1.Container) (cpu, mem *resource.Quantity) {
return limit.Cpu(), limit.Memory() return limit.Cpu(), limit.Memory()
} }
func resourceLimits(cc []v1.Container) (cpu, mem resource.Quantity) { func resourceLimits(cc []v1.Container) (cpu, mem *resource.Quantity) {
cpu, mem = new(resource.Quantity), new(resource.Quantity)
for _, co := range cc { for _, co := range cc {
limit := co.Resources.Limits limit := co.Resources.Limits
if len(limit) == 0 { if len(limit) == 0 {
@ -215,7 +235,28 @@ func resourceLimits(cc []v1.Container) (cpu, mem resource.Quantity) {
return return
} }
func resourceRequests(cc []v1.Container) (cpu, mem resource.Quantity) { func podLimits(spec v1.PodSpec) (*resource.Quantity, *resource.Quantity) {
cc, cm := resourceLimits(spec.Containers)
ic, im := resourceLimits(spec.InitContainers)
cc.Add(*ic)
cm.Add(*im)
return cc, cm
}
func podRequests(spec v1.PodSpec) (*resource.Quantity, *resource.Quantity) {
cc, cm := resourceRequests(spec.Containers)
ic, im := resourceRequests(spec.InitContainers)
cc.Add(*ic)
cm.Add(*im)
return cc, cm
}
func resourceRequests(cc []v1.Container) (cpu, mem *resource.Quantity) {
cpu, mem = new(resource.Quantity), new(resource.Quantity)
for _, co := range cc { for _, co := range cc {
c, m := containerResources(co) c, m := containerResources(co)
if c == nil || m == nil { if c == nil || m == nil {
@ -230,6 +271,7 @@ func resourceRequests(cc []v1.Container) (cpu, mem resource.Quantity) {
mem.Add(*m) mem.Add(*m)
} }
} }
return return
} }

View File

@ -159,8 +159,8 @@ func TestPodRender(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "default/nginx", r.ID) assert.Equal(t, "default/nginx", r.ID)
e := render.Fields{"default", "nginx", "●", "1/1", "0", "Running", "10", "10", "10", "14", render.NAValue, "5", "172.17.0.6", "minikube", "BE"} e := render.Fields{"default", "nginx", "●", "1/1", "0", "Running", "100m/-", "70Mi/170Mi", "10m", "10Mi", "10", "14", render.NAValue, "5", "172.17.0.6", "minikube", "BE"}
assert.Equal(t, e, r.Fields[:15]) assert.Equal(t, e, r.Fields[:17])
} }
func BenchmarkPodRender(b *testing.B) { func BenchmarkPodRender(b *testing.B) {
@ -190,8 +190,8 @@ func TestPodInitRender(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "default/nginx", r.ID) assert.Equal(t, "default/nginx", r.ID)
e := render.Fields{"default", "nginx", "●", "1/1", "0", "Init:0/1", "10", "10", "10", "14", render.NAValue, "5", "172.17.0.6", "minikube", "BE"} e := render.Fields{"default", "nginx", "●", "1/1", "0", "Init:0/1", "200m/-", "140Mi/340Mi", "10m", "10Mi", "5", "7", render.NAValue, "2", "172.17.0.6", "minikube", "BE"}
assert.Equal(t, e, r.Fields[:15]) assert.Equal(t, e, r.Fields[:17])
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View File

@ -40,4 +40,7 @@ const (
// UnknownValue represents an unknown. // UnknownValue represents an unknown.
UnknownValue = "<unknown>" UnknownValue = "<unknown>"
// UnsetValue represent an unset value
UnsetValue = ""
) )

View File

@ -86,6 +86,9 @@ func (a *App) SetRunning(f bool) {
a.running = f a.running = f
} }
// BufferCompleted indicates input was accepted.
func (a *App) BufferCompleted(s string) {}
// BufferChanged indicates the buffer was changed. // BufferChanged indicates the buffer was changed.
func (a *App) BufferChanged(s string) {} func (a *App) BufferChanged(s string) {}

View File

@ -62,7 +62,7 @@ func Pad(s string, width int) string {
func toAgeHuman(s string) string { func toAgeHuman(s string) string {
d, err := time.ParseDuration(s) d, err := time.ParseDuration(s)
if err != nil { if err != nil {
return "n/a" return render.NAValue
} }
return duration.HumanDuration(d) return duration.HumanDuration(d)

View File

@ -203,6 +203,9 @@ func (p *Prompt) write(text, suggest string) {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Event Listener protocol... // Event Listener protocol...
// BufferCompleted indicates input was accepted.
func (p *Prompt) BufferCompleted(s string) {}
// BufferChanged indicates the buffer was changed. // BufferChanged indicates the buffer was changed.
func (p *Prompt) BufferChanged(s string) { func (p *Prompt) BufferChanged(s string) {
p.update(s) p.update(s)

View File

@ -14,7 +14,9 @@ func TestCmdNew(t *testing.T) {
model := model.NewFishBuff(':', model.CommandBuffer) model := model.NewFishBuff(':', model.CommandBuffer)
v.SetModel(model) v.SetModel(model)
model.AddListener(v) model.AddListener(v)
model.SetText("blee") for _, r := range "blee" {
model.Add(r)
}
assert.Equal(t, "\x00> [::b]blee\n", v.GetText(false)) assert.Equal(t, "\x00> [::b]blee\n", v.GetText(false))
} }

View File

@ -26,7 +26,7 @@ func NewAlias(gvr client.GVR) ResourceViewer {
a.GetTable().SetColorerFn(render.Alias{}.ColorerFunc()) a.GetTable().SetColorerFn(render.Alias{}.ColorerFunc())
a.GetTable().SetBorderFocusColor(tcell.ColorAliceBlue) a.GetTable().SetBorderFocusColor(tcell.ColorAliceBlue)
a.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorAliceBlue, tcell.AttrNone) a.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorAliceBlue, tcell.AttrNone)
a.SetBindKeysFn(a.bindKeys) a.AddBindKeysFn(a.bindKeys)
a.SetContextFn(a.aliasContext) a.SetContextFn(a.aliasContext)
return &a return &a

View File

@ -35,6 +35,7 @@ func TestAliasSearch(t *testing.T) {
v.App().Prompt().SendStrokes("blee") v.App().Prompt().SendStrokes("blee")
assert.Equal(t, 3, v.GetTable().GetColumnCount()) assert.Equal(t, 3, v.GetTable().GetColumnCount())
time.Sleep(1_000 * time.Millisecond)
assert.Equal(t, 2, v.GetTable().GetRowCount()) assert.Equal(t, 2, v.GetTable().GetRowCount())
} }
@ -62,6 +63,8 @@ type buffL struct {
func (b *buffL) BufferChanged(s string) { func (b *buffL) BufferChanged(s string) {
b.changed++ b.changed++
} }
func (b *buffL) BufferCompleted(s string) {}
func (b *buffL) BufferActive(state bool, kind model.BufferKind) { func (b *buffL) BufferActive(state bool, kind model.BufferKind) {
b.active++ b.active++
} }

View File

@ -139,13 +139,11 @@ func (a *App) layout(ctx context.Context, version string) {
func (a *App) initSignals() { func (a *App) initSignals() {
sig := make(chan os.Signal, 1) sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGABRT, syscall.SIGINT, syscall.SIGHUP, syscall.SIGQUIT) signal.Notify(sig, syscall.SIGHUP)
go func(sig chan os.Signal) { go func(sig chan os.Signal) {
s := <-sig <-sig
if s == syscall.SIGHUP { os.Exit(0)
os.Exit(0)
}
}(sig) }(sig)
} }

View File

@ -61,9 +61,9 @@ func (b *Browser) Init(ctx context.Context) error {
b.app.CmdBuff().Reset() b.app.CmdBuff().Reset()
} }
b.bindKeys() b.bindKeys(b.Actions())
if b.bindKeysFn != nil { for _, f := range b.bindKeysFn {
b.bindKeysFn(b.Actions()) f(b.Actions())
} }
b.accessor, err = dao.AccessorFor(b.app.factory, b.GVR()) b.accessor, err = dao.AccessorFor(b.app.factory, b.GVR())
if err != nil { if err != nil {
@ -104,8 +104,8 @@ func (b *Browser) suggestFilter() model.SuggestionFunc {
} }
} }
func (b *Browser) bindKeys() { func (b *Browser) bindKeys(aa ui.KeyActions) {
b.Actions().Add(ui.KeyActions{ aa.Add(ui.KeyActions{
tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", b.resetCmd, false), tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", b.resetCmd, false),
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", b.filterCmd, false), tcell.KeyEnter: ui.NewSharedKeyAction("Filter", b.filterCmd, false),
}) })
@ -132,7 +132,6 @@ func (b *Browser) Start() {
// Stop terminates browser updates. // Stop terminates browser updates.
func (b *Browser) Stop() { func (b *Browser) Stop() {
log.Debug().Msgf("BRO-STOP %v", b.GVR())
if b.cancelFn != nil { if b.cancelFn != nil {
b.cancelFn() b.cancelFn()
b.cancelFn = nil b.cancelFn = nil
@ -143,7 +142,10 @@ func (b *Browser) Stop() {
} }
// BufferChanged indicates the buffer was changed. // BufferChanged indicates the buffer was changed.
func (b *Browser) BufferChanged(s string) { func (b *Browser) BufferChanged(s string) {}
// BufferCompleted indicates input was accepted.
func (b *Browser) BufferCompleted(s string) {
if ui.IsLabelSelector(s) { if ui.IsLabelSelector(s) {
b.GetModel().SetLabelFilter(ui.TrimLabelSelector(s)) b.GetModel().SetLabelFilter(ui.TrimLabelSelector(s))
} else { } else {
@ -437,7 +439,7 @@ func (b *Browser) refreshActions() {
if b.app.ConOK() { if b.app.ConOK() {
b.namespaceActions(aa) b.namespaceActions(aa)
if !b.app.Config.K9s.GetReadOnly() { if !b.app.Config.K9s.IsReadOnly() {
if client.Can(b.meta.Verbs, "edit") { if client.Can(b.meta.Verbs, "edit") {
aa[ui.KeyE] = ui.NewKeyAction("Edit", b.editCmd, true) aa[ui.KeyE] = ui.NewKeyAction("Edit", b.editCmd, true)
} }
@ -454,11 +456,10 @@ func (b *Browser) refreshActions() {
pluginActions(b, aa) pluginActions(b, aa)
hotKeyActions(b, aa) hotKeyActions(b, aa)
b.Actions().Add(aa) for _, f := range b.bindKeysFn {
f(aa)
if b.bindKeysFn != nil {
b.bindKeysFn(b.Actions())
} }
b.Actions().Add(aa)
b.app.Menu().HydrateMenu(b.Hints()) b.app.Menu().HydrateMenu(b.Hints())
} }

View File

@ -20,7 +20,7 @@ func NewConfigMap(gvr client.GVR) ResourceViewer {
s := ConfigMap{ s := ConfigMap{
ResourceViewer: NewBrowser(gvr), ResourceViewer: NewBrowser(gvr),
} }
s.SetBindKeysFn(s.bindKeys) s.AddBindKeysFn(s.bindKeys)
return &s return &s
} }

View File

@ -30,7 +30,7 @@ func NewContainer(gvr client.GVR) ResourceViewer {
c.GetTable().SetEnterFn(c.viewLogs) c.GetTable().SetEnterFn(c.viewLogs)
c.GetTable().SetColorerFn(render.Container{}.ColorerFunc()) c.GetTable().SetColorerFn(render.Container{}.ColorerFunc())
c.GetTable().SetDecorateFn(c.decorateRows) c.GetTable().SetDecorateFn(c.decorateRows)
c.SetBindKeysFn(c.bindKeys) c.AddBindKeysFn(c.bindKeys)
c.GetTable().SetDecorateFn(c.portForwardIndicator) c.GetTable().SetDecorateFn(c.portForwardIndicator)
return &c return &c
@ -66,7 +66,7 @@ func (c *Container) bindDangerousKeys(aa ui.KeyActions) {
func (c *Container) bindKeys(aa ui.KeyActions) { func (c *Container) bindKeys(aa ui.KeyActions) {
aa.Delete(tcell.KeyCtrlSpace, ui.KeySpace) aa.Delete(tcell.KeyCtrlSpace, ui.KeySpace)
if !c.App().Config.K9s.GetReadOnly() { if !c.App().Config.K9s.IsReadOnly() {
c.bindDangerousKeys(aa) c.bindDangerousKeys(aa)
} }

View File

@ -23,7 +23,7 @@ func NewContext(gvr client.GVR) ResourceViewer {
} }
c.GetTable().SetEnterFn(c.useCtx) c.GetTable().SetEnterFn(c.useCtx)
c.GetTable().SetColorerFn(render.Context{}.ColorerFunc()) c.GetTable().SetColorerFn(render.Context{}.ColorerFunc())
c.SetBindKeysFn(c.bindKeys) c.AddBindKeysFn(c.bindKeys)
return &c return &c
} }

View File

@ -25,7 +25,7 @@ type CronJob struct {
// NewCronJob returns a new viewer. // NewCronJob returns a new viewer.
func NewCronJob(gvr client.GVR) ResourceViewer { func NewCronJob(gvr client.GVR) ResourceViewer {
c := CronJob{ResourceViewer: NewBrowser(gvr)} c := CronJob{ResourceViewer: NewBrowser(gvr)}
c.SetBindKeysFn(c.bindKeys) c.AddBindKeysFn(c.bindKeys)
c.GetTable().SetEnterFn(c.showJobs) c.GetTable().SetEnterFn(c.showJobs)
c.GetTable().SetColorerFn(render.CronJob{}.ColorerFunc()) c.GetTable().SetColorerFn(render.CronJob{}.ColorerFunc())

View File

@ -98,7 +98,10 @@ func (d *Details) TextFiltered(lines []string, matches fuzzy.Matches) {
} }
// BufferChanged indicates the buffer was changed. // BufferChanged indicates the buffer was changed.
func (d *Details) BufferChanged(s string) { func (d *Details) BufferChanged(s string) {}
// BufferCompleted indicates input was accepted.
func (d *Details) BufferCompleted(s string) {
d.model.Filter(s) d.model.Filter(s)
d.updateTitle() d.updateTitle()
} }

View File

@ -10,11 +10,20 @@ 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/config"
"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/derailed/k9s/internal/ui/dialog" "github.com/derailed/k9s/internal/ui/dialog"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/rs/zerolog/log" )
const (
kustomize = "kustomization"
kustomizeNoExt = "Kustomization"
kustomizeYAML = kustomize + extYAML
kustomizeYML = kustomize + extYML
extYAML = ".yaml"
extYML = ".yml"
) )
// Dir represents a command directory view. // Dir represents a command directory view.
@ -31,7 +40,7 @@ func NewDir(path string) ResourceViewer {
} }
d.GetTable().SetBorderFocusColor(tcell.ColorAliceBlue) d.GetTable().SetBorderFocusColor(tcell.ColorAliceBlue)
d.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorAliceBlue, tcell.AttrNone) d.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorAliceBlue, tcell.AttrNone)
d.SetBindKeysFn(d.bindKeys) d.AddBindKeysFn(d.bindKeys)
d.SetContextFn(d.dirContext) d.SetContextFn(d.dirContext)
d.GetTable().SetColorerFn(render.Dir{}.ColorerFunc()) d.GetTable().SetColorerFn(render.Dir{}.ColorerFunc())
@ -51,13 +60,21 @@ func (d *Dir) dirContext(ctx context.Context) context.Context {
return context.WithValue(ctx, internal.KeyPath, d.path) return context.WithValue(ctx, internal.KeyPath, d.path)
} }
func (d *Dir) bindDangerousKeys(aa ui.KeyActions) {
aa.Add(ui.KeyActions{
ui.KeyA: ui.NewKeyAction("Apply", d.applyCmd, true),
ui.KeyD: ui.NewKeyAction("Delete", d.delCmd, true),
ui.KeyE: ui.NewKeyAction("Edit", d.editCmd, true),
})
}
func (d *Dir) bindKeys(aa ui.KeyActions) { func (d *Dir) bindKeys(aa ui.KeyActions) {
aa.Delete(ui.KeyShiftA, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) aa.Delete(ui.KeyShiftA, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)
aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL, tcell.KeyCtrlD, tcell.KeyCtrlZ) aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL, tcell.KeyCtrlD, tcell.KeyCtrlZ)
if !d.App().Config.K9s.IsReadOnly() {
d.bindDangerousKeys(aa)
}
aa.Add(ui.KeyActions{ aa.Add(ui.KeyActions{
ui.KeyA: ui.NewKeyAction("Apply", d.applyCmd, true),
ui.KeyD: ui.NewKeyAction("Delete", d.delCmd, true),
ui.KeyE: ui.NewKeyAction("Edit", d.editCmd, true),
ui.KeyY: ui.NewKeyAction("YAML", d.viewCmd, true), ui.KeyY: ui.NewKeyAction("YAML", d.viewCmd, true),
tcell.KeyEnter: ui.NewKeyAction("Goto", d.gotoCmd, true), tcell.KeyEnter: ui.NewKeyAction("Goto", d.gotoCmd, true),
}) })
@ -98,7 +115,6 @@ func (d *Dir) editCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt return evt
} }
log.Debug().Msgf("Selected %q", sel)
if !isManifest(sel) { if !isManifest(sel) {
d.App().Flash().Errf("you must select a manifest") d.App().Flash().Errf("you must select a manifest")
return nil return nil
@ -136,18 +152,62 @@ func (d *Dir) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt return evt
} }
func isKustomized(sel string) bool {
if isManifest(sel) {
return false
}
ff, err := ioutil.ReadDir(sel)
if err != nil {
return false
}
kk := []string{kustomizeNoExt, kustomizeYAML, kustomizeYML}
for _, f := range ff {
if config.InList(kk, f.Name()) {
return true
}
}
return false
}
func containsDir(sel string) bool {
if isManifest(sel) {
return false
}
ff, err := ioutil.ReadDir(sel)
if err != nil {
return false
}
for _, f := range ff {
if f.IsDir() {
return true
}
}
return false
}
func (d *Dir) applyCmd(evt *tcell.EventKey) *tcell.EventKey { func (d *Dir) applyCmd(evt *tcell.EventKey) *tcell.EventKey {
sel := d.GetTable().GetSelectedItem() sel := d.GetTable().GetSelectedItem()
if sel == "" { if sel == "" {
return evt return evt
} }
opts := []string{"-f"}
if containsDir(sel) {
opts = append(opts, "-R")
}
if isKustomized(sel) {
opts = []string{"-k"}
}
d.Stop() d.Stop()
defer d.Start() defer d.Start()
{ {
args := make([]string, 0, 10) args := make([]string, 0, 10)
args = append(args, "apply") args = append(args, "apply")
args = append(args, "-f") args = append(args, opts...)
args = append(args, sel) args = append(args, sel)
res, err := runKu(d.App(), shellOpts{clear: false, args: args}) res, err := runKu(d.App(), shellOpts{clear: false, args: args})
if err != nil { if err != nil {

View File

@ -0,0 +1,44 @@
package view
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsManifest(t *testing.T) {
uu := map[string]struct {
file string
e bool
}{
"yaml": {file: "fred.yaml", e: true},
"yml": {file: "fred.yml", e: true},
"nope": {file: "fred.txt"},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, isManifest(u.file))
})
}
}
func TestIsKustomized(t *testing.T) {
uu := map[string]struct {
path string
e bool
}{
"toast": {path: "testdata/fred"},
"yaml": {path: "testdata/kmanifests", e: true},
"yml": {path: "testdata/k1manifests", e: true},
"noExt": {path: "testdata/k2manifests", e: true},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, isKustomized(u.path))
})
}
}

View File

@ -21,7 +21,7 @@ func NewDeploy(gvr client.GVR) ResourceViewer {
ResourceViewer: NewPortForwardExtender( ResourceViewer: NewPortForwardExtender(
NewRestartExtender( NewRestartExtender(
NewScaleExtender( NewScaleExtender(
NewSetImageExtender( NewImageExtender(
NewLogsExtender( NewLogsExtender(
NewBrowser(gvr), NewBrowser(gvr),
nil, nil,
@ -31,7 +31,7 @@ func NewDeploy(gvr client.GVR) ResourceViewer {
), ),
), ),
} }
d.SetBindKeysFn(d.bindKeys) d.AddBindKeysFn(d.bindKeys)
d.GetTable().SetEnterFn(d.showPods) d.GetTable().SetEnterFn(d.showPods)
d.GetTable().SetColorerFn(render.Deployment{}.ColorerFunc()) d.GetTable().SetColorerFn(render.Deployment{}.ColorerFunc())

View File

@ -1,6 +1,7 @@
package view_test package view_test
import ( import (
"fmt"
"testing" "testing"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
@ -13,5 +14,6 @@ func TestDeploy(t *testing.T) {
assert.Nil(t, v.Init(makeCtx())) assert.Nil(t, v.Init(makeCtx()))
assert.Equal(t, "Deployments", v.Name()) assert.Equal(t, "Deployments", v.Name())
assert.Equal(t, 13, len(v.Hints())) fmt.Println(v.Hints())
assert.Equal(t, 14, len(v.Hints()))
} }

View File

@ -17,13 +17,13 @@ func NewDaemonSet(gvr client.GVR) ResourceViewer {
d := DaemonSet{ d := DaemonSet{
ResourceViewer: NewPortForwardExtender( ResourceViewer: NewPortForwardExtender(
NewRestartExtender( NewRestartExtender(
NewSetImageExtender( NewImageExtender(
NewLogsExtender(NewBrowser(gvr), nil), NewLogsExtender(NewBrowser(gvr), nil),
), ),
), ),
), ),
} }
d.SetBindKeysFn(d.bindKeys) d.AddBindKeysFn(d.bindKeys)
d.GetTable().SetEnterFn(d.showPods) d.GetTable().SetEnterFn(d.showPods)
d.GetTable().SetColorerFn(render.DaemonSet{}.ColorerFunc()) d.GetTable().SetColorerFn(render.DaemonSet{}.ColorerFunc())

View File

@ -13,5 +13,5 @@ func TestDaemonSet(t *testing.T) {
assert.Nil(t, v.Init(makeCtx())) assert.Nil(t, v.Init(makeCtx()))
assert.Equal(t, "DaemonSets", v.Name()) assert.Equal(t, "DaemonSets", v.Name())
assert.Equal(t, 14, len(v.Hints())) assert.Equal(t, 15, len(v.Hints()))
} }

View File

@ -18,7 +18,7 @@ func NewEvent(gvr client.GVR) ResourceViewer {
ResourceViewer: NewBrowser(gvr), ResourceViewer: NewBrowser(gvr),
} }
e.GetTable().SetColorerFn(render.Event{}.ColorerFunc()) e.GetTable().SetColorerFn(render.Event{}.ColorerFunc())
e.SetBindKeysFn(e.bindKeys) e.AddBindKeysFn(e.bindKeys)
e.GetTable().SetSortCol(ageCol, true) e.GetTable().SetSortCol(ageCol, true)
return &e return &e

View File

@ -105,7 +105,7 @@ func execute(opts shellOpts) error {
}() }()
log.Debug().Msgf("Running command> %s %s", opts.binary, strings.Join(opts.args, " ")) log.Debug().Msgf("Running command> %s %s", opts.binary, strings.Join(opts.args, " "))
cmd := exec.Command(opts.binary, opts.args...) cmd := exec.CommandContext(ctx, opts.binary, opts.args...)
var err error var err error
if opts.background { if opts.background {
@ -162,7 +162,6 @@ func oneShoot(opts shellOpts) (string, error) {
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, buff, buff cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, buff, buff
_, _ = cmd.Stdout.Write([]byte(opts.banner)) _, _ = cmd.Stdout.Write([]byte(opts.banner))
err = cmd.Run() err = cmd.Run()
log.Debug().Msgf("RES %q", buff)
return strings.Trim(buff.String(), "\n"), err return strings.Trim(buff.String(), "\n"), err
} }

View File

@ -19,7 +19,7 @@ type Group struct {
func NewGroup(gvr client.GVR) ResourceViewer { func NewGroup(gvr client.GVR) ResourceViewer {
g := Group{ResourceViewer: NewBrowser(gvr)} g := Group{ResourceViewer: NewBrowser(gvr)}
g.GetTable().SetColorerFn(render.Subject{}.ColorerFunc()) g.GetTable().SetColorerFn(render.Subject{}.ColorerFunc())
g.SetBindKeysFn(g.bindKeys) g.AddBindKeysFn(g.bindKeys)
g.SetContextFn(g.subjectCtx) g.SetContextFn(g.subjectCtx)
return &g return &g

View File

@ -22,7 +22,7 @@ func NewHelm(gvr client.GVR) ResourceViewer {
c.GetTable().SetColorerFn(render.Helm{}.ColorerFunc()) c.GetTable().SetColorerFn(render.Helm{}.ColorerFunc())
c.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen) c.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen)
c.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorMediumSpringGreen, tcell.AttrNone) c.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorMediumSpringGreen, tcell.AttrNone)
c.SetBindKeysFn(c.bindKeys) c.AddBindKeysFn(c.bindKeys)
c.SetContextFn(c.chartContext) c.SetContextFn(c.chartContext)
return &c return &c

View File

@ -96,7 +96,7 @@ func TestK8sEnv(t *testing.T) {
assert.Equal(t, cl, env["CLUSTER"]) assert.Equal(t, cl, env["CLUSTER"])
assert.Equal(t, ctx, env["CONTEXT"]) assert.Equal(t, ctx, env["CONTEXT"])
assert.Equal(t, u, env["USER"]) assert.Equal(t, u, env["USER"])
assert.Equal(t, "n/a", env["GROUPS"]) assert.Equal(t, render.NAValue, env["GROUPS"])
assert.Equal(t, cfg, env["KUBECONFIG"]) assert.Equal(t, cfg, env["KUBECONFIG"])
} }
@ -123,7 +123,7 @@ func TestK9sEnv(t *testing.T) {
assert.Equal(t, cl, env["CLUSTER"]) assert.Equal(t, cl, env["CLUSTER"])
assert.Equal(t, ctx, env["CONTEXT"]) assert.Equal(t, ctx, env["CONTEXT"])
assert.Equal(t, u, env["USER"]) assert.Equal(t, u, env["USER"])
assert.Equal(t, "n/a", env["GROUPS"]) assert.Equal(t, render.NAValue, env["GROUPS"])
assert.Equal(t, cfg, env["KUBECONFIG"]) assert.Equal(t, cfg, env["KUBECONFIG"])
assert.Equal(t, "fred", env["NAMESPACE"]) assert.Equal(t, "fred", env["NAMESPACE"])
assert.Equal(t, "blee", env["NAME"]) assert.Equal(t, "blee", env["NAME"])

View File

@ -3,21 +3,17 @@ package view
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"strings"
) )
const setImageKey = "setImage" const imageKey = "setImage"
// SetImageExtender adds set image extensions
type SetImageExtender struct {
ResourceViewer
}
type imageFormSpec struct { type imageFormSpec struct {
name, dockerImage, newDockerImage string name, dockerImage, newDockerImage string
@ -44,20 +40,28 @@ func (m *imageFormSpec) imageSpec() dao.ImageSpec {
return ret return ret
} }
func NewSetImageExtender(r ResourceViewer) ResourceViewer { // ImageExtender provides for overriding container images.
s := SetImageExtender{ResourceViewer: r} type ImageExtender struct {
s.bindKeys(s.Actions()) ResourceViewer
}
func NewImageExtender(r ResourceViewer) ResourceViewer {
s := ImageExtender{ResourceViewer: r}
s.AddBindKeysFn(s.bindKeys)
return &s return &s
} }
func (s *SetImageExtender) bindKeys(aa ui.KeyActions) { func (s *ImageExtender) bindKeys(aa ui.KeyActions) {
if s.App().Config.K9s.IsReadOnly() {
return
}
aa.Add(ui.KeyActions{ aa.Add(ui.KeyActions{
ui.KeyI: ui.NewKeyAction("SetImage", s.setImageCmd, true), ui.KeyI: ui.NewKeyAction("Set Image", s.setImageCmd, false),
}) })
} }
func (s *SetImageExtender) setImageCmd(evt *tcell.EventKey) *tcell.EventKey { func (s *ImageExtender) setImageCmd(evt *tcell.EventKey) *tcell.EventKey {
path := s.GetTable().GetSelectedItem() path := s.GetTable().GetSelectedItem()
if path == "" { if path == "" {
return nil return nil
@ -65,63 +69,58 @@ func (s *SetImageExtender) setImageCmd(evt *tcell.EventKey) *tcell.EventKey {
s.Stop() s.Stop()
defer s.Start() defer s.Start()
s.showSetImageDialog(path) s.showImageDialog(path)
return nil return nil
} }
func (s *SetImageExtender) showSetImageDialog(path string) { func (s *ImageExtender) showImageDialog(path string) {
confirm := tview.NewModalForm("<Set image>", s.makeSetImageForm(path)) confirm := tview.NewModalForm("<Set image>", s.makeSetImageForm(path))
confirm.SetText(fmt.Sprintf("Set image %s %s", s.GVR(), path)) confirm.SetText(fmt.Sprintf("Set image %s %s", s.GVR(), path))
confirm.SetDoneFunc(func(int, string) { confirm.SetDoneFunc(func(int, string) {
s.dismissDialog() s.dismissDialog()
}) })
s.App().Content.AddPage(setImageKey, confirm, false, false) s.App().Content.AddPage(imageKey, confirm, false, false)
s.App().Content.ShowPage(setImageKey) s.App().Content.ShowPage(imageKey)
} }
func (s *SetImageExtender) makeSetImageForm(sel string) *tview.Form { func (s *ImageExtender) makeSetImageForm(sel string) *tview.Form {
f := s.makeStyledForm() f := s.makeStyledForm()
podSpec, err := s.getPodSpec(sel) podSpec, err := s.getPodSpec(sel)
if err != nil { if err != nil {
s.App().Flash().Err(err) s.App().Flash().Err(err)
return nil return nil
} }
var formContainerLines []imageFormSpec formContainerLines := make([]*imageFormSpec, len(podSpec.InitContainers)+len(podSpec.Containers))
for _, spec := range podSpec.InitContainers { for _, spec := range podSpec.InitContainers {
formContainerLines = append(formContainerLines, imageFormSpec{init: true, name: spec.Name, dockerImage: spec.Image}) formContainerLines = append(formContainerLines, &imageFormSpec{init: true, name: spec.Name, dockerImage: spec.Image})
} }
for _, spec := range podSpec.Containers { for _, spec := range podSpec.Containers {
formContainerLines = append(formContainerLines, imageFormSpec{init: false, name: spec.Name, dockerImage: spec.Image}) formContainerLines = append(formContainerLines, &imageFormSpec{name: spec.Name, dockerImage: spec.Image})
} }
for _, ctn := range formContainerLines { for i := range formContainerLines {
ctnCopy := ctn ctn := formContainerLines[i]
f.AddInputField(ctn.name, ctn.dockerImage, 0, nil, func(changed string) { f.AddInputField(ctn.name, ctn.dockerImage, 0, nil, func(changed string) {
ctnCopy.newDockerImage = changed ctn.newDockerImage = changed
}) })
} }
f.AddButton("OK", func() { f.AddButton("OK", func() {
defer s.dismissDialog() defer s.dismissDialog()
if err != nil {
s.App().Flash().Err(err)
return
}
ctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout())
defer cancel()
var imageSpecsModified dao.ImageSpecs var imageSpecsModified dao.ImageSpecs
for _, v := range formContainerLines { for _, v := range formContainerLines {
if v.modified() { if v.modified() {
imageSpecsModified = append(imageSpecsModified, v.imageSpec()) imageSpecsModified = append(imageSpecsModified, v.imageSpec())
} }
} }
ctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout())
defer cancel()
if err := s.setImages(ctx, sel, imageSpecsModified); err != nil { if err := s.setImages(ctx, sel, imageSpecsModified); err != nil {
log.Error().Err(err).Msgf("PodSpec %s image update failed", sel) log.Error().Err(err).Msgf("PodSpec %s image update failed", sel)
s.App().Flash().Err(err) s.App().Flash().Err(err)
} else { return
s.App().Flash().Infof("Resource %s:%s image updated successfully", s.GVR(), sel)
} }
s.App().Flash().Infof("Resource %s:%s image updated successfully", s.GVR(), sel)
}) })
f.AddButton("Cancel", func() { f.AddButton("Cancel", func() {
s.dismissDialog() s.dismissDialog()
@ -129,11 +128,11 @@ func (s *SetImageExtender) makeSetImageForm(sel string) *tview.Form {
return f return f
} }
func (s *SetImageExtender) dismissDialog() { func (s *ImageExtender) dismissDialog() {
s.App().Content.RemovePage(setImageKey) s.App().Content.RemovePage(imageKey)
} }
func (s *SetImageExtender) makeStyledForm() *tview.Form { func (s *ImageExtender) makeStyledForm() *tview.Form {
f := tview.NewForm() f := tview.NewForm()
f.SetItemPadding(0) f.SetItemPadding(0)
f.SetButtonsAlign(tview.AlignCenter). f.SetButtonsAlign(tview.AlignCenter).
@ -144,19 +143,20 @@ func (s *SetImageExtender) makeStyledForm() *tview.Form {
return f return f
} }
func (s *SetImageExtender) getPodSpec(path string) (*corev1.PodSpec, error) { func (s *ImageExtender) getPodSpec(path string) (*corev1.PodSpec, error) {
res, err := dao.AccessorFor(s.App().factory, s.GVR()) res, err := dao.AccessorFor(s.App().factory, s.GVR())
if err != nil { if err != nil {
return nil, err return nil, err
} }
resourceWPodSpec, ok := res.(dao.ContainsPodSpec) resourceWPodSpec, ok := res.(dao.ContainsPodSpec)
if !ok { if !ok {
return nil, fmt.Errorf("expecting a resourceWPodSpec resource for %q", s.GVR()) return nil, fmt.Errorf("expecting a ContainsPodSpec for %q but got %T", s.GVR(), res)
} }
return resourceWPodSpec.GetPodSpec(path) return resourceWPodSpec.GetPodSpec(path)
} }
func (s *SetImageExtender) setImages(ctx context.Context, path string, imageSpecs dao.ImageSpecs) error { func (s *ImageExtender) setImages(ctx context.Context, path string, imageSpecs dao.ImageSpecs) error {
res, err := dao.AccessorFor(s.App().factory, s.GVR()) res, err := dao.AccessorFor(s.App().factory, s.GVR())
if err != nil { if err != nil {
return err return err

View File

@ -54,7 +54,7 @@ func NewLog(gvr client.GVR, path, co string, prev bool) *Log {
Flex: tview.NewFlex(), Flex: tview.NewFlex(),
model: model.NewLog( model: model.NewLog(
gvr, gvr,
buildLogOpts(path, co, prev, true, config.DefaultLoggerTailCount), buildLogOpts(path, co, prev, false, config.DefaultLoggerTailCount),
flushTimeout, flushTimeout,
), ),
} }
@ -105,7 +105,7 @@ func (l *Log) Init(ctx context.Context) (err error) {
func (l *Log) LogCleared() { func (l *Log) LogCleared() {
l.app.QueueUpdateDraw(func() { l.app.QueueUpdateDraw(func() {
l.logs.Clear() l.logs.Clear()
l.logs.ScrollTo(0, 0) // l.logs.ScrollTo(0, 0)
}) })
} }
@ -123,18 +123,21 @@ func (l *Log) LogFailed(err error) {
} }
// LogChanged updates the logs. // LogChanged updates the logs.
func (l *Log) LogChanged(lines dao.LogItems) { func (l *Log) LogChanged(lines [][]byte) {
l.app.QueueUpdateDraw(func() { l.app.QueueUpdateDraw(func() {
l.Flush(lines) l.Flush(lines)
}) })
} }
// BufferChanged indicates the buffer was changed. // BufferCompleted indicates input was accepted.
func (l *Log) BufferChanged(s string) { func (l *Log) BufferCompleted(s string) {
l.model.Filter(l.logs.cmdBuff.GetText()) l.model.Filter(l.logs.cmdBuff.GetText())
l.updateTitle() l.updateTitle()
} }
// BufferChanged indicates the buffer was changed.
func (l *Log) BufferChanged(s string) {}
// BufferActive indicates the buff activity changed. // BufferActive indicates the buff activity changed.
func (l *Log) BufferActive(state bool, k model.BufferKind) { func (l *Log) BufferActive(state bool, k model.BufferKind) {
l.app.BufferActive(state, k) l.app.BufferActive(state, k)
@ -181,24 +184,40 @@ 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{
ui.Key0: ui.NewKeyAction("all", l.sinceCmd(-1), true), ui.Key0: ui.NewKeyAction("all", l.sinceCmd(-1), true),
ui.Key1: ui.NewKeyAction("1m", l.sinceCmd(60), true), ui.Key1: ui.NewKeyAction("1m", l.sinceCmd(60), true),
ui.Key2: ui.NewKeyAction("5m", l.sinceCmd(5*60), true), ui.Key2: ui.NewKeyAction("5m", l.sinceCmd(5*60), true),
ui.Key3: ui.NewKeyAction("15m", l.sinceCmd(15*60), true), ui.Key3: ui.NewKeyAction("15m", l.sinceCmd(15*60), true),
ui.Key4: ui.NewKeyAction("30m", l.sinceCmd(30*60), true), ui.Key4: ui.NewKeyAction("30m", l.sinceCmd(30*60), true),
ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true), ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true),
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false), tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false),
tcell.KeyCtrlK: ui.NewKeyAction("Clear", l.clearCmd, true), tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, false),
ui.KeyM: ui.NewKeyAction("Mark", l.markCmd, true), tcell.KeyCtrlK: ui.NewKeyAction("Clear", l.clearCmd, true),
ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true), ui.KeyM: ui.NewKeyAction("Mark", l.markCmd, true),
ui.KeyF: ui.NewKeyAction("Toggle FullScreen", l.toggleFullScreenCmd, true), ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true),
ui.KeyT: ui.NewKeyAction("Toggle Timestamp", l.toggleTimestampCmd, true), ui.KeyF: ui.NewKeyAction("Toggle FullScreen", l.toggleFullScreenCmd, true),
ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.toggleTextWrapCmd, true), ui.KeyT: ui.NewKeyAction("Toggle Timestamp", l.toggleTimestampCmd, true),
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true), ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.toggleTextWrapCmd, true),
ui.KeyC: ui.NewKeyAction("Copy", l.cpCmd, true), tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true),
ui.KeyC: ui.NewKeyAction("Copy", l.cpCmd, true),
}) })
} }
func (l *Log) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !l.logs.cmdBuff.IsActive() {
if l.logs.cmdBuff.GetText() == "" {
return l.app.PrevCmd(evt)
}
}
l.logs.cmdBuff.Reset()
l.logs.cmdBuff.SetActive(false)
l.model.Filter(l.logs.cmdBuff.GetText())
l.updateTitle()
return nil
}
// SendStrokes (testing only!) // SendStrokes (testing only!)
func (l *Log) SendStrokes(s string) { func (l *Log) SendStrokes(s string) {
l.app.Prompt().SendStrokes(s) l.app.Prompt().SendStrokes(s)
@ -248,14 +267,13 @@ func (l *Log) Logs() *Details {
var EOL = []byte{'\n'} var EOL = []byte{'\n'}
// Flush write logs to viewer. // Flush write logs to viewer.
func (l *Log) Flush(lines dao.LogItems) { func (l *Log) Flush(lines [][]byte) {
log.Debug().Msgf("LOG-FLUSH %d", len(lines))
if !l.indicator.AutoScroll() { if !l.indicator.AutoScroll() {
return return
} }
ll := make([][]byte, len(lines))
lines.Render(l.Indicator().showTime, ll)
_, _ = l.ansiWriter.Write(EOL) _, _ = l.ansiWriter.Write(EOL)
if _, err := l.ansiWriter.Write(bytes.Join(ll, EOL)); err != nil { if _, err := l.ansiWriter.Write(bytes.Join(lines, EOL)); err != nil {
log.Error().Err(err).Msgf("write logs failed") log.Error().Err(err).Msgf("write logs failed")
} }
l.logs.ScrollToEnd() l.logs.ScrollToEnd()

View File

@ -51,7 +51,7 @@ func TestLogViewClear(t *testing.T) {
func TestLogTimestamp(t *testing.T) { func TestLogTimestamp(t *testing.T) {
l := NewLog(client.NewGVR("test"), "fred/blee", "c1", false) l := NewLog(client.NewGVR("test"), "fred/blee", "c1", false)
l.Init(makeContext()) l.Init(makeContext())
buff := dao.LogItems{ ii := dao.LogItems{
&dao.LogItem{ &dao.LogItem{
Pod: "fred/blee", Pod: "fred/blee",
Container: "c1", Container: "c1",
@ -61,10 +61,10 @@ func TestLogTimestamp(t *testing.T) {
} }
var list logList var list logList
l.GetModel().AddListener(&list) l.GetModel().AddListener(&list)
l.GetModel().Set(buff) l.GetModel().Set(ii)
l.SendKeys(ui.KeyT) l.SendKeys(ui.KeyT)
l.Logs().Clear() l.Logs().Clear()
l.Flush(buff) l.Flush(ii.Lines(true))
assert.Equal(t, fmt.Sprintf("\n%-30s %s", "ttt", "fred/blee:c1 Testing 1, 2, 3"), l.Logs().GetText(true)) assert.Equal(t, fmt.Sprintf("\n%-30s %s", "ttt", "fred/blee:c1 Testing 1, 2, 3"), l.Logs().GetText(true))
assert.Equal(t, 2, list.change) assert.Equal(t, 2, list.change)
@ -99,11 +99,11 @@ type logList struct {
lines string lines string
} }
func (l *logList) LogChanged(ii dao.LogItems) { func (l *logList) LogChanged(ll [][]byte) {
l.change++ l.change++
l.lines = "" l.lines = ""
for _, i := range ii { for _, line := range ll {
l.lines += string(i.Render(0, false)) l.lines += string(line)
} }
} }
func (l *logList) LogCleared() { l.clear++ } func (l *logList) LogCleared() { l.clear++ }

View File

@ -22,7 +22,7 @@ func TestLog(t *testing.T) {
v.Flush(dao.LogItems{ v.Flush(dao.LogItems{
dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("blee"),
dao.NewLogItemFromString("bozo"), dao.NewLogItemFromString("bozo"),
}) }.Lines(false))
assert.Equal(t, 29, len(v.Logs().GetText(true))) assert.Equal(t, 29, len(v.Logs().GetText(true)))
} }
@ -38,7 +38,7 @@ func BenchmarkLogFlush(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
v.Flush(items) v.Flush(items.Lines(false))
} }
} }
@ -61,7 +61,10 @@ func TestLogViewSave(t *testing.T) {
v.Init(makeContext()) v.Init(makeContext())
app := makeApp() app := makeApp()
v.Flush(dao.LogItems{dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo")}) v.Flush(dao.LogItems{
dao.NewLogItemFromString("blee"),
dao.NewLogItemFromString("bozo"),
}.Lines(false))
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)

View File

@ -19,7 +19,7 @@ func NewLogsExtender(v ResourceViewer, f ContainerFunc) ResourceViewer {
ResourceViewer: v, ResourceViewer: v,
containerFn: f, containerFn: f,
} }
l.bindKeys(l.Actions()) l.AddBindKeysFn(l.bindKeys)
return &l return &l
} }
@ -27,8 +27,8 @@ func NewLogsExtender(v ResourceViewer, f ContainerFunc) ResourceViewer {
// BindKeys injects new menu actions. // BindKeys injects new menu actions.
func (l *LogsExtender) bindKeys(aa ui.KeyActions) { func (l *LogsExtender) bindKeys(aa ui.KeyActions) {
aa.Add(ui.KeyActions{ aa.Add(ui.KeyActions{
ui.KeyL: ui.NewKeyAction("Logs", l.logsCmd(false), true), ui.KeyL: ui.NewKeyAction("Logs", l.logsCmd(false), true),
ui.KeyShiftL: ui.NewKeyAction("Logs Previous", l.logsCmd(true), true), ui.KeyP: ui.NewKeyAction("Logs Previous", l.logsCmd(true), true),
}) })
} }

View File

@ -26,7 +26,7 @@ func NewNode(gvr client.GVR) ResourceViewer {
n := Node{ n := Node{
ResourceViewer: NewBrowser(gvr), ResourceViewer: NewBrowser(gvr),
} }
n.SetBindKeysFn(n.bindKeys) n.AddBindKeysFn(n.bindKeys)
n.GetTable().SetEnterFn(n.showPods) n.GetTable().SetEnterFn(n.showPods)
return &n return &n
@ -49,7 +49,7 @@ func (n *Node) bindDangerousKeys(aa ui.KeyActions) {
func (n *Node) bindKeys(aa ui.KeyActions) { func (n *Node) bindKeys(aa ui.KeyActions) {
aa.Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlD) aa.Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlD)
if !n.App().Config.K9s.GetReadOnly() { if !n.App().Config.K9s.IsReadOnly() {
n.bindDangerousKeys(aa) n.bindDangerousKeys(aa)
} }

View File

@ -29,7 +29,7 @@ func NewNamespace(gvr client.GVR) ResourceViewer {
n.GetTable().SetDecorateFn(n.decorate) n.GetTable().SetDecorateFn(n.decorate)
n.GetTable().SetColorerFn(render.Namespace{}.ColorerFunc()) n.GetTable().SetColorerFn(render.Namespace{}.ColorerFunc())
n.GetTable().SetEnterFn(n.switchNs) n.GetTable().SetEnterFn(n.switchNs)
n.SetBindKeysFn(n.bindKeys) n.AddBindKeysFn(n.bindKeys)
return &n return &n
} }

View File

@ -16,7 +16,7 @@ type OpenFaas struct {
// NewOpenFaas returns a new viewer. // NewOpenFaas returns a new viewer.
func NewOpenFaas(gvr client.GVR) ResourceViewer { func NewOpenFaas(gvr client.GVR) ResourceViewer {
o := OpenFaas{ResourceViewer: NewBrowser(gvr)} o := OpenFaas{ResourceViewer: NewBrowser(gvr)}
o.SetBindKeysFn(o.bindKeys) o.AddBindKeysFn(o.bindKeys)
o.GetTable().SetEnterFn(o.showPods) o.GetTable().SetEnterFn(o.showPods)
o.GetTable().SetColorerFn(render.OpenFaas{}.ColorerFunc()) o.GetTable().SetColorerFn(render.OpenFaas{}.ColorerFunc())

View File

@ -36,7 +36,7 @@ func NewPortForward(gvr client.GVR) ResourceViewer {
p.GetTable().SetColorerFn(render.PortForward{}.ColorerFunc()) p.GetTable().SetColorerFn(render.PortForward{}.ColorerFunc())
p.GetTable().SetSortCol(ageCol, true) p.GetTable().SetSortCol(ageCol, true)
p.SetContextFn(p.portForwardContext) p.SetContextFn(p.portForwardContext)
p.SetBindKeysFn(p.bindKeys) p.AddBindKeysFn(p.bindKeys)
return &p return &p
} }

View File

@ -6,6 +6,7 @@ import (
"strings" "strings"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview" "github.com/derailed/tview"
) )
@ -117,7 +118,7 @@ func extractPort(p string) string {
func extractContainer(p string) string { func extractContainer(p string) string {
tokens := strings.Split(p, ":") tokens := strings.Split(p, ":")
if len(tokens) != 2 { if len(tokens) != 2 {
return "n/a" return render.NAValue
} }
co, _ := client.Namespaced(tokens[0]) co, _ := client.Namespaced(tokens[0])

View File

@ -26,10 +26,10 @@ type PortForwardExtender struct {
// NewPortForwardExtender returns a new extender. // NewPortForwardExtender returns a new extender.
func NewPortForwardExtender(r ResourceViewer) ResourceViewer { func NewPortForwardExtender(r ResourceViewer) ResourceViewer {
s := PortForwardExtender{ResourceViewer: r} p := PortForwardExtender{ResourceViewer: r}
s.bindKeys(s.Actions()) p.AddBindKeysFn(p.bindKeys)
return &s return &p
} }
func (p *PortForwardExtender) bindKeys(aa ui.KeyActions) { func (p *PortForwardExtender) bindKeys(aa ui.KeyActions) {

View File

@ -29,11 +29,11 @@ type Pod struct {
func NewPod(gvr client.GVR) ResourceViewer { func NewPod(gvr client.GVR) ResourceViewer {
p := Pod{} p := Pod{}
p.ResourceViewer = NewPortForwardExtender( p.ResourceViewer = NewPortForwardExtender(
NewSetImageExtender( NewImageExtender(
NewLogsExtender(NewBrowser(gvr), p.selectedContainer), NewLogsExtender(NewBrowser(gvr), p.selectedContainer),
), ),
) )
p.SetBindKeysFn(p.bindKeys) p.AddBindKeysFn(p.bindKeys)
p.GetTable().SetEnterFn(p.showContainers) p.GetTable().SetEnterFn(p.showContainers)
p.GetTable().SetColorerFn(render.Pod{}.ColorerFunc()) p.GetTable().SetColorerFn(render.Pod{}.ColorerFunc())
p.GetTable().SetDecorateFn(p.portForwardIndicator) p.GetTable().SetDecorateFn(p.portForwardIndicator)
@ -63,7 +63,7 @@ func (p *Pod) bindDangerousKeys(aa ui.KeyActions) {
} }
func (p *Pod) bindKeys(aa ui.KeyActions) { func (p *Pod) bindKeys(aa ui.KeyActions) {
if !p.App().Config.K9s.GetReadOnly() { if !p.App().Config.K9s.IsReadOnly() {
p.bindDangerousKeys(aa) p.bindDangerousKeys(aa)
} }
@ -351,7 +351,6 @@ func podIsRunning(f dao.Factory, path string) bool {
} }
var re render.Pod var re render.Pod
log.Debug().Msgf("Phase %#v", re.Phase(po))
return re.Phase(po) == render.Running return re.Phase(po) == render.Running
} }

View File

@ -31,7 +31,7 @@ func NewPolicy(app *App, subject, name string) *Policy {
subjectName: name, subjectName: name,
} }
p.GetTable().SetColorerFn(render.Policy{}.ColorerFunc()) p.GetTable().SetColorerFn(render.Policy{}.ColorerFunc())
p.SetBindKeysFn(p.bindKeys) p.AddBindKeysFn(p.bindKeys)
p.GetTable().SetSortCol(nameCol, false) p.GetTable().SetSortCol(nameCol, false)
p.SetContextFn(p.subjectCtx) p.SetContextFn(p.subjectCtx)
p.GetTable().SetEnterFn(blankEnterFn) p.GetTable().SetEnterFn(blankEnterFn)

View File

@ -28,7 +28,7 @@ func NewPopeye(gvr client.GVR) ResourceViewer {
p.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorMediumSpringGreen, tcell.AttrNone) p.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorMediumSpringGreen, tcell.AttrNone)
p.GetTable().SetSortCol("SCORE%", true) p.GetTable().SetSortCol("SCORE%", true)
p.GetTable().SetDecorateFn(p.decorateRows) p.GetTable().SetDecorateFn(p.decorateRows)
p.SetBindKeysFn(p.bindKeys) p.AddBindKeysFn(p.bindKeys)
return &p return &p
} }

View File

@ -264,8 +264,8 @@ func (p *Pulse) SetInstance(string) {}
// SetEnvFn sets the custom environment function. // SetEnvFn sets the custom environment function.
func (p *Pulse) SetEnvFn(EnvFunc) {} func (p *Pulse) SetEnvFn(EnvFunc) {}
// SetBindKeysFn sets up extra key bindings. // AddBindKeysFn sets up extra key bindings.
func (p *Pulse) SetBindKeysFn(BindKeysFunc) {} func (p *Pulse) AddBindKeysFn(BindKeysFunc) {}
// SetContextFn sets custom context. // SetContextFn sets custom context.
func (p *Pulse) SetContextFn(ContextFunc) {} func (p *Pulse) SetContextFn(ContextFunc) {}

View File

@ -17,7 +17,7 @@ func NewPersistentVolumeClaim(gvr client.GVR) ResourceViewer {
v := PersistentVolumeClaim{ v := PersistentVolumeClaim{
ResourceViewer: NewBrowser(gvr), ResourceViewer: NewBrowser(gvr),
} }
v.SetBindKeysFn(v.bindKeys) v.AddBindKeysFn(v.bindKeys)
v.GetTable().SetColorerFn(render.PersistentVolumeClaim{}.ColorerFunc()) v.GetTable().SetColorerFn(render.PersistentVolumeClaim{}.ColorerFunc())
return &v return &v

View File

@ -21,7 +21,7 @@ func NewRbac(gvr client.GVR) ResourceViewer {
ResourceViewer: NewBrowser(gvr), ResourceViewer: NewBrowser(gvr),
} }
r.GetTable().SetColorerFn(render.Rbac{}.ColorerFunc()) r.GetTable().SetColorerFn(render.Rbac{}.ColorerFunc())
r.SetBindKeysFn(r.bindKeys) r.AddBindKeysFn(r.bindKeys)
r.GetTable().SetSortCol("APIGROUP", true) r.GetTable().SetSortCol("APIGROUP", true)
r.GetTable().SetEnterFn(blankEnterFn) r.GetTable().SetEnterFn(blankEnterFn)

View File

@ -22,7 +22,7 @@ func NewReference(gvr client.GVR) ResourceViewer {
r.GetTable().SetColorerFn(render.Reference{}.ColorerFunc()) r.GetTable().SetColorerFn(render.Reference{}.ColorerFunc())
r.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen) r.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen)
r.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorMediumSpringGreen, tcell.AttrNone) r.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorMediumSpringGreen, tcell.AttrNone)
r.SetBindKeysFn(r.bindKeys) r.AddBindKeysFn(r.bindKeys)
return &r return &r
} }

View File

@ -19,13 +19,16 @@ type RestartExtender struct {
// NewRestartExtender returns a new extender. // NewRestartExtender returns a new extender.
func NewRestartExtender(v ResourceViewer) ResourceViewer { func NewRestartExtender(v ResourceViewer) ResourceViewer {
r := RestartExtender{ResourceViewer: v} r := RestartExtender{ResourceViewer: v}
r.bindKeys(v.Actions()) v.AddBindKeysFn(r.bindKeys)
return &r return &r
} }
// BindKeys creates additional menu actions. // BindKeys creates additional menu actions.
func (r *RestartExtender) bindKeys(aa ui.KeyActions) { func (r *RestartExtender) bindKeys(aa ui.KeyActions) {
if r.App().Config.K9s.IsReadOnly() {
return
}
aa.Add(ui.KeyActions{ aa.Add(ui.KeyActions{
tcell.KeyCtrlT: ui.NewKeyAction("Restart", r.restartCmd, true), tcell.KeyCtrlT: ui.NewKeyAction("Restart", r.restartCmd, true),
}) })

View File

@ -21,7 +21,7 @@ func NewReplicaSet(gvr client.GVR) ResourceViewer {
r := ReplicaSet{ r := ReplicaSet{
ResourceViewer: NewBrowser(gvr), ResourceViewer: NewBrowser(gvr),
} }
r.SetBindKeysFn(r.bindKeys) r.AddBindKeysFn(r.bindKeys)
r.GetTable().SetEnterFn(r.showPods) r.GetTable().SetEnterFn(r.showPods)
r.GetTable().SetColorerFn(render.ReplicaSet{}.ColorerFunc()) r.GetTable().SetColorerFn(render.ReplicaSet{}.ColorerFunc())

View File

@ -19,7 +19,7 @@ func NewServiceAccount(gvr client.GVR) ResourceViewer {
s := ServiceAccount{ s := ServiceAccount{
ResourceViewer: NewBrowser(gvr), ResourceViewer: NewBrowser(gvr),
} }
s.SetBindKeysFn(s.bindKeys) s.AddBindKeysFn(s.bindKeys)
return &s return &s
} }

View File

@ -315,7 +315,10 @@ func (s *Sanitizer) SetEnvFn(EnvFunc) {}
func (s *Sanitizer) Refresh() {} func (s *Sanitizer) Refresh() {}
// BufferChanged indicates the buffer was changed. // BufferChanged indicates the buffer was changed.
func (s *Sanitizer) BufferChanged(q string) { func (s *Sanitizer) BufferChanged(q string) {}
// BufferCompleted indicates input was accepted.
func (s *Sanitizer) BufferCompleted(q string) {
s.update(s.filter(s.model.Peek())) s.update(s.filter(s.model.Peek()))
} }
@ -360,8 +363,8 @@ func (s *Sanitizer) Stop() {
s.CmdBuff().RemoveListener(s) s.CmdBuff().RemoveListener(s)
} }
// SetBindKeysFn sets up extra key bindings. // AddBindKeysFn sets up extra key bindings.
func (s *Sanitizer) SetBindKeysFn(BindKeysFunc) {} func (s *Sanitizer) AddBindKeysFn(BindKeysFunc) {}
// SetContextFn sets custom context. // SetContextFn sets custom context.
func (s *Sanitizer) SetContextFn(f ContextFunc) { func (s *Sanitizer) SetContextFn(f ContextFunc) {

View File

@ -21,12 +21,15 @@ type ScaleExtender struct {
// NewScaleExtender returns a new extender. // NewScaleExtender returns a new extender.
func NewScaleExtender(r ResourceViewer) ResourceViewer { func NewScaleExtender(r ResourceViewer) ResourceViewer {
s := ScaleExtender{ResourceViewer: r} s := ScaleExtender{ResourceViewer: r}
s.bindKeys(s.Actions()) s.AddBindKeysFn(s.bindKeys)
return &s return &s
} }
func (s *ScaleExtender) bindKeys(aa ui.KeyActions) { func (s *ScaleExtender) bindKeys(aa ui.KeyActions) {
if s.App().Config.K9s.IsReadOnly() {
return
}
aa.Add(ui.KeyActions{ aa.Add(ui.KeyActions{
ui.KeyS: ui.NewKeyAction("Scale", s.scaleCmd, true), ui.KeyS: ui.NewKeyAction("Scale", s.scaleCmd, true),
}) })

View File

@ -22,7 +22,7 @@ func NewSecret(gvr client.GVR) ResourceViewer {
s := Secret{ s := Secret{
ResourceViewer: NewBrowser(gvr), ResourceViewer: NewBrowser(gvr),
} }
s.SetBindKeysFn(s.bindKeys) s.AddBindKeysFn(s.bindKeys)
return &s return &s
} }

View File

@ -21,14 +21,14 @@ func NewStatefulSet(gvr client.GVR) ResourceViewer {
ResourceViewer: NewPortForwardExtender( ResourceViewer: NewPortForwardExtender(
NewRestartExtender( NewRestartExtender(
NewScaleExtender( NewScaleExtender(
NewSetImageExtender( NewImageExtender(
NewLogsExtender(NewBrowser(gvr), nil), NewLogsExtender(NewBrowser(gvr), nil),
), ),
), ),
), ),
), ),
} }
s.SetBindKeysFn(s.bindKeys) s.AddBindKeysFn(s.bindKeys)
s.GetTable().SetEnterFn(s.showPods) s.GetTable().SetEnterFn(s.showPods)
s.GetTable().SetColorerFn(render.StatefulSet{}.ColorerFunc()) s.GetTable().SetColorerFn(render.StatefulSet{}.ColorerFunc())

View File

@ -35,7 +35,7 @@ func NewService(gvr client.GVR) ResourceViewer {
NewLogsExtender(NewBrowser(gvr), nil), NewLogsExtender(NewBrowser(gvr), nil),
), ),
} }
s.SetBindKeysFn(s.bindKeys) s.AddBindKeysFn(s.bindKeys)
s.GetTable().SetEnterFn(s.showPods) s.GetTable().SetEnterFn(s.showPods)
return &s return &s

View File

@ -20,7 +20,7 @@ type Table struct {
app *App app *App
enterFn EnterFunc enterFn EnterFunc
envFn EnvFunc envFn EnvFunc
bindKeysFn BindKeysFunc bindKeysFn []BindKeysFunc
} }
// NewTable returns a new viewer. // NewTable returns a new viewer.
@ -73,8 +73,10 @@ func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
// Name returns the table name. // Name returns the table name.
func (t *Table) Name() string { return t.GVR().R() } func (t *Table) Name() string { return t.GVR().R() }
// SetBindKeysFn adds additional key bindings. // AddBindKeysFn adds additional key bindings.
func (t *Table) SetBindKeysFn(f BindKeysFunc) { t.bindKeysFn = f } func (t *Table) AddBindKeysFn(f BindKeysFunc) {
t.bindKeysFn = append(t.bindKeysFn, f)
}
// SetEnvFn sets a function to pull viewer env vars for plugins. // SetEnvFn sets a function to pull viewer env vars for plugins.
func (t *Table) SetEnvFn(f EnvFunc) { t.envFn = f } func (t *Table) SetEnvFn(f EnvFunc) { t.envFn = f }
@ -125,11 +127,14 @@ func (t *Table) SetEnterFn(f EnterFunc) {
// SetExtraActionsFn specifies custom keyboard behavior. // SetExtraActionsFn specifies custom keyboard behavior.
func (t *Table) SetExtraActionsFn(BoostActionsFunc) {} func (t *Table) SetExtraActionsFn(BoostActionsFunc) {}
// BufferChanged indicates the buffer was changed. // BufferCompleted indicates input was accepted.
func (t *Table) BufferChanged(s string) { func (t *Table) BufferCompleted(s string) {
t.Filter(s) t.Filter(s)
} }
// BufferChanged indicates the buffer was changed.
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 model.BufferKind) { func (t *Table) BufferActive(state bool, k model.BufferKind) {
t.app.BufferActive(state, k) t.app.BufferActive(state, k)

View File

@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: the-map
data:
altGreeting: "Good Morning!"
enableRisky: "false"

View File

@ -0,0 +1,5 @@
commonLabels:
app: fred
resources:
- cm.yaml

View File

@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: the-map
data:
altGreeting: "Good Morning!"
enableRisky: "false"

View File

@ -0,0 +1,5 @@
commonLabels:
app: fred
resources:
- cm.yaml

View File

@ -0,0 +1,5 @@
commonLabels:
app: fred
resources:
- cm.yaml

View File

@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: the-map
data:
altGreeting: "Good Morning!"
enableRisky: "false"

View File

@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: the-map
data:
altGreeting: "Good Morning!"
enableRisky: "false"

View File

@ -0,0 +1,5 @@
commonLabels:
app: fred
resources:
- cm.yaml

View File

@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: the-map
data:
altGreeting: "Good Morning!"
enableRisky: "false"

View File

@ -0,0 +1,5 @@
commonLabels:
app: fred
resources:
- cm.yaml

View File

@ -86,8 +86,8 @@ type ResourceViewer interface {
// SetContextFn provision a custom context. // SetContextFn provision a custom context.
SetContextFn(ContextFunc) SetContextFn(ContextFunc)
// SetBindKeys provision additional key bindings. // AddBindKeys provision additional key bindings.
SetBindKeysFn(BindKeysFunc) AddBindKeysFn(BindKeysFunc)
// SetInstance sets a parent FQN // SetInstance sets a parent FQN
SetInstance(string) SetInstance(string)

View File

@ -19,7 +19,7 @@ type User struct {
func NewUser(gvr client.GVR) ResourceViewer { func NewUser(gvr client.GVR) ResourceViewer {
u := User{ResourceViewer: NewBrowser(gvr)} u := User{ResourceViewer: NewBrowser(gvr)}
u.GetTable().SetColorerFn(render.Subject{}.ColorerFunc()) u.GetTable().SetColorerFn(render.Subject{}.ColorerFunc())
u.SetBindKeysFn(u.bindKeys) u.AddBindKeysFn(u.bindKeys)
u.SetContextFn(u.subjectCtx) u.SetContextFn(u.subjectCtx)
return &u return &u

View File

@ -162,12 +162,12 @@ func (x *Xray) refreshActions() {
x.Actions().Delete(tcell.KeyEnter) x.Actions().Delete(tcell.KeyEnter)
aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, true) aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, true)
aa[ui.KeyL] = ui.NewKeyAction("Logs", x.logsCmd(false), true) aa[ui.KeyL] = ui.NewKeyAction("Logs", x.logsCmd(false), true)
aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", x.logsCmd(true), true) aa[ui.KeyP] = ui.NewKeyAction("Logs Previous", x.logsCmd(true), true)
case "v1/pods": case "v1/pods":
aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, true) aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, true)
aa[ui.KeyA] = ui.NewKeyAction("Attach", x.attachCmd, true) aa[ui.KeyA] = ui.NewKeyAction("Attach", x.attachCmd, true)
aa[ui.KeyL] = ui.NewKeyAction("Logs", x.logsCmd(false), true) aa[ui.KeyL] = ui.NewKeyAction("Logs", x.logsCmd(false), true)
aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", x.logsCmd(true), true) aa[ui.KeyP] = ui.NewKeyAction("Logs Previous", x.logsCmd(true), true)
} }
x.Actions().Add(aa) x.Actions().Add(aa)
} }
@ -545,11 +545,14 @@ func (x *Xray) SetEnvFn(EnvFunc) {}
// Refresh updates the view // Refresh updates the view
func (x *Xray) Refresh() {} func (x *Xray) Refresh() {}
// BufferChanged indicates the buffer was changed. // BufferCompleted indicates the buffer was changed.
func (x *Xray) BufferChanged(s string) { func (x *Xray) BufferCompleted(s string) {
x.update(x.filter(x.model.Peek())) x.update(x.filter(x.model.Peek()))
} }
// BufferChanged indicates the buffer was changed.
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 model.BufferKind) { func (x *Xray) BufferActive(state bool, k model.BufferKind) {
x.app.BufferActive(state, k) x.app.BufferActive(state, k)
@ -589,7 +592,7 @@ func (x *Xray) Stop() {
} }
// SetBindKeysFn sets up extra key bindings. // SetBindKeysFn sets up extra key bindings.
func (x *Xray) SetBindKeysFn(BindKeysFunc) {} func (x *Xray) AddBindKeysFn(BindKeysFunc) {}
// SetContextFn sets custom context. // SetContextFn sets custom context.
func (x *Xray) SetContextFn(ContextFunc) {} func (x *Xray) SetContextFn(ContextFunc) {}