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)
SOURCE_DATE_EPOCH ?= $(shell date +%s)
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
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?
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() {
panic("No connectivity")

View File

@ -34,7 +34,7 @@ var supportedMetricsAPIVersions = []string{"v1beta1"}
// APIClient represents a Kubernetes api client.
type APIClient struct {
client kubernetes.Interface
client, logClient kubernetes.Interface
dClient dynamic.Interface
nsClient dynamic.NamespaceableResourceInterface
mxsClient *versioned.Clientset
@ -279,6 +279,27 @@ func (a *APIClient) HasMetrics() bool {
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.
func (a *APIClient) Dial() (kubernetes.Interface, error) {
if !a.connOK {
@ -393,7 +414,7 @@ func (a *APIClient) reset() {
a.config.reset()
a.cache = cache.NewLRUExpireCache(cacheSize)
a.client, a.dClient, a.nsClient, a.mxsClient = nil, nil, nil, nil
a.cachedClient = nil
a.cachedClient, a.logClient = nil, nil
a.connOK = true
}

View File

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

View File

@ -85,9 +85,12 @@ type Connection interface {
// ConnectionOK checks api server connection status.
ConnectionOK() bool
// DialOrDie connects to api server.
// Dial connects to api server.
Dial() (kubernetes.Interface, error)
// DialLogs connects to api server for logs.
DialLogs() (kubernetes.Interface, error)
// SwitchContext switches cluster based on context.
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.ActivateCluster()
var cns string
if cl := c.K9s.ActiveCluster(); cl != nil && cl.Namespace != nil {
cns = cl.Namespace.Active
}
var ns string
if k9sFlags != nil && IsBoolSet(k9sFlags.AllNamespaces) {
ns = client.NamespaceAll
@ -99,11 +103,11 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, c
ns = *flags.Namespace
} else if context.Namespace != "" {
ns = context.Namespace
if cl := c.K9s.ActiveCluster(); cl != nil && cl.Namespace != nil && cl.Namespace.Active != "" {
ns = cl.Namespace.Active
if cns != "" {
ns = cns
}
} else if cl := c.K9s.ActiveCluster(); cl != nil && cl.Namespace != nil {
ns = cl.Namespace.Active
} else {
ns = cns
}
if ns != "" {
@ -112,11 +116,9 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, c
}
flags.Namespace = &ns
}
if isSet(flags.ClusterName) {
c.K9s.CurrentCluster = *flags.ClusterName
}
EnsurePath(c.K9s.GetScreenDumpDir(), DefaultDirMod)
return nil

View File

@ -164,6 +164,25 @@ func (mock *MockConnection) Dial() (kubernetes.Interface, error) {
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) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")

View File

@ -22,7 +22,7 @@ type Namespace struct {
func NewNamespace() *Namespace {
return &Namespace{
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) 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) SwitchContext(ctx string) error { return nil }
func (c *conn) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { return nil, nil }

View File

@ -19,6 +19,15 @@ var (
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.
func IsInverseSelector(s string) bool {
if s == "" {

View File

@ -14,6 +14,7 @@ type LogItem struct {
Pod, Container string
SingleContainer bool
Bytes []byte
IsError bool
}
// 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) {
index := bytes.Index(l.Bytes, []byte{' '})
if showTime && index > 0 {
bb.WriteString("[gray::]")
bb.WriteString("[gray::b]")
bb.Write(l.Bytes[:index])
bb.WriteString(" ")
for i := len(l.Bytes[:index]); i < 30; i++ {

View File

@ -63,7 +63,7 @@ func TestLogItemRender(t *testing.T) {
ShowTimestamp: true,
},
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": {
opts: dao.LogOptions{

View File

@ -108,7 +108,7 @@ func TestLogItemsRender(t *testing.T) {
Container: "blee",
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.
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.
@ -109,8 +112,8 @@ func (o *LogOptions) ToPodLogOptions() *v1.PodLogOptions {
return &opts
}
// DecorateLog add a log header to display po/co information along with the log message.
func (o *LogOptions) DecorateLog(bytes []byte) *LogItem {
// ToLogItem add a log header to display po/co information along with the log message.
func (o *LogOptions) ToLogItem(bytes []byte) *LogItem {
item := NewLogItem(bytes)
if len(bytes) == 0 {
return item
@ -128,3 +131,10 @@ func (o *LogOptions) DecorateLog(bytes []byte) *LogItem {
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")
}
dial, err := p.Client().Dial()
dial, err := p.Client().DialLogs()
if err != nil {
return nil, err
}
@ -178,7 +178,6 @@ func (p *Pod) GetInstance(fqn string) (*v1.Pod, error) {
// TailLogs tails a given container logs.
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)
if !ok {
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()
log.Debug().Msgf("TAIL_LOGS! %#v", o)
done:
for r := 0; r < logRetryCount; r++ {
var e error
req, err = logger.Logs(opts.Path, o)
if err == nil {
// This call will block if nothing is in the stream!!
if stream, err = req.Stream(ctx); err == nil {
go readLogs(ctx, stream, out, opts)
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 {
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 {
case <-ctx.Done():
log.Debug().Msgf("!!!!TAIL_LOGS CANCELED!!!!")
err = ctx.Err()
break done
default:
if e != nil {
out <- opts.ToErrLogItem(e)
}
time.Sleep(logRetryWait)
}
}
@ -365,7 +367,6 @@ done:
func readLogs(ctx context.Context, stream io.ReadCloser, c LogChan, opts *LogOptions) {
defer func() {
log.Debug().Msgf("READ_LOGS BAILED!!!")
if err := stream.Close(); err != nil {
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)
r := bufio.NewReader(stream)
for {
var item *LogItem
bytes, err := r.ReadBytes('\n')
if err != nil {
if err == nil {
item = opts.ToLogItem(bytes)
} else {
if errors.Is(err, io.EOF) {
log.Warn().Err(err).Msgf("Stream closed for %s", opts.Info())
// c <- ItemEOF
return
e := fmt.Errorf("Stream closed %w for %s", err, opts.Info())
item = opts.ToErrLogItem(e)
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 {
case c <- opts.DecorateLog(bytes):
case c <- item:
if item.IsError {
return
}
case <-ctx.Done():
log.Debug().Msgf("READER CANCELED")
return
}
}
@ -416,34 +424,8 @@ func extractFQN(o runtime.Object) string {
log.Error().Err(fmt.Errorf("expecting unstructured but got %T", o))
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)
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
return FQN(u.GetNamespace(), u.GetName())
}
// 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))
for _, cr := range crs {
if !in(nn, cr.Name) {
if !inList(nn, cr.Name) {
continue
}
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))
for _, cr := range crs {
if !in(ss, "ClusterRole:"+cr.Name) {
if !inList(ss, "ClusterRole:"+cr.Name) {
continue
}
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
}
for _, ro := range ros {
if !in(ss, "Role:"+ro.Name) {
if !inList(ss, "Role:"+ro.Name) {
continue
}
log.Debug().Msgf("Loading rules for role %q:%q", ro.Namespace, ro.Name)

View File

@ -16,7 +16,7 @@ var (
_ Nuker = (*ScreenDump)(nil)
// InvalidCharsRX contains invalid filename characters.
invalidPathCharsRX = regexp.MustCompile(`[:/\\]+`)
invalidPathCharsRX = regexp.MustCompile(`[:]+`)
)
// 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 {
log.Error().Err(err).Msgf("Tail logs failed")
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{},
Renderer: &render.Generic{},
}
// return nil, fmt.Errorf("No meta for %q", gvr)
}
if meta.DAO == nil {
meta.DAO = &dao.Resource{}
@ -116,19 +115,19 @@ func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check,
}
c := health.NewCheck(gvr)
if _, ok := meta.Renderer.(*render.Generic); ok {
if meta.Renderer.IsGeneric() {
table, ok := oo[0].(*metav1beta1.Table)
if !ok {
return nil, fmt.Errorf("expecting a meta table but got %T", oo[0])
}
rows := make(render.Rows, len(table.Rows))
gr, _ := meta.Renderer.(*render.Generic)
gr.SetTable(table)
re, _ := meta.Renderer.(Generic)
re.SetTable(table)
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
}
if !render.Happy(ns, gr.Header(ns), rows[i]) {
if !render.Happy(ns, re.Header(ns), rows[i]) {
c.Inc(health.S2)
continue
}

View File

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

View File

@ -8,15 +8,10 @@ import (
type DeltaRow []string
// 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))
// Exclude age col
oldFields := o.Fields[:len(o.Fields)-1]
if !excludeLast {
oldFields = o.Fields[:len(o.Fields)]
}
for i, old := range oldFields {
if old != "" && old != n.Fields[i] {
for i, old := range o.Fields {
if old != "" && old != n.Fields[i] && !h.IsTimeCol(i) {
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 {
u := uu[k]
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)
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 {
u := uu[k]
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))
d.Customize(u.cols, 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 {
u := uu[k]
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.blank, d.IsBlank())
})

View File

@ -21,14 +21,8 @@ func (*Event) IsGeneric() bool {
// ColorerFunc colors a resource row.
func (e *Event) ColorerFunc() ColorerFunc {
return func(ns string, h Header, re RowEvent) tcell.Color {
if !Happy(ns, h, re.Row) {
return ErrColor
}
reasonCol := h.IndexOf("REASON", true)
if reasonCol == -1 {
return DefaultColorer(ns, h, re)
}
if strings.TrimSpace(re.Row.Fields[reasonCol]) == "Killing" {
if reasonCol >= 0 && strings.TrimSpace(re.Row.Fields[reasonCol]) == "Killing" {
return KillColor
}
@ -86,13 +80,31 @@ func (e *Event) Render(o interface{}, ns string, r *Row) error {
r.ID = client.FQN(nns, name)
r.Fields = make(Fields, 0, len(e.Header(ns)))
r.Fields = append(r.Fields, nns)
for _, c := range row.Cells {
if c == nil {
for _, o := range row.Cells {
if o == nil {
r.Fields = append(r.Fields, Blank)
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
}
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
}
// IsAgeCol checks if given column index is the age column.
func (h Header) IsAgeCol(col int) bool {
if !h.HasAge() || col >= len(h) {
// IsTimeCol checks if given column index represents a timestamp.
func (h Header) IsTimeCol(col int) bool {
if col >= len(h) {
return false
}
return h[col].Time
}

View File

@ -241,7 +241,7 @@ func TestHeaderHasAge(t *testing.T) {
u := uu[k]
t.Run(k, func(t *testing.T) {
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.
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 {
return
}
@ -207,14 +207,14 @@ func (r RowEvents) Sort(ns string, sortCol int, ageCol, numCol, asc bool) {
Index: sortCol,
Asc: asc,
IsNumber: numCol,
IsDuration: ageCol,
IsDuration: isDuration,
}
sort.Sort(t)
iids, fields := map[string][]string{}, make(StringSet, 0, len(r))
for _, re := range r {
field := re.Row.Fields[sortCol]
if ageCol {
if isDuration {
field = toAgeDuration(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 {
delta := NewDeltaRow(t.RowEvents[index].Row, row, t.Header.HasAge())
delta := NewDeltaRow(t.RowEvents[index].Row, row, t.Header)
if delta.IsBlank() {
t.RowEvents[index].Kind, t.RowEvents[index].Deltas = EventUnchanged, blankDelta
t.RowEvents[index].Row = row

View File

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

View File

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

View File

@ -397,7 +397,7 @@ func (a *App) isValidNS(ns string) (bool, error) {
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())
a.Halt()
defer a.Resume()
@ -408,8 +408,8 @@ func (a *App) switchCtx(name string, loadPods bool) error {
}
a.initFactory(ns)
if err := a.command.Reset(true); err != nil {
return err
if e := a.command.Reset(true); e != nil {
return e
}
v := a.Config.ActiveView()
if v == "" || isContextCmd(v) || loadPods {
@ -418,8 +418,16 @@ func (a *App) switchCtx(name string, loadPods bool) error {
}
a.Config.Reset()
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 {
log.Error().Err(err).Msg("Config save failed!")
log.Error().Err(err).Msg("config save failed!")
}
a.Flash().Infof("Switching context to %s", name)

View File

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

View File

@ -25,8 +25,8 @@ import (
const (
logTitle = "logs"
logMessage = "Waiting for logs...\n"
logFmt = " Logs([hilite:bg:]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] "
logCoFmt = " Logs([hilite:bg:]%s:[hilite:bg:b]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] "
logFmt = "([hilite:bg:]%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
)
@ -207,8 +207,6 @@ func (l *Log) getContext() context.Context {
// Start runs the component.
func (l *Log) Start() {
log.Debug().Msgf("LOG_VIEW STARTED!!")
l.model.Restart(l.getContext(), l.logChan, true)
l.model.AddListener(l)
l.app.Styles.AddListener(l)
@ -219,10 +217,8 @@ func (l *Log) Start() {
// Stop terminates the component.
func (l *Log) Stop() {
log.Debug().Msgf("LOG_VIEW STOPPED!")
l.model.RemoveListener(l)
l.model.Stop()
log.Debug().Msgf("CLOSING LOG_CHANNEL!!!")
l.mx.Lock()
{
if l.cancelFn != nil {
@ -314,12 +310,15 @@ func (l *Log) updateTitle() {
since = "head"
}
var title string
title := " Logs"
if l.model.LogOptions().Previous {
title = " Previous Logs"
}
path, co := l.model.GetPath(), l.model.GetContainer()
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 {
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()
@ -409,11 +408,13 @@ func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
// SaveCmd dumps the logs to file.
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)
} else {
l.app.Flash().Infof("Log %s saved successfully!", path)
return nil
}
l.app.Flash().Infof("Log %s saved successfully!", path)
return nil
}
@ -429,14 +430,14 @@ func ensureDir(dir string) error {
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))
if err := ensureDir(dir); err != nil {
return "", err
}
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)
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)
c1, _ := os.ReadDir(dir)
fmt.Println("C1", c1)
v.SaveCmd(nil)
c2, _ := os.ReadDir(dir)
fmt.Println("C2", c2)
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)
return
}
opts := l.buildLogOpts(path, "", prev)
if l.optionsFn != nil {
if opts, err = l.optionsFn(prev); err != nil {
@ -68,7 +67,6 @@ func (l *LogsExtender) showLogs(path string, prev bool) {
return
}
}
if err := l.App().inject(NewLog(l.GVR(), opts)); err != nil {
l.App().Flash().Err(err)
}