diff --git a/change_logs/release_v0.19.7.md b/change_logs/release_v0.19.7.md new file mode 100644 index 00000000..b1a42ef7 --- /dev/null +++ b/change_logs/release_v0.19.7.md @@ -0,0 +1,27 @@ + + +# Release v0.19.7 + +## 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, consider joining our [sponsorhip program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +--- + +Maintenance Release! + +## Resolved Bugs/Features/PRs + +* [Issue #726](https://github.com/derailed/k9s/issues/726) +* [Issue #724](https://github.com/derailed/k9s/issues/724) +* [Issue #722](https://github.com/derailed/k9s/issues/722) +* [Issue #721](https://github.com/derailed/k9s/issues/721) +* [Issue #720](https://github.com/derailed/k9s/issues/720) + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/go.mod b/go.mod index c24602e2..37e42bfa 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/fsnotify/fsnotify v1.4.7 github.com/gdamore/tcell v1.3.0 github.com/ghodss/yaml v1.0.0 + github.com/golang/protobuf v1.4.2 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-runewidth v0.0.9 github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec @@ -23,7 +24,11 @@ require ( github.com/sahilm/fuzzy v0.1.0 github.com/spf13/cobra v1.0.0 github.com/stretchr/testify v1.5.1 + golang.org/x/net v0.0.0-20200519113804-d87ec0cfa476 // indirect + golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 // indirect golang.org/x/text v0.3.2 + google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587 // indirect + google.golang.org/grpc v1.29.1 // indirect gopkg.in/yaml.v2 v2.2.8 helm.sh/helm/v3 v3.2.0 k8s.io/api v0.18.2 diff --git a/go.sum b/go.sum index 1f7762b1..cce19a6e 100644 --- a/go.sum +++ b/go.sum @@ -98,6 +98,7 @@ github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= @@ -178,7 +179,9 @@ github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkg github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M= @@ -286,6 +289,16 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho= github.com/golangplus/fmt v0.0.0-20150411045040-2a5d6d7d2995/go.mod h1:lJgMEyOkYFkPcDKwRXegd+iM6E7matEszMG5HhwytU8= github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= @@ -681,6 +694,8 @@ golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200519113804-d87ec0cfa476 h1:E7ct1C6/33eOdrGZKMoyntcEvs2dwZnDe30crG5vpYU= +golang.org/x/net v0.0.0-20200519113804-d87ec0cfa476/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= @@ -719,6 +734,9 @@ golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -767,15 +785,28 @@ google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587 h1:1Ym+vvUpq1ZHvxzn34gENJX8U4aKO+vhy2P/2+Xl6qQ= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/client/client.go b/internal/client/client.go index 55978be4..3c36fd9c 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -68,8 +68,8 @@ func InitConnectionOrDie(config *Config) *APIClient { } func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview { - if ns == "-" { - ns = "" + if ns == ClusterScope { + ns = AllNamespaces } spec := NewGVR(gvr) res := spec.GVR() @@ -124,6 +124,7 @@ func (a *APIClient) clearCache() { // CanI checks if user has access to a certain resource. func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) { + // log.Debug().Msgf("Check Access %q::%q", ns, gvr) if IsClusterWide(ns) { ns = AllNamespaces } diff --git a/internal/client/metrics.go b/internal/client/metrics.go index 9df634e3..e13269e9 100644 --- a/internal/client/metrics.go +++ b/internal/client/metrics.go @@ -135,7 +135,7 @@ func (m *MetricsServer) FetchNodesMetrics(ctx context.Context) (*mv1beta1.NodeMe const msg = "user is not authorized to list node metrics" mx := new(mv1beta1.NodeMetricsList) - if err := m.checkAccess("", "metrics.k8s.io/v1beta1/nodes", msg); err != nil { + if err := m.checkAccess(ClusterScope, "metrics.k8s.io/v1beta1/nodes", msg); err != nil { return mx, err } diff --git a/internal/dao/dp.go b/internal/dao/dp.go index c743888a..5ec26343 100644 --- a/internal/dao/dp.go +++ b/internal/dao/dp.go @@ -110,7 +110,7 @@ func (d *Deployment) Pod(fqn string) (string, error) { // Load returns a deployment instance. func (*Deployment) Load(f Factory, fqn string) (*appsv1.Deployment, error) { - o, err := f.Get("apps/v1/deployments", fqn, false, labels.Everything()) + o, err := f.Get("apps/v1/deployments", fqn, true, labels.Everything()) if err != nil { return nil, err } diff --git a/internal/dao/ds.go b/internal/dao/ds.go index b4270501..e23d8580 100644 --- a/internal/dao/ds.go +++ b/internal/dao/ds.go @@ -129,7 +129,7 @@ func (d *DaemonSet) Pod(fqn string) (string, error) { // GetInstance returns a daemonset instance. func (d *DaemonSet) GetInstance(fqn string) (*appsv1.DaemonSet, error) { - o, err := d.Factory.Get(d.gvr.String(), fqn, false, labels.Everything()) + o, err := d.Factory.Get(d.gvr.String(), fqn, true, labels.Everything()) if err != nil { return nil, err } diff --git a/internal/dao/node.go b/internal/dao/node.go index bd1367a4..19d7b0f3 100644 --- a/internal/dao/node.go +++ b/internal/dao/node.go @@ -166,7 +166,7 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) { // FetchNode retrieves a node. func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) { - auth, err := f.Client().CanI("", "v1/nodes", []string{"get"}) + auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", []string{"get"}) if err != nil { return nil, err } @@ -179,7 +179,7 @@ func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) { // FetchNodes retrieves all nodes. func FetchNodes(ctx context.Context, f Factory, labelsSel string) (*v1.NodeList, error) { - auth, err := f.Client().CanI("", "v1/nodes", []string{client.ListVerb}) + auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", []string{client.ListVerb}) if err != nil { return nil, err } diff --git a/internal/dao/registry.go b/internal/dao/registry.go index d0fd7feb..36993a70 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -287,7 +287,7 @@ func loadPreferred(f Factory, m ResourceMetas) error { func loadCRDs(f Factory, m ResourceMetas) { const crdGVR = "apiextensions.k8s.io/v1beta1/customresourcedefinitions" - oo, err := f.List(crdGVR, "", true, labels.Everything()) + oo, err := f.List(crdGVR, client.ClusterScope, true, labels.Everything()) if err != nil { log.Warn().Err(err).Msgf("Fail CRDs load") return diff --git a/internal/model/cmd_buff.go b/internal/model/cmd_buff.go index 83960a3b..f2288eb8 100644 --- a/internal/model/cmd_buff.go +++ b/internal/model/cmd_buff.go @@ -40,37 +40,6 @@ func NewCmdBuff(key rune, kind BufferKind) *CmdBuff { } } -// CurrentSuggestion returns the current suggestion. -func (c *CmdBuff) CurrentSuggestion() (string, bool) { - return "", false -} - -// NextSuggestion returns the next suggestion. -func (c *CmdBuff) NextSuggestion() (string, bool) { - return "", false -} - -// PrevSuggestion returns the prev suggestion. -func (c *CmdBuff) PrevSuggestion() (string, bool) { - return "", false -} - -// ClearSuggestions clear out all suggestions. -func (c *CmdBuff) ClearSuggestions() {} - -// AutoSuggests returns true if model implements auto suggestions. -func (c *CmdBuff) AutoSuggests() bool { - return false -} - -// Suggestions returns suggestions. -func (c *CmdBuff) Suggestions() []string { - return nil -} - -// Notify notifies all listener of current suggestions. -func (c *CmdBuff) Notify() {} - // InCmdMode checks if a command exists and the buffer is active. func (c *CmdBuff) InCmdMode() bool { return c.active || len(c.buff) > 0 @@ -139,7 +108,7 @@ func (c *CmdBuff) AddListener(w BuffWatcher) { c.listeners = append(c.listeners, w) } -// RemoveListener unregisters a listener. +// RemoveListener removes a listener. func (c *CmdBuff) RemoveListener(l BuffWatcher) { victim := -1 for i, lis := range c.listeners { diff --git a/internal/model/fish_buff.go b/internal/model/fish_buff.go index 4a9a7cd8..834dd416 100644 --- a/internal/model/fish_buff.go +++ b/internal/model/fish_buff.go @@ -20,7 +20,6 @@ type FishBuff struct { *CmdBuff suggestionFn SuggestionFunc - suggestion string suggestions []string suggestionIndex int } @@ -35,43 +34,48 @@ func NewFishBuff(key rune, kind BufferKind) *FishBuff { // PrevSuggestion returns the prev suggestion. func (f *FishBuff) PrevSuggestion() (string, bool) { - if f.suggestionIndex < 0 { + if len(f.suggestions) == 0 { return "", false } - f.suggestionIndex-- + + if f.suggestionIndex < 0 { + f.suggestionIndex = 0 + } else { + f.suggestionIndex-- + } if f.suggestionIndex < 0 { f.suggestionIndex = len(f.suggestions) - 1 } + return f.suggestions[f.suggestionIndex], true } // NextSuggestion returns the next suggestion. func (f *FishBuff) NextSuggestion() (string, bool) { - if f.suggestionIndex < 0 { + if len(f.suggestions) == 0 { return "", false } + if f.suggestionIndex < 0 { + f.suggestionIndex = 0 + } else { + f.suggestionIndex++ + } if f.suggestionIndex >= len(f.suggestions) { f.suggestionIndex = 0 } - s := f.suggestions[f.suggestionIndex] - f.suggestionIndex++ - return s, true + return f.suggestions[f.suggestionIndex], true } // ClearSuggestions clear out all suggestions. func (f *FishBuff) ClearSuggestions() { - f.suggestion, f.suggestionIndex = "", -1 + f.suggestionIndex = -1 } // CurrentSuggestion returns the current suggestion. func (f *FishBuff) CurrentSuggestion() (string, bool) { - if f.suggestionIndex < 0 { - return "", false - } - - if f.suggestionIndex >= len(f.suggestions) { + if len(f.suggestions) == 0 || f.suggestionIndex < 0 || f.suggestionIndex >= len(f.suggestions) { return "", false } @@ -101,41 +105,31 @@ func (f *FishBuff) Notify() { if f.suggestionFn == nil { return } - cc := f.suggestionFn(string(f.buff)) - f.fireSuggestionChanged(cc) + f.fireSuggestionChanged(f.suggestionFn(string(f.buff))) } // Add adds a new charater to the buffer. func (f *FishBuff) Add(r rune) { f.CmdBuff.Add(r) - if f.suggestionFn == nil { - return - } - cc := f.suggestionFn(string(f.buff)) - f.fireSuggestionChanged(cc) + f.Notify() } // Delete removes the last character from the buffer. func (f *FishBuff) Delete() { f.CmdBuff.Delete() - if f.suggestionFn == nil { - return - } - cc := f.suggestionFn(string(f.buff)) - f.fireSuggestionChanged(cc) + f.Notify() } func (f *FishBuff) fireSuggestionChanged(ss []string) { f.suggestions, f.suggestionIndex = ss, 0 if len(ss) == 0 { - f.suggestionIndex, f.suggestion = -1, "" + f.suggestionIndex = -1 return } - f.suggestion = ss[f.suggestionIndex] for _, l := range f.listeners { if listener, ok := l.(SuggestionListener); ok { - listener.SuggestionChanged(f.GetText(), f.suggestion) + listener.SuggestionChanged(f.GetText(), ss[f.suggestionIndex]) } } } diff --git a/internal/model/fish_buff_test.go b/internal/model/fish_buff_test.go new file mode 100644 index 00000000..2db5d29b --- /dev/null +++ b/internal/model/fish_buff_test.go @@ -0,0 +1,89 @@ +package model_test + +import ( + "sort" + "testing" + + "github.com/derailed/k9s/internal/model" + "github.com/stretchr/testify/assert" +) + +func TestFishAdd(t *testing.T) { + m := mockSuggestionListener{} + + f := model.NewFishBuff(' ', model.FilterBuffer) + f.AddListener(&m) + f.SetSuggestionFn(func(text string) sort.StringSlice { + return sort.StringSlice{"blee", "duh"} + }) + f.Add('a') + f.SetActive(true) + + assert.Equal(t, 1, m.buff) + assert.Equal(t, 1, m.sugg) + assert.True(t, m.active) + assert.Equal(t, "blee", m.suggestion) + + c, ok := f.CurrentSuggestion() + assert.True(t, ok) + assert.Equal(t, "blee", c) + + c, ok = f.NextSuggestion() + assert.True(t, ok) + assert.Equal(t, "duh", c) + + c, ok = f.PrevSuggestion() + assert.True(t, ok) + assert.Equal(t, "blee", c) +} + +func TestFishDelete(t *testing.T) { + m := mockSuggestionListener{} + + f := model.NewFishBuff(' ', model.FilterBuffer) + f.AddListener(&m) + f.SetSuggestionFn(func(text string) sort.StringSlice { + return sort.StringSlice{"blee", "duh"} + }) + f.Add('a') + f.Delete() + f.SetActive(true) + + assert.Equal(t, 2, m.buff) + assert.Equal(t, 2, m.sugg) + assert.True(t, m.active) + assert.Equal(t, "blee", m.suggestion) + + c, ok := f.CurrentSuggestion() + assert.True(t, ok) + assert.Equal(t, "blee", c) + + c, ok = f.NextSuggestion() + assert.True(t, ok) + assert.Equal(t, "duh", c) + + c, ok = f.PrevSuggestion() + assert.True(t, ok) + assert.Equal(t, "blee", c) +} + +// Helpers... + +type mockSuggestionListener struct { + buff, sugg int + suggestion string + active bool +} + +func (m *mockSuggestionListener) BufferChanged(s string) { + m.buff++ +} + +func (m *mockSuggestionListener) BufferActive(state bool, kind model.BufferKind) { + m.active = state +} + +func (m *mockSuggestionListener) SuggestionChanged(text, sugg string) { + m.suggestion = sugg + m.sugg++ +} diff --git a/internal/model/history.go b/internal/model/history.go index 3561ff8f..f4c19380 100644 --- a/internal/model/history.go +++ b/internal/model/history.go @@ -2,6 +2,8 @@ package model import ( "strings" + + "github.com/rs/zerolog/log" ) // MaxHistory tracks max command history @@ -42,8 +44,9 @@ func (h *History) Push(c string) { h.commands = append([]string{c}, h.commands[:len(h.commands)-1]...) } -// Clear clear out the stack using pops. +// Clear clears out the stack. func (h *History) Clear() { + log.Debug().Msgf("History CLEARED!!!") h.commands = nil } diff --git a/internal/model/table.go b/internal/model/table.go index c0d1b087..12ea6736 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -75,6 +75,7 @@ func (t *Table) RemoveListener(l TableListener) { // Watch initiates model updates. func (t *Table) Watch(ctx context.Context) { + log.Debug().Msgf("TABLE-WATCH %q", t.gvr) t.refresh(ctx) go t.updater(ctx) } diff --git a/internal/ui/prompt.go b/internal/ui/prompt.go index cc924fb3..29ed48cd 100644 --- a/internal/ui/prompt.go +++ b/internal/ui/prompt.go @@ -14,10 +14,10 @@ const ( defaultSpacer = 4 ) -var _ PromptModel = (*model.CmdBuff)(nil) -var _ Suggester = (*model.CmdBuff)(nil) -var _ PromptModel = (*model.FishBuff)(nil) -var _ Suggester = (*model.FishBuff)(nil) +var ( + _ PromptModel = (*model.FishBuff)(nil) + _ Suggester = (*model.FishBuff)(nil) +) // Suggester provides suggestions. type Suggester interface { @@ -142,20 +142,21 @@ func (p *Prompt) keyboard(evt *tcell.EventKey) *tcell.EventKey { p.model.SetActive(false) case tcell.KeyCtrlW, tcell.KeyCtrlU: p.model.ClearText() - case tcell.KeyDown: - if next, ok := m.NextSuggestion(); ok { - p.suggest(p.model.GetText(), next) - } case tcell.KeyUp: - if prev, ok := m.PrevSuggestion(); ok { - p.suggest(p.model.GetText(), prev) + if s, ok := m.NextSuggestion(); ok { + p.suggest(p.model.GetText(), s) + } + case tcell.KeyDown: + if s, ok := m.PrevSuggestion(); ok { + p.suggest(p.model.GetText(), s) } case tcell.KeyTab, tcell.KeyRight, tcell.KeyCtrlF: - if curr, ok := m.CurrentSuggestion(); ok { - p.model.SetText(p.model.GetText() + curr) + if s, ok := m.CurrentSuggestion(); ok { + p.model.SetText(p.model.GetText() + s) m.ClearSuggestions() } } + return evt } diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index 5527ec24..b0765916 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -36,7 +36,7 @@ func TestTableUpdate(t *testing.T) { func TestTableSelection(t *testing.T) { v := ui.NewTable(client.NewGVR("fred")) v.Init(makeContext()) - m := &testModel{} + m := &mockModel{} v.SetModel(m) v.Update(m.Peek()) v.SelectRow(1, true) @@ -57,34 +57,36 @@ func TestTableSelection(t *testing.T) { // ---------------------------------------------------------------------------- // Helpers... -type testModel struct{} +type mockModel struct{} -var _ ui.Tabular = &testModel{} +var _ ui.Tabular = &mockModel{} -func (t *testModel) SetInstance(string) {} -func (t *testModel) Empty() bool { return false } -func (t *testModel) HasMetrics() bool { return true } -func (t *testModel) Peek() render.TableData { return makeTableData() } -func (t *testModel) ClusterWide() bool { return false } -func (t *testModel) GetNamespace() string { return "blee" } -func (t *testModel) SetNamespace(string) {} -func (t *testModel) ToggleToast() {} -func (t *testModel) AddListener(model.TableListener) {} -func (t *testModel) Watch(context.Context) {} -func (t *testModel) Get(ctx context.Context, path string) (runtime.Object, error) { +func (t *mockModel) SetInstance(string) {} +func (t *mockModel) Empty() bool { return false } +func (t *mockModel) HasMetrics() bool { return true } +func (t *mockModel) Peek() render.TableData { return makeTableData() } +func (t *mockModel) Refresh(context.Context) {} +func (t *mockModel) ClusterWide() bool { return false } +func (t *mockModel) GetNamespace() string { return "blee" } +func (t *mockModel) SetNamespace(string) {} +func (t *mockModel) ToggleToast() {} +func (t *mockModel) AddListener(model.TableListener) {} +func (t *mockModel) RemoveListener(model.TableListener) {} +func (t *mockModel) Watch(context.Context) {} +func (t *mockModel) Get(ctx context.Context, path string) (runtime.Object, error) { return nil, nil } -func (t *testModel) Delete(ctx context.Context, path string, c, f bool) error { +func (t *mockModel) Delete(ctx context.Context, path string, c, f bool) error { return nil } -func (t *testModel) Describe(context.Context, string) (string, error) { +func (t *mockModel) Describe(context.Context, string) (string, error) { return "", nil } -func (t *testModel) ToYAML(ctx context.Context, path string) (string, error) { +func (t *mockModel) ToYAML(ctx context.Context, path string) (string, error) { return "", nil } -func (t *testModel) InNamespace(string) bool { return true } -func (t *testModel) SetRefreshRate(time.Duration) {} +func (t *mockModel) InNamespace(string) bool { return true } +func (t *mockModel) SetRefreshRate(time.Duration) {} func makeTableData() render.TableData { t := render.NewTableData() diff --git a/internal/ui/tree.go b/internal/ui/tree.go index eb0e3e33..77915f7e 100644 --- a/internal/ui/tree.go +++ b/internal/ui/tree.go @@ -17,7 +17,7 @@ type Tree struct { actions KeyActions selectedItem string - cmdBuff *model.CmdBuff + cmdBuff *model.FishBuff expandNodes bool Count int keyListener KeyListenerFunc @@ -29,7 +29,7 @@ func NewTree() *Tree { TreeView: tview.NewTreeView(), expandNodes: true, actions: make(KeyActions), - cmdBuff: model.NewCmdBuff('/', model.FilterBuffer), + cmdBuff: model.NewFishBuff('/', model.FilterBuffer), } } @@ -62,7 +62,7 @@ func (t *Tree) ExpandNodes() bool { } // CmdBuff returns the filter command. -func (t *Tree) CmdBuff() *model.CmdBuff { +func (t *Tree) CmdBuff() *model.FishBuff { return t.cmdBuff } diff --git a/internal/ui/types.go b/internal/ui/types.go index 32a1e4ed..48794d4a 100644 --- a/internal/ui/types.go +++ b/internal/ui/types.go @@ -64,12 +64,18 @@ type Tabular interface { // Watch watches a given resource for changes. Watch(context.Context) + // Refresh forces a new refresh. + Refresh(context.Context) + // SetRefreshRate sets the model watch loop rate. SetRefreshRate(time.Duration) // AddListener registers a model listener. AddListener(model.TableListener) + // RemoveListener unregister a model listener. + RemoveListener(model.TableListener) + // Delete a resource. Delete(ctx context.Context, path string, cascade, force bool) error } diff --git a/internal/view/alias_test.go b/internal/view/alias_test.go index f4dfccce..3ff7ad4c 100644 --- a/internal/view/alias_test.go +++ b/internal/view/alias_test.go @@ -29,7 +29,7 @@ func TestAliasNew(t *testing.T) { func TestAliasSearch(t *testing.T) { v := view.NewAlias(client.NewGVR("aliases")) assert.Nil(t, v.Init(makeContext())) - v.GetTable().SetModel(&testModel{}) + v.GetTable().SetModel(&mockModel{}) v.GetTable().Refresh() v.App().Prompt().SetModel(v.GetTable().CmdBuff()) v.App().Prompt().SendStrokes("blee") @@ -94,41 +94,44 @@ func (k ks) NamespaceNames(nn []v1.Namespace) []string { return []string{"test"} } -type testModel struct{} +type mockModel struct{} -var _ ui.Tabular = (*testModel)(nil) -var _ ui.Suggester = (*testModel)(nil) +var _ ui.Tabular = (*mockModel)(nil) +var _ ui.Suggester = (*mockModel)(nil) -func (t *testModel) CurrentSuggestion() (string, bool) { return "", false } -func (t *testModel) NextSuggestion() (string, bool) { return "", false } -func (t *testModel) PrevSuggestion() (string, bool) { return "", false } -func (t *testModel) ClearSuggestions() {} +func (t *mockModel) CurrentSuggestion() (string, bool) { return "", false } +func (t *mockModel) NextSuggestion() (string, bool) { return "", false } +func (t *mockModel) PrevSuggestion() (string, bool) { return "", false } +func (t *mockModel) ClearSuggestions() {} + +func (t *mockModel) SetInstance(string) {} +func (t *mockModel) Empty() bool { return false } +func (t *mockModel) HasMetrics() bool { return true } +func (t *mockModel) Peek() render.TableData { return makeTableData() } +func (t *mockModel) ClusterWide() bool { return false } +func (t *mockModel) GetNamespace() string { return "blee" } +func (t *mockModel) SetNamespace(string) {} +func (t *mockModel) ToggleToast() {} +func (t *mockModel) AddListener(model.TableListener) {} +func (t *mockModel) RemoveListener(model.TableListener) {} +func (t *mockModel) Watch(context.Context) {} +func (t *mockModel) Refresh(context.Context) {} +func (t *mockModel) Get(context.Context, string) (runtime.Object, error) { -func (t *testModel) SetInstance(string) {} -func (t *testModel) Empty() bool { return false } -func (t *testModel) HasMetrics() bool { return true } -func (t *testModel) Peek() render.TableData { return makeTableData() } -func (t *testModel) ClusterWide() bool { return false } -func (t *testModel) GetNamespace() string { return "blee" } -func (t *testModel) SetNamespace(string) {} -func (t *testModel) ToggleToast() {} -func (t *testModel) AddListener(model.TableListener) {} -func (t *testModel) Watch(context.Context) {} -func (t *testModel) Get(context.Context, string) (runtime.Object, error) { return nil, nil } -func (t *testModel) Delete(context.Context, string, bool, bool) error { +func (t *mockModel) Delete(context.Context, string, bool, bool) error { return nil } -func (t *testModel) Describe(context.Context, string) (string, error) { +func (t *mockModel) Describe(context.Context, string) (string, error) { return "", nil } -func (t *testModel) ToYAML(ctx context.Context, path string) (string, error) { +func (t *mockModel) ToYAML(ctx context.Context, path string) (string, error) { return "", nil } -func (t *testModel) InNamespace(string) bool { return true } -func (t *testModel) SetRefreshRate(time.Duration) {} +func (t *mockModel) InNamespace(string) bool { return true } +func (t *mockModel) SetRefreshRate(time.Duration) {} func makeTableData() render.TableData { return render.TableData{ diff --git a/internal/view/app.go b/internal/view/app.go index c7f83590..1d1dd83b 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -330,14 +330,15 @@ func (a *App) isValidNS(ns string) bool { if ns == client.AllNamespaces || ns == client.NamespaceAll { return true } + ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) defer cancel() _, err := a.Conn().DialOrDie().CoreV1().Namespaces().Get(ctx, ns, metav1.GetOptions{}) if err != nil { - log.Warn().Err(err).Msgf("Unable to find namespace %q", ns) + log.Warn().Err(err).Msgf("Validation failed for namespace: %q", ns) } - return err == nil + return true } func (a *App) switchCtx(name string, loadPods bool) error { @@ -502,6 +503,12 @@ func (a *App) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } +func (a *App) meowCmd(msg string) { + if err := a.inject(NewMeow(a, msg)); err != nil { + a.Flash().Err(err) + } +} + func (a *App) helpCmd(evt *tcell.EventKey) *tcell.EventKey { if a.CmdBuff().InCmdMode() { return evt @@ -537,13 +544,25 @@ func (a *App) aliasCmd(evt *tcell.EventKey) *tcell.EventKey { } func (a *App) gotoResource(cmd, path string, clearStack bool) error { - return a.command.run(cmd, path, clearStack) + err := a.command.run(cmd, path, clearStack) + if err == nil { + return err + } + + c := NewMeow(a, err.Error()) + _ = c.Init(context.Background()) + a.Content.Stack.Clear() + a.Content.Push(c) + + return nil } func (a *App) inject(c model.Component) error { ctx := context.WithValue(context.Background(), internal.KeyApp, a) if err := c.Init(ctx); err != nil { - return fmt.Errorf("component init failed for %q %v", c.Name(), err) + log.Error().Err(err).Msgf("component init failed for %q %v", c.Name(), err) + c = NewMeow(a, err.Error()) + _ = c.Init(ctx) } a.Content.Push(c) diff --git a/internal/view/browser.go b/internal/view/browser.go index c01cabcb..97c9800d 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -73,7 +73,6 @@ func (b *Browser) Init(ctx context.Context) error { if row == 0 && b.GetRowCount() > 0 { b.Select(1, 0) } - b.GetModel().AddListener(b) b.GetModel().SetRefreshRate(time.Duration(b.App().Config.K9s.GetRefreshRate()) * time.Second) b.CmdBuff().SetSuggestionFn(b.suggestFilter()) @@ -92,16 +91,13 @@ func (b *Browser) suggestFilter() model.SuggestionFunc { s = strings.ToLower(s) for _, h := range b.App().filterHistory.List() { - if h == s { + if s == h { continue } if strings.HasPrefix(h, s) { entries = append(entries, strings.Replace(h, s, "", 1)) } } - if len(entries) == 0 { - return nil - } return } } @@ -126,6 +122,7 @@ func (b *Browser) Start() { } b.Stop() + b.GetModel().AddListener(b) b.Table.Start() b.CmdBuff().AddListener(b) b.GetModel().Watch(b.prepareContext()) @@ -133,6 +130,7 @@ func (b *Browser) Start() { // Stop terminates browser updates. func (b *Browser) Stop() { + b.GetModel().RemoveListener(b) b.CmdBuff().RemoveListener(b) b.Table.Stop() if b.cancelFn != nil { @@ -146,14 +144,12 @@ func (b *Browser) BufferChanged(s string) {} // BufferActive indicates the buff activity changed. func (b *Browser) BufferActive(state bool, k model.BufferKind) { - if b.cancelFn != nil { - b.cancelFn() - } - b.GetModel().Watch(b.prepareContext()) - - if !state && b.GetRowCount() > 1 { - b.App().filterHistory.Push(b.CmdBuff().GetText()) - } + b.app.QueueUpdateDraw(func() { + b.Update(b.GetModel().Peek()) + if b.GetRowCount() > 1 { + b.App().filterHistory.Push(b.CmdBuff().GetText()) + } + }) } func (b *Browser) prepareContext() context.Context { @@ -239,11 +235,10 @@ func (b *Browser) resetCmd(evt *tcell.EventKey) *tcell.EventKey { return b.App().PrevCmd(evt) } + b.CmdBuff().Reset() if ui.IsLabelSelector(b.CmdBuff().GetText()) { - b.CmdBuff().Reset() b.Start() } - b.CmdBuff().Reset() b.Refresh() return nil diff --git a/internal/view/command.go b/internal/view/command.go index fdeeb571..3c4bd729 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -106,23 +106,24 @@ func (c *Command) xrayCmd(cmd string) error { return c.exec(cmd, "xrays", x, true) } -func (c *Command) checkAccess(gvr string) error { - m, err := dao.MetaAccess.MetaFor(client.NewGVR(gvr)) - if err != nil { - return err - } - ns := client.CleanseNamespace(c.app.Config.ActiveNamespace()) - if dao.IsK8sMeta(m) && c.app.ConOK() { - if _, e := c.app.factory.CanForResource(ns, gvr, client.MonitorAccess); e != nil { - return e - } - } - return nil -} +// BOZO!! +// func (c *Command) checkAccess(gvr string) error { +// m, err := dao.MetaAccess.MetaFor(client.NewGVR(gvr)) +// if err != nil { +// return err +// } +// ns := client.CleanseNamespace(c.app.Config.ActiveNamespace()) +// if dao.IsK8sMeta(m) && c.app.ConOK() { +// if _, e := c.app.factory.CanForResource(ns, gvr, client.MonitorAccess); e != nil { +// return e +// } +// } +// return nil +// } // Exec the Command by showing associated display. func (c *Command) run(cmd, path string, clearStack bool) error { - if c.specialCmd(cmd) { + if c.specialCmd(cmd, path) { return nil } cmds := strings.Split(cmd, " ") @@ -130,17 +131,16 @@ func (c *Command) run(cmd, path string, clearStack bool) error { if err != nil { return err } - if err := c.checkAccess(gvr); err != nil { - return err - } + //if err := c.checkAccess(gvr); err != nil { + // return err + //} switch cmds[0] { case "ctx", "context", "contexts": if len(cmds) == 2 { return useContext(c.app, cmds[1]) } - view := c.componentFor(gvr, path, v) - return c.exec(cmd, gvr, view, clearStack) + return c.exec(cmd, gvr, c.componentFor(gvr, path, v), clearStack) default: // checks if Command includes a namespace ns := c.app.Config.ActiveNamespace() @@ -171,14 +171,17 @@ func (c *Command) defaultCmd() error { if err := c.run(cmd, "", true); err != nil { log.Error().Err(err).Msgf("Saved command load failed. Loading default view") - return c.run("pod", "", true) + return c.run("meow", err.Error(), true) } return nil } -func (c *Command) specialCmd(cmd string) bool { +func (c *Command) specialCmd(cmd, path string) bool { cmds := strings.Split(cmd, " ") switch cmds[0] { + case "meow": + c.app.meowCmd(path) + return true case "q", "Q", "quit": c.app.BailOut() return true diff --git a/internal/view/details.go b/internal/view/details.go index 15726044..490a5ae1 100644 --- a/internal/view/details.go +++ b/internal/view/details.go @@ -23,7 +23,7 @@ type Details struct { actions ui.KeyActions app *App title, subject string - cmdBuff *model.CmdBuff + cmdBuff *model.FishBuff model *model.Text currentRegion, maxRegions int searchable bool @@ -37,7 +37,7 @@ func NewDetails(app *App, title, subject string, searchable bool) *Details { title: title, subject: subject, actions: make(ui.KeyActions), - cmdBuff: model.NewCmdBuff('/', model.FilterBuffer), + cmdBuff: model.NewFishBuff('/', model.FilterBuffer), model: model.NewText(), searchable: searchable, } diff --git a/internal/view/ds.go b/internal/view/ds.go index 081d9e22..68334e51 100644 --- a/internal/view/ds.go +++ b/internal/view/ds.go @@ -45,6 +45,7 @@ func (d *DaemonSet) showPods(app *App, model ui.Tabular, _, path string) { ds, err := res.GetInstance(path) if err != nil { d.App().Flash().Err(err) + return } showPodsFromSelector(app, path, ds.Spec.Selector) diff --git a/internal/view/meow.go b/internal/view/meow.go new file mode 100644 index 00000000..864287d2 --- /dev/null +++ b/internal/view/meow.go @@ -0,0 +1,135 @@ +package view + +import ( + "context" + "fmt" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tview" + "github.com/gdamore/tcell" + "strings" +) + +// Meow represents a bomb viewer +type Meow struct { + *tview.TextView + + actions ui.KeyActions + app *App + says string +} + +// NewMeow returns a details viewer. +func NewMeow(app *App, says string) *Meow { + return &Meow{ + TextView: tview.NewTextView(), + app: app, + actions: make(ui.KeyActions), + says: says, + } +} + +// Init initializes the viewer. +func (m *Meow) Init(_ context.Context) error { + m.SetBorder(true) + m.SetScrollable(true).SetWrap(true).SetRegions(true) + m.SetDynamicColors(true) + m.SetHighlightColor(tcell.ColorOrange) + m.SetTitleColor(tcell.ColorAqua) + m.SetInputCapture(m.keyboard) + m.SetBorderPadding(0, 0, 1, 1) + m.SetChangedFunc(func() { + m.app.Draw() + }) + m.updateTitle() + m.SetTextAlign(tview.AlignCenter) + + m.app.Styles.AddListener(m) + m.StylesChanged(m.app.Styles) + + m.bindKeys() + m.SetInputCapture(m.keyboard) + m.talk() + + return nil +} + +func (m *Meow) talk() { + says := m.says + if len(says) == 0 { + says = "Nothing to report here. Please move along..." + } + buff := make([]string, 0, len(cow)+3) + buff = append(buff, " "+strings.Repeat("─", len(says)+8)) + buff = append(buff, fmt.Sprintf("< [red::b]MEOW! %s [-::-] >", says)) + buff = append(buff, " "+strings.Repeat("─", len(says)+8)) + spacer := strings.Repeat(" ", len(says)/2-8) + for _, s := range cow { + buff = append(buff, spacer+s) + } + m.SetText(strings.Join(buff, "\n")) +} + +func (m *Meow) bindKeys() { + m.actions.Set(ui.KeyActions{ + tcell.KeyEscape: ui.NewKeyAction("Back", m.resetCmd, false), + }) +} + +func (m *Meow) keyboard(evt *tcell.EventKey) *tcell.EventKey { + if a, ok := m.actions[ui.AsKey(evt)]; ok { + return a.Action(evt) + } + + return evt +} + +// StylesChanged notifies the skin changes. +func (m *Meow) StylesChanged(s *config.Styles) { + m.SetBackgroundColor(m.app.Styles.BgColor()) + m.SetTextColor(m.app.Styles.FgColor()) + m.SetBorderFocusColor(m.app.Styles.Frame().Border.FocusColor.Color()) +} + +func (m *Meow) resetCmd(evt *tcell.EventKey) *tcell.EventKey { + return m.app.PrevCmd(evt) +} + +// Actions returns menu actions +func (m *Meow) Actions() ui.KeyActions { + return m.actions +} + +// Name returns the component name. +func (m *Meow) Name() string { return "cow" } + +// Start starts the view updater. +func (m *Meow) Start() {} + +// Stop terminates the updater. +func (m *Meow) Stop() { + m.app.Styles.RemoveListener(m) +} + +// Hints returns menu hints. +func (m *Meow) Hints() model.MenuHints { + return m.actions.Hints() +} + +// ExtraHints returns additional hints. +func (m *Meow) ExtraHints() map[string]string { + return nil +} + +func (m *Meow) updateTitle() { + m.SetTitle(" Meow! ") +} + +var cow = []string{ + `\ ^__^ `, + ` \ (oo)\_______ `, + ` (__)\ )\/\`, + ` ||----w | `, + ` || || `, +} diff --git a/internal/view/pulse.go b/internal/view/pulse.go index 147ea186..7a899f8c 100644 --- a/internal/view/pulse.go +++ b/internal/view/pulse.go @@ -16,6 +16,7 @@ import ( "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" ) // Grapheable represents a graphic component. @@ -285,6 +286,10 @@ func (p *Pulse) Actions() ui.KeyActions { // Hints returns the view hints. func (p *Pulse) Hints() model.MenuHints { + log.Debug().Msgf("PULSES -- HINTS!!") + for k := range p.actions { + log.Debug().Msgf("KEY %v", p.actions[k].Description) + } return p.actions.Hints() } diff --git a/internal/view/table_int_test.go b/internal/view/table_int_test.go index 737c5b88..887c2c45 100644 --- a/internal/view/table_int_test.go +++ b/internal/view/table_int_test.go @@ -64,7 +64,7 @@ func TestTableNew(t *testing.T) { func TestTableViewFilter(t *testing.T) { v := NewTable(client.NewGVR("test")) v.Init(makeContext()) - v.SetModel(&testTableModel{}) + v.SetModel(&mockTableModel{}) v.Refresh() v.CmdBuff().SetActive(true) v.CmdBuff().SetText("blee") @@ -75,7 +75,7 @@ func TestTableViewFilter(t *testing.T) { func TestTableViewSort(t *testing.T) { v := NewTable(client.NewGVR("test")) v.Init(makeContext()) - v.SetModel(&testTableModel{}) + v.SetModel(&mockTableModel{}) v.SortColCmd("NAME", true)(nil) assert.Equal(t, 3, v.GetRowCount()) assert.Equal(t, "blee", v.GetCell(1, 0).Text) @@ -88,35 +88,37 @@ func TestTableViewSort(t *testing.T) { // ---------------------------------------------------------------------------- // Helpers... -type testTableModel struct{} +type mockTableModel struct{} -var _ ui.Tabular = (*testTableModel)(nil) +var _ ui.Tabular = (*mockTableModel)(nil) -func (t *testTableModel) SetInstance(string) {} -func (t *testTableModel) Empty() bool { return false } -func (t *testTableModel) HasMetrics() bool { return true } -func (t *testTableModel) Peek() render.TableData { return makeTableData() } -func (t *testTableModel) ClusterWide() bool { return false } -func (t *testTableModel) GetNamespace() string { return "blee" } -func (t *testTableModel) SetNamespace(string) {} -func (t *testTableModel) ToggleToast() {} -func (t *testTableModel) AddListener(model.TableListener) {} -func (t *testTableModel) Watch(context.Context) {} -func (t *testTableModel) Get(context.Context, string) (runtime.Object, error) { +func (t *mockTableModel) SetInstance(string) {} +func (t *mockTableModel) Empty() bool { return false } +func (t *mockTableModel) HasMetrics() bool { return true } +func (t *mockTableModel) Peek() render.TableData { return makeTableData() } +func (t *mockTableModel) Refresh(context.Context) {} +func (t *mockTableModel) ClusterWide() bool { return false } +func (t *mockTableModel) GetNamespace() string { return "blee" } +func (t *mockTableModel) SetNamespace(string) {} +func (t *mockTableModel) ToggleToast() {} +func (t *mockTableModel) AddListener(model.TableListener) {} +func (t *mockTableModel) RemoveListener(model.TableListener) {} +func (t *mockTableModel) Watch(context.Context) {} +func (t *mockTableModel) Get(context.Context, string) (runtime.Object, error) { return nil, nil } -func (t *testTableModel) Delete(context.Context, string, bool, bool) error { +func (t *mockTableModel) Delete(context.Context, string, bool, bool) error { return nil } -func (t *testTableModel) Describe(context.Context, string) (string, error) { +func (t *mockTableModel) Describe(context.Context, string) (string, error) { return "", nil } -func (t *testTableModel) ToYAML(ctx context.Context, path string) (string, error) { +func (t *mockTableModel) ToYAML(ctx context.Context, path string) (string, error) { return "", nil } -func (t *testTableModel) InNamespace(string) bool { return true } -func (t *testTableModel) SetRefreshRate(time.Duration) {} +func (t *mockTableModel) InNamespace(string) bool { return true } +func (t *mockTableModel) SetRefreshRate(time.Duration) {} func makeTableData() render.TableData { t := render.NewTableData() diff --git a/internal/watch/factory.go b/internal/watch/factory.go index e2d6cc2d..6acaca42 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -106,10 +106,6 @@ func (f *Factory) waitForCacheSync(ns string) { ns = client.AllNamespaces } - if f.isClusterWide() { - ns = client.AllNamespaces - } - f.mx.RLock() defer f.mx.RUnlock() fac, ok := f.factories[ns] @@ -156,8 +152,8 @@ func (f *Factory) SetActiveNS(ns string) { func (f *Factory) isClusterWide() bool { f.mx.RLock() defer f.mx.RUnlock() - _, ok := f.factories[client.AllNamespaces] + return ok }