derailed 2020-02-05 21:10:27 -08:00
parent 7df87a80ab
commit f7badc4a2a
20 changed files with 236 additions and 116 deletions

View File

@ -0,0 +1,29 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.13.8
## 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 is as ever very much noticed and appreciated!
Also if you dig this tool, please 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)
---
### GH Sponsorships
WOOT!! Big Thank you to [Mark Baumann](https://github.com/mtreeman) for your contributions and support for K9s!
---
## Resolved Bugs/Features/PRs
* [Issue #523](https://github.com/derailed/k9s/issues/523)
* [Issue #522](https://github.com/derailed/k9s/issues/522)
* [Issue #521](https://github.com/derailed/k9s/issues/521)
---
<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

@ -3,6 +3,7 @@ package config
import ( import (
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
"sync"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
@ -20,16 +21,17 @@ type ShortNames map[string][]string
// Aliases represents a collection of aliases. // Aliases represents a collection of aliases.
type Aliases struct { type Aliases struct {
Alias Alias `yaml:"alias"` Alias Alias `yaml:"alias"`
mx sync.RWMutex
} }
// NewAliases return a new alias. // NewAliases return a new alias.
func NewAliases() Aliases { func NewAliases() *Aliases {
return Aliases{ return &Aliases{
Alias: make(Alias, 50), Alias: make(Alias, 50),
} }
} }
func (a Aliases) loadDefaults() { func (a *Aliases) loadDefaults() {
const ( const (
contexts = "contexts" contexts = "contexts"
portFwds = "portforwards" portFwds = "portforwards"
@ -39,6 +41,9 @@ func (a Aliases) loadDefaults() {
users = "users" users = "users"
) )
a.mx.Lock()
defer a.mx.Unlock()
a.Alias["dp"] = "apps/v1/deployments" a.Alias["dp"] = "apps/v1/deployments"
a.Alias["sec"] = "v1/secrets" a.Alias["sec"] = "v1/secrets"
a.Alias["jo"] = "batch/v1/jobs" a.Alias["jo"] = "batch/v1/jobs"
@ -80,19 +85,52 @@ func (a Aliases) loadDefaults() {
} }
// Load K9s aliases. // Load K9s aliases.
func (a Aliases) Load() error { func (a *Aliases) Load() error {
a.loadDefaults() a.loadDefaults()
return a.LoadAliases(K9sAlias) return a.LoadAliases(K9sAlias)
} }
// ShortNames return all shortnames.
func (a *Aliases) ShortNames() ShortNames {
a.mx.RLock()
defer a.mx.RUnlock()
m := make(ShortNames, len(a.Alias))
for alias, gvr := range a.Alias {
if _, ok := m[gvr]; ok {
m[gvr] = append(m[gvr], alias)
} else {
m[gvr] = []string{alias}
}
}
return m
}
// Clear remove all aliases.
func (a *Aliases) Clear() {
a.mx.Lock()
defer a.mx.Unlock()
for k := range a.Alias {
delete(a.Alias, k)
}
}
// Get retrieves an alias. // Get retrieves an alias.
func (a Aliases) Get(k string) (string, bool) { func (a *Aliases) Get(k string) (string, bool) {
a.mx.RLock()
defer a.mx.RUnlock()
v, ok := a.Alias[k] v, ok := a.Alias[k]
return v, ok return v, ok
} }
// Define declares a new alias. // Define declares a new alias.
func (a Aliases) Define(gvr string, aliases ...string) { func (a *Aliases) Define(gvr string, aliases ...string) {
a.mx.Lock()
defer a.mx.Unlock()
for _, alias := range aliases { for _, alias := range aliases {
if _, ok := a.Alias[alias]; ok { if _, ok := a.Alias[alias]; ok {
continue continue
@ -102,17 +140,20 @@ func (a Aliases) Define(gvr string, aliases ...string) {
} }
// LoadAliases loads alias from a given file. // LoadAliases loads alias from a given file.
func (a Aliases) LoadAliases(path string) error { func (a *Aliases) LoadAliases(path string) error {
f, err := ioutil.ReadFile(path) f, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
log.Warn().Err(err).Msgf("No custom aliases found") log.Warn().Err(err).Msgf("No custom aliases found")
return nil return nil
} }
var aa Aliases var aa *Aliases
if err := yaml.Unmarshal(f, &aa); err != nil { if err := yaml.Unmarshal(f, &aa); err != nil {
return err return err
} }
a.mx.Lock()
defer a.mx.Unlock()
for k, v := range aa.Alias { for k, v := range aa.Alias {
a.Alias[k] = v a.Alias[k] = v
} }
@ -121,13 +162,13 @@ func (a Aliases) LoadAliases(path string) error {
} }
// Save alias to disk. // Save alias to disk.
func (a Aliases) Save() error { func (a *Aliases) Save() error {
log.Debug().Msg("[Config] Saving Aliases...") log.Debug().Msg("[Config] Saving Aliases...")
return a.SaveAliases(K9sAlias) return a.SaveAliases(K9sAlias)
} }
// SaveAliases saves aliases to a given file. // SaveAliases saves aliases to a given file.
func (a Aliases) SaveAliases(path string) error { func (a *Aliases) SaveAliases(path string) error {
EnsurePath(path, DefaultDirMod) EnsurePath(path, DefaultDirMod)
cfg, err := yaml.Marshal(a) cfg, err := yaml.Marshal(a)
if err != nil { if err != nil {

View File

@ -3,6 +3,7 @@ package dao
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"sort" "sort"
"strings" "strings"
@ -18,7 +19,7 @@ var _ Accessor = (*Alias)(nil)
// Alias tracks standard and custom command aliases. // Alias tracks standard and custom command aliases.
type Alias struct { type Alias struct {
NonResource NonResource
config.Aliases *config.Aliases
} }
// NewAlias returns a new set of aliases. // NewAlias returns a new set of aliases.
@ -29,13 +30,6 @@ func NewAlias(f Factory) *Alias {
return &a return &a
} }
// Clear remove all aliases.
func (a *Alias) Clear() {
for k := range a.Alias {
delete(a.Alias, k)
}
}
// Check verifies an alias is defined for this command. // Check verifies an alias is defined for this command.
func (a *Alias) Check(cmd string) bool { func (a *Alias) Check(cmd string) bool {
_, ok := a.Aliases.Get(cmd) _, ok := a.Aliases.Get(cmd)
@ -43,22 +37,12 @@ func (a *Alias) Check(cmd string) bool {
} }
// List returns a collection of aliases. // List returns a collection of aliases.
// BOZO!! Already have aliases here. Refact!!
func (a *Alias) List(ctx context.Context, _ string) ([]runtime.Object, error) { func (a *Alias) List(ctx context.Context, _ string) ([]runtime.Object, error) {
a, ok := ctx.Value(internal.KeyAliases).(*Alias) aa, ok := ctx.Value(internal.KeyAliases).(*Alias)
if !ok { if !ok {
return nil, errors.New("no aliases found in context") return nil, fmt.Errorf("expecting *Alias but got %T", ctx.Value(internal.KeyAliases))
} }
m := aa.ShortNames()
m := make(config.ShortNames, len(a.Alias))
for alias, gvr := range a.Alias {
if _, ok := m[gvr]; ok {
m[gvr] = append(m[gvr], alias)
} else {
m[gvr] = []string{alias}
}
}
oo := make([]runtime.Object, 0, len(m)) oo := make([]runtime.Object, 0, len(m))
for gvr, aliases := range m { for gvr, aliases := range m {
sort.StringSlice(aliases).Sort() sort.StringSlice(aliases).Sort()
@ -84,7 +68,7 @@ func (a *Alias) Get(_ context.Context, _ string) (runtime.Object, error) {
// Ensure makes sure alias are loaded. // Ensure makes sure alias are loaded.
func (a *Alias) Ensure() (config.Alias, error) { func (a *Alias) Ensure() (config.Alias, error) {
if err := LoadResources(a.Factory); err != nil { if err := MetaAccess.LoadResources(a.Factory); err != nil {
return config.Alias{}, err return config.Alias{}, err
} }
return a.Alias, a.load() return a.Alias, a.load()
@ -95,8 +79,8 @@ func (a *Alias) load() error {
return err return err
} }
for _, gvr := range AllGVRs() { for _, gvr := range MetaAccess.AllGVRs() {
meta, err := MetaFor(gvr) meta, err := MetaAccess.MetaFor(gvr)
if err != nil { if err != nil {
return err return err
} }

View File

@ -33,7 +33,7 @@ func TestAliasList(t *testing.T) {
func makeAliases() *dao.Alias { func makeAliases() *dao.Alias {
return &dao.Alias{ return &dao.Alias{
Aliases: config.Aliases{ Aliases: &config.Aliases{
Alias: config.Alias{ Alias: config.Alias{
"fred": "v1/fred", "fred": "v1/fred",
"f": "v1/fred", "f": "v1/fred",

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"sync"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -13,7 +14,19 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
) )
var resMetas = ResourceMetas{} // MetaAccess tracks resources metadata.
var MetaAccess = NewMeta()
// Meta represents available resource metas.
type Meta struct {
resMetas ResourceMetas
mx sync.RWMutex
}
// NewMeta returns a resource meta.
func NewMeta() *Meta {
return &Meta{resMetas: make(ResourceMetas)}
}
// AccessorFor returns a client accessor for a resource if registered. // AccessorFor returns a client accessor for a resource if registered.
// Otherwise it returns a generic accessor. // Otherwise it returns a generic accessor.
@ -47,14 +60,20 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
} }
// RegisterMeta registers a new resource meta object. // RegisterMeta registers a new resource meta object.
func RegisterMeta(gvr string, res metav1.APIResource) { func (m *Meta) RegisterMeta(gvr string, res metav1.APIResource) {
resMetas[client.NewGVR(gvr)] = res m.mx.Lock()
defer m.mx.Unlock()
m.resMetas[client.NewGVR(gvr)] = res
} }
// AllGVRs returns all cluster resources. // AllGVRs returns all cluster resources.
func AllGVRs() client.GVRs { func (m *Meta) AllGVRs() client.GVRs {
kk := make(client.GVRs, 0, len(resMetas)) m.mx.RLock()
for k := range resMetas { defer m.mx.RUnlock()
kk := make(client.GVRs, 0, len(m.resMetas))
for k := range m.resMetas {
kk = append(kk, k) kk = append(kk, k)
} }
sort.Sort(kk) sort.Sort(kk)
@ -63,12 +82,15 @@ func AllGVRs() client.GVRs {
} }
// MetaFor returns a resource metadata for a given gvr. // MetaFor returns a resource metadata for a given gvr.
func MetaFor(gvr client.GVR) (metav1.APIResource, error) { func (m *Meta) MetaFor(gvr client.GVR) (metav1.APIResource, error) {
m, ok := resMetas[gvr] m.mx.RLock()
defer m.mx.RUnlock()
meta, ok := m.resMetas[gvr]
if !ok { if !ok {
return metav1.APIResource{}, fmt.Errorf("no resource meta defined for %q", gvr) return metav1.APIResource{}, fmt.Errorf("no resource meta defined for %q", gvr)
} }
return m, nil return meta, nil
} }
// IsK8sMeta checks for non resource meta. // IsK8sMeta checks for non resource meta.
@ -94,13 +116,16 @@ func IsK9sMeta(m metav1.APIResource) bool {
} }
// LoadResources hydrates server preferred+CRDs resource metadata. // LoadResources hydrates server preferred+CRDs resource metadata.
func LoadResources(f Factory) error { func (m *Meta) LoadResources(f Factory) error {
resMetas = make(ResourceMetas, 100) m.mx.Lock()
if err := loadPreferred(f, resMetas); err != nil { defer m.mx.Unlock()
m.resMetas = make(ResourceMetas, 100)
if err := loadPreferred(f, m.resMetas); err != nil {
return err return err
} }
loadNonResource(resMetas) loadNonResource(m.resMetas)
loadCRDs(f, resMetas) loadCRDs(f, m.resMetas)
return nil return nil
} }

View File

@ -15,6 +15,8 @@ import (
"github.com/sahilm/fuzzy" "github.com/sahilm/fuzzy"
) )
const logMaxBufferSize = 50
// 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.
@ -65,8 +67,10 @@ func (l *Log) Init(f dao.Factory) {
// Clear the logs. // Clear the logs.
func (l *Log) Clear() { func (l *Log) Clear() {
l.mx.Lock() l.mx.Lock()
defer l.mx.Unlock() {
l.lines, l.lastSent = []string{}, 0 l.lines, l.lastSent = []string{}, 0
}
l.mx.Unlock()
l.fireLogCleared() l.fireLogCleared()
} }
@ -74,6 +78,7 @@ func (l *Log) Clear() {
func (l *Log) Start() { func (l *Log) Start() {
if err := l.load(); err != nil { if err := l.load(); err != nil {
log.Error().Err(err).Msgf("Tail logs failed!") log.Error().Err(err).Msgf("Tail logs failed!")
l.fireLogError(err)
} }
} }
@ -91,6 +96,7 @@ func (l *Log) Stop() {
func (l *Log) Set(lines []string) { func (l *Log) Set(lines []string) {
l.mx.Lock() l.mx.Lock()
defer l.mx.Unlock() defer l.mx.Unlock()
l.lines = lines l.lines = lines
l.fireLogChanged(lines) l.fireLogChanged(lines)
} }
@ -99,6 +105,7 @@ func (l *Log) Set(lines []string) {
func (l *Log) ClearFilter() { func (l *Log) ClearFilter() {
l.mx.RLock() l.mx.RLock()
defer l.mx.RUnlock() defer l.mx.RUnlock()
l.filter = "" l.filter = ""
l.fireLogChanged(l.lines) l.fireLogChanged(l.lines)
} }
@ -133,7 +140,7 @@ func (l *Log) load() error {
} }
logger, ok := accessor.(dao.Loggable) logger, ok := accessor.(dao.Loggable)
if !ok { if !ok {
return fmt.Errorf("Resource %s is not tailable", l.gvr) return fmt.Errorf("Resource %s is not Loggable", l.gvr)
} }
if err := logger.TailLogs(ctx, c, l.logOptions); err != nil { if err := logger.TailLogs(ctx, c, l.logOptions); err != nil {
@ -152,6 +159,7 @@ func (l *Log) Append(line string) {
if line == "" { if line == "" {
return return
} }
l.mx.Lock() l.mx.Lock()
defer l.mx.Unlock() defer l.mx.Unlock()
@ -196,6 +204,15 @@ func (l *Log) updateLogs(ctx context.Context, c <-chan string) {
return return
} }
l.Append(line) l.Append(line)
var overflow bool
l.mx.RLock()
{
overflow = len(l.lines)-l.lastSent > logMaxBufferSize
}
l.mx.RUnlock()
if overflow {
l.Notify(true)
}
case <-time.After(200 * time.Millisecond): case <-time.After(200 * time.Millisecond):
l.Notify(true) l.Notify(true)
case <-ctx.Done(): case <-ctx.Done():

View File

@ -105,7 +105,7 @@ func TestLogStartStop(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, 1, v.errCalled)
assert.Equal(t, 2, len(v.data)) assert.Equal(t, 2, len(v.data))
} }

View File

@ -1,6 +1,8 @@
package model package model
import ( import (
"sync"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -40,6 +42,7 @@ type StackListener interface {
type Stack struct { type Stack struct {
components []Component components []Component
listeners []StackListener listeners []StackListener
mx sync.RWMutex
} }
// NewStack returns a new initialized stack. // NewStack returns a new initialized stack.
@ -49,6 +52,9 @@ func NewStack() *Stack {
// Flatten returns a string representation of the component stack. // Flatten returns a string representation of the component stack.
func (s *Stack) Flatten() []string { func (s *Stack) Flatten() []string {
s.mx.RLock()
defer s.mx.RUnlock()
ss := make([]string, len(s.components)) ss := make([]string, len(s.components))
for i, c := range s.components { for i, c := range s.components {
ss[i] = c.Name() ss[i] = c.Name()
@ -84,7 +90,12 @@ func (s *Stack) Push(c Component) {
if top := s.Top(); top != nil { if top := s.Top(); top != nil {
top.Stop() top.Stop()
} }
s.components = append(s.components, c)
s.mx.Lock()
{
s.components = append(s.components, c)
}
s.mx.Unlock()
s.notify(StackPush, c) s.notify(StackPush, c)
} }
@ -94,8 +105,13 @@ func (s *Stack) Pop() (Component, bool) {
return nil, false return nil, false
} }
c := s.components[s.size()] var c Component
s.components = s.components[:s.size()] s.mx.Lock()
{
c = s.components[s.size()]
s.components = s.components[:s.size()]
}
s.mx.Unlock()
s.notify(StackPop, c) s.notify(StackPop, c)
return c, true return c, true
@ -103,6 +119,9 @@ func (s *Stack) Pop() (Component, bool) {
// Peek returns stack state. // Peek returns stack state.
func (s *Stack) Peek() []Component { func (s *Stack) Peek() []Component {
s.mx.RLock()
defer s.mx.RUnlock()
return s.components return s.components
} }
@ -115,6 +134,9 @@ func (s *Stack) Clear() {
// Empty returns true if the stack is empty. // Empty returns true if the stack is empty.
func (s *Stack) Empty() bool { func (s *Stack) Empty() bool {
s.mx.RLock()
defer s.mx.RUnlock()
return len(s.components) == 0 return len(s.components) == 0
} }

View File

@ -3,6 +3,7 @@ package model
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -35,6 +36,7 @@ type Table struct {
inUpdate int32 inUpdate int32
refreshRate time.Duration refreshRate time.Duration
instance string instance string
mx sync.RWMutex
} }
// NewTable returns a new table model. // NewTable returns a new table model.
@ -170,7 +172,10 @@ func (t *Table) Empty() bool {
// Peek returns model data. // Peek returns model data.
func (t *Table) Peek() render.TableData { func (t *Table) Peek() render.TableData {
return *t.data t.mx.RLock()
defer t.mx.RUnlock()
return t.data.Clone()
} }
func (t *Table) updater(ctx context.Context) { func (t *Table) updater(ctx context.Context) {
@ -200,7 +205,7 @@ func (t *Table) refresh(ctx context.Context) {
t.fireTableLoadFailed(err) t.fireTableLoadFailed(err)
return return
} }
t.fireTableChanged(*t.data) t.fireTableChanged()
} }
func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, error) { func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, error) {
@ -252,15 +257,16 @@ func (t *Table) reconcile(ctx context.Context) error {
} }
} }
t.data.Mutex.Lock() t.mx.Lock()
defer t.data.Mutex.Unlock() defer t.mx.Unlock()
// if labelSelector in place might as well clear the model data. // if labelSelector in place might as well clear the model data.
sel, ok := ctx.Value(internal.KeyLabels).(string) sel, ok := ctx.Value(internal.KeyLabels).(string)
if ok && sel != "" { if ok && sel != "" {
t.data.Clear() t.data.Clear()
} }
t.data.Update(rows) t.data.Update(rows)
t.data.Namespace, t.data.Header = t.namespace, meta.Renderer.Header(t.namespace) t.data.SetHeader(t.namespace, meta.Renderer.Header(t.namespace))
return nil return nil
} }
@ -292,7 +298,8 @@ func (t *Table) resourceMeta() ResourceMeta {
return meta return meta
} }
func (t *Table) fireTableChanged(data render.TableData) { func (t *Table) fireTableChanged() {
data := t.Peek()
for _, l := range t.listeners { for _, l := range t.listeners {
l.TableDataChanged(data) l.TableDataChanged(data)
} }

View File

@ -1,20 +1,15 @@
package render package render
import (
"sync"
)
// TableData tracks a K8s resource for tabular display. // TableData tracks a K8s resource for tabular display.
type TableData struct { type TableData struct {
Header HeaderRow Header HeaderRow
RowEvents RowEvents RowEvents RowEvents
Namespace string Namespace string
Mutex *sync.RWMutex
} }
// NewTableData returns a new table. // NewTableData returns a new table.
func NewTableData() *TableData { func NewTableData() *TableData {
return &TableData{Mutex: &sync.RWMutex{}} return &TableData{}
} }
// Clear clears out the entire table. // Clear clears out the entire table.
@ -31,13 +26,18 @@ func cloneTable(t TableData) TableData {
return t return t
} }
// SetHeader sets table header.
func (t *TableData) SetHeader(ns string, h HeaderRow) {
t.Namespace, t.Header = ns, h
}
// Update computes row deltas and update the table data. // Update computes row deltas and update the table data.
func (t *TableData) Update(rows Rows) { func (t *TableData) Update(rows Rows) {
empty := len(t.RowEvents) == 0 empty := len(t.RowEvents) == 0
kk := make([]string, 0, len(rows)) kk := make(map[string]struct{}, len(rows))
var blankDelta DeltaRow var blankDelta DeltaRow
for _, row := range rows { for _, row := range rows {
kk = append(kk, row.ID) kk[row.ID] = struct{}{}
if empty { if empty {
t.RowEvents = append(t.RowEvents, NewRowEvent(EventAdd, row)) t.RowEvents = append(t.RowEvents, NewRowEvent(EventAdd, row))
continue continue
@ -61,19 +61,11 @@ func (t *TableData) Update(rows Rows) {
} }
} }
// Delete delete items in cache that are no longer valid. // Delete removes items in cache that are no longer valid.
func (t *TableData) Delete(newKeys []string) { func (t *TableData) Delete(newKeys map[string]struct{}) {
var victims []string var victims []string
for _, re := range t.RowEvents { for _, re := range t.RowEvents {
var found bool if _, ok := newKeys[re.Row.ID]; !ok {
for i, key := range newKeys {
if key == re.Row.ID {
found = true
newKeys = append(newKeys[:i], newKeys[i+1:]...)
break
}
}
if !found {
victims = append(victims, re.Row.ID) victims = append(victims, re.Row.ID)
} }
} }
@ -88,12 +80,10 @@ func (t *TableData) Diff(table TableData) bool {
if t.Namespace != table.Namespace { if t.Namespace != table.Namespace {
return true return true
} }
if t.Header.Diff(table.Header) { if t.Header.Diff(table.Header) {
return true return true
} }
if t.RowEvents.Diff(table.RowEvents) {
return true
}
return false return t.RowEvents.Diff(table.RowEvents)
} }

View File

@ -10,7 +10,7 @@ import (
func TestTableDataDelete(t *testing.T) { func TestTableDataDelete(t *testing.T) {
uu := map[string]struct { uu := map[string]struct {
re render.RowEvents re render.RowEvents
kk []string kk map[string]struct{}
e render.RowEvents e render.RowEvents
}{ }{
"ordered": { "ordered": {
@ -19,7 +19,7 @@ func TestTableDataDelete(t *testing.T) {
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
}, },
kk: []string{"A", "C"}, kk: map[string]struct{}{"A": struct{}{}, "C": struct{}{}},
e: render.RowEvents{ e: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
@ -32,7 +32,7 @@ func TestTableDataDelete(t *testing.T) {
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
{Row: render.Row{ID: "D", Fields: render.Fields{"10", "2", "3"}}}, {Row: render.Row{ID: "D", Fields: render.Fields{"10", "2", "3"}}},
}, },
kk: []string{"C", "A"}, kk: map[string]struct{}{"C": struct{}{}, "A": struct{}{}},
e: render.RowEvents{ e: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},

View File

@ -163,9 +163,6 @@ func (t *Table) SetSortCol(index, count int, asc bool) {
// Update table content. // Update table content.
func (t *Table) Update(data render.TableData) { func (t *Table) Update(data render.TableData) {
data.Mutex.RLock()
defer data.Mutex.RUnlock()
if t.decorateFn != nil { if t.decorateFn != nil {
data = t.decorateFn(data) data = t.decorateFn(data)
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"sync"
"time" "time"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
@ -40,6 +41,7 @@ type App struct {
cancelFn context.CancelFunc cancelFn context.CancelFunc
conRetry int conRetry int
clusterModel *model.ClusterInfo clusterModel *model.ClusterInfo
mx sync.Mutex
} }
// NewApp returns a K9s app instance. // NewApp returns a K9s app instance.
@ -59,6 +61,8 @@ func NewApp(cfg *config.Config) *App {
// ConOK checks the connection is cool, returns false otherwise. // ConOK checks the connection is cool, returns false otherwise.
func (a *App) ConOK() bool { func (a *App) ConOK() bool {
a.mx.Lock()
defer a.mx.Unlock()
return a.conRetry == 0 return a.conRetry == 0
} }
@ -194,6 +198,9 @@ func (a *App) clusterUpdater(ctx context.Context) {
} }
func (a *App) refreshCluster() { func (a *App) refreshCluster() {
a.mx.Lock()
defer a.mx.Unlock()
c := a.Content.Top() c := a.Content.Top()
if ok := a.Conn().CheckConnectivity(); ok { if ok := a.Conn().CheckConnectivity(); ok {
if a.conRetry > 0 { if a.conRetry > 0 {

View File

@ -40,7 +40,7 @@ func NewBrowser(gvr client.GVR) ResourceViewer {
// Init watches all running pods in given namespace // Init watches all running pods in given namespace
func (b *Browser) Init(ctx context.Context) error { func (b *Browser) Init(ctx context.Context) error {
var err error var err error
b.meta, err = dao.MetaFor(b.gvr) b.meta, err = dao.MetaAccess.MetaFor(b.gvr)
if err != nil { if err != nil {
return err return err
} }
@ -55,6 +55,7 @@ func (b *Browser) Init(ctx context.Context) error {
return e return e
} }
} }
b.app.CmdBuff().Reset()
b.bindKeys() b.bindKeys()
if b.bindKeysFn != nil { if b.bindKeysFn != nil {

View File

@ -69,11 +69,6 @@ func (c *Container) selectedContainer() string {
} }
func (c *Container) viewLogs(app *App, model ui.Tabular, gvr, path string) { func (c *Container) viewLogs(app *App, model ui.Tabular, gvr, path string) {
status := c.GetTable().GetSelectedCell(3)
if status != "Running" && status != "Completed" {
app.Flash().Err(errors.New("No logs available"))
return
}
c.ResourceViewer.(*LogsExtender).showLogs(c.GetTable().Path, false) c.ResourceViewer.(*LogsExtender).showLogs(c.GetTable().Path, false)
} }

View File

@ -103,7 +103,9 @@ func (l *Log) LogCleared() {
// LogFailed notifies an error occurred. // LogFailed notifies an error occurred.
func (l *Log) LogFailed(err error) { func (l *Log) LogFailed(err error) {
l.app.Flash().Err(err) l.app.QueueUpdateDraw(func() {
l.app.Flash().Err(err)
})
} }
// LogChanged updates the logs. // LogChanged updates the logs.

View File

@ -11,7 +11,7 @@ import (
) )
func init() { func init() {
dao.RegisterMeta("v1/pods", metav1.APIResource{ dao.MetaAccess.RegisterMeta("v1/pods", metav1.APIResource{
Name: "pods", Name: "pods",
SingularName: "pod", SingularName: "pod",
Namespaced: true, Namespaced: true,
@ -19,7 +19,7 @@ func init() {
Verbs: []string{"get", "list", "watch", "delete"}, Verbs: []string{"get", "list", "watch", "delete"},
Categories: []string{"k9s"}, Categories: []string{"k9s"},
}) })
dao.RegisterMeta("v1/namespaces", metav1.APIResource{ dao.MetaAccess.RegisterMeta("v1/namespaces", metav1.APIResource{
Name: "namespaces", Name: "namespaces",
SingularName: "namespace", SingularName: "namespace",
Namespaced: true, Namespaced: true,
@ -27,7 +27,7 @@ func init() {
Verbs: []string{"get", "list", "watch", "delete"}, Verbs: []string{"get", "list", "watch", "delete"},
Categories: []string{"k9s"}, Categories: []string{"k9s"},
}) })
dao.RegisterMeta("v1/services", metav1.APIResource{ dao.MetaAccess.RegisterMeta("v1/services", metav1.APIResource{
Name: "services", Name: "services",
SingularName: "service", SingularName: "service",
Namespaced: true, Namespaced: true,
@ -35,7 +35,7 @@ func init() {
Verbs: []string{"get", "list", "watch", "delete"}, Verbs: []string{"get", "list", "watch", "delete"},
Categories: []string{"k9s"}, Categories: []string{"k9s"},
}) })
dao.RegisterMeta("v1/secrets", metav1.APIResource{ dao.MetaAccess.RegisterMeta("v1/secrets", metav1.APIResource{
Name: "secrets", Name: "secrets",
SingularName: "secret", SingularName: "secret",
Namespaced: true, Namespaced: true,
@ -44,7 +44,7 @@ func init() {
Categories: []string{"k9s"}, Categories: []string{"k9s"},
}) })
dao.RegisterMeta("aliases", metav1.APIResource{ dao.MetaAccess.RegisterMeta("aliases", metav1.APIResource{
Name: "aliases", Name: "aliases",
SingularName: "alias", SingularName: "alias",
Namespaced: true, Namespaced: true,
@ -52,7 +52,7 @@ func init() {
Verbs: []string{"get", "list", "watch", "delete"}, Verbs: []string{"get", "list", "watch", "delete"},
Categories: []string{"k9s"}, Categories: []string{"k9s"},
}) })
dao.RegisterMeta("containers", metav1.APIResource{ dao.MetaAccess.RegisterMeta("containers", metav1.APIResource{
Name: "containers", Name: "containers",
SingularName: "container", SingularName: "container",
Namespaced: true, Namespaced: true,
@ -60,7 +60,7 @@ func init() {
Verbs: []string{"get", "list", "watch", "delete"}, Verbs: []string{"get", "list", "watch", "delete"},
Categories: []string{"k9s"}, Categories: []string{"k9s"},
}) })
dao.RegisterMeta("contexts", metav1.APIResource{ dao.MetaAccess.RegisterMeta("contexts", metav1.APIResource{
Name: "contexts", Name: "contexts",
SingularName: "context", SingularName: "context",
Namespaced: true, Namespaced: true,
@ -68,7 +68,7 @@ func init() {
Verbs: []string{"get", "list", "watch", "delete"}, Verbs: []string{"get", "list", "watch", "delete"},
Categories: []string{"k9s"}, Categories: []string{"k9s"},
}) })
dao.RegisterMeta("subjects", metav1.APIResource{ dao.MetaAccess.RegisterMeta("subjects", metav1.APIResource{
Name: "subjects", Name: "subjects",
SingularName: "subject", SingularName: "subject",
Namespaced: true, Namespaced: true,
@ -76,7 +76,7 @@ func init() {
Verbs: []string{"get", "list", "watch", "delete"}, Verbs: []string{"get", "list", "watch", "delete"},
Categories: []string{"k9s"}, Categories: []string{"k9s"},
}) })
dao.RegisterMeta("rbac", metav1.APIResource{ dao.MetaAccess.RegisterMeta("rbac", metav1.APIResource{
Name: "rbacs", Name: "rbacs",
SingularName: "rbac", SingularName: "rbac",
Namespaced: true, Namespaced: true,
@ -84,7 +84,7 @@ func init() {
Verbs: []string{"get", "list", "watch", "delete"}, Verbs: []string{"get", "list", "watch", "delete"},
Categories: []string{"k9s"}, Categories: []string{"k9s"},
}) })
dao.RegisterMeta("portforwards", metav1.APIResource{ dao.MetaAccess.RegisterMeta("portforwards", metav1.APIResource{
Name: "portforwards", Name: "portforwards",
SingularName: "portforward", SingularName: "portforward",
Namespaced: true, Namespaced: true,
@ -93,7 +93,7 @@ func init() {
Categories: []string{"k9s"}, Categories: []string{"k9s"},
}) })
dao.RegisterMeta("screendumps", metav1.APIResource{ dao.MetaAccess.RegisterMeta("screendumps", metav1.APIResource{
Name: "screendumps", Name: "screendumps",
SingularName: "screendump", SingularName: "screendump",
Namespaced: true, Namespaced: true,
@ -101,7 +101,7 @@ func init() {
Verbs: []string{"get", "list", "watch", "delete"}, Verbs: []string{"get", "list", "watch", "delete"},
Categories: []string{"k9s"}, Categories: []string{"k9s"},
}) })
dao.RegisterMeta("apps/v1/statefulsets", metav1.APIResource{ dao.MetaAccess.RegisterMeta("apps/v1/statefulsets", metav1.APIResource{
Name: "statefulsets", Name: "statefulsets",
SingularName: "statefulset", SingularName: "statefulset",
Namespaced: true, Namespaced: true,
@ -109,7 +109,7 @@ func init() {
Verbs: []string{"get", "list", "watch", "delete"}, Verbs: []string{"get", "list", "watch", "delete"},
Categories: []string{"k9s"}, Categories: []string{"k9s"},
}) })
dao.RegisterMeta("apps/v1/daemonsets", metav1.APIResource{ dao.MetaAccess.RegisterMeta("apps/v1/daemonsets", metav1.APIResource{
Name: "daemonsets", Name: "daemonsets",
SingularName: "daemonset", SingularName: "daemonset",
Namespaced: true, Namespaced: true,
@ -117,7 +117,7 @@ func init() {
Verbs: []string{"get", "list", "watch", "delete"}, Verbs: []string{"get", "list", "watch", "delete"},
Categories: []string{"k9s"}, Categories: []string{"k9s"},
}) })
dao.RegisterMeta("apps/v1/deployments", metav1.APIResource{ dao.MetaAccess.RegisterMeta("apps/v1/deployments", metav1.APIResource{
Name: "deployments", Name: "deployments",
SingularName: "deployment", SingularName: "deployment",
Namespaced: true, Namespaced: true,

View File

@ -56,7 +56,7 @@ func (x *Xray) Init(ctx context.Context) error {
x.SetKeyListenerFn(x.keyEntered) x.SetKeyListenerFn(x.keyEntered)
var err error var err error
x.meta, err = dao.MetaFor(x.gvr) x.meta, err = dao.MetaAccess.MetaFor(x.gvr)
if err != nil { if err != nil {
return err return err
} }
@ -138,7 +138,7 @@ func (x *Xray) refreshActions() {
} }
var err error var err error
x.meta, err = dao.MetaFor(client.NewGVR(ref.GVR)) x.meta, err = dao.MetaAccess.MetaFor(client.NewGVR(ref.GVR))
if err != nil { if err != nil {
log.Warn().Msgf("NO meta for %q -- %s", ref.GVR, err) log.Warn().Msgf("NO meta for %q -- %s", ref.GVR, err)
return return
@ -288,7 +288,7 @@ func (x *Xray) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
defer x.Start() defer x.Start()
{ {
gvr := client.NewGVR(ref.GVR) gvr := client.NewGVR(ref.GVR)
meta, err := dao.MetaFor(gvr) meta, err := dao.MetaAccess.MetaFor(gvr)
if err != nil { if err != nil {
log.Warn().Msgf("NO meta for %q -- %s", ref.GVR, err) log.Warn().Msgf("NO meta for %q -- %s", ref.GVR, err)
return nil return nil

View File

@ -150,6 +150,9 @@ func (f *Factory) SetActiveNS(ns string) {
} }
func (f *Factory) isClusterWide() bool { func (f *Factory) isClusterWide() bool {
f.mx.RLock()
defer f.mx.RUnlock()
_, ok := f.factories[client.AllNamespaces] _, ok := f.factories[client.AllNamespaces]
return ok return ok
} }

View File

@ -336,7 +336,7 @@ func dumpStdOut(n *TreeNode, level int) {
} }
func category(gvr string) string { func category(gvr string) string {
meta, err := dao.MetaFor(client.NewGVR(gvr)) meta, err := dao.MetaAccess.MetaFor(client.NewGVR(gvr))
if err != nil { if err != nil {
return "" return ""
} }