parent
8af75df9f8
commit
c20d89b938
27
README.md
27
README.md
|
|
@ -427,27 +427,23 @@ roleRef:
|
|||
|
||||
## Skins
|
||||
|
||||
You can style K9s based on your own sense of look and style. This is very much an experimental feature at this time, more will be added/modified if this feature has legs so thread accordingly!
|
||||
|
||||
By default a K9s view displays resource information using the following coloring scheme:
|
||||
|
||||
1. Blue - All good.
|
||||
1. Orange/Red - Represents a potential issue with the resource ie a pod is not in a running state.
|
||||
1. Green - Indicates a row has changed. A change delta indicator indicates which column changed.
|
||||
|
||||
Skins are YAML files, that enable a user to change K9s presentation layer. K9s skins are loaded from `$HOME/.k9s/skin.yml`. If a skin file is detected then the skin would be loaded if not the current stock skin remains in effect.
|
||||
You can style K9s based on your own sense of look and style. Skins are YAML files, that enable a user to change the K9s presentation layer. K9s skins are loaded from `$HOME/.k9s/skin.yml`. If a skin file is detected then the skin would be loaded if not the current stock skin remains in effect.
|
||||
|
||||
You can also change K9s skins based on the cluster you are connecting too. In this case, you can specify the skin file name as `$HOME/.k9s/mycluster_skin.yml`
|
||||
Below is a sample skin file, more skins are available in the skins directory in this repo, just simply copy any of these in your user's home dir as `skin.yml`.
|
||||
|
||||
Colors can be defined by name or uing an hex representation.
|
||||
|
||||
> NOTE: This is very much an experimental feature at this time, more will be added/modified if this feature has legs so thread accordingly!
|
||||
|
||||
```yaml
|
||||
# InTheNavy Skin...
|
||||
k9s:
|
||||
# General K9s styles
|
||||
body:
|
||||
fgColor: dodgerblue
|
||||
bgColor: white
|
||||
logoColor: blue
|
||||
bgColor: #ffffff
|
||||
logoColor: #0000ff
|
||||
# ClusterInfoView styles.
|
||||
info:
|
||||
fgColor: lightskyblue
|
||||
|
|
@ -470,8 +466,7 @@ k9s:
|
|||
activeColor: skyblue
|
||||
# Resource status and update styles
|
||||
status:
|
||||
# You can also use hex colors!
|
||||
newColor: #0000ff
|
||||
newColor: #00ff00
|
||||
modifyColor: powderblue
|
||||
addColor: lightskyblue
|
||||
errorColor: indianred
|
||||
|
|
@ -507,7 +502,7 @@ k9s:
|
|||
bgColor: black
|
||||
```
|
||||
|
||||
Available color names are defined below:
|
||||
Here is a list of all available color names.
|
||||
|
||||
| Color Names | | | | |
|
||||
|----------------------|----------------|------------------|-------------------|-----------------|
|
||||
|
|
@ -548,7 +543,7 @@ Available color names are defined below:
|
|||
|
||||
This initial drop is brittle. K9s will most likely blow up...
|
||||
|
||||
1. You're running older versions of Kubernetes. K9s works best Kubernetes 1.15+.
|
||||
1. You're running older versions of Kubernetes. K9s works best on Kubernetes latest.
|
||||
2. You don't have enough RBAC fu to manage your cluster.
|
||||
|
||||
---
|
||||
|
|
@ -577,4 +572,4 @@ to make this project a reality!
|
|||
|
||||
---
|
||||
|
||||
<img src="assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
<img src="assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ Maintenance Release!
|
|||
|
||||
### Speedy Gonzales?
|
||||
|
||||
In this drop, we took a bit of a perf pass in light of recent issues and thanks to [Chris Werner Rau](https://github.com/cwrau) pushing me and keeping me up to speed, I've digged a bit deeper and found that there might be some seamingly innocent calls that sucked a bit of cycles during K9s refreshes. Long story short, I think this drop will improve perf by a factor of ~10x in some instances. Typically the initial load will be slower but subsequent loads should be much faster. Famous last words right? Anyhow, can't really take credit for this one as the awesome [Gustavo Silva Paiva](https://github.com/paivagustavo) suggested doing this a while back, but since I was already in flight with the refactor decided to punt until back online. And here we are...
|
||||
In this drop, we took a bit of a perf pass in light of recent issues and thanks to [Chris Werner Rau](https://github.com/cwrau) pushing me and keeping me up to speed, I've digged a bit deeper and found that there might be some seamingly innocent calls that sucked a bit of cycles during K9s refreshes. Long story short, I think this drop will improve perf by a factor of ~10x in some instances. Typically the initial load will be slower but subsequent loads should be much faster. Famous last words right? Anyhow, can't really take credit for this one as the awesome [Gustavo Silva Paiva](https://github.com/paivagustavo) suggested doing this a while back, but since I was already in flight with the refactor decided to punt until back online. And here we are now...
|
||||
|
||||
Hopefully these findings will coalesce with yours?? If not, please forward bulk Prozac patches at the address below ;)
|
||||
|
||||
Thanks Chris! Was up all night trying to figure out what was the deal with K9s and your specific clusters. Hopefully this time for sure??
|
||||
Thanks Chris! Was up all night trying to figure out and what was the deal with K9s and your specific clusters. Hopefully this time for sure??
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
|
||||
|
||||
# Release v0.12.0
|
||||
|
||||
## Notes
|
||||
|
||||
Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!
|
||||
|
||||
Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
|
||||
|
||||
---
|
||||
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_helm.png" align="center" width="300" height="auto"/>
|
||||
|
||||
This was a long week in the saddle, you guys have been so awesome and supportive thru these last few drops. Thank you!!
|
||||
|
||||
### Searchable Logs
|
||||
|
||||
There has been quiet a few demands for this feature. It should now be generally available in this drop. It works the same as the resource view ie `/fred`, you can also specify a fuzzy filter using `/-f blee-duh`. The paint is still fresh on that deal and not super confident that it will work nominaly as I had to rework the logs to enable. So totally possible I've hosed something in the process.
|
||||
|
||||
### APIServer Dud
|
||||
|
||||
At times, it could be you've lost your api server connection while K9s was up which resulted in the `K9s screen of death` or in other words a hosed terminal session ;(. K9s should now detect this condition and close out. Once again no super sure about this implementation on that deal. So if you see K9s close out under normal condition, that means I would need to go back to the drawing board.
|
||||
|
||||
### FullScreen Logs
|
||||
|
||||
I've been told having a flag to set fullScreen mode preference while viewing the logs would be `awesome`. Thanks [Fardin Khanjani](https://github.com/fardin01)!
|
||||
So there is now a new K9s config flag available to set your fullsreen logs `drathers` in your .k9s/config.yml. This flag defaults to false if not set.
|
||||
|
||||
Here is a snippet:
|
||||
|
||||
```yaml
|
||||
# .k9s/config.yml
|
||||
k9s:
|
||||
refreshRate: 2
|
||||
headless: false
|
||||
currentContext: crashandburn666
|
||||
currentCluster: slowassnot
|
||||
fullScreenLogs: true
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resolved Bugs/Features
|
||||
|
||||
* [Issue #484](https://github.com/derailed/k9s/issues/484)
|
||||
* [Issue #481](https://github.com/derailed/k9s/issues/481)
|
||||
* [Issue #480](https://github.com/derailed/k9s/issues/480)
|
||||
* [Issue #479](https://github.com/derailed/k9s/issues/479)
|
||||
* [Issue #477](https://github.com/derailed/k9s/issues/477)
|
||||
* [Issue #476](https://github.com/derailed/k9s/issues/476)
|
||||
* [Issue #468](https://github.com/derailed/k9s/issues/468)
|
||||
|
||||
---
|
||||
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
|
@ -67,7 +67,6 @@ func Execute() {
|
|||
|
||||
func run(cmd *cobra.Command, args []string) {
|
||||
defer func() {
|
||||
// view.ClearScreen()
|
||||
if err := recover(); err != nil {
|
||||
log.Error().Msgf("Boom! %v", err)
|
||||
log.Error().Msg(string(debug.Stack()))
|
||||
|
|
@ -117,7 +116,7 @@ func loadConfiguration() *config.Config {
|
|||
}
|
||||
|
||||
if err := k9sCfg.Refine(k8sFlags); err != nil {
|
||||
log.Panic().Err(err).Msg("Unable to locate kubeconfig file")
|
||||
log.Panic().Err(err)
|
||||
}
|
||||
k9sCfg.SetConnection(client.InitConnectionOrDie(k8sCfg))
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,9 @@ func makeCacheKey(ns, gvr string, vv []string) string {
|
|||
|
||||
// CanI checks if user has access to a certain resource.
|
||||
func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) {
|
||||
if IsClusterWide(ns) {
|
||||
ns = AllNamespaces
|
||||
}
|
||||
key := makeCacheKey(ns, gvr, verbs)
|
||||
defer func(t time.Time) string {
|
||||
log.Debug().Msgf("AUTH elapsed %t--%q %v", auth, key, time.Since(t))
|
||||
|
|
@ -171,12 +174,24 @@ func (a *APIClient) Config() *Config {
|
|||
// HasMetrics returns true if the cluster supports metrics.
|
||||
func (a *APIClient) HasMetrics() bool {
|
||||
v, ok := a.cache.Get(cacheMXKey)
|
||||
if !ok {
|
||||
return a.supportsMxServer()
|
||||
if ok {
|
||||
flag, k := v.(bool)
|
||||
return k && flag
|
||||
}
|
||||
|
||||
flag, ok := v.(bool)
|
||||
return ok && flag
|
||||
var flag bool
|
||||
dial, err := a.MXDial()
|
||||
if err != nil {
|
||||
a.cache.Add(cacheMXKey, flag, cacheExpiry)
|
||||
return flag
|
||||
}
|
||||
|
||||
if _, err := dial.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{Limit: 1}); err == nil {
|
||||
flag = true
|
||||
}
|
||||
a.cache.Add(cacheMXKey, flag, cacheExpiry)
|
||||
|
||||
return flag
|
||||
}
|
||||
|
||||
// DialOrDie returns a handle to api server or die.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ package client
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
|
|
@ -13,6 +17,8 @@ import (
|
|||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
const dialTimeout = 1 * time.Second
|
||||
|
||||
// Config tracks a kubernetes configuration.
|
||||
type Config struct {
|
||||
flags *genericclioptions.ConfigFlags
|
||||
|
|
@ -31,6 +37,21 @@ func NewConfig(f *genericclioptions.ConfigFlags) *Config {
|
|||
}
|
||||
}
|
||||
|
||||
// CheckConnectivity return true if api server is cool or false otherwise.
|
||||
func (c *Config) CheckConnectivity() bool {
|
||||
address := strings.Replace(c.restConfig.Host, "https://", "", 1)
|
||||
rx := regexp.MustCompile(`\A.+:\d+`)
|
||||
if !rx.MatchString(address) {
|
||||
address += ":443"
|
||||
}
|
||||
|
||||
if _, err := net.DialTimeout("tcp", address, dialTimeout); err != nil {
|
||||
log.Error().Err(err).Msgf("DIAL TIMEDOUT!")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Flags returns configuration flags.
|
||||
func (c *Config) Flags() *genericclioptions.ConfigFlags {
|
||||
return c.flags
|
||||
|
|
|
|||
|
|
@ -16,6 +16,15 @@ func IsClusterWide(ns string) bool {
|
|||
return ns == NamespaceAll || ns == AllNamespaces || ns == ClusterScope
|
||||
}
|
||||
|
||||
// CleanseNamespace ensures all ns maps to blank.
|
||||
func CleanseNamespace(ns string) string {
|
||||
if IsAllNamespace(ns) {
|
||||
return AllNamespaces
|
||||
}
|
||||
|
||||
return ns
|
||||
}
|
||||
|
||||
// IsAllNamespace returns true if ns == all.
|
||||
func IsAllNamespace(ns string) bool {
|
||||
return ns == NamespaceAll
|
||||
|
|
@ -36,16 +45,6 @@ func IsClusterScoped(ns string) bool {
|
|||
return ns == ClusterScope
|
||||
}
|
||||
|
||||
// NormalizeNS normalizes a namespace name to a k8s ns known designation.
|
||||
func NormalizeNS(ns string) string {
|
||||
switch ns {
|
||||
case NamespaceAll, ClusterScope:
|
||||
return ""
|
||||
default:
|
||||
return ns
|
||||
}
|
||||
}
|
||||
|
||||
// Namespaced converts a resource path to namespace and resource name.
|
||||
func Namespaced(p string) (string, string) {
|
||||
ns, n := path.Split(p)
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
|
|||
|
||||
// FetchNodesMetrics return all metrics for pods in a given namespace.
|
||||
func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
|
||||
var mx mv1beta1.NodeMetricsList
|
||||
mx := mv1beta1.NodeMetricsList{}
|
||||
if !m.HasMetrics() {
|
||||
return &mx, fmt.Errorf("No metrics-server detected on cluster")
|
||||
}
|
||||
|
|
@ -90,7 +90,7 @@ func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
|
|||
|
||||
// FetchPodsMetrics return all metrics for pods in a given namespace.
|
||||
func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, error) {
|
||||
var mx mv1beta1.PodMetricsList
|
||||
mx := mv1beta1.PodMetricsList{}
|
||||
if !m.HasMetrics() {
|
||||
return &mx, fmt.Errorf("No metrics-server detected on cluster")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,11 +69,13 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags) error {
|
|||
} else {
|
||||
c.K9s.CurrentContext = cfg.CurrentContext
|
||||
}
|
||||
log.Debug().Msgf("Active Context `%v`", c.K9s.CurrentContext)
|
||||
|
||||
log.Debug().Msgf("Active Context %q", c.K9s.CurrentContext)
|
||||
if c.K9s.CurrentContext == "" {
|
||||
return errors.New("Invalid kubeconfig context detected")
|
||||
}
|
||||
ctx, ok := cfg.Contexts[c.K9s.CurrentContext]
|
||||
if !ok {
|
||||
return fmt.Errorf("The specified context `%s does not exists in kubeconfig", c.K9s.CurrentContext)
|
||||
return fmt.Errorf("The specified context %q does not exists in kubeconfig", c.K9s.CurrentContext)
|
||||
}
|
||||
c.K9s.CurrentCluster = ctx.Cluster
|
||||
if len(ctx.Namespace) != 0 {
|
||||
|
|
|
|||
|
|
@ -264,6 +264,7 @@ var expectedConfig = `k9s:
|
|||
logRequestSize: 100
|
||||
currentContext: blee
|
||||
currentCluster: blee
|
||||
fullScreenLogs: false
|
||||
clusters:
|
||||
blee:
|
||||
namespace:
|
||||
|
|
@ -303,6 +304,7 @@ var resetConfig = `k9s:
|
|||
logRequestSize: 200
|
||||
currentContext: blee
|
||||
currentCluster: blee
|
||||
fullScreenLogs: false
|
||||
clusters:
|
||||
blee:
|
||||
namespace:
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ type K9s struct {
|
|||
LogRequestSize int `yaml:"logRequestSize"`
|
||||
CurrentContext string `yaml:"currentContext"`
|
||||
CurrentCluster string `yaml:"currentCluster"`
|
||||
FullScreenLogs bool `yaml:"fullScreenLogs"`
|
||||
Clusters map[string]*Cluster `yaml:"clusters,omitempty"`
|
||||
manualRefreshRate int
|
||||
manualHeadless *bool
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
"github.com/rs/zerolog/log"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
|
|
@ -65,6 +66,7 @@ func (a *Alias) List(ctx context.Context, _ string) ([]runtime.Object, error) {
|
|||
// AsGVR returns a matching gvr if it exists.
|
||||
func (a *Alias) AsGVR(cmd string) (client.GVR, bool) {
|
||||
gvr, ok := a.Aliases.Get(cmd)
|
||||
log.Debug().Msgf("ASGVR %q %q %v", cmd, gvr, ok)
|
||||
if ok {
|
||||
return client.NewGVR(gvr), true
|
||||
}
|
||||
|
|
@ -94,7 +96,7 @@ func (a *Alias) load() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := a.Alias[meta.Kind]; ok {
|
||||
if _, ok := a.Alias[meta.Kind]; ok || IsK9sMeta(meta) {
|
||||
continue
|
||||
}
|
||||
a.Define(gvr.String(), strings.ToLower(meta.Kind), meta.Name)
|
||||
|
|
|
|||
|
|
@ -104,13 +104,13 @@ func makeContainerRes(co v1.Container, po *v1.Pod, pmx *mv1beta1.PodMetrics, isI
|
|||
|
||||
cmx, err := containerMetrics(co.Name, pmx)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("Fail container metrics for %s", co.Name)
|
||||
log.Warn().Err(err).Msgf("No container metrics found for %s::%s", po.Name, co.Name)
|
||||
}
|
||||
|
||||
return render.ContainerRes{
|
||||
Container: &co,
|
||||
Status: getContainerStatus(co.Name, po.Status),
|
||||
Metrics: cmx,
|
||||
MX: cmx,
|
||||
IsInit: isInit,
|
||||
Age: po.ObjectMeta.CreationTimestamp,
|
||||
}
|
||||
|
|
@ -144,7 +144,7 @@ func getContainerStatus(co string, status v1.PodStatus) *v1.ContainerStatus {
|
|||
}
|
||||
|
||||
func (c *Container) fetchPod(fqn string) (*v1.Pod, error) {
|
||||
o, err := c.Factory.Get("v1/pods", fqn, true, labels.Everything())
|
||||
o, err := c.Factory.Get("v1/pods", fqn, false, labels.Everything())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,9 @@ func FetchNodes(f Factory) (*v1.NodeList, error) {
|
|||
}
|
||||
|
||||
func nodeMetricsFor(fqn string, mmx *mv1beta1.NodeMetricsList) *mv1beta1.NodeMetrics {
|
||||
if mmx == nil {
|
||||
return nil
|
||||
}
|
||||
for _, mx := range mmx.Items {
|
||||
if MetaFQN(mx.ObjectMeta) == fqn {
|
||||
return &mx
|
||||
|
|
|
|||
|
|
@ -229,6 +229,9 @@ func readLogs(ctx context.Context, stream io.ReadCloser, c chan<- string, opts L
|
|||
// Helpers...
|
||||
|
||||
func podMetricsFor(o runtime.Object, mmx *mv1beta1.PodMetricsList) *mv1beta1.PodMetrics {
|
||||
if mmx == nil {
|
||||
return nil
|
||||
}
|
||||
fqn := extractFQN(o)
|
||||
for _, mx := range mmx.Items {
|
||||
if MetaFQN(mx.ObjectMeta) == fqn {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ type Accessor interface {
|
|||
// Init the resource with a factory object.
|
||||
Init(Factory, client.GVR)
|
||||
|
||||
// GVR returns a gvr a string.
|
||||
GVR() string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,6 @@ import (
|
|||
|
||||
// MetaFQN returns a fully qualified resource name.
|
||||
func MetaFQN(m metav1.ObjectMeta) string {
|
||||
if m.Namespace == "" {
|
||||
return m.Name
|
||||
}
|
||||
|
||||
return FQN(m.Namespace, m.Name)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
package model_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestMetaFQN(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
meta metav1.ObjectMeta
|
||||
e string
|
||||
}{
|
||||
"all_namespaces": {
|
||||
meta: metav1.ObjectMeta{Name: "fred"},
|
||||
e: "fred",
|
||||
},
|
||||
"namespaced": {
|
||||
meta: metav1.ObjectMeta{Name: "fred", Namespace: "blee"},
|
||||
e: "blee/fred",
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, model.MetaFQN(u.meta))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncate(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
data string
|
||||
size int
|
||||
e string
|
||||
}{
|
||||
"same": {
|
||||
data: "fred",
|
||||
size: 4,
|
||||
e: "fred",
|
||||
},
|
||||
"small": {
|
||||
data: "fred",
|
||||
size: 10,
|
||||
e: "fred",
|
||||
},
|
||||
"larger": {
|
||||
data: "fred",
|
||||
size: 3,
|
||||
e: "fr…",
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, model.Truncate(u.data, u.size))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHints(t *testing.T) {
|
||||
func TestHint(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
hh model.MenuHints
|
||||
e int
|
||||
|
|
@ -34,6 +34,7 @@ func TestHints(t *testing.T) {
|
|||
h.SetHints(u.hh)
|
||||
|
||||
assert.Equal(t, u.e, l.count)
|
||||
assert.Equal(t, u.e, len(h.Peek()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +44,6 @@ func TestHintRemoveListener(t *testing.T) {
|
|||
l1, l2, l3 := &hintL{}, &hintL{}, &hintL{}
|
||||
h.AddListener(l1)
|
||||
h.AddListener(l2)
|
||||
h.AddListener(l3)
|
||||
|
||||
h.RemoveListener(l2)
|
||||
h.RemoveListener(l3)
|
||||
|
|
@ -58,6 +58,9 @@ func TestHintRemoveListener(t *testing.T) {
|
|||
assert.Equal(t, 0, l3.count)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
type hintL struct {
|
||||
count int
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,327 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sahilm/fuzzy"
|
||||
)
|
||||
|
||||
// LogsListener represents a log model listener.
|
||||
type LogsListener interface {
|
||||
// LogChanged notifies the model changed.
|
||||
LogChanged([]string)
|
||||
|
||||
// LogCleanred indicates logs are cleared.
|
||||
LogCleared()
|
||||
|
||||
// LogFailed indicates a log failure.
|
||||
LogFailed(error)
|
||||
}
|
||||
|
||||
// Log represents a resource logger.
|
||||
type Log struct {
|
||||
factory dao.Factory
|
||||
lines []string
|
||||
listeners []LogsListener
|
||||
gvr client.GVR
|
||||
logOptions dao.LogOptions
|
||||
cancelFn context.CancelFunc
|
||||
initialized bool
|
||||
mx sync.RWMutex
|
||||
filter string
|
||||
lastSent int
|
||||
}
|
||||
|
||||
// NewLog returns a new model.
|
||||
func NewLog(gvr client.GVR, msg string, opts dao.LogOptions, timeOut time.Duration) *Log {
|
||||
return &Log{
|
||||
gvr: gvr,
|
||||
logOptions: opts,
|
||||
initialized: true,
|
||||
lines: []string{msg},
|
||||
}
|
||||
}
|
||||
|
||||
// GetPath returns resource path.
|
||||
func (l *Log) GetPath() string { return l.logOptions.Path }
|
||||
|
||||
// GetContainer returns the resource container if any or "" otherwise.
|
||||
func (l *Log) GetContainer() string { return l.logOptions.Container }
|
||||
|
||||
// Init initializes the model.
|
||||
func (l *Log) Init(f dao.Factory) {
|
||||
l.factory = f
|
||||
}
|
||||
|
||||
// Clear the logs.
|
||||
func (l *Log) Clear() {
|
||||
l.mx.Lock()
|
||||
defer l.mx.Unlock()
|
||||
l.lines, l.lastSent = []string{}, 0
|
||||
l.fireLogCleared()
|
||||
}
|
||||
|
||||
// Start initialize log tailer.
|
||||
func (l *Log) Start() {
|
||||
if err := l.load(); err != nil {
|
||||
log.Error().Err(err).Msgf("Tail logs failed!")
|
||||
}
|
||||
}
|
||||
|
||||
// Stop terminates log tailing.
|
||||
func (l *Log) Stop() {
|
||||
if l.cancelFn == nil {
|
||||
return
|
||||
}
|
||||
log.Debug().Msgf("<<<< Logger STOP!")
|
||||
l.cancelFn()
|
||||
l.cancelFn = nil
|
||||
}
|
||||
|
||||
// Set sets the log lines (for testing only!)
|
||||
func (l *Log) Set(lines []string) {
|
||||
l.mx.Lock()
|
||||
defer l.mx.Unlock()
|
||||
l.lines = lines
|
||||
l.fireLogChanged(lines)
|
||||
}
|
||||
|
||||
// ClearFilter resets the log filter if any.
|
||||
func (l *Log) ClearFilter() {
|
||||
l.mx.RLock()
|
||||
defer l.mx.RUnlock()
|
||||
l.filter = ""
|
||||
l.fireLogChanged(l.lines)
|
||||
}
|
||||
|
||||
// Filter filters the model using either fuzzy or regexp.
|
||||
func (l *Log) Filter(q string) error {
|
||||
l.mx.RLock()
|
||||
defer l.mx.RUnlock()
|
||||
|
||||
l.filter = q
|
||||
filtered, err := applyFilter(l.filter, l.lines)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.fireLogCleared()
|
||||
l.fireLogChanged(filtered)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) load() error {
|
||||
var ctx context.Context
|
||||
ctx = context.WithValue(context.Background(), internal.KeyFactory, l.factory)
|
||||
ctx, l.cancelFn = context.WithCancel(ctx)
|
||||
|
||||
c := make(chan string, 10)
|
||||
go l.updateLogs(ctx, c)
|
||||
|
||||
accessor, err := dao.AccessorFor(l.factory, l.gvr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger, ok := accessor.(dao.Loggable)
|
||||
if !ok {
|
||||
return fmt.Errorf("Resource %s is not tailable", l.gvr)
|
||||
}
|
||||
|
||||
if err := logger.TailLogs(ctx, c, l.logOptions); err != nil {
|
||||
l.cancelFn()
|
||||
close(c)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) Append(line string) {
|
||||
if line == "" {
|
||||
return
|
||||
}
|
||||
|
||||
l.mx.Lock()
|
||||
defer l.mx.Unlock()
|
||||
|
||||
if l.initialized {
|
||||
l.lines = []string{}
|
||||
l.initialized = false
|
||||
l.fireLogCleared()
|
||||
}
|
||||
log.Debug().Msgf("APPEND %s", line)
|
||||
if len(l.lines) < int(l.logOptions.Lines) {
|
||||
l.lines = append(l.lines, line)
|
||||
} else {
|
||||
l.lines = append(l.lines[1:], line)
|
||||
l.lastSent--
|
||||
if l.lastSent < 0 {
|
||||
l.lastSent = 0
|
||||
}
|
||||
}
|
||||
log.Debug().Msgf("MODEL %d--%d", len(l.lines), l.lastSent)
|
||||
}
|
||||
|
||||
func (l *Log) Notify(timedOut bool) {
|
||||
l.mx.Lock()
|
||||
defer l.mx.Unlock()
|
||||
|
||||
if timedOut || l.lastSent < len(l.lines) {
|
||||
l.fireLogBuffChanged(l.lines[l.lastSent:])
|
||||
l.lastSent = len(l.lines)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Log) updateLogs(ctx context.Context, c <-chan string) {
|
||||
defer func() {
|
||||
log.Debug().Msgf("updateLogs view bailing out!")
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case line, ok := <-c:
|
||||
if !ok {
|
||||
log.Debug().Msgf("Closed channel detected. Bailing out...")
|
||||
l.Append(line)
|
||||
l.Notify(false)
|
||||
return
|
||||
}
|
||||
l.Append(line)
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
l.Notify(true)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddListener adds a new model listener.
|
||||
func (l *Log) AddListener(listener LogsListener) {
|
||||
l.listeners = append(l.listeners, listener)
|
||||
l.fireLogChanged(l.lines)
|
||||
}
|
||||
|
||||
// RemoveListener delete a listener from the lisl.
|
||||
func (l *Log) RemoveListener(listener LogsListener) {
|
||||
victim := -1
|
||||
for i, lis := range l.listeners {
|
||||
if lis == listener {
|
||||
victim = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if victim >= 0 {
|
||||
l.listeners = append(l.listeners[:victim], l.listeners[victim+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func applyFilter(q string, lines []string) ([]string, error) {
|
||||
indexes, err := filter(q, lines)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// No filter!
|
||||
if indexes == nil {
|
||||
return lines, nil
|
||||
}
|
||||
// Blank filter
|
||||
if len(indexes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
filtered := make([]string, 0, len(indexes))
|
||||
for _, idx := range indexes {
|
||||
filtered = append(filtered, lines[idx])
|
||||
}
|
||||
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func (l *Log) fireLogBuffChanged(lines []string) {
|
||||
filtered, err := applyFilter(l.filter, lines)
|
||||
if err != nil {
|
||||
l.fireLogError(err)
|
||||
return
|
||||
}
|
||||
if len(filtered) > 0 {
|
||||
l.fireLogChanged(filtered)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Log) fireLogError(err error) {
|
||||
for _, lis := range l.listeners {
|
||||
lis.LogFailed(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Log) fireLogChanged(lines []string) {
|
||||
for _, lis := range l.listeners {
|
||||
lis.LogChanged(lines)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Log) fireLogCleared() {
|
||||
for _, lis := range l.listeners {
|
||||
lis.LogCleared()
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
var fuzzyRx = regexp.MustCompile(`\A\-f`)
|
||||
|
||||
func isFuzzySelector(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
return fuzzyRx.MatchString(s)
|
||||
}
|
||||
|
||||
func filter(q string, lines []string) ([]int, error) {
|
||||
if q == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if isFuzzySelector(q) {
|
||||
return fuzzyFilter(strings.TrimSpace(q[2:]), lines), nil
|
||||
}
|
||||
indexes, err := filterLogs(q, lines)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Logs filter failed")
|
||||
return nil, err
|
||||
}
|
||||
return indexes, nil
|
||||
}
|
||||
|
||||
func fuzzyFilter(q string, lines []string) []int {
|
||||
matches := make([]int, 0, len(lines))
|
||||
mm := fuzzy.Find(q, lines)
|
||||
for _, m := range mm {
|
||||
matches = append(matches, m.Index)
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
func filterLogs(q string, lines []string) ([]int, error) {
|
||||
rx, err := regexp.Compile(`(?i)` + q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matches := make([]int, 0, len(lines))
|
||||
for i, l := range lines {
|
||||
if rx.MatchString(l) {
|
||||
matches = append(matches, i)
|
||||
}
|
||||
}
|
||||
|
||||
return matches, nil
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/color"
|
||||
)
|
||||
|
||||
type (
|
||||
// Fqn uniquely describes a container
|
||||
Fqn struct {
|
||||
Namespace, Name, Container string
|
||||
}
|
||||
|
||||
// LogOptions represent logger options.
|
||||
LogOptions struct {
|
||||
Fqn
|
||||
|
||||
Lines int64
|
||||
Color color.Paint
|
||||
Previous bool
|
||||
SingleContainer bool
|
||||
MultiPods bool
|
||||
}
|
||||
)
|
||||
|
||||
// HasContainer checks if a container is present.
|
||||
func (o LogOptions) HasContainer() bool {
|
||||
return o.Container != ""
|
||||
}
|
||||
|
||||
// FQN returns resource fully qualified name.
|
||||
func (o LogOptions) FQN() string {
|
||||
return FQN(o.Namespace, o.Name)
|
||||
}
|
||||
|
||||
// Path returns resource descriptor path.
|
||||
func (o LogOptions) Path() string {
|
||||
return o.FQN() + ":" + o.Container
|
||||
}
|
||||
|
||||
// FixedSizeName returns a normalize fixed size pod name if possible.
|
||||
func (o LogOptions) FixedSizeName() string {
|
||||
tokens := strings.Split(o.Name, "-")
|
||||
if len(tokens) < 3 {
|
||||
return o.Name
|
||||
}
|
||||
var s []string
|
||||
for i := 0; i < len(tokens)-1; i++ {
|
||||
s = append(s, tokens[i])
|
||||
}
|
||||
return Truncate(strings.Join(s, "-"), 15) + "-" + tokens[len(tokens)-1]
|
||||
}
|
||||
|
||||
// // NormalizeName colorizes a pod name.
|
||||
// func (o LogOptions) NormalizeName() string {
|
||||
// if o.Color == 0 {
|
||||
// return ""
|
||||
// }
|
||||
|
||||
// return color.Colorize(o.Name+":"+o.Container+" ", o.Color)
|
||||
// // return o.Name + ":" + o.Container + " "
|
||||
// }
|
||||
|
||||
func colorize(c color.Paint, txt string) string {
|
||||
if c == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return color.Colorize(txt, c)
|
||||
}
|
||||
|
||||
// DecorateLog add a log header to display po/co information along with the log message.
|
||||
func (o LogOptions) DecorateLog(msg string) string {
|
||||
if msg == "" {
|
||||
return msg
|
||||
}
|
||||
|
||||
if o.MultiPods {
|
||||
return colorize(o.Color, o.Name+":"+o.Container+" ") + msg
|
||||
}
|
||||
|
||||
if !o.SingleContainer {
|
||||
return colorize(o.Color, o.Container+" ") + msg
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
package model_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/watch"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/informers"
|
||||
)
|
||||
|
||||
func TestLogFullBuffer(t *testing.T) {
|
||||
size := 4
|
||||
m := model.NewLog(client.NewGVR("fred"), "Blee", makeLogOpts(size), 10*time.Millisecond)
|
||||
m.Init(makeFactory())
|
||||
|
||||
v := newTestView()
|
||||
m.AddListener(v)
|
||||
|
||||
data := make([]string, 0, 2*size)
|
||||
for i := 0; i < 2*size; i++ {
|
||||
data = append(data, "line"+strconv.Itoa(i))
|
||||
m.Append(data[i])
|
||||
}
|
||||
m.Notify(false)
|
||||
|
||||
assert.Equal(t, 2, v.dataCalled)
|
||||
assert.Equal(t, 1, v.clearCalled)
|
||||
assert.Equal(t, 0, v.errCalled)
|
||||
assert.Equal(t, data[4:], v.data)
|
||||
}
|
||||
|
||||
func TestLogFilter(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
q string
|
||||
e int
|
||||
}{
|
||||
"plain": {
|
||||
q: "line-1",
|
||||
e: 2,
|
||||
},
|
||||
"regexp": {
|
||||
q: `\Apod-line-[1-3]{1}\z`,
|
||||
e: 3,
|
||||
},
|
||||
"fuzzy": {
|
||||
q: `-f po-l1`,
|
||||
e: 2,
|
||||
},
|
||||
}
|
||||
|
||||
size := 10
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
m := model.NewLog(client.NewGVR("fred"), "Blee", makeLogOpts(size), 10*time.Millisecond)
|
||||
m.Init(makeFactory())
|
||||
|
||||
v := newTestView()
|
||||
m.AddListener(v)
|
||||
|
||||
m.Filter(u.q)
|
||||
var data []string
|
||||
for i := 0; i < size; i++ {
|
||||
data = append(data, fmt.Sprintf("pod-line-%d", i+1))
|
||||
m.Append(data[i])
|
||||
}
|
||||
|
||||
m.Notify(true)
|
||||
assert.Equal(t, 3, v.dataCalled)
|
||||
assert.Equal(t, 2, v.clearCalled)
|
||||
assert.Equal(t, 0, v.errCalled)
|
||||
assert.Equal(t, u.e, len(v.data))
|
||||
|
||||
m.ClearFilter()
|
||||
assert.Equal(t, 4, v.dataCalled)
|
||||
assert.Equal(t, 2, v.clearCalled)
|
||||
assert.Equal(t, 0, v.errCalled)
|
||||
assert.Equal(t, size, len(v.data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogStartStop(t *testing.T) {
|
||||
m := model.NewLog(client.NewGVR("fred"), "Blee", makeLogOpts(4), 10*time.Millisecond)
|
||||
m.Init(makeFactory())
|
||||
|
||||
v := newTestView()
|
||||
m.AddListener(v)
|
||||
|
||||
m.Start()
|
||||
data := []string{"line1", "line2"}
|
||||
for _, d := range data {
|
||||
m.Append(d)
|
||||
}
|
||||
m.Notify(true)
|
||||
m.Stop()
|
||||
|
||||
assert.Equal(t, 2, v.dataCalled)
|
||||
assert.Equal(t, 1, v.clearCalled)
|
||||
assert.Equal(t, 0, v.errCalled)
|
||||
assert.Equal(t, 2, len(v.data))
|
||||
}
|
||||
|
||||
func TestLogClear(t *testing.T) {
|
||||
m := model.NewLog(client.NewGVR("fred"), "Blee", makeLogOpts(4), 10*time.Millisecond)
|
||||
m.Init(makeFactory())
|
||||
assert.Equal(t, "fred", m.GetPath())
|
||||
assert.Equal(t, "blee", m.GetContainer())
|
||||
|
||||
v := newTestView()
|
||||
m.AddListener(v)
|
||||
|
||||
data := []string{"line1", "line2"}
|
||||
for _, d := range data {
|
||||
m.Append(d)
|
||||
}
|
||||
m.Notify(true)
|
||||
m.Clear()
|
||||
|
||||
assert.Equal(t, 2, v.dataCalled)
|
||||
assert.Equal(t, 2, v.clearCalled)
|
||||
assert.Equal(t, 0, v.errCalled)
|
||||
assert.Equal(t, 0, len(v.data))
|
||||
}
|
||||
|
||||
func TestLogBasic(t *testing.T) {
|
||||
m := model.NewLog(client.NewGVR("fred"), "Blee", makeLogOpts(2), 10*time.Millisecond)
|
||||
m.Init(makeFactory())
|
||||
|
||||
v := newTestView()
|
||||
m.AddListener(v)
|
||||
|
||||
data := []string{"line1", "line2"}
|
||||
m.Set(data)
|
||||
|
||||
assert.Equal(t, 2, v.dataCalled)
|
||||
assert.Equal(t, 0, v.clearCalled)
|
||||
assert.Equal(t, 0, v.errCalled)
|
||||
assert.Equal(t, data, v.data)
|
||||
}
|
||||
|
||||
func TestLogAppend(t *testing.T) {
|
||||
m := model.NewLog(client.NewGVR("fred"), "blah blah", makeLogOpts(4), 5*time.Millisecond)
|
||||
m.Init(makeFactory())
|
||||
|
||||
v := newTestView()
|
||||
m.AddListener(v)
|
||||
assert.Equal(t, []string{"blah blah"}, v.data)
|
||||
|
||||
data := []string{"line1", "line2"}
|
||||
for _, d := range data {
|
||||
m.Append(d)
|
||||
}
|
||||
assert.Equal(t, 1, v.dataCalled)
|
||||
assert.Equal(t, []string{}, v.data)
|
||||
|
||||
m.Notify(true)
|
||||
assert.Equal(t, 2, v.dataCalled)
|
||||
assert.Equal(t, 1, v.clearCalled)
|
||||
assert.Equal(t, 0, v.errCalled)
|
||||
assert.Equal(t, data, v.data)
|
||||
}
|
||||
|
||||
func TestLogTimedout(t *testing.T) {
|
||||
m := model.NewLog(client.NewGVR("fred"), "Blee", makeLogOpts(4), 10*time.Millisecond)
|
||||
m.Init(makeFactory())
|
||||
|
||||
v := newTestView()
|
||||
m.AddListener(v)
|
||||
|
||||
m.Filter("line1")
|
||||
data := []string{"line1", "line2", "line3", "line4"}
|
||||
for _, d := range data {
|
||||
m.Append(d)
|
||||
}
|
||||
m.Notify(true)
|
||||
assert.Equal(t, 3, v.dataCalled)
|
||||
assert.Equal(t, 2, v.clearCalled)
|
||||
assert.Equal(t, 0, v.errCalled)
|
||||
assert.Equal(t, []string{"line1"}, v.data)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func makeLogOpts(count int) dao.LogOptions {
|
||||
return dao.LogOptions{
|
||||
Path: "fred",
|
||||
Container: "blee",
|
||||
Lines: int64(count),
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type testView struct {
|
||||
data []string
|
||||
dataCalled int
|
||||
clearCalled int
|
||||
errCalled int
|
||||
}
|
||||
|
||||
func newTestView() *testView {
|
||||
return &testView{}
|
||||
}
|
||||
|
||||
func (t *testView) LogChanged(d []string) {
|
||||
t.data = d
|
||||
t.dataCalled++
|
||||
}
|
||||
func (t *testView) LogCleared() {
|
||||
t.clearCalled++
|
||||
t.data = []string{}
|
||||
}
|
||||
func (t *testView) LogFailed(err error) {
|
||||
fmt.Println("LogErr", err)
|
||||
t.errCalled++
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type testFactory struct{}
|
||||
|
||||
var _ dao.Factory = testFactory{}
|
||||
|
||||
func (f testFactory) Client() client.Connection {
|
||||
return nil
|
||||
}
|
||||
func (f testFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f testFactory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f testFactory) ForResource(ns, gvr string) informers.GenericInformer {
|
||||
return nil
|
||||
}
|
||||
func (f testFactory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f testFactory) WaitForCacheSync() {}
|
||||
func (f testFactory) Forwarders() watch.Forwarders {
|
||||
return nil
|
||||
}
|
||||
func (f testFactory) DeleteForwarder(string) {}
|
||||
|
||||
func makeFactory() dao.Factory {
|
||||
return testFactory{}
|
||||
}
|
||||
|
|
@ -8,15 +8,66 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMenuHintOrder(t *testing.T) {
|
||||
h1 := model.MenuHint{Mnemonic: "b", Description: "Duh"}
|
||||
h2 := model.MenuHint{Mnemonic: "a", Description: "Blee"}
|
||||
h3 := model.MenuHint{Mnemonic: "1", Description: "Zorg"}
|
||||
func TestMenuHintsSort(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
hh model.MenuHints
|
||||
e []int
|
||||
}{
|
||||
"mixed": {
|
||||
hh: model.MenuHints{
|
||||
model.MenuHint{Mnemonic: "2", Description: "Bubba"},
|
||||
model.MenuHint{Mnemonic: "b", Description: "Duh"},
|
||||
model.MenuHint{Mnemonic: "a", Description: "Blee"},
|
||||
model.MenuHint{Mnemonic: "1", Description: "Zorg"},
|
||||
},
|
||||
e: []int{3, 0, 2, 1},
|
||||
},
|
||||
"all_strs": {
|
||||
hh: model.MenuHints{
|
||||
model.MenuHint{Mnemonic: "b", Description: "Bob"},
|
||||
model.MenuHint{Mnemonic: "a", Description: "Abby"},
|
||||
model.MenuHint{Mnemonic: "c", Description: "Chris"},
|
||||
},
|
||||
e: []int{1, 0, 2},
|
||||
},
|
||||
"all_ints": {
|
||||
hh: model.MenuHints{
|
||||
model.MenuHint{Mnemonic: "3", Description: "Bob"},
|
||||
model.MenuHint{Mnemonic: "2", Description: "Abby"},
|
||||
model.MenuHint{Mnemonic: "1", Description: "Chris"},
|
||||
},
|
||||
e: []int{2, 1, 0},
|
||||
},
|
||||
}
|
||||
|
||||
hh := model.MenuHints{h1, h2, h3}
|
||||
sort.Sort(hh)
|
||||
|
||||
assert.Equal(t, h3, hh[0])
|
||||
assert.Equal(t, h2, hh[1])
|
||||
assert.Equal(t, h1, hh[2])
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
o := make(model.MenuHints, len(u.hh))
|
||||
for i := range u.hh {
|
||||
o[i] = u.hh[i]
|
||||
}
|
||||
sort.Sort(u.hh)
|
||||
for i, idx := range u.e {
|
||||
assert.Equal(t, o[idx], u.hh[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMenuHintBlank(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
hint model.MenuHint
|
||||
e bool
|
||||
}{
|
||||
"yes": {hint: model.MenuHint{}, e: true},
|
||||
"no": {hint: model.MenuHint{Mnemonic: "a", Description: "blee"}},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, u.hint.IsBlank())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,15 +79,6 @@ func (s *Stack) AddListener(l StackListener) {
|
|||
}
|
||||
}
|
||||
|
||||
// Dump prints out the stack.
|
||||
func (s *Stack) Dump() {
|
||||
log.Debug().Msgf("--- Stack Dump %p---", s)
|
||||
for i, c := range s.components {
|
||||
log.Debug().Msgf("%d -- %s -- %#v", i, c.Name(), c)
|
||||
}
|
||||
log.Debug().Msg("------------------")
|
||||
}
|
||||
|
||||
// Push adds a new item.
|
||||
func (s *Stack) Push(c Component) {
|
||||
if top := s.Top(); top != nil {
|
||||
|
|
@ -115,8 +106,8 @@ func (s *Stack) Peek() []Component {
|
|||
return s.components
|
||||
}
|
||||
|
||||
// ClearHistory clear out the stack history up to most recent.
|
||||
func (s *Stack) ClearHistory() {
|
||||
// Clear clear out the stack using pops.
|
||||
func (s *Stack) Clear() {
|
||||
for range s.components {
|
||||
s.Pop()
|
||||
}
|
||||
|
|
@ -134,6 +125,9 @@ func (s *Stack) IsLast() bool {
|
|||
|
||||
// Previous returns the previous component if any.
|
||||
func (s *Stack) Previous() Component {
|
||||
if s.Empty() {
|
||||
return nil
|
||||
}
|
||||
if s.IsLast() {
|
||||
return s.Top()
|
||||
}
|
||||
|
|
@ -164,3 +158,15 @@ func (s *Stack) notify(a StackAction, c Component) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
// Dump prints out the stack.
|
||||
func (s *Stack) Dump() {
|
||||
log.Debug().Msgf("--- Stack Dump %p---", s)
|
||||
for i, c := range s.components {
|
||||
log.Debug().Msgf("%d -- %s -- %#v", i, c.Name(), c)
|
||||
}
|
||||
log.Debug().Msg("------------------")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,134 @@ func init() {
|
|||
zerolog.SetGlobalLevel(zerolog.FatalLevel)
|
||||
}
|
||||
|
||||
func TestStackClear(t *testing.T) {
|
||||
comps := []model.Component{makeC("c1"), makeC("c2"), makeC("c3")}
|
||||
uu := map[string]struct {
|
||||
items []model.Component
|
||||
}{
|
||||
"empty": {
|
||||
items: []model.Component{},
|
||||
},
|
||||
"items": {
|
||||
items: comps,
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
s := model.NewStack()
|
||||
for _, c := range u.items {
|
||||
s.Push(c)
|
||||
}
|
||||
s.Clear()
|
||||
assert.True(t, s.Empty())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackPrevious(t *testing.T) {
|
||||
comps := []model.Component{makeC("c1"), makeC("c2"), makeC("c3")}
|
||||
uu := map[string]struct {
|
||||
items []model.Component
|
||||
pops int
|
||||
e model.Component
|
||||
}{
|
||||
"empty": {
|
||||
items: []model.Component{},
|
||||
pops: 0,
|
||||
e: nil,
|
||||
},
|
||||
"one_left": {
|
||||
items: comps,
|
||||
pops: 1,
|
||||
e: comps[0],
|
||||
},
|
||||
"none_left": {
|
||||
items: comps,
|
||||
pops: 2,
|
||||
e: comps[0],
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
s := model.NewStack()
|
||||
for _, c := range u.items {
|
||||
s.Push(c)
|
||||
}
|
||||
for i := 0; i < u.pops; i++ {
|
||||
s.Pop()
|
||||
}
|
||||
assert.Equal(t, u.e, s.Previous())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackIsLast(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
items []model.Component
|
||||
pops int
|
||||
e bool
|
||||
}{
|
||||
"empty": {
|
||||
items: []model.Component{},
|
||||
},
|
||||
"normal": {
|
||||
items: []model.Component{makeC("c1"), makeC("c2"), makeC("c3")},
|
||||
pops: 1,
|
||||
},
|
||||
"last": {
|
||||
items: []model.Component{makeC("c1"), makeC("c2"), makeC("c3")},
|
||||
pops: 2,
|
||||
e: true,
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
s := model.NewStack()
|
||||
for _, c := range u.items {
|
||||
s.Push(c)
|
||||
}
|
||||
for i := 0; i < u.pops; i++ {
|
||||
s.Pop()
|
||||
}
|
||||
assert.Equal(t, u.e, s.IsLast())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackFlatten(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
items []model.Component
|
||||
e []string
|
||||
}{
|
||||
"empty": {
|
||||
items: []model.Component{},
|
||||
e: []string{},
|
||||
},
|
||||
"normal": {
|
||||
items: []model.Component{makeC("c1"), makeC("c2"), makeC("c3")},
|
||||
e: []string{"c1", "c2", "c3"},
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
s := model.NewStack()
|
||||
for _, c := range u.items {
|
||||
s.Push(c)
|
||||
}
|
||||
assert.Equal(t, u.e, s.Flatten())
|
||||
assert.Equal(t, len(u.e), len(s.Peek()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackPush(t *testing.T) {
|
||||
top := c{}
|
||||
uu := map[string]struct {
|
||||
|
|
@ -91,7 +219,7 @@ func TestStackTop(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStackListener(t *testing.T) {
|
||||
func TestStackAddListener(t *testing.T) {
|
||||
items := []model.Component{c{}, c{}, c{}}
|
||||
s := model.NewStack()
|
||||
l := stackL{}
|
||||
|
|
@ -107,12 +235,23 @@ func TestStackListener(t *testing.T) {
|
|||
assert.Equal(t, 0, l.count)
|
||||
}
|
||||
|
||||
func TestStackAddListenerAfter(t *testing.T) {
|
||||
items := []model.Component{c{}, c{}, c{}}
|
||||
s := model.NewStack()
|
||||
l := stackL{}
|
||||
for _, item := range items {
|
||||
s.Push(item)
|
||||
}
|
||||
s.AddListener(&l)
|
||||
assert.Equal(t, 1, l.tops)
|
||||
assert.Equal(t, 0, l.count)
|
||||
}
|
||||
|
||||
func TestStackRemoveListener(t *testing.T) {
|
||||
s := model.NewStack()
|
||||
l1, l2, l3 := &stackL{}, &stackL{}, &stackL{}
|
||||
s.AddListener(l1)
|
||||
s.AddListener(l2)
|
||||
s.AddListener(l3)
|
||||
|
||||
s.RemoveListener(l2)
|
||||
s.RemoveListener(l3)
|
||||
|
|
@ -125,8 +264,11 @@ func TestStackRemoveListener(t *testing.T) {
|
|||
assert.Equal(t, 0, l3.count)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
type stackL struct {
|
||||
count int
|
||||
count, tops int
|
||||
}
|
||||
|
||||
func (s *stackL) StackPushed(model.Component) {
|
||||
|
|
@ -135,11 +277,17 @@ func (s *stackL) StackPushed(model.Component) {
|
|||
func (s *stackL) StackPopped(c, top model.Component) {
|
||||
s.count--
|
||||
}
|
||||
func (s *stackL) StackTop(model.Component) {}
|
||||
func (s *stackL) StackTop(model.Component) { s.tops++ }
|
||||
|
||||
type c struct{}
|
||||
type c struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (c c) Name() string { return "test" }
|
||||
func makeC(n string) c {
|
||||
return c{name: n}
|
||||
}
|
||||
|
||||
func (c c) Name() string { return c.name }
|
||||
func (c c) Hints() model.MenuHints { return nil }
|
||||
func (c c) Draw(tcell.Screen) {}
|
||||
func (c c) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return nil }
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, err
|
|||
}
|
||||
a.Init(factory, client.NewGVR(t.gvr))
|
||||
|
||||
return a.List(ctx, t.namespace)
|
||||
return a.List(ctx, client.CleanseNamespace(t.namespace))
|
||||
}
|
||||
|
||||
func (t *Table) reconcile(ctx context.Context) error {
|
||||
|
|
@ -230,12 +230,12 @@ func (t *Table) reconcile(ctx context.Context) error {
|
|||
return fmt.Errorf("expecting a meta table but got %T", oo[0])
|
||||
}
|
||||
rows = make(render.Rows, len(table.Rows))
|
||||
if err := genericHydrate(t.namespace, table, rows, meta.Renderer); err != nil {
|
||||
if err := genericHydrate(client.CleanseNamespace(t.namespace), table, rows, meta.Renderer); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
rows = make(render.Rows, len(oo))
|
||||
if err := hydrate(t.namespace, oo, rows, meta.Renderer); err != nil {
|
||||
if err := hydrate(client.CleanseNamespace(t.namespace), oo, rows, meta.Renderer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,233 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
"github.com/derailed/k9s/internal/watch"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/informers"
|
||||
)
|
||||
|
||||
func TestTableReconcile(t *testing.T) {
|
||||
ta := NewTable("v1/pods")
|
||||
ta.SetNamespace(client.NamespaceAll)
|
||||
|
||||
f := makeFactory()
|
||||
f.rows = []runtime.Object{load(t, "p1")}
|
||||
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
||||
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
||||
err := ta.reconcile(ctx)
|
||||
assert.Nil(t, err)
|
||||
data := ta.Peek()
|
||||
assert.Equal(t, 13, len(data.Header))
|
||||
assert.Equal(t, 1, len(data.RowEvents))
|
||||
assert.Equal(t, client.NamespaceAll, data.Namespace)
|
||||
}
|
||||
|
||||
func TestTableList(t *testing.T) {
|
||||
ta := NewTable("v1/pods")
|
||||
ta.SetNamespace("blee")
|
||||
|
||||
acc := accessor{}
|
||||
ctx := context.WithValue(context.Background(), internal.KeyFactory, makeFactory())
|
||||
rows, err := ta.list(ctx, &acc)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(rows))
|
||||
}
|
||||
|
||||
func TestTableGet(t *testing.T) {
|
||||
ta := NewTable("v1/pods")
|
||||
ta.SetNamespace("blee")
|
||||
|
||||
f := makeFactory()
|
||||
f.rows = []runtime.Object{load(t, "p1")}
|
||||
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
||||
row, err := ta.Get(ctx, "fred")
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, row)
|
||||
assert.Equal(t, 5, len(row.(*unstructured.Unstructured).Object))
|
||||
}
|
||||
|
||||
func TestTableMeta(t *testing.T) {
|
||||
pd := dao.Pod{}
|
||||
pd.Init(makeFactory(), client.NewGVR("v1/pods"))
|
||||
uu := map[string]struct {
|
||||
gvr string
|
||||
accessor dao.Accessor
|
||||
renderer Renderer
|
||||
}{
|
||||
// BOZO!!
|
||||
// "full": {
|
||||
// gvr: "v1/pods",
|
||||
// accessor: &pd,
|
||||
// renderer: &render.Pod{},
|
||||
// },
|
||||
"generic": {
|
||||
gvr: "containers",
|
||||
accessor: &dao.Container{},
|
||||
renderer: &render.Container{},
|
||||
},
|
||||
"node": {
|
||||
gvr: "v1/nodes",
|
||||
accessor: &dao.Node{},
|
||||
renderer: &render.Node{},
|
||||
},
|
||||
"table": {
|
||||
gvr: "v1/configmaps",
|
||||
accessor: &dao.Table{},
|
||||
renderer: &render.Generic{},
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
ta := NewTable(u.gvr)
|
||||
m := ta.resourceMeta()
|
||||
|
||||
assert.Equal(t, u.accessor, m.DAO)
|
||||
assert.Equal(t, u.renderer, m.Renderer)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableHydrate(t *testing.T) {
|
||||
oo := []runtime.Object{
|
||||
&render.PodWithMetrics{Raw: load(t, "p1")},
|
||||
}
|
||||
rr := make([]render.Row, 1)
|
||||
|
||||
assert.Nil(t, hydrate("blee", oo, rr, render.Pod{}))
|
||||
assert.Equal(t, 1, len(rr))
|
||||
assert.Equal(t, 12, len(rr[0].Fields))
|
||||
}
|
||||
|
||||
func TestTableGenericHydrate(t *testing.T) {
|
||||
raw := raw(t, "p1")
|
||||
tt := metav1beta1.Table{
|
||||
ColumnDefinitions: []metav1beta1.TableColumnDefinition{
|
||||
{Name: "c1"},
|
||||
{Name: "c2"},
|
||||
},
|
||||
Rows: []metav1beta1.TableRow{
|
||||
{
|
||||
Cells: []interface{}{"fred", 10},
|
||||
Object: runtime.RawExtension{Raw: raw},
|
||||
},
|
||||
{
|
||||
Cells: []interface{}{"blee", 20},
|
||||
Object: runtime.RawExtension{Raw: raw},
|
||||
},
|
||||
},
|
||||
}
|
||||
rr := make([]render.Row, 2)
|
||||
re := render.Generic{}
|
||||
re.SetTable(&tt)
|
||||
|
||||
assert.Nil(t, genericHydrate("blee", &tt, rr, &re))
|
||||
assert.Equal(t, 2, len(rr))
|
||||
assert.Equal(t, 2, len(rr[0].Fields))
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func mustLoad(n string) *unstructured.Unstructured {
|
||||
raw, err := ioutil.ReadFile(fmt.Sprintf("test_assets/%s.json", n))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var o unstructured.Unstructured
|
||||
if err = json.Unmarshal(raw, &o); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &o
|
||||
}
|
||||
|
||||
func load(t *testing.T, n string) *unstructured.Unstructured {
|
||||
raw, err := ioutil.ReadFile(fmt.Sprintf("test_assets/%s.json", n))
|
||||
assert.Nil(t, err)
|
||||
var o unstructured.Unstructured
|
||||
err = json.Unmarshal(raw, &o)
|
||||
assert.Nil(t, err)
|
||||
return &o
|
||||
}
|
||||
|
||||
func raw(t *testing.T, n string) []byte {
|
||||
raw, err := ioutil.ReadFile(fmt.Sprintf("test_assets/%s.json", n))
|
||||
assert.Nil(t, err)
|
||||
return raw
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type testFactory struct {
|
||||
rows []runtime.Object
|
||||
}
|
||||
|
||||
var _ dao.Factory = testFactory{}
|
||||
|
||||
func (f testFactory) Client() client.Connection {
|
||||
return nil
|
||||
}
|
||||
func (f testFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) {
|
||||
if len(f.rows) > 0 {
|
||||
return f.rows[0], nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (f testFactory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) {
|
||||
if len(f.rows) > 0 {
|
||||
return f.rows, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (f testFactory) ForResource(ns, gvr string) informers.GenericInformer {
|
||||
return nil
|
||||
}
|
||||
func (f testFactory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f testFactory) WaitForCacheSync() {}
|
||||
func (f testFactory) Forwarders() watch.Forwarders {
|
||||
return nil
|
||||
}
|
||||
func (f testFactory) DeleteForwarder(string) {}
|
||||
|
||||
func makeFactory() testFactory {
|
||||
return testFactory{}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type accessor struct {
|
||||
gvr client.GVR
|
||||
}
|
||||
|
||||
var _ dao.Accessor = (*accessor)(nil)
|
||||
|
||||
func (a *accessor) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||
return []runtime.Object{&render.PodWithMetrics{Raw: mustLoad("p1")}}, nil
|
||||
}
|
||||
|
||||
func (a *accessor) Get(ctx context.Context, path string) (runtime.Object, error) {
|
||||
return &render.PodWithMetrics{Raw: mustLoad("p1")}, nil
|
||||
}
|
||||
|
||||
func (a *accessor) Init(_ dao.Factory, gvr client.GVR) {
|
||||
a.gvr = gvr
|
||||
}
|
||||
func (a *accessor) GVR() string {
|
||||
return a.gvr.String()
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
package model_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
"github.com/derailed/k9s/internal/watch"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/informers"
|
||||
)
|
||||
|
||||
func TestTableRefresh(t *testing.T) {
|
||||
ta := model.NewTable("v1/pods")
|
||||
ta.SetNamespace(client.NamespaceAll)
|
||||
|
||||
l := tableListener{}
|
||||
ta.AddListener(&l)
|
||||
f := makeTableFactory()
|
||||
f.rows = []runtime.Object{mustLoad("p1")}
|
||||
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
||||
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
||||
ta.Refresh(ctx)
|
||||
data := ta.Peek()
|
||||
assert.Equal(t, 13, len(data.Header))
|
||||
assert.Equal(t, 1, len(data.RowEvents))
|
||||
assert.Equal(t, client.NamespaceAll, data.Namespace)
|
||||
assert.Equal(t, 1, l.count)
|
||||
assert.Equal(t, 0, l.errs)
|
||||
}
|
||||
|
||||
func TestTableNS(t *testing.T) {
|
||||
ta := model.NewTable("v1/pods")
|
||||
ta.SetNamespace("blee")
|
||||
|
||||
assert.Equal(t, "blee", ta.GetNamespace())
|
||||
assert.False(t, ta.ClusterWide())
|
||||
assert.False(t, ta.InNamespace("zorg"))
|
||||
}
|
||||
|
||||
func TestTableAddListener(t *testing.T) {
|
||||
ta := model.NewTable("v1/pods")
|
||||
ta.SetNamespace("blee")
|
||||
|
||||
assert.True(t, ta.Empty())
|
||||
l := tableListener{}
|
||||
ta.AddListener(&l)
|
||||
}
|
||||
|
||||
func TestTableRmListener(t *testing.T) {
|
||||
ta := model.NewTable("v1/pods")
|
||||
ta.SetNamespace("blee")
|
||||
|
||||
l := tableListener{}
|
||||
ta.RemoveListener(&l)
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
type tableListener struct {
|
||||
count, errs int
|
||||
}
|
||||
|
||||
func (l *tableListener) TableDataChanged(render.TableData) {
|
||||
l.count++
|
||||
}
|
||||
func (l *tableListener) TableLoadFailed(error) {
|
||||
l.errs++
|
||||
}
|
||||
|
||||
type tableFactory struct {
|
||||
rows []runtime.Object
|
||||
}
|
||||
|
||||
var _ dao.Factory = tableFactory{}
|
||||
|
||||
func (f tableFactory) Client() client.Connection {
|
||||
return nil
|
||||
}
|
||||
func (f tableFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) {
|
||||
if len(f.rows) > 0 {
|
||||
return f.rows[0], nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (f tableFactory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) {
|
||||
if len(f.rows) > 0 {
|
||||
return f.rows, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (f tableFactory) ForResource(ns, gvr string) informers.GenericInformer {
|
||||
return nil
|
||||
}
|
||||
func (f tableFactory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f tableFactory) WaitForCacheSync() {}
|
||||
func (f tableFactory) Forwarders() watch.Forwarders {
|
||||
return nil
|
||||
}
|
||||
func (f tableFactory) DeleteForwarder(string) {}
|
||||
|
||||
func makeTableFactory() tableFactory {
|
||||
return tableFactory{}
|
||||
}
|
||||
|
||||
func mustLoad(n string) *unstructured.Unstructured {
|
||||
raw, err := ioutil.ReadFile(fmt.Sprintf("test_assets/%s.json", n))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var o unstructured.Unstructured
|
||||
if err = json.Unmarshal(raw, &o); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &o
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"kubectl.kubernetes.io/restartedAt": "2019-12-31T12:26:47-07:00"
|
||||
},
|
||||
"creationTimestamp": "2019-12-31T19:27:22Z",
|
||||
"generateName": "nginx-7fb78fb6d8-",
|
||||
"labels": {
|
||||
"app": "nginx",
|
||||
"pod-template-hash": "7fb78fb6d8"
|
||||
},
|
||||
"name": "nginx-7fb78fb6d8-2w75j",
|
||||
"namespace": "default",
|
||||
"ownerReferences": [
|
||||
{
|
||||
"apiVersion": "apps/v1",
|
||||
"blockOwnerDeletion": true,
|
||||
"controller": true,
|
||||
"kind": "ReplicaSet",
|
||||
"name": "nginx-7fb78fb6d8",
|
||||
"uid": "7ccd0600-2c03-11ea-883f-42010a800044"
|
||||
}
|
||||
],
|
||||
"resourceVersion": "87290191",
|
||||
"selfLink": "/api/v1/namespaces/default/pods/nginx-7fb78fb6d8-2w75j",
|
||||
"uid": "91bb1cf2-2c03-11ea-883f-42010a800044"
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"image": "k8s.gcr.io/nginx-slim:0.8",
|
||||
"imagePullPolicy": "IfNotPresent",
|
||||
"name": "nginx",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80,
|
||||
"protocol": "TCP"
|
||||
}
|
||||
],
|
||||
"resources": {
|
||||
"limits": {
|
||||
"cpu": "200m",
|
||||
"memory": "20Mi"
|
||||
},
|
||||
"requests": {
|
||||
"cpu": "200m",
|
||||
"memory": "20Mi"
|
||||
}
|
||||
},
|
||||
"terminationMessagePath": "/dev/termination-log",
|
||||
"terminationMessagePolicy": "File",
|
||||
"volumeMounts": [
|
||||
{
|
||||
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
|
||||
"name": "default-token-dsl46",
|
||||
"readOnly": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"dnsPolicy": "ClusterFirst",
|
||||
"enableServiceLinks": true,
|
||||
"nodeName": "gke-k9s-default-pool-0fa2fb89-lbtf",
|
||||
"priority": 0,
|
||||
"restartPolicy": "Always",
|
||||
"schedulerName": "default-scheduler",
|
||||
"securityContext": {},
|
||||
"serviceAccount": "default",
|
||||
"serviceAccountName": "default",
|
||||
"terminationGracePeriodSeconds": 30,
|
||||
"tolerations": [
|
||||
{
|
||||
"effect": "NoExecute",
|
||||
"key": "node.kubernetes.io/not-ready",
|
||||
"operator": "Exists",
|
||||
"tolerationSeconds": 300
|
||||
},
|
||||
{
|
||||
"effect": "NoExecute",
|
||||
"key": "node.kubernetes.io/unreachable",
|
||||
"operator": "Exists",
|
||||
"tolerationSeconds": 300
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"name": "default-token-dsl46",
|
||||
"secret": {
|
||||
"defaultMode": 420,
|
||||
"secretName": "default-token-dsl46"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"conditions": [
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2019-12-31T19:27:23Z",
|
||||
"status": "True",
|
||||
"type": "Initialized"
|
||||
},
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2019-12-31T19:27:25Z",
|
||||
"status": "True",
|
||||
"type": "Ready"
|
||||
},
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2019-12-31T19:27:25Z",
|
||||
"status": "True",
|
||||
"type": "ContainersReady"
|
||||
},
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2019-12-31T19:27:22Z",
|
||||
"status": "True",
|
||||
"type": "PodScheduled"
|
||||
}
|
||||
],
|
||||
"containerStatuses": [
|
||||
{
|
||||
"containerID": "docker://90e0abf7a779dd76d36038883312baed57a8351428a1d6340df3cff698f51809",
|
||||
"image": "k8s.gcr.io/nginx-slim:0.8",
|
||||
"imageID": "docker-pullable://k8s.gcr.io/nginx-slim@sha256:8b4501fe0fe221df663c22e16539f399e89594552f400408303c42f3dd8d0e52",
|
||||
"lastState": {},
|
||||
"name": "nginx",
|
||||
"ready": true,
|
||||
"restartCount": 0,
|
||||
"state": {
|
||||
"running": {
|
||||
"startedAt": "2019-12-31T19:27:24Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"hostIP": "10.128.0.15",
|
||||
"phase": "Running",
|
||||
"podIP": "10.44.0.229",
|
||||
"qosClass": "Guaranteed",
|
||||
"startTime": "2019-12-31T19:27:23Z"
|
||||
}
|
||||
}
|
||||
|
|
@ -89,7 +89,7 @@ func (c Container) Render(o interface{}, name string, r *Row) error {
|
|||
return fmt.Errorf("Expected ContainerRes, but got %T", o)
|
||||
}
|
||||
|
||||
cur, perc := gatherMetrics(co)
|
||||
cur, perc := gatherMetrics(co.Container, co.MX)
|
||||
ready, state, restarts := "false", MissingValue, "0"
|
||||
if co.Status != nil {
|
||||
ready, state, restarts = boolToStr(co.Status.Ready), toState(co.Status.State), strconv.Itoa(int(co.Status.RestartCount))
|
||||
|
|
@ -119,20 +119,20 @@ func (c Container) Render(o interface{}, name string, r *Row) error {
|
|||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func gatherMetrics(co ContainerRes) (c, p metric) {
|
||||
func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, p metric) {
|
||||
c, p = noMetric(), noMetric()
|
||||
if co.Metrics == nil {
|
||||
if mx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cpu := co.Metrics.Usage.Cpu().MilliValue()
|
||||
mem := ToMB(co.Metrics.Usage.Memory().Value())
|
||||
cpu := mx.Usage.Cpu().MilliValue()
|
||||
mem := ToMB(mx.Usage.Memory().Value())
|
||||
c = metric{
|
||||
cpu: ToMillicore(cpu),
|
||||
mem: ToMi(mem),
|
||||
}
|
||||
|
||||
rcpu, rmem := containerResources(*co.Container)
|
||||
rcpu, rmem := containerResources(*co)
|
||||
if rcpu != nil {
|
||||
p.cpu = AsPerc(toPerc(float64(cpu), float64(rcpu.MilliValue())))
|
||||
}
|
||||
|
|
@ -189,7 +189,7 @@ func probe(p *v1.Probe) string {
|
|||
type ContainerRes struct {
|
||||
Container *v1.Container
|
||||
Status *v1.ContainerStatus
|
||||
Metrics *mv1beta1.ContainerMetrics
|
||||
MX *mv1beta1.ContainerMetrics
|
||||
IsInit bool
|
||||
Age metav1.Time
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func TestContainer(t *testing.T) {
|
|||
cres := render.ContainerRes{
|
||||
Container: makeContainer(),
|
||||
Status: makeContainerStatus(),
|
||||
Metrics: makeContainerMetrics(),
|
||||
MX: makeContainerMetrics(),
|
||||
IsInit: false,
|
||||
Age: makeAge(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error {
|
|||
if !ok {
|
||||
return fmt.Errorf("expecting a TableRow but got %T", o)
|
||||
}
|
||||
|
||||
_, nns, err := resourceNS(row.Object.Raw)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@ func (t *Table) Refresh() {
|
|||
|
||||
// GetSelectedRow returns the entire selected row.
|
||||
func (t *Table) GetSelectedRow() render.Row {
|
||||
return t.model.Peek().RowEvents[t.GetSelectedRowIndex()].Row
|
||||
log.Debug().Msgf("INDEX %d", t.GetSelectedRowIndex())
|
||||
return t.model.Peek().RowEvents[t.GetSelectedRowIndex()-1].Row
|
||||
}
|
||||
|
||||
// NameColIndex returns the index of the resource name column.
|
||||
|
|
@ -307,7 +308,7 @@ func (t *Table) filtered(data render.TableData) render.TableData {
|
|||
return data
|
||||
}
|
||||
q := t.cmdBuff.String()
|
||||
if isFuzzySelector(q) {
|
||||
if IsFuzzySelector(q) {
|
||||
return fuzzyFilter(q[2:], t.NameColIndex(), data)
|
||||
}
|
||||
|
||||
|
|
@ -348,7 +349,7 @@ func (t *Table) styleTitle() string {
|
|||
|
||||
base := strings.Title(t.BaseTitle)
|
||||
ns := t.GetModel().GetNamespace()
|
||||
if ns == client.AllNamespaces {
|
||||
if client.IsAllNamespaces(ns) {
|
||||
ns = client.NamespaceAll
|
||||
}
|
||||
path := t.Path
|
||||
|
|
|
|||
|
|
@ -30,10 +30,10 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
// LabelCmd identifies a label query
|
||||
LabelCmd = regexp.MustCompile(`\A\-l`)
|
||||
// LableRx identifies a label query
|
||||
LableRx = regexp.MustCompile(`\A\-l`)
|
||||
|
||||
fuzzyCmd = regexp.MustCompile(`\A\-f`)
|
||||
fuzzyRx = regexp.MustCompile(`\A\-f`)
|
||||
)
|
||||
|
||||
func mustExtractSyles(ctx context.Context) *config.Styles {
|
||||
|
|
@ -59,15 +59,15 @@ func IsLabelSelector(s string) bool {
|
|||
if s == "" {
|
||||
return false
|
||||
}
|
||||
return LabelCmd.MatchString(s)
|
||||
return LableRx.MatchString(s)
|
||||
}
|
||||
|
||||
// IsFuzztySelector checks if query is fuzzy.
|
||||
func isFuzzySelector(s string) bool {
|
||||
func IsFuzzySelector(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
return fuzzyCmd.MatchString(s)
|
||||
return fuzzyRx.MatchString(s)
|
||||
}
|
||||
|
||||
// TrimLabelSelector extracts label query.
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ func TestTableSelection(t *testing.T) {
|
|||
v.SelectRow(1, true)
|
||||
|
||||
assert.Equal(t, "r1", v.GetSelectedItem())
|
||||
assert.Equal(t, render.Row{ID: "r2", Fields: render.Fields{"blee", "duh", "zorg"}}, v.GetSelectedRow())
|
||||
assert.Equal(t, render.Row{ID: "r1", Fields: render.Fields{"blee", "duh", "fred"}}, v.GetSelectedRow())
|
||||
assert.Equal(t, "blee", v.GetSelectedCell(0))
|
||||
assert.Equal(t, 1, v.GetSelectedRowIndex())
|
||||
assert.Equal(t, []string{"r1"}, v.GetSelectedItems())
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import (
|
|||
|
||||
const (
|
||||
splashTime = 1
|
||||
clusterRefresh = time.Duration(5 * time.Second)
|
||||
clusterRefresh = 5 * time.Second
|
||||
statusIndicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%"
|
||||
clusterInfoWidth = 50
|
||||
clusterInfoPad = 15
|
||||
|
|
@ -178,20 +178,26 @@ func (a *App) clusterUpdater(ctx context.Context) {
|
|||
log.Debug().Msg("Cluster updater canceled!")
|
||||
return
|
||||
case <-time.After(clusterRefresh):
|
||||
a.QueueUpdateDraw(func() {
|
||||
a.refreshClusterInfo()
|
||||
})
|
||||
// BOZO!! refact - should not hold ui for updating clusterinfo
|
||||
a.refreshClusterInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BOZO!! Refact to use model/view strategy.
|
||||
func (a *App) refreshClusterInfo() {
|
||||
if !a.showHeader {
|
||||
a.refreshIndicator()
|
||||
} else {
|
||||
a.clusterInfo().refresh()
|
||||
if !a.Conn().Config().CheckConnectivity() {
|
||||
log.Error().Msgf("Something is wrong with the connection. Bailing out!")
|
||||
a.BailOut()
|
||||
}
|
||||
|
||||
a.QueueUpdateDraw(func() {
|
||||
if !a.showHeader {
|
||||
a.refreshIndicator()
|
||||
} else {
|
||||
a.clusterInfo().refresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) refreshIndicator() {
|
||||
|
|
@ -379,7 +385,6 @@ func (a *App) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
if err := a.gotoResource(a.GetCmd(), true); err != nil {
|
||||
log.Error().Err(err).Msgf("Goto resource for %q failed", a.GetCmd())
|
||||
a.Flash().Err(err)
|
||||
return nil
|
||||
}
|
||||
a.ResetCmd()
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ func (b *Browser) Init(ctx context.Context) error {
|
|||
if err = b.Table.Init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
ns := b.app.Config.ActiveNamespace()
|
||||
ns := client.CleanseNamespace(b.app.Config.ActiveNamespace())
|
||||
if dao.IsK8sMeta(b.meta) {
|
||||
if _, e := b.app.factory.CanForResource(ns, b.GVR(), client.MonitorAccess); e != nil {
|
||||
return e
|
||||
|
|
@ -290,11 +290,12 @@ func (b *Browser) editCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
|
||||
func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
i, _ := strconv.Atoi(string(evt.Rune()))
|
||||
ns := b.namespaces[i]
|
||||
if ns == "" {
|
||||
ns = client.NamespaceAll
|
||||
i, err := strconv.Atoi(string(evt.Rune()))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Fail to switch namespace")
|
||||
return nil
|
||||
}
|
||||
ns := b.namespaces[i]
|
||||
|
||||
auth, err := b.App().factory.Client().CanI(ns, b.GVR(), client.MonitorAccess)
|
||||
if !auth {
|
||||
|
|
@ -323,15 +324,14 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
// Helpers...
|
||||
|
||||
func (b *Browser) setNamespace(ns string) {
|
||||
if b.GetModel().InNamespace(ns) {
|
||||
return
|
||||
}
|
||||
if !b.meta.Namespaced {
|
||||
b.GetModel().SetNamespace(client.ClusterScope)
|
||||
return
|
||||
}
|
||||
if b.GetModel().InNamespace(ns) {
|
||||
return
|
||||
}
|
||||
|
||||
b.GetModel().SetNamespace(client.NormalizeNS(ns))
|
||||
b.GetModel().SetNamespace(client.CleanseNamespace(ns))
|
||||
}
|
||||
|
||||
func (b *Browser) defaultContext() context.Context {
|
||||
|
|
@ -345,7 +345,7 @@ func (b *Browser) defaultContext() context.Context {
|
|||
ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(b.SearchBuff().String()))
|
||||
}
|
||||
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
||||
ctx = context.WithValue(ctx, internal.KeyNamespace, client.NormalizeNS(b.App().Config.ActiveNamespace()))
|
||||
ctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace((b.App().Config.ActiveNamespace())))
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
|
@ -358,12 +358,12 @@ func (b *Browser) namespaceActions(aa ui.KeyActions) {
|
|||
aa[tcell.Key(ui.NumKeys[0])] = ui.NewKeyAction(client.NamespaceAll, b.switchNamespaceCmd, true)
|
||||
b.namespaces[0] = client.NamespaceAll
|
||||
index := 1
|
||||
for _, n := range b.app.Config.FavNamespaces() {
|
||||
if n == client.NamespaceAll {
|
||||
for _, ns := range b.app.Config.FavNamespaces() {
|
||||
if ns == client.NamespaceAll {
|
||||
continue
|
||||
}
|
||||
aa[tcell.Key(ui.NumKeys[index])] = ui.NewKeyAction(n, b.switchNamespaceCmd, true)
|
||||
b.namespaces[index] = n
|
||||
aa[tcell.Key(ui.NumKeys[index])] = ui.NewKeyAction(ns, b.switchNamespaceCmd, true)
|
||||
b.namespaces[index] = ns
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
|
@ -407,7 +407,6 @@ func (b *Browser) simpleDelete(selections []string, msg string) {
|
|||
b.app.Flash().Infof("Delete resource %s %s", b.gvr, selections[0])
|
||||
}
|
||||
for _, sel := range selections {
|
||||
log.Debug().Msgf("YO!! %#v", b.accessor)
|
||||
nuker, ok := b.accessor.(dao.Nuker)
|
||||
if !ok {
|
||||
b.app.Flash().Errf("Invalid nuker %T", b.accessor)
|
||||
|
|
|
|||
|
|
@ -128,7 +128,9 @@ func (c *ClusterInfo) refresh() {
|
|||
cell = c.GetCell(row+1, 1)
|
||||
cell.SetText(render.NAValue)
|
||||
|
||||
c.refreshMetrics(cluster, row)
|
||||
if c.app.Conn().HasMetrics() {
|
||||
c.refreshMetrics(cluster, row)
|
||||
}
|
||||
c.updateStyle()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ func (c *Command) run(cmd string, clearStack bool) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug().Msgf("CMD %v %v %v", gvr, v, err)
|
||||
switch cmds[0] {
|
||||
case "ctx", "context", "contexts":
|
||||
if len(cmds) == 2 && c.app.switchCtx(cmds[1], true) != nil {
|
||||
|
|
@ -160,7 +161,7 @@ func (c *Command) exec(gvr string, comp model.Component, clearStack bool) error
|
|||
log.Error().Err(err).Msg("Config save failed!")
|
||||
}
|
||||
if clearStack {
|
||||
c.app.Content.Stack.ClearHistory()
|
||||
c.app.Content.Stack.Clear()
|
||||
}
|
||||
|
||||
return c.app.inject(comp)
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ func showPodsWithLabels(app *App, path string, sel map[string]string) {
|
|||
|
||||
func showPods(app *App, path, labelSel, fieldSel string) {
|
||||
log.Debug().Msgf("SHOW PODS %q -- %q -- %q", path, labelSel, fieldSel)
|
||||
app.switchNS("")
|
||||
app.switchNS(client.AllNamespaces)
|
||||
|
||||
v := NewPod(client.NewGVR("v1/pods"))
|
||||
v.SetContextFn(podCtx(app, path, labelSel, fieldSel))
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
|
|
@ -21,41 +20,39 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
logTitle = "logs"
|
||||
logBuffSize = 100
|
||||
logTitle = "logs"
|
||||
logMessage = "Waiting for logs..."
|
||||
logCoFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:bg:-]) "
|
||||
logFmt = " Logs([fg:bg:]%s) "
|
||||
|
||||
// FlushTimeout represents a duration between log flushes.
|
||||
FlushTimeout = 200 * time.Millisecond
|
||||
|
||||
logCoFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:bg:-]) "
|
||||
logFmt = " Logs([fg:bg:]%s) "
|
||||
// BOZO!! Canned! Need config tail line counts!
|
||||
tailLineCount = 1_000
|
||||
defaultTimeout = 200 * time.Millisecond
|
||||
)
|
||||
|
||||
// Log represents a generic log viewer.
|
||||
type Log struct {
|
||||
*tview.Flex
|
||||
|
||||
app *App
|
||||
logs *Details
|
||||
indicator *LogIndicator
|
||||
ansiWriter io.Writer
|
||||
path, container string
|
||||
cancelFn context.CancelFunc
|
||||
previous bool
|
||||
gvr client.GVR
|
||||
app *App
|
||||
logs *Details
|
||||
indicator *LogIndicator
|
||||
ansiWriter io.Writer
|
||||
cmdBuff *ui.CmdBuff
|
||||
model *model.Log
|
||||
}
|
||||
|
||||
var _ model.Component = &Log{}
|
||||
var _ model.Component = (*Log)(nil)
|
||||
|
||||
// NewLog returns a new viewer.
|
||||
func NewLog(gvr client.GVR, path, co string, prev bool) *Log {
|
||||
return &Log{
|
||||
gvr: gvr,
|
||||
Flex: tview.NewFlex(),
|
||||
path: path,
|
||||
container: co,
|
||||
previous: prev,
|
||||
l := Log{
|
||||
Flex: tview.NewFlex(),
|
||||
cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff),
|
||||
model: model.NewLog(gvr, logMessage, buildLogOpts(path, co, prev, tailLineCount), defaultTimeout),
|
||||
}
|
||||
|
||||
return &l
|
||||
}
|
||||
|
||||
// Init initialiazes the viewer.
|
||||
|
|
@ -64,12 +61,11 @@ func (l *Log) Init(ctx context.Context) (err error) {
|
|||
if l.app, err = extractApp(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.SetBorder(true)
|
||||
l.SetBorderPadding(0, 0, 1, 1)
|
||||
l.SetDirection(tview.FlexRow)
|
||||
|
||||
l.indicator = NewLogIndicator(l.app.Styles)
|
||||
l.indicator = NewLogIndicator(l.app.Config, l.app.Styles)
|
||||
l.AddItem(l.indicator, 1, 1, false)
|
||||
|
||||
l.logs = NewDetails(l.app, "", "")
|
||||
|
|
@ -86,10 +82,48 @@ func (l *Log) Init(ctx context.Context) (err error) {
|
|||
|
||||
l.StylesChanged(l.app.Styles)
|
||||
l.app.Styles.AddListener(l)
|
||||
l.goFullScreen()
|
||||
|
||||
l.model.Init(l.app.factory)
|
||||
l.model.AddListener(l)
|
||||
l.updateTitle()
|
||||
|
||||
l.cmdBuff.AddListener(l.app.Cmd())
|
||||
l.cmdBuff.AddListener(l)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogCleared clears the logs.
|
||||
func (l *Log) LogCleared() {
|
||||
log.Debug().Msgf("LOG-CLEARED")
|
||||
l.app.QueueUpdateDraw(func() {
|
||||
l.logs.Clear()
|
||||
l.logs.ScrollTo(0, 0)
|
||||
})
|
||||
}
|
||||
|
||||
// LogErrored notifies an error occurred.
|
||||
func (l *Log) LogFailed(err error) {
|
||||
l.app.Flash().Err(err)
|
||||
}
|
||||
|
||||
// LogsChanged updates the logs.
|
||||
func (l *Log) LogChanged(lines []string) {
|
||||
log.Debug().Msgf("LOG-CHANGED %d", len(lines))
|
||||
l.app.QueueUpdateDraw(func() {
|
||||
l.Flush(lines)
|
||||
})
|
||||
}
|
||||
|
||||
// BufferChanged indicates the buffer was changed.
|
||||
func (l *Log) BufferChanged(s string) {}
|
||||
|
||||
// BufferActive indicates the buff activity changed.
|
||||
func (l *Log) BufferActive(state bool, k ui.BufferKind) {
|
||||
l.app.BufferActive(state, k)
|
||||
}
|
||||
|
||||
// StylesChanged reports skin changes.
|
||||
func (l *Log) StylesChanged(s *config.Styles) {
|
||||
l.SetBackgroundColor(config.AsColor(s.Views().Log.BgColor))
|
||||
|
|
@ -97,6 +131,11 @@ func (l *Log) StylesChanged(s *config.Styles) {
|
|||
l.logs.SetBackgroundColor(config.AsColor(s.Views().Log.BgColor))
|
||||
}
|
||||
|
||||
// GetModel returns the log model.
|
||||
func (l *Log) GetModel() *model.Log {
|
||||
return l.model
|
||||
}
|
||||
|
||||
// Hints returns a collection of menu hints.
|
||||
func (l *Log) Hints() model.MenuHints {
|
||||
return l.logs.Actions().Hints()
|
||||
|
|
@ -104,23 +143,17 @@ func (l *Log) Hints() model.MenuHints {
|
|||
|
||||
// Start runs the component.
|
||||
func (l *Log) Start() {
|
||||
l.Stop()
|
||||
if err := l.doLoad(); err != nil {
|
||||
l.app.Flash().Err(err)
|
||||
l.log("😂 Doh! No logs are available at this time. Check again later on...")
|
||||
return
|
||||
}
|
||||
l.model.Start()
|
||||
l.app.SetFocus(l)
|
||||
}
|
||||
|
||||
// Stop terminates the component.
|
||||
func (l *Log) Stop() {
|
||||
if l.cancelFn != nil {
|
||||
log.Debug().Msgf("<<<< Logger STOP!")
|
||||
l.cancelFn()
|
||||
l.cancelFn = nil
|
||||
}
|
||||
l.model.Stop()
|
||||
l.model.RemoveListener(l)
|
||||
l.app.Styles.RemoveListener(l)
|
||||
l.cmdBuff.RemoveListener(l)
|
||||
l.cmdBuff.RemoveListener(l.app.Cmd())
|
||||
}
|
||||
|
||||
// Name returns the component name.
|
||||
|
|
@ -128,82 +161,43 @@ func (l *Log) Name() string { return logTitle }
|
|||
|
||||
func (l *Log) bindKeys() {
|
||||
l.logs.Actions().Set(ui.KeyActions{
|
||||
tcell.KeyEscape: ui.NewKeyAction("Back", l.app.PrevCmd, true),
|
||||
ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true),
|
||||
ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true),
|
||||
ui.KeyF: ui.NewKeyAction("FullScreen", l.fullScreenCmd, true),
|
||||
ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.textWrapCmd, true),
|
||||
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true),
|
||||
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false),
|
||||
tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, true),
|
||||
ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true),
|
||||
ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.ToggleAutoScrollCmd, true),
|
||||
ui.KeyF: ui.NewKeyAction("FullScreen", l.fullScreenCmd, true),
|
||||
ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.textWrapCmd, true),
|
||||
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true),
|
||||
ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", l.activateCmd, false),
|
||||
tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", l.clearCmd, false),
|
||||
tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", l.eraseCmd, false),
|
||||
tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", l.eraseCmd, false),
|
||||
tcell.KeyDelete: ui.NewSharedKeyAction("Erase", l.eraseCmd, false),
|
||||
})
|
||||
}
|
||||
|
||||
func (l *Log) doLoad() error {
|
||||
l.logs.Clear()
|
||||
l.setTitle(l.path, l.container)
|
||||
|
||||
var ctx context.Context
|
||||
ctx = context.WithValue(context.Background(), internal.KeyFactory, l.app.factory)
|
||||
ctx, l.cancelFn = context.WithCancel(ctx)
|
||||
|
||||
c := make(chan string, 10)
|
||||
go l.updateLogs(ctx, c, logBuffSize)
|
||||
|
||||
accessor, err := dao.AccessorFor(l.app.factory, l.gvr)
|
||||
if err != nil {
|
||||
return err
|
||||
func (l *Log) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
key := evt.Key()
|
||||
if key == tcell.KeyUp || key == tcell.KeyDown {
|
||||
return evt
|
||||
}
|
||||
logger, ok := accessor.(dao.Loggable)
|
||||
if !ok {
|
||||
return fmt.Errorf("Resource %s is not tailable", l.gvr)
|
||||
}
|
||||
|
||||
if err := logger.TailLogs(ctx, c, l.logOpts(l.path, l.container, l.previous)); err != nil {
|
||||
l.cancelFn()
|
||||
close(c)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) logOpts(path, co string, prevLogs bool) dao.LogOptions {
|
||||
return dao.LogOptions{
|
||||
Path: path,
|
||||
Container: co,
|
||||
Lines: int64(l.app.Config.K9s.LogRequestSize),
|
||||
Previous: prevLogs,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Log) updateLogs(ctx context.Context, c <-chan string, buffSize int) {
|
||||
defer func() {
|
||||
log.Debug().Msgf("updateLogs view bailing out!")
|
||||
}()
|
||||
buff, index := make([]string, buffSize), 0
|
||||
for {
|
||||
select {
|
||||
case line, ok := <-c:
|
||||
if !ok {
|
||||
log.Debug().Msgf("Closed channel detected. Bailing out...")
|
||||
l.Flush(index, buff)
|
||||
return
|
||||
if key == tcell.KeyRune {
|
||||
if l.cmdBuff.IsActive() {
|
||||
l.cmdBuff.Add(evt.Rune())
|
||||
if err := l.model.Filter(l.cmdBuff.String()); err != nil {
|
||||
l.app.Flash().Err(err)
|
||||
}
|
||||
if index < buffSize {
|
||||
buff[index] = line
|
||||
index++
|
||||
continue
|
||||
}
|
||||
l.Flush(index, buff)
|
||||
index = 0
|
||||
buff[index] = line
|
||||
index++
|
||||
case <-time.After(FlushTimeout):
|
||||
l.Flush(index, buff)
|
||||
index = 0
|
||||
case <-ctx.Done():
|
||||
return
|
||||
l.updateTitle()
|
||||
return nil
|
||||
}
|
||||
key = extractKey(evt)
|
||||
}
|
||||
|
||||
if a, ok := l.logs.Actions()[key]; ok {
|
||||
return a.Action(evt)
|
||||
}
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
// Indicator returns the scroll mode viewer.
|
||||
|
|
@ -211,58 +205,101 @@ func (l *Log) Indicator() *LogIndicator {
|
|||
return l.indicator
|
||||
}
|
||||
|
||||
func (l *Log) setTitle(path, co string) {
|
||||
func (l *Log) updateTitle() {
|
||||
var fmat string
|
||||
path, co := l.model.GetPath(), l.model.GetContainer()
|
||||
if co == "" {
|
||||
fmat = ui.SkinTitle(fmt.Sprintf(logFmt, path), l.app.Styles.Frame())
|
||||
} else {
|
||||
fmat = ui.SkinTitle(fmt.Sprintf(logCoFmt, path, co), l.app.Styles.Frame())
|
||||
}
|
||||
l.path = path
|
||||
|
||||
buff := l.cmdBuff.String()
|
||||
if buff != "" {
|
||||
fmat += ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), l.app.Styles.Frame())
|
||||
}
|
||||
l.SetTitle(fmat)
|
||||
}
|
||||
|
||||
func (l *Log) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
key := evt.Key()
|
||||
if key == tcell.KeyRune {
|
||||
key = tcell.Key(evt.Rune())
|
||||
}
|
||||
if m, ok := l.logs.Actions()[key]; ok {
|
||||
log.Debug().Msgf(">> LogView handled %s", tcell.KeyNames[key])
|
||||
return m.Action(evt)
|
||||
}
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
// Logs returns the log viewer.
|
||||
func (l *Log) Logs() *Details {
|
||||
return l.logs
|
||||
}
|
||||
|
||||
func (l *Log) log(lines string) {
|
||||
func (l *Log) write(lines string) {
|
||||
fmt.Fprintln(l.ansiWriter, tview.Escape(lines))
|
||||
log.Debug().Msgf("LOG LINES %d", l.logs.GetLineCount())
|
||||
}
|
||||
|
||||
// Flush write logs to viewer.
|
||||
func (l *Log) Flush(index int, buff []string) {
|
||||
if index == 0 || !l.indicator.AutoScroll() {
|
||||
func (l *Log) Flush(lines []string) {
|
||||
if !l.indicator.AutoScroll() {
|
||||
return
|
||||
}
|
||||
l.log(strings.Join(buff[:index], "\n"))
|
||||
l.app.QueueUpdateDraw(func() {
|
||||
l.indicator.Refresh()
|
||||
l.logs.ScrollToEnd()
|
||||
})
|
||||
l.write(strings.Join(lines, "\n"))
|
||||
l.indicator.Refresh()
|
||||
l.logs.ScrollToEnd()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Actions()...
|
||||
|
||||
func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !l.cmdBuff.IsActive() {
|
||||
return evt
|
||||
}
|
||||
l.cmdBuff.SetActive(false)
|
||||
if err := l.model.Filter(l.cmdBuff.String()); err != nil {
|
||||
l.app.Flash().Err(err)
|
||||
}
|
||||
l.updateTitle()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if l.app.InCmdMode() {
|
||||
return evt
|
||||
}
|
||||
l.app.Flash().Info("Filter mode activated.")
|
||||
l.cmdBuff.SetActive(true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !l.cmdBuff.IsActive() {
|
||||
return nil
|
||||
}
|
||||
l.cmdBuff.Delete()
|
||||
if err := l.model.Filter(l.cmdBuff.String()); err != nil {
|
||||
l.app.Flash().Err(err)
|
||||
}
|
||||
l.updateTitle()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !l.cmdBuff.InCmdMode() {
|
||||
l.cmdBuff.Reset()
|
||||
return l.app.PrevCmd(evt)
|
||||
}
|
||||
|
||||
if l.cmdBuff.String() != "" {
|
||||
l.model.ClearFilter()
|
||||
}
|
||||
l.app.Flash().Info("Clearing filter...")
|
||||
l.cmdBuff.SetActive(false)
|
||||
l.cmdBuff.Reset()
|
||||
l.updateTitle()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveCmd dumps the logs to file.
|
||||
func (l *Log) SaveCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if path, err := saveData(l.app.Config.K9s.CurrentCluster, l.path, l.logs.GetText(true)); err != nil {
|
||||
if path, err := saveData(l.app.Config.K9s.CurrentCluster, l.model.GetPath(), l.logs.GetText(true)); err != nil {
|
||||
l.app.Flash().Err(err)
|
||||
} else {
|
||||
l.app.Flash().Infof("Log %s saved successfully!", path)
|
||||
|
|
@ -304,8 +341,7 @@ func saveData(cluster, name, data string) (string, error) {
|
|||
|
||||
func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey {
|
||||
l.app.Flash().Info("Clearing logs...")
|
||||
l.logs.Clear()
|
||||
l.logs.ScrollTo(0, 0)
|
||||
l.model.Clear()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -315,13 +351,18 @@ func (l *Log) textWrapCmd(*tcell.EventKey) *tcell.EventKey {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) toggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
func (l *Log) ToggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
l.indicator.ToggleAutoScroll()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) fullScreenCmd(*tcell.EventKey) *tcell.EventKey {
|
||||
l.indicator.ToggleFullScreen()
|
||||
l.goFullScreen()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) goFullScreen() {
|
||||
sidePadding := 1
|
||||
if l.indicator.FullScreen() {
|
||||
sidePadding = 0
|
||||
|
|
@ -329,6 +370,25 @@ func (l *Log) fullScreenCmd(*tcell.EventKey) *tcell.EventKey {
|
|||
l.SetFullScreen(l.indicator.FullScreen())
|
||||
l.Box.SetBorder(!l.indicator.FullScreen())
|
||||
l.Flex.SetBorderPadding(0, 0, sidePadding, sidePadding)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
// AsKey converts rune to keyboard key.,
|
||||
func extractKey(evt *tcell.EventKey) tcell.Key {
|
||||
key := tcell.Key(evt.Rune())
|
||||
if evt.Modifiers() == tcell.ModAlt {
|
||||
key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers()))
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func buildLogOpts(path, co string, prevLogs bool, tailLineCount int) dao.LogOptions {
|
||||
return dao.LogOptions{
|
||||
Path: path,
|
||||
Container: co,
|
||||
Lines: int64(tailLineCount),
|
||||
Previous: prevLogs,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,11 +19,12 @@ type LogIndicator struct {
|
|||
}
|
||||
|
||||
// NewLogIndicator returns a new indicator.
|
||||
func NewLogIndicator(styles *config.Styles) *LogIndicator {
|
||||
func NewLogIndicator(cfg *config.Config, styles *config.Styles) *LogIndicator {
|
||||
l := LogIndicator{
|
||||
styles: styles,
|
||||
TextView: tview.NewTextView(),
|
||||
scrollStatus: 1,
|
||||
fullScreen: cfg.K9s.FullScreenLogs,
|
||||
}
|
||||
l.SetBackgroundColor(config.AsColor(styles.Views().Log.BgColor))
|
||||
l.SetTextAlign(tview.AlignRight)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
func TestLogIndicatorRefresh(t *testing.T) {
|
||||
defaults := config.NewStyles()
|
||||
v := view.NewLogIndicator(defaults)
|
||||
v := view.NewLogIndicator(config.NewConfig(nil), defaults)
|
||||
v.Refresh()
|
||||
|
||||
assert.Equal(t, "[black:orange:b] Autoscroll: On [black:orange:b] FullScreen: Off [black:orange:b] Wrap: Off \n", v.GetText(false))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package view
|
||||
package view_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/view"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
|
@ -27,25 +28,25 @@ func TestLogAnsi(t *testing.T) {
|
|||
assert.Equal(t, s+"\n", v.GetText(false))
|
||||
}
|
||||
|
||||
func TestLogFlush(t *testing.T) {
|
||||
v := NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
|
||||
func TestLogAutoScroll(t *testing.T) {
|
||||
v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
|
||||
v.Init(makeContext())
|
||||
v.Flush(2, []string{"blee", "bozo"})
|
||||
v.GetModel().Set([]string{"blee", "bozo"})
|
||||
v.GetModel().Notify(true)
|
||||
|
||||
v.toggleAutoScrollCmd(nil)
|
||||
assert.Equal(t, "blee\nbozo\n", v.Logs().GetText(true))
|
||||
v.ToggleAutoScrollCmd(nil)
|
||||
assert.Equal(t, " Autoscroll: Off FullScreen: Off Wrap: Off ", v.Indicator().GetText(true))
|
||||
v.toggleAutoScrollCmd(nil)
|
||||
v.ToggleAutoScrollCmd(nil)
|
||||
assert.Equal(t, " Autoscroll: On FullScreen: Off Wrap: Off ", v.Indicator().GetText(true))
|
||||
assert.Equal(t, 6, len(v.Hints()))
|
||||
}
|
||||
|
||||
func TestLogViewSave(t *testing.T) {
|
||||
v := NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
|
||||
v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
|
||||
v.Init(makeContext())
|
||||
|
||||
app := makeApp()
|
||||
v.Flush(2, []string{"blee", "bozo"})
|
||||
v.Flush([]string{"blee", "bozo"})
|
||||
config.K9sDumpDir = "/tmp"
|
||||
dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster)
|
||||
c1, _ := ioutil.ReadDir(dir)
|
||||
|
|
@ -55,28 +56,26 @@ func TestLogViewSave(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLogViewNav(t *testing.T) {
|
||||
v := NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
|
||||
v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
|
||||
v.Init(makeContext())
|
||||
|
||||
var buff []string
|
||||
for i := 0; i < 100; i++ {
|
||||
buff = append(buff, fmt.Sprintf("line-%d\n", i))
|
||||
}
|
||||
v.Flush(100, buff)
|
||||
v.toggleAutoScrollCmd(nil)
|
||||
v.GetModel().Set(buff)
|
||||
v.ToggleAutoScrollCmd(nil)
|
||||
|
||||
r, _ := v.Logs().GetScrollOffset()
|
||||
assert.Equal(t, 0, r)
|
||||
}
|
||||
|
||||
func TestLogViewClear(t *testing.T) {
|
||||
v := NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
|
||||
v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
|
||||
v.Init(makeContext())
|
||||
|
||||
v.Flush(2, []string{"blee", "bozo"})
|
||||
|
||||
v.toggleAutoScrollCmd(nil)
|
||||
assert.Equal(t, "blee\nbozo\n", v.Logs().GetText(true))
|
||||
v.ToggleAutoScrollCmd(nil)
|
||||
v.Logs().SetText("blee\nblah")
|
||||
v.Logs().Clear()
|
||||
assert.Equal(t, "", v.Logs().GetText(true))
|
||||
}
|
||||
|
|
@ -84,6 +83,6 @@ func TestLogViewClear(t *testing.T) {
|
|||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func makeApp() *App {
|
||||
return NewApp(config.NewConfig(ks{}))
|
||||
func makeApp() *view.App {
|
||||
return view.NewApp(config.NewConfig(ks{}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ func (l *LogsExtender) logsCmd(prev bool) func(evt *tcell.EventKey) *tcell.Event
|
|||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !isResourcePath(path) {
|
||||
path = l.GetTable().Path
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ func (s *Service) showPods(app *App, _ ui.Tabular, gvr, path string) {
|
|||
app.Flash().Err(err)
|
||||
return
|
||||
}
|
||||
|
||||
var svc v1.Service
|
||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &svc)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -25,10 +25,13 @@ type Table struct {
|
|||
|
||||
// NewTable returns a new viewer.
|
||||
func NewTable(gvr client.GVR) *Table {
|
||||
return &Table{
|
||||
t := Table{
|
||||
Table: ui.NewTable(gvr.String()),
|
||||
gvr: gvr,
|
||||
}
|
||||
t.envFn = t.defaultK9sEnv
|
||||
|
||||
return &t
|
||||
}
|
||||
|
||||
// Init initializes the component
|
||||
|
|
@ -40,7 +43,6 @@ func (t *Table) Init(ctx context.Context) (err error) {
|
|||
t.Table.Init(ctx)
|
||||
t.bindKeys()
|
||||
t.GetModel().SetRefreshRate(time.Duration(t.app.Config.K9s.GetRefreshRate()) * time.Second)
|
||||
t.envFn = t.defaultK9sEnv
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,8 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
defaultResync = 10 * time.Minute
|
||||
allNamespaces = ""
|
||||
clusterScope = "-"
|
||||
defaultResync = 10 * time.Minute
|
||||
defaultWaitTime = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
// Factory tracks various resource informers.
|
||||
|
|
@ -65,12 +64,9 @@ func (f *Factory) Terminate() {
|
|||
// List returns a resource collection.
|
||||
func (f *Factory) List(gvr, ns string, wait bool, labels labels.Selector) ([]runtime.Object, error) {
|
||||
defer func(t time.Time) {
|
||||
log.Debug().Msgf("FACTORY-LIST %q::%q elapsed %v", ns, gvr, time.Since(t))
|
||||
log.Debug().Msgf("FACTORY-LIST [%t] %q::%q elapsed %v", wait, ns, gvr, time.Since(t))
|
||||
}(time.Now())
|
||||
|
||||
if ns == clusterScope {
|
||||
ns = allNamespaces
|
||||
}
|
||||
log.Debug().Msgf("List %q:%q", ns, gvr)
|
||||
inf, err := f.CanForResource(ns, gvr, client.MonitorAccess)
|
||||
if err != nil {
|
||||
|
|
@ -83,13 +79,16 @@ func (f *Factory) List(gvr, ns string, wait bool, labels labels.Selector) ([]run
|
|||
return inf.Lister().List(labels)
|
||||
}
|
||||
|
||||
if client.IsAllNamespace(ns) {
|
||||
ns = client.AllNamespaces
|
||||
}
|
||||
return inf.Lister().ByNamespace(ns).List(labels)
|
||||
}
|
||||
|
||||
// Get retrieves a given resource.
|
||||
func (f *Factory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) {
|
||||
defer func(t time.Time) {
|
||||
log.Debug().Msgf("FACTORY-GET %q--%q elapsed %v", gvr, path, time.Since(t))
|
||||
log.Debug().Msgf("FACTORY-GET [%t] %q--%q elapsed %v", wait, gvr, path, time.Since(t))
|
||||
}(time.Now())
|
||||
|
||||
ns, n := namespaced(path)
|
||||
|
|
@ -111,18 +110,30 @@ func (f *Factory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime
|
|||
}
|
||||
|
||||
func (f *Factory) waitForCacheSync(ns string) {
|
||||
if fac, ok := f.factories[ns]; ok {
|
||||
// Hang for a sec for the cache to refresh if still not done bail out!
|
||||
const dur = 1 * time.Second
|
||||
c := make(chan struct{})
|
||||
go func(c chan struct{}) {
|
||||
<-time.After(dur)
|
||||
log.Debug().Msgf("Wait for sync timed out!")
|
||||
close(c)
|
||||
}(c)
|
||||
fac.WaitForCacheSync(c)
|
||||
log.Debug().Msgf("Sync completed for ns %q", ns)
|
||||
if client.IsClusterWide(ns) {
|
||||
ns = client.AllNamespaces
|
||||
}
|
||||
|
||||
if f.isClusterWide() {
|
||||
ns = client.AllNamespaces
|
||||
}
|
||||
fac, ok := f.factories[ns]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Debug().Msgf("!!!!!! WAIT FOR CACHE-SYNC %q", ns)
|
||||
// Hang for a sec for the cache to refresh if still not done bail out!
|
||||
c := make(chan struct{})
|
||||
go func(c chan struct{}) {
|
||||
<-time.After(defaultWaitTime)
|
||||
log.Debug().Msgf("Wait for sync timed out!")
|
||||
close(c)
|
||||
}(c)
|
||||
mm := fac.WaitForCacheSync(c)
|
||||
for k, v := range mm {
|
||||
log.Debug().Msgf("%t -- %s", v, k)
|
||||
}
|
||||
log.Debug().Msgf("Sync completed for ns %q", ns)
|
||||
}
|
||||
|
||||
// WaitForCacheSync waits for all factories to update their cache.
|
||||
|
|
@ -153,17 +164,17 @@ func (f *Factory) SetActiveNS(ns string) {
|
|||
}
|
||||
|
||||
func (f *Factory) isClusterWide() bool {
|
||||
_, ok := f.factories[allNamespaces]
|
||||
_, ok := f.factories[client.AllNamespaces]
|
||||
return ok
|
||||
}
|
||||
|
||||
// CanForResource return an informer is user has access.
|
||||
func (f *Factory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) {
|
||||
// If user can access resource cluster wide, prefer cluster wide factory.
|
||||
if ns != allNamespaces {
|
||||
auth, err := f.Client().CanI(allNamespaces, gvr, verbs)
|
||||
if !client.IsClusterWide(ns) {
|
||||
auth, err := f.Client().CanI(client.AllNamespaces, gvr, verbs)
|
||||
if auth && err == nil {
|
||||
return f.ForResource(allNamespaces, gvr), nil
|
||||
return f.ForResource(client.AllNamespaces, gvr), nil
|
||||
}
|
||||
}
|
||||
auth, err := f.Client().CanI(ns, gvr, verbs)
|
||||
|
|
@ -192,16 +203,15 @@ func (f *Factory) ForResource(ns, gvr string) informers.GenericInformer {
|
|||
}
|
||||
|
||||
func (f *Factory) ensureFactory(ns string) di.DynamicSharedInformerFactory {
|
||||
if ns == clusterScope {
|
||||
ns = allNamespaces
|
||||
if client.IsClusterWide(ns) {
|
||||
ns = client.AllNamespaces
|
||||
}
|
||||
f.mx.Lock()
|
||||
defer f.mx.Unlock()
|
||||
if fac, ok := f.factories[ns]; ok {
|
||||
return fac
|
||||
}
|
||||
|
||||
log.Debug().Msgf("FACTORY_NEW for ns %q", ns)
|
||||
f.mx.Lock()
|
||||
defer f.mx.Unlock()
|
||||
log.Debug().Msgf("FACTORY_CREATE for ns %q", ns)
|
||||
f.factories[ns] = di.NewFilteredDynamicSharedInformerFactory(
|
||||
f.client.DynDialOrDie(),
|
||||
defaultResync,
|
||||
|
|
|
|||
Loading…
Reference in New Issue