diff --git a/go.sum b/go.sum index bd43693a..51e30b22 100644 --- a/go.sum +++ b/go.sum @@ -386,6 +386,7 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/storageos/go-api v0.0.0-20180912212459-343b3eff91fc/go.mod h1:ZrLn+e0ZuF3Y65PNF6dIwbJPZqfmtCXxFm9ckv0agOY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/internal/config/alias.go b/internal/config/alias.go new file mode 100644 index 00000000..875f8a5e --- /dev/null +++ b/internal/config/alias.go @@ -0,0 +1,122 @@ +package config + +import ( + "io/ioutil" + "path/filepath" + + "github.com/rs/zerolog/log" + "gopkg.in/yaml.v2" +) + +// K9sAlias stores K9s command aliases. +var K9sAlias = filepath.Join(K9sHome, "alias.yml") + +type Alias map[string]string + +// Aliases represents a collection of aliases. +type Aliases struct { + Alias Alias `yaml:"alias"` +} + +// NewAliases return a new alias. +func NewAliases() Aliases { + aa := Aliases{Alias: make(Alias, 50)} + aa.loadDefaults() + return aa +} + +func (a Aliases) loadDefaults() { + a.Alias["dp"] = "apps/v1/deployments" + a.Alias["sec"] = "v1/secrets" + a.Alias["jo"] = "batch/v1/jobs" + a.Alias["cr"] = "rbac.authorization.k8s.io/v1/clusterroles" + a.Alias["crb"] = "rbac.authorization.k8s.io/v1/clusterrolebindings" + a.Alias["ro"] = "rbac.authorization.k8s.io/v1/roles" + a.Alias["rob"] = "rbac.authorization.k8s.io/v1/rolebindings" + a.Alias["np"] = "networking.k8s.io/v1beta1/rolebindings" + { + a.Alias["ctx"] = "contexts" + a.Alias["contexts"] = "contexts" + a.Alias["context"] = "contexts" + } + { + a.Alias["usr"] = "users" + a.Alias["users"] = "users" + a.Alias["user"] = "user" + } + { + a.Alias["grp"] = "groups" + a.Alias["group"] = "groups" + a.Alias["groups"] = "groups" + } + { + a.Alias["pf"] = "portforwards" + a.Alias["portforwards"] = "portforwards" + a.Alias["portforward"] = "portforwards" + } + { + a.Alias["be"] = "benchmarks" + a.Alias["benchmark"] = "benchmarks" + a.Alias["benchmarks"] = "benchmarks" + } + { + a.Alias["sd"] = "screendumps" + a.Alias["screendump"] = "screendumps" + a.Alias["screendumps"] = "screendumps" + } +} + +// Load K9s aliases. +func (a Aliases) Load() error { + return a.LoadAliases(K9sAlias) +} + +// Get retrieves an alias. +func (a Aliases) Get(k string) (string, bool) { + v, ok := a.Alias[k] + return v, ok +} + +// Define declares a new alias. +func (a Aliases) Define(args ...string) { + if len(args)%2 != 0 { + panic("Invalid alias definition. You must specify pairs") + } + for i := 0; i < len(args); i += 2 { + a.Alias[args[i]] = args[i+1] + } +} + +// LoadAliases K9s alias from a given file. +func (a Aliases) LoadAliases(path string) error { + f, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + var aa Aliases + if err := yaml.Unmarshal(f, &aa); err != nil { + return err + } + for k, v := range aa.Alias { + a.Alias[k] = v + } + + return nil +} + +// Save alias to disk. +func (a Aliases) Save() error { + log.Debug().Msg("[Config] Saving Aliases...") + return a.SaveAliases(K9sAlias) +} + +// SaveAliases saves aliases to a given file. +func (a Aliases) SaveAliases(path string) error { + EnsurePath(path, DefaultDirMod) + cfg, err := yaml.Marshal(a) + if err != nil { + return err + } + return ioutil.WriteFile(path, cfg, 0644) +} diff --git a/internal/config/alias_test.go b/internal/config/alias_test.go new file mode 100644 index 00000000..f1a199be --- /dev/null +++ b/internal/config/alias_test.go @@ -0,0 +1,26 @@ +package config_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/stretchr/testify/assert" +) + +func TestAliasesLoad(t *testing.T) { + aa := config.NewAliases() + assert.Nil(t, aa.LoadAliases("test_assets/alias.yml")) + + assert.Equal(t, 27, len(aa.Alias)) +} + +func TestAliasesSave(t *testing.T) { + aa := config.NewAliases() + + aa.Alias["test"] = "fred" + aa.Alias["blee"] = "duh" + aa.SaveAliases("/tmp/a.yml") + + assert.Nil(t, aa.LoadAliases("/tmp/a.yml")) + assert.Equal(t, 28, len(aa.Alias)) +} diff --git a/internal/config/config.go b/internal/config/config.go index 8fc213b5..8a6a9c22 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -30,15 +30,24 @@ var ( ) type ( - // Connection present a kubernetes api server connection. + // Connection represents a kubernetes api server connection. Connection k8s.Connection - // KubeSettings exposes kubeconfig context informations. + // KubeSettings exposes kubeconfig context information. KubeSettings interface { + // CurrentContextName returns the name of the current context. CurrentContextName() (string, error) + + // CurrentClusterName returns the name of the current cluster. CurrentClusterName() (string, error) + + // CurrentNamespace returns the name of the current namespace. CurrentNamespaceName() (string, error) + + // ClusterNames() returns all available cluster names. ClusterNames() ([]string, error) + + // NamespaceNames returns all available namespace names. NamespaceNames(nn []v1.Namespace) []string } @@ -180,7 +189,6 @@ func (c *Config) Load(path string) error { if cfg.K9s != nil { c.K9s = cfg.K9s } - log.Debug().Msgf("Headless ? %t", c.K9s.Headless) return nil } diff --git a/internal/config/flags.go b/internal/config/flags.go index b2fa0b80..ee36b7e9 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -8,7 +8,7 @@ const ( DefaultLogLevel = "info" // DefaultCommand represents the default command to run. - DefaultCommand = "po" + DefaultCommand = "" ) // Flags represents K9s configuration flags. diff --git a/internal/config/k9s.go b/internal/config/k9s.go index 3bd0dade..af85ba86 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -80,7 +80,6 @@ func (k *K9s) ActiveCluster() *Cluster { if c, ok := k.Clusters[k.CurrentCluster]; ok { return c } - k.Clusters[k.CurrentCluster] = NewCluster() return k.Clusters[k.CurrentCluster] diff --git a/internal/config/test_assets/alias.yml b/internal/config/test_assets/alias.yml new file mode 100644 index 00000000..10835dee --- /dev/null +++ b/internal/config/test_assets/alias.yml @@ -0,0 +1,3 @@ +alias: + dp: "apps.v1.deployments" + pe: ".v1.pods" diff --git a/internal/k8s/gvr.go b/internal/k8s/gvr.go new file mode 100644 index 00000000..a0a11bf4 --- /dev/null +++ b/internal/k8s/gvr.go @@ -0,0 +1,62 @@ +package k8s + +import ( + "path" + "strings" + + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// GVR represents a fully qualified kubernetes resource. +type GVR string + +// NewGVR returns a new gvr. +func NewGVR(g, v, r string) GVR { + return GVR(path.Join(g, v, r)) +} + +// ToGVR returns a new gvr from a string. +func ToGVR(gv, r string) GVR { + return GVR(path.Join(gv, r)) +} + +// ResName returns a full res name ie dp.v1.apps. +func (g GVR) ResName() string { + return g.ToR() + "." + g.ToV() + "." + g.ToG() +} + +// AsGR returns the group version. +func (g GVR) AsGR() schema.GroupVersion { + return schema.GroupVersion{ + Group: g.ToG(), + Version: g.ToV(), + } +} + +// String returns a GVR as a string. +func (g GVR) String() string { + return string(g) +} + +// ToV returns the resource version. +func (g GVR) ToV() string { + tokens := strings.Split(string(g), "/") + return tokens[len(tokens)-2] +} + +// ToR returns the resource name. +func (g GVR) ToR() string { + tokens := strings.Split(string(g), "/") + return tokens[len(tokens)-1] +} + +// ToG returns the resource group name. +func (g GVR) ToG() string { + tokens := strings.Split(string(g), "/") + switch len(tokens) { + case 3: + return tokens[0] + default: + return "" + } +} diff --git a/internal/k8s/mapper.go b/internal/k8s/mapper.go index 9c8d9d9c..28f95533 100644 --- a/internal/k8s/mapper.go +++ b/internal/k8s/mapper.go @@ -26,14 +26,6 @@ type RestMapper struct { Connection } -// Find a mapping given a resource kind. -func (*RestMapper) Find(kind string) (*meta.RESTMapping, error) { - if m, ok := kindToMapper[kind]; ok { - return m, nil - } - return nil, fmt.Errorf("no mapping for kind %s", kind) -} - // ToRESTMapper map resources to kind, and map kind and version to interfaces for manipulating K8s objects. func (r *RestMapper) ToRESTMapper() (meta.RESTMapper, error) { rc := r.RestConfigOrDie() @@ -115,154 +107,3 @@ func (*RestMapper) toRESTMapping(gvr schema.GroupVersionResource, res string) *m func (*RestMapper) Name() meta.RESTScopeName { return meta.RESTScopeNameNamespace } - -var kindToMapper = map[string]*meta.RESTMapping{ - "ConfigMap": { - Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmap"}, - GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"}, - Scope: RestMapping, - }, - "Pod": { - Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pod"}, - GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, - Scope: RestMapping, - }, - "Service": { - Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "service"}, - GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"}, - Scope: RestMapping, - }, - "EndPoints": { - Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "endpoints"}, - GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Endpoints"}, - Scope: RestMapping, - }, - "Namespace": { - Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "namespace"}, - GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"}, - Scope: RestMapping, - }, - "Node": { - Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "node"}, - GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Node"}, - Scope: RestMapping, - }, - "PersistentVolume": { - Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "persistentvolume"}, - GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "PersistentVolume"}, - Scope: RestMapping, - }, - "PersistentVolumeClaim": { - Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "persistentvolumeclaim"}, - GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "PersistentVolumeClaim"}, - Scope: RestMapping, - }, - "ReplicationController": { - Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "replicationcontroller"}, - GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ReplicationController"}, - Scope: RestMapping, - }, - "Secret": { - Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secret"}, - GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"}, - Scope: RestMapping, - }, - "StorageClasse": { - Resource: schema.GroupVersionResource{Group: "storage.k8s.io", Version: "v1", Resource: "storageclass"}, - GroupVersionKind: schema.GroupVersionKind{Group: "storage.k8s.io", Version: "v1", Kind: "StorageClass"}, - Scope: RestMapping, - }, - "ServiceAccount": { - Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "serviceaccount"}, - GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ServiceAccount"}, - Scope: RestMapping, - }, - - "Deployment": { - Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"}, - GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, - Scope: RestMapping, - }, - "ReplicaSet": { - Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "replicaset"}, - GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"}, - Scope: RestMapping, - }, - "StatefulSet": { - Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "statefulsets"}, - GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "StatefulSet"}, - Scope: RestMapping, - }, - - "HorizontalPodAutoscaler": { - Resource: schema.GroupVersionResource{Group: "autoscaling", Version: "v1", Resource: "horizontalpodautoscaler"}, - GroupVersionKind: schema.GroupVersionKind{Group: "autoscaling", Version: "v1", Kind: "HorizontalPodAutoscaler"}, - Scope: RestMapping, - }, - - "Job": { - Resource: schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "job"}, - GroupVersionKind: schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "Job"}, - Scope: RestMapping, - }, - "CronJob": { - Resource: schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "cronjob"}, - GroupVersionKind: schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "CronJob"}, - Scope: RestMapping, - }, - - "DaemonSet": { - Resource: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "daemonset"}, - GroupVersionKind: schema.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "DaemonSet"}, - Scope: RestMapping, - }, - "Ingress": { - Resource: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "ingress"}, - GroupVersionKind: schema.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Ingress"}, - Scope: RestMapping, - }, - - "ClusterRole": { - Resource: schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "clusterrole"}, - GroupVersionKind: schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole"}, - Scope: RestMapping, - }, - "ClusterRoleBinding": { - Resource: schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "clusterrolebinding"}, - GroupVersionKind: schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding"}, - Scope: RestMapping, - }, - "Role": { - Resource: schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "role"}, - GroupVersionKind: schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "Role"}, - Scope: RestMapping, - }, - "RoleBinding": { - Resource: schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "rolebinding"}, - GroupVersionKind: schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBinding"}, - Scope: RestMapping, - }, - - "CustomResourceDefinition": { - Resource: schema.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1beta1", Resource: "customresourcedefinitions"}, - GroupVersionKind: schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1beta1", Kind: "CustomResourceDefinition"}, - Scope: RestMapping, - }, - "NetworkPolicy": { - Resource: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "networkpolicies"}, - GroupVersionKind: schema.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "NetworkPolicy"}, - Scope: RestMapping, - }, - - "Event": { - Resource: schema.GroupVersionResource{Group: "events.k8s.io", Version: "v1beta1", Resource: "events"}, - GroupVersionKind: schema.GroupVersionKind{Group: "events.k8s.io", Version: "v1beta1", Kind: "Event"}, - Scope: RestMapping, - }, - - "PodDisruptionBudget": { - Resource: schema.GroupVersionResource{Group: "policy", Version: "v1beta1", Resource: "poddisruptionbudgets"}, - GroupVersionKind: schema.GroupVersionKind{Group: "policy", Version: "v1beta1", Kind: "PodDisruptionBudget"}, - Scope: RestMapping, - }, -} diff --git a/internal/k8s/resource.go b/internal/k8s/resource.go index b1585893..d93f2631 100644 --- a/internal/k8s/resource.go +++ b/internal/k8s/resource.go @@ -17,24 +17,24 @@ type Resource struct { *base Connection - group, version, name string + gvr GVR } // NewResource returns a new Resource. -func NewResource(c Connection, group, version, name string) *Resource { - return &Resource{base: &base{}, Connection: c, group: group, version: version, name: name} +func NewResource(c Connection, gvr GVR) *Resource { + return &Resource{base: &base{}, Connection: c, gvr: gvr} } // GetInfo returns info about apigroup. -func (r *Resource) GetInfo() (string, string, string) { - return r.group, r.version, r.name +func (r *Resource) GetInfo() GVR { + return r.gvr } func (r *Resource) nsRes() dynamic.NamespaceableResourceInterface { g := schema.GroupVersionResource{ - Group: r.group, - Version: r.version, - Resource: r.name, + Group: r.gvr.ToG(), + Version: r.gvr.ToV(), + Resource: r.gvr.ToR(), } return r.DynDialOrDie().Resource(g) } @@ -46,7 +46,7 @@ func (r *Resource) Get(ns, n string) (interface{}, error) { // List all Resources in a given namespace. func (r *Resource) List(ns string) (Collection, error) { - obj, err := r.listAll(ns, r.name) + obj, err := r.listAll(ns, r.gvr.ToR()) if err != nil { return nil, err } @@ -82,9 +82,10 @@ func (r *Resource) listAll(ns, n string) (runtime.Object, error) { func (r *Resource) getClient() (*rest.RESTClient, error) { crConfig := r.RestConfigOrDie() - crConfig.GroupVersion = &schema.GroupVersion{Group: r.group, Version: r.version} + gv := r.gvr.AsGR() + crConfig.GroupVersion = &gv crConfig.APIPath = "/apis" - if len(r.group) == 0 { + if len(r.gvr.ToG()) == 0 { crConfig.APIPath = "/api" } codec, _ := r.codec() @@ -99,7 +100,7 @@ func (r *Resource) getClient() (*rest.RESTClient, error) { func (r *Resource) codec() (serializer.CodecFactory, runtime.ParameterCodec) { scheme := runtime.NewScheme() - gv := schema.GroupVersion{Group: r.group, Version: r.version} + gv := r.gvr.AsGR() metav1.AddToGroupVersion(scheme, gv) scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) diff --git a/internal/resource/base.go b/internal/resource/base.go index 624ff388..132acfa7 100644 --- a/internal/resource/base.go +++ b/internal/resource/base.go @@ -3,14 +3,12 @@ package resource import ( "bytes" "context" - "fmt" "path" "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/watch" "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" genericprinters "k8s.io/cli-runtime/pkg/printers" @@ -102,9 +100,7 @@ func (*Base) NumCols(n string) map[string]bool { } // ExtFields returns extended fields in relation to headers. -func (*Base) ExtFields() Properties { - return Properties{} -} +func (*Base) ExtFields(*TypeMeta) {} // Get a resource by name func (b *Base) Get(path string) (Columnar, error) { @@ -133,30 +129,13 @@ func (b *Base) List(ns string) (Columnars, error) { } // Describe a given resource. -func (b *Base) Describe(kind, pa string) (string, error) { - mapping, err := k8s.RestMapping.Find(kind) - if err == nil { - return b.doDescribe(pa, mapping) - } - - resource, ok := b.Resource.(*k8s.Resource) - if !ok { - log.Debug().Msgf("resource not a (*k8s.Resource) and %s", err) - return "", fmt.Errorf("resource not a (*k8s.Resource) and %s", err) - } - g, v, n := resource.GetInfo() +func (b *Base) Describe(gvr, pa string) (string, error) { mapper := k8s.RestMapper{Connection: b.Connection} - var e error - mapping, e = mapper.ResourceFor(fmt.Sprintf("%s.%s.%s", n, v, g)) - if e != nil { - log.Debug().Err(e).Msgf("Unable to find mapper for %s %s", kind, pa) - return "", e + mapping, err := mapper.ResourceFor(k8s.GVR(gvr).ResName()) + if err != nil { + log.Debug().Err(err).Msgf("Unable to find mapper for %s %s", gvr, pa) + return "", err } - - return b.doDescribe(pa, mapping) -} - -func (b *Base) doDescribe(pa string, mapping *meta.RESTMapping) (string, error) { ns, n := Namespaced(pa) d, err := versioned.Describer(b.Connection.Config().Flags(), mapping) if err != nil { diff --git a/internal/resource/crd.go b/internal/resource/crd.go index bd03a1c0..aca6d4d6 100644 --- a/internal/resource/crd.go +++ b/internal/resource/crd.go @@ -89,24 +89,33 @@ func (r *CustomResourceDefinition) Fields(ns string) Row { } // ExtFields returns extended fields. -func (r *CustomResourceDefinition) ExtFields() Properties { - var ( - pp = Properties{} - i = r.instance - ) - - if spec, ok := i.Object["spec"].(map[string]interface{}); ok { - if meta, ok := i.Object["metadata"].(map[string]interface{}); ok { - pp["name"] = meta["name"] - } - pp["group"], pp["version"] = spec["group"], spec["version"] - - if names, ok := spec["names"].(map[string]interface{}); ok { - pp["kind"] = names["kind"] - pp["singular"], pp["plural"] = names["singular"], names["plural"] - pp["aliases"] = names["shortNames"] - } +func (r *CustomResourceDefinition) ExtFields(m *TypeMeta) { + i := r.instance + spec, ok := i.Object["spec"].(map[string]interface{}) + if !ok { + return } - return pp + if meta, ok := i.Object["metadata"].(map[string]interface{}); ok { + m.Name = meta["name"].(string) + } + m.Group, m.Version = spec["group"].(string), spec["version"].(string) + m.Namespaced = isNamespaced(spec["scope"].(string)) + names, ok := spec["names"].(map[string]interface{}) + if !ok { + return + } + m.Kind = names["kind"].(string) + m.Singular, m.Plural = names["singular"].(string), names["plural"].(string) + if names["shortNames"] != nil { + for _, s := range names["shortNames"].([]interface{}) { + m.ShortNames = append(m.ShortNames, s.(string)) + } + } else { + m.ShortNames = nil + } +} + +func isNamespaced(scope string) bool { + return scope == "Namespaced" } diff --git a/internal/resource/crd_test.go b/internal/resource/crd_test.go index 80df837b..c060fe9a 100644 --- a/internal/resource/crd_test.go +++ b/internal/resource/crd_test.go @@ -43,12 +43,6 @@ func TestCRDFields(t *testing.T) { assert.Equal(t, "fred", r[0]) } -func TestCRDExtFields(t *testing.T) { - p := newCRDFull().ExtFields() - - assert.Equal(t, 7, len(p)) -} - func TestCRDFieldsAllNS(t *testing.T) { r := newCRD().Fields(resource.AllNamespaces) diff --git a/internal/resource/custom.go b/internal/resource/custom.go index 182c663a..148df75c 100644 --- a/internal/resource/custom.go +++ b/internal/resource/custom.go @@ -17,35 +17,36 @@ import ( type Custom struct { *Base - instance *metav1beta1.TableRow - group, version, name string - headers Row + instance *metav1beta1.TableRow + gvr k8s.GVR + headers Row } // NewCustomList returns a new resource list. -func NewCustomList(c k8s.Connection, ns, group, version, name string) List { - if !c.IsNamespaced(name) { +func NewCustomList(c k8s.Connection, namespaced bool, ns, gvr string) List { + if !namespaced { ns = NotNamespaced } + g := k8s.GVR(gvr) return NewList( ns, - name, - NewCustom(c, group, version, name), AllVerbsAccess|DescribeAccess, + g.ToR(), + NewCustom(c, g), AllVerbsAccess|DescribeAccess, ) } // NewCustom instantiates a new Kubernetes Resource. -func NewCustom(c k8s.Connection, group, version, name string) *Custom { - cr := &Custom{Base: &Base{Connection: c, Resource: k8s.NewResource(c, group, version, name)}} +func NewCustom(c k8s.Connection, gvr k8s.GVR) *Custom { + cr := &Custom{Base: &Base{Connection: c, Resource: k8s.NewResource(c, gvr)}} cr.Factory = cr - cr.group, cr.version, cr.name = group, version, name + cr.gvr = gvr return cr } // New builds a new Custom instance from a k8s resource. func (r *Custom) New(i interface{}) Columnar { - cr := NewCustom(r.Connection, "", "", "") + cr := NewCustom(r.Connection, "") switch instance := i.(type) { case *metav1beta1.TableRow: cr.instance = instance @@ -66,7 +67,7 @@ func (r *Custom) New(i interface{}) Columnar { } name := meta["name"].(string) cr.path = path.Join(ns, name) - cr.group, cr.version, cr.name = obj["kind"].(string), obj["apiVersion"].(string), name + cr.gvr = k8s.NewGVR(obj["kind"].(string), obj["apiVersion"].(string), name) return cr } diff --git a/internal/resource/custom_test.go b/internal/resource/custom_test.go index 52e68ec0..83e08b52 100644 --- a/internal/resource/custom_test.go +++ b/internal/resource/custom_test.go @@ -130,7 +130,7 @@ func k8sCustomRow() *metav1beta1.TableRow { func newCustom() resource.Columnar { mc := NewMockConnection() - return resource.NewCustom(mc, "g", "v1", "fred").New(k8sCustomRow()) + return resource.NewCustom(mc, "g/v1/fred").New(k8sCustomRow()) } func customYaml() string { diff --git a/internal/resource/list.go b/internal/resource/list.go index b6cd2008..b56571f2 100644 --- a/internal/resource/list.go +++ b/internal/resource/list.go @@ -48,8 +48,17 @@ type ( // RowEvents tracks resource update events. RowEvents map[string]*RowEvent - // Properties a collection of extra properties on a K8s resource. - Properties map[string]interface{} + // TypeMeta represents resource type meta data. + TypeMeta struct { + Name string + Namespaced bool + Group string + Version string + Kind string + Singular string + Plural string + ShortNames []string + } // TableData tracks a K8s resource for tabular display. TableData struct { @@ -81,7 +90,7 @@ type ( Columnar interface { Header(ns string) Row Fields(ns string) Row - ExtFields() Properties + ExtFields(*TypeMeta) Name() string SetPodMetrics(*mv1beta1.PodMetrics) SetNodeMetrics(*mv1beta1.NodeMetrics) @@ -102,7 +111,7 @@ type ( Get(path string) (Columnar, error) List(ns string) (Columnars, error) Delete(path string, cascade, force bool) error - Describe(kind, pa string) (string, error) + Describe(gvr, pa string) (string, error) Marshal(pa string) (string, error) Header(ns string) Row NumCols(ns string) map[string]bool diff --git a/internal/resource/sts_test.go b/internal/resource/sts_test.go index a0c535b4..a0ad4338 100644 --- a/internal/resource/sts_test.go +++ b/internal/resource/sts_test.go @@ -123,7 +123,7 @@ func stsHeader() resource.Row { } func stsYaml() string { - return `apiVersion: v1 + return `apiVersion: apps/v1 kind: StatefulSet metadata: creationTimestamp: "2018-12-14T17:36:43Z" diff --git a/internal/ui/action.go b/internal/ui/action.go index ed1d7ab6..ef865e7a 100644 --- a/internal/ui/action.go +++ b/internal/ui/action.go @@ -30,19 +30,21 @@ func NewKeyAction(d string, a ActionHandler, display bool) KeyAction { // Hints returns a collection of hints. func (a KeyActions) Hints() Hints { kk := make([]int, 0, len(a)) - for k, v := range a { - if v.Visible { - kk = append(kk, int(k)) - } + for k := range a { + kk = append(kk, int(k)) } sort.Ints(kk) hh := make(Hints, 0, len(kk)) for _, k := range kk { if name, ok := tcell.KeyNames[tcell.Key(k)]; ok { - hh = append(hh, Hint{ - Mnemonic: name, - Description: a[tcell.Key(k)].Description}) + hh = append(hh, + Hint{ + Mnemonic: name, + Description: a[tcell.Key(k)].Description, + Visible: a[tcell.Key(k)].Visible, + }, + ) } else { log.Error().Msgf("Unable to locate KeyName for %#v", string(k)) } diff --git a/internal/ui/app.go b/internal/ui/app.go index 5a2f5175..3f6541b5 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -7,7 +7,6 @@ import ( "github.com/derailed/k9s/internal/k8s" "github.com/derailed/tview" "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" ) // Igniter represents an initializable view. @@ -125,8 +124,8 @@ func (a *App) GetCmd() string { return a.cmdBuff.String() } -// GetCmdBuff returns a cmd buffer. -func (a *App) GetCmdBuff() *CmdBuff { +// CmdBuff returns a cmd buffer. +func (a *App) CmdBuff() *CmdBuff { return a.cmdBuff } @@ -177,7 +176,6 @@ func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey { } if a, ok := a.actions[key]; ok { - log.Debug().Msgf(">> App handled key: %s", tcell.KeyNames[key]) return a.Action(evt) } diff --git a/internal/ui/cmd.go b/internal/ui/cmd.go index 0a3d3695..8ee3509e 100644 --- a/internal/ui/cmd.go +++ b/internal/ui/cmd.go @@ -61,11 +61,13 @@ func (v *CmdView) write(s string) { // ---------------------------------------------------------------------------- // Event Listener protocol... -func (v *CmdView) changed(s string) { +// BufferChanged indicates the buffer was changed. +func (v *CmdView) BufferChanged(s string) { v.update(s) } -func (v *CmdView) active(f bool) { +// BufferActive indicates the buff activity changed. +func (v *CmdView) BufferActive(f bool) { v.activated = f if f { v.SetBorder(true) diff --git a/internal/ui/cmd_buff.go b/internal/ui/cmd_buff.go index 04599a98..7be4b3de 100644 --- a/internal/ui/cmd_buff.go +++ b/internal/ui/cmd_buff.go @@ -3,9 +3,13 @@ package ui const maxBuff = 10 type ( - buffWatcher interface { - changed(s string) - active(state bool) + // BuffWatcher represents a command buffer listener. + BuffWatcher interface { + // Changed indicates the buffer was changed. + BufferChanged(s string) + + // Active indicates the buff activity changed. + BufferActive(state bool) } // CmdBuff represents user command input. @@ -13,7 +17,7 @@ type ( buff []rune hotKey rune active bool - listeners []buffWatcher + listeners []BuffWatcher } ) @@ -22,7 +26,7 @@ func NewCmdBuff(key rune) *CmdBuff { return &CmdBuff{ hotKey: key, buff: make([]rune, 0, maxBuff), - listeners: []buffWatcher{}, + listeners: []BuffWatcher{}, } } @@ -88,18 +92,18 @@ func (c *CmdBuff) Empty() bool { // Event Listeners... // AddListener registers a cmd buffer listener. -func (c *CmdBuff) AddListener(w ...buffWatcher) { +func (c *CmdBuff) AddListener(w ...BuffWatcher) { c.listeners = append(c.listeners, w...) } func (c *CmdBuff) fireChanged() { for _, l := range c.listeners { - l.changed(c.String()) + l.BufferChanged(c.String()) } } func (c *CmdBuff) fireActive(b bool) { for _, l := range c.listeners { - l.active(b) + l.BufferActive(b) } } diff --git a/internal/ui/cmd_buff_test.go b/internal/ui/cmd_buff_test.go index c78e3752..347865da 100644 --- a/internal/ui/cmd_buff_test.go +++ b/internal/ui/cmd_buff_test.go @@ -12,11 +12,11 @@ type testListener struct { inact int } -func (l *testListener) changed(s string) { +func (l *testListener) BufferChanged(s string) { l.text = s } -func (l *testListener) active(s bool) { +func (l *testListener) BufferActive(s bool) { if s { l.act++ return diff --git a/internal/ui/cmd_test.go b/internal/ui/cmd_test.go index f510e770..8425945c 100644 --- a/internal/ui/cmd_test.go +++ b/internal/ui/cmd_test.go @@ -23,6 +23,6 @@ func TestCmdInCmdMode(t *testing.T) { assert.Equal(t, "T> blee!\n", v.GetText(false)) assert.False(t, v.InCmdMode()) - v.active(true) + v.BufferActive(true) assert.True(t, v.InCmdMode()) } diff --git a/internal/ui/hint.go b/internal/ui/hint.go index 5e84fd99..1845dffa 100644 --- a/internal/ui/hint.go +++ b/internal/ui/hint.go @@ -8,7 +8,9 @@ import ( type ( // Hint represents keyboard mnemonic. Hint struct { - Mnemonic, Description string + Mnemonic string + Description string + Visible bool } // Hints a collection of keyboard mnemonics. Hints []Hint diff --git a/internal/ui/indicator.go b/internal/ui/indicator.go index 3a0ffa11..8d5bea25 100644 --- a/internal/ui/indicator.go +++ b/internal/ui/indicator.go @@ -13,14 +13,14 @@ import ( // IndicatorView represents a status indicator. type IndicatorView struct { *tview.TextView + app *App styles *config.Styles permanent string - - cancel context.CancelFunc + cancel context.CancelFunc } -// NewIndicatorView returns a new logo. +// NewIndicatorView returns a new status indicator. func NewIndicatorView(app *App, styles *config.Styles) *IndicatorView { v := IndicatorView{ TextView: tview.NewTextView(), diff --git a/internal/ui/menu.go b/internal/ui/menu.go index 09c14bcd..809d0925 100644 --- a/internal/ui/menu.go +++ b/internal/ui/menu.go @@ -69,6 +69,9 @@ func (v *MenuView) buildMenuTable(hh Hints) [][]string { firstCmd := true maxKeys := make([]int, colCount+1) for _, h := range hh { + if !h.Visible { + continue + } isDigit := menuRX.MatchString(h.Mnemonic) if !isDigit && firstCmd { row, col, firstCmd = 0, col+1, false diff --git a/internal/ui/menu_test.go b/internal/ui/menu_test.go index 94ab186f..47fbf577 100644 --- a/internal/ui/menu_test.go +++ b/internal/ui/menu_test.go @@ -12,9 +12,9 @@ func TestNewMenuView(t *testing.T) { defaults, _ := config.NewStyles("") v := NewMenuView(defaults) v.HydrateMenu(Hints{ - {"a", "bleeA"}, - {"b", "bleeB"}, - {"0", "zero"}, + {"a", "bleeA", true}, + {"b", "bleeB", true}, + {"0", "zero", true}, }) assert.Equal(t, " [fuchsia:black:b]<0> [white:black:d]zero ", v.GetCell(0, 0).Text) @@ -35,9 +35,10 @@ func TestKeyActions(t *testing.T) { tcell.Key(Key1): NewKeyAction("one", nil, false), }, e: Hints{ - {"0", "zero"}, - {"a", "bleeA"}, - {"b", "bleeB"}, + {"0", "zero", true}, + {"1", "one", false}, + {"a", "bleeA", true}, + {"b", "bleeB", true}, }, }, } diff --git a/internal/ui/table.go b/internal/ui/table.go index a9836410..373b42af 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -174,8 +174,8 @@ func (v *Table) GetSelectedItem() string { func (v *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey { key := evt.Key() if key == tcell.KeyRune { - if v.Cmd().IsActive() { - v.Cmd().Add(evt.Rune()) + if v.SearchBuff().IsActive() { + v.SearchBuff().Add(evt.Rune()) v.ClearSelection() v.doUpdate(v.filtered()) v.SelectFirstRow() @@ -185,7 +185,6 @@ func (v *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey { } if a, ok := v.actions[key]; ok { - log.Debug().Msgf(">> TableView handled %s", tcell.KeyNames[key]) return a.Action(evt) } @@ -242,7 +241,7 @@ func (v *Table) Update(data resource.TableData) { func (v *Table) doUpdate(data resource.TableData) { v.activeNS = data.Namespace if v.activeNS == resource.AllNamespaces && v.activeNS != "*" { - v.actions[KeyShiftP] = NewKeyAction("Sort Namespace", v.SortColCmd(-2), true) + v.actions[KeyShiftP] = NewKeyAction("Sort Namespace", v.SortColCmd(-2), false) } else { delete(v.actions, KeyShiftP) } @@ -438,8 +437,8 @@ func (v *Table) KeyBindings() KeyActions { return v.actions } -// Cmd returns the associated command buffer. -func (v *Table) Cmd() *CmdBuff { +// SearchBuff returns the associated command buffer. +func (v *Table) SearchBuff() *CmdBuff { return v.cmdBuff } diff --git a/internal/views/alias.go b/internal/views/alias.go index b3bfb198..f8c2eed2 100644 --- a/internal/views/alias.go +++ b/internal/views/alias.go @@ -3,7 +3,9 @@ package views import ( "context" "fmt" + "strings" + "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" @@ -11,7 +13,7 @@ import ( const ( aliasTitle = "Aliases" - aliasTitleFmt = " [aqua::b]%s([fuchsia::b]%d[fuchsia::-])[aqua::-] " + aliasTitleFmt = " [aqua::b]%s([fuchsia::b]%d[fuchsia::-][aqua::-]) " ) type aliasView struct { @@ -52,8 +54,8 @@ func (v *aliasView) registerActions() { tcell.KeyEnter: ui.NewKeyAction("Goto", v.gotoCmd, true), tcell.KeyEscape: ui.NewKeyAction("Reset", v.resetCmd, false), ui.KeySlash: ui.NewKeyAction("Filter", v.activateCmd, false), - ui.KeyShiftR: ui.NewKeyAction("Sort Resources", v.SortColCmd(1), true), - ui.KeyShiftO: ui.NewKeyAction("Sort Groups", v.SortColCmd(2), true), + ui.KeyShiftR: ui.NewKeyAction("Sort Resources", v.SortColCmd(1), false), + ui.KeyShiftO: ui.NewKeyAction("Sort Groups", v.SortColCmd(2), false), }) } @@ -62,8 +64,8 @@ func (v *aliasView) getTitle() string { } func (v *aliasView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.Cmd().Empty() { - v.Cmd().Reset() + if !v.SearchBuff().Empty() { + v.SearchBuff().Reset() return nil } @@ -73,13 +75,15 @@ func (v *aliasView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { func (v *aliasView) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { r, _ := v.GetSelection() if r != 0 { - return v.runCmd(evt) + s := ui.TrimCell(v.Table, r, 1) + tokens := strings.Split(s, ",") + v.app.gotoResource(tokens[0], true) + return nil } - if v.Cmd().IsActive() { + if v.SearchBuff().IsActive() { return v.activateCmd(evt) } - return evt } @@ -88,8 +92,8 @@ func (v *aliasView) backCmd(evt *tcell.EventKey) *tcell.EventKey { v.cancel() } - if v.Cmd().IsActive() { - v.Cmd().Reset() + if v.SearchBuff().IsActive() { + v.SearchBuff().Reset() } else { v.app.inject(v.current) } @@ -97,32 +101,30 @@ func (v *aliasView) backCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (v *aliasView) runCmd(evt *tcell.EventKey) *tcell.EventKey { - r, _ := v.GetSelection() - if r > 0 { - v.app.gotoResource(ui.TrimCell(v.Table, r, 0), true) - } - - return nil -} - func (v *aliasView) hydrate() resource.TableData { - cmds := make(map[string]*resCmd, 40) - aliasCmds(v.app.Conn(), cmds) - data := resource.TableData{ - Header: resource.Row{"ALIAS", "RESOURCE", "APIGROUP"}, - Rows: make(resource.RowEvents, len(cmds)), + Header: resource.Row{"RESOURCE", "COMMAND", "APIGROUP"}, + Rows: make(resource.RowEvents, len(aliases.Alias)), Namespace: resource.NotNamespaced, } - for k := range cmds { - fields := resource.Row{ - ui.Pad(k, 30), - ui.Pad(cmds[k].gvr, 30), - ui.Pad(cmds[k].api, 30), + aa := make(map[string][]string, len(aliases.Alias)) + for alias, gvr := range aliases.Alias { + if _, ok := aa[gvr]; ok { + aa[gvr] = append(aa[gvr], alias) + } else { + aa[gvr] = []string{alias} } - data.Rows[k] = &resource.RowEvent{ + } + + for gvr, aliases := range aa { + g := k8s.GVR(gvr) + fields := resource.Row{ + ui.Pad(g.ToR(), 30), + ui.Pad(strings.Join(aliases, ","), 70), + ui.Pad(g.ToG(), 30), + } + data.Rows[string(gvr)] = &resource.RowEvent{ Action: resource.New, Fields: fields, Deltas: fields, diff --git a/internal/views/alias_test.go b/internal/views/alias_test.go index 0c4b1bf5..88935fae 100644 --- a/internal/views/alias_test.go +++ b/internal/views/alias_test.go @@ -13,6 +13,6 @@ func TestAliasView(t *testing.T) { v.Init(nil, "") assert.Equal(t, 3, len(td.Header)) - assert.Equal(t, 41, len(td.Rows)) + assert.Equal(t, 15, len(td.Rows)) assert.Equal(t, "Aliases", v.getTitle()) } diff --git a/internal/views/app.go b/internal/views/app.go index 4462c2c2..4be89320 100644 --- a/internal/views/app.go +++ b/internal/views/app.go @@ -54,6 +54,7 @@ type ( stopCh chan struct{} forwarders map[string]forwarder version string + showHeader bool } ) @@ -76,11 +77,13 @@ func NewApp(cfg *config.Config) *appView { func (a *appView) Init(version string, rate int) { a.version = version + a.CmdBuff().AddListener(a) a.App.Init() a.AddActions(ui.KeyActions{ + tcell.KeyCtrlH: ui.NewKeyAction("ToggleHeader", a.toggleHeaderCmd, false), ui.KeyHelp: ui.NewKeyAction("Help", a.helpCmd, false), - tcell.KeyCtrlA: ui.NewKeyAction("Aliases", a.aliasCmd, true), + tcell.KeyCtrlA: ui.NewKeyAction("Aliases", a.aliasCmd, false), tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, false), }) @@ -96,29 +99,60 @@ func (a *appView) Init(version string, rate int) { } } - header := tview.NewFlex() - { - header.SetDirection(tview.FlexColumn) - header.AddItem(a.clusterInfo(), 35, 1, false) - header.AddItem(a.Menu(), 0, 1, false) - header.AddItem(a.Logo(), 26, 1, false) - } - main := tview.NewFlex() main.SetDirection(tview.FlexRow) + a.Main().AddPage("main", main, true, false) + a.Main().AddPage("splash", ui.NewSplash(a.Styles, version), true, true) - if !a.Config.K9s.GetHeadless() { - main.AddItem(header, 7, 1, false) - } else { - main.AddItem(a.indicator(), 1, 1, false) - } - main.AddItem(a.Cmd(), 3, 1, false) + main.AddItem(a.indicator(), 1, 1, false) + // main.AddItem(a.Cmd(), 3, 1, false) main.AddItem(a.Frame(), 0, 10, true) main.AddItem(a.Crumbs(), 2, 1, false) main.AddItem(a.Flash(), 1, 1, false) + a.toggleHeader(!a.Config.K9s.GetHeadless()) +} - a.Main().AddPage("main", main, true, false) - a.Main().AddPage("splash", ui.NewSplash(a.Styles, version), true, true) +// Changed indicates the buffer was changed. +func (a *appView) BufferChanged(s string) {} + +// Active indicates the buff activity changed. +func (a *appView) BufferActive(state bool) { + flex, ok := a.Main().GetPrimitive("main").(*tview.Flex) + if !ok { + return + } + if state { + flex.AddItemAtIndex(1, a.Cmd(), 3, 1, false) + } else if flex.ItemAt(1) == a.Cmd() { + flex.RemoveItemAtIndex(1) + } + a.Draw() +} + +func (a *appView) toggleHeader(flag bool) { + a.showHeader = flag + flex := a.Main().GetPrimitive("main").(*tview.Flex) + if a.showHeader { + flex.RemoveItemAtIndex(0) + flex.AddItemAtIndex(0, a.buildHeader(), 7, 1, false) + } else { + flex.RemoveItemAtIndex(0) + flex.AddItemAtIndex(0, a.indicator(), 1, 1, false) + a.refreshIndicator() + } +} + +func (a *appView) buildHeader() tview.Primitive { + header := tview.NewFlex() + header.SetDirection(tview.FlexColumn) + if !a.showHeader { + return header + } + header.AddItem(a.clusterInfo(), 35, 1, false) + header.AddItem(a.Menu(), 0, 1, false) + header.AddItem(a.Logo(), 26, 1, false) + + return header } func (a *appView) clusterUpdater(ctx context.Context) { @@ -129,10 +163,11 @@ func (a *appView) clusterUpdater(ctx context.Context) { return case <-time.After(clusterRefresh): a.QueueUpdateDraw(func() { - if a.Config.K9s.GetHeadless() { + if !a.showHeader { a.refreshIndicator() + } else { + a.clusterInfo().refresh() } - a.clusterInfo().refresh() }) } } @@ -273,6 +308,14 @@ func (a *appView) setIndicator(l ui.FlashLevel, msg string) { a.Draw() } +func (a *appView) toggleHeaderCmd(evt *tcell.EventKey) *tcell.EventKey { + a.showHeader = !a.showHeader + a.toggleHeader(a.showHeader) + a.Draw() + + return nil +} + func (a *appView) prevCmd(evt *tcell.EventKey) *tcell.EventKey { if top, ok := a.command.previousCmd(); ok { log.Debug().Msgf("Previous command %s", top) @@ -283,7 +326,7 @@ func (a *appView) prevCmd(evt *tcell.EventKey) *tcell.EventKey { } func (a *appView) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { - if a.GetCmdBuff().IsActive() && !a.GetCmdBuff().Empty() { + if a.CmdBuff().IsActive() && !a.CmdBuff().Empty() { a.gotoResource(a.GetCmd(), true) a.ResetCmd() return nil @@ -294,9 +337,6 @@ func (a *appView) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { } func (a *appView) helpCmd(evt *tcell.EventKey) *tcell.EventKey { - if a.InCmdMode() { - return evt - } if _, ok := a.Frame().GetPrimitive("main").(*helpView); ok { return evt } @@ -307,9 +347,6 @@ func (a *appView) helpCmd(evt *tcell.EventKey) *tcell.EventKey { } func (a *appView) aliasCmd(evt *tcell.EventKey) *tcell.EventKey { - if a.InCmdMode() { - return evt - } if _, ok := a.Frame().GetPrimitive("main").(*aliasView); ok { return evt } @@ -337,10 +374,8 @@ func (a *appView) inject(i ui.Igniter) { } a.Frame().RemovePage("main") var ctx context.Context - { - ctx, a.cancel = context.WithCancel(context.Background()) - i.Init(ctx, a.Config.ActiveNamespace()) - } + ctx, a.cancel = context.WithCancel(context.Background()) + i.Init(ctx, a.Config.ActiveNamespace()) a.Frame().AddPage("main", i, true, true) a.SetFocus(i) } diff --git a/internal/views/bench.go b/internal/views/bench.go index e02077e6..1e6f8d46 100644 --- a/internal/views/bench.go +++ b/internal/views/bench.go @@ -40,7 +40,7 @@ type benchView struct { app *appView } -func newBenchView(title string, app *appView, _ resource.List) resourceViewer { +func newBenchView(title, gvr string, app *appView, _ resource.List) resourceViewer { v := benchView{app: app} v.masterDetail = newMasterDetail(benchTitle, "", app, v.backCmd) v.keyBindings() @@ -108,7 +108,7 @@ func (v *benchView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tce } func (v *benchView) enterCmd(evt *tcell.EventKey) *tcell.EventKey { - if v.masterPage().Cmd().IsActive() { + if v.masterPage().SearchBuff().IsActive() { return v.masterPage().filterCmd(evt) } diff --git a/internal/views/command.go b/internal/views/command.go index b821abf4..2656d7ee 100644 --- a/internal/views/command.go +++ b/internal/views/command.go @@ -1,8 +1,11 @@ package views import ( + "fmt" "regexp" + "time" + "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" "github.com/rs/zerolog/log" @@ -41,15 +44,18 @@ func (c *command) previousCmd() (string, bool) { // DefaultCmd reset default command ie show pods. func (c *command) defaultCmd() { - c.pushCmd(c.app.Config.ActiveView()) - c.run(c.app.Config.ActiveView()) + cmd := c.app.Config.ActiveView() + c.pushCmd(cmd) + if !c.run(cmd) { + log.Error().Err(fmt.Errorf("Unable to load command %s", cmd)).Msg("Command failed") + } } // Helpers... var policyMatcher = regexp.MustCompile(`\Apol\s([u|g|s]):([\w-:]+)\b`) -func (c *command) isStdCmd(cmd string) bool { +func (c *command) isCustCmd(cmd string) bool { switch { case cmd == "q", cmd == "quit": c.app.BailOut() @@ -70,87 +76,72 @@ func (c *command) isStdCmd(cmd string) bool { return false } -func (c *command) isAliasCmd(alias string, cmds map[string]resCmd) bool { - res, ok := cmds[alias] - if !ok { - return false - } - - var r resource.List - if res.listFn != nil { - r = res.listFn(c.app.Conn(), resource.DefaultNamespace) - } - - v := newResourceView(res.gvr, c.app, r) - if res.viewFn != nil { - v = res.viewFn(res.gvr, c.app, r) - } - - if res.colorerFn != nil { - v.setColorerFn(res.colorerFn) - } - if res.enterFn != nil { - v.setEnterFn(res.enterFn) - } - if res.decorateFn != nil { - v.setDecorateFn(res.decorateFn) - } - - const fmat = "Viewing resource %s..." - c.app.Flash().Infof(fmat, res.gvr) - log.Debug().Msgf("Running command %s", alias) - c.exec(alias, v) - - return true -} - -func (c *command) isCRDCmd(cmd string) bool { - crds := map[string]resCmd{} - - res, ok := crds[cmd] - if !ok { - return false - } - - name := res.plural - if name == "" { - name = res.singular - } - v := newResourceView( - res.gvr, - c.app, - resource.NewCustomList(c.app.Conn(), "", res.api, res.version, name), - ) - v.setColorerFn(ui.DefaultColorer) - c.exec(cmd, v) - - return true -} - // Exec the command by showing associated display. func (c *command) run(cmd string) bool { - if c.isStdCmd(cmd) { + defer func(t time.Time) { + log.Debug().Msgf("RUN CMD Elapsed %v", time.Since(t)) + }(time.Now()) + + if c.isCustCmd(cmd) { return true } - cmds := make(map[string]resCmd, 30) - resourceViews(c.app.Conn(), cmds) - allCRDs(c.app.Conn(), cmds) - - a, ok := aliases[cmd] + vv := make(viewers, 200) + resourceViews(c.app.Conn(), vv) + allCRDs(c.app.Conn(), vv) + gvr, ok := aliases.Get(cmd) if !ok { + log.Error().Err(fmt.Errorf("Huh? `%s` command not found", cmd)).Msg("Command Failed") c.app.Flash().Warnf("Huh? `%s` command not found", cmd) return false } - - return c.isAliasCmd(a, cmds) + v, ok := vv[gvr] + if !ok { + log.Error().Err(fmt.Errorf("Huh? `%s` viewer not found", cmd)).Msg("Viewer Failed") + c.app.Flash().Warnf("Huh? `%s` viewer not found", gvr) + return false + } + return c.execCmd(gvr, v) } -func (c *command) exec(cmd string, v ui.Igniter) { - if v == nil { - return +func (c *command) execCmd(gvr string, v viewer) bool { + log.Debug().Msgf("ExecCmd gvr %s", gvr) + var r resource.List + if v.listFn != nil { + r = v.listFn(c.app.Conn(), resource.DefaultNamespace) } - c.app.Config.SetActiveView(cmd) + + var view resourceViewer + if v.viewFn != nil { + view = v.viewFn(v.kind, gvr, c.app, r) + } else { + view = newResourceView(v.kind, gvr, c.app, r) + } + if v.colorerFn != nil { + view.setColorerFn(v.colorerFn) + } + if v.enterFn != nil { + view.setEnterFn(v.enterFn) + } + if v.decorateFn != nil { + view.setDecorateFn(v.decorateFn) + } + + return c.exec(gvr, view) +} + +func (c *command) exec(gvr string, v ui.Igniter) bool { + if v == nil { + log.Error().Err(fmt.Errorf("No igniter given for %s", gvr)) + return false + } + + g := k8s.GVR(gvr) + c.app.Flash().Infof("Viewing resource %s...", g.ToR()) + log.Debug().Msgf("Running command %s", gvr) + c.app.Config.SetActiveView(g.ToR()) c.app.Config.Save() c.app.inject(v) + + return true } diff --git a/internal/views/container.go b/internal/views/container.go index 4c8e0d73..11346219 100644 --- a/internal/views/container.go +++ b/internal/views/container.go @@ -22,8 +22,8 @@ type containerView struct { exitFn func() } -func newContainerView(ns string, app *appView, list resource.List, path string, exitFn func()) resourceViewer { - v := containerView{logResourceView: newLogResourceView(ns, app, list)} +func newContainerView(title string, app *appView, list resource.List, path string, exitFn func()) resourceViewer { + v := containerView{logResourceView: newLogResourceView(title, "", app, list)} v.path = &path v.envFn = v.k9sEnv v.containerFn = v.selectedContainer @@ -48,10 +48,10 @@ func (v *containerView) extraActions(aa ui.KeyActions) { aa[ui.KeyS] = ui.NewKeyAction("Shell", v.shellCmd, true) aa[tcell.KeyEscape] = ui.NewKeyAction("Back", v.backCmd, false) aa[ui.KeyP] = ui.NewKeyAction("Previous", v.backCmd, false) - aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", v.sortColCmd(6, false), true) - aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", v.sortColCmd(7, false), true) - aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", v.sortColCmd(8, false), true) - aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", v.sortColCmd(9, false), true) + aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", v.sortColCmd(6, false), false) + aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", v.sortColCmd(7, false), false) + aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", v.sortColCmd(8, false), false) + aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", v.sortColCmd(9, false), false) } func (v *containerView) k9sEnv() K9sEnv { diff --git a/internal/views/context.go b/internal/views/context.go index 75300ee4..4756585e 100644 --- a/internal/views/context.go +++ b/internal/views/context.go @@ -12,8 +12,8 @@ type contextView struct { *resourceView } -func newContextView(title string, app *appView, list resource.List) resourceViewer { - v := contextView{newResourceView(title, app, list).(*resourceView)} +func newContextView(title, gvr string, app *appView, list resource.List) resourceViewer { + v := contextView{newResourceView(title, gvr, app, list).(*resourceView)} v.extraActionsFn = v.extraActions v.enterFn = v.useCtx v.masterPage().SetSelectedFn(v.cleanser) diff --git a/internal/views/context_test.go b/internal/views/context_test.go index bafc4520..73f61fbf 100644 --- a/internal/views/context_test.go +++ b/internal/views/context_test.go @@ -12,7 +12,7 @@ func TestContextView(t *testing.T) { l := resource.NewContextList(nil, "fred") v := newContextView("blee", NewApp(config.NewConfig(ks{})), l).(*contextView) - assert.Equal(t, 3, len(v.hints())) + assert.Equal(t, 10, len(v.hints())) } func TestCleaner(t *testing.T) { diff --git a/internal/views/cronjob.go b/internal/views/cronjob.go index a16fec4c..aab5be93 100644 --- a/internal/views/cronjob.go +++ b/internal/views/cronjob.go @@ -10,8 +10,8 @@ type cronJobView struct { *resourceView } -func newCronJobView(t string, app *appView, list resource.List) resourceViewer { - v := cronJobView{resourceView: newResourceView(t, app, list).(*resourceView)} +func newCronJobView(title, gvr string, app *appView, list resource.List) resourceViewer { + v := cronJobView{resourceView: newResourceView(title, gvr, app, list).(*resourceView)} v.extraActionsFn = v.extraActions return &v diff --git a/internal/views/dp.go b/internal/views/dp.go index 515fe639..d1bea114 100644 --- a/internal/views/dp.go +++ b/internal/views/dp.go @@ -15,8 +15,8 @@ type deployView struct { const scaleDialogKey = "scale" -func newDeployView(title string, app *appView, list resource.List) resourceViewer { - logResourceView := newLogResourceView(title, app, list) +func newDeployView(title, gvr string, app *appView, list resource.List) resourceViewer { + logResourceView := newLogResourceView(title, gvr, app, list) v := deployView{logResourceView, newScalableResourceViewForParent(logResourceView.resourceView)} v.extraActionsFn = v.extraActions v.enterFn = v.showPods @@ -27,8 +27,8 @@ func newDeployView(title string, app *appView, list resource.List) resourceViewe func (v *deployView) extraActions(aa ui.KeyActions) { v.logResourceView.extraActions(aa) v.scalableResourceView.extraActions(aa) - aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(2, false), true) - aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(3, false), true) + aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(2, false), false) + aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(3, false), false) } func (v *deployView) showPods(app *appView, _, res, sel string) { diff --git a/internal/views/dp_test.go b/internal/views/dp_test.go index 3586d521..ac7a6f30 100644 --- a/internal/views/dp_test.go +++ b/internal/views/dp_test.go @@ -12,5 +12,5 @@ func TestDeployView(t *testing.T) { l := resource.NewDeploymentList(nil, "fred") v := newDeployView("blee", NewApp(config.NewConfig(ks{})), l).(*deployView) - assert.Equal(t, 3, len(v.hints())) + assert.Equal(t, 10, len(v.hints())) } diff --git a/internal/views/ds.go b/internal/views/ds.go index df6d5d0a..3a3c38ed 100644 --- a/internal/views/ds.go +++ b/internal/views/ds.go @@ -12,8 +12,8 @@ type daemonSetView struct { *logResourceView } -func newDaemonSetView(t string, app *appView, list resource.List) resourceViewer { - v := daemonSetView{newLogResourceView(t, app, list)} +func newDaemonSetView(title, gvr string, app *appView, list resource.List) resourceViewer { + v := daemonSetView{newLogResourceView(title, gvr, app, list)} v.extraActionsFn = v.extraActions v.enterFn = v.showPods @@ -22,8 +22,8 @@ func newDaemonSetView(t string, app *appView, list resource.List) resourceViewer func (v *daemonSetView) extraActions(aa ui.KeyActions) { v.logResourceView.extraActions(aa) - aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(2, false), true) - aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(3, false), true) + aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(2, false), false) + aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(3, false), false) } func (v *daemonSetView) showPods(app *appView, _, res, sel string) { diff --git a/internal/views/ds_test.go b/internal/views/ds_test.go index c03e1f91..908751f2 100644 --- a/internal/views/ds_test.go +++ b/internal/views/ds_test.go @@ -12,5 +12,5 @@ func TestDaemonSetView(t *testing.T) { l := resource.NewDaemonSetList(nil, "fred") v := newDaemonSetView("blee", NewApp(config.NewConfig(ks{})), l).(*daemonSetView) - assert.Equal(t, 3, len(v.hints())) + assert.Equal(t, 10, len(v.hints())) } diff --git a/internal/views/dump.go b/internal/views/dump.go index 4613aa53..9d663fa6 100644 --- a/internal/views/dump.go +++ b/internal/views/dump.go @@ -34,7 +34,7 @@ type dumpView struct { cancel context.CancelFunc } -func newDumpView(_ string, app *appView, _ resource.List) resourceViewer { +func newDumpView(_, _ string, app *appView, _ resource.List) resourceViewer { v := dumpView{ Pages: tview.NewPages(), app: app, @@ -112,7 +112,7 @@ func (v *dumpView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcel func (v *dumpView) enterCmd(evt *tcell.EventKey) *tcell.EventKey { log.Debug().Msg("Dump enter!") tv := v.getTV() - if tv.Cmd().IsActive() { + if tv.SearchBuff().IsActive() { return tv.filterCmd(evt) } sel := tv.GetSelectedItem() diff --git a/internal/views/forward.go b/internal/views/forward.go index 718ffb39..e4a1d779 100644 --- a/internal/views/forward.go +++ b/internal/views/forward.go @@ -33,7 +33,7 @@ type forwardView struct { var _ resourceViewer = &forwardView{} -func newForwardView(ns string, app *appView, list resource.List) resourceViewer { +func newForwardView(title, gvr string, app *appView, list resource.List) resourceViewer { v := forwardView{ Pages: tview.NewPages(), app: app, @@ -107,8 +107,8 @@ func (v *forwardView) registerActions() { tcell.KeyCtrlD: ui.NewKeyAction("Delete", v.deleteCmd, true), ui.KeySlash: ui.NewKeyAction("Filter", tv.activateCmd, false), ui.KeyP: ui.NewKeyAction("Previous", v.app.prevCmd, false), - ui.KeyShiftP: ui.NewKeyAction("Sort Ports", v.sortColCmd(2, true), true), - ui.KeyShiftU: ui.NewKeyAction("Sort URL", v.sortColCmd(4, true), true), + ui.KeyShiftP: ui.NewKeyAction("Sort Ports", v.sortColCmd(2, true), false), + ui.KeyShiftU: ui.NewKeyAction("Sort URL", v.sortColCmd(4, true), false), }) } @@ -210,8 +210,8 @@ func (v *forwardView) getSelectedItem() string { func (v *forwardView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { tv := v.getTV() - if !tv.Cmd().Empty() { - tv.Cmd().Reset() + if !tv.SearchBuff().Empty() { + tv.SearchBuff().Reset() return nil } @@ -243,8 +243,8 @@ func (v *forwardView) backCmd(evt *tcell.EventKey) *tcell.EventKey { } tv := v.getTV() - if tv.Cmd().IsActive() { - tv.Cmd().Reset() + if tv.SearchBuff().IsActive() { + tv.SearchBuff().Reset() } else { v.app.inject(v.app.Frame().GetPrimitive("main").(ui.Igniter)) } diff --git a/internal/views/help.go b/internal/views/help.go index 25d5c834..74f12bec 100644 --- a/internal/views/help.go +++ b/internal/views/help.go @@ -5,6 +5,7 @@ import ( "fmt" "runtime" "sort" + "strconv" "strings" "github.com/derailed/k9s/internal/ui" @@ -80,35 +81,95 @@ func (v *helpView) Init(_ context.Context, _ string) { func (v *helpView) showHelp() ui.Hints { return ui.Hints{ - {"?", "Help"}, - {"Ctrl-a", "Aliases view"}, + { + Mnemonic: "?", + Description: "Help", + }, + { + Mnemonic: "Ctrl-a", + Description: "Aliases", + }, } } func (v *helpView) showNav() ui.Hints { return ui.Hints{ - {"g", "Goto Top"}, - {"Shift-g", "Goto Bottom"}, - {"Ctrl-b", "Page Down"}, - {"Ctrl-f", "Page Up"}, - {"h", "Left"}, - {"l", "Right"}, - {"k", "Up"}, - {"j", "Down"}, + { + Mnemonic: "g", + Description: "Goto Top", + }, + { + Mnemonic: "Shift-g", + Description: "Goto Bottom", + }, + { + Mnemonic: "Ctrl-b", + Description: "Page Down"}, + { + Mnemonic: "Ctrl-f", + Description: "Page Up", + }, + { + Mnemonic: "h", + Description: "Left", + }, + { + Mnemonic: "l", + Description: "Right", + }, + { + Mnemonic: "k", + Description: "Up", + }, + { + Mnemonic: "j", + Description: "Down", + }, } } func (v *helpView) showGeneral() ui.Hints { return ui.Hints{ - {":cmd", "Command mode"}, - {"/term", "Filter mode"}, - {"esc", "Clear filter"}, - {"tab", "Next term match"}, - {"backtab", "Previous term match"}, - {"Ctrl-r", "Refresh"}, - {"Shift-i", "Invert Sort"}, - {"p", "Previous resource view"}, - {":q", "Quit"}, + { + Mnemonic: ":cmd", + Description: "Command mode", + }, + { + Mnemonic: "/term", + Description: "Filter mode", + }, + { + Mnemonic: "esc", + Description: "Clear filter", + }, + { + Mnemonic: "tab", + Description: "Next Field", + }, + { + Mnemonic: "backtab", + Description: "Previous Field", + }, + { + Mnemonic: "Ctrl-r", + Description: "Refresh", + }, + { + Mnemonic: "Ctrl-h", + Description: "Toggle Header", + }, + { + Mnemonic: "Shift-i", + Description: "Invert Sort", + }, + { + Mnemonic: "p", + Description: "Previous View", + }, + { + Mnemonic: ":q", + Description: "Quit", + }, } } @@ -127,24 +188,30 @@ func (v *helpView) resetTitle() { func (v *helpView) build(hh ui.Hints) { v.Clear() sort.Sort(hh) - v.addSection(0, 0, "Resource", hh) - v.addSection(0, 4, "General", v.showGeneral()) - v.addSection(0, 6, "Navigation", v.showNav()) - v.addSection(0, 8, "Help", v.showHelp()) + v.addSection(0, 0, "RESOURCE", hh) + v.addSection(0, 4, "GENERAL", v.showGeneral()) + v.addSection(0, 6, "NAVIGATION", v.showNav()) + v.addSection(0, 8, "HELP", v.showHelp()) } func (v *helpView) addSection(r, c int, title string, hh ui.Hints) { row := r cell := tview.NewTableCell(title) - cell.SetTextColor(tcell.ColorWhite) + cell.SetTextColor(tcell.ColorGreen) cell.SetAttributes(tcell.AttrBold) - v.SetCell(r, c, cell) + cell.SetExpansion(2) + cell.SetAlign(tview.AlignLeft) + v.SetCell(r, c+1, cell) row++ for _, h := range hh { col := c cell := tview.NewTableCell(toMnemonic(h.Mnemonic)) - cell.SetTextColor(tcell.ColorDodgerBlue) + if _, err := strconv.Atoi(h.Mnemonic); err != nil { + cell.SetTextColor(tcell.ColorDodgerBlue) + } else { + cell.SetTextColor(tcell.ColorFuchsia) + } cell.SetAttributes(tcell.AttrBold) cell.SetAlign(tview.AlignRight) v.SetCell(row, col, cell) diff --git a/internal/views/help_test.go b/internal/views/help_test.go index f3dbf5c5..f43c4832 100644 --- a/internal/views/help_test.go +++ b/internal/views/help_test.go @@ -42,7 +42,7 @@ func TestNewHelpView(t *testing.T) { cfg := config.NewConfig(ks{}) a := NewApp(cfg) - v := newHelpView(a, nil, ui.Hints{{"blee", "duh"}}) + v := newHelpView(a, nil, ui.Hints{{Mnemonic: "blee", Description: "duh"}}) v.Init(nil, "") assert.Equal(t, "", v.GetCell(1, 0).Text) diff --git a/internal/views/job.go b/internal/views/job.go index 2600cca9..e1c715c0 100644 --- a/internal/views/job.go +++ b/internal/views/job.go @@ -12,8 +12,8 @@ type jobView struct { *logResourceView } -func newJobView(t string, app *appView, list resource.List) resourceViewer { - v := jobView{newLogResourceView(t, app, list)} +func newJobView(title, gvr string, app *appView, list resource.List) resourceViewer { + v := jobView{newLogResourceView(title, gvr, app, list)} v.extraActionsFn = v.extraActions v.enterFn = v.showPods diff --git a/internal/views/log_resource.go b/internal/views/log_resource.go index 0d9d8533..825b03a6 100644 --- a/internal/views/log_resource.go +++ b/internal/views/log_resource.go @@ -16,9 +16,9 @@ type ( } ) -func newLogResourceView(ns string, app *appView, list resource.List) *logResourceView { +func newLogResourceView(title, gvr string, app *appView, list resource.List) *logResourceView { v := logResourceView{ - resourceView: newResourceView(ns, app, list).(*resourceView), + resourceView: newResourceView(title, gvr, app, list).(*resourceView), } v.AddPage("logs", newLogsView(list.GetName(), app, &v), true, false) diff --git a/internal/views/no.go b/internal/views/no.go index b53de064..a29aea8f 100644 --- a/internal/views/no.go +++ b/internal/views/no.go @@ -10,8 +10,8 @@ type nodeView struct { *resourceView } -func newNodeView(t string, app *appView, list resource.List) resourceViewer { - v := nodeView{newResourceView(t, app, list).(*resourceView)} +func newNodeView(title, gvr string, app *appView, list resource.List) resourceViewer { + v := nodeView{newResourceView(title, gvr, app, list).(*resourceView)} v.extraActionsFn = v.extraActions v.enterFn = v.showPods @@ -19,10 +19,10 @@ func newNodeView(t string, app *appView, list resource.List) resourceViewer { } func (v *nodeView) extraActions(aa ui.KeyActions) { - aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", v.sortColCmd(7, false), true) - aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", v.sortColCmd(8, false), true) - aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", v.sortColCmd(9, false), true) - aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", v.sortColCmd(10, false), true) + aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", v.sortColCmd(7, false), false) + aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", v.sortColCmd(8, false), false) + aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", v.sortColCmd(9, false), false) + aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", v.sortColCmd(10, false), false) } func (v *nodeView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { @@ -50,7 +50,7 @@ func showPods(app *appView, ns, labelSel, fieldSel string, a ui.ActionHandler) { list.SetLabelSelector(labelSel) list.SetFieldSelector(fieldSel) - pv := newPodView("Pods", app, list) + pv := newPodView("Pods", "v1/pods", app, list) pv.setColorerFn(podColorer) // pv.setExtraActionsFn(func(aa ui.KeyActions) { pv.masterPage().SetActions(ui.KeyActions{ diff --git a/internal/views/ns.go b/internal/views/ns.go index bf523bf1..a04d6ca7 100644 --- a/internal/views/ns.go +++ b/internal/views/ns.go @@ -21,8 +21,8 @@ type namespaceView struct { *resourceView } -func newNamespaceView(t string, app *appView, list resource.List) resourceViewer { - v := namespaceView{newResourceView(t, app, list).(*resourceView)} +func newNamespaceView(title, gvr string, app *appView, list resource.List) resourceViewer { + v := namespaceView{newResourceView(title, gvr, app, list).(*resourceView)} v.extraActionsFn = v.extraActions v.masterPage().SetSelectedFn(v.cleanser) v.decorateFn = v.decorate diff --git a/internal/views/pod.go b/internal/views/pod.go index e575d8f3..cded5185 100644 --- a/internal/views/pod.go +++ b/internal/views/pod.go @@ -32,8 +32,8 @@ type loggable interface { switchPage(n string) } -func newPodView(t string, app *appView, list resource.List) resourceViewer { - v := podView{resourceView: newResourceView(t, app, list).(*resourceView)} +func newPodView(title, gvr string, app *appView, list resource.List) resourceViewer { + v := podView{resourceView: newResourceView(title, gvr, app, list).(*resourceView)} v.extraActionsFn = v.extraActions v.enterFn = v.listContainers @@ -56,15 +56,15 @@ func (v *podView) extraActions(aa ui.KeyActions) { aa[ui.KeyL] = ui.NewKeyAction("Logs", v.logsCmd, true) aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", v.prevLogsCmd, true) - aa[ui.KeyShiftR] = ui.NewKeyAction("Sort Ready", v.sortColCmd(1, false), true) - aa[ui.KeyShiftS] = ui.NewKeyAction("Sort Status", v.sortColCmd(2, true), true) - aa[ui.KeyShiftT] = ui.NewKeyAction("Sort Restart", v.sortColCmd(3, false), true) - aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", v.sortColCmd(4, false), true) - aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", v.sortColCmd(5, false), true) - aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", v.sortColCmd(6, false), true) - aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", v.sortColCmd(7, false), true) - aa[ui.KeyShiftD] = ui.NewKeyAction("Sort IP", v.sortColCmd(8, true), true) - aa[ui.KeyShiftO] = ui.NewKeyAction("Sort Node", v.sortColCmd(9, true), true) + aa[ui.KeyShiftR] = ui.NewKeyAction("Sort Ready", v.sortColCmd(1, false), false) + aa[ui.KeyShiftS] = ui.NewKeyAction("Sort Status", v.sortColCmd(2, true), false) + aa[ui.KeyShiftT] = ui.NewKeyAction("Sort Restart", v.sortColCmd(3, false), false) + aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", v.sortColCmd(4, false), false) + aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", v.sortColCmd(5, false), false) + aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", v.sortColCmd(6, false), false) + aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", v.sortColCmd(7, false), false) + aa[ui.KeyShiftD] = ui.NewKeyAction("Sort IP", v.sortColCmd(8, true), false) + aa[ui.KeyShiftO] = ui.NewKeyAction("Sort Node", v.sortColCmd(9, true), false) } func (v *podView) listContainers(app *appView, _, res, sel string) { diff --git a/internal/views/policy.go b/internal/views/policy.go index 71664161..d3dc978c 100644 --- a/internal/views/policy.go +++ b/internal/views/policy.go @@ -76,10 +76,10 @@ func (v *policyView) bindKeys() { tcell.KeyEscape: ui.NewKeyAction("Reset", v.resetCmd, false), ui.KeySlash: ui.NewKeyAction("Filter", v.activateCmd, false), ui.KeyP: ui.NewKeyAction("Previous", v.app.prevCmd, false), - ui.KeyShiftS: ui.NewKeyAction("Sort Namespace", v.SortColCmd(0), true), - ui.KeyShiftN: ui.NewKeyAction("Sort Name", v.SortColCmd(1), true), - ui.KeyShiftO: ui.NewKeyAction("Sort Group", v.SortColCmd(2), true), - ui.KeyShiftB: ui.NewKeyAction("Sort Binding", v.SortColCmd(3), true), + ui.KeyShiftS: ui.NewKeyAction("Sort Namespace", v.SortColCmd(0), false), + ui.KeyShiftN: ui.NewKeyAction("Sort Name", v.SortColCmd(1), false), + ui.KeyShiftO: ui.NewKeyAction("Sort Group", v.SortColCmd(2), false), + ui.KeyShiftB: ui.NewKeyAction("Sort Binding", v.SortColCmd(3), false), }) } @@ -96,8 +96,8 @@ func (v *policyView) refresh() { } func (v *policyView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.Cmd().Empty() { - v.Cmd().Reset() + if !v.SearchBuff().Empty() { + v.SearchBuff().Reset() return nil } @@ -109,8 +109,8 @@ func (v *policyView) backCmd(evt *tcell.EventKey) *tcell.EventKey { v.cancel() } - if v.Cmd().IsActive() { - v.Cmd().Reset() + if v.SearchBuff().IsActive() { + v.SearchBuff().Reset() return nil } diff --git a/internal/views/rbac.go b/internal/views/rbac.go index 90b0b7af..7f314603 100644 --- a/internal/views/rbac.go +++ b/internal/views/rbac.go @@ -125,7 +125,7 @@ func (v *rbacView) bindKeys() { tcell.KeyEscape: ui.NewKeyAction("Reset", v.resetCmd, false), ui.KeySlash: ui.NewKeyAction("Filter", v.activateCmd, false), ui.KeyP: ui.NewKeyAction("Previous", v.app.prevCmd, false), - ui.KeyShiftO: ui.NewKeyAction("Sort APIGroup", v.SortColCmd(1), true), + ui.KeyShiftO: ui.NewKeyAction("Sort APIGroup", v.SortColCmd(1), false), }) } @@ -142,8 +142,8 @@ func (v *rbacView) refresh() { } func (v *rbacView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.Cmd().Empty() { - v.Cmd().Reset() + if !v.SearchBuff().Empty() { + v.SearchBuff().Reset() return nil } @@ -155,8 +155,8 @@ func (v *rbacView) backCmd(evt *tcell.EventKey) *tcell.EventKey { v.cancel() } - if v.Cmd().IsActive() { - v.Cmd().Reset() + if v.SearchBuff().IsActive() { + v.SearchBuff().Reset() return nil } diff --git a/internal/views/registrar.go b/internal/views/registrar.go index 2eb13dbe..29461dd6 100644 --- a/internal/views/registrar.go +++ b/internal/views/registrar.go @@ -1,10 +1,10 @@ package views import ( - "fmt" - "path" "strings" + "time" + "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" @@ -13,22 +13,14 @@ import ( ) type ( - viewFn func(ns string, app *appView, list resource.List) resourceViewer + viewFn func(title, gvr string, app *appView, list resource.List) resourceViewer listFn func(c resource.Connection, ns string) resource.List enterFn func(app *appView, ns, resource, selection string) decorateFn func(resource.TableData) resource.TableData - crdCmd struct { - api string - version string - plural string - singular string - } - - resCmd struct { - crdCmd - + viewer struct { gvr string + kind string namespaced bool verbs metav1.Verbs viewFn viewFn @@ -38,51 +30,25 @@ type ( decorateFn decorateFn } - AliasConfig struct { - Aliases map[string]string `yaml:"aliases"` - } + viewers map[string]viewer ) -var DefaultAliasConfig = AliasConfig{ - Aliases: map[string]string{ - "Deployment": "dp", - "Secret": "sec", - "Jobs": "jo", - - "ClusterRoles": "cr", - "ClusterRoleBindings": "crb", - "RoleBindings": "rb", - "Roles": "ro", - - "NetworkPolicies": "np", - - "Contexts": "ctx", - "Users": "usr", - "Groups": "grp", - "PortForward": "pf", - "Benchmark": "be", - "ScreenDumps": "sd", - }, -} - -func aliasCmds(c k8s.Connection, m map[string]resCmd) { - resourceViews(c, m) +func aliasCmds(c k8s.Connection, vv viewers) { + resourceViews(c, vv) if c != nil { - allCRDs(c, m) + allCRDs(c, vv) } } func listFunc(l resource.List) viewFn { - return func(ns string, app *appView, list resource.List) resourceViewer { - return newResourceView( - ns, - app, - l, - ) + return func(title, gvr string, app *appView, list resource.List) resourceViewer { + return newResourceView(title, gvr, app, l) } } -func allCRDs(c k8s.Connection, m map[string]resCmd) { +var aliases = config.NewAliases() + +func allCRDs(c k8s.Connection, vv viewers) { crds, err := resource.NewCustomResourceDefinitionList(c, resource.AllNamespaces). Resource(). List(resource.AllNamespaces) @@ -91,30 +57,31 @@ func allCRDs(c k8s.Connection, m map[string]resCmd) { return } + t := time.Now() + var meta resource.TypeMeta for _, crd := range crds { - ff := crd.ExtFields() + crd.ExtFields(&meta) - gvr := path.Join(ff["group"].(string), ff["version"].(string), ff["kind"].(string)) - var name string - if p, ok := ff["plural"].(string); ok { - aliases[p] = gvr - name = p + gvr := k8s.NewGVR(meta.Group, meta.Version, meta.Plural) + gvrs := gvr.String() + if meta.Plural != "" { + aliases.Define(meta.Plural, gvrs) } - if s, ok := ff["singular"].(string); ok { - aliases[s] = gvr - name = s + if meta.Singular != "" { + aliases.Define(meta.Singular, gvrs) } - if aa, ok := ff["aliases"].([]interface{}); ok { - for _, a := range aa { - aliases[a.(string)] = gvr - } + for _, a := range meta.ShortNames { + aliases.Define(a, gvrs) } - m[gvr] = resCmd{ - gvr: gvr, - viewFn: listFunc(resource.NewCustomList(c, "", ff["group"].(string), ff["version"].(string), name)), + + vv[gvrs] = viewer{ + gvr: gvrs, + kind: meta.Kind, + viewFn: listFunc(resource.NewCustomList(c, meta.Namespaced, "", gvrs)), colorerFn: ui.DefaultColorer, } } + log.Debug().Msgf("Loading CRDS %v", time.Since(t)) } func showRBAC(app *appView, ns, resource, selection string) { @@ -156,40 +123,44 @@ func showSAPolicy(app *appView, _, _, selection string) { app.inject(newPolicyView(app, mapFuSubject("ServiceAccount"), n)) } -type Aliases map[string]string - -var aliases Aliases - -func load(c k8s.Connection, viewRes map[string]resCmd) { - // cc := map[string]resCmd{} - aliases = make(Aliases, len(viewRes)) +func load(c k8s.Connection, vv viewers) { + if err := aliases.Load(); err != nil { + log.Error().Err(err).Msg("No custom aliases defined in config") + } rr, _ := c.DialOrDie().Discovery().ServerPreferredResources() for _, r := range rr { - log.Debug().Msgf("Group %#v", r.GroupVersion) for _, res := range r.APIResources { - log.Debug().Msgf("\tRes %s -- %q:%q -- %+v", res.Name, res.Group, res.Version, res.ShortNames) - gvr := path.Join(r.GroupVersion, res.Name) - // Get singular, plural, shortname and to alias under gvr name - if cmd, ok := viewRes[gvr]; !ok { - log.Error().Msgf(fmt.Sprintf(">> Missed %s", gvr)) - } else { - log.Debug().Msgf("Res %#v", res) - cmd.namespaced = res.Namespaced - cmd.verbs = res.Verbs - cmd.gvr = gvr - viewRes[gvr] = cmd - aliases[strings.ToLower(res.Kind)] = gvr - aliases[res.Name] = gvr - aliases[res.SingularName] = gvr - for _, s := range res.ShortNames { - aliases[s] = gvr - } + gvr := k8s.ToGVR(r.GroupVersion, res.Name) + cmd, ok := vv[gvr.String()] + if !ok { + // log.Debug().Msgf(fmt.Sprintf(">> No viewer defined for `%s`", gvr)) + continue + } + cmd.namespaced = res.Namespaced + cmd.kind = res.Kind + cmd.verbs = res.Verbs + cmd.gvr = gvr.String() + vv[gvr.String()] = cmd + gvrStr := gvr.String() + aliases.Define( + strings.ToLower(res.Kind), gvrStr, + res.Name, gvrStr, + ) + if len(res.SingularName) > 0 { + aliases.Define(res.SingularName, gvrStr) + } + for _, s := range res.ShortNames { + aliases.Define(s, gvrStr) } } } } -func resourceViews(c k8s.Connection, m map[string]resCmd) { +func resourceViews(c k8s.Connection, m viewers) { + defer func(t time.Time) { + log.Debug().Msgf("Loading Views Elapsed %v", time.Since(t)) + }(time.Now()) + coreRes(m) miscRes(m) appsRes(m) @@ -203,171 +174,170 @@ func resourceViews(c k8s.Connection, m map[string]resCmd) { load(c, m) } -func coreRes(m map[string]resCmd) { - m["v1/nodes"] = resCmd{ +func coreRes(vv viewers) { + vv["v1/nodes"] = viewer{ viewFn: newNodeView, listFn: resource.NewNodeList, colorerFn: nsColorer, } - m["v1/namespaces"] = resCmd{ + vv["v1/namespaces"] = viewer{ viewFn: newNamespaceView, listFn: resource.NewNamespaceList, colorerFn: nsColorer, } - m["v1/pods"] = resCmd{ + vv["v1/pods"] = viewer{ viewFn: newPodView, listFn: resource.NewPodList, colorerFn: podColorer, } - m["v1/serviceaccounts"] = resCmd{ - viewFn: newResourceView, + vv["v1/serviceaccounts"] = viewer{ listFn: resource.NewServiceAccountList, enterFn: showSAPolicy, } - m["v1/services"] = resCmd{ + vv["v1/services"] = viewer{ viewFn: newSvcView, listFn: resource.NewServiceList, } - m["v1/configmaps"] = resCmd{ + vv["v1/configmaps"] = viewer{ listFn: resource.NewConfigMapList, } - m["v1/persistentvolumes"] = resCmd{ + vv["v1/persistentvolumes"] = viewer{ listFn: resource.NewPersistentVolumeList, colorerFn: pvColorer, } - m["v1/persistentvolumeclaims"] = resCmd{ + vv["v1/persistentvolumeclaims"] = viewer{ listFn: resource.NewPersistentVolumeClaimList, colorerFn: pvcColorer, } - m["v1/secrets"] = resCmd{ + vv["v1/secrets"] = viewer{ viewFn: newSecretView, listFn: resource.NewSecretList, } - m["v1/endpoints"] = resCmd{ + vv["v1/endpoints"] = viewer{ listFn: resource.NewEndpointsList, } - m["v1/events"] = resCmd{ + vv["v1/events"] = viewer{ listFn: resource.NewEventList, colorerFn: evColorer, } - m["v1/replicationcontrollers"] = resCmd{ + vv["v1/replicationcontrollers"] = viewer{ viewFn: newScalableResourceView, listFn: resource.NewReplicationControllerList, colorerFn: rsColorer, } } -func miscRes(m map[string]resCmd) { - m["storage.k8s.io/storageclasses"] = resCmd{ +func miscRes(vv viewers) { + vv["storage.k8s.io/v1/storageclasses"] = viewer{ listFn: resource.NewStorageClassList, } - m["ctx"] = resCmd{ - gvr: "Contexts", + vv["contexts"] = viewer{ + gvr: "contexts", + kind: "Contexts", viewFn: newContextView, listFn: resource.NewContextList, colorerFn: ctxColorer, } - m["usr"] = resCmd{ + vv["users"] = viewer{ + gvr: "users", viewFn: newSubjectView, } - m["grp"] = resCmd{ + vv["groups"] = viewer{ + gvr: "groups", viewFn: newSubjectView, } - m["pf"] = resCmd{ - gvr: "PortForward", + vv["portforwards"] = viewer{ + gvr: "portforwards", viewFn: newForwardView, } - m["be"] = resCmd{ - gvr: "Benchmark", + vv["benchmarks"] = viewer{ + gvr: "benchmarks", viewFn: newBenchView, } - m["sd"] = resCmd{ - gvr: "ScreenDumps", + vv["screendumps"] = viewer{ + gvr: "screendumps", viewFn: newDumpView, } - } -func appsRes(m map[string]resCmd) { - m["apps/v1/deployments"] = resCmd{ +func appsRes(vv viewers) { + vv["apps/v1/deployments"] = viewer{ viewFn: newDeployView, listFn: resource.NewDeploymentList, colorerFn: dpColorer, } - m["apps/v1/replicasets"] = resCmd{ + vv["apps/v1/replicasets"] = viewer{ viewFn: newReplicaSetView, listFn: resource.NewReplicaSetList, colorerFn: rsColorer, } - m["apps/v1/statefulsets"] = resCmd{ + vv["apps/v1/statefulsets"] = viewer{ viewFn: newStatefulSetView, listFn: resource.NewStatefulSetList, colorerFn: stsColorer, } - m["apps/v1/daemonsets"] = resCmd{ + vv["apps/v1/daemonsets"] = viewer{ viewFn: newDaemonSetView, listFn: resource.NewDaemonSetList, colorerFn: dpColorer, } } -func authRes(m map[string]resCmd) { - m["rbac.authorization.k8s.io/v1/clusterroles"] = resCmd{ +func authRes(vv viewers) { + vv["rbac.authorization.k8s.io/v1/clusterroles"] = viewer{ listFn: resource.NewClusterRoleList, enterFn: showRBAC, } - m["rbac.authorization.k8s.io/v1/clusterrolebindings"] = resCmd{ + vv["rbac.authorization.k8s.io/v1/clusterrolebindings"] = viewer{ listFn: resource.NewClusterRoleBindingList, enterFn: showClusterRole, } - m["rbac.authorization.k8s.io/v1/rolebindings"] = resCmd{ + vv["rbac.authorization.k8s.io/v1/rolebindings"] = viewer{ listFn: resource.NewRoleBindingList, enterFn: showRole, } - m["rbac.authorization.k8s.io/v1/roles"] = resCmd{ + vv["rbac.authorization.k8s.io/v1/roles"] = viewer{ listFn: resource.NewRoleList, enterFn: showRBAC, } } -func extRes(m map[string]resCmd) { - m["apiextensions.k8s.io/v1/customresourcedefinitions"] = resCmd{ +func extRes(vv viewers) { + vv["apiextensions.k8s.io/v1/customresourcedefinitions"] = viewer{ listFn: resource.NewCustomResourceDefinitionList, enterFn: showCRD, } } -func netRes(m map[string]resCmd) { - m["networking.k8s.io/v1/networkpolicies"] = resCmd{ - gvr: "apiextensions.k8s.io/NetworkPolicies", +func netRes(vv viewers) { + vv["networking.k8s.io/v1/networkpolicies"] = viewer{ listFn: resource.NewNetworkPolicyList, } - m["networking.k8s.io/v1beta1/ingresses"] = resCmd{ + vv["extensions/v1beta1/ingresses"] = viewer{ listFn: resource.NewIngressList, } } -func batchRes(m map[string]resCmd) { - m["batch/v1/cronjobs"] = resCmd{ +func batchRes(vv viewers) { + vv["batch/v1beta1/cronjobs"] = viewer{ viewFn: newCronJobView, listFn: resource.NewCronJobList, } - m["batch/v1/jobs"] = resCmd{ + vv["batch/v1/jobs"] = viewer{ viewFn: newJobView, listFn: resource.NewJobList, } } -func policyRes(m map[string]resCmd) { - m["policy/v1beta1/poddisruptionbudgets"] = resCmd{ - viewFn: newResourceView, +func policyRes(vv viewers) { + vv["policy/v1beta1/poddisruptionbudgets"] = viewer{ listFn: resource.NewPDBList, colorerFn: pdbColorer, } } -func hpaRes(m map[string]resCmd) { - m["autoscaling/v1/horizontalpodautoscalers"] = resCmd{ +func hpaRes(vv viewers) { + vv["autoscaling/v1/horizontalpodautoscalers"] = viewer{ listFn: resource.NewHorizontalPodAutoscalerV1List, } } diff --git a/internal/views/resource.go b/internal/views/resource.go index 6cc60770..08630bcd 100644 --- a/internal/views/resource.go +++ b/internal/views/resource.go @@ -37,12 +37,14 @@ type ( colorerFn ui.ColorerFunc decorateFn decorateFn envFn envFn + gvr string } ) -func newResourceView(title string, app *appView, list resource.List) resourceViewer { +func newResourceView(title, gvr string, app *appView, list resource.List) resourceViewer { v := resourceView{ list: list, + gvr: gvr, } v.masterDetail = newMasterDetail(title, list.GetNamespace(), app, v.backCmd) v.envFn = v.defaultK9sEnv @@ -223,7 +225,7 @@ func (v *resourceView) defaultEnter(ns, _, selection string) { return } - yaml, err := v.list.Resource().Describe(v.masterPage().GetBaseTitle(), selection) + yaml, err := v.list.Resource().Describe(v.gvr, selection) if err != nil { v.app.Flash().Errf("Describe command failed: %s", err) return @@ -316,7 +318,7 @@ func (v *resourceView) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey { v.refresh() v.masterPage().UpdateTitle() v.masterPage().SelectRow(1, true) - v.app.GetCmdBuff().Reset() + v.app.CmdBuff().Reset() v.app.Config.SetActiveNamespace(v.currentNS) v.app.Config.Save() diff --git a/internal/views/rs.go b/internal/views/rs.go index 585716c1..40565dea 100644 --- a/internal/views/rs.go +++ b/internal/views/rs.go @@ -23,8 +23,8 @@ type replicaSetView struct { *resourceView } -func newReplicaSetView(t string, app *appView, list resource.List) resourceViewer { - v := replicaSetView{newResourceView(t, app, list).(*resourceView)} +func newReplicaSetView(title, gvr string, app *appView, list resource.List) resourceViewer { + v := replicaSetView{newResourceView(title, gvr, app, list).(*resourceView)} v.extraActionsFn = v.extraActions v.enterFn = v.showPods @@ -32,8 +32,8 @@ func newReplicaSetView(t string, app *appView, list resource.List) resourceViewe } func (v *replicaSetView) extraActions(aa ui.KeyActions) { - aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(2, false), true) - aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(3, false), true) + aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(2, false), false) + aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(3, false), false) aa[tcell.KeyCtrlB] = ui.NewKeyAction("Rollback", v.rollbackCmd, true) } diff --git a/internal/views/scalable_resource.go b/internal/views/scalable_resource.go index 121dcfa5..be1f6325 100644 --- a/internal/views/scalable_resource.go +++ b/internal/views/scalable_resource.go @@ -17,8 +17,8 @@ type ( } ) -func newScalableResourceView(title string, app *appView, list resource.List) resourceViewer { - return *newScalableResourceViewForParent(newResourceView(title, app, list).(*resourceView)) +func newScalableResourceView(title, gvr string, app *appView, list resource.List) resourceViewer { + return *newScalableResourceViewForParent(newResourceView(title, gvr, app, list).(*resourceView)) } func newScalableResourceViewForParent(parent *resourceView) *scalableResourceView { diff --git a/internal/views/secret.go b/internal/views/secret.go index 8ad5031f..617537b9 100644 --- a/internal/views/secret.go +++ b/internal/views/secret.go @@ -13,8 +13,8 @@ type secretView struct { *resourceView } -func newSecretView(t string, app *appView, list resource.List) resourceViewer { - v := secretView{newResourceView(t, app, list).(*resourceView)} +func newSecretView(title, gvr string, app *appView, list resource.List) resourceViewer { + v := secretView{newResourceView(title, gvr, app, list).(*resourceView)} v.extraActionsFn = v.extraActions return &v diff --git a/internal/views/sts.go b/internal/views/sts.go index f3219fb1..9d3c57d2 100644 --- a/internal/views/sts.go +++ b/internal/views/sts.go @@ -14,8 +14,8 @@ type statefulSetView struct { scalableResourceView *scalableResourceView } -func newStatefulSetView(t string, app *appView, list resource.List) resourceViewer { - logResourceView := newLogResourceView(t, app, list) +func newStatefulSetView(title, gvr string, app *appView, list resource.List) resourceViewer { + logResourceView := newLogResourceView(title, gvr, app, list) v := statefulSetView{logResourceView, newScalableResourceViewForParent(logResourceView.resourceView)} v.extraActionsFn = v.extraActions v.enterFn = v.showPods @@ -26,8 +26,8 @@ func newStatefulSetView(t string, app *appView, list resource.List) resourceView func (v *statefulSetView) extraActions(aa ui.KeyActions) { v.logResourceView.extraActions(aa) v.scalableResourceView.extraActions(aa) - aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(1, false), true) - aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(2, false), true) + aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(1, false), false) + aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(2, false), false) } func (v *statefulSetView) showPods(app *appView, ns, res, sel string) { diff --git a/internal/views/subject.go b/internal/views/subject.go index 9c914084..7a181bca 100644 --- a/internal/views/subject.go +++ b/internal/views/subject.go @@ -33,7 +33,7 @@ type ( } ) -func newSubjectView(ns string, app *appView, list resource.List) resourceViewer { +func newSubjectView(title, gvr string, app *appView, list resource.List) resourceViewer { v := subjectView{} v.tableView = newTableView(app, "Subject") v.SetActiveNS("*") @@ -95,7 +95,7 @@ func (v *subjectView) bindKeys() { tcell.KeyEscape: ui.NewKeyAction("Reset", v.resetCmd, false), ui.KeySlash: ui.NewKeyAction("Filter", v.activateCmd, false), ui.KeyP: ui.NewKeyAction("Previous", v.app.prevCmd, false), - ui.KeyShiftK: ui.NewKeyAction("Sort Kind", v.SortColCmd(1), true), + ui.KeyShiftK: ui.NewKeyAction("Sort Kind", v.SortColCmd(1), false), }) } @@ -136,8 +136,8 @@ func (v *subjectView) policyCmd(evt *tcell.EventKey) *tcell.EventKey { } func (v *subjectView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.Cmd().Empty() { - v.Cmd().Reset() + if !v.SearchBuff().Empty() { + v.SearchBuff().Reset() return nil } @@ -149,8 +149,8 @@ func (v *subjectView) backCmd(evt *tcell.EventKey) *tcell.EventKey { v.cancel() } - if v.Cmd().IsActive() { - v.Cmd().Reset() + if v.SearchBuff().IsActive() { + v.SearchBuff().Reset() return nil } @@ -285,8 +285,9 @@ func (v *subjectView) namespacedSubjects() (resource.RowEvents, error) { } func mapCmdSubject(subject string) string { + log.Debug().Msgf("!!!!!!Subject %q", subject) switch subject { - case "grp": + case "groups": return "Group" case "sas": return "ServiceAccount" diff --git a/internal/views/svc.go b/internal/views/svc.go index 1fb0b22f..e8eea32d 100644 --- a/internal/views/svc.go +++ b/internal/views/svc.go @@ -22,9 +22,9 @@ type svcView struct { bench *perf.Benchmark } -func newSvcView(t string, app *appView, list resource.List) resourceViewer { +func newSvcView(title, gvr string, app *appView, list resource.List) resourceViewer { v := svcView{ - resourceView: newResourceView(t, app, list).(*resourceView), + resourceView: newResourceView(title, gvr, app, list).(*resourceView), } v.extraActionsFn = v.extraActions v.enterFn = v.showPods @@ -47,7 +47,7 @@ func (v *svcView) extraActions(aa ui.KeyActions) { aa[ui.KeyL] = ui.NewKeyAction("Logs", v.logsCmd, true) aa[tcell.KeyCtrlB] = ui.NewKeyAction("Bench", v.benchCmd, true) aa[tcell.KeyCtrlK] = ui.NewKeyAction("Bench Stop", v.benchStopCmd, true) - aa[ui.KeyShiftT] = ui.NewKeyAction("Sort Type", v.sortColCmd(1, false), true) + aa[ui.KeyShiftT] = ui.NewKeyAction("Sort Type", v.sortColCmd(1, false), false) } func (v *svcView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { @@ -217,7 +217,7 @@ func (v *svcView) showSvcPods(ns string, sel map[string]string, b ui.ActionHandl list := resource.NewPodList(v.app.Conn(), ns) list.SetLabelSelector(strings.Join(s, ",")) - pv := newPodView("Pods", v.app, list) + pv := newPodView("Pods", "v1/pods", v.app, list) pv.setColorerFn(podColorer) pv.setExtraActionsFn(func(aa ui.KeyActions) { aa[tcell.KeyEsc] = ui.NewKeyAction("Back", b, true) diff --git a/internal/views/table.go b/internal/views/table.go index d691e521..27dc3582 100644 --- a/internal/views/table.go +++ b/internal/views/table.go @@ -17,14 +17,23 @@ func newTableView(app *appView, title string) *tableView { Table: ui.NewTable(title, app.Styles), app: app, } - v.Cmd().AddListener(app.Cmd()) - v.Cmd().Reset() + v.SearchBuff().AddListener(app.Cmd()) + v.SearchBuff().AddListener(&v) + v.SearchBuff().Reset() v.bindKeys() return &v } +// BufferChanged indicates the buffer was changed. +func (v *tableView) BufferChanged(s string) {} + +// BufferActive indicates the buff activity changed. +func (v *tableView) BufferActive(state bool) { + v.app.BufferActive(state) +} + func (v *tableView) saveCmd(evt *tcell.EventKey) *tcell.EventKey { if path, err := saveTable(v.app.Config.K9s.CurrentCluster, v.GetBaseTitle(), v.GetData()); err != nil { v.app.Flash().Err(err) @@ -49,18 +58,18 @@ func (v *tableView) bindKeys() { tcell.KeyBackspace: ui.NewKeyAction("Erase", v.eraseCmd, false), tcell.KeyDelete: ui.NewKeyAction("Erase", v.eraseCmd, false), ui.KeyShiftI: ui.NewKeyAction("Invert", v.SortInvertCmd, false), - ui.KeyShiftN: ui.NewKeyAction("Sort Name", v.SortColCmd(0), true), - ui.KeyShiftA: ui.NewKeyAction("Sort Age", v.SortColCmd(-1), true), + ui.KeyShiftN: ui.NewKeyAction("Sort Name", v.SortColCmd(0), false), + ui.KeyShiftA: ui.NewKeyAction("Sort Age", v.SortColCmd(-1), false), }) } func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.Cmd().IsActive() { + if !v.SearchBuff().IsActive() { return evt } - v.Cmd().SetActive(false) - cmd := v.Cmd().String() + v.SearchBuff().SetActive(false) + cmd := v.SearchBuff().String() if isLabelSelector(cmd) && v.filterFn != nil { v.filterFn(trimLabelSelector(cmd)) return nil @@ -71,21 +80,21 @@ func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey { } func (v *tableView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { - if v.Cmd().IsActive() { - v.Cmd().Delete() + if v.SearchBuff().IsActive() { + v.SearchBuff().Delete() } return nil } func (v *tableView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.Cmd().Empty() { + if !v.SearchBuff().Empty() { v.app.Flash().Info("Clearing filter...") } - if isLabelSelector(v.Cmd().String()) { + if isLabelSelector(v.SearchBuff().String()) { v.filterFn("") } - v.Cmd().Reset() + v.SearchBuff().Reset() v.Refresh() return nil @@ -97,11 +106,11 @@ func (v *tableView) activateCmd(evt *tcell.EventKey) *tcell.EventKey { } v.app.Flash().Info("Filter mode activated.") - if isLabelSelector(v.Cmd().String()) { + if isLabelSelector(v.SearchBuff().String()) { return nil } - v.Cmd().Reset() - v.Cmd().SetActive(true) + v.SearchBuff().Reset() + v.SearchBuff().SetActive(true) return nil } diff --git a/internal/views/table_test.go b/internal/views/table_test.go index 3c3b13f6..ade5cb9c 100644 --- a/internal/views/table_test.go +++ b/internal/views/table_test.go @@ -72,8 +72,8 @@ func TestTableViewFilter(t *testing.T) { Namespace: "", } v.Update(data) - v.Cmd().SetActive(true) - v.Cmd().Set([]rune("blee")) + v.SearchBuff().SetActive(true) + v.SearchBuff().Set([]rune("blee")) v.filterCmd(nil) assert.Equal(t, 2, v.GetRowCount()) v.resetCmd(nil)