release v0.25.13 (#1383)

mine
Fernand Galiana 2021-12-16 09:30:10 -07:00 committed by GitHub
parent 01bdc85020
commit fdc638c5d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 259 additions and 133 deletions

View File

@ -5,7 +5,7 @@ PACKAGE := github.com/derailed/$(NAME)
GIT_REV ?= $(shell git rev-parse --short HEAD) GIT_REV ?= $(shell git rev-parse --short HEAD)
SOURCE_DATE_EPOCH ?= $(shell date +%s) SOURCE_DATE_EPOCH ?= $(shell date +%s)
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ") DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
VERSION ?= v0.25.12 VERSION ?= v0.25.13
IMG_NAME := derailed/k9s IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION} IMAGE := ${IMG_NAME}:${VERSION}

View File

@ -0,0 +1,46 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.25.13
## 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 [sponsorship 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)
### A Word From Our Sponsors...
I want to recognize the following folks that have been kind enough to join our sponsorship program and opted to `pay it forward`!
* [uderik](https://github.com/uderik)
* [Daimler](https://github.com/Daimler) wOOt!! Mercedes Benz sponsorship! How cool is that?
So if you feel K9s is helping with your productivity while administering your Kubernetes clusters, please consider pitching in as it will go a long way in ensuring a thriving environment for this repo and our k9ers community at large.
Also please take some time and give a huge shoot out to all the good folks below that have spent time plowing thru the code to help improve K9s for all of us!
Thank you!!
---
## ♫ Sounds Behind The Release ♭
* [Gash Dem - Chuck Fenda](https://www.youtube.com/watch?v=Y4NSYW4wusI)
## Maintenance Release!
---
## Resolved Issues
* [Issue #1382](https://github.com/derailed/k9s/issues/1382) Watcher failed for screendumps
* [Issue #1381](https://github.com/derailed/k9s/issues/1381) --request-timeout affects logs streaming
* [Issue #1380](https://github.com/derailed/k9s/issues/1380) :pulse returning error: expecting a TableRow but got *v1.Table
* [Issue #1376](https://github.com/derailed/k9s/issues/1376) Events are not sorted correctly by dates - with feelings...
* [Issue #1291](https://github.com/derailed/k9s/issues/1291) K9s do not show any error when is unable to get logs, just do not show anything.
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -121,7 +121,7 @@ func loadConfiguration() *config.Config {
} }
// Try to access server version if that fail. Connectivity issue? // Try to access server version if that fail. Connectivity issue?
if !k9sCfg.GetConnection().CheckConnectivity() { if !k9sCfg.GetConnection().CheckConnectivity() {
log.Panic().Msgf("Cannot connect to cluster") log.Panic().Msgf("Cannot connect to cluster %s", k9sCfg.K9s.CurrentCluster)
} }
if !k9sCfg.GetConnection().ConnectionOK() { if !k9sCfg.GetConnection().ConnectionOK() {
panic("No connectivity") panic("No connectivity")

View File

@ -34,7 +34,7 @@ var supportedMetricsAPIVersions = []string{"v1beta1"}
// APIClient represents a Kubernetes api client. // APIClient represents a Kubernetes api client.
type APIClient struct { type APIClient struct {
client kubernetes.Interface client, logClient kubernetes.Interface
dClient dynamic.Interface dClient dynamic.Interface
nsClient dynamic.NamespaceableResourceInterface nsClient dynamic.NamespaceableResourceInterface
mxsClient *versioned.Clientset mxsClient *versioned.Clientset
@ -279,6 +279,27 @@ func (a *APIClient) HasMetrics() bool {
return err == nil return err == nil
} }
// LogDial returns a handle to api server for logs.
func (a *APIClient) DialLogs() (kubernetes.Interface, error) {
if !a.connOK {
return nil, errors.New("No connection to dial")
}
if a.logClient != nil {
return a.logClient, nil
}
cfg, err := a.RestConfig()
if err != nil {
return nil, err
}
cfg.Timeout = 0
if a.logClient, err = kubernetes.NewForConfig(cfg); err != nil {
return nil, err
}
return a.logClient, nil
}
// Dial returns a handle to api server or die. // Dial returns a handle to api server or die.
func (a *APIClient) Dial() (kubernetes.Interface, error) { func (a *APIClient) Dial() (kubernetes.Interface, error) {
if !a.connOK { if !a.connOK {
@ -393,7 +414,7 @@ func (a *APIClient) reset() {
a.config.reset() a.config.reset()
a.cache = cache.NewLRUExpireCache(cacheSize) a.cache = cache.NewLRUExpireCache(cacheSize)
a.client, a.dClient, a.nsClient, a.mxsClient = nil, nil, nil, nil a.client, a.dClient, a.nsClient, a.mxsClient = nil, nil, nil, nil
a.cachedClient = nil a.cachedClient, a.logClient = nil, nil
a.connOK = true a.connOK = true
} }

View File

@ -31,7 +31,6 @@ type Config struct {
func NewConfig(f *genericclioptions.ConfigFlags) *Config { func NewConfig(f *genericclioptions.ConfigFlags) *Config {
return &Config{ return &Config{
flags: f, flags: f,
// pathOptions: clientcmd.NewDefaultPathOptions(),
mutex: &sync.RWMutex{}, mutex: &sync.RWMutex{},
} }
} }

View File

@ -85,9 +85,12 @@ type Connection interface {
// ConnectionOK checks api server connection status. // ConnectionOK checks api server connection status.
ConnectionOK() bool ConnectionOK() bool
// DialOrDie connects to api server. // Dial connects to api server.
Dial() (kubernetes.Interface, error) Dial() (kubernetes.Interface, error)
// DialLogs connects to api server for logs.
DialLogs() (kubernetes.Interface, error)
// SwitchContext switches cluster based on context. // SwitchContext switches cluster based on context.
SwitchContext(ctx string) error SwitchContext(ctx string) error

View File

@ -92,6 +92,10 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, c
c.K9s.CurrentCluster = context.Cluster c.K9s.CurrentCluster = context.Cluster
c.K9s.ActivateCluster() c.K9s.ActivateCluster()
var cns string
if cl := c.K9s.ActiveCluster(); cl != nil && cl.Namespace != nil {
cns = cl.Namespace.Active
}
var ns string var ns string
if k9sFlags != nil && IsBoolSet(k9sFlags.AllNamespaces) { if k9sFlags != nil && IsBoolSet(k9sFlags.AllNamespaces) {
ns = client.NamespaceAll ns = client.NamespaceAll
@ -99,11 +103,11 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, c
ns = *flags.Namespace ns = *flags.Namespace
} else if context.Namespace != "" { } else if context.Namespace != "" {
ns = context.Namespace ns = context.Namespace
if cl := c.K9s.ActiveCluster(); cl != nil && cl.Namespace != nil && cl.Namespace.Active != "" { if cns != "" {
ns = cl.Namespace.Active ns = cns
} }
} else if cl := c.K9s.ActiveCluster(); cl != nil && cl.Namespace != nil { } else {
ns = cl.Namespace.Active ns = cns
} }
if ns != "" { if ns != "" {
@ -112,11 +116,9 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, c
} }
flags.Namespace = &ns flags.Namespace = &ns
} }
if isSet(flags.ClusterName) { if isSet(flags.ClusterName) {
c.K9s.CurrentCluster = *flags.ClusterName c.K9s.CurrentCluster = *flags.ClusterName
} }
EnsurePath(c.K9s.GetScreenDumpDir(), DefaultDirMod) EnsurePath(c.K9s.GetScreenDumpDir(), DefaultDirMod)
return nil return nil

View File

@ -164,6 +164,25 @@ func (mock *MockConnection) Dial() (kubernetes.Interface, error) {
return ret0, ret1 return ret0, ret1
} }
func (mock *MockConnection) DialLogs() (kubernetes.Interface, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("DialLogs", params, []reflect.Type{reflect.TypeOf((*kubernetes.Interface)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 kubernetes.Interface
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(kubernetes.Interface)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockConnection) DynDial() (dynamic.Interface, error) { func (mock *MockConnection) DynDial() (dynamic.Interface, error) {
if mock == nil { if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().") panic("mock must not be nil. Use myMock := NewMockConnection().")

View File

@ -22,7 +22,7 @@ type Namespace struct {
func NewNamespace() *Namespace { func NewNamespace() *Namespace {
return &Namespace{ return &Namespace{
Active: defaultNS, Active: defaultNS,
Favorites: []string{defaultNS}, Favorites: []string{"default"},
} }
} }

View File

@ -44,6 +44,7 @@ func makeConn() *conn {
func (c *conn) Config() *client.Config { return nil } func (c *conn) Config() *client.Config { return nil }
func (c *conn) Dial() (kubernetes.Interface, error) { return nil, nil } func (c *conn) Dial() (kubernetes.Interface, error) { return nil, nil }
func (c *conn) DialLogs() (kubernetes.Interface, error) { return nil, nil }
func (c *conn) ConnectionOK() bool { return true } func (c *conn) ConnectionOK() bool { return true }
func (c *conn) SwitchContext(ctx string) error { return nil } func (c *conn) SwitchContext(ctx string) error { return nil }
func (c *conn) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { return nil, nil } func (c *conn) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { return nil, nil }

View File

@ -19,6 +19,15 @@ var (
fuzzyRx = regexp.MustCompile(`\A\-f`) fuzzyRx = regexp.MustCompile(`\A\-f`)
) )
func inList(ll []string, s string) bool {
for _, l := range ll {
if l == s {
return true
}
}
return false
}
// IsInverseSelector checks if inverse char has been provided. // IsInverseSelector checks if inverse char has been provided.
func IsInverseSelector(s string) bool { func IsInverseSelector(s string) bool {
if s == "" { if s == "" {

View File

@ -14,6 +14,7 @@ type LogItem struct {
Pod, Container string Pod, Container string
SingleContainer bool SingleContainer bool
Bytes []byte Bytes []byte
IsError bool
} }
// NewLogItem returns a new item. // NewLogItem returns a new item.
@ -66,7 +67,7 @@ func (l *LogItem) Size() int {
func (l *LogItem) Render(paint string, showTime bool, bb *bytes.Buffer) { func (l *LogItem) Render(paint string, showTime bool, bb *bytes.Buffer) {
index := bytes.Index(l.Bytes, []byte{' '}) index := bytes.Index(l.Bytes, []byte{' '})
if showTime && index > 0 { if showTime && index > 0 {
bb.WriteString("[gray::]") bb.WriteString("[gray::b]")
bb.Write(l.Bytes[:index]) bb.Write(l.Bytes[:index])
bb.WriteString(" ") bb.WriteString(" ")
for i := len(l.Bytes[:index]); i < 30; i++ { for i := len(l.Bytes[:index]); i < 30; i++ {

View File

@ -63,7 +63,7 @@ func TestLogItemRender(t *testing.T) {
ShowTimestamp: true, ShowTimestamp: true,
}, },
log: fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3..."), log: fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3..."),
e: "[gray::]2018-12-14T10:36:43.326972-07:00 [yellow::]fred [yellow::b]blee[-::-] Testing 1,2,3...\n", e: "[gray::b]2018-12-14T10:36:43.326972-07:00 [yellow::]fred [yellow::b]blee[-::-] Testing 1,2,3...\n",
}, },
"log-level": { "log-level": {
opts: dao.LogOptions{ opts: dao.LogOptions{

View File

@ -108,7 +108,7 @@ func TestLogItemsRender(t *testing.T) {
Container: "blee", Container: "blee",
ShowTimestamp: true, ShowTimestamp: true,
}, },
e: "[gray::]2018-12-14T10:36:43.326972-07:00 [teal::]fred [teal::b]blee[-::-] Testing 1,2,3...\n", e: "[gray::b]2018-12-14T10:36:43.326972-07:00 [teal::]fred [teal::b]blee[-::-] Testing 1,2,3...\n",
}, },
} }

View File

@ -28,7 +28,10 @@ type LogOptions struct {
// Info returns the option pod and container info. // Info returns the option pod and container info.
func (o *LogOptions) Info() string { func (o *LogOptions) Info() string {
return fmt.Sprintf("%q::%q", o.Path, o.Container) if len(o.Container) != 0 {
return fmt.Sprintf("%s (%s)", o.Path, o.Container)
}
return o.Path
} }
// Clone clones options. // Clone clones options.
@ -109,8 +112,8 @@ func (o *LogOptions) ToPodLogOptions() *v1.PodLogOptions {
return &opts return &opts
} }
// DecorateLog add a log header to display po/co information along with the log message. // ToLogItem add a log header to display po/co information along with the log message.
func (o *LogOptions) DecorateLog(bytes []byte) *LogItem { func (o *LogOptions) ToLogItem(bytes []byte) *LogItem {
item := NewLogItem(bytes) item := NewLogItem(bytes)
if len(bytes) == 0 { if len(bytes) == 0 {
return item return item
@ -128,3 +131,10 @@ func (o *LogOptions) DecorateLog(bytes []byte) *LogItem {
return item return item
} }
func (o *LogOptions) ToErrLogItem(err error) *LogItem {
t := time.Now().UTC().Format(time.RFC3339Nano)
item := NewLogItem([]byte(fmt.Sprintf("%s [red::b]%s\n", t, err)))
item.IsError = true
return item
}

View File

@ -125,7 +125,7 @@ func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, er
return nil, fmt.Errorf("user is not authorized to view pod logs") return nil, fmt.Errorf("user is not authorized to view pod logs")
} }
dial, err := p.Client().Dial() dial, err := p.Client().DialLogs()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -178,7 +178,6 @@ func (p *Pod) GetInstance(fqn string) (*v1.Pod, error) {
// TailLogs tails a given container logs. // TailLogs tails a given container logs.
func (p *Pod) TailLogs(ctx context.Context, out LogChan, opts *LogOptions) error { func (p *Pod) TailLogs(ctx context.Context, out LogChan, opts *LogOptions) error {
log.Debug().Msgf("TAIL-LOGS for %q:%q", opts.Path, opts.Container)
fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
if !ok { if !ok {
return errors.New("No factory in context") return errors.New("No factory in context")
@ -334,28 +333,31 @@ func tailLogs(ctx context.Context, logger Logger, out LogChan, opts *LogOptions)
) )
o := opts.ToPodLogOptions() o := opts.ToPodLogOptions()
log.Debug().Msgf("TAIL_LOGS! %#v", o)
done: done:
for r := 0; r < logRetryCount; r++ { for r := 0; r < logRetryCount; r++ {
var e error
req, err = logger.Logs(opts.Path, o) req, err = logger.Logs(opts.Path, o)
if err == nil { if err == nil {
// This call will block if nothing is in the stream!! // This call will block if nothing is in the stream!!
if stream, err = req.Stream(ctx); err == nil { if stream, err = req.Stream(ctx); err == nil {
go readLogs(ctx, stream, out, opts) go readLogs(ctx, stream, out, opts)
break break
} else {
log.Error().Err(err).Msg("Streaming logs")
} }
e = fmt.Errorf("stream logs failed %w for %s", err, opts.Info())
log.Error().Err(e).Msg("logs-stream")
} else { } else {
log.Error().Err(err).Msg("Requesting logs") e = fmt.Errorf("stream logs failed %w for %s", err, opts.Info())
log.Error().Err(e).Msg("log-request")
} }
select { select {
case <-ctx.Done(): case <-ctx.Done():
log.Debug().Msgf("!!!!TAIL_LOGS CANCELED!!!!")
err = ctx.Err() err = ctx.Err()
break done break done
default: default:
if e != nil {
out <- opts.ToErrLogItem(e)
}
time.Sleep(logRetryWait) time.Sleep(logRetryWait)
} }
} }
@ -365,7 +367,6 @@ done:
func readLogs(ctx context.Context, stream io.ReadCloser, c LogChan, opts *LogOptions) { func readLogs(ctx context.Context, stream io.ReadCloser, c LogChan, opts *LogOptions) {
defer func() { defer func() {
log.Debug().Msgf("READ_LOGS BAILED!!!")
if err := stream.Close(); err != nil { if err := stream.Close(); err != nil {
log.Error().Err(err).Msgf("Fail to close stream %s", opts.Info()) log.Error().Err(err).Msgf("Fail to close stream %s", opts.Info())
} }
@ -374,20 +375,27 @@ func readLogs(ctx context.Context, stream io.ReadCloser, c LogChan, opts *LogOpt
log.Debug().Msgf("READ_LOGS PROCESSING %#v", opts) log.Debug().Msgf("READ_LOGS PROCESSING %#v", opts)
r := bufio.NewReader(stream) r := bufio.NewReader(stream)
for { for {
var item *LogItem
bytes, err := r.ReadBytes('\n') bytes, err := r.ReadBytes('\n')
if err != nil { if err == nil {
item = opts.ToLogItem(bytes)
} else {
if errors.Is(err, io.EOF) { if errors.Is(err, io.EOF) {
log.Warn().Err(err).Msgf("Stream closed for %s", opts.Info()) e := fmt.Errorf("Stream closed %w for %s", err, opts.Info())
// c <- ItemEOF item = opts.ToErrLogItem(e)
return log.Warn().Err(e).Msgf("stream closed")
} else {
e := fmt.Errorf("Stream failed %w for %s", err, opts.Info())
item = opts.ToErrLogItem(e)
log.Warn().Err(e).Msgf("stream read failed")
} }
log.Warn().Err(err).Msgf("Stream READ error %s", opts.Info())
return
} }
select { select {
case c <- opts.DecorateLog(bytes): case c <- item:
if item.IsError {
return
}
case <-ctx.Done(): case <-ctx.Done():
log.Debug().Msgf("READER CANCELED")
return return
} }
} }
@ -416,34 +424,8 @@ func extractFQN(o runtime.Object) string {
log.Error().Err(fmt.Errorf("expecting unstructured but got %T", o)) log.Error().Err(fmt.Errorf("expecting unstructured but got %T", o))
return client.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 client.NA
}
n, ok := m["name"].(string) return FQN(u.GetNamespace(), u.GetName())
if !ok {
log.Error().Err(fmt.Errorf("expecting interface map for name but got %T", m["name"]))
return client.NA
}
ns, ok := m["namespace"].(string)
if !ok {
return FQN("", n)
}
return FQN(ns, n)
}
// Check if string is in a string list.
func in(ll []string, s string) bool {
for _, l := range ll {
if l == s {
return true
}
}
return false
} }
// GetPodSpec returns a pod spec given a resource. // GetPodSpec returns a pod spec given a resource.

View File

@ -76,7 +76,7 @@ func (p *Policy) loadClusterRoleBinding(kind, name string) (render.Policies, err
rows := make(render.Policies, 0, len(nn)) rows := make(render.Policies, 0, len(nn))
for _, cr := range crs { for _, cr := range crs {
if !in(nn, cr.Name) { if !inList(nn, cr.Name) {
continue continue
} }
rows = append(rows, parseRules("*", "CR:"+cr.Name, cr.Rules)...) rows = append(rows, parseRules("*", "CR:"+cr.Name, cr.Rules)...)
@ -97,7 +97,7 @@ func (p *Policy) loadRoleBinding(kind, name string) (render.Policies, error) {
} }
rows := make(render.Policies, 0, len(crs)) rows := make(render.Policies, 0, len(crs))
for _, cr := range crs { for _, cr := range crs {
if !in(ss, "ClusterRole:"+cr.Name) { if !inList(ss, "ClusterRole:"+cr.Name) {
continue continue
} }
rows = append(rows, parseRules("*", "CR:"+cr.Name, cr.Rules)...) rows = append(rows, parseRules("*", "CR:"+cr.Name, cr.Rules)...)
@ -108,7 +108,7 @@ func (p *Policy) loadRoleBinding(kind, name string) (render.Policies, error) {
return nil, err return nil, err
} }
for _, ro := range ros { for _, ro := range ros {
if !in(ss, "Role:"+ro.Name) { if !inList(ss, "Role:"+ro.Name) {
continue continue
} }
log.Debug().Msgf("Loading rules for role %q:%q", ro.Namespace, ro.Name) log.Debug().Msgf("Loading rules for role %q:%q", ro.Namespace, ro.Name)

View File

@ -16,7 +16,7 @@ var (
_ Nuker = (*ScreenDump)(nil) _ Nuker = (*ScreenDump)(nil)
// InvalidCharsRX contains invalid filename characters. // InvalidCharsRX contains invalid filename characters.
invalidPathCharsRX = regexp.MustCompile(`[:/\\]+`) invalidPathCharsRX = regexp.MustCompile(`[:]+`)
) )
// ScreenDump represents a scraped resources. // ScreenDump represents a scraped resources.

View File

@ -237,6 +237,7 @@ func (l *Log) load(ctx context.Context, c dao.LogChan) error {
if err = loggable.TailLogs(ctx, c, l.logOptions); err != nil { if err = loggable.TailLogs(ctx, c, l.logOptions); err != nil {
log.Error().Err(err).Msgf("Tail logs failed") log.Error().Err(err).Msgf("Tail logs failed")
l.cancel() l.cancel()
l.fireLogError(err)
} }
}() }()

View File

@ -103,7 +103,6 @@ func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check,
DAO: &dao.Table{}, DAO: &dao.Table{},
Renderer: &render.Generic{}, Renderer: &render.Generic{},
} }
// return nil, fmt.Errorf("No meta for %q", gvr)
} }
if meta.DAO == nil { if meta.DAO == nil {
meta.DAO = &dao.Resource{} meta.DAO = &dao.Resource{}
@ -116,19 +115,19 @@ func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check,
} }
c := health.NewCheck(gvr) c := health.NewCheck(gvr)
if _, ok := meta.Renderer.(*render.Generic); ok { if meta.Renderer.IsGeneric() {
table, ok := oo[0].(*metav1beta1.Table) table, ok := oo[0].(*metav1beta1.Table)
if !ok { if !ok {
return nil, fmt.Errorf("expecting a meta table but got %T", oo[0]) return nil, fmt.Errorf("expecting a meta table but got %T", oo[0])
} }
rows := make(render.Rows, len(table.Rows)) rows := make(render.Rows, len(table.Rows))
gr, _ := meta.Renderer.(*render.Generic) re, _ := meta.Renderer.(Generic)
gr.SetTable(table) re.SetTable(table)
for i, row := range table.Rows { for i, row := range table.Rows {
if err := gr.Render(row, ns, &rows[i]); err != nil { if err := re.Render(row, ns, &rows[i]); err != nil {
return nil, err return nil, err
} }
if !render.Happy(ns, gr.Header(ns), rows[i]) { if !render.Happy(ns, re.Header(ns), rows[i]) {
c.Inc(health.S2) c.Inc(health.S2)
continue continue
} }

View File

@ -302,6 +302,7 @@ func hydrate(ns string, oo []runtime.Object, rr render.Rows, re Renderer) error
type Generic interface { type Generic interface {
SetTable(*metav1beta1.Table) SetTable(*metav1beta1.Table)
Header(string) render.Header
Render(interface{}, string, *render.Row) error Render(interface{}, string, *render.Row) error
} }

View File

@ -8,15 +8,10 @@ import (
type DeltaRow []string type DeltaRow []string
// NewDeltaRow computes the delta between 2 rows. // NewDeltaRow computes the delta between 2 rows.
func NewDeltaRow(o, n Row, excludeLast bool) DeltaRow { func NewDeltaRow(o, n Row, h Header) DeltaRow {
deltas := make(DeltaRow, len(o.Fields)) deltas := make(DeltaRow, len(o.Fields))
// Exclude age col for i, old := range o.Fields {
oldFields := o.Fields[:len(o.Fields)-1] if old != "" && old != n.Fields[i] && !h.IsTimeCol(i) {
if !excludeLast {
oldFields = o.Fields[:len(o.Fields)]
}
for i, old := range oldFields {
if old != "" && old != n.Fields[i] {
deltas[i] = old deltas[i] = old
} }
} }

View File

@ -24,10 +24,15 @@ func TestDeltaLabelize(t *testing.T) {
}, },
} }
hh := render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
}
for k := range uu { for k := range uu {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
d := render.NewDeltaRow(u.o, u.n, false) d := render.NewDeltaRow(u.o, u.n, hh)
d = d.Labelize([]int{0, 1}, 2) d = d.Labelize([]int{0, 1}, 2)
assert.Equal(t, u.e, d) assert.Equal(t, u.e, d)
}) })
@ -111,10 +116,15 @@ func TestDeltaCustomize(t *testing.T) {
}, },
} }
hh := render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
}
for k := range uu { for k := range uu {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
d := render.NewDeltaRow(u.r1, u.r2, false) d := render.NewDeltaRow(u.r1, u.r2, hh)
out := make(render.DeltaRow, len(u.cols)) out := make(render.DeltaRow, len(u.cols))
d.Customize(u.cols, out) d.Customize(u.cols, out)
assert.Equal(t, u.e, out) assert.Equal(t, u.e, out)
@ -168,10 +178,15 @@ func TestDeltaNew(t *testing.T) {
}, },
} }
hh := render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
}
for k := range uu { for k := range uu {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
d := render.NewDeltaRow(u.o, u.n, false) d := render.NewDeltaRow(u.o, u.n, hh)
assert.Equal(t, u.e, d) assert.Equal(t, u.e, d)
assert.Equal(t, u.blank, d.IsBlank()) assert.Equal(t, u.blank, d.IsBlank())
}) })

View File

@ -21,14 +21,8 @@ func (*Event) IsGeneric() bool {
// ColorerFunc colors a resource row. // ColorerFunc colors a resource row.
func (e *Event) ColorerFunc() ColorerFunc { func (e *Event) ColorerFunc() ColorerFunc {
return func(ns string, h Header, re RowEvent) tcell.Color { return func(ns string, h Header, re RowEvent) tcell.Color {
if !Happy(ns, h, re.Row) {
return ErrColor
}
reasonCol := h.IndexOf("REASON", true) reasonCol := h.IndexOf("REASON", true)
if reasonCol == -1 { if reasonCol >= 0 && strings.TrimSpace(re.Row.Fields[reasonCol]) == "Killing" {
return DefaultColorer(ns, h, re)
}
if strings.TrimSpace(re.Row.Fields[reasonCol]) == "Killing" {
return KillColor return KillColor
} }
@ -86,13 +80,31 @@ func (e *Event) Render(o interface{}, ns string, r *Row) error {
r.ID = client.FQN(nns, name) r.ID = client.FQN(nns, name)
r.Fields = make(Fields, 0, len(e.Header(ns))) r.Fields = make(Fields, 0, len(e.Header(ns)))
r.Fields = append(r.Fields, nns) r.Fields = append(r.Fields, nns)
for _, c := range row.Cells { for _, o := range row.Cells {
if c == nil { if o == nil {
r.Fields = append(r.Fields, Blank) r.Fields = append(r.Fields, Blank)
continue continue
} }
r.Fields = append(r.Fields, fmt.Sprintf("%v", c)) if s, ok := o.(fmt.Stringer); ok {
r.Fields = append(r.Fields, s.String())
continue
}
if s, ok := o.(string); ok {
r.Fields = append(r.Fields, s)
continue
}
r.Fields = append(r.Fields, fmt.Sprintf("%v", o))
} }
return nil return nil
} }
func (e *Event) cellFor(n string, row metav1beta1.TableRow) (string, bool) {
for i, h := range e.table.ColumnDefinitions {
if h.Name == n {
return fmt.Sprintf("%v", row.Cells[i]), true
}
}
return "", false
}

View File

@ -150,11 +150,12 @@ func (h Header) IsMetricsCol(col int) bool {
return h[col].MX return h[col].MX
} }
// IsAgeCol checks if given column index is the age column. // IsTimeCol checks if given column index represents a timestamp.
func (h Header) IsAgeCol(col int) bool { func (h Header) IsTimeCol(col int) bool {
if !h.HasAge() || col >= len(h) { if col >= len(h) {
return false return false
} }
return h[col].Time return h[col].Time
} }

View File

@ -241,7 +241,7 @@ func TestHeaderHasAge(t *testing.T) {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.h.HasAge()) assert.Equal(t, u.e, u.h.HasAge())
assert.Equal(t, u.e, u.h.IsAgeCol(2)) assert.Equal(t, u.e, u.h.IsTimeCol(2))
}) })
} }
} }

View File

@ -196,7 +196,7 @@ func (r RowEvents) FindIndex(id string) (int, bool) {
} }
// Sort rows based on column index and order. // Sort rows based on column index and order.
func (r RowEvents) Sort(ns string, sortCol int, ageCol, numCol, asc bool) { func (r RowEvents) Sort(ns string, sortCol int, isDuration, numCol, asc bool) {
if sortCol == -1 { if sortCol == -1 {
return return
} }
@ -207,14 +207,14 @@ func (r RowEvents) Sort(ns string, sortCol int, ageCol, numCol, asc bool) {
Index: sortCol, Index: sortCol,
Asc: asc, Asc: asc,
IsNumber: numCol, IsNumber: numCol,
IsDuration: ageCol, IsDuration: isDuration,
} }
sort.Sort(t) sort.Sort(t)
iids, fields := map[string][]string{}, make(StringSet, 0, len(r)) iids, fields := map[string][]string{}, make(StringSet, 0, len(r))
for _, re := range r { for _, re := range r {
field := re.Row.Fields[sortCol] field := re.Row.Fields[sortCol]
if ageCol { if isDuration {
field = toAgeDuration(field) field = toAgeDuration(field)
} }
fields = fields.Add(field) fields = fields.Add(field)

View File

@ -81,7 +81,7 @@ func (t *TableData) Update(rows Rows) {
} }
if index, ok := t.RowEvents.FindIndex(row.ID); ok { if index, ok := t.RowEvents.FindIndex(row.ID); ok {
delta := NewDeltaRow(t.RowEvents[index].Row, row, t.Header.HasAge()) delta := NewDeltaRow(t.RowEvents[index].Row, row, t.Header)
if delta.IsBlank() { if delta.IsBlank() {
t.RowEvents[index].Kind, t.RowEvents[index].Deltas = EventUnchanged, blankDelta t.RowEvents[index].Kind, t.RowEvents[index].Deltas = EventUnchanged, blankDelta
t.RowEvents[index].Row = row t.RowEvents[index].Row = row

View File

@ -26,7 +26,7 @@ func ComputeMaxColumns(pads MaxyPad, sortColName string, header render.Header, e
var row int var row int
for _, e := range ee { for _, e := range ee {
for index, field := range e.Row.Fields { for index, field := range e.Row.Fields {
if header.IsAgeCol(index) { if header.IsTimeCol(index) {
field = toAgeHuman(field) field = toAgeHuman(field)
} }
width := len(field) + colPadding width := len(field) + colPadding

View File

@ -235,7 +235,7 @@ func (t *Table) doUpdate(data render.TableData) {
custData.RowEvents.Sort( custData.RowEvents.Sort(
custData.Namespace, custData.Namespace,
colIndex, colIndex,
t.sortCol.name == "AGE", custData.Header.IsTimeCol(colIndex),
data.Header.IsMetricsCol(colIndex), data.Header.IsMetricsCol(colIndex),
t.sortCol.asc, t.sortCol.asc,
) )
@ -270,7 +270,7 @@ func (t *Table) buildRow(r int, re, ore render.RowEvent, h render.Header, pads M
continue continue
} }
if !re.Deltas.IsBlank() && !h.IsAgeCol(c) { if !re.Deltas.IsBlank() && !h.IsTimeCol(c) {
field += Deltas(re.Deltas[c], field) field += Deltas(re.Deltas[c], field)
} }

View File

@ -397,7 +397,7 @@ func (a *App) isValidNS(ns string) (bool, error) {
return true, nil return true, nil
} }
func (a *App) switchCtx(name string, loadPods bool) error { func (a *App) switchContext(name string, loadPods bool) error {
log.Debug().Msgf("--> Switching Context %q--%q", name, a.Config.ActiveView()) log.Debug().Msgf("--> Switching Context %q--%q", name, a.Config.ActiveView())
a.Halt() a.Halt()
defer a.Resume() defer a.Resume()
@ -408,8 +408,8 @@ func (a *App) switchCtx(name string, loadPods bool) error {
} }
a.initFactory(ns) a.initFactory(ns)
if err := a.command.Reset(true); err != nil { if e := a.command.Reset(true); e != nil {
return err return e
} }
v := a.Config.ActiveView() v := a.Config.ActiveView()
if v == "" || isContextCmd(v) || loadPods { if v == "" || isContextCmd(v) || loadPods {
@ -418,8 +418,16 @@ func (a *App) switchCtx(name string, loadPods bool) error {
} }
a.Config.Reset() a.Config.Reset()
a.Config.K9s.CurrentContext = name a.Config.K9s.CurrentContext = name
cluster, err := a.Conn().Config().CurrentClusterName()
if err != nil {
return err
}
a.Config.K9s.CurrentCluster = cluster
if err := a.Config.SetActiveNamespace(ns); err != nil {
log.Error().Err(err).Msg("unable to set active ns")
}
if err := a.Config.Save(); err != nil { if err := a.Config.Save(); err != nil {
log.Error().Err(err).Msg("Config save failed!") log.Error().Err(err).Msg("config save failed!")
} }
a.Flash().Infof("Switching context to %s", name) a.Flash().Infof("Switching context to %s", name)

View File

@ -59,5 +59,5 @@ func useContext(app *App, name string) error {
return err return err
} }
return app.switchCtx(name, true) return app.switchContext(name, true)
} }

View File

@ -25,8 +25,8 @@ import (
const ( const (
logTitle = "logs" logTitle = "logs"
logMessage = "Waiting for logs...\n" logMessage = "Waiting for logs...\n"
logFmt = " Logs([hilite:bg:]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] " logFmt = "([hilite:bg:]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] "
logCoFmt = " Logs([hilite:bg:]%s:[hilite:bg:b]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] " logCoFmt = "([hilite:bg:]%s:[hilite:bg:b]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] "
defaultFlushTimeout = 50 * time.Millisecond defaultFlushTimeout = 50 * time.Millisecond
) )
@ -207,8 +207,6 @@ func (l *Log) getContext() context.Context {
// Start runs the component. // Start runs the component.
func (l *Log) Start() { func (l *Log) Start() {
log.Debug().Msgf("LOG_VIEW STARTED!!")
l.model.Restart(l.getContext(), l.logChan, true) l.model.Restart(l.getContext(), l.logChan, true)
l.model.AddListener(l) l.model.AddListener(l)
l.app.Styles.AddListener(l) l.app.Styles.AddListener(l)
@ -219,10 +217,8 @@ func (l *Log) Start() {
// Stop terminates the component. // Stop terminates the component.
func (l *Log) Stop() { func (l *Log) Stop() {
log.Debug().Msgf("LOG_VIEW STOPPED!")
l.model.RemoveListener(l) l.model.RemoveListener(l)
l.model.Stop() l.model.Stop()
log.Debug().Msgf("CLOSING LOG_CHANNEL!!!")
l.mx.Lock() l.mx.Lock()
{ {
if l.cancelFn != nil { if l.cancelFn != nil {
@ -314,12 +310,15 @@ func (l *Log) updateTitle() {
since = "head" since = "head"
} }
var title string title := " Logs"
if l.model.LogOptions().Previous {
title = " Previous Logs"
}
path, co := l.model.GetPath(), l.model.GetContainer() path, co := l.model.GetPath(), l.model.GetContainer()
if co == "" { if co == "" {
title = ui.SkinTitle(fmt.Sprintf(logFmt, path, since), l.app.Styles.Frame()) title += ui.SkinTitle(fmt.Sprintf(logFmt, path, since), l.app.Styles.Frame())
} else { } else {
title = ui.SkinTitle(fmt.Sprintf(logCoFmt, path, co, since), l.app.Styles.Frame()) title += ui.SkinTitle(fmt.Sprintf(logCoFmt, path, co, since), l.app.Styles.Frame())
} }
buff := l.logs.cmdBuff.GetText() buff := l.logs.cmdBuff.GetText()
@ -409,11 +408,13 @@ func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
// SaveCmd dumps the logs to file. // SaveCmd dumps the logs to file.
func (l *Log) SaveCmd(*tcell.EventKey) *tcell.EventKey { func (l *Log) SaveCmd(*tcell.EventKey) *tcell.EventKey {
if path, err := saveData(l.app.Config.K9s.GetScreenDumpDir(), l.app.Config.K9s.CurrentContext, l.model.GetPath(), l.logs.GetText(true)); err != nil { path, err := saveData(l.app.Config.K9s.GetScreenDumpDir(), l.app.Config.K9s.CurrentContext, l.model.GetPath(), l.logs.GetText(true))
if err != nil {
l.app.Flash().Err(err) l.app.Flash().Err(err)
} else { return nil
l.app.Flash().Infof("Log %s saved successfully!", path)
} }
l.app.Flash().Infof("Log %s saved successfully!", path)
return nil return nil
} }
@ -429,14 +430,14 @@ func ensureDir(dir string) error {
return os.MkdirAll(dir, 0744) return os.MkdirAll(dir, 0744)
} }
func saveData(screenDumpDir, cluster, name, data string) (string, error) { func saveData(screenDumpDir, cluster, fqn, data string) (string, error) {
dir := filepath.Join(screenDumpDir, dao.SanitizeFilename(cluster)) dir := filepath.Join(screenDumpDir, dao.SanitizeFilename(cluster))
if err := ensureDir(dir); err != nil { if err := ensureDir(dir); err != nil {
return "", err return "", err
} }
now := time.Now().UnixNano() now := time.Now().UnixNano()
fName := fmt.Sprintf("%s-%d.log", dao.SanitizeFilename(name), now) fName := fmt.Sprintf("%s-%d.log", strings.Replace(fqn, "/", "-", 1), now)
path := filepath.Join(dir, fName) path := filepath.Join(dir, fName)
mod := os.O_CREATE | os.O_WRONLY mod := os.O_CREATE | os.O_WRONLY

View File

@ -108,8 +108,10 @@ func TestLogViewSave(t *testing.T) {
dir := filepath.Join(app.Config.K9s.GetScreenDumpDir(), app.Config.K9s.CurrentCluster) dir := filepath.Join(app.Config.K9s.GetScreenDumpDir(), app.Config.K9s.CurrentCluster)
c1, _ := os.ReadDir(dir) c1, _ := os.ReadDir(dir)
fmt.Println("C1", c1)
v.SaveCmd(nil) v.SaveCmd(nil)
c2, _ := os.ReadDir(dir) c2, _ := os.ReadDir(dir)
fmt.Println("C2", c2)
assert.Equal(t, len(c2), len(c1)+1) assert.Equal(t, len(c2), len(c1)+1)
} }

View File

@ -60,7 +60,6 @@ func (l *LogsExtender) showLogs(path string, prev bool) {
l.App().Flash().Err(err) l.App().Flash().Err(err)
return return
} }
opts := l.buildLogOpts(path, "", prev) opts := l.buildLogOpts(path, "", prev)
if l.optionsFn != nil { if l.optionsFn != nil {
if opts, err = l.optionsFn(prev); err != nil { if opts, err = l.optionsFn(prev); err != nil {
@ -68,7 +67,6 @@ func (l *LogsExtender) showLogs(path string, prev bool) {
return return
} }
} }
if err := l.App().inject(NewLog(l.GVR(), opts)); err != nil { if err := l.App().inject(NewLog(l.GVR(), opts)); err != nil {
l.App().Flash().Err(err) l.App().Flash().Err(err)
} }