release v0.25.13 (#1383)
parent
01bdc85020
commit
fdc638c5d4
2
Makefile
2
Makefile
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -34,15 +34,15 @@ 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
|
||||||
cachedClient *disk.CachedDiscoveryClient
|
cachedClient *disk.CachedDiscoveryClient
|
||||||
config *Config
|
config *Config
|
||||||
mx sync.Mutex
|
mx sync.Mutex
|
||||||
cache *cache.LRUExpireCache
|
cache *cache.LRUExpireCache
|
||||||
connOK bool
|
connOK bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTestAPIClient for testing ONLY!!
|
// NewTestAPIClient for testing ONLY!!
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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().")
|
||||||
|
|
|
||||||
|
|
@ -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"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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 == "" {
|
||||||
|
|
|
||||||
|
|
@ -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++ {
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue