merging master

mine
derailed 2020-10-27 09:01:43 -06:00
commit dbfa66c9de
98 changed files with 977 additions and 341 deletions

View File

@ -280,9 +280,8 @@ K9s uses aliases to navigate most K8s resources.
tail: 200
# Defines the total number of log lines to allow in the view. Default 1000
buffer: 500
# Represents how far to go back in the log timeline in seconds. Default is 1min
# Set to -1 to disable filter
sinceSeconds: 60
# Represents how far to go back in the log timeline in seconds. Setting to -1 will show all available logs. Default is 5min.
sinceSeconds: 300
# Go full screen while displaying logs. Default false
fullScreenLogs: false
# Toggles log line wrap. Default false

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 (
version, commit, date = "dev", "dev", "n/a"
version, commit, date = "dev", "dev", client.NA
k9sFlags *config.Flags
k8sFlags *genericclioptions.ConfigFlags
demoMode = new(bool)
@ -79,17 +79,14 @@ func run(cmd *cobra.Command, args []string) {
zerolog.SetGlobalLevel(parseLevel(*k9sFlags.LogLevel))
app := view.NewApp(loadConfiguration())
{
defer app.BailOut()
if err := app.Init(version, *k9sFlags.RefreshRate); err != nil {
panic(fmt.Sprintf("app init failed -- %v", err))
}
if err := app.Run(); err != nil {
panic(fmt.Sprintf("app run failed %v", err))
}
if view.ExitStatus != "" {
panic(fmt.Sprintf("view exit status %s", view.ExitStatus))
}
if err := app.Init(version, *k9sFlags.RefreshRate); err != nil {
panic(fmt.Sprintf("app init failed -- %v", err))
}
if err := app.Run(); err != nil {
panic(fmt.Sprintf("app run failed %v", err))
}
if view.ExitStatus != "" {
panic(fmt.Sprintf("view exit status %s", view.ExitStatus))
}
}
@ -219,7 +216,7 @@ func initK9sFlags() {
k9sFlags.ReadOnly,
"readonly",
false,
"Disable all commands that modify the cluster",
"Toggles readOnly mode by overriding configuration setting",
)
rootCmd.Flags().BoolVar(
k9sFlags.Crumbsless,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -80,7 +80,7 @@ var (
)
// 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)
if showTime {
t := l.Timestamp
@ -92,11 +92,11 @@ func (l *LogItem) Render(c int, showTime bool) []byte {
}
if l.Pod != "" {
bb = append(bb, color.ANSIColorize(l.Pod, c)...)
bb = append(bb, color.ANSIColorize(l.Pod, paint)...)
bb = append(bb, ':')
}
if !l.SingleContainer && l.Container != "" {
bb = append(bb, color.ANSIColorize(l.Container, c)...)
bb = append(bb, color.ANSIColorize(l.Container, paint)...)
bb = append(bb, ' ')
}
@ -122,10 +122,20 @@ func colorFor(n string) int {
type LogItems []*LogItem
// 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))
for i, item := range l {
ll[i] = string(item.Render(0, false))
ll[i] = string(item.Render(0, showTime))
}
return ll
@ -154,15 +164,15 @@ func (l LogItems) DumpDebug(m string) {
}
// 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 == "" {
return nil, nil, nil
}
if IsFuzzySelector(q) {
mm, ii := l.fuzzyFilter(strings.TrimSpace(q[2:]))
mm, ii := l.fuzzyFilter(strings.TrimSpace(q[2:]), showTime)
return mm, ii, nil
}
matches, indices, err := l.filterLogs(q)
matches, indices, err := l.filterLogs(q, showTime)
if err != nil {
log.Error().Err(err).Msgf("Logs filter failed")
return nil, nil, err
@ -172,10 +182,10 @@ func (l LogItems) Filter(q string) ([]int, [][]int, error) {
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)
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 {
matches = append(matches, m.Index)
indices = append(indices, m.MatchedIndexes)
@ -184,14 +194,14 @@ func (l LogItems) fuzzyFilter(q string) ([]int, [][]int) {
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)
if err != nil {
return nil, nil, err
}
matches, indices := make([]int, 0, len(l)), make([][]int, 0, 10)
for i, line := range l.Lines() {
if locs := rx.FindStringIndex(line); locs != nil {
for i, line := range l.Lines(showTime) {
if locs := rx.FindIndex(line); locs != nil {
matches = append(matches, i)
ii := make([]int, 0, 10)
for i := 0; i < len(locs); i += 2 {

View File

@ -70,7 +70,7 @@ func TestLogItemsFilter(t *testing.T) {
for _, i := range ii {
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)
if err == nil {
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
for _, co := range po.Spec.InitContainers {
log.Debug().Msgf("Tailing INIT-CO %q", co.Name)
opts.Container = co.Name
if err := tailLogs(ctx, p, c, opts); err != nil {
return err
@ -213,7 +212,6 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
tailed = true
}
for _, co := range po.Spec.Containers {
log.Debug().Msgf("Tailing CO %q", co.Name)
opts.Container = co.Name
if err := tailLogs(ctx, p, c, opts); err != nil {
return err
@ -221,7 +219,6 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
tailed = true
}
for _, co := range po.Spec.EphemeralContainers {
log.Debug().Msgf("Tailing EPH-CO %q", co.Name)
opts.Container = co.Name
if err := tailLogs(ctx, p, c, opts); err != nil {
return err
@ -236,7 +233,7 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
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) {
ns, n := client.Namespaced(fqn)
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:
for r := 0; r < logRetryCount; r++ {
log.Debug().Msgf("Retry logs %d", r)
req, err = logger.Logs(opts.Path, opts.ToPodLogOptions())
if err == nil {
// This call will block if nothing is in the stream!!
if stream, err = req.Stream(ctx); err == nil {
log.Debug().Msgf("Reading logs")
go readLogs(stream, c, opts)
break
} else {
@ -426,18 +421,18 @@ func extractFQN(o runtime.Object) string {
u, ok := o.(*unstructured.Unstructured)
if !ok {
log.Error().Err(fmt.Errorf("expecting unstructured but got %T", o))
return "na"
return client.NA
}
m, ok := u.Object["metadata"].(map[string]interface{})
if !ok {
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)
if !ok {
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)

View File

@ -174,7 +174,7 @@ func parseRules(ns, binding string, rules []rbacv1.PolicyRule) render.Policies {
if nres[0] != '/' {
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 {
info, err := c.factory.Client().ServerVersion()
if err != nil {
return NA
return client.NA
}
return info.GitVersion
@ -55,7 +55,7 @@ func (c *Cluster) Version() string {
func (c *Cluster) ContextName() string {
n, err := c.factory.Client().Config().CurrentContextName()
if err != nil {
return NA
return client.NA
}
return n
}
@ -64,7 +64,7 @@ func (c *Cluster) ContextName() string {
func (c *Cluster) ClusterName() string {
n, err := c.factory.Client().Config().CurrentClusterName()
if err != nil {
return NA
return client.NA
}
return n
}
@ -73,7 +73,7 @@ func (c *Cluster) ClusterName() string {
func (c *Cluster) UserName() string {
n, err := c.factory.Client().Config().CurrentUserName()
if err != nil {
return NA
return client.NA
}
return n
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -72,8 +72,8 @@ func newMockLogView() *mockLogView {
return &mockLogView{}
}
func (t *mockLogView) LogChanged(d dao.LogItems) {
t.count += len(d.Lines())
func (t *mockLogView) LogChanged(ll [][]byte) {
t.count += len(ll)
}
func (t *mockLogView) LogCleared() {}
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.clearCalled)
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) {
@ -144,7 +144,7 @@ func TestLogBasic(t *testing.T) {
assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 1, v.clearCalled)
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) {
@ -153,9 +153,11 @@ func TestLogAppend(t *testing.T) {
v := newTestView()
m.AddListener(v)
items := dao.LogItems{dao.NewLogItemFromString("blah blah")}
items := dao.LogItems{
dao.NewLogItemFromString("blah blah"),
}
m.Set(items)
assert.Equal(t, items, v.data)
assert.Equal(t, items.Lines(false), v.data)
data := dao.LogItems{
dao.NewLogItemFromString("line1"),
@ -165,13 +167,13 @@ func TestLogAppend(t *testing.T) {
m.Append(d)
}
assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, items, v.data)
assert.Equal(t, items.Lines(false), v.data)
m.Notify()
assert.Equal(t, 2, v.dataCalled)
assert.Equal(t, 1, v.clearCalled)
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) {
@ -196,7 +198,7 @@ func TestLogTimedout(t *testing.T) {
assert.Equal(t, 1, v.clearCalled)
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"
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 {
data dao.LogItems
data [][]byte
dataCalled int
clearCalled int
errCalled int
@ -223,13 +225,13 @@ func newTestView() *testView {
return &testView{}
}
func (t *testView) LogChanged(d dao.LogItems) {
t.data = d
func (t *testView) LogChanged(ll [][]byte) {
t.data = ll
t.dataCalled++
}
func (t *testView) LogCleared() {
t.clearCalled++
t.data = dao.LogItems{}
t.data = nil
}
func (t *testView) LogFailed(err error) {
fmt.Println("LogErr", err)

View File

@ -33,7 +33,7 @@ func TestTableReconcile(t *testing.T) {
err := ta.reconcile(ctx)
assert.Nil(t, err)
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, client.NamespaceAll, data.Namespace)
}
@ -106,7 +106,7 @@ func TestTableHydrate(t *testing.T) {
assert.Nil(t, hydrate("blee", oo, rr, render.Pod{}))
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) {

View File

@ -33,7 +33,7 @@ func TestTableRefresh(t *testing.T) {
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
ta.Refresh(ctx)
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, client.NamespaceAll, data.Namespace)
assert.Equal(t, 1, l.count)

View File

@ -76,6 +76,8 @@ func (Container) Header(ns string) Header {
HeaderColumn{Name: "INIT"},
HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight},
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: "MEM", 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)
}
cur, perc, limit := gatherMetrics(co.Container, co.MX)
cur, perc, limit, res := gatherMetrics(co.Container, co.MX)
ready, state, restarts := "false", MissingValue, "0"
if co.Status != nil {
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),
restarts,
probe(co.Container.LivenessProbe) + ":" + probe(co.Container.ReadinessProbe),
ToResourcesMc(res),
ToResourcesMi(res),
cur.cpu,
cur.mem,
perc.cpu,
@ -140,8 +144,24 @@ func (Container) diagnose(state, ready string) error {
// ----------------------------------------------------------------------------
// 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()
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 {
return
}
@ -149,11 +169,10 @@ func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, p, l met
cpu := mx.Usage.Cpu().MilliValue()
mem := client.ToMB(mx.Usage.Memory().Value())
c = metric{
cpu: ToMillicore(cpu),
cpu: ToMc(cpu),
mem: ToMi(mem),
}
rcpu, rmem := containerResources(*co)
if rcpu != nil {
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()))
}
lcpu, lmem := containerLimits(*co)
if lcpu != nil {
l.cpu = client.ToPercentageStr(cpu, lcpu.MilliValue())
}

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import (
"strings"
"time"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/tview"
runewidth "github.com/mattn/go-runewidth"
"github.com/rs/zerolog/log"
@ -63,13 +64,6 @@ func Happy(ns string, h Header, r Row) bool {
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 {
if err == nil {
return ""
@ -257,14 +251,64 @@ func mapToIfc(m interface{}) (s string) {
return
}
// ToMillicore shows cpu reading for human.
func ToMillicore(v int64) string {
return strconv.Itoa(int(v))
// ToResourcesMi prints out request:limit mem resources.
func ToResourcesMi(res resources) string {
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 {
return strconv.Itoa(int(v))
p := message.NewPrinter(language.English)
return p.Sprintf("%dMi", v)
}
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 {
v int64
e string
}{
{0, "0"},
{2, "2"},
{1000, "1000"},
{0, "0m"},
{2, "2m"},
{1000, "1,000m"},
}
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
e string
}{
{0, "0"},
{2, "2"},
{1000, "1000"},
{0, "0Mi"},
{2, "2Mi"},
{1000, "1,000Mi"},
}
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())
c = metric{
cpu: ToMillicore(cpu),
cpu: ToMc(cpu),
mem: ToMi(mem),
}
acpu, amem := no.Status.Allocatable.Cpu().MilliValue(), client.ToMB(no.Status.Allocatable.Memory().Value())
a = metric{
cpu: ToMillicore(acpu),
cpu: ToMc(acpu),
mem: ToMi(amem),
}

View File

@ -21,7 +21,7 @@ func TestNodeRender(t *testing.T) {
assert.Nil(t, err)
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])
}

View File

@ -13,5 +13,5 @@ func TestPodDisruptionBudgetRender(t *testing.T) {
c.Render(load(t, "pdb"), "", &r)
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: "RESTARTS", Align: tview.AlignRight},
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: "MEM", 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
cr, _, rc := p.Statuses(ss)
c, perc := p.gatherPodMX(&po, pwm.MX)
c, perc, res := p.gatherPodMX(&po, pwm.MX)
phase := p.Phase(&po)
r.ID = client.MetaFQN(po.ObjectMeta)
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(rc),
phase,
ToResourcesMc(res),
ToResourcesMi(res),
c.cpu,
c.mem,
perc.cpu,
@ -149,7 +153,19 @@ func (p *PodWithMetrics) DeepCopyObject() runtime.Object {
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()
if mx == nil {
return
@ -161,12 +177,15 @@ func (*Pod) gatherPodMX(pod *v1.Pod, mx *mv1beta1.PodMetrics) (c, p metric) {
}
cpu, mem := currentRes(mx)
c = metric{
cpu: ToMillicore(cpu.MilliValue()),
cpu: ToMc(cpu.MilliValue()),
mem: ToMi(client.ToMB(mem.Value())),
}
rc, rm := resourceRequests(pod.Spec.Containers)
lc, lm := resourceLimits(pod.Spec.Containers)
rc, rm := podRequests(pod.Spec)
lc, lm := podLimits(pod.Spec)
r = make(resources, 4)
r[requestCPU], r[requestMEM] = rc, rm
r[limitCPU], r[limitMEM] = lc, lm
p = metric{
cpu: client.ToPercentageStr(cpu.MilliValue(), rc.MilliValue()),
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()
}
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 {
limit := co.Resources.Limits
if len(limit) == 0 {
@ -215,7 +235,28 @@ func resourceLimits(cc []v1.Container) (cpu, mem resource.Quantity) {
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 {
c, m := containerResources(co)
if c == nil || m == nil {
@ -230,6 +271,7 @@ func resourceRequests(cc []v1.Container) (cpu, mem resource.Quantity) {
mem.Add(*m)
}
}
return
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,9 @@ func TestCmdNew(t *testing.T) {
model := model.NewFishBuff(':', model.CommandBuffer)
v.SetModel(model)
model.AddListener(v)
model.SetText("blee")
for _, r := range "blee" {
model.Add(r)
}
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().SetBorderFocusColor(tcell.ColorAliceBlue)
a.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorAliceBlue, tcell.AttrNone)
a.SetBindKeysFn(a.bindKeys)
a.AddBindKeysFn(a.bindKeys)
a.SetContextFn(a.aliasContext)
return &a

View File

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

View File

@ -114,8 +114,6 @@ func (a *App) Init(version string, rate int) error {
return err
}
a.CmdBuff().SetSuggestionFn(a.suggestCommand())
// BOZO!!
// a.CmdBuff().AddListener(a)
a.layout(ctx, version)
a.initSignals()
@ -140,13 +138,11 @@ func (a *App) layout(ctx context.Context, version string) {
func (a *App) initSignals() {
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) {
s := <-sig
if s == syscall.SIGHUP {
os.Exit(0)
}
<-sig
os.Exit(0)
}(sig)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,8 +18,9 @@ const detailsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] "
// Details represents a generic text viewer.
type Details struct {
*tview.TextView
*tview.Flex
text *tview.TextView
actions ui.KeyActions
app *App
title, subject string
@ -27,12 +28,14 @@ type Details struct {
model *model.Text
currentRegion, maxRegions int
searchable bool
fullScreen bool
}
// NewDetails returns a details viewer.
func NewDetails(app *App, title, subject string, searchable bool) *Details {
d := Details{
TextView: tview.NewTextView(),
Flex: tview.NewFlex(),
text: tview.NewTextView(),
app: app,
title: title,
subject: subject,
@ -41,6 +44,7 @@ func NewDetails(app *App, title, subject string, searchable bool) *Details {
model: model.NewText(),
searchable: searchable,
}
d.AddItem(d.text, 0, 1, true)
return &d
}
@ -50,9 +54,9 @@ func (d *Details) Init(_ context.Context) error {
if d.title != "" {
d.SetBorder(true)
}
d.SetScrollable(true).SetWrap(true).SetRegions(true)
d.SetDynamicColors(true)
d.SetHighlightColor(tcell.ColorOrange)
d.text.SetScrollable(true).SetWrap(true).SetRegions(true)
d.text.SetDynamicColors(true)
d.text.SetHighlightColor(tcell.ColorOrange)
d.SetTitleColor(tcell.ColorAqua)
d.SetInputCapture(d.keyboard)
d.SetBorderPadding(0, 0, 1, 1)
@ -73,8 +77,8 @@ func (d *Details) Init(_ context.Context) error {
// TextChanged notifies the model changed.
func (d *Details) TextChanged(lines []string) {
d.SetText(colorizeYAML(d.app.Styles.Views().Yaml, strings.Join(lines, "\n")))
d.ScrollToBeginning()
d.text.SetText(colorizeYAML(d.app.Styles.Views().Yaml, strings.Join(lines, "\n")))
d.text.ScrollToBeginning()
}
// TextFiltered notifies when the filter changed.
@ -89,16 +93,19 @@ func (d *Details) TextFiltered(lines []string, matches fuzzy.Matches) {
d.maxRegions++
}
d.SetText(colorizeYAML(d.app.Styles.Views().Yaml, strings.Join(ll, "\n")))
d.Highlight()
d.text.SetText(colorizeYAML(d.app.Styles.Views().Yaml, strings.Join(ll, "\n")))
d.text.Highlight()
if d.maxRegions > 0 {
d.Highlight("search_0")
d.ScrollToHighlight()
d.text.Highlight("search_0")
d.text.ScrollToHighlight()
}
}
// 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.updateTitle()
}
@ -114,6 +121,7 @@ func (d *Details) bindKeys() {
tcell.KeyEscape: ui.NewKeyAction("Back", d.resetCmd, false),
tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, false),
ui.KeyC: ui.NewKeyAction("Copy", d.cpCmd, true),
ui.KeyF: ui.NewKeyAction("Toggle FullScreen", d.toggleFullScreenCmd, true),
ui.KeyN: ui.NewKeyAction("Next Match", d.nextCmd, true),
ui.KeyShiftN: ui.NewKeyAction("Prev Match", d.prevCmd, true),
ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", d.activateCmd, false),
@ -136,7 +144,7 @@ func (d *Details) keyboard(evt *tcell.EventKey) *tcell.EventKey {
// StylesChanged notifies the skin changed.
func (d *Details) StylesChanged(s *config.Styles) {
d.SetBackgroundColor(d.app.Styles.BgColor())
d.SetTextColor(d.app.Styles.FgColor())
d.text.SetTextColor(d.app.Styles.FgColor())
d.SetBorderFocusColor(d.app.Styles.Frame().Border.FocusColor.Color())
d.TextChanged(d.model.Peek())
}
@ -187,13 +195,25 @@ func (d *Details) nextCmd(evt *tcell.EventKey) *tcell.EventKey {
if d.currentRegion >= d.maxRegions {
d.currentRegion = 0
}
d.Highlight(fmt.Sprintf("search_%d", d.currentRegion))
d.ScrollToHighlight()
d.text.Highlight(fmt.Sprintf("search_%d", d.currentRegion))
d.text.ScrollToHighlight()
d.updateTitle()
return nil
}
func (d *Details) toggleFullScreenCmd(evt *tcell.EventKey) *tcell.EventKey {
if d.app.InCmdMode() {
return evt
}
d.fullScreen = !d.fullScreen
d.SetFullScreen(d.fullScreen)
d.Box.SetBorder(!d.fullScreen)
return nil
}
func (d *Details) prevCmd(evt *tcell.EventKey) *tcell.EventKey {
if d.cmdBuff.Empty() {
return evt
@ -203,8 +223,8 @@ func (d *Details) prevCmd(evt *tcell.EventKey) *tcell.EventKey {
if d.currentRegion < 0 {
d.currentRegion = d.maxRegions - 1
}
d.Highlight(fmt.Sprintf("search_%d", d.currentRegion))
d.ScrollToHighlight()
d.text.Highlight(fmt.Sprintf("search_%d", d.currentRegion))
d.text.ScrollToHighlight()
d.updateTitle()
return nil
@ -253,7 +273,7 @@ func (d *Details) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (d *Details) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
if path, err := saveYAML(d.app.Config.K9s.CurrentCluster, d.title, d.GetText(true)); err != nil {
if path, err := saveYAML(d.app.Config.K9s.CurrentCluster, d.title, d.text.GetText(true)); err != nil {
d.app.Flash().Err(err)
} else {
d.app.Flash().Infof("Log %s saved successfully!", path)
@ -264,7 +284,7 @@ func (d *Details) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
func (d *Details) cpCmd(evt *tcell.EventKey) *tcell.EventKey {
d.app.Flash().Info("Content copied to clipboard...")
if err := clipboard.WriteAll(d.GetText(true)); err != nil {
if err := clipboard.WriteAll(d.text.GetText(true)); err != nil {
d.app.Flash().Err(err)
}

View File

@ -10,11 +10,20 @@ import (
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/ui/dialog"
"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.
@ -31,7 +40,7 @@ func NewDir(path string) ResourceViewer {
}
d.GetTable().SetBorderFocusColor(tcell.ColorAliceBlue)
d.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorAliceBlue, tcell.AttrNone)
d.SetBindKeysFn(d.bindKeys)
d.AddBindKeysFn(d.bindKeys)
d.SetContextFn(d.dirContext)
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)
}
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) {
aa.Delete(ui.KeyShiftA, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)
aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL, tcell.KeyCtrlD, tcell.KeyCtrlZ)
if !d.App().Config.K9s.IsReadOnly() {
d.bindDangerousKeys(aa)
}
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),
tcell.KeyEnter: ui.NewKeyAction("Goto", d.gotoCmd, true),
})
@ -98,7 +115,6 @@ func (d *Dir) editCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt
}
log.Debug().Msgf("Selected %q", sel)
if !isManifest(sel) {
d.App().Flash().Errf("you must select a manifest")
return nil
@ -136,18 +152,62 @@ func (d *Dir) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
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 {
sel := d.GetTable().GetSelectedItem()
if sel == "" {
return evt
}
opts := []string{"-f"}
if containsDir(sel) {
opts = append(opts, "-R")
}
if isKustomized(sel) {
opts = []string{"-k"}
}
d.Stop()
defer d.Start()
{
args := make([]string, 0, 10)
args = append(args, "apply")
args = append(args, "-f")
args = append(args, opts...)
args = append(args, sel)
res, err := runKu(d.App(), shellOpts{clear: false, args: args})
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(
NewRestartExtender(
NewScaleExtender(
NewSetImageExtender(
NewImageExtender(
NewLogsExtender(
NewBrowser(gvr),
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().SetColorerFn(render.Deployment{}.ColorerFunc())

View File

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

View File

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

View File

@ -13,5 +13,5 @@ func TestDaemonSet(t *testing.T) {
assert.Nil(t, v.Init(makeCtx()))
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),
}
e.GetTable().SetColorerFn(render.Event{}.ColorerFunc())
e.SetBindKeysFn(e.bindKeys)
e.AddBindKeysFn(e.bindKeys)
e.GetTable().SetSortCol(ageCol, true)
return &e

View File

@ -162,7 +162,6 @@ func oneShoot(opts shellOpts) (string, error) {
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, buff, buff
_, _ = cmd.Stdout.Write([]byte(opts.banner))
err = cmd.Run()
log.Debug().Msgf("RES %q", buff)
return strings.Trim(buff.String(), "\n"), err
}

View File

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

View File

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

View File

@ -96,7 +96,7 @@ func TestK8sEnv(t *testing.T) {
assert.Equal(t, cl, env["CLUSTER"])
assert.Equal(t, ctx, env["CONTEXT"])
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"])
}
@ -123,7 +123,7 @@ func TestK9sEnv(t *testing.T) {
assert.Equal(t, cl, env["CLUSTER"])
assert.Equal(t, ctx, env["CONTEXT"])
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, "fred", env["NAMESPACE"])
assert.Equal(t, "blee", env["NAME"])

View File

@ -3,21 +3,17 @@ package view
import (
"context"
"fmt"
"strings"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
corev1 "k8s.io/api/core/v1"
"strings"
)
const setImageKey = "setImage"
// SetImageExtender adds set image extensions
type SetImageExtender struct {
ResourceViewer
}
const imageKey = "setImage"
type imageFormSpec struct {
name, dockerImage, newDockerImage string
@ -44,20 +40,28 @@ func (m *imageFormSpec) imageSpec() dao.ImageSpec {
return ret
}
func NewSetImageExtender(r ResourceViewer) ResourceViewer {
s := SetImageExtender{ResourceViewer: r}
s.bindKeys(s.Actions())
// ImageExtender provides for overriding container images.
type ImageExtender struct {
ResourceViewer
}
func NewImageExtender(r ResourceViewer) ResourceViewer {
s := ImageExtender{ResourceViewer: r}
s.AddBindKeysFn(s.bindKeys)
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{
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()
if path == "" {
return nil
@ -65,63 +69,58 @@ func (s *SetImageExtender) setImageCmd(evt *tcell.EventKey) *tcell.EventKey {
s.Stop()
defer s.Start()
s.showSetImageDialog(path)
s.showImageDialog(path)
return nil
}
func (s *SetImageExtender) showSetImageDialog(path string) {
func (s *ImageExtender) showImageDialog(path string) {
confirm := tview.NewModalForm("<Set image>", s.makeSetImageForm(path))
confirm.SetText(fmt.Sprintf("Set image %s %s", s.GVR(), path))
confirm.SetDoneFunc(func(int, string) {
s.dismissDialog()
})
s.App().Content.AddPage(setImageKey, confirm, false, false)
s.App().Content.ShowPage(setImageKey)
s.App().Content.AddPage(imageKey, confirm, false, false)
s.App().Content.ShowPage(imageKey)
}
func (s *SetImageExtender) makeSetImageForm(sel string) *tview.Form {
func (s *ImageExtender) makeSetImageForm(sel string) *tview.Form {
f := s.makeStyledForm()
podSpec, err := s.getPodSpec(sel)
if err != nil {
s.App().Flash().Err(err)
return nil
}
var formContainerLines []imageFormSpec
formContainerLines := make([]*imageFormSpec, len(podSpec.InitContainers)+len(podSpec.Containers))
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 {
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 {
ctnCopy := ctn
for i := range formContainerLines {
ctn := formContainerLines[i]
f.AddInputField(ctn.name, ctn.dockerImage, 0, nil, func(changed string) {
ctnCopy.newDockerImage = changed
ctn.newDockerImage = changed
})
}
f.AddButton("OK", func() {
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
for _, v := range formContainerLines {
if v.modified() {
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 {
log.Error().Err(err).Msgf("PodSpec %s image update failed", sel)
s.App().Flash().Err(err)
} else {
s.App().Flash().Infof("Resource %s:%s image updated successfully", s.GVR(), sel)
return
}
s.App().Flash().Infof("Resource %s:%s image updated successfully", s.GVR(), sel)
})
f.AddButton("Cancel", func() {
s.dismissDialog()
@ -129,11 +128,11 @@ func (s *SetImageExtender) makeSetImageForm(sel string) *tview.Form {
return f
}
func (s *SetImageExtender) dismissDialog() {
s.App().Content.RemovePage(setImageKey)
func (s *ImageExtender) dismissDialog() {
s.App().Content.RemovePage(imageKey)
}
func (s *SetImageExtender) makeStyledForm() *tview.Form {
func (s *ImageExtender) makeStyledForm() *tview.Form {
f := tview.NewForm()
f.SetItemPadding(0)
f.SetButtonsAlign(tview.AlignCenter).
@ -144,19 +143,20 @@ func (s *SetImageExtender) makeStyledForm() *tview.Form {
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())
if err != nil {
return nil, err
}
resourceWPodSpec, ok := res.(dao.ContainsPodSpec)
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)
}
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())
if err != nil {
return err

View File

@ -40,7 +40,7 @@ type Log struct {
*tview.Flex
app *App
logs *Details
logs *Logger
indicator *LogIndicator
ansiWriter io.Writer
model *model.Log
@ -54,7 +54,7 @@ func NewLog(gvr client.GVR, path, co string, prev bool) *Log {
Flex: tview.NewFlex(),
model: model.NewLog(
gvr,
buildLogOpts(path, co, prev, true, config.DefaultLoggerTailCount),
buildLogOpts(path, co, prev, false, config.DefaultLoggerTailCount),
flushTimeout,
),
}
@ -76,7 +76,7 @@ func (l *Log) Init(ctx context.Context) (err error) {
l.AddItem(l.indicator, 1, 1, false)
l.indicator.Refresh()
l.logs = NewDetails(l.app, "", "", false)
l.logs = NewLogger(l.app)
if err = l.logs.Init(ctx); err != nil {
return err
}
@ -105,7 +105,6 @@ func (l *Log) Init(ctx context.Context) (err error) {
func (l *Log) LogCleared() {
l.app.QueueUpdateDraw(func() {
l.logs.Clear()
l.logs.ScrollTo(0, 0)
})
}
@ -123,18 +122,21 @@ func (l *Log) LogFailed(err error) {
}
// LogChanged updates the logs.
func (l *Log) LogChanged(lines dao.LogItems) {
func (l *Log) LogChanged(lines [][]byte) {
l.app.QueueUpdateDraw(func() {
l.Flush(lines)
})
}
// BufferChanged indicates the buffer was changed.
func (l *Log) BufferChanged(s string) {
// BufferCompleted indicates input was accepted.
func (l *Log) BufferCompleted(s string) {
l.model.Filter(l.logs.cmdBuff.GetText())
l.updateTitle()
}
// BufferChanged indicates the buffer was changed.
func (l *Log) BufferChanged(s string) {}
// BufferActive indicates the buff activity changed.
func (l *Log) BufferActive(state bool, k model.BufferKind) {
l.app.BufferActive(state, k)
@ -181,24 +183,40 @@ func (l *Log) Name() string { return logTitle }
func (l *Log) bindKeys() {
l.logs.Actions().Set(ui.KeyActions{
ui.Key0: ui.NewKeyAction("all", l.sinceCmd(-1), true),
ui.Key1: ui.NewKeyAction("1m", l.sinceCmd(60), true),
ui.Key2: ui.NewKeyAction("5m", l.sinceCmd(5*60), true),
ui.Key3: ui.NewKeyAction("15m", l.sinceCmd(15*60), true),
ui.Key4: ui.NewKeyAction("30m", l.sinceCmd(30*60), true),
ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true),
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false),
tcell.KeyCtrlK: ui.NewKeyAction("Clear", l.clearCmd, true),
ui.KeyM: ui.NewKeyAction("Mark", l.markCmd, true),
ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true),
ui.KeyF: ui.NewKeyAction("Toggle FullScreen", l.toggleFullScreenCmd, true),
ui.KeyT: ui.NewKeyAction("Toggle Timestamp", l.toggleTimestampCmd, true),
ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.toggleTextWrapCmd, true),
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true),
ui.KeyC: ui.NewKeyAction("Copy", l.cpCmd, true),
ui.Key0: ui.NewKeyAction("all", l.sinceCmd(-1), true),
ui.Key1: ui.NewKeyAction("1m", l.sinceCmd(60), true),
ui.Key2: ui.NewKeyAction("5m", l.sinceCmd(5*60), true),
ui.Key3: ui.NewKeyAction("15m", l.sinceCmd(15*60), true),
ui.Key4: ui.NewKeyAction("30m", l.sinceCmd(30*60), true),
ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true),
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false),
tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, false),
tcell.KeyCtrlK: ui.NewKeyAction("Clear", l.clearCmd, true),
ui.KeyM: ui.NewKeyAction("Mark", l.markCmd, true),
ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true),
ui.KeyF: ui.NewKeyAction("Toggle FullScreen", l.toggleFullScreenCmd, true),
ui.KeyT: ui.NewKeyAction("Toggle Timestamp", l.toggleTimestampCmd, true),
ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.toggleTextWrapCmd, true),
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true),
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!)
func (l *Log) SendStrokes(s string) {
l.app.Prompt().SendStrokes(s)
@ -240,7 +258,7 @@ func (l *Log) updateTitle() {
}
// Logs returns the log viewer.
func (l *Log) Logs() *Details {
func (l *Log) Logs() *Logger {
return l.logs
}
@ -248,14 +266,13 @@ func (l *Log) Logs() *Details {
var EOL = []byte{'\n'}
// 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() {
return
}
ll := make([][]byte, len(lines))
lines.Render(l.Indicator().showTime, ll)
_, _ = 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")
}
l.logs.ScrollToEnd()

View File

@ -34,7 +34,7 @@ func TestLogViewNav(t *testing.T) {
v.toggleAutoScrollCmd(nil)
r, _ := v.Logs().GetScrollOffset()
assert.Equal(t, 0, r)
assert.Equal(t, -1, r)
}
func TestLogViewClear(t *testing.T) {
@ -51,7 +51,7 @@ func TestLogViewClear(t *testing.T) {
func TestLogTimestamp(t *testing.T) {
l := NewLog(client.NewGVR("test"), "fred/blee", "c1", false)
l.Init(makeContext())
buff := dao.LogItems{
ii := dao.LogItems{
&dao.LogItem{
Pod: "fred/blee",
Container: "c1",
@ -61,10 +61,10 @@ func TestLogTimestamp(t *testing.T) {
}
var list logList
l.GetModel().AddListener(&list)
l.GetModel().Set(buff)
l.GetModel().Set(ii)
l.SendKeys(ui.KeyT)
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, 2, list.change)
@ -99,11 +99,11 @@ type logList struct {
lines string
}
func (l *logList) LogChanged(ii dao.LogItems) {
func (l *logList) LogChanged(ll [][]byte) {
l.change++
l.lines = ""
for _, i := range ii {
l.lines += string(i.Render(0, false))
for _, line := range ll {
l.lines += string(line)
}
}
func (l *logList) LogCleared() { l.clear++ }

View File

@ -22,7 +22,7 @@ func TestLog(t *testing.T) {
v.Flush(dao.LogItems{
dao.NewLogItemFromString("blee"),
dao.NewLogItemFromString("bozo"),
})
}.Lines(false))
assert.Equal(t, 29, len(v.Logs().GetText(true)))
}
@ -38,7 +38,7 @@ func BenchmarkLogFlush(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
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())
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"
dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster)
c1, _ := ioutil.ReadDir(dir)

173
internal/view/logger.go Normal file
View File

@ -0,0 +1,173 @@
package view
import (
"context"
"github.com/atotto/clipboard"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
)
const loggerTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] "
// Logger represents a generic log viewer.
type Logger struct {
*tview.TextView
actions ui.KeyActions
app *App
title, subject string
cmdBuff *model.FishBuff
}
// NewLogger returns a logger viewer.
func NewLogger(app *App) *Logger {
return &Logger{
TextView: tview.NewTextView(),
app: app,
actions: make(ui.KeyActions),
cmdBuff: model.NewFishBuff('/', model.FilterBuffer),
}
}
// Init initializes the viewer.
func (l *Logger) Init(_ context.Context) error {
if l.title != "" {
l.SetBorder(true)
}
l.SetScrollable(true).SetWrap(true).SetRegions(true)
l.SetDynamicColors(true)
l.SetHighlightColor(tcell.ColorOrange)
l.SetTitleColor(tcell.ColorAqua)
l.SetInputCapture(l.keyboard)
l.SetBorderPadding(0, 0, 1, 1)
l.app.Styles.AddListener(l)
l.StylesChanged(l.app.Styles)
l.app.Prompt().SetModel(l.cmdBuff)
l.cmdBuff.AddListener(l)
l.bindKeys()
l.SetInputCapture(l.keyboard)
return nil
}
// BufferChanged indicates the buffer was changel.
func (l *Logger) BufferChanged(s string) {}
// BufferCompleted indicates input was acceptel.
func (l *Logger) BufferCompleted(s string) {
}
// BufferActive indicates the buff activity changel.
func (l *Logger) BufferActive(state bool, k model.BufferKind) {
l.app.BufferActive(state, k)
}
func (l *Logger) bindKeys() {
l.actions.Set(ui.KeyActions{
tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, false),
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.saveCmd, false),
ui.KeyC: ui.NewKeyAction("Copy", l.cpCmd, true),
ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", l.activateCmd, false),
tcell.KeyDelete: ui.NewSharedKeyAction("Erase", l.eraseCmd, false),
})
}
func (l *Logger) keyboard(evt *tcell.EventKey) *tcell.EventKey {
if a, ok := l.actions[ui.AsKey(evt)]; ok {
return a.Action(evt)
}
return evt
}
// StylesChanged notifies the skin changel.
func (l *Logger) StylesChanged(s *config.Styles) {
l.SetBackgroundColor(l.app.Styles.BgColor())
l.SetTextColor(l.app.Styles.FgColor())
l.SetBorderFocusColor(l.app.Styles.Frame().Border.FocusColor.Color())
}
// SetSubject updates the subject.
func (l *Logger) SetSubject(s string) {
l.subject = s
}
// Actions returns menu actions
func (l *Logger) Actions() ui.KeyActions {
return l.actions
}
// Name returns the component name.
func (l *Logger) Name() string { return l.title }
// Start starts the view updater.
func (l *Logger) Start() {}
// Stop terminates the updater.
func (l *Logger) Stop() {
l.app.Styles.RemoveListener(l)
}
// Hints returns menu hints.
func (l *Logger) Hints() model.MenuHints {
return l.actions.Hints()
}
// ExtraHints returns additional hints.
func (l *Logger) ExtraHints() map[string]string {
return nil
}
func (l *Logger) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
if l.app.InCmdMode() {
return evt
}
l.app.ResetPrompt(l.cmdBuff)
return nil
}
func (l *Logger) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
if !l.cmdBuff.IsActive() {
return nil
}
l.cmdBuff.Delete()
return nil
}
func (l *Logger) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !l.cmdBuff.InCmdMode() {
l.cmdBuff.Reset()
return l.app.PrevCmd(evt)
}
l.cmdBuff.SetActive(false)
l.cmdBuff.Reset()
return nil
}
func (l *Logger) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
if path, err := saveYAML(l.app.Config.K9s.CurrentCluster, l.title, l.GetText(true)); err != nil {
l.app.Flash().Err(err)
} else {
l.app.Flash().Infof("Log %s saved successfully!", path)
}
return nil
}
func (l *Logger) cpCmd(evt *tcell.EventKey) *tcell.EventKey {
l.app.Flash().Info("Content copied to clipboard...")
if err := clipboard.WriteAll(l.GetText(true)); err != nil {
l.app.Flash().Err(err)
}
return nil
}

View File

@ -19,7 +19,7 @@ func NewLogsExtender(v ResourceViewer, f ContainerFunc) ResourceViewer {
ResourceViewer: v,
containerFn: f,
}
l.bindKeys(l.Actions())
l.AddBindKeysFn(l.bindKeys)
return &l
}
@ -27,8 +27,8 @@ func NewLogsExtender(v ResourceViewer, f ContainerFunc) ResourceViewer {
// BindKeys injects new menu actions.
func (l *LogsExtender) bindKeys(aa ui.KeyActions) {
aa.Add(ui.KeyActions{
ui.KeyL: ui.NewKeyAction("Logs", l.logsCmd(false), true),
ui.KeyShiftL: ui.NewKeyAction("Logs Previous", l.logsCmd(true), true),
ui.KeyL: ui.NewKeyAction("Logs", l.logsCmd(false), 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{
ResourceViewer: NewBrowser(gvr),
}
n.SetBindKeysFn(n.bindKeys)
n.AddBindKeysFn(n.bindKeys)
n.GetTable().SetEnterFn(n.showPods)
return &n
@ -49,7 +49,7 @@ func (n *Node) bindDangerousKeys(aa ui.KeyActions) {
func (n *Node) bindKeys(aa ui.KeyActions) {
aa.Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlD)
if !n.App().Config.K9s.GetReadOnly() {
if !n.App().Config.K9s.IsReadOnly() {
n.bindDangerousKeys(aa)
}

View File

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

View File

@ -16,7 +16,7 @@ type OpenFaas struct {
// NewOpenFaas returns a new viewer.
func NewOpenFaas(gvr client.GVR) ResourceViewer {
o := OpenFaas{ResourceViewer: NewBrowser(gvr)}
o.SetBindKeysFn(o.bindKeys)
o.AddBindKeysFn(o.bindKeys)
o.GetTable().SetEnterFn(o.showPods)
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().SetSortCol(ageCol, true)
p.SetContextFn(p.portForwardContext)
p.SetBindKeysFn(p.bindKeys)
p.AddBindKeysFn(p.bindKeys)
return &p
}

View File

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

View File

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

View File

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

View File

@ -31,7 +31,7 @@ func NewPolicy(app *App, subject, name string) *Policy {
subjectName: name,
}
p.GetTable().SetColorerFn(render.Policy{}.ColorerFunc())
p.SetBindKeysFn(p.bindKeys)
p.AddBindKeysFn(p.bindKeys)
p.GetTable().SetSortCol(nameCol, false)
p.SetContextFn(p.subjectCtx)
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().SetSortCol("SCORE%", true)
p.GetTable().SetDecorateFn(p.decorateRows)
p.SetBindKeysFn(p.bindKeys)
p.AddBindKeysFn(p.bindKeys)
return &p
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -319,7 +319,10 @@ func (s *Sanitizer) SetEnvFn(EnvFunc) {}
func (s *Sanitizer) Refresh() {}
// 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()))
}
@ -364,8 +367,8 @@ func (s *Sanitizer) Stop() {
s.CmdBuff().RemoveListener(s)
}
// SetBindKeysFn sets up extra key bindings.
func (s *Sanitizer) SetBindKeysFn(BindKeysFunc) {}
// AddBindKeysFn sets up extra key bindings.
func (s *Sanitizer) AddBindKeysFn(BindKeysFunc) {}
// SetContextFn sets custom context.
func (s *Sanitizer) SetContextFn(f ContextFunc) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -162,12 +162,12 @@ func (x *Xray) refreshActions() {
x.Actions().Delete(tcell.KeyEnter)
aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, 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":
aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, true)
aa[ui.KeyA] = ui.NewKeyAction("Attach", x.attachCmd, 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)
}
@ -549,11 +549,14 @@ func (x *Xray) SetEnvFn(EnvFunc) {}
// Refresh updates the view
func (x *Xray) Refresh() {}
// BufferChanged indicates the buffer was changed.
func (x *Xray) BufferChanged(s string) {
// BufferCompleted indicates the buffer was changed.
func (x *Xray) BufferCompleted(s string) {
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.
func (x *Xray) BufferActive(state bool, k model.BufferKind) {
x.app.BufferActive(state, k)
@ -593,7 +596,7 @@ func (x *Xray) Stop() {
}
// SetBindKeysFn sets up extra key bindings.
func (x *Xray) SetBindKeysFn(BindKeysFunc) {}
func (x *Xray) AddBindKeysFn(BindKeysFunc) {}
// SetContextFn sets custom context.
func (x *Xray) SetContextFn(ContextFunc) {}