From 4c2c4793dc49f0e1ecfc1d8420d244a59f81c32b Mon Sep 17 00:00:00 2001 From: derailed Date: Mon, 2 Dec 2019 16:11:39 -0700 Subject: [PATCH] checkpoint --- go.mod | 5 +- go.sum | 6 + internal/config/alias.go | 15 +- internal/config/config.go | 3 +- internal/config/style.go | 2 +- internal/dao/context.go | 122 +++ internal/dao/describe.go | 37 + internal/dao/dp.go | 80 ++ internal/dao/ds.go | 122 +++ internal/dao/gvr.go | 124 +++ internal/dao/log_options.go | 104 +++ internal/dao/logger.go | 1 + internal/dao/pod.go | 196 +++++ internal/dao/reconcile.go | 100 +++ internal/dao/registry.go | 213 +++++ internal/dao/resource.go | 32 + internal/dao/screen_dump.go | 21 + internal/dao/sts.go | 80 ++ internal/dao/types.go | 64 ++ internal/k8s/api.go | 5 +- internal/k8s/cluster_role.go | 2 + internal/k8s/cluster_roleb.go | 2 + internal/k8s/context.go | 1 - internal/k8s/dp.go | 2 + internal/k8s/ds.go | 115 +-- internal/k8s/gvr.go | 4 +- internal/k8s/gvr_test.go | 4 +- internal/k8s/mapper.go | 1 - internal/k8s/{no.go => node.go} | 0 internal/k8s/ns.go | 2 + internal/k8s/pod.go | 3 + internal/k8s/resource.go | 4 +- internal/k8s/role.go | 2 + internal/k8s/role_binding.go | 2 + internal/k8s/svc.go | 2 + internal/keys.go | 14 + internal/model/{co.go => container.go} | 75 +- internal/model/context.go | 49 ++ internal/model/generic.go | 130 +++ internal/model/{no.go => node.go} | 28 +- internal/model/{po.go => pod.go} | 58 +- internal/model/registry.go | 102 ++- internal/model/resource.go | 32 +- internal/model/screen_dump.go | 81 ++ internal/model/types.go | 78 ++ internal/render/alias.go | 64 ++ internal/render/bench.go | 158 ++++ internal/render/cj.go | 2 +- internal/render/cm.go | 2 +- internal/render/colorer_test.go | 280 ++++++ internal/render/{co.go => container.go} | 60 +- internal/render/context.go | 76 +- internal/render/cr.go | 2 +- internal/render/crb.go | 2 +- internal/render/crd.go | 92 +- internal/render/delta.go | 21 +- internal/render/dp.go | 55 +- internal/render/dp_test.go | 2 +- internal/render/ds.go | 31 +- internal/render/ep.go | 2 +- internal/render/ev.go | 21 +- internal/render/event.go | 148 +++- internal/render/event_test.go | 2 +- internal/render/forward.go | 110 +++ internal/render/generic.go | 99 +++ internal/render/helpers.go | 4 +- internal/render/hpa.go | 2 +- internal/render/ing.go | 2 +- internal/render/job.go | 2 +- internal/render/{no.go => node.go} | 2 +- internal/render/{no_test.go => node_test.go} | 0 internal/render/np.go | 2 +- internal/render/ns.go | 21 +- internal/render/pdb.go | 22 +- internal/render/{po.go => pod.go} | 54 +- internal/render/{po_test.go => pod_test.go} | 0 internal/render/policy.go | 49 ++ internal/render/pv.go | 23 +- internal/render/pvc.go | 23 +- internal/render/rb.go | 2 +- internal/render/rbac.go | 33 + internal/render/ro.go | 2 +- internal/render/row.go | 35 +- internal/render/rs.go | 22 +- internal/render/sa.go | 2 +- internal/render/screen_dump.go | 61 ++ internal/render/secret.go | 2 +- internal/render/sts.go | 81 ++ internal/render/subject.go | 30 + internal/render/svc.go | 2 +- internal/render/table.go | 16 + internal/render/types.go | 35 + internal/resource/base.go | 7 +- internal/resource/custom.go | 1 + internal/resource/custom_test.go | 21 +- internal/resource/ds.go | 224 ++--- internal/resource/list.go | 89 +- internal/resource/{no.go => node.go} | 0 .../{no_int_test.go => node_int_test.go} | 0 .../resource/{no_test.go => node_test.go} | 0 internal/resource/pod.go | 87 +- internal/resource/pod_test.go | 24 +- internal/resource/types.go | 17 +- internal/ui/colorer.go | 67 +- internal/ui/colorer_test.go | 49 +- internal/ui/config.go | 13 +- internal/ui/dialog/confirm.go | 2 +- internal/ui/padding.go | 16 +- internal/ui/padding_test.go | 11 +- internal/ui/select_table.go | 29 +- internal/ui/table.go | 130 ++- internal/ui/table_helper.go | 32 +- internal/ui/table_test.go | 4 +- internal/view/alias.go | 39 +- internal/view/alias_test.go | 2 +- internal/view/app.go | 112 ++- internal/view/bench.go | 130 +-- internal/view/colorer.go | 256 ------ internal/view/colorer_test.go | 289 ------ internal/view/command.go | 41 +- internal/view/container.go | 18 +- internal/view/context.go | 48 +- internal/view/details.go | 6 +- internal/view/dp.go | 25 +- internal/view/ds.go | 28 +- internal/view/exec.go | 3 + internal/view/generic.go | 512 +++++++++++ internal/view/help.go | 8 +- internal/view/help_test.go | 4 +- internal/view/log.go | 56 +- internal/view/logs_extender.go | 3 +- internal/view/{no.go => node.go} | 3 +- internal/view/ns.go | 83 +- internal/view/page_stack.go | 2 + internal/view/picker.go | 8 +- internal/view/pod.go | 9 +- internal/view/policy.go | 180 ++-- internal/view/port_forward.go | 109 +-- internal/view/rbac.go | 194 ++-- internal/view/rbac_int_test.go | 39 +- internal/view/rbac_test.go | 2 +- internal/view/registrar.go | 211 ++--- internal/view/resource.go | 76 +- internal/view/restart_extender.go | 11 +- internal/view/rs.go | 90 +- internal/view/scale_extender.go | 24 +- internal/view/screen_dump.go | 172 +--- internal/view/screen_dump_test.go | 2 +- internal/view/sts.go | 40 +- internal/view/subject.go | 198 +++-- internal/view/table.go | 15 +- internal/view/table_helper.go | 5 +- internal/view/table_int_test.go | 13 +- internal/view/types.go | 27 +- internal/views/mock_connection.go | 825 ------------------ internal/watch/factory.go | 216 ++--- 156 files changed, 5506 insertions(+), 3334 deletions(-) create mode 100644 internal/dao/context.go create mode 100644 internal/dao/describe.go create mode 100644 internal/dao/dp.go create mode 100644 internal/dao/ds.go create mode 100644 internal/dao/gvr.go create mode 100644 internal/dao/log_options.go create mode 100644 internal/dao/logger.go create mode 100644 internal/dao/pod.go create mode 100644 internal/dao/reconcile.go create mode 100644 internal/dao/registry.go create mode 100644 internal/dao/resource.go create mode 100644 internal/dao/screen_dump.go create mode 100644 internal/dao/sts.go create mode 100644 internal/dao/types.go rename internal/k8s/{no.go => node.go} (100%) create mode 100644 internal/keys.go rename internal/model/{co.go => container.go} (69%) create mode 100644 internal/model/context.go create mode 100644 internal/model/generic.go rename internal/model/{no.go => node.go} (86%) rename internal/model/{po.go => pod.go} (59%) create mode 100644 internal/model/screen_dump.go create mode 100644 internal/render/alias.go create mode 100644 internal/render/bench.go create mode 100644 internal/render/colorer_test.go rename internal/render/{co.go => container.go} (81%) create mode 100644 internal/render/forward.go create mode 100644 internal/render/generic.go rename internal/render/{no.go => node.go} (99%) rename internal/render/{no_test.go => node_test.go} (100%) rename internal/render/{po.go => pod.go} (87%) rename internal/render/{po_test.go => pod_test.go} (100%) create mode 100644 internal/render/policy.go create mode 100644 internal/render/rbac.go create mode 100644 internal/render/screen_dump.go create mode 100644 internal/render/sts.go create mode 100644 internal/render/subject.go create mode 100644 internal/render/table.go create mode 100644 internal/render/types.go rename internal/resource/{no.go => node.go} (100%) rename internal/resource/{no_int_test.go => node_int_test.go} (100%) rename internal/resource/{no_test.go => node_test.go} (100%) delete mode 100644 internal/view/colorer.go delete mode 100644 internal/view/colorer_test.go create mode 100644 internal/view/generic.go rename internal/view/{no.go => node.go} (94%) delete mode 100644 internal/views/mock_connection.go diff --git a/go.mod b/go.mod index 3913d071..6a9febff 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/derailed/k9s go 1.13 +replace github.com/derailed/tview => /Users/fernand/go_wk/derailed/src/github.com/derailed/tview + replace ( k8s.io/api => k8s.io/api v0.0.0-20190918155943-95b840bb6a1f k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783 @@ -36,6 +38,7 @@ require ( github.com/gdamore/tcell v1.3.0 github.com/ghodss/yaml v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect + github.com/golang/mock v1.2.0 github.com/google/btree v1.0.0 // indirect github.com/googleapis/gnostic v0.2.0 // indirect github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect @@ -44,7 +47,7 @@ require ( github.com/mattn/go-runewidth v0.0.5 github.com/petergtz/pegomock v2.6.0+incompatible github.com/rakyll/hey v0.1.2 - github.com/rs/zerolog v1.14.3 + github.com/rs/zerolog v1.17.2 github.com/sahilm/fuzzy v0.1.0 github.com/spf13/cobra v0.0.5 github.com/stretchr/testify v1.3.0 diff --git a/go.sum b/go.sum index f5945ef9..0b278da0 100644 --- a/go.sum +++ b/go.sum @@ -176,6 +176,7 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -346,6 +347,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.14.3 h1:4EGfSkR2hJDB0s3oFfrlPqjU1e4WLncergLil3nEKW0= github.com/rs/zerolog v1.14.3/go.mod h1:3WXPzbXEEliJ+a6UFE4vhIxV8qR1EML6ngzP9ug4eYg= +github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo= +github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I= github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -433,6 +436,7 @@ golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68= golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -485,6 +489,8 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= diff --git a/internal/config/alias.go b/internal/config/alias.go index 281174c8..ba071a0f 100644 --- a/internal/config/alias.go +++ b/internal/config/alias.go @@ -14,6 +14,9 @@ var K9sAlias = filepath.Join(K9sHome, "alias.yml") // Alias tracks shortname to GVR mappings. type Alias map[string]string +// ShortNames represents a collection of shortnames for aliases. +type ShortNames map[string][]string + // Aliases represents a collection of aliases. type Aliases struct { Alias Alias `yaml:"alias"` @@ -88,13 +91,13 @@ func (a Aliases) Get(k string) (string, bool) { } // Define declares a new alias. -func (a Aliases) Define(command, alias string) { - if _, ok := a.Alias[alias]; ok { - // Don't override aliases. Take order of alias registration as precedence. - return +func (a Aliases) Define(gvr string, aliases ...string) { + for _, alias := range aliases { + if _, ok := a.Alias[alias]; ok { + continue + } + a.Alias[alias] = gvr } - - a.Alias[alias] = command } // LoadAliases loads alias from a given file. diff --git a/internal/config/config.go b/internal/config/config.go index 1818d86d..9772872c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,7 +11,6 @@ import ( "path/filepath" "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" "github.com/rs/zerolog/log" "gopkg.in/yaml.v2" v1 "k8s.io/api/core/v1" @@ -123,7 +122,7 @@ func (c *Config) ActiveNamespace() string { return cl.Namespace.Active } } - return resource.DefaultNamespace + return "" } // FavNamespaces returns fav namespaces in the current cluster. diff --git a/internal/config/style.go b/internal/config/style.go index 7401694b..1edf125c 100644 --- a/internal/config/style.go +++ b/internal/config/style.go @@ -216,7 +216,7 @@ func newTable() Table { FgColor: "aqua", BgColor: "black", CursorColor: "aqua", - MarkColor: "khaki", + MarkColor: "violet", Header: newTableHeader(), } } diff --git a/internal/dao/context.go b/internal/dao/context.go new file mode 100644 index 00000000..547b9f32 --- /dev/null +++ b/internal/dao/context.go @@ -0,0 +1,122 @@ +package dao + +import ( + "fmt" + + "github.com/derailed/k9s/internal/k8s" + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd/api" +) + +type Context struct { + Resource +} + +var _ Accessor = &Context{} +var _ Switchable = &Context{} + +func (c *Context) config() *k8s.Config { + return c.Factory.Client().Config() +} + +// Get a Context. +func (c *Context) Get(_, n string) (runtime.Object, error) { + ctx, err := c.config().GetContext(n) + if err != nil { + return nil, err + } + return &NamedContext{Name: n, Context: ctx}, nil +} + +// List all Contexts on the current cluster. +func (c *Context) List(string, metav1.ListOptions) ([]runtime.Object, error) { + ctxs, err := c.config().Contexts() + if err != nil { + return nil, err + } + cc := make([]runtime.Object, 0, len(ctxs)) + for k, v := range ctxs { + cc = append(cc, NewNamedContext(c.config(), k, v)) + } + + return cc, nil +} + +// Delete a Context. +func (c *Context) Delete(ns, n string, cascade, force bool) error { + ctx, err := c.config().CurrentContextName() + if err != nil { + return err + } + if ctx == n { + return fmt.Errorf("trying to delete your current context %s", n) + } + + return c.config().DelContext(n) +} + +// MustCurrentContextName return the active context name. +func (c *Context) MustCurrentContextName() string { + cl, err := c.config().CurrentContextName() + if err != nil { + log.Fatal().Err(err).Msg("Fetching current context") + } + return cl +} + +// Switch to another context. +func (c *Context) Switch(ctx string) error { + c.Factory.Client().SwitchContextOrDie(ctx) + return nil +} + +// KubeUpdate modifies kubeconfig default context. +func (c *Context) KubeUpdate(n string) error { + config, err := c.config().RawConfig() + if err != nil { + return err + } + if err := c.Switch(n); err != nil { + return err + } + return clientcmd.ModifyConfig( + clientcmd.NewDefaultPathOptions(), config, true, + ) +} + +// ---------------------------------------------------------------------------- + +// NamedContext represents a named cluster context. +type NamedContext struct { + Name string + Context *api.Context + config *k8s.Config +} + +// NewNamedContext returns a new named context. +func NewNamedContext(c *k8s.Config, n string, ctx *api.Context) *NamedContext { + return &NamedContext{Name: n, Context: ctx, config: c} +} + +// MustCurrentContextName return the active context name. +func (c *NamedContext) MustCurrentContextName() string { + cl, err := c.config.CurrentContextName() + if err != nil { + log.Fatal().Err(err).Msg("Fetching current context") + } + return cl +} + +// GetObjectKind returns a schema object. +func (c *NamedContext) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (c *NamedContext) DeepCopyObject() runtime.Object { + return c +} diff --git a/internal/dao/describe.go b/internal/dao/describe.go new file mode 100644 index 00000000..ccba22b9 --- /dev/null +++ b/internal/dao/describe.go @@ -0,0 +1,37 @@ +package dao + +import ( + "github.com/derailed/k9s/internal/k8s" + "github.com/rs/zerolog/log" + "k8s.io/kubectl/pkg/describe" + "k8s.io/kubectl/pkg/describe/versioned" +) + +func Describe(c k8s.Connection, gvr GVR, ns, n string) (string, error) { + mapper := k8s.RestMapper{Connection: c} + m, err := mapper.ToRESTMapper() + if err != nil { + log.Error().Err(err).Msgf("No REST mapper for resource %s", gvr) + return "", err + } + + GVR := k8s.GVR(gvr) + gvk, err := m.KindFor(GVR.AsGVR()) + if err != nil { + log.Error().Err(err).Msgf("No GVK for resource %s", gvr) + return "", err + } + + mapping, err := mapper.ResourceFor(GVR.ResName(), gvk.Kind) + if err != nil { + log.Error().Err(err).Msgf("Unable to find mapper for %s %s", gvr, n) + return "", err + } + d, err := versioned.Describer(c.Config().Flags(), mapping) + if err != nil { + log.Error().Err(err).Msgf("Unable to find describer for %#v", mapping) + return "", err + } + + return d.Describe(ns, n, describe.DescriberSettings{ShowEvents: true}) +} diff --git a/internal/dao/dp.go b/internal/dao/dp.go new file mode 100644 index 00000000..3f30b866 --- /dev/null +++ b/internal/dao/dp.go @@ -0,0 +1,80 @@ +package dao + +import ( + "context" + "errors" + "fmt" + + "github.com/rs/zerolog/log" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/kubectl/pkg/polymorphichelpers" +) + +type Deployment struct { + Resource +} + +var _ Accessor = &Deployment{} +var _ Loggable = &Deployment{} +var _ Restartable = &Deployment{} +var _ Scalable = &Deployment{} + +// Scale a Deployment. +func (d *Deployment) Scale(ns, n string, replicas int32) error { + scale, err := d.Client().DialOrDie().AppsV1().Deployments(ns).GetScale(n, metav1.GetOptions{}) + if err != nil { + return err + } + scale.Spec.Replicas = replicas + _, err = d.Client().DialOrDie().AppsV1().Deployments(ns).UpdateScale(n, scale) + + return err +} + +// Restart a Deployment rollout. +func (d *Deployment) Restart(ns, n string) error { + o, err := d.Get(ns, string(d.gvr), n, labels.Everything()) + if err != nil { + return err + } + + var ds appsv1.Deployment + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) + if err != nil { + return err + } + + update, err := polymorphichelpers.ObjectRestarterFn(&ds) + if err != nil { + return err + } + + _, err = d.Client().DialOrDie().AppsV1().Deployments(ns).Patch(ds.Name, types.StrategicMergePatchType, update) + return err +} + +// Logs tail logs for all pods represented by this Deployment. +func (d *Deployment) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { + log.Debug().Msgf("Tailing Deployment %q -- %q", opts.Namespace, opts.Name) + o, err := d.Get(opts.Namespace, string(d.gvr), opts.Name, labels.Everything()) + if err != nil { + return err + } + + var dp appsv1.Deployment + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp) + if err != nil { + return errors.New("expecting Deployment resource") + } + + if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 { + return fmt.Errorf("No valid selector found on Deployment %s", opts.FQN()) + } + + return podLogs(ctx, c, dp.Spec.Selector.MatchLabels, opts) +} diff --git a/internal/dao/ds.go b/internal/dao/ds.go new file mode 100644 index 00000000..d031f1da --- /dev/null +++ b/internal/dao/ds.go @@ -0,0 +1,122 @@ +package dao + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/watch" + "github.com/rs/zerolog/log" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/kubectl/pkg/polymorphichelpers" +) + +type DaemonSet struct { + Resource +} + +var _ Accessor = &DaemonSet{} +var _ Loggable = &DaemonSet{} +var _ Restartable = &DaemonSet{} + +// Restart a DaemonSet rollout. +func (d *DaemonSet) Restart(ns, n string) error { + o, err := d.Get(ns, string(d.gvr), n, labels.Everything()) + if err != nil { + return err + } + + var ds appsv1.DaemonSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) + if err != nil { + return err + } + + update, err := polymorphichelpers.ObjectRestarterFn(&ds) + if err != nil { + return err + } + + _, err = d.Client().DialOrDie().AppsV1().DaemonSets(ns).Patch(ds.Name, types.StrategicMergePatchType, update) + return err +} + +// Logs tail logs for all pods represented by this DaemonSet. +func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { + log.Debug().Msgf("Tailing DaemonSet %q -- %q", opts.Namespace, opts.Name) + o, err := d.Get(opts.Namespace, "apps/v1/daemonsets", opts.Name, labels.Everything()) + if err != nil { + return err + } + + var ds appsv1.DaemonSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) + if err != nil { + return errors.New("expecting daemonset resource") + } + + if ds.Spec.Selector == nil || len(ds.Spec.Selector.MatchLabels) == 0 { + return fmt.Errorf("No valid selector found on daemonset %s", opts.FQN()) + } + + return podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts) +} + +func podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts LogOptions) error { + f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) + if !ok { + return errors.New("expecting a context factory") + } + ls, err := metav1.ParseToLabelSelector(toSelector(sel)) + if err != nil { + return err + } + lsel, err := metav1.LabelSelectorAsSelector(ls) + if err != nil { + return err + } + + oo, err := f.List(opts.Namespace, "v1/pods", lsel) + if err != nil { + return err + } + + if len(oo) > 1 { + opts.MultiPods = true + } + + po := Pod{} + for _, o := range oo { + var pod v1.Pod + err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod) + if err != nil { + return err + } + if pod.Status.Phase == v1.PodRunning { + opts.Namespace, opts.Name = pod.Namespace, pod.Name + if err := po.TailLogs(ctx, c, opts); err != nil { + return err + } + } + } + return nil +} + +// Helpers... + +func toSelector(m map[string]string) string { + s := make([]string, 0, len(m)) + for k, v := range m { + s = append(s, k+"="+v) + } + + return strings.Join(s, ",") +} diff --git a/internal/dao/gvr.go b/internal/dao/gvr.go new file mode 100644 index 00000000..732db08f --- /dev/null +++ b/internal/dao/gvr.go @@ -0,0 +1,124 @@ +package dao + +import ( + "fmt" + "path" + "strings" + + "github.com/rs/zerolog/log" + "k8s.io/apimachinery/pkg/runtime/schema" + "vbom.ml/util/sortorder" +) + +// GVR represents a kubernetes resource schema as a string. +// Format is group/version/resources +type GVR string + +// NewGVR builds a new gvr from a group, version, resource. +func NewGVR(g, v, r string) GVR { + return GVR(path.Join(g, v, r)) +} + +// FromGVAndR builds a gvr from a group/version and resource. +func FromGVAndR(gv, r string) GVR { + return GVR(path.Join(gv, r)) +} + +// ResName returns a resource . separated descriptor in the shape of kind.version.group. +func (g GVR) ResName() string { + return g.ToR() + "." + g.ToV() + "." + g.ToG() +} + +// AsGV returns the group version scheme representation. +func (g GVR) AsGV() schema.GroupVersion { + return schema.GroupVersion{ + Group: g.ToG(), + Version: g.ToV(), + } +} + +// AsGVR returns a a full schema representation. +func (g GVR) AsGVR() schema.GroupVersionResource { + return schema.GroupVersionResource{ + Group: g.ToG(), + Version: g.ToV(), + Resource: g.ToR(), + } +} + +// ToV returns the resource version. +func (g GVR) ToV() string { + tokens := strings.Split(string(g), "/") + if len(tokens) < 2 { + return "" + } + 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 "" + } +} + +type GVRs []GVR + +func (g GVRs) Len() int { + return len(g) +} + +func (g GVRs) Swap(i, j int) { + g[i], g[j] = g[j], g[i] +} + +func (g GVRs) Less(i, j int) bool { + g1, g2 := g[i].ToG(), g[j].ToG() + + return sortorder.NaturalLess(g1, g2) +} + +// Helper... + +// Can determines the available actions for a given resource. +func Can(verbs []string, v string) bool { + for _, verb := range verbs { + candidates, err := mapVerb(v) + if err != nil { + log.Error().Err(err).Msgf("verb mapping failed") + return false + } + for _, c := range candidates { + if verb == c { + return true + } + } + } + + return false +} + +func mapVerb(v string) ([]string, error) { + switch v { + case "describe": + return []string{"get"}, nil + case "view": + return []string{"get", "list"}, nil + case "delete": + return []string{"delete"}, nil + case "edit": + return []string{"patch", "update"}, nil + default: + return []string{}, fmt.Errorf("no standard verb for %q", v) + } +} diff --git a/internal/dao/log_options.go b/internal/dao/log_options.go new file mode 100644 index 00000000..0eb5fd71 --- /dev/null +++ b/internal/dao/log_options.go @@ -0,0 +1,104 @@ +package dao + +import ( + "path" + "strings" + + "github.com/derailed/k9s/internal/color" + "github.com/derailed/tview" + runewidth "github.com/mattn/go-runewidth" +) + +type ( + // Fqn uniquely describes a container + Fqn struct { + Namespace, Name, Container string + } + + // LogOptions represent logger options. + LogOptions struct { + Fqn + + Lines int64 + Color color.Paint + Previous bool + SingleContainer bool + MultiPods bool + } +) + +// HasContainer checks if a container is present. +func (o LogOptions) HasContainer() bool { + return o.Container != "" +} + +// FQN returns resource fully qualified name. +func (o LogOptions) FQN() string { + return FQN(o.Namespace, o.Name) +} + +// Path returns resource descriptor path. +func (o LogOptions) Path() string { + return o.FQN() + ":" + o.Container +} + +// FixedSizeName returns a normalize fixed size pod name if possible. +func (o LogOptions) FixedSizeName() string { + tokens := strings.Split(o.Name, "-") + if len(tokens) < 3 { + return o.Name + } + var s []string + for i := 0; i < len(tokens)-1; i++ { + s = append(s, tokens[i]) + } + return Truncate(strings.Join(s, "-"), 15) + "-" + tokens[len(tokens)-1] +} + +func colorize(c color.Paint, txt string) string { + if c == 0 { + return "" + } + + return color.Colorize(txt, c) +} + +// DecorateLog add a log header to display po/co information along with the log message. +func (o LogOptions) DecorateLog(msg string) string { + if msg == "" { + return msg + } + + if o.MultiPods { + return colorize(o.Color, o.Name+":"+o.Container+" ") + msg + } + + if !o.SingleContainer { + return colorize(o.Color, o.Container+" ") + msg + } + + return msg +} + +// Helpers... + +// BOZO!! Consolidate!! +// Truncate a string to the given l and suffix ellipsis if needed. +func Truncate(str string, width int) string { + return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis)) +} + +// Namespaced return a namesapace and a name. +func Namespaced(n string) (string, string) { + ns, po := path.Split(n) + + return strings.Trim(ns, "/"), po +} + +// FQN returns a fully qualified resource name. +func FQN(ns, n string) string { + if ns == "" { + return n + } + return ns + "/" + n +} diff --git a/internal/dao/logger.go b/internal/dao/logger.go new file mode 100644 index 00000000..07a0cc0f --- /dev/null +++ b/internal/dao/logger.go @@ -0,0 +1 @@ +package dao diff --git a/internal/dao/pod.go b/internal/dao/pod.go new file mode 100644 index 00000000..d4548e9b --- /dev/null +++ b/internal/dao/pod.go @@ -0,0 +1,196 @@ +package dao + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "sync/atomic" + "time" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/color" + "github.com/derailed/k9s/internal/watch" + "github.com/rs/zerolog/log" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + restclient "k8s.io/client-go/rest" +) + +const defaultTimeout = 1 * time.Second + +type Logger interface { + Logs(ns, n string, opts *v1.PodLogOptions) *restclient.Request +} + +type Pod struct { + Resource +} + +var _ Accessor = &Pod{} + +// Logs fetch container logs for a given pod and container. +func (p *Pod) Logs(ns, n string, opts *v1.PodLogOptions) *restclient.Request { + return p.Client().DialOrDie().CoreV1().Pods(ns).GetLogs(n, opts) +} + +// Containers returns all container names on pod +func (p *Pod) Containers(ns, n string, includeInit bool) ([]string, error) { + o, err := p.Get(ns, "v1/pod", n, labels.Everything()) + if err != nil { + return nil, err + } + + var pod v1.Pod + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod) + if err != nil { + return nil, err + } + + cc := []string{} + for _, c := range pod.Spec.Containers { + cc = append(cc, c.Name) + } + + if includeInit { + for _, c := range pod.Spec.InitContainers { + cc = append(cc, c.Name) + } + } + + return cc, nil +} + +// Logs tails a given container logs +func (p *Pod) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { + if !opts.HasContainer() { + return p.logs(ctx, c, opts) + } + return tailLogs(ctx, p, c, opts) +} + +// PodLogs tail logs for all containers in a running Pod. +func (p *Pod) logs(ctx context.Context, c chan<- string, opts LogOptions) error { + fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) + if !ok { + return errors.New("Expecting an informer") + } + ns, n := Namespaced(opts.FQN()) + o, err := fac.Get(ns, "v1/pods", n, labels.Everything()) + if err != nil { + return err + } + + var po v1.Pod + if runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil { + return err + } + opts.Color = asColor(po.Name) + if len(po.Spec.InitContainers)+len(po.Spec.Containers) == 1 { + opts.SingleContainer = true + } + + for _, co := range po.Spec.InitContainers { + opts.Container = co.Name + if err := p.TailLogs(ctx, c, opts); err != nil { + return err + } + } + rcos := loggableContainers(po.Status) + for _, co := range po.Spec.Containers { + if in(rcos, co.Name) { + opts.Container = co.Name + if err := p.TailLogs(ctx, c, opts); err != nil { + log.Error().Err(err).Msgf("Getting logs for %s failed", co.Name) + return err + } + } + } + + return nil +} + +func tailLogs(ctx context.Context, logger Logger, c chan<- string, opts LogOptions) error { + log.Debug().Msgf("Tailing logs for %q -- %q -- %q", opts.Namespace, opts.Name, opts.Container) + o := v1.PodLogOptions{ + Container: opts.Container, + Follow: true, + TailLines: &opts.Lines, + Previous: opts.Previous, + } + req := logger.Logs(opts.Namespace, opts.Name, &o) + ctxt, cancelFunc := context.WithCancel(ctx) + req.Context(ctxt) + + var blocked int32 = 1 + go logsTimeout(cancelFunc, &blocked) + + // This call will block if nothing is in the stream!! + stream, err := req.Stream() + atomic.StoreInt32(&blocked, 0) + if err != nil { + log.Error().Err(err).Msgf("Log stream failed for `%s", opts.Path()) + return fmt.Errorf("Unable to obtain log stream for %s", opts.Path()) + } + go readLogs(ctx, stream, c, opts) + + return nil +} + +func logsTimeout(cancel context.CancelFunc, blocked *int32) { + <-time.After(defaultTimeout) + if atomic.LoadInt32(blocked) == 1 { + log.Debug().Msg("Timed out reading the log stream") + cancel() + } +} + +func readLogs(ctx context.Context, stream io.ReadCloser, c chan<- string, opts LogOptions) { + defer func() { + log.Debug().Msgf(">>> Closing stream `%s", opts.Path()) + if err := stream.Close(); err != nil { + log.Error().Err(err).Msg("Cloing stream") + } + }() + + scanner := bufio.NewScanner(stream) + for scanner.Scan() { + select { + case <-ctx.Done(): + return + default: + c <- opts.DecorateLog(scanner.Text()) + } + } +} + +// Helpers... + +func loggableContainers(s v1.PodStatus) []string { + var rcos []string + for _, c := range s.ContainerStatuses { + rcos = append(rcos, c.Name) + } + return rcos +} + +func asColor(n string) color.Paint { + var sum int + for _, r := range n { + sum += int(r) + } + return color.Paint(30 + 2 + sum%6) +} + +// Check if string is in a string list. +func in(ll []string, s string) bool { + for _, l := range ll { + if l == s { + return true + } + } + return false +} diff --git a/internal/dao/reconcile.go b/internal/dao/reconcile.go new file mode 100644 index 00000000..52d5c1ae --- /dev/null +++ b/internal/dao/reconcile.go @@ -0,0 +1,100 @@ +package dao + +import ( + "context" + "fmt" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" + "github.com/rs/zerolog/log" +) + +// Reconcile previous vs current state and emits delta events. +func Reconcile(ctx context.Context, table render.TableData, gvr GVR) (render.TableData, error) { + path, ok := ctx.Value(internal.KeySelection).(string) + if !ok { + return table, fmt.Errorf("no path specified for %s", gvr) + } + if path != "" { + log.Debug().Msgf("########## OVERRIDING NS %q", path) + table.Namespace = path + } + log.Debug().Msgf(" Reconcile %q in ns %q with path %q", gvr, table.Namespace, path) + factory, ok := ctx.Value(internal.KeyFactory).(Factory) + if !ok { + return table, fmt.Errorf("no factory found for %s", gvr) + } + m, ok := model.Registry[string(gvr)] + if !ok { + log.Warn().Msgf("Resource %s not found in registry. Going generic!", gvr) + m = model.ResourceMeta{ + Model: &model.Generic{}, + Renderer: &render.Generic{}, + } + } + if m.Model == nil { + m.Model = &model.Resource{} + } + m.Model.Init(table.Namespace, string(gvr), factory) + + table.Header = m.Renderer.Header(table.Namespace) + oo, err := m.Model.List(ctx) + if err != nil { + panic(err) + } + log.Debug().Msgf("Model returned [%d] items", len(oo)) + rows := make(render.Rows, len(oo)) + if err := m.Model.Hydrate(oo, rows, m.Renderer); err != nil { + return table, err + } + update(&table, rows) + + log.Debug().Msgf("Table returned [%d] events", len(table.RowEvents)) + return table, nil +} + +func update(table *render.TableData, rows render.Rows) { + cacheEmpty := len(table.RowEvents) == 0 + kk := make([]string, 0, len(rows)) + var blankDelta render.DeltaRow + for _, row := range rows { + kk = append(kk, row.ID) + if cacheEmpty { + table.RowEvents = append(table.RowEvents, render.NewRowEvent(render.EventAdd, row)) + continue + } + if index, ok := table.RowEvents.FindIndex(row.ID); ok { + delta := render.NewDeltaRow(table.RowEvents[index].Row, row) + if delta.IsBlank() { + table.RowEvents[index].Kind, table.RowEvents[index].Deltas = render.EventUnchanged, blankDelta + } else { + table.RowEvents[index] = render.NewDeltaRowEvent(row, delta) + } + continue + } + table.RowEvents = append(table.RowEvents, render.NewRowEvent(render.EventAdd, row)) + } + + if cacheEmpty { + return + } + ensureDeletes(table, kk) +} + +// EnsureDeletes delete items in cache that are no longer valid. +func ensureDeletes(table *render.TableData, newKeys []string) { + for _, re := range table.RowEvents { + var found bool + for i, key := range newKeys { + if key == re.Row.ID { + found = true + newKeys = append(newKeys[:i], newKeys[i+1:]...) + break + } + } + if !found { + table.RowEvents = table.RowEvents.Delete(re.Row.ID) + } + } +} diff --git a/internal/dao/registry.go b/internal/dao/registry.go new file mode 100644 index 00000000..04f46e3a --- /dev/null +++ b/internal/dao/registry.go @@ -0,0 +1,213 @@ +package dao + +import ( + "fmt" + "sort" + + "github.com/derailed/k9s/internal/watch" + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +// MetaViewers represents a collection of meta viewers. +type ResourceMetas map[GVR]metav1.APIResource + +var resMetas ResourceMetas + +func AccessorFor(f Factory, gvr GVR) (Accessor, error) { + m := map[GVR]Accessor{ + "contexts": &Context{}, + "screendumps": &ScreenDump{}, + "apps/v1/deployments": &Deployment{}, + "apps/v1/daemonsets": &DaemonSet{}, + "extensions/v1beta1/daemonsets": &DaemonSet{}, + "apps/v1/statefulsets": &StatefulSet{}, + } + + r, ok := m[gvr] + if !ok { + r = &Resource{} + log.Warn().Msgf("No DAO registry entry for %q. Going generic!", gvr) + } + r.Init(f, gvr) + + return r, nil +} + +func AllGVRs() []GVR { + kk := make(GVRs, 0, len(resMetas)) + for k := range resMetas { + kk = append(kk, k) + } + sort.Sort(kk) + + return kk +} + +// MetaFor returns a resource metadata for a given gvr. +func MetaFor(gvr GVR) (metav1.APIResource, error) { + m, ok := resMetas[gvr] + if !ok { + return metav1.APIResource{}, fmt.Errorf("no resource meta defined for %q", gvr) + } + return m, nil +} + +// Load hydrates server preferred+CRDs resource metadata. +func Load(f *watch.Factory) error { + resMetas = make(ResourceMetas, 100) + if err := loadPreferred(f, resMetas); err != nil { + return err + } + if err := loadNonResource(resMetas); err != nil { + return err + } + + return loadCRDs(f, resMetas) +} + +func loadNonResource(m ResourceMetas) error { + m["contexts"] = metav1.APIResource{ + Name: "contexts", + SingularName: "context", + Namespaced: false, + Kind: "Context", + ShortNames: []string{"ctx"}, + Verbs: []string{}, + Categories: []string{"K9s"}, + } + m["screendumps"] = metav1.APIResource{ + Name: "screendumps", + SingularName: "screendump", + Namespaced: false, + Kind: "ScreenDump", + ShortNames: []string{"sd"}, + Verbs: []string{"delete"}, + Categories: []string{"K9s"}, + } + + return nil +} + +func loadPreferred(f *watch.Factory, m ResourceMetas) error { + discovery, err := f.Client().CachedDiscovery() + if err != nil { + return err + } + rr, err := discovery.ServerPreferredResources() + if err != nil { + return err + } + for _, r := range rr { + for _, res := range r.APIResources { + gvr := FromGVAndR(r.GroupVersion, res.Name) + res.Group, res.Version = gvr.ToG(), gvr.ToV() + m[gvr] = res + } + } + + return nil +} + +func loadCRDs(f *watch.Factory, m ResourceMetas) error { + oo, err := f.List("", "apiextensions.k8s.io/v1beta1/customresourcedefinitions", labels.Everything()) + if err != nil { + return err + } + f.WaitForCacheSync() + + for _, o := range oo { + meta, errs := extractMeta(o) + if len(errs) > 0 { + log.Error().Err(errs[0]).Msgf("Fail to extract CRD meta (%d) errors", len(errs)) + continue + } + gvr := NewGVR(meta.Group, meta.Version, meta.Name) + m[gvr] = meta + } + + return nil +} + +func extractMeta(o runtime.Object) (metav1.APIResource, []error) { + var ( + m metav1.APIResource + errs []error + ) + + crd, ok := o.(*unstructured.Unstructured) + if !ok { + return m, append(errs, fmt.Errorf("Expected CustomResourceDefinition, but got %T", o)) + } + + var spec map[string]interface{} + spec, errs = extractMap(crd.Object, "spec", errs) + + var meta map[string]interface{} + meta, errs = extractMap(crd.Object, "metadata", errs) + + m.Name, errs = extractStr(meta, "name", errs) + m.Group, errs = extractStr(spec, "group", errs) + m.Version, errs = extractStr(spec, "version", errs) + + var scope string + scope, errs = extractStr(spec, "scope", errs) + + m.Namespaced = isNamespaced(scope) + + var names map[string]interface{} + names, errs = extractMap(spec, "names", errs) + m.Kind, errs = extractStr(names, "kind", errs) + m.SingularName, errs = extractStr(names, "singular", errs) + m.Name, errs = extractStr(names, "plural", errs) + m.ShortNames, errs = extractSlice(names, "shortNames", errs) + + return m, errs +} + +func isNamespaced(scope string) bool { + return scope == "Namespaced" +} + +func extractSlice(m map[string]interface{}, n string, errs []error) ([]string, []error) { + if m[n] == nil { + return nil, errs + } + s, ok := m[n].([]string) + if ok { + return s, errs + } + + ii, ok := m[n].([]interface{}) + if !ok { + return s, append(errs, fmt.Errorf("failed to extract slice %s -- %#v", n, m)) + } + + ss := make([]string, len(ii)) + for i, name := range ii { + ss[i], ok = name.(string) + if !ok { + return s, append(errs, fmt.Errorf("expecting string shortnames")) + } + } + return s, errs +} + +func extractStr(m map[string]interface{}, n string, errs []error) (string, []error) { + s, ok := m[n].(string) + if !ok { + return s, append(errs, fmt.Errorf("failed to extract string %s", n)) + } + return s, errs +} + +func extractMap(m map[string]interface{}, n string, errs []error) (map[string]interface{}, []error) { + v, ok := m[n].(map[string]interface{}) + if !ok { + return v, append(errs, fmt.Errorf("failed to extract field %s", n)) + } + return v, errs +} diff --git a/internal/dao/resource.go b/internal/dao/resource.go new file mode 100644 index 00000000..1441e4f1 --- /dev/null +++ b/internal/dao/resource.go @@ -0,0 +1,32 @@ +package dao + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" +) + +type Resource struct { + Factory + + gvr GVR +} + +func (r *Resource) Init(f Factory, gvr GVR) { + r.Factory, r.gvr = f, gvr +} + +// Delete a Generic. +func (r *Resource) Delete(ns, n string, cascade, force bool) error { + p := metav1.DeletePropagationOrphan + if cascade { + p = metav1.DeletePropagationBackground + } + + return r.dynClient().Namespace(ns).Delete(n, &metav1.DeleteOptions{ + PropagationPolicy: &p, + }) +} + +func (r *Resource) dynClient() dynamic.NamespaceableResourceInterface { + return r.Client().DynDialOrDie().Resource(r.gvr.AsGVR()) +} diff --git a/internal/dao/screen_dump.go b/internal/dao/screen_dump.go new file mode 100644 index 00000000..bfd4ee06 --- /dev/null +++ b/internal/dao/screen_dump.go @@ -0,0 +1,21 @@ +package dao + +import ( + "os" + "path/filepath" + + "github.com/rs/zerolog/log" +) + +type ScreenDump struct { + Resource +} + +var _ Accessor = &ScreenDump{} +var _ Nuker = &ScreenDump{} + +// Delete a ScreenDump. +func (d *ScreenDump) Delete(dir, sel string, cascade, force bool) error { + log.Debug().Msgf("ScreenDump DELETE %q:%q", dir, sel) + return os.Remove(filepath.Join("/"+dir, sel)) +} diff --git a/internal/dao/sts.go b/internal/dao/sts.go new file mode 100644 index 00000000..1ff40a9e --- /dev/null +++ b/internal/dao/sts.go @@ -0,0 +1,80 @@ +package dao + +import ( + "context" + "errors" + "fmt" + + "github.com/rs/zerolog/log" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/kubectl/pkg/polymorphichelpers" +) + +type StatefulSet struct { + Resource +} + +var _ Accessor = &StatefulSet{} +var _ Loggable = &StatefulSet{} +var _ Restartable = &StatefulSet{} +var _ Scalable = &StatefulSet{} + +// Scale a StatefulSet. +func (s *StatefulSet) Scale(ns, n string, replicas int32) error { + scale, err := s.Client().DialOrDie().AppsV1().StatefulSets(ns).GetScale(n, metav1.GetOptions{}) + if err != nil { + return err + } + scale.Spec.Replicas = replicas + _, err = s.Client().DialOrDie().AppsV1().StatefulSets(ns).UpdateScale(n, scale) + + return err +} + +// Restart a StatefulSet rollout. +func (s *StatefulSet) Restart(ns, n string) error { + o, err := s.Get(ns, string(s.gvr), n, labels.Everything()) + if err != nil { + return err + } + + var ds appsv1.StatefulSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) + if err != nil { + return err + } + + update, err := polymorphichelpers.ObjectRestarterFn(&ds) + if err != nil { + return err + } + + _, err = s.Client().DialOrDie().AppsV1().StatefulSets(ns).Patch(ds.Name, types.StrategicMergePatchType, update) + return err +} + +// Logs tail logs for all pods represented by this StatefulSet. +func (s *StatefulSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { + log.Debug().Msgf("Tailing StatefulSet %q -- %q", opts.Namespace, opts.Name) + o, err := s.Get(opts.Namespace, string(s.gvr), opts.Name, labels.Everything()) + if err != nil { + return err + } + + var dp appsv1.StatefulSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp) + if err != nil { + return errors.New("expecting StatefulSet resource") + } + + if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 { + return fmt.Errorf("No valid selector found on StatefulSet %s", opts.FQN()) + } + + return podLogs(ctx, c, dp.Spec.Selector.MatchLabels, opts) +} diff --git a/internal/dao/types.go b/internal/dao/types.go new file mode 100644 index 00000000..4ba488ca --- /dev/null +++ b/internal/dao/types.go @@ -0,0 +1,64 @@ +package dao + +import ( + "context" + + "github.com/derailed/k9s/internal/k8s" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/informers" +) + +type Factory interface { + // Client retrieves an api client. + Client() k8s.Connection + + // Get fetch a given resource. + Get(ns, gvr, n string, sel labels.Selector) (runtime.Object, error) + + // List fetch a collection of resources. + List(ns, gvr string, sel labels.Selector) ([]runtime.Object, error) + + // ForResource fetch an informer for a given resource. + ForResource(ns, gvr string) informers.GenericInformer + + // WaitForCacheSync synchronize the cache. + WaitForCacheSync() map[schema.GroupVersionResource]bool +} + +// Accessor represents an accessible k8s resource. +type Accessor interface { + Nuker + + // Init the resource with a factory object. + Init(Factory, GVR) +} + +// Loggable represents resources with logs. +type Loggable interface { + // TaiLogs streams resource logs. + TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error +} + +type Scalable interface { + Scale(ns, n string, replicas int32) error +} + +// Nuker represents a resource deleter. +type Nuker interface { + // Delete removes a resource from the api server. + Delete(ns, n string, cascade, force bool) error +} + +// Switchable represents a switchable resource. +type Switchable interface { + // Switch changes the active context. + Switch(ctx string) error +} + +// Restartable represents a restartable resource. +type Restartable interface { + // Restart performs a rollout restart. + Restart(ns, n string) error +} diff --git a/internal/k8s/api.go b/internal/k8s/api.go index 4bf12fa5..e19003de 100644 --- a/internal/k8s/api.go +++ b/internal/k8s/api.go @@ -6,8 +6,6 @@ import ( "sync" "time" - "k8s.io/client-go/discovery/cached/disk" - "github.com/rs/zerolog/log" authorizationv1 "k8s.io/api/authorization/v1" v1 "k8s.io/api/core/v1" @@ -15,6 +13,7 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/version" + "k8s.io/client-go/discovery/cached/disk" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" @@ -107,7 +106,6 @@ func (a *APIClient) CheckNSAccess(n string) error { func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview { res := GVR(gvr).AsGVR() - log.Debug().Msgf("GVR for %s -- %#v", gvr, res) return &authorizationv1.SelfSubjectAccessReview{ Spec: authorizationv1.SelfSubjectAccessReviewSpec{ ResourceAttributes: &authorizationv1.ResourceAttributes{ @@ -168,6 +166,7 @@ func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) { // NodePods returns a collection of all available pods on a given node. func (a *APIClient) NodePods(node string) (*v1.PodList, error) { + panic("NYI") const selFmt = "spec.nodeName=%s,status.phase!=%s,status.phase!=%s" fieldSelector, err := fields.ParseSelector(fmt.Sprintf(selFmt, node, v1.PodSucceeded, v1.PodFailed)) if err != nil { diff --git a/internal/k8s/cluster_role.go b/internal/k8s/cluster_role.go index abe707e8..97819983 100644 --- a/internal/k8s/cluster_role.go +++ b/internal/k8s/cluster_role.go @@ -17,11 +17,13 @@ func NewClusterRole(c Connection) *ClusterRole { // Get a cluster role. func (c *ClusterRole) Get(_, n string) (interface{}, error) { + panic("NYI") return c.DialOrDie().RbacV1().ClusterRoles().Get(n, metav1.GetOptions{}) } // List all ClusterRoles on a cluster. func (c *ClusterRole) List(ns string, opts metav1.ListOptions) (Collection, error) { + panic("NYI") rr, err := c.DialOrDie().RbacV1().ClusterRoles().List(opts) if err != nil { return nil, err diff --git a/internal/k8s/cluster_roleb.go b/internal/k8s/cluster_roleb.go index fc853e34..07ba7a74 100644 --- a/internal/k8s/cluster_roleb.go +++ b/internal/k8s/cluster_roleb.go @@ -17,11 +17,13 @@ func NewClusterRoleBinding(c Connection) *ClusterRoleBinding { // Get a service. func (c *ClusterRoleBinding) Get(_, n string) (interface{}, error) { + panic("NYI") return c.DialOrDie().RbacV1().ClusterRoleBindings().Get(n, metav1.GetOptions{}) } // List all ClusterRoleBindings on a cluster. func (c *ClusterRoleBinding) List(ns string, opts metav1.ListOptions) (Collection, error) { + panic("NYI") rr, err := c.DialOrDie().RbacV1().ClusterRoleBindings().List(opts) if err != nil { return Collection{}, err diff --git a/internal/k8s/context.go b/internal/k8s/context.go index e6a8a53a..b6f334f8 100644 --- a/internal/k8s/context.go +++ b/internal/k8s/context.go @@ -5,7 +5,6 @@ import ( "github.com/rs/zerolog/log" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" ) diff --git a/internal/k8s/dp.go b/internal/k8s/dp.go index 2b1993d2..20990a7a 100644 --- a/internal/k8s/dp.go +++ b/internal/k8s/dp.go @@ -19,11 +19,13 @@ func NewDeployment(c Connection) *Deployment { // Get a deployment. func (d *Deployment) Get(ns, n string) (interface{}, error) { + panic("NYI") return d.DialOrDie().AppsV1().Deployments(ns).Get(n, metav1.GetOptions{}) } // List all Deployments in a given namespace. func (d *Deployment) List(ns string, opts metav1.ListOptions) (Collection, error) { + panic("NYI") rr, err := d.DialOrDie().AppsV1().Deployments(ns).List(opts) if err != nil { return nil, err diff --git a/internal/k8s/ds.go b/internal/k8s/ds.go index 4df02ea4..c21da128 100644 --- a/internal/k8s/ds.go +++ b/internal/k8s/ds.go @@ -1,64 +1,73 @@ package k8s -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/kubectl/pkg/polymorphichelpers" -) +// BOZO!! +// import ( +// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +// "k8s.io/apimachinery/pkg/types" +// "k8s.io/kubectl/pkg/polymorphichelpers" +// ) -// DaemonSet represents a Kubernetes DaemonSet -type DaemonSet struct { - *base - Connection -} +// // DaemonSet represents a Kubernetes DaemonSet +// type DaemonSet struct { +// *base +// Connection +// } -// NewDaemonSet returns a new DaemonSet. -func NewDaemonSet(c Connection) *DaemonSet { - return &DaemonSet{&base{}, c} -} +// // NewDaemonSet returns a new DaemonSet. +// func NewDaemonSet(c Connection) *DaemonSet { +// return &DaemonSet{&base{}, c} +// } -// Get a DaemonSet. -func (d *DaemonSet) Get(ns, n string) (interface{}, error) { - return d.DialOrDie().AppsV1().DaemonSets(ns).Get(n, metav1.GetOptions{}) -} +// // Get a DaemonSet. +// func (d *DaemonSet) Get(ns, n string) (interface{}, error) { +// panic("NYI") +// return d.DialOrDie().AppsV1().DaemonSets(ns).Get(n, metav1.GetOptions{}) +// } -// List all DaemonSets in a given namespace. -func (d *DaemonSet) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := d.DialOrDie().AppsV1().DaemonSets(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } +// // List all DaemonSets in a given namespace. +// func (d *DaemonSet) List(ns string, opts metav1.ListOptions) (Collection, error) { +// panic("NYI") +// rr, err := d.DialOrDie().AppsV1().DaemonSets(ns).List(opts) +// if err != nil { +// return nil, err +// } +// cc := make(Collection, len(rr.Items)) +// for i, r := range rr.Items { +// cc[i] = r +// } - return cc, nil -} +// return cc, nil +// } -// Delete a DaemonSet. -func (d *DaemonSet) Delete(ns, n string, cascade, force bool) error { - p := metav1.DeletePropagationOrphan - if cascade { - p = metav1.DeletePropagationBackground - } - return d.DialOrDie().AppsV1().DaemonSets(ns).Delete(n, &metav1.DeleteOptions{ - PropagationPolicy: &p, - }) -} +// // Delete a DaemonSet. +// func (d *DaemonSet) Delete(ns, n string, cascade, force bool) error { +// p := metav1.DeletePropagationOrphan +// if cascade { +// p = metav1.DeletePropagationBackground +// } +// return d.DialOrDie().AppsV1().DaemonSets(ns).Delete(n, &metav1.DeleteOptions{ +// PropagationPolicy: &p, +// }) +// } -// Restart a DaemonSet rollout. -func (d *DaemonSet) Restart(ns, n string) error { +// // Restart a DaemonSet rollout. +// func (d *DaemonSet) Restart(f *watch.Factory, ns, n string) error { +// o, err := f.Get(ns, "apps/v1/deamonsets", n, labels.Everything()) +// if err != nil { +// return err +// } - ds, err := d.DialOrDie().AppsV1().DaemonSets(ns).Get(n, metav1.GetOptions{}) - if err != nil { - return err - } - update, err := polymorphichelpers.ObjectRestarterFn(ds) - if err != nil { - return err - } +// var ds appsv1.DaemonSet +// err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) +// if err != nil { +// return err +// } - _, err = d.DialOrDie().AppsV1().DaemonSets(ns).Patch(ds.Name, types.StrategicMergePatchType, update) - return err -} +// update, err := polymorphichelpers.ObjectRestarterFn(ds) +// if err != nil { +// return err +// } + +// _, err = f.Client().DialOrDie().AppsV1().DaemonSets(ns).Patch(ds.Name, types.StrategicMergePatchType, update) +// return err +// } diff --git a/internal/k8s/gvr.go b/internal/k8s/gvr.go index 5d7ea45f..96a67b7f 100644 --- a/internal/k8s/gvr.go +++ b/internal/k8s/gvr.go @@ -25,8 +25,8 @@ func (g GVR) ResName() string { return g.ToR() + "." + g.ToV() + "." + g.ToG() } -// AsGR returns the group version. -func (g GVR) AsGR() schema.GroupVersion { +// AsGV returns the group version. +func (g GVR) AsGV() schema.GroupVersion { return schema.GroupVersion{ Group: g.ToG(), Version: g.ToV(), diff --git a/internal/k8s/gvr_test.go b/internal/k8s/gvr_test.go index 750a3675..23faba01 100644 --- a/internal/k8s/gvr_test.go +++ b/internal/k8s/gvr_test.go @@ -8,7 +8,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) -func TestAsGR(t *testing.T) { +func TestAsGV(t *testing.T) { uu := map[string]struct { gvr string e schema.GroupVersion @@ -21,7 +21,7 @@ func TestAsGR(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, k8s.GVR(u.gvr).AsGR()) + assert.Equal(t, u.e, k8s.GVR(u.gvr).AsGV()) }) } } diff --git a/internal/k8s/mapper.go b/internal/k8s/mapper.go index f44433f9..3714464c 100644 --- a/internal/k8s/mapper.go +++ b/internal/k8s/mapper.go @@ -73,7 +73,6 @@ func (r *RestMapper) resourceFor(resourceArg string) (schema.GroupVersionResourc } fullGVR, gr := schema.ParseResourceArg(strings.ToLower(resourceArg)) - log.Debug().Msgf("GVR %#v -- %#v", fullGVR, gr) if fullGVR != nil { return mapper.ResourceFor(*fullGVR) } diff --git a/internal/k8s/no.go b/internal/k8s/node.go similarity index 100% rename from internal/k8s/no.go rename to internal/k8s/node.go diff --git a/internal/k8s/ns.go b/internal/k8s/ns.go index 04baeb8b..505cbfae 100644 --- a/internal/k8s/ns.go +++ b/internal/k8s/ns.go @@ -17,11 +17,13 @@ func NewNamespace(c Connection) *Namespace { // Get a active namespace. func (n *Namespace) Get(_, name string) (interface{}, error) { + panic("NYI") return n.DialOrDie().CoreV1().Namespaces().Get(name, metav1.GetOptions{}) } // List all active namespaces on the cluster. func (n *Namespace) List(ns string, opts metav1.ListOptions) (Collection, error) { + panic("NYI") rr, err := n.DialOrDie().CoreV1().Namespaces().List(opts) if err != nil { return nil, err diff --git a/internal/k8s/pod.go b/internal/k8s/pod.go index 44c0bf01..5194bb68 100644 --- a/internal/k8s/pod.go +++ b/internal/k8s/pod.go @@ -22,11 +22,14 @@ func NewPod(c Connection) *Pod { // Get a pod. func (p *Pod) Get(ns, name string) (interface{}, error) { + panic("POd GEt") return p.DialOrDie().CoreV1().Pods(ns).Get(name, metav1.GetOptions{}) } // List all pods in a given namespace. func (p *Pod) List(ns string, opts metav1.ListOptions) (Collection, error) { + panic("POd List") + rr, err := p.DialOrDie().CoreV1().Pods(ns).List(opts) if err != nil { return nil, err diff --git a/internal/k8s/resource.go b/internal/k8s/resource.go index ec5874c6..7a1b9991 100644 --- a/internal/k8s/resource.go +++ b/internal/k8s/resource.go @@ -76,7 +76,7 @@ func (r *Resource) listAll(ns, n string) (runtime.Object, error) { func (r *Resource) getClient() (*rest.RESTClient, error) { crConfig := r.RestConfigOrDie() - gv := r.gvr.AsGR() + gv := r.gvr.AsGV() crConfig.GroupVersion = &gv crConfig.APIPath = "/apis" if len(r.gvr.ToG()) == 0 { @@ -94,7 +94,7 @@ func (r *Resource) getClient() (*rest.RESTClient, error) { func (r *Resource) codec() (serializer.CodecFactory, runtime.ParameterCodec) { scheme := runtime.NewScheme() - gv := r.gvr.AsGR() + gv := r.gvr.AsGV() metav1.AddToGroupVersion(scheme, gv) scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) diff --git a/internal/k8s/role.go b/internal/k8s/role.go index 011a8cda..dbccc062 100644 --- a/internal/k8s/role.go +++ b/internal/k8s/role.go @@ -18,11 +18,13 @@ func NewRole(c Connection) *Role { // Get a Role. func (r *Role) Get(ns, n string) (interface{}, error) { + panic("NYI") return r.DialOrDie().RbacV1().Roles(ns).Get(n, metav1.GetOptions{}) } // List all Roles in a given namespace. func (r *Role) List(ns string, opts metav1.ListOptions) (Collection, error) { + panic("NYI") rr, err := r.DialOrDie().RbacV1().Roles(ns).List(opts) if err != nil { return nil, err diff --git a/internal/k8s/role_binding.go b/internal/k8s/role_binding.go index 1d5f24b2..62a29021 100644 --- a/internal/k8s/role_binding.go +++ b/internal/k8s/role_binding.go @@ -15,11 +15,13 @@ func NewRoleBinding(c Connection) *RoleBinding { // Get a RoleBinding. func (r *RoleBinding) Get(ns, n string) (interface{}, error) { + panic("NYI") return r.DialOrDie().RbacV1().RoleBindings(ns).Get(n, metav1.GetOptions{}) } // List all RoleBindings in a given namespace. func (r *RoleBinding) List(ns string, opts metav1.ListOptions) (Collection, error) { + panic("NYI") rr, err := r.DialOrDie().RbacV1().RoleBindings(ns).List(opts) if err != nil { return nil, err diff --git a/internal/k8s/svc.go b/internal/k8s/svc.go index 6a570e78..7165a170 100644 --- a/internal/k8s/svc.go +++ b/internal/k8s/svc.go @@ -17,11 +17,13 @@ func NewService(c Connection) *Service { // Get a service. func (s *Service) Get(ns, n string) (interface{}, error) { + panic("NYI") return s.DialOrDie().CoreV1().Services(ns).Get(n, metav1.GetOptions{}) } // List all Services in a given namespace. func (s *Service) List(ns string, opts metav1.ListOptions) (Collection, error) { + panic("NYI") rr, err := s.DialOrDie().CoreV1().Services(ns).List(opts) if err != nil { return nil, err diff --git a/internal/keys.go b/internal/keys.go new file mode 100644 index 00000000..a583fc28 --- /dev/null +++ b/internal/keys.go @@ -0,0 +1,14 @@ +package internal + +// ContextKey represents context key. +type ContextKey string + +const ( + // Factory represents a factory context key. + KeyFactory ContextKey = "factory" + KeySelection = "selection" + KeyLabels = "labels" + KeyFields = "fields" + KeyTable = "table" + KeyDir = "dir" +) diff --git a/internal/model/co.go b/internal/model/container.go similarity index 69% rename from internal/model/co.go rename to internal/model/container.go index c17fdfb4..f866a816 100644 --- a/internal/model/co.go +++ b/internal/model/container.go @@ -1,6 +1,9 @@ package model import ( + "context" + + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/render" "github.com/rs/zerolog/log" @@ -9,6 +12,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) @@ -16,18 +20,15 @@ var _ render.ContainerWithMetrics = &ContainerWithMetrics{} // Container represents a container model. type Container struct { - *Resource -} + Resource -// NewContainer returns a new container model -func NewContainer() *Container { - return &Container{ - Resource: NewResource(), - } + pod *v1.Pod } // List returns a collection of containers -func (c *Container) List(sel string) ([]runtime.Object, error) { +func (c *Container) List(ctx context.Context) ([]runtime.Object, error) { + c.pod = nil + sel := ctx.Value(internal.KeySelection).(string) ns, n := render.Namespaced(sel) c.namespace = ns o, err := c.factory.Get(ns, "v1/pods", n, labels.Everything()) @@ -40,46 +41,43 @@ func (c *Container) List(sel string) ([]runtime.Object, error) { if err != nil { return nil, err } + c.pod = &po + + res := make([]runtime.Object, 0, len(po.Spec.InitContainers)+len(po.Spec.Containers)) + for _, co := range po.Spec.InitContainers { + res = append(res, ContainerRes{co}) + } + for _, co := range po.Spec.Containers { + res = append(res, ContainerRes{co}) + } - res := make([]runtime.Object, 1, len(po.Spec.InitContainers)+len(po.Spec.Containers)) - res[0] = &po return res, nil } // Hydrate returns a pod as container rows. -func (c *Container) Hydrate(cc []runtime.Object, rr render.Rows, re Renderer) error { - po := cc[0].(*v1.Pod) +func (c *Container) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { mx := k8s.NewMetricsServer(c.factory.Client().(k8s.Connection)) - mmx, err := mx.FetchPodMetrics(c.namespace, po.Name) + mmx, err := mx.FetchPodMetrics(c.namespace, c.pod.Name) if err != nil { - return err + log.Warn().Err(err).Msgf("No metrics found for pod %q:%q", c.namespace, c.pod.Name) } var index int - size := len(re.Header(c.namespace)) - for _, co := range po.Spec.InitContainers { - row, err := renderCoRow(co.Name, index, size, coMetricsFor(co, po, mmx, true), re) + for _, o := range oo { + co := o.(ContainerRes) + row, err := renderCoRow(co.Container.Name, index, coMetricsFor(co.Container, c.pod, mmx, true), re) if err != nil { return err } rr[index] = row - log.Debug().Msgf("Init Containers %#v", rr[index]) - index++ - } - for _, co := range po.Spec.Containers { - row, err := renderCoRow(co.Name, index, size, coMetricsFor(co, po, mmx, false), re) - if err != nil { - return err - } - rr[index] = row - log.Debug().Msgf("Containers %#v", row) index++ } + return nil } -func renderCoRow(n string, index, size int, pmx *ContainerWithMetrics, re Renderer) (render.Row, error) { - row := render.Row{Fields: make([]string, size)} +func renderCoRow(n string, index int, pmx *ContainerWithMetrics, re Renderer) (render.Row, error) { + var row render.Row if err := re.Render(pmx, n, &row); err != nil { return render.Row{}, err } @@ -98,9 +96,7 @@ func coMetricsFor(co v1.Container, po *v1.Pod, mmx *mv1beta1.PodMetrics, isInit func containerMetrics(n string, mx runtime.Object) *mv1beta1.ContainerMetrics { pmx := mx.(*mv1beta1.PodMetrics) - log.Debug().Msgf("CO MX fo %s", n) for _, m := range pmx.Containers { - log.Debug().Msgf("Container Metrics %#v", m) if m.Name == n { return &m } @@ -155,3 +151,20 @@ func (c *ContainerWithMetrics) Metrics() *mv1beta1.ContainerMetrics { func (c *ContainerWithMetrics) Age() metav1.Time { return c.age } + +// ---------------------------------------------------------------------------- + +// ContainerRes represents a container K8s resource. +type ContainerRes struct { + v1.Container +} + +// GetObjectKind returns a schema object. +func (c ContainerRes) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (c ContainerRes) DeepCopyObject() runtime.Object { + return c +} diff --git a/internal/model/context.go b/internal/model/context.go new file mode 100644 index 00000000..6f930a4b --- /dev/null +++ b/internal/model/context.go @@ -0,0 +1,49 @@ +package model + +import ( + "context" + "fmt" + + "github.com/derailed/k9s/internal/render" + "k8s.io/apimachinery/pkg/runtime" +) + +// Context represents a kube context model. +type Context struct { + Resource +} + +// List returns a collection of node resources. +func (c *Context) List(_ context.Context) ([]runtime.Object, error) { + cfg := c.factory.Client().Config() + ctxs, err := cfg.Contexts() + if err != nil { + return nil, err + } + cc := make([]runtime.Object, 0, len(ctxs)) + for name, ctx := range ctxs { + cc = append(cc, render.NewNamedContext(cfg, name, ctx)) + } + + return cc, nil +} + +// Hydrate returns nodes as rows. +func (n *Context) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { + var index int + for _, o := range oo { + ctx, ok := o.(*render.NamedContext) + if !ok { + return fmt.Errorf("expecting named context but got %T", o) + } + + var row render.Row + if err := re.Render(ctx, "", &row); err != nil { + return err + } + rr[index] = row + index++ + } + + return nil +} diff --git a/internal/model/generic.go b/internal/model/generic.go new file mode 100644 index 00000000..0f199162 --- /dev/null +++ b/internal/model/generic.go @@ -0,0 +1,130 @@ +package model + +import ( + "context" + "fmt" + + "github.com/derailed/k9s/internal/k8s" + "github.com/derailed/k9s/internal/render" + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/client-go/rest" +) + +const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json" + +// Generic represents a generic model. +type Generic struct { + Resource + + table *metav1beta1.Table +} + +// List returns a collection of node resources. +func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) { + // Ensures the factory is tracking this resource + _ = g.factory.ForResource(g.namespace, g.gvr) + + gvr := k8s.GVR(g.gvr) + fcodec, codec := g.codec(gvr.AsGV()) + + c, err := g.client(fcodec, gvr) + if err != nil { + return nil, err + } + + // BOZO!! Need to know if gvr is namespaced or not + o, err := c.Get(). + SetHeader("Accept", fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)). + // Namespace(g.namespace). + Resource(gvr.ToR()). + VersionedParams(&metav1beta1.TableOptions{}, codec). + Do().Get() + + table, ok := o.(*metav1beta1.Table) + if !ok { + return nil, fmt.Errorf("invalid table found on generic %s -- %T", g.gvr, o) + } + g.table = table + res := make([]runtime.Object, len(g.table.Rows)) + for i := range g.table.Rows { + res[i] = RowRes{&g.table.Rows[i]} + } + + log.Debug().Msgf("!!!!GENERIC lister returns %d", len(res)) + return res, err +} + +// Hydrate returns nodes as rows. +func (g *Generic) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { + gr, ok := re.(*render.Generic) + if !ok { + return fmt.Errorf("expecting generic renderer for %s but got %T", g.gvr, re) + } + gr.SetTable(g.table) + for i, o := range oo { + res, ok := o.(RowRes) + if !ok { + return fmt.Errorf("expecting RowRes but got %#v", o) + } + count := len(res.Cells) + if g.namespace == "" { + count++ + } + if err := gr.Render(res.TableRow, g.namespace, &rr[i]); err != nil { + return err + } + } + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func (g *Generic) client(codec serializer.CodecFactory, gvr k8s.GVR) (*rest.RESTClient, error) { + crConfig := g.factory.Client().RestConfigOrDie() + gv := gvr.AsGV() + crConfig.GroupVersion = &gv + crConfig.APIPath = "/apis" + if len(gvr.ToG()) == 0 { + crConfig.APIPath = "/api" + } + crConfig.NegotiatedSerializer = codec.WithoutConversion() + + crRestClient, err := rest.RESTClientFor(crConfig) + if err != nil { + return nil, err + } + return crRestClient, nil +} + +func (r *Resource) codec(gv schema.GroupVersion) (serializer.CodecFactory, runtime.ParameterCodec) { + scheme := runtime.NewScheme() + metav1.AddToGroupVersion(scheme, gv) + scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) + scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) + + return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme) +} + +// ---------------------------------------------------------------------------- + +// RowRes represents a table row. +type RowRes struct { + *metav1beta1.TableRow +} + +// GetObjectKind returns a schema object. +func (r RowRes) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (r RowRes) DeepCopyObject() runtime.Object { + return r +} diff --git a/internal/model/no.go b/internal/model/node.go similarity index 86% rename from internal/model/no.go rename to internal/model/node.go index 7b46aded..d1ce2c84 100644 --- a/internal/model/no.go +++ b/internal/model/node.go @@ -1,9 +1,10 @@ package model import ( + "context" + "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/render" - "github.com/derailed/k9s/internal/watch" "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -17,16 +18,11 @@ var _ render.NodeWithMetrics = &NodeWithMetrics{} // Node represents a node model. type Node struct { - *Resource -} - -// NewNode returns a new node model. -func NewNode() *Node { - return &Node{Resource: NewResource()} + Resource } // List returns a collection of node resources. -func (n *Node) List(_ string) ([]runtime.Object, error) { +func (n *Node) List(_ context.Context) ([]runtime.Object, error) { nn, err := n.factory.Client().DialOrDie().CoreV1().Nodes().List(metav1.ListOptions{}) if err != nil { return nil, err @@ -52,19 +48,17 @@ func (n *Node) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { } var index int - size := len(re.Header("")) for _, no := range oo { o := no.(*unstructured.Unstructured) pods, err := n.nodePods(n.factory, o.Object["metadata"].(map[string]interface{})["name"].(string)) if err != nil { - panic(err) - } - row := render.Row{Fields: make([]string, size)} - nmx := NodeWithMetrics{ - o, - nodeMetricsFor(o, mmx), - pods, + return err } + + var ( + row render.Row + nmx = NodeWithMetrics{object: o, mx: nodeMetricsFor(o, mmx), pods: pods} + ) if err := re.Render(&nmx, "", &row); err != nil { return err } @@ -85,7 +79,7 @@ func nodeMetricsFor(o runtime.Object, mmx *mv1beta1.NodeMetricsList) *mv1beta1.N return nil } -func (n *Node) nodePods(f *watch.Factory, node string) ([]*v1.Pod, error) { +func (n *Node) nodePods(f Factory, node string) ([]*v1.Pod, error) { pp, err := f.List("", "v1/pods", labels.Everything()) if err != nil { return nil, err diff --git a/internal/model/po.go b/internal/model/pod.go similarity index 59% rename from internal/model/po.go rename to internal/model/pod.go index a6884370..d5e40c2c 100644 --- a/internal/model/po.go +++ b/internal/model/pod.go @@ -1,9 +1,13 @@ package model import ( + "context" + "fmt" + + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/render" - v1 "k8s.io/api/core/v1" + "github.com/rs/zerolog/log" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -12,37 +16,42 @@ import ( // Pod represents a pod model. type Pod struct { - *Resource + Resource } -// NewPod returns a new pod model. -func NewPod() *Pod { - return &Pod{NewResource()} -} +// List returns a collection of nodes. +func (p *Pod) List(ctx context.Context) ([]runtime.Object, error) { + oo, err := p.Resource.List(ctx) + if err != nil { + return oo, err + } -func (p *Pod) FetchContainers(sel string, includeInit bool) ([]string, error) { - o, err := p.factory.Get(p.namespace, p.gvr, sel, labels.Everything()) + fieldSel, ok := ctx.Value(internal.KeyFields).(string) + if !ok { + return oo, nil + } + + sel, err := labels.ConvertSelectorToLabelsMap(fieldSel) if err != nil { return nil, err } - var po v1.Pod - if runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil { - return nil, err + nodeName, ok := sel["spec.nodeName"] + if !ok { + return nil, fmt.Errorf("NYI field selector %q", nodeName) } - cc := make([]string, 0, len(po.Spec.Containers)) - for _, c := range po.Spec.Containers { - cc = append(cc, c.Name) - } - - if includeInit { - for _, c := range po.Spec.InitContainers { - cc = append(cc, c.Name) + var res []runtime.Object + for _, o := range oo { + u := o.(*unstructured.Unstructured) + spec := u.Object["spec"].(map[string]interface{}) + log.Debug().Msgf("Spec node %q -- %q", nodeName, spec["nodeName"]) + if spec["nodeName"] == nodeName { + res = append(res, o) } } - return cc, nil + return res, nil } // Render returns pod resources as rows. @@ -50,14 +59,15 @@ func (p *Pod) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { mx := k8s.NewMetricsServer(p.factory.Client().(k8s.Connection)) mmx, err := mx.FetchPodsMetrics(p.namespace) if err != nil { - return err + log.Warn().Err(err).Msgf("No metrics found for pod") } var index int - size := len(re.Header(p.namespace)) for _, o := range oo { - row := render.Row{Fields: make([]string, size)} - pmx := PodWithMetrics{o, podMetricsFor(o, mmx)} + var ( + row render.Row + pmx = PodWithMetrics{object: o, mx: podMetricsFor(o, mmx)} + ) if err := re.Render(&pmx, p.namespace, &row); err != nil { return err } diff --git a/internal/model/registry.go b/internal/model/registry.go index 8115c0b9..d102a9bb 100644 --- a/internal/model/registry.go +++ b/internal/model/registry.go @@ -2,51 +2,81 @@ package model import ( "github.com/derailed/k9s/internal/render" - "github.com/derailed/k9s/internal/watch" - "k8s.io/apimachinery/pkg/runtime" ) -type Renderer interface { - // Render converts raw resources to tabular data. - Render(o interface{}, ns string, row *render.Row) error - - // Header returns the resource header. - Header(ns string) render.HeaderRow - - ColorerFunc() render.ColorerFunc -} - -type Lister interface { - // Init initializes a resource. - Init(ns, gvr string, f *watch.Factory) - - // List returns a collection of resources. - List(sel string) ([]runtime.Object, error) - - // Hydrate converts resource rows into tabular data. - Hydrate([]runtime.Object, render.Rows, Renderer) error -} - -type ResourceMeta struct { - Model Lister - Renderer Renderer -} - +// BOZO!! Break up deps and merge into single registrar var Registry = map[string]ResourceMeta{ + "containers": ResourceMeta{ + Model: &Container{}, + Renderer: &render.Container{}, + }, + "contexts": ResourceMeta{ + Model: &Context{}, + Renderer: &render.Context{}, + }, + "screendumps": ResourceMeta{ + Model: &ScreenDump{}, + Renderer: &render.ScreenDump{}, + }, + "v1/pods": ResourceMeta{ - Model: NewPod(), + Model: &Pod{}, Renderer: &render.Pod{}, }, "v1/nodes": ResourceMeta{ - Model: NewNode(), + Model: &Node{}, Renderer: &render.Node{}, }, - "v1/configmaps": ResourceMeta{ - Model: NewResource(), - Renderer: &render.ConfigMap{}, + "v1/namespaces": ResourceMeta{ + Renderer: &render.Namespace{}, }, - "containers": ResourceMeta{ - Model: NewContainer(), - Renderer: &render.Container{}, + + "apps/v1/deployments": ResourceMeta{ + Renderer: &render.Deployment{}, + }, + "apps/v1/replicasets": ResourceMeta{ + Renderer: &render.ReplicaSet{}, + }, + "apps/v1/statefulsets": ResourceMeta{ + Renderer: &render.StatefulSet{}, + }, + "apps/v1/daemonsets": ResourceMeta{ + Renderer: &render.DaemonSet{}, + }, + "extensions/v1beta1/daemonsets": ResourceMeta{ + Renderer: &render.DaemonSet{}, + }, + + // "v1/services": ResourceMeta{ + // Renderer: &render.Service{}, + // }, + // "v1/configmaps": ResourceMeta{ + // Renderer: &render.ConfigMap{}, + // }, + // "v1/secrets": ResourceMeta{ + // Renderer: &render.ConfigMap{}, + // }, + // "batch/v1beta1/cronjobs": ResourceMeta{ + // Renderer: &render.CronJob{}, + // }, + // "batch/v1/jobs": ResourceMeta{ + // Renderer: &render.Job{}, + // }, + + "apiextensions.k8s.io/v1beta1/customresourcedefinitions": ResourceMeta{ + Renderer: &render.CustomResourceDefinition{}, + }, + + "rbac.authorization.k8s.io/v1/clusterroles": ResourceMeta{ + Renderer: &render.ClusterRole{}, + }, + "rbac.authorization.k8s.io/v1/clusterrolebindings": ResourceMeta{ + Renderer: &render.ClusterRoleBinding{}, + }, + "rbac.authorization.k8s.io/v1/roles": ResourceMeta{ + Renderer: &render.Role{}, + }, + "rbac.authorization.k8s.io/v1/rolebindings": ResourceMeta{ + Renderer: &render.RoleBinding{}, }, } diff --git a/internal/model/resource.go b/internal/model/resource.go index 8d5ae188..0c093855 100644 --- a/internal/model/resource.go +++ b/internal/model/resource.go @@ -1,8 +1,11 @@ package model import ( + "context" + + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/render" - "github.com/derailed/k9s/internal/watch" + "github.com/rs/zerolog/log" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -11,30 +14,35 @@ import ( // Resource represents a generic resource model. type Resource struct { namespace, gvr string - factory *watch.Factory + factory Factory } -func NewResource() *Resource { - return &Resource{} -} - -// NewResource returns a new model. -func (r *Resource) Init(ns, gvr string, f *watch.Factory) { +func (r *Resource) Init(ns, gvr string, f Factory) { r.namespace, r.gvr, r.factory = ns, gvr, f } // List returns a collection of nodes. -func (r *Resource) List(_ string) ([]runtime.Object, error) { - return r.factory.List(r.namespace, r.gvr, labels.Everything()) +func (r *Resource) List(ctx context.Context) ([]runtime.Object, error) { + strLabel, ok := ctx.Value(internal.KeyLabels).(string) + lsel := labels.Everything() + if sel, err := labels.ConvertSelectorToLabelsMap(strLabel); ok && err == nil { + lsel = sel.AsSelector() + } + + oo, err := r.factory.List(r.namespace, r.gvr, lsel) + r.factory.WaitForCacheSync() + + return oo, err } // Render returns a node as a row. func (r *Resource) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { + log.Debug().Msgf("^^^^^^ HYDRATING (%q) %d", r.namespace, len(oo)) + var index int - size := len(re.Header(r.namespace)) for _, o := range oo { res := o.(*unstructured.Unstructured) - row := render.Row{Fields: make([]string, size)} + var row render.Row if err := re.Render(res, r.namespace, &row); err != nil { return err } diff --git a/internal/model/screen_dump.go b/internal/model/screen_dump.go new file mode 100644 index 00000000..9ec31c2e --- /dev/null +++ b/internal/model/screen_dump.go @@ -0,0 +1,81 @@ +package model + +import ( + "context" + "errors" + "fmt" + "io/ioutil" + "os" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/render" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// ScreenDump represents a container model. +type ScreenDump struct { + Resource + + pod *v1.Pod +} + +// List returns a collection of containers +func (c *ScreenDump) List(ctx context.Context) ([]runtime.Object, error) { + dir, ok := ctx.Value(internal.KeyDir).(string) + if !ok { + return nil, errors.New("no screendump dir found in context") + } + + ff, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + + oo := make([]runtime.Object, len(ff)) + for i, f := range ff { + oo[i] = FileRes{file: f, dir: dir} + } + + return oo, nil +} + +// Hydrate returns a pod as container rows. +func (c *ScreenDump) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { + for i, o := range oo { + res, ok := o.(FileRes) + if !ok { + return fmt.Errorf("expecting a file resource but got %T", o) + } + + if err := re.Render(res, render.NonResource, &rr[i]); err != nil { + return err + } + } + + return nil +} + +// ---------------------------------------------------------------------------- + +// FileRes represents a file resource. +type FileRes struct { + file os.FileInfo + dir string +} + +func (c FileRes) GetFile() os.FileInfo { return c.file } +func (c FileRes) GetDir() string { return c.dir } + +// GetObjectKind returns a schema object. +func (c FileRes) GetObjectKind() schema.ObjectKind { + + return nil +} + +// DeepCopyObject returns a container copy. +func (c FileRes) DeepCopyObject() runtime.Object { + + return c +} diff --git a/internal/model/types.go b/internal/model/types.go index 41d7cc48..a082b8ef 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -3,7 +3,13 @@ package model import ( "context" + "github.com/derailed/k9s/internal/k8s" + "github.com/derailed/k9s/internal/render" "github.com/derailed/tview" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/informers" ) // Igniter represents a runnable view. @@ -20,6 +26,7 @@ type Igniter interface { // Hinter represent a menu mnemonic provider. type Hinter interface { + // Hints returns a collection of menu hints. Hints() MenuHints } @@ -37,3 +44,74 @@ type Component interface { Igniter Hinter } + +// Renderer represents a resource renderer. +type Renderer interface { + // Render converts raw resources to tabular data. + Render(o interface{}, ns string, row *render.Row) error + + // Header returns the resource header. + Header(ns string) render.HeaderRow + + // ColorerFunc returns a row colorer function. + ColorerFunc() render.ColorerFunc +} + +// Lister represents a resource lister. +type Lister interface { + // Init initializes a resource. + Init(ns, gvr string, f Factory) + + // List returns a collection of resources. + List(context.Context) ([]runtime.Object, error) + + // Hydrate converts resource rows into tabular data. + Hydrate([]runtime.Object, render.Rows, Renderer) error +} + +// BOZO!! +// type Connection interface { +// // DialOrDie dials client api. +// DialOrDie() kubernetes.Interface + +// // MXDial dials metrics api. +// MXDial() (*versioned.Clientset, error) + +// // DynDialOrDie dials dynamic client api. +// DynDialOrDie() dynamic.Interface + +// // RestConfigOrDie return a client configuration. +// RestConfigOrDie() *restclient.Config + +// // Config returns the current kubeconfig. +// Config() *k8s.Config + +// // CachedDiscovery returns a cached client. +// CachedDiscovery() (*disk.CachedDiscoveryClient, error) + +// // SwithContextOrDie switch to a new kube context. +// SwitchContextOrDie(ctx string) +// } + +type Factory interface { + // Client retrieves an api client. + Client() k8s.Connection + + // Get fetch a given resource. + Get(ns, gvr, n string, sel labels.Selector) (runtime.Object, error) + + // List fetch a collection of resources. + List(ns, gvr string, sel labels.Selector) ([]runtime.Object, error) + + // ForResource fetch an informer for a given resource. + ForResource(ns, gvr string) informers.GenericInformer + + // WaitForCacheSync synchronize the cache. + WaitForCacheSync() map[schema.GroupVersionResource]bool +} + +// ResourceMeta represents model info about a resource. +type ResourceMeta struct { + Model Lister + Renderer Renderer +} diff --git a/internal/render/alias.go b/internal/render/alias.go new file mode 100644 index 00000000..23a0155d --- /dev/null +++ b/internal/render/alias.go @@ -0,0 +1,64 @@ +package render + +import ( + "fmt" + "strings" + + "github.com/derailed/k9s/internal/k8s" + "github.com/gdamore/tcell" +) + +// Alias renders a aliases to screen. +type Alias struct{} + +// ColorerFunc colors a resource row. +func (Alias) ColorerFunc() ColorerFunc { + return func(ns string, re RowEvent) tcell.Color { + return tcell.ColorMediumSpringGreen + } +} + +// Header returns a header row. +func (Alias) Header(ns string) HeaderRow { + return HeaderRow{ + Header{Name: "RESOURCE"}, + Header{Name: "COMMAND"}, + Header{Name: "APIGROUP"}, + } +} + +// Render renders a K8s resource to screen. +func (Alias) Render(o interface{}, gvr string, r *Row) error { + aliases, ok := o.([]string) + if !ok { + return fmt.Errorf("Expected Alias, but got %T", o) + } + + g := k8s.GVR(gvr) + r.ID = string(gvr) + r.Fields = Fields{ + g.ToR(), + strings.Join(aliases, ","), + g.ToG(), + // Pad(g.ToR(), 30), + // Pad(strings.Join(aliases, ","), 70), + // Pad(g.ToG(), 30), + } + + return nil +} + +// Helpers... + +// Pad a string up to the given length or truncates if greater than length. +func Pad(s string, width int) string { + if len(s) == width { + return s + } + + if len(s) > width { + return Truncate(s, width) + } + + return s + strings.Repeat(" ", width-len(s)) +} diff --git a/internal/render/bench.go b/internal/render/bench.go new file mode 100644 index 00000000..8b157129 --- /dev/null +++ b/internal/render/bench.go @@ -0,0 +1,158 @@ +package render + +import ( + "fmt" + "io/ioutil" + "os" + "regexp" + "strconv" + "strings" + "time" + + "github.com/derailed/tview" + "github.com/gdamore/tcell" + "golang.org/x/text/language" + "golang.org/x/text/message" +) + +var ( + totalRx = regexp.MustCompile(`Total:\s+([0-9.]+)\ssecs`) + reqRx = regexp.MustCompile(`Requests/sec:\s+([0-9.]+)`) + okRx = regexp.MustCompile(`\[2\d{2}\]\s+(\d+)\s+responses`) + errRx = regexp.MustCompile(`\[[4-5]\d{2}\]\s+(\d+)\s+responses`) + toastRx = regexp.MustCompile(`Error distribution`) +) + +// BenchInfo represents benchmark run info. +type BenchInfo struct { + File os.FileInfo + Path string +} + +// Bench renders a benchmarks to screen. +type Bench struct{} + +// ColorerFunc colors a resource row. +func (Bench) ColorerFunc() ColorerFunc { + return func(ns string, re RowEvent) tcell.Color { + c := tcell.ColorPaleGreen + statusCol := 2 + if strings.TrimSpace(re.Row.Fields[statusCol]) != "pass" { + c = ErrColor + } + return c + } +} + +// Header returns a header row. +func (Bench) Header(ns string) HeaderRow { + return HeaderRow{ + Header{Name: "NAMESPACE", Align: tview.AlignLeft}, + Header{Name: "NAME", Align: tview.AlignLeft}, + Header{Name: "STATUS", Align: tview.AlignLeft}, + Header{Name: "TIME", Align: tview.AlignLeft}, + Header{Name: "REQ/S", Align: tview.AlignRight}, + Header{Name: "2XX", Align: tview.AlignRight}, + Header{Name: "4XX/5XX", Align: tview.AlignRight}, + Header{Name: "REPORT", Align: tview.AlignLeft}, + Header{Name: "AGE", Align: tview.AlignLeft}, + } +} + +// Render renders a K8s resource to screen. +func (b Bench) Render(o interface{}, ns string, r *Row) error { + bench, ok := o.(BenchInfo) + if !ok { + return fmt.Errorf("Expected string, but got %T", o) + } + + data, err := b.readFile(bench.Path) + if err != nil { + return fmt.Errorf("Unable to load bench file %s", bench.Path) + } + + r.Fields = make(Fields, len(b.Header(ns))) + if err := b.initRow(r.Fields, bench.File); err != nil { + return err + } + b.augmentRow(r.Fields, data) + r.ID = bench.Path + + return nil +} + +// Helpers... + +func (Bench) readFile(file string) (string, error) { + data, err := ioutil.ReadFile(file) + if err != nil { + return "", err + } + return string(data), nil +} + +func (Bench) initRow(row Fields, f os.FileInfo) error { + tokens := strings.Split(f.Name(), "_") + if len(tokens) < 2 { + return fmt.Errorf("Invalid file name %s", f.Name()) + } + row[0] = tokens[0] + row[1] = tokens[1] + row[7] = f.Name() + row[8] = time.Since(f.ModTime()).String() + + return nil +} + +func (b Bench) augmentRow(fields Fields, data string) { + if len(data) == 0 { + return + } + + col := 2 + fields[col] = "pass" + mf := toastRx.FindAllStringSubmatch(data, 1) + if len(mf) > 0 { + fields[col] = "fail" + } + col++ + + mt := totalRx.FindAllStringSubmatch(data, 1) + if len(mt) > 0 { + fields[col] = mt[0][1] + } + col++ + + mr := reqRx.FindAllStringSubmatch(data, 1) + if len(mr) > 0 { + fields[col] = mr[0][1] + } + col++ + + ms := okRx.FindAllStringSubmatch(data, -1) + fields[col] = b.countReq(ms) + col++ + + me := errRx.FindAllStringSubmatch(data, -1) + fields[col] = b.countReq(me) +} + +func (Bench) countReq(rr [][]string) string { + if len(rr) == 0 { + return "0" + } + + var sum int + for _, m := range rr { + if m, err := strconv.Atoi(string(m[1])); err == nil { + sum += m + } + } + return asNum(sum) +} + +// AsNumb prints a number with thousand separator. +func asNum(n int) string { + p := message.NewPrinter(language.English) + return p.Sprintf("%d", n) +} diff --git a/internal/render/cj.go b/internal/render/cj.go index 00da72b4..7c192cf6 100644 --- a/internal/render/cj.go +++ b/internal/render/cj.go @@ -30,7 +30,7 @@ func (CronJob) Header(ns string) HeaderRow { Header{Name: "SUSPEND"}, Header{Name: "ACTIVE"}, Header{Name: "LAST_SCHEDULE"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } diff --git a/internal/render/cm.go b/internal/render/cm.go index d8ff5cfc..2f01f85e 100644 --- a/internal/render/cm.go +++ b/internal/render/cm.go @@ -28,7 +28,7 @@ func (ConfigMap) Header(ns string) HeaderRow { return append(h, Header{Name: "NAME"}, Header{Name: "DATA", Align: tview.AlignRight}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } diff --git a/internal/render/colorer_test.go b/internal/render/colorer_test.go new file mode 100644 index 00000000..44cc92aa --- /dev/null +++ b/internal/render/colorer_test.go @@ -0,0 +1,280 @@ +package render + +// BOZO!! +// type ( +// colorerUC struct { +// ns string +// r RowEvent +// e tcell.Color +// } +// colorerUCs []colorerUC +// ) + +// func TestNSColorer(t *testing.T) { +// var ( +// ns = Row{Fields: Fields{"blee", "Active"}} +// term = Row{Fields: Fields{"blee", Terminating}} +// dead = Row{Fields: Fields{"blee", "Inactive"}} +// ) + +// uu := colorerUCs{ +// // Add AllNS +// {"", RowEvent{ +// Kind: EventAdd, +// Row: ns, +// }, +// AddColor}, +// // Mod AllNS +// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor}, +// // MoChange AllNS +// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor}, +// // Bust NS +// {"", RowEvent{Kind: EventUnchanged, Row: term}, ErrColor}, +// // Bust NS +// {"", RowEvent{Kind: EventUnchanged, Row: dead}, ErrColor}, +// } +// for _, u := range uu { +// assert.Equal(t, u.e, nsColorer(u.ns, u.r)) +// } +// } + +// func TestEvColorer(t *testing.T) { +// var ( +// ns = Row{Fields: Fields{"", "blee", "fred", "Normal"}} +// nonNS = Row{Fields: Fields{"", "fred", "Normal"}} +// failNS = Row{Fields: Fields{"", "blee", "fred", "Failed"}} +// failNoNS = Row{Fields: Fields{"", "fred", "Failed"}} +// killNS = Row{Fields: Fields{"", "blee", "fred", "Killing"}} +// killNoNS = Row{Fields: Fields{"", "fred", "Killing"}} +// ) + +// uu := colorerUCs{ +// // Add AllNS +// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor}, +// // Add NS +// {"blee", RowEvent{Kind: EventAdd, Row: nonNS}, AddColor}, +// // Mod AllNS +// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor}, +// // Mod NS +// {"blee", RowEvent{Kind: EventUpdate, Row: nonNS}, ModColor}, +// // Bust AllNS +// {"", RowEvent{Kind: EventUnchanged, Row: failNS}, ErrColor}, +// // Bust NS +// {"blee", RowEvent{Kind: EventUnchanged, Row: failNoNS}, ErrColor}, +// // Bust AllNS +// {"", RowEvent{Kind: EventUnchanged, Row: killNS}, KillColor}, +// // Bust NS +// {"blee", RowEvent{Kind: EventUnchanged, Row: killNoNS}, KillColor}, +// } +// for _, u := range uu { +// assert.Equal(t, u.e, evColorer(u.ns, u.r)) +// } +// } + +// func TestRSColorer(t *testing.T) { +// var ( +// ns = Row{Fields: Fields{"blee", "fred", "1", "1"}} +// noNs = Row{Fields: Fields{"fred", "1", "1"}} +// bustNS = Row{Fields: Fields{"blee", "fred", "1", "0"}} +// bustNoNS = Row{Fields: Fields{"fred", "1", "0"}} +// ) + +// uu := colorerUCs{ +// // Add AllNS +// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor}, +// // Add NS +// {"blee", RowEvent{Kind: EventAdd, Row: noNs}, AddColor}, +// // Bust AllNS +// {"", RowEvent{Kind: EventUnchanged, Row: bustNS}, ErrColor}, +// // Bust NS +// {"blee", RowEvent{Kind: EventUnchanged, Row: bustNoNS}, ErrColor}, +// // Nochange AllNS +// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor}, +// // Nochange NS +// {"blee", RowEvent{Kind: EventUnchanged, Row: noNs}, StdColor}, +// } +// for _, u := range uu { +// assert.Equal(t, u.e, rsColorer(u.ns, u.r)) +// } +// } + +// func TestStsColorer(t *testing.T) { +// var ( +// ns = Row{Fields: Fields{"blee", "fred", "1", "1"}} +// nonNS = Row{Fields: Fields{"fred", "1", "1"}} +// bustNS = Row{Fields: Fields{"blee", "fred", "2", "1"}} +// bustNoNS = Row{Fields: Fields{"fred", "2", "1"}} +// ) + +// uu := colorerUCs{ +// // Add AllNS +// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor}, +// // Add NS +// {"blee", RowEvent{Kind: EventAdd, Row: nonNS}, AddColor}, +// // Mod AllNS +// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor}, +// // Mod NS +// {"blee", RowEvent{Kind: EventUpdate, Row: nonNS}, ModColor}, +// // Bust AllNS +// {"", RowEvent{Kind: EventUnchanged, Row: bustNS}, ErrColor}, +// // Bust NS +// {"blee", RowEvent{Kind: EventUnchanged, Row: bustNoNS}, ErrColor}, +// // Unchanged cool AllNS +// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor}, +// } +// for _, u := range uu { +// assert.Equal(t, u.e, stsColorer(u.ns, u.r)) +// } +// } + +// func TestDpColorer(t *testing.T) { +// var ( +// ns = Row{Fields: Fields{"blee", "fred", "1", "1"}} +// nonNS = Row{Fields: Fields{"fred", "1", "1"}} +// bustNS = Row{Fields: Fields{"blee", "fred", "2", "1"}} +// bustNoNS = Row{Fields: Fields{"fred", "2", "1"}} +// ) + +// uu := colorerUCs{ +// // Add AllNS +// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor}, +// // Add NS +// {"blee", RowEvent{Kind: EventAdd, Row: nonNS}, AddColor}, +// // Mod AllNS +// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor}, +// // Mod NS +// {"blee", RowEvent{Kind: EventUpdate, Row: nonNS}, ModColor}, +// // Unchanged cool +// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor}, +// // Bust AllNS +// {"", RowEvent{Kind: EventUnchanged, Row: bustNS}, ErrColor}, +// // Bust NS +// {"blee", RowEvent{Kind: EventUnchanged, Row: bustNoNS}, ErrColor}, +// } +// for _, u := range uu { +// assert.Equal(t, u.e, dpColorer(u.ns, u.r)) +// } +// } + +// func TestPdbColorer(t *testing.T) { +// var ( +// ns = Row{Fields: Fields{"blee", "fred", "1", "1", "1", "1", "1"}} +// nonNS = Row{Fields: Fields{"fred", "1", "1", "1", "1", "1"}} +// bustNS = Row{Fields: Fields{"blee", "fred", "1", "1", "1", "1", "2"}} +// bustNoNS = Row{Fields: Fields{"fred", "1", "1", "1", "1", "2"}} +// ) + +// uu := colorerUCs{ +// // Add AllNS +// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor}, +// // Add NS +// {"blee", RowEvent{Kind: EventAdd, Row: nonNS}, AddColor}, +// // Mod AllNS +// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor}, +// // Mod NS +// {"blee", RowEvent{Kind: EventUpdate, Row: nonNS}, ModColor}, +// // Unchanged cool +// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor}, +// // Bust AllNS +// {"", RowEvent{Kind: EventUnchanged, Row: bustNS}, ErrColor}, +// // Bust NS +// {"blee", RowEvent{Kind: EventUnchanged, Row: bustNoNS}, ErrColor}, +// } +// for _, u := range uu { +// assert.Equal(t, u.e, pdbColorer(u.ns, u.r)) +// } +// } + +// func TestPVColorer(t *testing.T) { +// var ( +// pv = Row{Fields: Fields{"blee", "1G", "RO", "Duh", "Bound"}} +// bustPv = Row{Fields: Fields{"blee", "1G", "RO", "Duh", "UnBound"}} +// ) + +// uu := colorerUCs{ +// // Add Normal +// {"", RowEvent{Kind: EventAdd, Row: pv}, AddColor}, +// // Unchanged Bound +// {"", RowEvent{Kind: EventUnchanged, Row: pv}, StdColor}, +// // Unchanged Bound +// {"", RowEvent{Kind: EventUnchanged, Row: bustPv}, ErrColor}, +// } +// for _, u := range uu { +// assert.Equal(t, u.e, pvColorer(u.ns, u.r)) +// } +// } + +// func TestPVCColorer(t *testing.T) { +// var ( +// pvc = Row{Fields: Fields{"blee", "fred", "Bound"}} +// bustPvc = Row{Fields: Fields{"blee", "fred", "UnBound"}} +// ) + +// uu := colorerUCs{ +// // Add Normal +// {"", RowEvent{Kind: EventAdd, Row: pvc}, AddColor}, +// // Add Bound +// {"", RowEvent{Kind: EventUnchanged, Row: bustPvc}, ErrColor}, +// } +// for _, u := range uu { +// assert.Equal(t, u.e, pvcColorer(u.ns, u.r)) +// } +// } + +// func TestCtxColorer(t *testing.T) { +// var ( +// ctx = Row{Fields: Fields{"blee"}} +// defCtx = Row{Fields: Fields{"blee*"}} +// ) + +// uu := colorerUCs{ +// // Add Normal +// {"", RowEvent{Kind: EventAdd, Row: ctx}, AddColor}, +// // Add Default +// {"", RowEvent{Kind: EventAdd, Row: defCtx}, AddColor}, +// // Mod Normal +// {"", RowEvent{Kind: EventUpdate, Row: ctx}, ModColor}, +// // Mod Default +// {"", RowEvent{Kind: EventUpdate, Row: defCtx}, ModColor}, +// // Unchanged Normal +// {"", RowEvent{Kind: EventUnchanged, Row: ctx}, StdColor}, +// // Unchanged Default +// {"", RowEvent{Kind: EventUnchanged, Row: defCtx}, HighlightColor}, +// } +// for _, u := range uu { +// assert.Equal(t, u.e, ctxColorer(u.ns, u.r)) +// } +// } + +// func TestPodColorer(t *testing.T) { +// var ( +// nsRow = Row{Fields: Fields{"blee", "fred", "1/1", "Running"}} +// toastNS = Row{Fields: Fields{"blee", "fred", "1/1", "Boom"}} +// notReadyNS = Row{Fields: Fields{"blee", "fred", "0/1", "Boom"}} +// row = Row{Fields: Fields{"fred", "1/1", "Running"}} +// toast = Row{Fields: Fields{"fred", "1/1", "Boom"}} +// notReady = Row{Fields: Fields{"fred", "0/1", "Boom"}} +// ) + +// uu := colorerUCs{ +// // Add allNS +// {"", RowEvent{Kind: EventAdd, Row: nsRow}, AddColor}, +// // Add Namespaced +// {"blee", RowEvent{Kind: EventAdd, Row: row}, AddColor}, +// // Mod AllNS +// {"", RowEvent{Kind: EventUpdate, Row: nsRow}, ModColor}, +// // Mod Namespaced +// {"blee", RowEvent{Kind: EventUpdate, Row: row}, ModColor}, +// // Mod Busted AllNS +// {"", RowEvent{Kind: EventUpdate, Row: toastNS}, ErrColor}, +// // Mod Busted Namespaced +// {"blee", RowEvent{Kind: EventUpdate, Row: toast}, ErrColor}, +// // NotReady AllNS +// {"", RowEvent{Kind: EventUpdate, Row: notReadyNS}, ErrColor}, +// // NotReady Namespaced +// {"blee", RowEvent{Kind: EventUpdate, Row: notReady}, ErrColor}, +// } +// for _, u := range uu { +// assert.Equal(t, u.e, podColorer(u.ns, u.r)) +// } +// } diff --git a/internal/render/co.go b/internal/render/container.go similarity index 81% rename from internal/render/co.go rename to internal/render/container.go index a3049530..dcfd0551 100644 --- a/internal/render/co.go +++ b/internal/render/container.go @@ -7,6 +7,7 @@ import ( "github.com/derailed/k9s/internal/k8s" "github.com/derailed/tview" + "github.com/gdamore/tcell" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" @@ -35,7 +36,29 @@ type Container struct{} // ColorerFunc colors a resource row. func (Container) ColorerFunc() ColorerFunc { - return DefaultColorer + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + + readyCol := 2 + if strings.TrimSpace(r.Row.Fields[readyCol]) == "false" { + c = ErrColor + } + + stateCol := readyCol + 1 + switch strings.TrimSpace(r.Row.Fields[stateCol]) { + case ContainerCreating, PodInitializing: + return AddColor + case Terminating, Initialized: + return HighlightColor + case Completed: + return CompletedColor + case Running: + default: + c = ErrColor + } + + return c + } } // Header returns a header row. @@ -53,12 +76,12 @@ func (Container) Header(ns string) HeaderRow { Header{Name: "%CPU", Align: tview.AlignRight}, Header{Name: "%MEM", Align: tview.AlignRight}, Header{Name: "PORTS"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, } } // Render renders a K8s resource to screen. -func (Container) Render(o interface{}, name string, r *Row) error { +func (c Container) Render(o interface{}, name string, r *Row) error { oo, ok := o.(ContainerWithMetrics) if !ok { return fmt.Errorf("Expected ContainerWithMetrics, but got %T", o) @@ -66,14 +89,15 @@ func (Container) Render(o interface{}, name string, r *Row) error { co, cs := oo.Container(), oo.ContainerStatus() - c, p := gatherMetrics(co, oo.Metrics()) + cur, perc := gatherMetrics(co, oo.Metrics()) ready, state, restarts := "false", MissingValue, "0" if cs != nil { ready, state, restarts = boolToStr(cs.Ready), toState(cs.State), strconv.Itoa(int(cs.RestartCount)) } - fields := make(Fields, 0, len(r.Fields)) - fields = append(fields, + r.ID = co.Name + r.Fields = make(Fields, 0, len(c.Header(AllNamespaces))) + r.Fields = append(r.Fields, co.Name, co.Image, ready, @@ -81,14 +105,13 @@ func (Container) Render(o interface{}, name string, r *Row) error { boolToStr(oo.IsInit()), restarts, probe(co.LivenessProbe)+":"+probe(co.ReadinessProbe), - c.cpu, - c.mem, - p.cpu, - p.mem, + cur.cpu, + cur.mem, + perc.cpu, + perc.mem, toStrPorts(co.Ports), toAge(oo.Age()), ) - r.ID, r.Fields = co.Name, fields return nil } @@ -96,21 +119,6 @@ func (Container) Render(o interface{}, name string, r *Row) error { // ---------------------------------------------------------------------------- // Helpers... -// func findContainer(po v1.Pod, n string) *v1.Container { -// for _, c := range po.Spec.InitContainers { -// if c.Name == n { -// return &c -// } -// } -// for _, c := range po.Spec.Containers { -// if c.Name == n { -// return &c -// } -// } - -// return nil -// } - func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, p metric) { c, p = noMetric(), noMetric() if mx == nil { diff --git a/internal/render/context.go b/internal/render/context.go index 2564f7f7..54f95d21 100644 --- a/internal/render/context.go +++ b/internal/render/context.go @@ -2,8 +2,14 @@ package render import ( "fmt" + "strings" - api "k8s.io/client-go/tools/clientcmd/api" + "github.com/derailed/k9s/internal/k8s" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/clientcmd/api" ) // Context renders a K8s ConfigMap to screen. @@ -11,7 +17,17 @@ type Context struct{} // ColorerFunc colors a resource row. func (Context) ColorerFunc() ColorerFunc { - return DefaultColorer + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + if r.Kind == EventAdd || r.Kind == EventUpdate { + return c + } + if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") { + c = HighlightColor + } + + return c + } } // Header returns a header row. @@ -25,16 +41,58 @@ func (Context) Header(ns string) HeaderRow { } // Render renders a K8s resource to screen. -func (Context) Render(o interface{}, _ string, r *Row) error { - i, ok := o.(*api.Context) +func (c Context) Render(o interface{}, _ string, r *Row) error { + ctx, ok := o.(*NamedContext) if !ok { - return fmt.Errorf("Expected api.Context, but got %T", o) + return fmt.Errorf("Expected NamedContext, but got %T", o) } - r.Fields[0] = r.ID - r.Fields[1] = i.Cluster - r.Fields[2] = i.AuthInfo - r.Fields[3] = i.Namespace + name := ctx.Name + if ctx.IsCurrentContext(ctx.Name) { + name += "(*)" + } + + r.ID = ctx.Name + r.Fields = Fields{ + name, + ctx.Context.Cluster, + ctx.Context.AuthInfo, + ctx.Context.Namespace, + } return nil } + +// Helpers... + +// NamedContext represents a named cluster context. +type NamedContext struct { + Name string + Context *api.Context + config *k8s.Config +} + +// NewNamedContext returns a new named context. +func NewNamedContext(c *k8s.Config, n string, ctx *api.Context) *NamedContext { + return &NamedContext{Name: n, Context: ctx, config: c} +} + +// MustCurrentContextName return the active context name. +func (c *NamedContext) IsCurrentContext(n string) bool { + cl, err := c.config.CurrentContextName() + if err != nil { + log.Fatal().Err(err).Msg("Fetching current context") + return false + } + return cl == n +} + +// GetObjectKind returns a schema object. +func (c *NamedContext) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (c *NamedContext) DeepCopyObject() runtime.Object { + return c +} diff --git a/internal/render/cr.go b/internal/render/cr.go index 649f3563..8fed9612 100644 --- a/internal/render/cr.go +++ b/internal/render/cr.go @@ -20,7 +20,7 @@ func (ClusterRole) ColorerFunc() ColorerFunc { func (ClusterRole) Header(string) HeaderRow { return HeaderRow{ Header{Name: "NAME"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, } } diff --git a/internal/render/crb.go b/internal/render/crb.go index 4f122f48..87480b93 100644 --- a/internal/render/crb.go +++ b/internal/render/crb.go @@ -23,7 +23,7 @@ func (ClusterRoleBinding) Header(string) HeaderRow { Header{Name: "ROLE"}, Header{Name: "KIND"}, Header{Name: "SUBJECTS"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, } } diff --git a/internal/render/crd.go b/internal/render/crd.go index 719e6d56..7f5d4b9a 100644 --- a/internal/render/crd.go +++ b/internal/render/crd.go @@ -1,7 +1,6 @@ package render import ( - "errors" "fmt" "time" @@ -22,7 +21,7 @@ func (CustomResourceDefinition) ColorerFunc() ColorerFunc { func (CustomResourceDefinition) Header(string) HeaderRow { return HeaderRow{ Header{Name: "NAME"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, } } @@ -50,52 +49,53 @@ func (CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error { return nil } -// TypeMeta represents resource type meta data. -type TypeMeta struct { - Name string - Namespaced bool - Group string - Version string - Kind string - Singular string - Plural string - ShortNames []string -} +// BOZO!! +// // TypeMeta represents resource type meta data. +// type TypeMeta struct { +// Name string +// Namespaced bool +// Group string +// Version string +// Kind string +// Singular string +// Plural string +// ShortNames []string +// } -func (CustomResourceDefinition) Meta(o interface{}) (TypeMeta, error) { - var m TypeMeta +// func (CustomResourceDefinition) Meta(o interface{}) (TypeMeta, error) { +// var m TypeMeta - crd, ok := o.(*unstructured.Unstructured) - if !ok { - return m, fmt.Errorf("Expected CustomResourceDefinition, but got %T", o) - } +// crd, ok := o.(*unstructured.Unstructured) +// if !ok { +// return m, fmt.Errorf("Expected CustomResourceDefinition, but got %T", o) +// } - spec, ok := crd.Object["spec"].(map[string]interface{}) - if !ok { - return m, errors.New("missing crd specs") - } +// spec, ok := crd.Object["spec"].(map[string]interface{}) +// if !ok { +// return m, errors.New("missing crd specs") +// } - if meta, ok := crd.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, errors.New("missing crd names") - } - 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 - } - return m, nil -} +// if meta, ok := crd.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, errors.New("missing crd names") +// } +// 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 +// } +// return m, nil +// } -func isNamespaced(scope string) bool { - return scope == "Namespaced" -} +// func isNamespaced(scope string) bool { +// return scope == "Namespaced" +// } diff --git a/internal/render/delta.go b/internal/render/delta.go index 25c25598..d93bec52 100644 --- a/internal/render/delta.go +++ b/internal/render/delta.go @@ -1,5 +1,7 @@ package render +import "github.com/rs/zerolog/log" + // DeltaRow represents a collection of row detlas between old and new row. type DeltaRow []string @@ -7,10 +9,11 @@ type DeltaRow []string func NewDeltaRow(o, n Row) DeltaRow { deltas := make(DeltaRow, len(o.Fields)) // Exclude age col - fields := o.Fields[:len(o.Fields)-1] - for i, v := range fields { - if v != "" && n.Fields[i] != v { - deltas[i] = v + oldFields := o.Fields[:len(o.Fields)-1] + for i, old := range oldFields { + if old != "" && old != n.Fields[i] { + log.Debug().Msgf("OLD VS NEW %q:%q", old, n.Fields[i]) + deltas[i] = old } } @@ -31,3 +34,13 @@ func (d DeltaRow) IsBlank() bool { return true } + +// Clone returns a delta copy. +func (d DeltaRow) Clone() DeltaRow { + res := make(DeltaRow, len(d)) + for i, f := range d { + res[i] = f + } + + return res +} diff --git a/internal/render/dp.go b/internal/render/dp.go index da2598c2..814e6c85 100644 --- a/internal/render/dp.go +++ b/internal/render/dp.go @@ -3,9 +3,13 @@ package render import ( "fmt" "strconv" + "strings" "github.com/derailed/tview" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" ) @@ -19,7 +23,23 @@ func isAllNamespace(ns string) bool { // ColorerFunc colors a resource row. func (Deployment) ColorerFunc() ColorerFunc { - return DefaultColorer + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + if r.Kind == EventAdd || r.Kind == EventUpdate { + return c + } + + markCol := 2 + if ns != AllNamespaces { + markCol = 1 + } + tokens := strings.Split(r.Row.Fields[markCol], "/") + if tokens[0] != tokens[1] { + return ErrColor + } + + return StdColor + } } // Header returns a header row. @@ -31,16 +51,16 @@ func (Deployment) Header(ns string) HeaderRow { return append(h, Header{Name: "NAME"}, - Header{Name: "DESIRED", Align: tview.AlignRight}, - Header{Name: "CURRENT", Align: tview.AlignRight}, + Header{Name: "READY"}, Header{Name: "UP-TO-DATE", Align: tview.AlignRight}, Header{Name: "AVAILABLE", Align: tview.AlignRight}, - Header{Name: "AGE"}, + Header{Name: "SELECTOR"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } // Render renders a K8s resource to screen. -func (Deployment) Render(o interface{}, ns string, r *Row) error { +func (d Deployment) Render(o interface{}, ns string, r *Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("Expected Deployment, but got %T", o) @@ -51,20 +71,31 @@ func (Deployment) Render(o interface{}, ns string, r *Row) error { return err } - fields := make(Fields, 0, len(r.Fields)) + r.ID = MetaFQN(dp.ObjectMeta) + r.Fields = make(Fields, 0, len(d.Header(ns))) if isAllNamespace(ns) { - fields = append(fields, dp.Namespace) + r.Fields = append(r.Fields, dp.Namespace) } - fields = append(fields, + r.Fields = append(r.Fields, dp.Name, - strconv.Itoa(int(*dp.Spec.Replicas)), - strconv.Itoa(int(dp.Status.Replicas)), + strconv.Itoa(int(dp.Status.AvailableReplicas))+"/"+strconv.Itoa(int(*dp.Spec.Replicas)), strconv.Itoa(int(dp.Status.UpdatedReplicas)), strconv.Itoa(int(dp.Status.AvailableReplicas)), + asSelector(dp.Spec.Selector), toAge(dp.ObjectMeta.CreationTimestamp), ) - r.ID, r.Fields = MetaFQN(dp.ObjectMeta), fields - return nil } + +//Helpers... + +func asSelector(s *metav1.LabelSelector) string { + sel, err := metav1.LabelSelectorAsSelector(s) + if err != nil { + log.Error().Err(err).Msg("Selector conversion failed") + return NAValue + } + + return sel.String() +} diff --git a/internal/render/dp_test.go b/internal/render/dp_test.go index c0912a1f..157eb26a 100644 --- a/internal/render/dp_test.go +++ b/internal/render/dp_test.go @@ -13,5 +13,5 @@ func TestDeploymentRender(t *testing.T) { c.Render(load(t, "dp"), "", &r) assert.Equal(t, "icx/icx-db", r.ID) - assert.Equal(t, render.Fields{"icx", "icx-db", "1", "1", "1", "1"}, r.Fields[:6]) + assert.Equal(t, render.Fields{"icx", "icx-db", "1/1", "1", "1", "app=icx-db"}, r.Fields[:6]) } diff --git a/internal/render/ds.go b/internal/render/ds.go index 1975dbcb..b8584b1c 100644 --- a/internal/render/ds.go +++ b/internal/render/ds.go @@ -3,8 +3,10 @@ package render import ( "fmt" "strconv" + "strings" "github.com/derailed/tview" + "github.com/gdamore/tcell" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -15,7 +17,22 @@ type DaemonSet struct{} // ColorerFunc colors a resource row. func (DaemonSet) ColorerFunc() ColorerFunc { - return DefaultColorer + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + if r.Kind == EventAdd || r.Kind == EventUpdate { + return c + } + + markCol := 2 + if ns != AllNamespaces { + markCol = 1 + } + if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+2]) { + return ErrColor + } + + return StdColor + } } // Header returns a header row. @@ -33,12 +50,12 @@ func (DaemonSet) Header(ns string) HeaderRow { Header{Name: "UP-TO-DATE", Align: tview.AlignRight}, Header{Name: "AVAILABLE", Align: tview.AlignRight}, Header{Name: "NODE_SELECTOR"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } // Render renders a K8s resource to screen. -func (DaemonSet) Render(o interface{}, ns string, r *Row) error { +func (d DaemonSet) Render(o interface{}, ns string, r *Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("Expected DaemonSet, but got %T", o) @@ -49,11 +66,12 @@ func (DaemonSet) Render(o interface{}, ns string, r *Row) error { return err } - fields := make(Fields, 0, len(r.Fields)) + r.ID = MetaFQN(ds.ObjectMeta) + r.Fields = make(Fields, 0, len(d.Header(ns))) if isAllNamespace(ns) { - fields = append(fields, ds.Namespace) + r.Fields = append(r.Fields, ds.Namespace) } - fields = append(fields, + r.Fields = append(r.Fields, ds.Name, strconv.Itoa(int(ds.Status.DesiredNumberScheduled)), strconv.Itoa(int(ds.Status.CurrentNumberScheduled)), @@ -63,7 +81,6 @@ func (DaemonSet) Render(o interface{}, ns string, r *Row) error { mapToStr(ds.Spec.Template.Spec.NodeSelector), toAge(ds.ObjectMeta.CreationTimestamp), ) - r.ID, r.Fields = MetaFQN(ds.ObjectMeta), fields return nil } diff --git a/internal/render/ep.go b/internal/render/ep.go index 22f5fe17..dbf609b1 100644 --- a/internal/render/ep.go +++ b/internal/render/ep.go @@ -28,7 +28,7 @@ func (Endpoints) Header(ns string) HeaderRow { return append(h, Header{Name: "NAME"}, Header{Name: "ENDPOINTS"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } diff --git a/internal/render/ev.go b/internal/render/ev.go index f334afea..648900d5 100644 --- a/internal/render/ev.go +++ b/internal/render/ev.go @@ -3,8 +3,10 @@ package render import ( "fmt" "strconv" + "strings" "github.com/derailed/tview" + "github.com/gdamore/tcell" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -15,7 +17,22 @@ type Event struct{} // ColorerFunc colors a resource row. func (Event) ColorerFunc() ColorerFunc { - return DefaultColorer + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + + markCol := 3 + if ns != AllNamespaces { + markCol = 2 + } + switch strings.TrimSpace(r.Row.Fields[markCol]) { + case "Failed": + c = ErrColor + case "Killing": + c = KillColor + } + + return c + } } // Header returns a header rbw. @@ -31,7 +48,7 @@ func (Event) Header(ns string) HeaderRow { Header{Name: "SOURCE"}, Header{Name: "COUNT", Align: tview.AlignRight}, Header{Name: "MESSAGE"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } diff --git a/internal/render/event.go b/internal/render/event.go index ff56164d..50b9c69d 100644 --- a/internal/render/event.go +++ b/internal/render/event.go @@ -1,9 +1,11 @@ package render import ( + "fmt" "sort" "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" ) const ( @@ -53,26 +55,63 @@ func NewDeltaRowEvent(row Row, delta DeltaRow) RowEvent { } } +// Clone returns a rowevent deep copy. +func (r RowEvent) Clone() RowEvent { + return RowEvent{ + Kind: r.Kind, + Row: r.Row.Clone(), + Deltas: r.Deltas.Clone(), + } +} + +// Clone returns a rowevents deep copy. +func (rr RowEvents) Clone() RowEvents { + res := make(RowEvents, len(rr)) + for i, r := range rr { + res[i] = r.Clone() + } + + return res +} + +// Upsert add or update a row if it exists. +func (rr RowEvents) Upsert(e RowEvent) RowEvents { + if idx, ok := rr.FindIndex(e.Row.ID); ok { + rr[idx] = e + } else { + rr = append(rr, e) + } + return rr +} + // Delete removes an element by id. -func (re RowEvents) Delete(id string) RowEvents { - idx, ok := re.FindIndex(id) +func (rr RowEvents) Delete(id string) RowEvents { + idx, ok := rr.FindIndex(id) if !ok { - return re + return rr } if idx == 0 { - return re[1:] + return rr[1:] } - if idx == len(re)-1 { - return re[:len(re)-1] + if idx == len(rr)-1 { + return rr[:len(rr)-1] } - return append(re[:idx], re[idx+1:]...) + return append(rr[:idx], rr[idx+1:]...) +} + +// Clear delete all row events +func (rr RowEvents) Clear() RowEvents { + for _, e := range rr { + rr = rr.Delete(e.Row.ID) + } + return rr } // FindIndex locates a row index by id. Returns false is not found. -func (re RowEvents) FindIndex(id string) (int, bool) { - for i, e := range re { +func (rr RowEvents) FindIndex(id string) (int, bool) { + for i, e := range rr { if e.Row.ID == id { return i, true } @@ -82,9 +121,28 @@ func (re RowEvents) FindIndex(id string) (int, bool) { } // Sort rows based on column index and order. -func (re RowEvents) Sort(ns string, col int, asc bool) { - t := RowEventSorter{NS: ns, Events: re, Index: col, Asc: asc} +func (rr RowEvents) Sort(ns string, col int, asc bool) { + t := RowEventSorter{NS: ns, Events: rr, Index: col, Asc: asc} sort.Sort(t) + + gg, kk := map[string][]string{}, make(StringSet, 0, len(rr)) + for _, e := range rr { + g := e.Row.Fields[col] + kk = kk.Add(g) + if ss, ok := gg[g]; ok { + gg[g] = append(ss, e.Row.ID) + } else { + gg[g] = []string{e.Row.ID} + } + } + + ids := make([]string, 0, len(rr)) + for _, k := range kk { + sort.StringSlice(gg[k]).Sort() + ids = append(ids, gg[k]...) + } + s := IdSorter{Ids: ids, Events: rr} + sort.Sort(s) } // ---------------------------------------------------------------------------- @@ -107,17 +165,39 @@ func (r RowEventSorter) Swap(i, j int) { func (r RowEventSorter) Less(i, j int) bool { f1, f2 := r.Events[i].Row.Fields, r.Events[j].Row.Fields + return Less(r.Asc, f1[r.Index], f2[r.Index]) +} - var col int - if r.NS == "" { - col++ - } - if col >= len(f1) || col >= len(f2) { - return false - } - n1, n2 := f1[col], f2[col] +// ---------------------------------------------------------------------------- - return Less(r.Asc, f1[r.Index]+n1, f2[r.Index]+n2) +// IdSorter sorts row events by a given id. +type IdSorter struct { + Ids []string + Events RowEvents +} + +func (s IdSorter) Len() int { + return len(s.Events) +} + +func (s IdSorter) Swap(i, j int) { + s.Events[i], s.Events[j] = s.Events[j], s.Events[i] +} + +func (s IdSorter) Less(i, j int) bool { + id1, id2 := s.Events[i].Row.ID, s.Events[j].Row.ID + i1, i2 := findIndex(s.Ids, id1), findIndex(s.Ids, id2) + return i1 < i2 +} + +func findIndex(ss []string, s string) int { + for i := range ss { + if ss[i] == s { + return i + } + } + log.Error().Err(fmt.Errorf("Doh! index not found for %s", s)) + return -1 } // ---------------------------------------------------------------------------- @@ -140,11 +220,11 @@ var ( ) // ColorerFunc represents a resource row colorer. -type ColorerFunc func(ns string, evt ResEvent, r Row) tcell.Color +type ColorerFunc func(ns string, evt RowEvent) tcell.Color // DefaultColorer set the default table row colors. -func DefaultColorer(ns string, evt ResEvent, r Row) tcell.Color { - switch evt { +func DefaultColorer(ns string, evt RowEvent) tcell.Color { + switch evt.Kind { case EventAdd: return AddColor case EventUpdate: @@ -155,3 +235,25 @@ func DefaultColorer(ns string, evt ResEvent, r Row) tcell.Color { return StdColor } } + +type StringSet []string + +func (ss StringSet) Add(item string) StringSet { + if ss.In(item) { + return ss + } + return append(ss, item) +} + +func (ss StringSet) In(item string) bool { + return ss.indexOf(item) >= 0 +} + +func (ss StringSet) indexOf(item string) int { + for i, s := range ss { + if s == item { + return i + } + } + return -1 +} diff --git a/internal/render/event_test.go b/internal/render/event_test.go index 45ba2fc6..56c65693 100644 --- a/internal/render/event_test.go +++ b/internal/render/event_test.go @@ -52,7 +52,7 @@ func TestDefaultColorer(t *testing.T) { for k, u := range uu { t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, render.DefaultColorer("", u.k, render.Row{})) + assert.Equal(t, u.e, render.DefaultColorer("", render.RowEvent{})) }) } } diff --git a/internal/render/forward.go b/internal/render/forward.go new file mode 100644 index 00000000..8222e387 --- /dev/null +++ b/internal/render/forward.go @@ -0,0 +1,110 @@ +package render + +import ( + "fmt" + "strings" + + "github.com/gdamore/tcell" +) + +// Forwarder represents a port forwarder. +type Forwarder interface { + // Path returns a resource FQN. + Path() string + + // Container returns a container name. + Container() string + + // Ports returns container exposed ports. + Ports() []string + + // Active returns forwarder current state. + Active() bool + + // Age returns forwarder age. + Age() string +} + +// Forward renders a portforwards to screen. +type Forward struct{} + +// ColorerFunc colors a resource row. +func (Forward) ColorerFunc() ColorerFunc { + return func(ns string, re RowEvent) tcell.Color { + return tcell.ColorSkyblue + } +} + +// Header returns a header row. +func (Forward) Header(ns string) HeaderRow { + return HeaderRow{ + Header{Name: "NAMESPACE"}, + Header{Name: "NAME"}, + Header{Name: "CONTAINER"}, + Header{Name: "PORTS"}, + Header{Name: "URL"}, + Header{Name: "C"}, + Header{Name: "N"}, + Header{Name: "AGE", Decorator: ageDecorator}, + } +} + +// Render renders a K8s resource to screen. +func (f Forward) Render(o interface{}, gvr string, r *Row) error { + pf, ok := o.(PortForwarder) + if !ok { + return fmt.Errorf("expecting a portforward but got %T", o) + } + + ports := strings.Split(pf.Ports()[0], ":") + ns, na := Namespaced(pf.Path()) + + r.ID = pf.Path() + r.Fields = Fields{ + ns, + na, + pf.Container(), + strings.Join(pf.Ports(), ","), + UrlFor(pf.Host(), pf.HttpPath(), ports[0]), + asNum(pf.C()), + asNum(pf.N()), + pf.Age(), + } + + return nil +} + +// Helpers... + +type PortForwarder interface { + Forwarder + BenchConfigurator +} + +type BenchConfigurators map[string]BenchConfigurator + +type BenchConfigurator interface { + // C returns the number of concurent connections. + C() int + + // N returns the number of requests. + N() int + + // Host returns the forward host address. + Host() string + + // Path returns the http path. + HttpPath() string +} + +// UrlFor computes fq url for a given benchmark configuration. +func UrlFor(host, path, port string) string { + if host == "" { + host = "localhost" + } + if path == "" { + path = "/" + } + + return "http://" + host + ":" + port + path +} diff --git a/internal/render/generic.go b/internal/render/generic.go new file mode 100644 index 00000000..45c67928 --- /dev/null +++ b/internal/render/generic.go @@ -0,0 +1,99 @@ +package render + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/rs/zerolog/log" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" +) + +// Generic renders a generic resource to screen. +type Generic struct { + table *metav1beta1.Table +} + +func (g *Generic) SetTable(t *metav1beta1.Table) { + g.table = t +} + +// ColorerFunc colors a resource row. +func (Generic) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header row. +func (g *Generic) Header(ns string) HeaderRow { + h := make(HeaderRow, 0, len(g.table.ColumnDefinitions)) + + if ns == "" { + h = append(h, Header{Name: "NAMESPACE"}) + } + for _, c := range g.table.ColumnDefinitions { + h = append(h, Header{Name: strings.ToUpper(c.Name)}) + } + + log.Debug().Msgf("Generic Header %#v", h) + return h +} + +// Render renders a K8s resource to screen. +func (g *Generic) Render(o interface{}, ns string, r *Row) error { + row, ok := o.(*metav1beta1.TableRow) + if !ok { + return fmt.Errorf("expecting a table but got %#v", o) + } + + count := len(row.Cells) + if ns == AllNamespaces { + count++ + } + r.ID, ok = row.Cells[0].(string) + if !ok { + return fmt.Errorf("expecting row id to be a string but got %#v", row.Cells[0]) + } + r.Fields = make(Fields, count) + + var index int + if ns == AllNamespaces { + rns, err := extractNamespace(row.Object.Raw) + if err != nil { + return err + } + r.Fields[index] = rns + r.ID = FQN(rns, r.ID) + index++ + } + + for _, c := range row.Cells { + r.Fields[index] = fmt.Sprintf("%v", c) + index++ + } + log.Debug().Msgf("Generic row %#v", r) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func extractNamespace(raw []byte) (string, error) { + var obj map[string]interface{} + err := json.Unmarshal(raw, &obj) + if err != nil { + return "", err + } + + meta, ok := obj["metadata"].(map[string]interface{}) + if !ok { + return "", errors.New("no metadata found on generic resource") + } + ns, ok := meta["namespace"].(string) + if !ok { + return "", errors.New("invalid namespace found on generic metadata") + } + + return ns, nil +} diff --git a/internal/render/helpers.go b/internal/render/helpers.go index 4ec5f3b7..074cc900 100644 --- a/internal/render/helpers.go +++ b/internal/render/helpers.go @@ -152,13 +152,13 @@ func boolToStr(b bool) string { } func toAge(timestamp metav1.Time) string { - return toAgeHuman(time.Since(timestamp.Time).String()) + return time.Since(timestamp.Time).String() } func toAgeHuman(s string) string { d, err := time.ParseDuration(s) if err != nil { - return "" + return NAValue } return duration.HumanDuration(d) diff --git a/internal/render/hpa.go b/internal/render/hpa.go index 7a5e4237..a8ae77e2 100644 --- a/internal/render/hpa.go +++ b/internal/render/hpa.go @@ -32,7 +32,7 @@ func (HorizontalPodAutoscaler) Header(ns string) HeaderRow { Header{Name: "MINPODS", Align: tview.AlignRight}, Header{Name: "MAXPODS", Align: tview.AlignRight}, Header{Name: "REPLICAS", Align: tview.AlignRight}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } diff --git a/internal/render/ing.go b/internal/render/ing.go index 8c4f25b2..fae2f5ef 100644 --- a/internal/render/ing.go +++ b/internal/render/ing.go @@ -30,7 +30,7 @@ func (Ingress) Header(ns string) HeaderRow { Header{Name: "HOSTS"}, Header{Name: "ADDRESS"}, Header{Name: "PORT"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } diff --git a/internal/render/job.go b/internal/render/job.go index 12dc6a94..6fe545c0 100644 --- a/internal/render/job.go +++ b/internal/render/job.go @@ -34,7 +34,7 @@ func (Job) Header(ns string) HeaderRow { Header{Name: "DURATION"}, Header{Name: "CONTAINERS"}, Header{Name: "IMAGES"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } diff --git a/internal/render/no.go b/internal/render/node.go similarity index 99% rename from internal/render/no.go rename to internal/render/node.go index 4953faa1..fcbcdb49 100644 --- a/internal/render/no.go +++ b/internal/render/node.go @@ -50,7 +50,7 @@ func (Node) Header(_ string) HeaderRow { Header{Name: "%MEM", Align: tview.AlignRight}, Header{Name: "ACPU", Align: tview.AlignRight}, Header{Name: "AMEM", Align: tview.AlignRight}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, } } diff --git a/internal/render/no_test.go b/internal/render/node_test.go similarity index 100% rename from internal/render/no_test.go rename to internal/render/node_test.go diff --git a/internal/render/np.go b/internal/render/np.go index 6923519d..9123ccb7 100644 --- a/internal/render/np.go +++ b/internal/render/np.go @@ -33,7 +33,7 @@ func (NetworkPolicy) Header(ns string) HeaderRow { Header{Name: "EGR-SELECTOR"}, Header{Name: "EGR-PORTS"}, Header{Name: "EGR-BLOCK"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } diff --git a/internal/render/ns.go b/internal/render/ns.go index 8044a078..2c5caa26 100644 --- a/internal/render/ns.go +++ b/internal/render/ns.go @@ -2,7 +2,9 @@ package render import ( "fmt" + "strings" + "github.com/gdamore/tcell" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -13,7 +15,22 @@ type Namespace struct{} // ColorerFunc colors a resource row. func (Namespace) ColorerFunc() ColorerFunc { - return DefaultColorer + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + + if r.Kind == EventAdd || r.Kind == EventUpdate { + return c + } + switch strings.TrimSpace(r.Row.Fields[1]) { + case "Inactive", Terminating: + c = ErrColor + } + if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") { + c = HighlightColor + } + + return c + } } // Header returns a header rbw. @@ -21,7 +38,7 @@ func (Namespace) Header(string) HeaderRow { return HeaderRow{ Header{Name: "NAME"}, Header{Name: "STATUS"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, } } diff --git a/internal/render/pdb.go b/internal/render/pdb.go index 3d9c2877..7833e037 100644 --- a/internal/render/pdb.go +++ b/internal/render/pdb.go @@ -3,8 +3,10 @@ package render import ( "fmt" "strconv" + "strings" "github.com/derailed/tview" + "github.com/gdamore/tcell" v1beta1 "k8s.io/api/policy/v1beta1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -16,7 +18,23 @@ type PodDisruptionBudget struct{} // ColorerFunc colors a resource row. func (PodDisruptionBudget) ColorerFunc() ColorerFunc { - return DefaultColorer + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + if r.Kind == EventAdd || r.Kind == EventUpdate { + return c + } + + markCol := 5 + if ns != AllNamespaces { + markCol = 4 + } + if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) { + return ErrColor + } + + return StdColor + } + } // Header returns a header row. @@ -34,7 +52,7 @@ func (PodDisruptionBudget) Header(ns string) HeaderRow { Header{Name: "CURRENT", Align: tview.AlignRight}, Header{Name: "DESIRED", Align: tview.AlignRight}, Header{Name: "EXPECTED", Align: tview.AlignRight}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } diff --git a/internal/render/po.go b/internal/render/pod.go similarity index 87% rename from internal/render/po.go rename to internal/render/pod.go index 954a5ee8..2183d145 100644 --- a/internal/render/po.go +++ b/internal/render/pod.go @@ -28,9 +28,9 @@ type PodWithMetrics interface { type Pod struct{} // ColorerFunc colors a resource row. -func (Pod) ColorerFunc() ColorerFunc { - return func(ns string, evt ResEvent, r Row) tcell.Color { - c := DefaultColorer(ns, evt, r) +func (p Pod) ColorerFunc() ColorerFunc { + return func(ns string, re RowEvent) tcell.Color { + c := DefaultColorer(ns, re) readyCol := 2 if len(ns) != 0 { @@ -38,29 +38,39 @@ func (Pod) ColorerFunc() ColorerFunc { } statusCol := readyCol + 1 - tokens := strings.Split(strings.TrimSpace(r.Fields[readyCol]), "/") - if len(tokens) == 2 && (tokens[0] == "0" || tokens[0] != tokens[1]) { - if strings.TrimSpace(r.Fields[statusCol]) != "Completed" { - c = ErrColor - } - } + ready, status := strings.TrimSpace(re.Row.Fields[readyCol]), strings.TrimSpace(re.Row.Fields[statusCol]) + c = p.checkReadyCol(ready, status, c) - switch strings.TrimSpace(r.Fields[statusCol]) { - case "ContainerCreating", "PodInitializing": + switch status { + case ContainerCreating, PodInitializing: return AddColor - case "Terminating", "Initialized": + case Initialized: return HighlightColor - case "Completed": + case Completed: return CompletedColor - case "Running": + case Running: + case Terminating: + return KillColor default: - c = ErrColor + return ErrColor } return c } } +func (Pod) checkReadyCol(readyCol, statusCol string, c tcell.Color) tcell.Color { + if statusCol == "Completed" { + return c + } + + tokens := strings.Split(readyCol, "/") + if len(tokens) == 2 && (tokens[0] == "0" || tokens[0] != tokens[1]) { + return ErrColor + } + return c +} + // Header returns a header row. func (Pod) Header(ns string) HeaderRow { var h HeaderRow @@ -80,7 +90,7 @@ func (Pod) Header(ns string) HeaderRow { Header{Name: "IP"}, Header{Name: "NODE"}, Header{Name: "QOS"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } @@ -94,7 +104,7 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error { var po v1.Pod err := runtime.DefaultUnstructuredConverter.FromUnstructured(oo.Object().(*unstructured.Unstructured).Object, &po) if err != nil { - log.Error().Err(err).Msg("Converting Pod") + log.Error().Err(err).Msg("Expecting a pod resource") return err } @@ -102,11 +112,12 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error { cr, _, rc := p.statuses(ss) c, perc := p.gatherPodMX(&po, oo.Metrics()) - fields := make(Fields, 0, len(r.Fields)) + r.ID = MetaFQN(po.ObjectMeta) + r.Fields = make(Fields, 0, len(p.Header(ns))) if isAllNamespace(ns) { - fields = append(fields, po.Namespace) + r.Fields = append(r.Fields, po.Namespace) } - fields = append(fields, + r.Fields = append(r.Fields, po.ObjectMeta.Name, strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)), p.phase(&po), @@ -120,11 +131,8 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error { p.mapQOS(po.Status.QOSClass), toAge(po.ObjectMeta.CreationTimestamp), ) - r.ID = MetaFQN(po.ObjectMeta) - r.Fields = fields return nil - } // ---------------------------------------------------------------------------- diff --git a/internal/render/po_test.go b/internal/render/pod_test.go similarity index 100% rename from internal/render/po_test.go rename to internal/render/pod_test.go diff --git a/internal/render/policy.go b/internal/render/policy.go new file mode 100644 index 00000000..16728634 --- /dev/null +++ b/internal/render/policy.go @@ -0,0 +1,49 @@ +package render + +import ( + "github.com/gdamore/tcell" +) + +func rbacVerbHeader() HeaderRow { + return HeaderRow{ + Header{Name: "GET "}, + Header{Name: "LIST "}, + Header{Name: "WATCH "}, + Header{Name: "CREATE"}, + Header{Name: "PATCH "}, + Header{Name: "UPDATE"}, + Header{Name: "DELETE"}, + Header{Name: "DLIST "}, + Header{Name: "EXTRAS"}, + } +} + +// Policy renders a rbac policy to screen. +type Policy struct{} + +// ColorerFunc colors a resource row. +func (Policy) ColorerFunc() ColorerFunc { + return func(ns string, re RowEvent) tcell.Color { + return tcell.ColorMediumSpringGreen + } +} + +// Header returns a header row. +func (Policy) Header(ns string) HeaderRow { + h := HeaderRow{ + Header{Name: "NAMESPACE"}, + Header{Name: "NAME"}, + Header{Name: "API GROUP"}, + Header{Name: "BINDING"}, + } + + return append(h, rbacVerbHeader()...) +} + +// Render renders a K8s resource to screen. +func (Policy) Render(o interface{}, gvr string, r *Row) error { + panic("NYI") + return nil +} + +// Helpers... diff --git a/internal/render/pv.go b/internal/render/pv.go index 1323ebd2..a7f62f23 100644 --- a/internal/render/pv.go +++ b/internal/render/pv.go @@ -5,6 +5,7 @@ import ( "path" "strings" + "github.com/gdamore/tcell" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -15,7 +16,25 @@ type PersistentVolume struct{} // ColorerFunc colors a resource row. func (PersistentVolume) ColorerFunc() ColorerFunc { - return DefaultColorer + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + if r.Kind == EventAdd || r.Kind == EventUpdate { + return c + } + + status := strings.TrimSpace(r.Row.Fields[4]) + switch status { + case "Bound": + c = StdColor + case "Available": + c = tcell.ColorYellow + default: + c = ErrColor + } + + return c + } + } // Header returns a header rbw. @@ -29,7 +48,7 @@ func (PersistentVolume) Header(string) HeaderRow { Header{Name: "CLAIM"}, Header{Name: "STORAGECLASS"}, Header{Name: "REASON"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, } } diff --git a/internal/render/pvc.go b/internal/render/pvc.go index 8a686005..fd4ab6c3 100644 --- a/internal/render/pvc.go +++ b/internal/render/pvc.go @@ -2,7 +2,9 @@ package render import ( "fmt" + "strings" + "github.com/gdamore/tcell" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -13,7 +15,24 @@ type PersistentVolumeClaim struct{} // ColorerFunc colors a resource row. func (PersistentVolumeClaim) ColorerFunc() ColorerFunc { - return DefaultColorer + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + if r.Kind == EventAdd || r.Kind == EventUpdate { + return c + } + + markCol := 2 + if ns != AllNamespaces { + markCol = 1 + } + + if strings.TrimSpace(r.Row.Fields[markCol]) != "Bound" { + c = ErrColor + } + + return c + } + } // Header returns a header rbw. @@ -30,7 +49,7 @@ func (PersistentVolumeClaim) Header(ns string) HeaderRow { Header{Name: "CAPACITY"}, Header{Name: "ACCESS MODES"}, Header{Name: "STORAGECLASS"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } diff --git a/internal/render/rb.go b/internal/render/rb.go index 93d08e46..53a80db2 100644 --- a/internal/render/rb.go +++ b/internal/render/rb.go @@ -29,7 +29,7 @@ func (RoleBinding) Header(ns string) HeaderRow { Header{Name: "ROLE"}, Header{Name: "KIND"}, Header{Name: "SUBJECTS"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } diff --git a/internal/render/rbac.go b/internal/render/rbac.go new file mode 100644 index 00000000..a24351d4 --- /dev/null +++ b/internal/render/rbac.go @@ -0,0 +1,33 @@ +package render + +import ( + "github.com/gdamore/tcell" +) + +// Rbac renders a rbac to screen. +type Rbac struct{} + +// ColorerFunc colors a resource row. +func (Rbac) ColorerFunc() ColorerFunc { + return func(ns string, re RowEvent) tcell.Color { + return tcell.ColorMediumSpringGreen + } +} + +// Header returns a header row. +func (Rbac) Header(ns string) HeaderRow { + h := HeaderRow{ + Header{Name: "NAME"}, + Header{Name: "API GROUP"}, + } + + return append(h, rbacVerbHeader()...) +} + +// Render renders a K8s resource to screen. +func (Rbac) Render(o interface{}, gvr string, r *Row) error { + panic("NYI") + return nil +} + +// Helpers... diff --git a/internal/render/ro.go b/internal/render/ro.go index 4f99da42..7292f3a4 100644 --- a/internal/render/ro.go +++ b/internal/render/ro.go @@ -25,7 +25,7 @@ func (Role) Header(ns string) HeaderRow { return append(h, Header{Name: "NAME"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } diff --git a/internal/render/row.go b/internal/render/row.go index d88df738..742e401a 100644 --- a/internal/render/row.go +++ b/internal/render/row.go @@ -21,13 +21,18 @@ type Rows []Row // Header represent a table header type Header struct { - Name string - Align int + Name string + Align int + Decorator DecoratorFunc } // HeaderRow represents a table header. type HeaderRow []Header +func (h HeaderRow) AgeCol(col int) bool { + return col == len(h)-1 +} + // RowSorter sorts rows. type RowSorter struct { Rows Rows @@ -35,6 +40,22 @@ type RowSorter struct { Asc bool } +func (r Row) Clone() Row { + return Row{ + ID: r.ID, + Fields: r.Fields.Clone(), + } +} + +func (f Fields) Clone() Fields { + res := make(Fields, len(f)) + for i, f := range f { + res[i] = f + } + + return res +} + // Delete removes an element by id. func (rr Rows) Delete(id string) Rows { idx, ok := rr.Find(id) @@ -57,6 +78,16 @@ func NewRow(cols int) Row { return Row{Fields: make([]string, cols)} } +func (rr Rows) Upsert(r Row) Rows { + idx, ok := rr.Find(r.ID) + if !ok { + return append(rr, r) + } + rr[idx] = r + + return rr +} + // Find locates a row by id. Retturns false is not found. func (rr Rows) Find(id string) (int, bool) { for i, r := range rr { diff --git a/internal/render/rs.go b/internal/render/rs.go index 1a952540..1c945756 100644 --- a/internal/render/rs.go +++ b/internal/render/rs.go @@ -3,8 +3,10 @@ package render import ( "fmt" "strconv" + "strings" "github.com/derailed/tview" + "github.com/gdamore/tcell" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -15,7 +17,23 @@ type ReplicaSet struct{} // ColorerFunc colors a resource row. func (ReplicaSet) ColorerFunc() ColorerFunc { - return DefaultColorer + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + if r.Kind == EventAdd || r.Kind == EventUpdate { + return c + } + + markCol := 2 + if ns != AllNamespaces { + markCol = 1 + } + if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) { + return ErrColor + } + + return StdColor + } + } // Header returns a header row. @@ -30,7 +48,7 @@ func (ReplicaSet) Header(ns string) HeaderRow { Header{Name: "DESIRED", Align: tview.AlignRight}, Header{Name: "CURRENT", Align: tview.AlignRight}, Header{Name: "READY", Align: tview.AlignRight}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } diff --git a/internal/render/sa.go b/internal/render/sa.go index 40e14fc9..f1045bf3 100644 --- a/internal/render/sa.go +++ b/internal/render/sa.go @@ -27,7 +27,7 @@ func (ServiceAccount) Header(ns string) HeaderRow { return append(h, Header{Name: "NAME"}, Header{Name: "SECRET"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } diff --git a/internal/render/screen_dump.go b/internal/render/screen_dump.go new file mode 100644 index 00000000..89d053ab --- /dev/null +++ b/internal/render/screen_dump.go @@ -0,0 +1,61 @@ +package render + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "github.com/gdamore/tcell" +) + +// ScreenDump renders a screendumps to screen. +type ScreenDump struct{} + +// ColorerFunc colors a resource row. +func (ScreenDump) ColorerFunc() ColorerFunc { + return func(ns string, re RowEvent) tcell.Color { + return tcell.ColorNavajoWhite + } +} + +type DecoratorFunc func(string) string + +var ageDecorator = func(a string) string { + return toAgeHuman(a) +} + +// Header returns a header row. +func (ScreenDump) Header(ns string) HeaderRow { + return HeaderRow{ + Header{Name: "NAME"}, + Header{Name: "AGE", Decorator: ageDecorator}, + } +} + +// Render renders a K8s resource to screen. +func (b ScreenDump) Render(o interface{}, ns string, r *Row) error { + f, ok := o.(ScreenDumper) + if !ok { + return fmt.Errorf("Expected string, but got %T", o) + } + + r.ID = filepath.Join(f.GetDir(), f.GetFile().Name()) + r.Fields = Fields{ + f.GetFile().Name(), + timeToAge(f.GetFile().ModTime()), + } + + return nil +} + +// Helpers... + +func timeToAge(timestamp time.Time) string { + return time.Since(timestamp).String() +} + +type ScreenDumper interface { + GetFile() os.FileInfo + GetDir() string +} diff --git a/internal/render/secret.go b/internal/render/secret.go index e833a5c9..8818488b 100644 --- a/internal/render/secret.go +++ b/internal/render/secret.go @@ -29,7 +29,7 @@ func (Secret) Header(ns string) HeaderRow { Header{Name: "NAME"}, Header{Name: "TYPE"}, Header{Name: "DATA", Align: tview.AlignRight}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } diff --git a/internal/render/sts.go b/internal/render/sts.go new file mode 100644 index 00000000..1199bfd7 --- /dev/null +++ b/internal/render/sts.go @@ -0,0 +1,81 @@ +package render + +import ( + "fmt" + "strconv" + "strings" + + "github.com/gdamore/tcell" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// StatefulSet renders a K8s StatefulSet to screen. +type StatefulSet struct{} + +// ColorerFunc colors a resource row. +func (StatefulSet) ColorerFunc() ColorerFunc { + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + if r.Kind == EventAdd || r.Kind == EventUpdate { + return c + } + + readyCol := 2 + if ns != AllNamespaces { + readyCol-- + } + tokens := strings.Split(strings.TrimSpace(r.Row.Fields[readyCol]), "/") + curr, des := tokens[0], tokens[1] + if curr != des { + return ErrColor + } + + return StdColor + } +} + +// Header returns a header row. +func (StatefulSet) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "READY"}, + Header{Name: "SELECTOR"}, + Header{Name: "SERVICE"}, + Header{Name: "AGE", Decorator: ageDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (s StatefulSet) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected StatefulSet, but got %T", o) + } + var sts appsv1.StatefulSet + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sts) + if err != nil { + return err + } + + r.ID = MetaFQN(sts.ObjectMeta) + r.Fields = make(Fields, 0, len(s.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, sts.Namespace) + } + r.Fields = append(r.Fields, + sts.Name, + strconv.Itoa(int(sts.Status.Replicas))+"/"+strconv.Itoa(int(*sts.Spec.Replicas)), + asSelector(sts.Spec.Selector), + na(sts.Spec.ServiceName), + toAge(sts.ObjectMeta.CreationTimestamp), + ) + + return nil +} diff --git a/internal/render/subject.go b/internal/render/subject.go new file mode 100644 index 00000000..9c5d701e --- /dev/null +++ b/internal/render/subject.go @@ -0,0 +1,30 @@ +package render + +import ( + "github.com/gdamore/tcell" +) + +// Subject renders a rbac to screen. +type Subject struct{} + +// ColorerFunc colors a resource row. +func (Subject) ColorerFunc() ColorerFunc { + return func(ns string, re RowEvent) tcell.Color { + return tcell.ColorMediumSpringGreen + } +} + +// Header returns a header row. +func (Subject) Header(ns string) HeaderRow { + return HeaderRow{ + Header{Name: "NAME"}, + Header{Name: "KIND"}, + Header{Name: "FIRST LOCATION"}, + } +} + +// Render renders a K8s resource to screen. +func (Subject) Render(o interface{}, gvr string, r *Row) error { + panic("NYI") + return nil +} diff --git a/internal/render/svc.go b/internal/render/svc.go index fe9b0971..d2fb5aa4 100644 --- a/internal/render/svc.go +++ b/internal/render/svc.go @@ -33,7 +33,7 @@ func (Service) Header(ns string) HeaderRow { Header{Name: "EXTERNAL-IP"}, Header{Name: "SELECTOR"}, Header{Name: "PORTS"}, - Header{Name: "AGE"}, + Header{Name: "AGE", Decorator: ageDecorator}, ) } diff --git a/internal/render/table.go b/internal/render/table.go new file mode 100644 index 00000000..08de9276 --- /dev/null +++ b/internal/render/table.go @@ -0,0 +1,16 @@ +package render + +// TableData tracks a K8s resource for tabular display. +type TableData struct { + Header HeaderRow + RowEvents RowEvents + Namespace string +} + +func (t TableData) Clone() TableData { + return TableData{ + Header: t.Header, + RowEvents: t.RowEvents.Clone(), + Namespace: t.Namespace, + } +} diff --git a/internal/render/types.go b/internal/render/types.go new file mode 100644 index 00000000..5676ea74 --- /dev/null +++ b/internal/render/types.go @@ -0,0 +1,35 @@ +package render + +const ( + // AllNamespaces represents all namespaces. + AllNamespaces = "" + + // NamespaceAll represent the all namespace. + NamespaceAll = "all" + + // ClusterWide represents a cluster resources. + ClusterWide = "-" + + // NonResource represents a custom resource. + NonResource = "*" +) + +const ( + // Terminating represents a pod terminating status. + Terminating = "Terminating" + + // Running represents a pod running status. + Running = "Running" + + // Initialized represents a pod intialized status. + Initialized = "Initialized" + + // Completed represents a pod completed status. + Completed = "Completed" + + // ContainerCreating represents a pod container status. + ContainerCreating = "ContainerCreating" + + // PodInitializing represents a pod initializing status. + PodInitializing = "PodInitializing" +) diff --git a/internal/resource/base.go b/internal/resource/base.go index a067845a..be25df2b 100644 --- a/internal/resource/base.go +++ b/internal/resource/base.go @@ -4,8 +4,10 @@ import ( "bytes" "context" "errors" + "fmt" "path" + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/watch" "github.com/rs/zerolog/log" @@ -162,7 +164,10 @@ func (*Base) marshalObject(o runtime.Object) (string, error) { } func (b *Base) podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts LogOptions) error { - f := ctx.Value(IKey("factory")).(*watch.Factory) + f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) + if !ok { + return fmt.Errorf("no factory in context for pod logs") + } ls, err := metav1.ParseToLabelSelector(toSelector(sel)) if err != nil { diff --git a/internal/resource/custom.go b/internal/resource/custom.go index 6ad053da..45e87e2c 100644 --- a/internal/resource/custom.go +++ b/internal/resource/custom.go @@ -81,6 +81,7 @@ func (r *Custom) New(i interface{}) (Columnar, error) { // Marshal resource to yaml. func (r *Custom) Marshal(path string) (string, error) { + panic("NYI") ns, n := Namespaced(path) i, err := r.Resource.Get(ns, n) if err != nil { diff --git a/internal/resource/custom_test.go b/internal/resource/custom_test.go index 616f4e22..75c3386b 100644 --- a/internal/resource/custom_test.go +++ b/internal/resource/custom_test.go @@ -44,18 +44,19 @@ func TestCustomFields(t *testing.T) { assert.Equal(t, "a", r[0]) } -func TestCustomMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sCustomTable(), nil) +// BOZO!! +// func TestCustomMarshal(t *testing.T) { +// mc := NewMockConnection() +// mr := NewMockCruder() +// m.When(mr.Get("blee", "fred")).ThenReturn(k8sCustomTable(), nil) - cm := NewCustomWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") +// cm := NewCustomWithArgs(mc, mr) +// ma, err := cm.Marshal("blee/fred") +// mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, customYaml(), ma) -} +// assert.Nil(t, err) +// assert.Equal(t, customYaml(), ma) +// } func TestCustomMarshalWithUnstructured(t *testing.T) { mc := NewMockConnection() diff --git a/internal/resource/ds.go b/internal/resource/ds.go index f268e40b..516a734a 100644 --- a/internal/resource/ds.go +++ b/internal/resource/ds.go @@ -1,128 +1,140 @@ package resource -import ( - "context" - "errors" - "fmt" - "strconv" +// import ( +// "context" +// "errors" +// "fmt" +// "strconv" - "github.com/derailed/k9s/internal/k8s" - appsv1 "k8s.io/api/apps/v1" -) +// "github.com/derailed/k9s/internal" +// "github.com/derailed/k9s/internal/k8s" +// "github.com/derailed/k9s/internal/watch" +// appsv1 "k8s.io/api/apps/v1" +// "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +// "k8s.io/apimachinery/pkg/labels" +// "k8s.io/apimachinery/pkg/runtime" +// ) -// Compile time checks to ensure type satisfies interface -var _ Restartable = (*DaemonSet)(nil) +// // Compile time checks to ensure type satisfies interface +// var _ Restartable = (*DaemonSet)(nil) -// DaemonSet tracks a kubernetes resource. -type DaemonSet struct { - *Base - instance *appsv1.DaemonSet -} +// // DaemonSet tracks a kubernetes resource. +// type DaemonSet struct { +// *Base +// instance *appsv1.DaemonSet +// } -// NewDaemonSetList returns a new resource list. -func NewDaemonSetList(c Connection, ns string) List { - return NewList( - ns, - "ds", - NewDaemonSet(c), - AllVerbsAccess|DescribeAccess, - ) -} +// // NewDaemonSetList returns a new resource list. +// func NewDaemonSetList(c Connection, ns string) List { +// return NewList( +// ns, +// "ds", +// NewDaemonSet(c), +// AllVerbsAccess|DescribeAccess, +// ) +// } -// NewDaemonSet instantiates a new DaemonSet. -func NewDaemonSet(c Connection) *DaemonSet { - ds := &DaemonSet{&Base{Connection: c, Resource: k8s.NewDaemonSet(c)}, nil} - ds.Factory = ds +// // NewDaemonSet instantiates a new DaemonSet. +// func NewDaemonSet(c Connection) *DaemonSet { +// ds := &DaemonSet{&Base{Connection: c, Resource: k8s.NewDaemonSet(c)}, nil} +// ds.Factory = ds - return ds -} +// return ds +// } -// New builds a new DaemonSet instance from a k8s resource. -func (r *DaemonSet) New(i interface{}) (Columnar, error) { - c := NewDaemonSet(r.Connection) - switch instance := i.(type) { - case *appsv1.DaemonSet: - c.instance = instance - case appsv1.DaemonSet: - c.instance = &instance - default: - return nil, fmt.Errorf("Expecting DaemonSet but got %T", instance) - } - c.path = c.namespacedName(c.instance.ObjectMeta) +// // New builds a new DaemonSet instance from a k8s resource. +// func (r *DaemonSet) New(i interface{}) (Columnar, error) { +// c := NewDaemonSet(r.Connection) +// switch instance := i.(type) { +// case *appsv1.DaemonSet: +// c.instance = instance +// case appsv1.DaemonSet: +// c.instance = &instance +// default: +// return nil, fmt.Errorf("Expecting DaemonSet but got %T", instance) +// } +// c.path = c.namespacedName(c.instance.ObjectMeta) - return c, nil -} +// return c, nil +// } -// Marshal resource to yaml. -func (r *DaemonSet) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } +// // Marshal resource to yaml. +// func (r *DaemonSet) Marshal(path string) (string, error) { +// ns, n := Namespaced(path) +// i, err := r.Resource.Get(ns, n) +// if err != nil { +// return "", err +// } - ds, ok := i.(*appsv1.DaemonSet) - if !ok { - return "", errors.New("expecting ds resource") - } - ds.TypeMeta.APIVersion = "apps/v1" - ds.TypeMeta.Kind = "DaemonSet" +// ds, ok := i.(*appsv1.DaemonSet) +// if !ok { +// return "", errors.New("expecting ds resource") +// } +// ds.TypeMeta.APIVersion = "apps/v1" +// ds.TypeMeta.Kind = "DaemonSet" - return r.marshalObject(ds) -} +// return r.marshalObject(ds) +// } -// Logs tail logs for all pods represented by this DaemonSet. -func (r *DaemonSet) Logs(ctx context.Context, c chan<- string, opts LogOptions) error { - instance, err := r.Resource.Get(opts.Namespace, opts.Name) - if err != nil { - return err - } +// // Logs tail logs for all pods represented by this DaemonSet. +// func (r *DaemonSet) Logs(ctx context.Context, c chan<- string, opts LogOptions) error { +// f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) +// if !ok { +// return errors.New("no factory in context for pod logs") +// } - ds, ok := instance.(*appsv1.DaemonSet) - if !ok { - return errors.New("expecting ds resource") - } - if ds.Spec.Selector == nil || len(ds.Spec.Selector.MatchLabels) == 0 { - return fmt.Errorf("No valid selector found on daemonset %s", opts.FQN()) - } +// o, err := f.Get(opts.Namespace, "apps/v1/daemonsets", opts.Name, labels.Everything()) +// if err != nil { +// return err +// } - return r.podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts) -} +// var ds appsv1.DaemonSet +// err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) +// if err != nil { +// return errors.New("expecting daemonset resource") +// } -// Header return resource header. -func (*DaemonSet) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - hh = append(hh, "NAME", "DESIRED", "CURRENT", "READY", "UP-TO-DATE") - hh = append(hh, "AVAILABLE", "NODE_SELECTOR", "AGE") +// if ds.Spec.Selector == nil || len(ds.Spec.Selector.MatchLabels) == 0 { +// return fmt.Errorf("No valid selector found on daemonset %s", opts.FQN()) +// } - return hh -} +// return r.podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts) +// } -// Fields retrieves displayable fields. -func (r *DaemonSet) Fields(ns string) Row { - ff := make([]string, 0, len(r.Header(ns))) +// // Header return resource header. +// func (*DaemonSet) Header(ns string) Row { +// hh := Row{} +// if ns == AllNamespaces { +// hh = append(hh, "NAMESPACE") +// } +// hh = append(hh, "NAME", "DESIRED", "CURRENT", "READY", "UP-TO-DATE") +// hh = append(hh, "AVAILABLE", "NODE_SELECTOR", "AGE") - i := r.instance - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } +// return hh +// } - return append(ff, - i.Name, - strconv.Itoa(int(i.Status.DesiredNumberScheduled)), - strconv.Itoa(int(i.Status.CurrentNumberScheduled)), - strconv.Itoa(int(i.Status.NumberReady)), - strconv.Itoa(int(i.Status.UpdatedNumberScheduled)), - strconv.Itoa(int(i.Status.NumberAvailable)), - mapToStr(i.Spec.Template.Spec.NodeSelector), - toAge(i.ObjectMeta.CreationTimestamp), - ) -} +// // Fields retrieves displayable fields. +// func (r *DaemonSet) Fields(ns string) Row { +// ff := make([]string, 0, len(r.Header(ns))) -// Restart the rollout of the specified resource. -func (r *DaemonSet) Restart(ns, n string) error { - return r.Resource.(Restartable).Restart(ns, n) -} +// i := r.instance +// if ns == AllNamespaces { +// ff = append(ff, i.Namespace) +// } + +// return append(ff, +// i.Name, +// strconv.Itoa(int(i.Status.DesiredNumberScheduled)), +// strconv.Itoa(int(i.Status.CurrentNumberScheduled)), +// strconv.Itoa(int(i.Status.NumberReady)), +// strconv.Itoa(int(i.Status.UpdatedNumberScheduled)), +// strconv.Itoa(int(i.Status.NumberAvailable)), +// mapToStr(i.Spec.Template.Spec.NodeSelector), +// toAge(i.ObjectMeta.CreationTimestamp), +// ) +// } + +// // Restart the rollout of the specified resource. +// func (r *DaemonSet) Restart(ns, n string) error { +// return r.Resource.(Restartable).Restart(ns, n) +// } diff --git a/internal/resource/list.go b/internal/resource/list.go index d71b93ce..94724b81 100644 --- a/internal/resource/list.go +++ b/internal/resource/list.go @@ -2,14 +2,9 @@ package resource import ( "context" - "errors" - "fmt" "reflect" - "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/render" - w "github.com/derailed/k9s/internal/watch" - "github.com/rs/zerolog/log" "k8s.io/apimachinery/pkg/watch" ) @@ -67,6 +62,11 @@ func (l *list) Namespaced() bool { return l.namespace != NotNamespaced } +// IsClusterWide returns true if the resource is cluster scoped. +func (l *list) IsCluterWide() bool { + return l.namespace == render.ClusterWide +} + // AllNamespaces checks if this resource spans all namespaces. func (l *list) AllNamespaces() bool { return l.namespace == AllNamespaces @@ -114,14 +114,15 @@ func (l *list) Resource() Resource { } // Cache tracks previous resource state. -func (l *list) Data() TableData { - return TableData{ +func (l *list) Data() render.TableData { + return render.TableData{ Header: l.header, RowEvents: l.cache, Namespace: l.namespace, } } +// BOZO!! // func (l *list) load(informer *wa.Informer, ns string) (Columnars, error) { // rr, err := informer.List(l.name, ns, metav1.ListOptions{ // FieldSelector: l.fieldSelector, @@ -178,39 +179,53 @@ func (l *list) Data() TableData { // return res, nil // } -type ContextKey string - -const KeyFactory ContextKey = "factory" - // Reconcile previous vs current state and emits delta events. -func (l *list) Reconcile(ctx context.Context, gvr, path string) error { - log.Debug().Msgf("Reconcile %q in path %q", gvr, path) - ns := l.namespace - if path != "" { - ns = path - } +func (l *list) Reconcile(ctx context.Context, gvr string) error { + panic("NYI") + // path := ctx.Value(internal.KeySelection).(string) - factory, ok := ctx.Value(KeyFactory).(*w.Factory) - if !ok { - return errors.New("no factory found in context") - } - m, ok := model.Registry[gvr] - if !ok { - panic(fmt.Errorf("no model registered for %q", gvr)) - } - m.Model.Init(ns, gvr, factory) - oo, err := m.Model.List(path) - if err != nil { - panic(err) - } - items := make(render.Rows, cap(oo)) - if err := m.Model.Hydrate(oo, items, m.Renderer); err != nil { - panic(err) - } - l.update(ns, items) - l.header = m.Renderer.Header(ns) + // log.Debug().Msgf("Reconcile %q in path %q", gvr, path) + // ns := l.namespace + // if path != "" { + // ns = path + // } - return nil + // factory, ok := ctx.Value(internal.KeyFactory).(*w.Factory) + // if !ok { + // return errors.New("no factory found in context") + // } + // m, ok := model.Registry[gvr] + // if !ok { + // log.Warn().Msgf("Resource %s not found in registry. Going generic!", gvr) + // m = model.ResourceMeta{ + // Model: &model.Generic{}, + // Renderer: &render.Generic{}, + // } + // } + // if m.Model == nil { + // m.Model = &model.Resource{} + // } + // m.Model.Init(ns, gvr, factory) + + // if l.labelSelector != "" { + // ctx = context.WithValue(ctx, internal.KeyLabels, l.labelSelector) + // } + // if l.fieldSelector != "" { + // ctx = context.WithValue(ctx, internal.KeyFields, l.fieldSelector) + // } + // oo, err := m.Model.List(ctx) + // if err != nil { + // panic(err) + // } + // log.Debug().Msgf("Model returned [%d] items", len(oo)) + // rows := make(render.Rows, len(oo)) + // if err := m.Model.Hydrate(oo, rows, m.Renderer); err != nil { + // panic(err) + // } + // l.update(ns, rows) + // l.header = m.Renderer.Header(ns) + + // return nil } func (l *list) update(ns string, rows render.Rows) { diff --git a/internal/resource/no.go b/internal/resource/node.go similarity index 100% rename from internal/resource/no.go rename to internal/resource/node.go diff --git a/internal/resource/no_int_test.go b/internal/resource/node_int_test.go similarity index 100% rename from internal/resource/no_int_test.go rename to internal/resource/node_int_test.go diff --git a/internal/resource/no_test.go b/internal/resource/node_test.go similarity index 100% rename from internal/resource/no_test.go rename to internal/resource/node_test.go diff --git a/internal/resource/pod.go b/internal/resource/pod.go index d6e69079..8ebdac8a 100644 --- a/internal/resource/pod.go +++ b/internal/resource/pod.go @@ -10,20 +10,26 @@ import ( "sync/atomic" "time" + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/color" "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/resource" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) const ( defaultTimeout = 1 * time.Second - Terminating = "Terminating" - Running = "Running" - Initialized = "Initialized" - Completed = "Completed" + // BOZO!! + Terminating = "Terminating" + Running = "Running" + Initialized = "Initialized" + Completed = "Completed" ) // Pod that can be displayed in a table and interacted with. @@ -83,6 +89,7 @@ func (r *Pod) SetPodMetrics(m *mv1beta1.PodMetrics) { // Marshal resource to yaml. func (r *Pod) Marshal(path string) (string, error) { + panic("Should not be called") ns, n := Namespaced(path) i, err := r.Resource.Get(ns, n) if err != nil { @@ -107,43 +114,43 @@ func (r *Pod) Containers(path string, includeInit bool) ([]string, error) { // PodLogs tail logs for all containers in a running Pod. func (r *Pod) PodLogs(ctx context.Context, c chan<- string, opts LogOptions) error { + fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) + if !ok { + return errors.New("Expecting an informer") + } + ns, n := Namespaced(opts.FQN()) + o, err := fac.Get(ns, "v1/pods", n, labels.Everything()) + if err != nil { + return err + } + + var po v1.Pod + if runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil { + return err + } + opts.Color = asColor(po.Name) + if len(po.Spec.InitContainers)+len(po.Spec.Containers) == 1 { + opts.SingleContainer = true + } + + for _, co := range po.Spec.InitContainers { + opts.Container = co.Name + if err := r.Logs(ctx, c, opts); err != nil { + return err + } + } + rcos := r.loggableContainers(po.Status) + for _, co := range po.Spec.Containers { + if in(rcos, co.Name) { + opts.Container = co.Name + if err := r.Logs(ctx, c, opts); err != nil { + log.Error().Err(err).Msgf("Getting logs for %s failed", co.Name) + return err + } + } + } + return nil - // inf, ok := ctx.Value(IKey("informer")).(*watch.Informer) - // if !ok { - // return errors.New("Expecting an informer") - // } - // p, err := inf.Get(watch.PodIndex, opts.FQN(), metav1.GetOptions{}) - // if err != nil { - // return err - // } - - // po, ok := p.(*v1.Pod) - // if !ok { - // return errors.New("Expecting a pod resource") - // } - // opts.Color = asColor(po.Name) - // if len(po.Spec.InitContainers)+len(po.Spec.Containers) == 1 { - // opts.SingleContainer = true - // } - - // for _, co := range po.Spec.InitContainers { - // opts.Container = co.Name - // if err := r.Logs(ctx, c, opts); err != nil { - // return err - // } - // } - // rcos := r.loggableContainers(po.Status) - // for _, co := range po.Spec.Containers { - // if in(rcos, co.Name) { - // opts.Container = co.Name - // if err := r.Logs(ctx, c, opts); err != nil { - // log.Error().Err(err).Msgf("Getting logs for %s failed", co.Name) - // return err - // } - // } - // } - - // return nil } // Logs tails a given container logs diff --git a/internal/resource/pod_test.go b/internal/resource/pod_test.go index dbc99327..5f91c7bc 100644 --- a/internal/resource/pod_test.go +++ b/internal/resource/pod_test.go @@ -5,7 +5,6 @@ import ( "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -88,19 +87,20 @@ func TestPodGatherMX(t *testing.T) { } } -func TestPodMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(makePod(), nil) - mx := NewMockMetricsServer() +// BOZO!! +// func TestPodMarshal(t *testing.T) { +// mc := NewMockConnection() +// mr := NewMockCruder() +// m.When(mr.Get("blee", "fred")).ThenReturn(makePod(), nil) +// mx := NewMockMetricsServer() - cm := NewPodWithArgs(mc, mr, mx) - ma, err := cm.Marshal("blee/fred") +// cm := NewPodWithArgs(mc, mr, mx) +// ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, poYaml(), ma) -} +// mr.VerifyWasCalledOnce().Get("blee", "fred") +// assert.Nil(t, err) +// assert.Equal(t, poYaml(), ma) +// } // BOZO!! // func TestPodListData(t *testing.T) { diff --git a/internal/resource/types.go b/internal/resource/types.go index 50abf94f..2f54be4f 100644 --- a/internal/resource/types.go +++ b/internal/resource/types.go @@ -36,9 +36,6 @@ const ( // Connection represents an apiserver connection. type Connection k8s.Connection -// RowEvents tracks resource update events. -type RowEvents map[string]*RowEvent - // TypeName captures resource names. type TypeName struct { Singular string @@ -57,22 +54,15 @@ type TypeMeta struct { Kind string } -// TableData tracks a K8s resource for tabular display. -type TableData struct { - Header render.HeaderRow - RowEvents render.RowEvents - Namespace string -} - // List protocol to display and update a collection of resources type List interface { - Data() TableData + Data() render.TableData Resource() Resource Namespaced() bool AllNamespaces() bool GetNamespace() string SetNamespace(string) - Reconcile(ctx context.Context, gvr, path string) error + Reconcile(ctx context.Context, gvr string) error GetName() string Access(flag int) bool GetAccess() int @@ -144,9 +134,6 @@ type Factory interface { New(interface{}) (Columnar, error) } -// IKey informer context key. -type IKey string - // Containers represents a resource that supports containers. type Containers interface { Containers(path string, includeInit bool) ([]string, error) diff --git a/internal/ui/colorer.go b/internal/ui/colorer.go index 6656f474..011cc2a6 100644 --- a/internal/ui/colorer.go +++ b/internal/ui/colorer.go @@ -1,38 +1,39 @@ package ui -import ( - "github.com/derailed/k9s/internal/render" - "github.com/gdamore/tcell" -) +// BOZO!! +// import ( +// "github.com/derailed/k9s/internal/render" +// "github.com/gdamore/tcell" +// ) -var ( - // ModColor row modified color. - ModColor tcell.Color - // AddColor row added color. - AddColor tcell.Color - // ErrColor row err color. - ErrColor tcell.Color - // StdColor row default color. - StdColor tcell.Color - // HighlightColor row highlight color. - HighlightColor tcell.Color - // KillColor row deleted color. - KillColor tcell.Color - // CompletedColor row completed color. - CompletedColor tcell.Color -) +// var ( +// // ModColor row modified color. +// ModColor tcell.Color +// // AddColor row added color. +// AddColor tcell.Color +// // ErrColor row err color. +// ErrColor tcell.Color +// // StdColor row default color. +// StdColor tcell.Color +// // HighlightColor row highlight color. +// HighlightColor tcell.Color +// // KillColor row deleted color. +// KillColor tcell.Color +// // CompletedColor row completed color. +// CompletedColor tcell.Color +// ) -// DefaultColorer set the default table row colors. -func DefaultColorer(ns string, r render.RowEvent) tcell.Color { - c := StdColor - switch r.Kind { - case render.EventAdd: - c = AddColor - case render.EventUpdate: - c = ModColor - case render.EventDelete: - c = KillColor - } +// // DefaultColorer set the default table row colors. +// func DefaultColorer(ns string, r render.RowEvent) tcell.Color { +// c := StdColor +// switch r.Kind { +// case render.EventAdd: +// c = AddColor +// case render.EventUpdate: +// c = ModColor +// case render.EventDelete: +// c = KillColor +// } - return c -} +// return c +// } diff --git a/internal/ui/colorer_test.go b/internal/ui/colorer_test.go index c2c40c08..095582ff 100644 --- a/internal/ui/colorer_test.go +++ b/internal/ui/colorer_test.go @@ -1,29 +1,30 @@ package ui_test -import ( - "testing" +// BOZO!! +// import ( +// "testing" - "github.com/derailed/k9s/internal/render" - "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" - "github.com/stretchr/testify/assert" -) +// "github.com/derailed/k9s/internal/render" +// "github.com/derailed/k9s/internal/ui" +// "github.com/gdamore/tcell" +// "github.com/stretchr/testify/assert" +// ) -func TestDefaultColorer(t *testing.T) { - uu := map[string]struct { - re render.RowEvent - e tcell.Color - }{ - "default": {render.RowEvent{}, ui.StdColor}, - "add": {render.RowEvent{Kind: render.EventAdd}, ui.AddColor}, - "delete": {render.RowEvent{Kind: render.EventDelete}, ui.KillColor}, - "update": {render.RowEvent{Kind: render.EventUpdate}, ui.ModColor}, - } +// func TestDefaultColorer(t *testing.T) { +// uu := map[string]struct { +// re render.RowEvent +// e tcell.Color +// }{ +// "default": {render.RowEvent{}, ui.StdColor}, +// "add": {render.RowEvent{Kind: render.EventAdd}, ui.AddColor}, +// "delete": {render.RowEvent{Kind: render.EventDelete}, ui.KillColor}, +// "update": {render.RowEvent{Kind: render.EventUpdate}, ui.ModColor}, +// } - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, ui.DefaultColorer("", u.re)) - }) - } -} +// for k := range uu { +// u := uu[k] +// t.Run(k, func(t *testing.T) { +// assert.Equal(t, u.e, ui.DefaultColorer("", u.re)) +// }) +// } +// } diff --git a/internal/ui/config.go b/internal/ui/config.go index 0d98100c..b3432415 100644 --- a/internal/ui/config.go +++ b/internal/ui/config.go @@ -5,6 +5,7 @@ import ( "path/filepath" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/render" "github.com/derailed/tview" "github.com/fsnotify/fsnotify" "github.com/rs/zerolog/log" @@ -78,10 +79,10 @@ func (c *Configurator) RefreshStyles() { } c.Styles.Update() - StdColor = config.AsColor(c.Styles.Frame().Status.NewColor) - AddColor = config.AsColor(c.Styles.Frame().Status.AddColor) - ModColor = config.AsColor(c.Styles.Frame().Status.ModifyColor) - ErrColor = config.AsColor(c.Styles.Frame().Status.ErrorColor) - HighlightColor = config.AsColor(c.Styles.Frame().Status.HighlightColor) - CompletedColor = config.AsColor(c.Styles.Frame().Status.CompletedColor) + render.StdColor = config.AsColor(c.Styles.Frame().Status.NewColor) + render.AddColor = config.AsColor(c.Styles.Frame().Status.AddColor) + render.ModColor = config.AsColor(c.Styles.Frame().Status.ModifyColor) + render.ErrColor = config.AsColor(c.Styles.Frame().Status.ErrorColor) + render.HighlightColor = config.AsColor(c.Styles.Frame().Status.HighlightColor) + render.CompletedColor = config.AsColor(c.Styles.Frame().Status.CompletedColor) } diff --git a/internal/ui/dialog/confirm.go b/internal/ui/dialog/confirm.go index 9e256cf9..315496c2 100644 --- a/internal/ui/dialog/confirm.go +++ b/internal/ui/dialog/confirm.go @@ -31,7 +31,7 @@ func ShowConfirm(pages *ui.Pages, title, msg string, ack confirmFunc, cancel can cancel() }) - modal := tview.NewModalForm(title, f) + modal := tview.NewModalForm(" <"+title+"> ", f) modal.SetText(msg) modal.SetDoneFunc(func(int, string) { dismissConfirm(pages) diff --git a/internal/ui/padding.go b/internal/ui/padding.go index 3e3baf4e..0bfe5602 100644 --- a/internal/ui/padding.go +++ b/internal/ui/padding.go @@ -2,10 +2,12 @@ package ui import ( "strings" + "time" "unicode" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/resource" + "k8s.io/apimachinery/pkg/util/duration" ) // MaxyPad tracks uniform column padding. @@ -25,8 +27,11 @@ func ComputeMaxColumns(pads MaxyPad, sortCol int, header render.HeaderRow, ee re var row int for _, e := range ee { for index, field := range e.Row.Fields { + if header.AgeCol(index) { + field = toAgeHuman(field) + } width := len(field) + colPadding - if width > pads[index] { + if index < len(pads) && width > pads[index] { pads[index] = width } } @@ -56,3 +61,12 @@ func Pad(s string, width int) string { return s + strings.Repeat(" ", width-len(s)) } + +func toAgeHuman(s string) string { + d, err := time.ParseDuration(s) + if err != nil { + return "n/a" + } + + return duration.HumanDuration(d) +} diff --git a/internal/ui/padding_test.go b/internal/ui/padding_test.go index 50dd1a4c..526ce0d8 100644 --- a/internal/ui/padding_test.go +++ b/internal/ui/padding_test.go @@ -4,18 +4,17 @@ import ( "testing" "github.com/derailed/k9s/internal/render" - "github.com/derailed/k9s/internal/resource" "github.com/stretchr/testify/assert" ) func TestMaxColumn(t *testing.T) { uu := map[string]struct { - t resource.TableData + t render.TableData s int e MaxyPad }{ "ascii col 0": { - resource.TableData{ + render.TableData{ Header: render.HeaderRow{render.Header{Name: "A"}, render.Header{Name: "B"}}, RowEvents: render.RowEvents{ render.RowEvent{ @@ -34,7 +33,7 @@ func TestMaxColumn(t *testing.T) { MaxyPad{6, 6}, }, "ascii col 1": { - resource.TableData{ + render.TableData{ Header: render.HeaderRow{render.Header{Name: "A"}, render.Header{Name: "B"}}, RowEvents: render.RowEvents{ render.RowEvent{ @@ -53,7 +52,7 @@ func TestMaxColumn(t *testing.T) { MaxyPad{6, 6}, }, "non_ascii": { - resource.TableData{ + render.TableData{ Header: render.HeaderRow{render.Header{Name: "A"}, render.Header{Name: "B"}}, RowEvents: render.RowEvents{ render.RowEvent{ @@ -114,7 +113,7 @@ func TestPad(t *testing.T) { } func BenchmarkMaxColumn(b *testing.B) { - table := resource.TableData{ + table := render.TableData{ Header: render.HeaderRow{render.Header{Name: "A"}, render.Header{Name: "B"}}, RowEvents: render.RowEvents{ render.RowEvent{ diff --git a/internal/ui/select_table.go b/internal/ui/select_table.go index c6b2039b..3ae62ab3 100644 --- a/internal/ui/select_table.go +++ b/internal/ui/select_table.go @@ -1,9 +1,9 @@ package ui import ( - "path" "strings" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/resource" "github.com/derailed/tview" "github.com/gdamore/tcell" @@ -13,7 +13,7 @@ import ( type SelectTable struct { *tview.Table - ActiveNS string + Data render.TableData selectedItem string selectedRow int selectedFn func(string) string @@ -89,20 +89,15 @@ func (s *SelectTable) GetRow() resource.Row { } func (s *SelectTable) updateSelectedItem(r int) { - if r <= 0 || s.GetCell(r, 0) == nil { + if r <= 0 || len(s.Data.RowEvents) == 0 { s.selectedItem = "" return } - col0 := TrimCell(s, r, 0) - switch s.ActiveNS { - case resource.NotNamespaced: - s.selectedItem = col0 - case resource.AllNamespace, resource.AllNamespaces: - s.selectedItem = path.Join(col0, TrimCell(s, r, 1)) - default: - s.selectedItem = path.Join(s.ActiveNS, col0) + if r-1 >= len(s.Data.RowEvents) { + return } + s.selectedItem = s.Data.RowEvents[r-1].Row.ID } // SelectRow select a given row by index. @@ -139,6 +134,18 @@ func (s *SelectTable) selChanged(r, c int) { } } +// ClearMarks delete all marked items. +func (s *SelectTable) ClearMarks() { + for k := range s.marks { + delete(s.marks, k) + } +} + +// DeleteMark delete a marked item. +func (s *SelectTable) DeleteMark(k string) { + delete(s.marks, k) +} + // ToggleMark toggles marked row func (s *SelectTable) ToggleMark() { s.marks[s.GetSelectedItem()] = !s.marks[s.GetSelectedItem()] diff --git a/internal/ui/table.go b/internal/ui/table.go index 8be15b54..d31b9463 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -7,7 +7,6 @@ import ( "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/render" - "github.com/derailed/k9s/internal/resource" "github.com/derailed/tview" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" @@ -17,6 +16,9 @@ type ( // ColorerFunc represents a row colorer. ColorerFunc func(ns string, evt render.RowEvent) tcell.Color + // DecorateFunc represents a row decorator. + DecorateFunc func(render.TableData) render.TableData + // SelectedRowFunc a table selection callback. SelectedRowFunc func(r, c int) ) @@ -25,15 +27,15 @@ type ( type Table struct { *SelectTable - actions KeyActions - BaseTitle string - Path string - Data resource.TableData - cmdBuff *CmdBuff - styles *config.Styles - sortCol SortColumn - sortFn SortFn - colorerFn ColorerFunc + actions KeyActions + BaseTitle string + Path string + cmdBuff *CmdBuff + styles *config.Styles + sortCol SortColumn + sortFn SortFn + colorerFn render.ColorerFunc + decorateFn DecorateFunc } // NewTable returns a new table view. @@ -66,7 +68,6 @@ func (t *Table) Init(ctx context.Context) { config.AsColor(t.styles.Table().CursorColor), tcell.AttrBold, ) - t.SetSelectionChangedFunc(t.selChanged) t.SetInputCapture(t.keyboard) } @@ -83,6 +84,10 @@ func (t *Table) SendKey(evt *tcell.EventKey) { func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey { key := evt.Key() + if key == tcell.KeyUp || key == tcell.KeyDown { + return evt + } + if key == tcell.KeyRune { if t.SearchBuff().IsActive() { t.SearchBuff().Add(evt.Rune()) @@ -107,16 +112,20 @@ func (t *Table) Hints() model.MenuHints { } // GetFilteredData fetch filtered tabular data. -func (t *Table) GetFilteredData() resource.TableData { +func (t *Table) GetFilteredData() render.TableData { return t.filtered() } +// SetDecorateFn specifies the default row decorator. +func (t *Table) SetDecorateFn(f DecorateFunc) { + t.decorateFn = f +} + // SetColorerFn specifies the default colorer. -func (t *Table) SetColorerFn(f ColorerFunc) { +func (t *Table) SetColorerFn(f render.ColorerFunc) { if f == nil { return } - log.Debug().Msgf("Setting Colorer %#v", f) t.colorerFn = f } @@ -126,10 +135,14 @@ func (t *Table) SetSortCol(index, count int, asc bool) { } // Update table content. -func (t *Table) Update(data resource.TableData) { +func (t *Table) Update(data render.TableData) { t.Data = data + if t.decorateFn != nil { + data = t.decorateFn(data) + } + if t.cmdBuff.Empty() { - t.doUpdate(t.Data) + t.doUpdate(data) } else { t.doUpdate(t.filtered()) } @@ -137,9 +150,8 @@ func (t *Table) Update(data resource.TableData) { t.updateSelection(true) } -func (t *Table) doUpdate(data resource.TableData) { - t.ActiveNS = data.Namespace - if t.ActiveNS == resource.AllNamespaces && t.ActiveNS != "*" { +func (t *Table) doUpdate(data render.TableData) { + if data.Namespace == render.AllNamespaces { t.actions[KeyShiftP] = NewKeyAction("Sort Namespace", t.SortColCmd(-2, true), false) } else { t.actions.Delete(KeyShiftP) @@ -166,25 +178,26 @@ func (t *Table) doUpdate(data resource.TableData) { for i, r := range data.RowEvents { t.buildRow(data.Namespace, i+1, r, data.Header, pads) } - // t.resetSelection() + t.updateSelection(false) } // SortColCmd designates a sorted column. func (t *Table) SortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { return func(evt *tcell.EventKey) *tcell.EventKey { + var index int switch col { case -2: - col = 0 + index = 0 case -1: - col = t.GetColumnCount() - 1 + index = t.GetColumnCount() - 1 default: - col = t.NameColIndex() + col + index = t.NameColIndex() + col } t.sortCol.asc = !t.sortCol.asc - if t.sortCol.index != col { + if t.sortCol.index != index { t.sortCol.asc = asc } - t.sortCol.index = col + t.sortCol.index = index t.Refresh() return nil @@ -199,7 +212,7 @@ func (t *Table) SortInvertCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (t *Table) adjustSorter(data resource.TableData) { +func (t *Table) adjustSorter(data render.TableData) { // Going from namespace to non namespace or vice-versa? switch { case t.sortCol.colCount == 0: @@ -214,51 +227,30 @@ func (t *Table) adjustSorter(data resource.TableData) { } } -// BOZO!! -// func (t *Table) sort(data resource.TableData, row int) { -// pads := make(MaxyPad, len(data.Header)) -// ComputeMaxColumns(pads, t.sortCol.index, data.Header, data.RowEvents) - -// sortFn := defaultSort -// if t.sortFn != nil { -// sortFn = t.sortFn -// } - -// prim, sec := sortAllRows(t.sortCol, data.RowEvents, sortFn) -// for _, pk := range prim { -// for _, sk := range sec[pk] { -// t.buildRow(row, data, sk, pads) -// row++ -// } -// } - -// // check marks if a row is deleted make sure we blow the mark too. -// for k := range t.marks { -// if _, ok := t.Data.Rows[k]; !ok { -// delete(t.marks, k) -// } -// } -// } - func (t *Table) buildRow(ns string, r int, re render.RowEvent, header render.HeaderRow, pads MaxyPad) { - color := DefaultColorer + color := render.DefaultColorer if t.colorerFn != nil { color = t.colorerFn } marked := t.IsMarked(re.Row.ID) for col, field := range re.Row.Fields { - delta := field - if len(re.Deltas) > 0 { - delta = re.Deltas[col] + if !re.Deltas.IsBlank() && !header.AgeCol(col) { + field += Deltas(re.Deltas[col], field) } - c := tview.NewTableCell(formatCell(field+Deltas(delta, field), pads[col])) - { - c.SetExpansion(1) - c.SetAlign(header[col].Align) - c.SetTextColor(color(ns, re)) - if marked { - c.SetTextColor(config.AsColor(t.styles.Table().MarkColor)) - } + + if header[col].Decorator != nil { + field = header[col].Decorator(field) + } + + if header[col].Align == tview.AlignLeft { + field = formatCell(field, pads[col]) + } + c := tview.NewTableCell(field) + c.SetExpansion(1) + c.SetAlign(header[col].Align) + c.SetTextColor(color(ns, re)) + if marked { + c.SetTextColor(config.AsColor(t.styles.Table().MarkColor)) } t.SetCell(r, col, c) } @@ -277,7 +269,7 @@ func (t *Table) Refresh() { // NameColIndex returns the index of the resource name column. func (t *Table) NameColIndex() int { col := 0 - if t.ActiveNS == resource.AllNamespaces { + if t.Data.Namespace == render.AllNamespaces { col++ } return col @@ -291,7 +283,7 @@ func (t *Table) AddHeaderCell(col int, h render.Header) { t.SetCell(0, col, c) } -func (t *Table) filtered() resource.TableData { +func (t *Table) filtered() render.TableData { if t.cmdBuff.Empty() || IsLabelSelector(t.cmdBuff.String()) { return t.Data } @@ -327,5 +319,9 @@ func (t *Table) ShowDeleted() { // UpdateTitle refreshes the table title. func (t *Table) UpdateTitle() { - t.SetTitle(styleTitle(t.GetRowCount(), t.ActiveNS, t.BaseTitle, t.Path, t.cmdBuff.String(), t.styles)) + ns := t.Data.Namespace + if ns == render.AllNamespaces { + ns = render.NamespaceAll + } + t.SetTitle(styleTitle(t.GetRowCount(), ns, t.BaseTitle, t.Path, t.cmdBuff.String(), t.styles)) } diff --git a/internal/ui/table_helper.go b/internal/ui/table_helper.go index abd20707..3c311781 100644 --- a/internal/ui/table_helper.go +++ b/internal/ui/table_helper.go @@ -18,6 +18,7 @@ const ( SearchFmt = "<[filter:bg:r]/%s[fg:bg:-]> " nsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] " + titleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] " descIndicator = "↓" ascIndicator = "↑" @@ -88,17 +89,18 @@ func SkinTitle(fmat string, style config.Frame) string { return fmat } -func sortRows(evts resource.RowEvents, sortFn SortFn, sortCol SortColumn, keys []string) { - rows := make(resource.Rows, 0, len(evts)) - for k, r := range evts { - rows = append(rows, append(r.Fields, k)) - } - sortFn(rows, sortCol) +// BOZO!! +// func sortRows(evts resource.RowEvents, sortFn SortFn, sortCol SortColumn, keys []string) { +// rows := make(resource.Rows, 0, len(evts)) +// for k, r := range evts { +// rows = append(rows, append(r.Fields, k)) +// } +// sortFn(rows, sortCol) - for i, r := range rows { - keys[i] = r[len(r)-1] - } -} +// for i, r := range rows { +// keys[i] = r[len(r)-1] +// } +// } // func defaultSort(rows resource.Rows, sortCol SortColumn) { // t := RowSorter{rows: rows, index: sortCol.index, asc: sortCol.asc} @@ -147,13 +149,13 @@ func formatCell(field string, padding int) string { return field } -func rxFilter(q string, data resource.TableData) (resource.TableData, error) { +func rxFilter(q string, data render.TableData) (render.TableData, error) { rx, err := regexp.Compile(`(?i)` + q) if err != nil { return data, err } - filtered := resource.TableData{ + filtered := render.TableData{ Header: data.Header, RowEvents: make(render.RowEvents, 0, len(data.RowEvents)), Namespace: data.Namespace, @@ -168,14 +170,14 @@ func rxFilter(q string, data resource.TableData) (resource.TableData, error) { return filtered, nil } -func fuzzyFilter(q string, index int, data resource.TableData) resource.TableData { +func fuzzyFilter(q string, index int, data render.TableData) render.TableData { var ss, kk []string for _, re := range data.RowEvents { ss = append(ss, re.Row.Fields[index]) kk = append(kk, re.Row.ID) } - filtered := resource.TableData{ + filtered := render.TableData{ Header: data.Header, RowEvents: make(render.RowEvents, 0, len(data.RowEvents)), Namespace: data.Namespace, @@ -201,7 +203,7 @@ func styleTitle(rc int, ns, base, path, buff string, styles *config.Styles) stri } switch ns { case resource.NotNamespaced, "*": - title = SkinTitle(fmt.Sprintf(nsTitleFmt, base, path, rc), styles.Frame()) + title = SkinTitle(fmt.Sprintf(titleFmt, base, rc), styles.Frame()) default: if ns == resource.AllNamespaces { ns = resource.AllNamespace diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index e1840a28..abcfb2d8 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -54,8 +54,8 @@ func TestTableSelection(t *testing.T) { // Helpers... -func makeTableData() resource.TableData { - return resource.TableData{ +func makeTableData() render.TableData { + return render.TableData{ Namespace: "", Header: render.HeaderRow{render.Header{Name: "a"}, render.Header{Name: "b"}, render.Header{Name: "c"}}, RowEvents: render.RowEvents{ diff --git a/internal/view/alias.go b/internal/view/alias.go index 238c68b4..6bcf49bd 100644 --- a/internal/view/alias.go +++ b/internal/view/alias.go @@ -5,11 +5,12 @@ import ( "fmt" "strings" - "github.com/derailed/k9s/internal/k8s" + "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" ) const ( @@ -35,10 +36,9 @@ func (a *Alias) Init(ctx context.Context) error { return err } + a.SetColorerFn(render.Alias{}.ColorerFunc()) a.SetBorderFocusColor(tcell.ColorMediumSpringGreen) a.SetSelectedStyle(tcell.ColorWhite, tcell.ColorMediumSpringGreen, tcell.AttrNone) - a.SetColorerFn(aliasColorer) - a.ActiveNS = resource.AllNamespaces a.registerActions() a.Update(a.hydrate()) a.resetTitle() @@ -96,18 +96,16 @@ func (a *Alias) backCmd(_ *tcell.EventKey) *tcell.EventKey { return nil } -func (a *Alias) hydrate() resource.TableData { - data := resource.TableData{ - Header: render.HeaderRow{ - render.Header{Name: "RESOURCE"}, - render.Header{Name: "COMMAND"}, - render.Header{Name: "APIGROUP"}, - }, - RowEvents: make(render.RowEvents, len(aliases.Alias)), +func (a *Alias) hydrate() render.TableData { + var re render.Alias + + data := render.TableData{ + Header: re.Header(render.AllNamespaces), + RowEvents: make(render.RowEvents, 0, len(aliases.Alias)), Namespace: resource.NotNamespaced, } - aa := make(map[string][]string, len(aliases.Alias)) + aa := make(config.ShortNames, len(aliases.Alias)) for alias, gvr := range aliases.Alias { if _, ok := aa[gvr]; ok { aa[gvr] = append(aa[gvr], alias) @@ -117,16 +115,15 @@ func (a *Alias) hydrate() resource.TableData { } for gvr, aliases := range aa { - g := k8s.GVR(gvr) - row := render.Row{ - ID: string(gvr), - Fields: render.Fields{ - ui.Pad(g.ToR(), 30), - ui.Pad(strings.Join(aliases, ","), 70), - ui.Pad(g.ToG(), 30), - }, + var row render.Row + if err := re.Render(aliases, gvr, &row); err != nil { + log.Error().Err(err).Msgf("Alias render failed") + continue } - data.RowEvents = append(data.RowEvents, render.RowEvent{Kind: render.EventAdd, Row: row}) + data.RowEvents = append(data.RowEvents, render.RowEvent{ + Kind: render.EventAdd, + Row: row, + }) } return data diff --git a/internal/view/alias_test.go b/internal/view/alias_test.go index 75b50aff..2631f25b 100644 --- a/internal/view/alias_test.go +++ b/internal/view/alias_test.go @@ -17,7 +17,7 @@ func TestAliasNew(t *testing.T) { v.Init(makeContext()) assert.Equal(t, 3, v.GetColumnCount()) - assert.Equal(t, 41, v.GetRowCount()) + assert.Equal(t, 15, v.GetRowCount()) assert.Equal(t, "Aliases", v.Name()) assert.Equal(t, 9, len(v.Hints())) } diff --git a/internal/view/app.go b/internal/view/app.go index 4c100306..57762dfc 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -30,10 +30,10 @@ type App struct { Content *PageStack command *command factory *watch.Factory - cancelFn context.CancelFunc forwarders model.Forwarders version string showHeader bool + cancelFn context.CancelFunc } // NewApp returns a K9s app instance. @@ -104,6 +104,10 @@ func (a *App) Init(version string, rate int) error { a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true) a.toggleHeader(!a.Config.K9s.GetHeadless()) + if err := a.command.Init(); err != nil { + panic(err) + } + return nil } @@ -164,6 +168,18 @@ func (a *App) buildHeader() tview.Primitive { return header } +func (a *App) Halt() { + if a.cancelFn != nil { + a.cancelFn() + } +} + +func (a *App) Resume() { + var ctx context.Context + ctx, a.cancelFn = context.WithCancel(context.Background()) + go a.clusterUpdater(ctx) +} + func (a *App) clusterUpdater(ctx context.Context) { for { select { @@ -172,16 +188,21 @@ func (a *App) clusterUpdater(ctx context.Context) { return case <-time.After(clusterRefresh): a.QueueUpdateDraw(func() { - if !a.showHeader { - a.refreshIndicator() - } else { - a.clusterInfo().refresh() - } + a.refreshClusterInfo() }) } } } +func (a *App) refreshClusterInfo() { + log.Debug().Msgf("***** REFRESHING CLUSTER ******") + if !a.showHeader { + a.refreshIndicator() + } else { + a.clusterInfo().refresh() + } +} + func (a *App) refreshIndicator() { mx := k8s.NewMetricsServer(a.Conn()) cluster := resource.NewCluster(a.Conn(), &log.Logger, mx) @@ -225,53 +246,42 @@ func (a *App) switchNS(ns string) bool { return true } -func (a *App) switchCtx(name string, load bool) error { - l := resource.NewContext(a.Conn()) - if err := l.Switch(name); err != nil { - return err - } +func (a *App) switchCtx(name string, loadPods bool) error { + log.Debug().Msgf("Switching Context %q", name) - a.forwarders.DeleteAll() - ns, err := a.Conn().Config().CurrentNamespaceName() - if err != nil { - log.Info().Err(err).Msg("No namespace specified using all namespaces") - } - a.initFactory(ns) + a.Halt() + defer a.Resume() + { + a.forwarders.DeleteAll() + ns, err := a.Conn().Config().CurrentNamespaceName() + if err != nil { + log.Info().Err(err).Msg("No namespace specified in context. Using K9s config") + } + a.initFactory(ns) - a.Config.Reset() - if err := a.Config.Save(); err != nil { - log.Error().Err(err).Msg("Config save failed!") - } - a.Flash().Infof("Switching context to %s", name) - if load && !a.gotoResource("po") { - a.Flash().Err(errors.New("Goto pod failed")) - } - if a.Config.K9s.GetHeadless() { - a.refreshIndicator() + a.Config.Reset() + if err := a.Config.Save(); err != nil { + log.Error().Err(err).Msg("Config save failed!") + } + a.Flash().Infof("Switching context to %s", name) + if loadPods && !a.gotoResource("pods") { + a.Flash().Err(errors.New("Goto pods failed")) + } + a.refreshClusterInfo() } return nil } func (a *App) initFactory(ns string) { - if a.cancelFn != nil { - a.cancelFn() - a.cancelFn = nil - } - var ctx context.Context - ctx, a.cancelFn = context.WithCancel(context.Background()) - a.factory.Init(ctx) + a.factory.Terminate() + a.factory.Init() a.factory.SetActive(ns) } // BailOut exists the application. func (a *App) BailOut() { - if a.cancelFn != nil { - log.Debug().Msg("<<<< Stopping Factory") - a.cancelFn() - a.cancelFn = nil - } - + a.factory.Terminate() a.forwarders.DeleteAll() a.App.BailOut() } @@ -280,7 +290,7 @@ func (a *App) BailOut() { func (a *App) Run() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go a.clusterUpdater(ctx) + a.Halt() // Only enable skin updater while in dev mode. if a.HasSkins { @@ -304,11 +314,8 @@ func (a *App) Run() { func (a *App) status(l ui.FlashLevel, msg string) { a.Flash().Info(msg) - if a.Config.K9s.GetHeadless() { - a.setIndicator(l, msg) - } else { - a.setLogo(l, msg) - } + a.setIndicator(l, msg) + a.setLogo(l, msg) a.Draw() } @@ -369,7 +376,11 @@ func (a *App) helpCmd(evt *tcell.EventKey) *tcell.EventKey { if _, ok := a.Content.GetPrimitive("main").(*Help); ok { return evt } - a.inject(NewHelp()) + if a.Content.Top() != nil && a.Content.Top().Name() == helpTitle { + a.Content.Pop() + } else { + a.inject(NewHelp()) + } return nil } @@ -378,7 +389,12 @@ func (a *App) aliasCmd(evt *tcell.EventKey) *tcell.EventKey { if _, ok := a.Content.GetPrimitive("main").(*Alias); ok { return evt } - a.inject(NewAlias()) + + if a.Content.Top() != nil && a.Content.Top().Name() == aliasTitle { + a.Content.Pop() + } else { + a.inject(NewAlias()) + } return nil } diff --git a/internal/view/bench.go b/internal/view/bench.go index fcf74917..45209c66 100644 --- a/internal/view/bench.go +++ b/internal/view/bench.go @@ -6,17 +6,12 @@ import ( "io/ioutil" "os" "path/filepath" - "regexp" - "strconv" - "strings" - "time" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/perf" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tview" "github.com/fsnotify/fsnotify" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" @@ -27,25 +22,6 @@ const ( resultTitle = "Benchmark Results" ) -var ( - totalRx = regexp.MustCompile(`Total:\s+([0-9.]+)\ssecs`) - reqRx = regexp.MustCompile(`Requests/sec:\s+([0-9.]+)`) - okRx = regexp.MustCompile(`\[2\d{2}\]\s+(\d+)\s+responses`) - errRx = regexp.MustCompile(`\[[4-5]\d{2}\]\s+(\d+)\s+responses`) - toastRx = regexp.MustCompile(`Error distribution`) - benchHeader = render.HeaderRow{ - render.Header{Name: "NAMESPACE", Align: tview.AlignLeft}, - render.Header{Name: "NAME", Align: tview.AlignLeft}, - render.Header{Name: "STATUS", Align: tview.AlignLeft}, - render.Header{Name: "TIME", Align: tview.AlignLeft}, - render.Header{Name: "REQ/S", Align: tview.AlignRight}, - render.Header{Name: "2XX", Align: tview.AlignRight}, - render.Header{Name: "4XX/5XX", Align: tview.AlignRight}, - render.Header{Name: "REPORT", Align: tview.AlignLeft}, - render.Header{Name: "AGE", Align: tview.AlignLeft}, - } -) - // Bench represents a service benchmark results view. type Bench struct { *Table @@ -61,6 +37,8 @@ func NewBench(title, _ string, _ resource.List) ResourceViewer { } } +func (*Bench) SetContextFn(ContextFunc) {} + // Init initializes the viewer. func (b *Bench) Init(ctx context.Context) error { log.Debug().Msgf(">>> Bench INIT") @@ -69,7 +47,7 @@ func (b *Bench) Init(ctx context.Context) error { } b.SetBorderFocusColor(tcell.ColorSeaGreen) b.SetSelectedStyle(tcell.ColorWhite, tcell.ColorSeaGreen, tcell.AttrNone) - b.SetColorerFn(benchColorer) + b.SetColorerFn(render.Bench{}.ColorerFunc()) b.bindKeys() b.details.SetTextColor(tcell.ColorSeaGreen) @@ -86,6 +64,11 @@ func (b *Bench) Init(ctx context.Context) error { return nil } +// GVR returns a resource descriptor. +func (b *Bench) GVR() string { + return "n/a" +} + // SetEnvFn sets k9s env vars. func (b *Bench) SetEnvFn(EnvFunc) {} @@ -168,47 +151,39 @@ func (b *Bench) benchFile() string { return ui.TrimCell(b.SelectTable, r, 7) } -func (b *Bench) hydrate() resource.TableData { +func (b *Bench) hydrate() render.TableData { ff, err := loadBenchDir(b.app.Config) if err != nil { b.app.Flash().Errf("Unable to read bench directory %s", err) } - data := initTable() + var re render.Bench + data := render.TableData{ + Header: re.Header(render.AllNamespaces), + RowEvents: make(render.RowEvents, 0, 10), + Namespace: render.AllNamespaces, + } + for _, f := range ff { - bench, err := readBenchFile(b.app.Config, f.Name()) - if err != nil { - log.Error().Err(err).Msgf("Unable to load bench file %s", f.Name()) + bench := render.BenchInfo{ + File: f, + Path: filepath.Join(benchDir(b.app.Config), f.Name()), + } + + var row render.Row + if err := re.Render(bench, render.AllNamespaces, &row); err != nil { + log.Error().Err(err).Msg("Bench render failed") continue } - fields := make(render.Fields, len(benchHeader)) - if err := initRow(fields, f); err != nil { - log.Error().Err(err).Msg("Load bench file") - continue - } - augmentRow(fields, bench) data.RowEvents = append(data.RowEvents, render.RowEvent{ Kind: render.EventAdd, - Row: render.Row{ID: f.Name(), Fields: fields}, + Row: row, }) } return data } -func initRow(row render.Fields, f os.FileInfo) error { - tokens := strings.Split(f.Name(), "_") - if len(tokens) < 2 { - return fmt.Errorf("Invalid file name %s", f.Name()) - } - row[0] = tokens[0] - row[1] = tokens[1] - row[7] = f.Name() - row[8] = time.Since(f.ModTime()).String() - - return nil -} - func (b *Bench) watchBenchDir(ctx context.Context) error { w, err := fsnotify.NewWatcher() if err != nil { @@ -242,61 +217,6 @@ func (b *Bench) watchBenchDir(ctx context.Context) error { // ---------------------------------------------------------------------------- // Helpers... -func initTable() resource.TableData { - return resource.TableData{ - Header: benchHeader, - RowEvents: make(render.RowEvents, 10), - Namespace: resource.AllNamespaces, - } -} - -func augmentRow(fields render.Fields, data string) { - if len(data) == 0 { - return - } - - col := 2 - fields[col] = "pass" - mf := toastRx.FindAllStringSubmatch(data, 1) - if len(mf) > 0 { - fields[col] = "fail" - } - col++ - - mt := totalRx.FindAllStringSubmatch(data, 1) - if len(mt) > 0 { - fields[col] = mt[0][1] - } - col++ - - mr := reqRx.FindAllStringSubmatch(data, 1) - if len(mr) > 0 { - fields[col] = mr[0][1] - } - col++ - - ms := okRx.FindAllStringSubmatch(data, -1) - fields[col] = countReq(ms) - col++ - - me := errRx.FindAllStringSubmatch(data, -1) - fields[col] = countReq(me) -} - -func countReq(rr [][]string) string { - if len(rr) == 0 { - return "0" - } - - var sum int - for _, m := range rr { - if m, err := strconv.Atoi(string(m[1])); err == nil { - sum += m - } - } - return asNum(sum) -} - func benchDir(cfg *config.Config) string { return filepath.Join(perf.K9sBenchDir, cfg.K9s.CurrentCluster) } diff --git a/internal/view/colorer.go b/internal/view/colorer.go deleted file mode 100644 index 70eda373..00000000 --- a/internal/view/colorer.go +++ /dev/null @@ -1,256 +0,0 @@ -package view - -import ( - "strings" - - "github.com/derailed/k9s/internal/render" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" -) - -func forwardColorer(string, render.RowEvent) tcell.Color { - return tcell.ColorSkyblue -} - -func dumpColorer(ns string, r render.RowEvent) tcell.Color { - return tcell.ColorNavajoWhite -} - -func benchColorer(ns string, r render.RowEvent) tcell.Color { - c := tcell.ColorPaleGreen - - statusCol := 2 - if strings.TrimSpace(r.Row.Fields[statusCol]) != "pass" { - c = ui.ErrColor - } - - return c -} - -func aliasColorer(string, render.RowEvent) tcell.Color { - return tcell.ColorMediumSpringGreen -} - -func rbacColorer(ns string, r render.RowEvent) tcell.Color { - return ui.DefaultColorer(ns, r) -} - -func checkReadyCol(readyCol, statusCol string, c tcell.Color) tcell.Color { - if statusCol == "Completed" { - return c - } - - tokens := strings.Split(readyCol, "/") - if len(tokens) == 2 && (tokens[0] == "0" || tokens[0] != tokens[1]) { - return ui.ErrColor - } - return c -} - -func podColorer(ns string, r render.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - - readyCol := 2 - if len(ns) != 0 { - readyCol = 1 - } - statusCol := readyCol + 1 - - ready, status := strings.TrimSpace(r.Row.Fields[readyCol]), strings.TrimSpace(r.Row.Fields[statusCol]) - c = checkReadyCol(ready, status, c) - - switch status { - case "ContainerCreating", "PodInitializing": - return ui.AddColor - case resource.Initialized: - return ui.HighlightColor - case resource.Completed: - return ui.CompletedColor - case resource.Running: - case resource.Terminating: - return ui.KillColor - default: - return ui.ErrColor - } - - return c -} - -func containerColorer(ns string, r render.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - - readyCol := 2 - if strings.TrimSpace(r.Row.Fields[readyCol]) == "false" { - c = ui.ErrColor - } - - stateCol := readyCol + 1 - switch strings.TrimSpace(r.Row.Fields[stateCol]) { - case "ContainerCreating", "PodInitializing": - return ui.AddColor - case resource.Terminating, resource.Initialized: - return ui.HighlightColor - case resource.Completed: - return ui.CompletedColor - case resource.Running: - default: - c = ui.ErrColor - } - - return c -} - -func ctxColorer(ns string, r render.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - if r.Kind == render.EventAdd || r.Kind == render.EventUpdate { - return c - } - - if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") { - c = ui.HighlightColor - } - - return c -} - -func pvColorer(ns string, r render.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - if r.Kind == render.EventAdd || r.Kind == render.EventUpdate { - return c - } - - status := strings.TrimSpace(r.Row.Fields[4]) - switch status { - case "Bound": - c = ui.StdColor - case "Available": - c = tcell.ColorYellow - default: - c = ui.ErrColor - } - - return c -} - -func pvcColorer(ns string, r render.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - if r.Kind == render.EventAdd || r.Kind == render.EventUpdate { - return c - } - - markCol := 2 - if ns != resource.AllNamespaces { - markCol = 1 - } - - if strings.TrimSpace(r.Row.Fields[markCol]) != "Bound" { - c = ui.ErrColor - } - - return c -} - -func pdbColorer(ns string, r render.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - if r.Kind == render.EventAdd || r.Kind == render.EventUpdate { - return c - } - - markCol := 5 - if ns != resource.AllNamespaces { - markCol = 4 - } - if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) { - return ui.ErrColor - } - - return ui.StdColor -} - -func dpColorer(ns string, r render.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - if r.Kind == render.EventAdd || r.Kind == render.EventUpdate { - return c - } - - markCol := 2 - if ns != resource.AllNamespaces { - markCol = 1 - } - if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) { - return ui.ErrColor - } - - return ui.StdColor -} - -func stsColorer(ns string, r render.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - if r.Kind == render.EventAdd || r.Kind == render.EventUpdate { - return c - } - - markCol := 2 - if ns != resource.AllNamespaces { - markCol = 1 - } - if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) { - return ui.ErrColor - } - - return ui.StdColor -} - -func rsColorer(ns string, r render.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - if r.Kind == render.EventAdd || r.Kind == render.EventUpdate { - return c - } - - markCol := 2 - if ns != resource.AllNamespaces { - markCol = 1 - } - if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) { - return ui.ErrColor - } - - return ui.StdColor -} - -func evColorer(ns string, r render.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - - markCol := 3 - if ns != resource.AllNamespaces { - markCol = 2 - } - - switch strings.TrimSpace(r.Row.Fields[markCol]) { - case "Failed": - c = ui.ErrColor - case "Killing": - c = ui.KillColor - } - - return c -} - -func nsColorer(ns string, r render.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - if r.Kind == render.EventAdd || r.Kind == render.EventUpdate { - return c - } - - switch strings.TrimSpace(r.Row.Fields[1]) { - case "Inactive", resource.Terminating: - c = ui.ErrColor - } - - if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") { - c = ui.HighlightColor - } - - return c -} diff --git a/internal/view/colorer_test.go b/internal/view/colorer_test.go deleted file mode 100644 index 7b1b4c8c..00000000 --- a/internal/view/colorer_test.go +++ /dev/null @@ -1,289 +0,0 @@ -package view - -import ( - "testing" - - "github.com/derailed/k9s/internal/render" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" - "github.com/stretchr/testify/assert" -) - -type ( - colorerUC struct { - ns string - r render.RowEvent - e tcell.Color - } - colorerUCs []colorerUC -) - -func TestNSColorer(t *testing.T) { - var ( - ns = render.Row{Fields: render.Fields{"blee", "Active"}} - term = render.Row{Fields: render.Fields{"blee", resource.Terminating}} - dead = render.Row{Fields: render.Fields{"blee", "Inactive"}} - ) - - uu := colorerUCs{ - // Add AllNS - {"", render.RowEvent{ - Kind: render.EventAdd, - Row: ns, - }, - ui.AddColor}, - // Mod AllNS - {"", render.RowEvent{Kind: render.EventUpdate, Row: ns}, ui.ModColor}, - // MoChange AllNS - {"", render.RowEvent{Kind: render.EventUnchanged, Row: ns}, ui.StdColor}, - // Bust NS - {"", render.RowEvent{Kind: render.EventUnchanged, Row: term}, ui.ErrColor}, - // Bust NS - {"", render.RowEvent{Kind: render.EventUnchanged, Row: dead}, ui.ErrColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, nsColorer(u.ns, u.r)) - } -} - -func TestEvColorer(t *testing.T) { - var ( - ns = render.Row{Fields: render.Fields{"", "blee", "fred", "Normal"}} - nonNS = render.Row{Fields: render.Fields{"", "fred", "Normal"}} - failNS = render.Row{Fields: render.Fields{"", "blee", "fred", "Failed"}} - failNoNS = render.Row{Fields: render.Fields{"", "fred", "Failed"}} - killNS = render.Row{Fields: render.Fields{"", "blee", "fred", "Killing"}} - killNoNS = render.Row{Fields: render.Fields{"", "fred", "Killing"}} - ) - - uu := colorerUCs{ - // Add AllNS - {"", render.RowEvent{Kind: render.EventAdd, Row: ns}, ui.AddColor}, - // Add NS - {"blee", render.RowEvent{Kind: render.EventAdd, Row: nonNS}, ui.AddColor}, - // Mod AllNS - {"", render.RowEvent{Kind: render.EventUpdate, Row: ns}, ui.ModColor}, - // Mod NS - {"blee", render.RowEvent{Kind: render.EventUpdate, Row: nonNS}, ui.ModColor}, - // Bust AllNS - {"", render.RowEvent{Kind: render.EventUnchanged, Row: failNS}, ui.ErrColor}, - // Bust NS - {"blee", render.RowEvent{Kind: render.EventUnchanged, Row: failNoNS}, ui.ErrColor}, - // Bust AllNS - {"", render.RowEvent{Kind: render.EventUnchanged, Row: killNS}, ui.KillColor}, - // Bust NS - {"blee", render.RowEvent{Kind: render.EventUnchanged, Row: killNoNS}, ui.KillColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, evColorer(u.ns, u.r)) - } -} - -func TestRSColorer(t *testing.T) { - var ( - ns = render.Row{Fields: render.Fields{"blee", "fred", "1", "1"}} - noNs = render.Row{Fields: render.Fields{"fred", "1", "1"}} - bustNS = render.Row{Fields: render.Fields{"blee", "fred", "1", "0"}} - bustNoNS = render.Row{Fields: render.Fields{"fred", "1", "0"}} - ) - - uu := colorerUCs{ - // Add AllNS - {"", render.RowEvent{Kind: render.EventAdd, Row: ns}, ui.AddColor}, - // Add NS - {"blee", render.RowEvent{Kind: render.EventAdd, Row: noNs}, ui.AddColor}, - // Bust AllNS - {"", render.RowEvent{Kind: render.EventUnchanged, Row: bustNS}, ui.ErrColor}, - // Bust NS - {"blee", render.RowEvent{Kind: render.EventUnchanged, Row: bustNoNS}, ui.ErrColor}, - // Nochange AllNS - {"", render.RowEvent{Kind: render.EventUnchanged, Row: ns}, ui.StdColor}, - // Nochange NS - {"blee", render.RowEvent{Kind: render.EventUnchanged, Row: noNs}, ui.StdColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, rsColorer(u.ns, u.r)) - } -} - -func TestStsColorer(t *testing.T) { - var ( - ns = render.Row{Fields: render.Fields{"blee", "fred", "1", "1"}} - nonNS = render.Row{Fields: render.Fields{"fred", "1", "1"}} - bustNS = render.Row{Fields: render.Fields{"blee", "fred", "2", "1"}} - bustNoNS = render.Row{Fields: render.Fields{"fred", "2", "1"}} - ) - - uu := colorerUCs{ - // Add AllNS - {"", render.RowEvent{Kind: render.EventAdd, Row: ns}, ui.AddColor}, - // Add NS - {"blee", render.RowEvent{Kind: render.EventAdd, Row: nonNS}, ui.AddColor}, - // Mod AllNS - {"", render.RowEvent{Kind: render.EventUpdate, Row: ns}, ui.ModColor}, - // Mod NS - {"blee", render.RowEvent{Kind: render.EventUpdate, Row: nonNS}, ui.ModColor}, - // Bust AllNS - {"", render.RowEvent{Kind: render.EventUnchanged, Row: bustNS}, ui.ErrColor}, - // Bust NS - {"blee", render.RowEvent{Kind: render.EventUnchanged, Row: bustNoNS}, ui.ErrColor}, - // Unchanged cool AllNS - {"", render.RowEvent{Kind: render.EventUnchanged, Row: ns}, ui.StdColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, stsColorer(u.ns, u.r)) - } -} - -func TestDpColorer(t *testing.T) { - var ( - ns = render.Row{Fields: render.Fields{"blee", "fred", "1", "1"}} - nonNS = render.Row{Fields: render.Fields{"fred", "1", "1"}} - bustNS = render.Row{Fields: render.Fields{"blee", "fred", "2", "1"}} - bustNoNS = render.Row{Fields: render.Fields{"fred", "2", "1"}} - ) - - uu := colorerUCs{ - // Add AllNS - {"", render.RowEvent{Kind: render.EventAdd, Row: ns}, ui.AddColor}, - // Add NS - {"blee", render.RowEvent{Kind: render.EventAdd, Row: nonNS}, ui.AddColor}, - // Mod AllNS - {"", render.RowEvent{Kind: render.EventUpdate, Row: ns}, ui.ModColor}, - // Mod NS - {"blee", render.RowEvent{Kind: render.EventUpdate, Row: nonNS}, ui.ModColor}, - // Unchanged cool - {"", render.RowEvent{Kind: render.EventUnchanged, Row: ns}, ui.StdColor}, - // Bust AllNS - {"", render.RowEvent{Kind: render.EventUnchanged, Row: bustNS}, ui.ErrColor}, - // Bust NS - {"blee", render.RowEvent{Kind: render.EventUnchanged, Row: bustNoNS}, ui.ErrColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, dpColorer(u.ns, u.r)) - } -} - -func TestPdbColorer(t *testing.T) { - var ( - ns = render.Row{Fields: render.Fields{"blee", "fred", "1", "1", "1", "1", "1"}} - nonNS = render.Row{Fields: render.Fields{"fred", "1", "1", "1", "1", "1"}} - bustNS = render.Row{Fields: render.Fields{"blee", "fred", "1", "1", "1", "1", "2"}} - bustNoNS = render.Row{Fields: render.Fields{"fred", "1", "1", "1", "1", "2"}} - ) - - uu := colorerUCs{ - // Add AllNS - {"", render.RowEvent{Kind: render.EventAdd, Row: ns}, ui.AddColor}, - // Add NS - {"blee", render.RowEvent{Kind: render.EventAdd, Row: nonNS}, ui.AddColor}, - // Mod AllNS - {"", render.RowEvent{Kind: render.EventUpdate, Row: ns}, ui.ModColor}, - // Mod NS - {"blee", render.RowEvent{Kind: render.EventUpdate, Row: nonNS}, ui.ModColor}, - // Unchanged cool - {"", render.RowEvent{Kind: render.EventUnchanged, Row: ns}, ui.StdColor}, - // Bust AllNS - {"", render.RowEvent{Kind: render.EventUnchanged, Row: bustNS}, ui.ErrColor}, - // Bust NS - {"blee", render.RowEvent{Kind: render.EventUnchanged, Row: bustNoNS}, ui.ErrColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, pdbColorer(u.ns, u.r)) - } -} - -func TestPVColorer(t *testing.T) { - var ( - pv = render.Row{Fields: render.Fields{"blee", "1G", "RO", "Duh", "Bound"}} - bustPv = render.Row{Fields: render.Fields{"blee", "1G", "RO", "Duh", "UnBound"}} - ) - - uu := colorerUCs{ - // Add Normal - {"", render.RowEvent{Kind: render.EventAdd, Row: pv}, ui.AddColor}, - // Unchanged Bound - {"", render.RowEvent{Kind: render.EventUnchanged, Row: pv}, ui.StdColor}, - // Unchanged Bound - {"", render.RowEvent{Kind: render.EventUnchanged, Row: bustPv}, ui.ErrColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, pvColorer(u.ns, u.r)) - } -} - -func TestPVCColorer(t *testing.T) { - var ( - pvc = render.Row{Fields: render.Fields{"blee", "fred", "Bound"}} - bustPvc = render.Row{Fields: render.Fields{"blee", "fred", "UnBound"}} - ) - - uu := colorerUCs{ - // Add Normal - {"", render.RowEvent{Kind: render.EventAdd, Row: pvc}, ui.AddColor}, - // Add Bound - {"", render.RowEvent{Kind: render.EventUnchanged, Row: bustPvc}, ui.ErrColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, pvcColorer(u.ns, u.r)) - } -} - -func TestCtxColorer(t *testing.T) { - var ( - ctx = render.Row{Fields: render.Fields{"blee"}} - defCtx = render.Row{Fields: render.Fields{"blee*"}} - ) - - uu := colorerUCs{ - // Add Normal - {"", render.RowEvent{Kind: render.EventAdd, Row: ctx}, ui.AddColor}, - // Add Default - {"", render.RowEvent{Kind: render.EventAdd, Row: defCtx}, ui.AddColor}, - // Mod Normal - {"", render.RowEvent{Kind: render.EventUpdate, Row: ctx}, ui.ModColor}, - // Mod Default - {"", render.RowEvent{Kind: render.EventUpdate, Row: defCtx}, ui.ModColor}, - // Unchanged Normal - {"", render.RowEvent{Kind: render.EventUnchanged, Row: ctx}, ui.StdColor}, - // Unchanged Default - {"", render.RowEvent{Kind: render.EventUnchanged, Row: defCtx}, ui.HighlightColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, ctxColorer(u.ns, u.r)) - } -} - -func TestPodColorer(t *testing.T) { - var ( - nsRow = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "Running"}} - toastNS = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "Boom"}} - notReadyNS = render.Row{Fields: render.Fields{"blee", "fred", "0/1", "Boom"}} - row = render.Row{Fields: render.Fields{"fred", "1/1", "Running"}} - toast = render.Row{Fields: render.Fields{"fred", "1/1", "Boom"}} - notReady = render.Row{Fields: render.Fields{"fred", "0/1", "Boom"}} - ) - - uu := colorerUCs{ - // Add allNS - {"", render.RowEvent{Kind: render.EventAdd, Row: nsRow}, ui.AddColor}, - // Add Namespaced - {"blee", render.RowEvent{Kind: render.EventAdd, Row: row}, ui.AddColor}, - // Mod AllNS - {"", render.RowEvent{Kind: render.EventUpdate, Row: nsRow}, ui.ModColor}, - // Mod Namespaced - {"blee", render.RowEvent{Kind: render.EventUpdate, Row: row}, ui.ModColor}, - // Mod Busted AllNS - {"", render.RowEvent{Kind: render.EventUpdate, Row: toastNS}, ui.ErrColor}, - // Mod Busted Namespaced - {"blee", render.RowEvent{Kind: render.EventUpdate, Row: toast}, ui.ErrColor}, - // NotReady AllNS - {"", render.RowEvent{Kind: render.EventUpdate, Row: notReadyNS}, ui.ErrColor}, - // NotReady Namespaced - {"blee", render.RowEvent{Kind: render.EventUpdate, Row: notReady}, ui.ErrColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, podColorer(u.ns, u.r)) - } -} diff --git a/internal/view/command.go b/internal/view/command.go index cb567b92..1c1be055 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -5,12 +5,15 @@ import ( "regexp" "strings" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/resource" "github.com/rs/zerolog/log" ) +var customViewers MetaViewers + type command struct { app *App } @@ -19,6 +22,18 @@ func newCommand(app *App) *command { return &command{app: app} } +func (c *command) Init() error { + if err := dao.Load(c.app.factory); err != nil { + return err + } + if err := loadAliases(); err != nil { + return err + } + customViewers = loadCustomViewers() + + return nil +} + func (c *command) defaultCmd() { cmd := c.app.Config.ActiveView() if !c.run(cmd) { @@ -53,24 +68,14 @@ func (c *command) isK9sCmd(cmd string) bool { return false } -// load scrape api for resources and populate aliases. -func (c *command) load() MetaViewers { - vv := make(MetaViewers, 100) - resourceViews(c.app.Conn(), vv) - allCRDs(c.app.factory, vv) - - return vv -} - func (c *command) viewMetaFor(cmd string) (string, *MetaViewer) { - vv := c.load() 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 "", nil } - v, ok := vv[gvr] + v, ok := customViewers[dao.GVR(gvr)] if !ok { log.Error().Err(fmt.Errorf("Huh? `%s` viewer not found", gvr)).Msg("MetaViewer Failed") c.app.Flash().Warnf("Huh? viewer for %s not found", cmd) @@ -100,6 +105,7 @@ func (c *command) run(cmd string) bool { view := c.componentFor(gvr, v) return c.exec(gvr, view) default: + // checks if command includes a namespace ns := c.app.Config.ActiveNamespace() if len(cmds) == 2 { ns = cmds[1] @@ -118,19 +124,20 @@ func (c *command) componentFor(gvr string, v *MetaViewer) ResourceViewer { } var view ResourceViewer - if v.viewFn != nil { + if v.viewerFn != nil { log.Debug().Msgf("Custom viewer for %s", gvr) - view = v.viewFn(v.kind, gvr, r) + view = v.viewerFn(dao.GVR(gvr)) + // view = NewGeneric(dao.GVR(gvr)) } else { log.Debug().Msgf("Standard viewer for %s", gvr) - view = NewResource(v.kind, gvr, r) + view = NewResource("BLAH", gvr, r) } switch o := view.(type) { case TableViewer: - o.GetTable().SetColorerFn(v.colorerFn) - o.GetTable().SetEnterFn(v.enterFn) - o.GetTable().SetDecorateFn(v.decorateFn) + if v.enterFn != nil { + o.GetTable().SetEnterFn(v.enterFn) + } } return view diff --git a/internal/view/container.go b/internal/view/container.go index 44e5588b..41029c8c 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/derailed/k9s/internal/k8s" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" @@ -42,7 +43,7 @@ func (c *Container) Init(ctx context.Context) error { } c.SetEnvFn(c.k9sEnv) c.GetTable().SetEnterFn(c.viewLogs) - c.GetTable().SetColorerFn(containerColorer) + c.GetTable().SetColorerFn(render.Container{}.ColorerFunc()) c.bindKeys() return nil @@ -54,12 +55,12 @@ func (c *Container) Name() string { return containerTitle } func (c *Container) bindKeys() { c.Actions().Delete(tcell.KeyCtrlSpace, ui.KeySpace) c.Actions().Add(ui.KeyActions{ - ui.KeyShiftF: ui.NewKeyAction("PortForward", c.portFwdCmd, true), - ui.KeyS: ui.NewKeyAction("Shell", c.shellCmd, true), - ui.KeyShiftC: ui.NewKeyAction("Sort CPU", c.GetTable().SortColCmd(6, false), false), - ui.KeyShiftM: ui.NewKeyAction("Sort MEM", c.GetTable().SortColCmd(7, false), false), - ui.KeyShiftX: ui.NewKeyAction("Sort CPU%", c.GetTable().SortColCmd(8, false), false), - ui.KeyShiftZ: ui.NewKeyAction("Sort MEM%", c.GetTable().SortColCmd(9, false), false), + tcell.KeyCtrlF: ui.NewKeyAction("PortForward", c.portFwdCmd, true), + ui.KeyS: ui.NewKeyAction("Shell", c.shellCmd, true), + ui.KeyShiftC: ui.NewKeyAction("Sort CPU", c.GetTable().SortColCmd(6, false), false), + ui.KeyShiftM: ui.NewKeyAction("Sort MEM", c.GetTable().SortColCmd(7, false), false), + ui.KeyShiftX: ui.NewKeyAction("Sort CPU%", c.GetTable().SortColCmd(8, false), false), + ui.KeyShiftZ: ui.NewKeyAction("Sort MEM%", c.GetTable().SortColCmd(9, false), false), }) } @@ -75,6 +76,7 @@ func (c *Container) k9sEnv() K9sEnv { func (c *Container) selectedContainer() string { log.Debug().Msgf("Container SELECTED %s", c.GetTable().GetSelectedItem()) tokens := strings.Split(c.GetTable().GetSelectedItem(), "/") + return tokens[0] } @@ -120,7 +122,7 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - portC := c.GetTable().GetSelectedCell(10) + portC := c.GetTable().GetSelectedCell(11) ports := strings.Split(portC, ",") if len(ports) == 0 { c.App().Flash().Err(errors.New("Container exposes no ports")) diff --git a/internal/view/context.go b/internal/view/context.go index f71c9bb4..7226ed0e 100644 --- a/internal/view/context.go +++ b/internal/view/context.go @@ -1,13 +1,14 @@ package view import ( - "context" "errors" "strings" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" ) // Context presents a context viewer. @@ -16,24 +17,19 @@ type Context struct { } // NewContext return a new context viewer. -func NewContext(title, gvr string, list resource.List) ResourceViewer { - return &Context{ - ResourceViewer: NewResource(title, gvr, list).(ResourceViewer), +func NewContext(gvr dao.GVR) ResourceViewer { + c := Context{ + ResourceViewer: NewGeneric(gvr), } -} - -func (c *Context) Init(ctx context.Context) error { c.GetTable().SetEnterFn(c.useCtx) - if err := c.ResourceViewer.Init(ctx); err != nil { - return err - } c.GetTable().SetSelectedFn(c.cleanser) - c.bindKeys() + c.GetTable().SetColorerFn(render.Context{}.ColorerFunc()) + c.BindKeys() - return nil + return &c } -func (c *Context) bindKeys() { +func (c *Context) BindKeys() { c.Actions().Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace) } @@ -43,14 +39,14 @@ func (c *Context) useCtx(app *App, _, res, sel string) { return } if !app.gotoResource("po") { - app.Flash().Err(errors.New("Goto pod failed")) + app.Flash().Err(errors.New("goto pod failed")) } } func (*Context) cleanser(s string) string { name := strings.TrimSpace(s) - if strings.HasSuffix(name, "*") { - name = strings.TrimRight(name, "*") + if strings.HasSuffix(name, "(*)") { + name = strings.TrimRight(name, "(*)") } if strings.HasSuffix(name, "(𝜟)") { name = strings.TrimRight(name, "(𝜟)") @@ -59,12 +55,24 @@ func (*Context) cleanser(s string) string { } func (c *Context) useContext(name string) error { - ctx := c.cleanser(name) - if err := c.List().Resource().(*resource.Context).Switch(ctx); err != nil { + res, err := dao.AccessorFor(c.App().factory, dao.GVR(c.GVR())) + if err != nil { + return nil + } + + switcher, ok := res.(dao.Switchable) + if !ok { + return errors.New("Expecting a switchable resource") + } + + log.Debug().Msgf("Context %q", name) + ctx, _ := namespaced(name) + ctx = c.cleanser(ctx) + if err := switcher.Switch(ctx); err != nil { return err } - if err := c.App().switchCtx(name, false); err != nil { + if err := c.App().switchCtx(ctx, false); err != nil { return err } c.Refresh() diff --git a/internal/view/details.go b/internal/view/details.go index 6f79a555..36f9363f 100644 --- a/internal/view/details.go +++ b/internal/view/details.go @@ -85,7 +85,7 @@ func (d *Details) Hints() model.MenuHints { func (d *Details) bindKeys() { d.actions.Set(ui.KeyActions{ - tcell.KeyEscape: ui.NewKeyAction("Back", d.backCmd, true), + tcell.KeyEscape: ui.NewKeyAction("Back", d.app.PrevCmd, true), tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, true), ui.KeyC: ui.NewKeyAction("Copy", d.cpCmd, true), }) @@ -121,10 +121,6 @@ func (d *Details) cpCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (d *Details) backCmd(evt *tcell.EventKey) *tcell.EventKey { - return d.app.PrevCmd(evt) -} - func (d *Details) SetSubject(s string) { d.subject = s } diff --git a/internal/view/dp.go b/internal/view/dp.go index 5c1d07bf..7cd1701b 100644 --- a/internal/view/dp.go +++ b/internal/view/dp.go @@ -1,12 +1,14 @@ package view import ( - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/apps/v1" + appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" ) const scaleDialogKey = "scale" @@ -17,12 +19,12 @@ type Deploy struct { } // NewDeploy returns a new deployment view. -func NewDeploy(title, gvr string, list resource.List) ResourceViewer { +func NewDeploy(gvr dao.GVR) ResourceViewer { d := Deploy{ ResourceViewer: NewRestartExtender( NewScaleExtender( NewLogsExtender( - NewResource(title, gvr, list), + NewGeneric(gvr), func() string { return "" }, ), ), @@ -30,6 +32,7 @@ func NewDeploy(title, gvr string, list resource.List) ResourceViewer { } d.BindKeys() d.GetTable().SetEnterFn(d.showPods) + d.GetTable().SetColorerFn(render.Deployment{}.ColorerFunc()) return &d } @@ -43,16 +46,18 @@ func (d *Deploy) BindKeys() { func (d *Deploy) showPods(app *App, _, res, sel string) { ns, n := namespaced(sel) - dep, err := k8s.NewDeployment(app.Conn()).Get(ns, n) + o, err := app.factory.Get(ns, d.GVR(), n, labels.Everything()) if err != nil { app.Flash().Err(err) return } - dp, ok := dep.(*v1.Deployment) - if !ok { - log.Fatal().Msg("Expecting valid deployment") + var dp appsv1.Deployment + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp) + if err != nil { + app.Flash().Err(err) } + showPodsFromSelector(app, ns, dp.Spec.Selector) } diff --git a/internal/view/ds.go b/internal/view/ds.go index 4618d38c..c655bcd5 100644 --- a/internal/view/ds.go +++ b/internal/view/ds.go @@ -1,29 +1,31 @@ package view import ( - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" - "github.com/rs/zerolog/log" appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" ) type DaemonSet struct { ResourceViewer } -func NewDaemonSet(title, gvr string, list resource.List) ResourceViewer { +func NewDaemonSet(gvr dao.GVR) ResourceViewer { d := DaemonSet{ ResourceViewer: NewRestartExtender( NewLogsExtender( - NewResource(title, gvr, list), + NewGeneric(gvr), func() string { return "" }, ), ), } d.BindKeys() d.GetTable().SetEnterFn(d.showPods) + d.GetTable().SetColorerFn(render.DaemonSet{}.ColorerFunc()) return &d } @@ -37,21 +39,17 @@ func (d *DaemonSet) BindKeys() { func (d *DaemonSet) showPods(app *App, _, res, sel string) { ns, n := namespaced(sel) - dset, err := k8s.NewDaemonSet(app.Conn()).Get(ns, n) + o, err := app.factory.Get(ns, d.GVR(), n, labels.Everything()) if err != nil { d.App().Flash().Err(err) return } - ds, ok := dset.(*appsv1.DaemonSet) - if !ok { - log.Fatal().Msg("Expecting a valid ds") - } - l, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector) + var ds appsv1.DaemonSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) if err != nil { - app.Flash().Err(err) - return + d.App().Flash().Err(err) } - showPods(app, ns, l.String(), "") + showPodsFromSelector(app, ns, ds.Spec.Selector) } diff --git a/internal/view/exec.go b/internal/view/exec.go index 4ea5cac6..dc5e39e2 100644 --- a/internal/view/exec.go +++ b/internal/view/exec.go @@ -24,6 +24,9 @@ func runK(clear bool, app *App, args ...string) bool { } func run(clear bool, app *App, bin string, bg bool, args ...string) bool { + app.Halt() + defer app.Resume() + return app.Suspend(func() { if err := execute(clear, bin, bg, args...); err != nil { app.Flash().Errf("Command exited: %v", err) diff --git a/internal/view/generic.go b/internal/view/generic.go new file mode 100644 index 00000000..488d82fa --- /dev/null +++ b/internal/view/generic.go @@ -0,0 +1,512 @@ +package view + +import ( + "bytes" + "context" + "errors" + "fmt" + "strconv" + "time" + + "github.com/atotto/clipboard" + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/ui/dialog" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/printers" +) + +// ContextFunc enhances a given context. +type ContextFunc func(context.Context) context.Context + +// Generic represents a generic resource vieweg. +type Generic struct { + *Table + + namespaces map[int]string + path string + gvr dao.GVR + envFn EnvFunc + meta metav1.APIResource + accessor dao.Accessor + contextFn ContextFunc +} + +// NewGeneric returns a new vieweg. +func NewGeneric(gvr dao.GVR) *Generic { + return &Generic{ + Table: NewTable(string(gvr)), + gvr: gvr, + } +} + +// Init watches all running pods in given namespace +func (g *Generic) Init(ctx context.Context) error { + log.Debug().Msgf(">>> GENERIC VIEW INIT %s", g.gvr) + var err error + g.meta, err = dao.MetaFor(g.gvr) + if err != nil { + return err + } + + if err := g.Table.Init(ctx); err != nil { + return err + } + g.Table.BaseTitle = g.meta.Kind + g.accessor, err = dao.AccessorFor(g.app.factory, g.gvr) + if err != nil { + return err + } + + g.envFn = g.defaultK9sEnv + g.Table.setFilterFn(g.filterGeneric) + g.setNamespace(g.App().Config.ActiveNamespace()) + g.refresh() + row, _ := g.GetSelection() + if row == 0 && g.GetRowCount() > 0 { + g.Select(1, 0) + } + + return nil +} + +// Start initializes updates. +func (g *Generic) Start() { + g.Stop() + + log.Debug().Msgf(">>>>>>> START %s", g.gvr) + g.Table.Start() + + var ctx context.Context + ctx, g.cancelFn = context.WithCancel(context.Background()) + go g.update(ctx) +} + +func (g *Generic) Refresh() { + g.app.QueueUpdateDraw(func() { + g.refresh() + }) +} + +// Name returns the component name. +func (g *Generic) Name() string { + return g.meta.Kind +} + +func (g *Generic) SetContextFn(f ContextFunc) { + g.contextFn = f +} + +// List returns a resource List. +func (g *Generic) List() resource.List { return nil } + +// SetEnvFn sets a function to pull viewer env vars for plugins. +func (g *Generic) SetEnvFn(f EnvFunc) { g.envFn = f } + +// SetPath set parents selector. +func (g *Generic) SetPath(p string) { g.Path = p } + +// GVR returns a resource descriptor. +func (g *Generic) GVR() string { return string(g.gvr) } + +func (g *Generic) GetTable() *Table { + return g.Table +} +func (g *Generic) filterGeneric(sel string) { + panic("NYI") + // g.list.SetLabelSelector(sel) + g.refresh() +} + +func (g *Generic) update(ctx context.Context) { + for { + select { + case <-ctx.Done(): + log.Debug().Msgf("%s updater canceled!", g.gvr) + return + case <-time.After(time.Duration(g.app.Config.K9s.GetRefreshRate()) * time.Second): + g.app.QueueUpdateDraw(func() { + g.refresh() + }) + } + } +} + +// ---------------------------------------------------------------------------- +// Actions()... + +func (g *Generic) cpCmd(evt *tcell.EventKey) *tcell.EventKey { + if !g.RowSelected() { + return evt + } + + _, n := namespaced(g.GetSelectedItem()) + log.Debug().Msgf("Copied selection to clipboard %q", n) + g.app.Flash().Info("Current selection copied to clipboard...") + if err := clipboard.WriteAll(n); err != nil { + g.app.Flash().Err(err) + } + + return nil +} + +func (g *Generic) enterCmd(evt *tcell.EventKey) *tcell.EventKey { + log.Debug().Msgf("RES ENTER CMD...") + // If in command mode run filter otherwise enter function. + if g.filterCmd(evt) == nil || !g.RowSelected() { + return nil + } + + f := g.defaultEnter + if g.enterFn != nil { + log.Debug().Msgf("Found custom enter") + f = g.enterFn + } + f(g.app, g.Data.Namespace, string(g.gvr), g.GetSelectedItem()) + + return nil +} + +func (g *Generic) refreshCmd(*tcell.EventKey) *tcell.EventKey { + g.app.Flash().Info("Refreshing...") + g.refresh() + return nil +} + +func (g *Generic) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { + selections := g.GetSelectedItems() + if len(selections) == 0 { + return evt + } + log.Debug().Msgf("DEL SELECTIONS %#v", selections) + + var msg string + if len(selections) > 1 { + msg = fmt.Sprintf("Delete %d marked %s?", len(selections), g.gvr) + } else { + msg = fmt.Sprintf("Delete %s %s?", g.gvr, selections[0]) + } + + cancelFn := func() {} + if in(g.meta.Categories, "K9s") { + dialog.ShowConfirm(g.app.Content.Pages, "Confirm Delete", msg, func() { + g.ShowDeleted() + if len(selections) > 1 { + g.app.Flash().Infof("Delete %d marked %s", len(selections), g.gvr) + } else { + g.app.Flash().Infof("Delete resource %s %s", g.gvr, selections[0]) + } + for _, sel := range selections { + ns, n := namespaced(sel) + if err := g.accessor.(dao.Nuker).Delete(ns, n, true, true); err != nil { + g.app.Flash().Errf("Delete failed with %s", err) + } else { + g.GetTable().DeleteMark(sel) + } + } + g.refresh() + g.SelectRow(1, true) + }, cancelFn) + return nil + } + + dialog.ShowDelete(g.app.Content.Pages, msg, func(cascade, force bool) { + g.ShowDeleted() + if len(selections) > 1 { + g.app.Flash().Infof("Delete %d marked %s", len(selections), g.gvr) + } else { + g.app.Flash().Infof("Delete resource %s %s", g.gvr, selections[0]) + } + for _, sel := range selections { + ns, n := namespaced(sel) + if err := g.accessor.(dao.Nuker).Delete(ns, n, cascade, force); err != nil { + g.app.Flash().Errf("Delete failed with %s", err) + } else { + g.app.forwarders.Kill(sel) + g.GetTable().DeleteMark(sel) + } + } + g.refresh() + g.SelectRow(1, true) + }, func() {}) + return nil +} + +func (g *Generic) defaultEnter(app *App, ns, _, sel string) { + log.Debug().Msgf("--------- Resource %q Verbs %v", sel, g.meta.Verbs) + ns, n := namespaced(sel) + yaml, err := dao.Describe(g.app.Conn(), g.gvr, ns, n) + if err != nil { + g.app.Flash().Errf("Describe command failed: %s", err) + return + } + + details := NewDetails("Describe") + details.SetSubject(sel) + details.SetTextColor(g.app.Styles.FgColor()) + details.SetText(colorizeYAML(g.app.Styles.Views().Yaml, yaml)) + details.ScrollToBeginning() + g.app.inject(details) +} + +func (g *Generic) describeCmd(evt *tcell.EventKey) *tcell.EventKey { + log.Debug().Msgf("DESCRIBE %t -- %#v", g.RowSelected(), g.GetSelectedItems()) + if !g.RowSelected() { + return evt + } + g.defaultEnter(g.app, g.Data.Namespace, string(g.gvr), g.GetSelectedItem()) + + return nil +} + +func (g *Generic) viewCmd(evt *tcell.EventKey) *tcell.EventKey { + if !g.RowSelected() { + return evt + } + + sel := g.GetSelectedItem() + ns, n := resource.Namespaced(sel) + if ns == "" { + ns = g.Data.Namespace + } + log.Debug().Msgf("------ NAMESPACES %q vs %q", ns, g.Data.Namespace) + o, err := g.app.factory.Get(ns, string(g.gvr), n, labels.Everything()) + if err != nil { + g.app.Flash().Errf("Unable to get resource %s", err) + return nil + } + + raw, err := toYAML(o) + if err != nil { + g.app.Flash().Errf("Unable to marshal resource %s", err) + return nil + } + + details := NewDetails("YAML") + details.SetSubject(sel) + details.SetTextColor(g.app.Styles.FgColor()) + details.SetText(colorizeYAML(g.app.Styles.Views().Yaml, raw)) + details.ScrollToBeginning() + g.app.inject(details) + + return nil +} + +func toYAML(o runtime.Object) (string, error) { + var ( + buff bytes.Buffer + p printers.YAMLPrinter + ) + err := p.PrintObj(o, &buff) + if err != nil { + log.Error().Msgf("Marshal Error %v", err) + return "", err + } + + return buff.String(), nil +} + +func (g *Generic) editCmd(evt *tcell.EventKey) *tcell.EventKey { + if !g.RowSelected() { + return evt + } + + g.Stop() + defer g.Start() + { + ns, po := namespaced(g.GetSelectedItem()) + args := make([]string, 0, 10) + args = append(args, "edit") + args = append(args, g.meta.Kind) + args = append(args, "-n", ns) + args = append(args, "--context", g.app.Config.K9s.CurrentContext) + if cfg := g.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { + args = append(args, "--kubeconfig", *cfg) + } + if !runK(true, g.app, append(args, po)...) { + g.app.Flash().Err(errors.New("Edit exec failed")) + } + } + + return evt +} + +func (g *Generic) setNamespace(ns string) { + if !g.meta.Namespaced { + g.Data.Namespace = render.ClusterWide + return + } + if g.Data.Namespace == ns { + return + } + + if ns == render.NamespaceAll { + ns = render.AllNamespaces + } + log.Debug().Msgf("!!!!!! SETTING NS %q", ns) + g.Data.Namespace = ns + g.Data.RowEvents = g.Data.RowEvents.Clear() +} + +func (g *Generic) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey { + i, _ := strconv.Atoi(string(evt.Rune())) + ns := g.namespaces[i] + if ns == "" { + ns = render.NamespaceAll + } + + g.app.switchNS(ns) + g.setNamespace(ns) + g.app.Flash().Infof("Viewing namespace `%s`...", ns) + g.refresh() + g.UpdateTitle() + g.SelectRow(1, true) + g.app.CmdBuff().Reset() + if err := g.app.Config.SetActiveNamespace(g.Data.Namespace); err != nil { + log.Error().Err(err).Msg("Config save NS failed!") + } + if err := g.app.Config.Save(); err != nil { + log.Error().Err(err).Msg("Config save failed!") + } + + return nil +} + +func (g *Generic) refresh() { + if g.app.Conn() == nil { + log.Error().Msg("No api connection") + return + } + + log.Debug().Msgf("REFRESHING (%q) in ns %q", g.gvr, g.Data.Namespace) + ctx := g.defaultContext() + if g.contextFn != nil { + ctx = g.contextFn(ctx) + } + data, err := dao.Reconcile(ctx, g.Table.Data, g.gvr) + if err != nil { + g.app.Flash().Err(err) + } + g.refreshActions() + g.Update(data) +} + +func (g *Generic) defaultContext() context.Context { + ctx := context.WithValue(context.Background(), internal.KeyFactory, g.app.factory) + ctx = context.WithValue(ctx, internal.KeySelection, g.Path) + ctx = context.WithValue(ctx, internal.KeyLabels, "") + ctx = context.WithValue(ctx, internal.KeyFields, "") + + return ctx +} + +func (g *Generic) namespaceActions(aa ui.KeyActions) { + if g.app.Conn() == nil || !g.meta.Namespaced { + return + } + g.namespaces = make(map[int]string, config.MaxFavoritesNS) + aa[tcell.Key(ui.NumKeys[0])] = ui.NewKeyAction(resource.AllNamespace, g.switchNamespaceCmd, true) + g.namespaces[0] = resource.AllNamespace + index := 1 + for _, n := range g.app.Config.FavNamespaces() { + if n == resource.AllNamespace { + continue + } + aa[tcell.Key(ui.NumKeys[index])] = ui.NewKeyAction(n, g.switchNamespaceCmd, true) + g.namespaces[index] = n + index++ + } +} + +func (g *Generic) refreshActions() { + aa := ui.KeyActions{ + ui.KeyC: ui.NewKeyAction("Copy", g.cpCmd, false), + tcell.KeyEnter: ui.NewKeyAction("View", g.enterCmd, false), + tcell.KeyCtrlR: ui.NewKeyAction("Refresh", g.refreshCmd, false), + } + g.namespaceActions(aa) + + if dao.Can(g.meta.Verbs, "edit") { + aa[ui.KeyE] = ui.NewKeyAction("Edit", g.editCmd, true) + } + if dao.Can(g.meta.Verbs, "delete") { + aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", g.deleteCmd, true) + } + if dao.Can(g.meta.Verbs, "view") { + aa[ui.KeyY] = ui.NewKeyAction("YAML", g.viewCmd, true) + } + if dao.Can(g.meta.Verbs, "describe") { + aa[ui.KeyD] = ui.NewKeyAction("Describe", g.describeCmd, true) + } + g.customActions(aa) + g.Actions().Set(aa) +} + +func (g *Generic) customActions(aa ui.KeyActions) { + pp := config.NewPlugins() + if err := pp.Load(); err != nil { + log.Warn().Msgf("No plugin configuration found") + return + } + + for k, plugin := range pp.Plugin { + if !in(plugin.Scopes, g.meta.Name) { + continue + } + key, err := asKey(plugin.ShortCut) + if err != nil { + log.Error().Err(err).Msg("Unable to map shortcut to a key") + continue + } + _, ok := aa[key] + if ok { + log.Error().Err(fmt.Errorf("Doh! you are trying to overide an existing command `%s", k)).Msg("Invalid shortcut") + continue + } + aa[key] = ui.NewKeyAction( + plugin.Description, + g.execCmd(plugin.Command, plugin.Background, plugin.Args...), + true) + } +} + +func (g *Generic) execCmd(bin string, bg bool, args ...string) ui.ActionHandler { + return func(evt *tcell.EventKey) *tcell.EventKey { + if !g.RowSelected() { + + return evt + } + + var ( + env = g.envFn() + aa = make([]string, len(args)) + err error + ) + for i, a := range args { + aa[i], err = env.envFor(a) + if err != nil { + log.Error().Err(err).Msg("Args match failed") + return nil + } + } + + if run(true, g.app, bin, bg, aa...) { + g.app.Flash().Info("Custom CMD launched!") + } else { + g.app.Flash().Info("Custom CMD failed!") + } + return nil + } +} + +func (g *Generic) defaultK9sEnv() K9sEnv { + return defaultK9sEnv(g.app, g.GetSelectedItem(), g.GetRow()) +} diff --git a/internal/view/help.go b/internal/view/help.go index d464fdb0..01f12afb 100644 --- a/internal/view/help.go +++ b/internal/view/help.go @@ -48,15 +48,11 @@ func (v *Help) Init(ctx context.Context) (err error) { func (v *Help) bindKeys() { v.Actions().Set(ui.KeyActions{ - tcell.KeyEsc: ui.NewKeyAction("Back", v.backCmd, true), - tcell.KeyEnter: ui.NewKeyAction("Back", v.backCmd, false), + tcell.KeyEsc: ui.NewKeyAction("Back", v.app.PrevCmd, true), + tcell.KeyEnter: ui.NewKeyAction("Back", v.app.PrevCmd, false), }) } -func (v *Help) backCmd(evt *tcell.EventKey) *tcell.EventKey { - return v.app.PrevCmd(evt) -} - func (v *Help) showHelp() model.MenuHints { return model.MenuHints{ { diff --git a/internal/view/help_test.go b/internal/view/help_test.go index 71e829de..6eba2005 100644 --- a/internal/view/help_test.go +++ b/internal/view/help_test.go @@ -22,6 +22,6 @@ func TestHelpNew(t *testing.T) { assert.Equal(t, 32, v.GetRowCount()) assert.Equal(t, 10, v.GetColumnCount()) - assert.Equal(t, "", v.GetCell(1, 0).Text) - assert.Equal(t, "Copy", v.GetCell(1, 1).Text) + assert.Equal(t, "", v.GetCell(1, 0).Text) + assert.Equal(t, "Erase", v.GetCell(1, 1).Text) } diff --git a/internal/view/log.go b/internal/view/log.go index 1c657660..8574c772 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -9,7 +9,9 @@ import ( "strings" "time" + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" @@ -39,13 +41,15 @@ type Log struct { cancelFn context.CancelFunc previous bool list resource.List + gvr dao.GVR } var _ model.Component = &Log{} // NewLog returns a new viewer. -func NewLog(path, co string, l resource.List, prev bool) *Log { +func NewLog(gvr dao.GVR, path, co string, l resource.List, prev bool) *Log { return &Log{ + gvr: gvr, Flex: tview.NewFlex(), path: path, container: co, @@ -134,7 +138,7 @@ func (l *Log) Name() string { return logTitle } func (l *Log) bindKeys() { l.logs.Actions().Set(ui.KeyActions{ - tcell.KeyEscape: ui.NewKeyAction("Back", l.backCmd, true), + tcell.KeyEscape: ui.NewKeyAction("Back", l.app.PrevCmd, true), ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true), ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.ToggleAutoScrollCmd, true), ui.KeyG: ui.NewKeyAction("Top", l.topCmd, false), @@ -146,36 +150,38 @@ func (l *Log) bindKeys() { } func (l *Log) doLoad() error { - // BOZO!! - // l.logs.Clear() - // l.setTitle(l.path, l.container) + l.logs.Clear() + l.setTitle(l.path, l.container) - // var ctx context.Context - // ctx = context.WithValue(context.Background(), resource.IKey("informer"), l.app.informers.ActiveInformer()) - // ctx, l.cancelFn = context.WithCancel(ctx) + var ctx context.Context + ctx = context.WithValue(context.Background(), internal.KeyFactory, l.app.factory) + ctx, l.cancelFn = context.WithCancel(ctx) - // c := make(chan string, 10) - // go l.updateLogs(ctx, c, logBuffSize) + c := make(chan string, 10) + go l.updateLogs(ctx, c, logBuffSize) - // res, ok := l.list.Resource().(resource.Tailable) - // if !ok { - // close(c) - // return fmt.Errorf("Resource %T is not tailable", l.list.Resource()) - // } + accessor, err := dao.AccessorFor(l.app.factory, l.gvr) + if err != nil { + return err + } + logger, ok := accessor.(dao.Loggable) + if !ok { + return fmt.Errorf("Resource %s is not tailable", l.gvr) + } - // if err := res.Logs(ctx, c, l.logOpts(l.path, l.container, l.previous)); err != nil { - // l.cancelFn() - // close(c) - // return err - // } + if err := logger.TailLogs(ctx, c, l.logOpts(l.path, l.container, l.previous)); err != nil { + l.cancelFn() + close(c) + return err + } return nil } -func (l *Log) logOpts(path, co string, prevLogs bool) resource.LogOptions { +func (l *Log) logOpts(path, co string, prevLogs bool) dao.LogOptions { ns, po := namespaced(path) - return resource.LogOptions{ - Fqn: resource.Fqn{ + return dao.LogOptions{ + Fqn: dao.Fqn{ Namespace: ns, Name: po, Container: co, @@ -319,10 +325,6 @@ func (l *Log) ToggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (l *Log) backCmd(evt *tcell.EventKey) *tcell.EventKey { - return l.app.PrevCmd(evt) -} - func (l *Log) topCmd(evt *tcell.EventKey) *tcell.EventKey { l.app.Flash().Info("Top of logs...") l.logs.ScrollToBeginning() diff --git a/internal/view/logs_extender.go b/internal/view/logs_extender.go index 1629aca8..b2b8c06e 100644 --- a/internal/view/logs_extender.go +++ b/internal/view/logs_extender.go @@ -1,6 +1,7 @@ package view import ( + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" ) @@ -51,6 +52,6 @@ func (l *LogsExtender) showLogs(path string, prev bool) { if l.containerFn != nil { co = l.containerFn() } - log := NewLog(path, co, l.List(), prev) + log := NewLog(dao.GVR(l.GVR()), path, co, l.List(), prev) l.App().inject(log) } diff --git a/internal/view/no.go b/internal/view/node.go similarity index 94% rename from internal/view/no.go rename to internal/view/node.go index 4700da28..f1ebeb8d 100644 --- a/internal/view/no.go +++ b/internal/view/node.go @@ -3,6 +3,7 @@ package view import ( "context" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" @@ -56,7 +57,7 @@ func showPods(app *App, path, labelSel, fieldSel string) { list.SetFieldSelector(fieldSel) v := NewPod(path, "v1/pods", list) - v.GetTable().SetColorerFn(podColorer) + v.GetTable().SetColorerFn(render.Pod{}.ColorerFunc()) ns, _ := namespaced(path) if err := app.Config.SetActiveNamespace(ns); err != nil { diff --git a/internal/view/ns.go b/internal/view/ns.go index 2c830d90..e16e4f6a 100644 --- a/internal/view/ns.go +++ b/internal/view/ns.go @@ -1,10 +1,11 @@ package view import ( - "context" "regexp" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" @@ -23,25 +24,20 @@ type Namespace struct { } // NewNamespace returns a new viewer -func NewNamespace(title, gvr string, list resource.List) ResourceViewer { - return &Namespace{ - ResourceViewer: NewResource(title, gvr, list), +func NewNamespace(gvr dao.GVR) ResourceViewer { + n := Namespace{ + ResourceViewer: NewGeneric(gvr), } -} - -func (n *Namespace) Init(ctx context.Context) error { n.GetTable().SetDecorateFn(n.decorate) + n.GetTable().SetColorerFn(render.Namespace{}.ColorerFunc()) n.GetTable().SetEnterFn(n.switchNs) - if err := n.ResourceViewer.Init(ctx); err != nil { - return err - } n.GetTable().SetSelectedFn(n.cleanser) - n.bindKeys() + n.BindKeys() - return nil + return &n } -func (n *Namespace) bindKeys() { +func (n *Namespace) BindKeys() { n.Actions().Add(ui.KeyActions{ ui.KeyU: ui.NewKeyAction("Use", n.useNsCmd, true), }) @@ -75,35 +71,42 @@ func (n *Namespace) useNamespace(ns string) { } func (*Namespace) cleanser(s string) string { + log.Debug().Msgf("NS CLEANZ %q", s) return nsCleanser.ReplaceAllString(s, `$1`) } -func (n *Namespace) decorate(data resource.TableData) resource.TableData { - return resource.TableData{} - // BOZO!! - // if n.App().Conn() == nil { - // return resource.TableData{} - // } +func (n *Namespace) decorate(data render.TableData) render.TableData { + if n.App().Conn() == nil { + return render.TableData{} + } - // if _, ok := data.Rows[resource.AllNamespaces]; !ok { - // if err := n.App().Conn().CheckNSAccess(""); err == nil { - // data.Rows[resource.AllNamespace] = &resource.RowEvent{ - // Action: resource.Unchanged, - // Fields: resource.Row{resource.AllNamespace, "Active", "0"}, - // Deltas: resource.Row{"", "", ""}, - // } - // } - // } - // for k, r := range data.Rows { - // if config.InList(n.App().Config.FavNamespaces(), k) { - // r.Fields[0] += favNSIndicator - // r.Action = resource.Unchanged - // } - // if n.App().Config.ActiveNamespace() == k { - // r.Fields[0] += defaultNSIndicator - // r.Action = resource.Unchanged - // } - // } + log.Debug().Msgf("CLONING %q", data.Namespace) + // don't want to change the cache here thus need to clone!! + res := data.Clone() + // checks if all ns is in the list if not add it. + if _, ok := data.RowEvents.FindIndex(render.NamespaceAll); !ok { + res.RowEvents = append(render.RowEvents{ + render.RowEvent{ + Kind: render.EventUnchanged, + Row: render.Row{ + ID: render.AllNamespaces, + Fields: render.Fields{render.NamespaceAll, "Active", "0"}, + }, + }, + }, + res.RowEvents...) + } - // return data + for _, re := range res.RowEvents { + if config.InList(n.App().Config.FavNamespaces(), re.Row.ID) { + re.Row.Fields[0] += favNSIndicator + re.Kind = render.EventUnchanged + } + if n.App().Config.ActiveNamespace() == re.Row.ID { + re.Row.Fields[0] += defaultNSIndicator + re.Kind = render.EventUnchanged + } + } + + return res } diff --git a/internal/view/page_stack.go b/internal/view/page_stack.go index 085626d2..260eca3e 100644 --- a/internal/view/page_stack.go +++ b/internal/view/page_stack.go @@ -34,6 +34,8 @@ func (p *PageStack) StackPushed(c model.Component) { ctx := context.WithValue(context.Background(), ui.KeyApp, p.app) if err := c.Init(ctx); err != nil { log.Error().Err(err).Msgf("Component Init failed!") + p.app.Flash().Err(err) + return } c.Start() p.app.SetFocus(c) diff --git a/internal/view/picker.go b/internal/view/picker.go index a86113bb..7330f1fb 100644 --- a/internal/view/picker.go +++ b/internal/view/picker.go @@ -25,12 +25,18 @@ func NewPicker() *Picker { } func (v *Picker) Init(ctx context.Context) error { + app, err := extractApp(ctx) + if err != nil { + return err + } + v.actions[tcell.KeyEscape] = ui.NewKeyAction("Back", app.PrevCmd, true) + v.SetBorder(true) v.SetMainTextColor(tcell.ColorWhite) v.ShowSecondaryText(false) v.SetShortcutColor(tcell.ColorAqua) v.SetSelectedBackgroundColor(tcell.ColorAqua) - v.SetTitle(" [aqua::b]Container Selector ") + v.SetTitle(" [aqua::b]Containers Picker ") v.SetInputCapture(func(evt *tcell.EventKey) *tcell.EventKey { if a, ok := v.actions[evt.Key()]; ok { a.Action(evt) diff --git a/internal/view/pod.go b/internal/view/pod.go index 0e901920..f208abb0 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -3,6 +3,7 @@ package view import ( "errors" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" @@ -48,7 +49,7 @@ func (p *Pod) BindKeys() { ui.KeyShiftM: ui.NewKeyAction("Sort MEM", p.GetTable().SortColCmd(5, false), false), ui.KeyShiftX: ui.NewKeyAction("Sort CPU%", p.GetTable().SortColCmd(6, false), false), ui.KeyShiftZ: ui.NewKeyAction("Sort MEM%", p.GetTable().SortColCmd(7, false), false), - ui.KeyShiftD: ui.NewKeyAction("Sort IP", p.GetTable().SortColCmd(8, true), false), + ui.KeyShiftI: ui.NewKeyAction("Sort IP", p.GetTable().SortColCmd(8, true), false), ui.KeyShiftO: ui.NewKeyAction("Sort Node", p.GetTable().SortColCmd(9, true), false), }) } @@ -100,6 +101,12 @@ func (p *Pod) shellCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } + row := p.GetTable().GetSelectedRowIndex() + status := ui.TrimCell(p.GetTable().SelectTable, row, p.GetTable().NameColIndex()+2) + if status != render.Running { + p.App().Flash().Errf("%s is not in a running state", sel) + return nil + } cc, err := fetchContainers(p.List(), sel, false) if err != nil { p.App().Flash().Errf("Unable to retrieve containers %s", err) diff --git a/internal/view/policy.go b/internal/view/policy.go index 0f0824af..e46aad76 100644 --- a/internal/view/policy.go +++ b/internal/view/policy.go @@ -5,12 +5,14 @@ import ( "fmt" "time" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" ) const ( @@ -20,8 +22,6 @@ const ( sa = "ServiceAccount" ) -var policyHeader = append(resource.Row{"NAMESPACE", "NAME", "API GROUP", "BINDING"}, rbacHeaderVerbs...) - type ( namespacedRole struct { ns, role string @@ -34,36 +34,30 @@ type ( cancel context.CancelFunc subjectKind string subjectName string - cache resource.RowEvents + cache render.RowEvents } ) // NewPolicy returns a new viewer. func NewPolicy(app *App, subject, name string) *Policy { - p := Policy{} - p.subjectKind, p.subjectName = mapSubject(subject), name - p.Table = NewTable(policyTitle) - p.Table.Path = p.subjectKind + ":" + p.subjectName - p.SetColorerFn(rbacColorer) - p.bindKeys() - - return &p + return &Policy{ + Table: NewTable(policyTitle), + subjectKind: mapSubject(subject), + subjectName: name, + } } // Init the view. func (p *Policy) Init(ctx context.Context) error { - defer func(t time.Time) { - log.Debug().Msgf("Policy elapsed %v", time.Since(t)) - }(time.Now()) - + p.Table.Path = p.subjectKind + ":" + p.subjectName if err := p.Table.Init(ctx); err != nil { return err } + p.SetColorerFn(render.Policy{}.ColorerFunc()) p.bindKeys() - p.SetSortCol(1, len(rbacHeader), false) + p.SetSortCol(1, len(render.Policy{}.Header(render.AllNamespaces)), false) p.refresh() p.SelectRow(1, true) - p.Start() return nil } @@ -101,10 +95,12 @@ func (p *Policy) bindKeys() { } func (p *Policy) getTitle() string { - return fmt.Sprintf(rbacTitleFmt, policyTitle, p.subjectKind+":"+p.subjectName) + return fmt.Sprintf(rbacTitleFmt, policyTitle, p.subjectKind+":"+p.subjectName, p.GetRowCount()) } func (p *Policy) refresh() { + log.Debug().Msgf(">>>>>>>>>>>>>>> Refreshing Policies") + // BOZO!! defer func(t time.Time) { log.Debug().Msgf("Policy Refresh elapsed %v", time.Since(t)) }(time.Now()) @@ -141,14 +137,15 @@ func (p *Policy) backCmd(evt *tcell.EventKey) *tcell.EventKey { return p.app.PrevCmd(evt) } -func (p *Policy) reconcile() (resource.TableData, error) { +func (p *Policy) reconcile() (render.TableData, error) { + // BOZO!! defer func(t time.Time) { log.Debug().Msgf("Policy Reconcile elapsed %v", time.Since(t)) }(time.Now()) - var table resource.TableData + var table render.TableData - evts, errs := p.clusterPolicies() + evts, errs := p.fetchClusterRoleBindings() if len(errs) > 0 { for _, err := range errs { log.Error().Err(err).Msg("Unable to find cluster policies") @@ -164,8 +161,8 @@ func (p *Policy) reconcile() (resource.TableData, error) { return table, errs[0] } - for k, v := range nevts { - evts[k] = v + for _, v := range nevts { + evts = append(evts, v) } return buildTable(p, evts), nil @@ -173,59 +170,74 @@ func (p *Policy) reconcile() (resource.TableData, error) { // Protocol... -func (p *Policy) header() resource.Row { - return policyHeader +func (p *Policy) Header() render.HeaderRow { + return render.Policy{}.Header(render.AllNamespaces) } -func (p *Policy) getCache() resource.RowEvents { +func (p *Policy) GetCache() render.RowEvents { return p.cache } -func (p *Policy) setCache(evts resource.RowEvents) { +func (p *Policy) SetCache(evts render.RowEvents) { p.cache = evts } -func (p *Policy) clusterPolicies() (resource.RowEvents, []error) { +func (p *Policy) fetchClusterRoleBindings() (render.Rows, []error) { var errs []error - evts := make(resource.RowEvents) - - crbs, err := p.app.Conn().DialOrDie().RbacV1().ClusterRoleBindings().List(metav1.ListOptions{}) + oo, err := p.app.factory.List(render.ClusterWide, "rbac.authorization.k8s.io/v1/clusterrolebindings", labels.Everything()) if err != nil { - return evts, errs + return nil, append(errs, err) } - var rr []string - for _, crb := range crbs.Items { + roles := make([]string, 0, len(oo)) + for _, o := range oo { + var crb rbacv1.ClusterRoleBinding + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crb) + if err != nil { + errs = append(errs, err) + continue + } for _, s := range crb.Subjects { if s.Kind == p.subjectKind && s.Name == p.subjectName { - rr = append(rr, crb.RoleRef.Name) + roles = append(roles, crb.RoleRef.Name) } } } - for _, r := range rr { - role, err := p.app.Conn().DialOrDie().RbacV1().ClusterRoles().Get(r, metav1.GetOptions{}) + rows := make(render.Rows, 0, len(oo)) + for _, role := range roles { + o, err := p.app.factory.Get(render.ClusterWide, "rbac.authorization.k8s.io/v1/clusterroles", role, labels.Everything()) + if err != nil { + return nil, append(errs, err) + } + var cr rbacv1.ClusterRole + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr) if err != nil { errs = append(errs, err) + continue } - for k, v := range p.parseRules("*", "CR:"+r, role.Rules) { - evts[k] = v + + for _, v := range p.parseRules("*", "CR:"+role, cr.Rules) { + rows = append(rows, v) } } - return evts, errs + return rows, errs } -func (p *Policy) loadRoleBindings() ([]namespacedRole, error) { - var rr []namespacedRole - - dial := p.app.Conn().DialOrDie().RbacV1() - rbs, err := dial.RoleBindings("").List(metav1.ListOptions{}) +func (p *Policy) fetchRoleBindings() ([]namespacedRole, error) { + oo, err := p.app.factory.List(render.AllNamespaces, "rbac.authorization.k8s.io/v1/rolebindings", labels.Everything()) if err != nil { - return rr, err + return nil, err } - for _, rb := range rbs.Items { + rr := make([]namespacedRole, 0, len(oo)) + for _, o := range oo { + var rb rbacv1.RoleBinding + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rb) + if err != nil { + return nil, err + } for _, s := range rb.Subjects { if s.Kind == p.subjectKind && s.Name == p.subjectName { rr = append(rr, namespacedRole{rb.Namespace, rb.RoleRef.Name}) @@ -236,37 +248,38 @@ func (p *Policy) loadRoleBindings() ([]namespacedRole, error) { return rr, nil } -func (p *Policy) loadRoles(errs []error, rr []namespacedRole) (resource.RowEvents, []error) { - var ( - dial = p.app.Conn().DialOrDie().RbacV1() - evts = make(resource.RowEvents) - ) +func (p *Policy) fetchClusterRoles(errs []error, rr []namespacedRole) (render.Rows, []error) { + rows := make(render.Rows, 0, len(rr)) for _, r := range rr { - if cr, err := dial.Roles(r.ns).Get(r.role, metav1.GetOptions{}); err != nil { - errs = append(errs, err) - } else { - for k, v := range p.parseRules(r.ns, "RO:"+r.role, cr.Rules) { - evts[k] = v - } + o, err := p.app.factory.Get(r.ns, "rbac.authorization.k8s.io/v1/clusterroles", r.role, labels.Everything()) + if err != nil { + return nil, append(errs, err) } + + var cr rbacv1.ClusterRole + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr) + if err != nil { + errs = append(errs, err) + continue + } + rows = append(rows, p.parseRules(r.ns, "RO:"+r.role, cr.Rules)...) } - return evts, errs + return rows, errs } -func (p *Policy) namespacedPolicies() (resource.RowEvents, []error) { +func (p *Policy) namespacedPolicies() (render.Rows, []error) { var errs []error - rr, err := p.loadRoleBindings() + roles, err := p.fetchRoleBindings() if err != nil { errs = append(errs, err) } - evts, errs := p.loadRoles(errs, rr) - return evts, errs + return p.fetchClusterRoles(errs, roles) } -func (p *Policy) parseRules(ns, binding string, rules []rbacv1.PolicyRule) resource.RowEvents { - m := make(resource.RowEvents, len(rules)) +func (p *Policy) parseRules(ns, binding string, rules []rbacv1.PolicyRule) render.Rows { + m := make(render.Rows, 0, len(rules)) for _, r := range rules { for _, grp := range r.APIGroups { for _, res := range r.Resources { @@ -276,34 +289,37 @@ func (p *Policy) parseRules(ns, binding string, rules []rbacv1.PolicyRule) resou } for _, na := range r.ResourceNames { n := fqn(k, na) - m[fqn(ns, n)] = &resource.RowEvent{ + m = append(m, render.Row{ + ID: fqn(ns, n), Fields: append(policyRow(ns, n, grp, binding), asVerbs(r.Verbs...)...), - } + }) } - m[fqn(ns, k)] = &resource.RowEvent{ + m = append(m, render.Row{ + ID: fqn(ns, k), Fields: append(policyRow(ns, k, grp, binding), asVerbs(r.Verbs...)...), - } + }) } } for _, nres := range r.NonResourceURLs { if nres[0] != '/' { nres = "/" + nres } - m[fqn(ns, nres)] = &resource.RowEvent{ - Fields: append(policyRow(ns, nres, resource.NAValue, binding), asVerbs(r.Verbs...)...), - } + m = append(m, render.Row{ + ID: fqn(ns, nres), + Fields: append(policyRow(ns, nres, "", binding), asVerbs(r.Verbs...)...), + }) } } return m } -func policyRow(ns, res, grp, binding string) resource.Row { - if grp != resource.NAValue { +func policyRow(ns, res, grp, binding string) render.Fields { + if grp != "" { grp = toGroup(grp) } - r := make(resource.Row, 0, len(policyHeader)) + r := make(render.Fields, 0, len(render.Policy{}.Header(render.AllNamespaces))) return append(r, ns, res, grp, binding) } @@ -317,3 +333,13 @@ func mapSubject(subject string) string { return user } } + +func showSAPolicy(app *App, _, _, selection string) { + _, n := namespaced(selection) + subject, err := mapFuSubject("ServiceAccount") + if err != nil { + app.Flash().Err(err) + return + } + app.inject(NewPolicy(app, subject, n)) +} diff --git a/internal/view/port_forward.go b/internal/view/port_forward.go index 8780ca8d..9bbae4c2 100644 --- a/internal/view/port_forward.go +++ b/internal/view/port_forward.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strings" "time" "github.com/derailed/k9s/internal/config" @@ -37,26 +36,29 @@ func NewPortForward(title, gvr string, list resource.List) ResourceViewer { } } +func (*PortForward) SetContextFn(ContextFunc) {} + // Init the view. func (p *PortForward) Init(ctx context.Context) error { if err := p.Table.Init(ctx); err != nil { return err } p.registerActions() - p.SetBorderFocusColor(tcell.ColorDodgerBlue) p.SetSelectedStyle(tcell.ColorWhite, tcell.ColorDodgerBlue, tcell.AttrNone) - p.SetColorerFn(forwardColorer) - p.ActiveNS = resource.AllNamespaces + p.SetColorerFn(render.Forward{}.ColorerFunc()) p.SetSortCol(p.NameColIndex()+6, 0, true) p.Select(1, 0) - - p.Start() p.refresh() return nil } +// GVR returns a resource descriptor. +func (p *PortForward) GVR() string { + return "n/a" +} + // List returns the resource list. func (p *PortForward) List() resource.List { return nil } @@ -113,7 +115,7 @@ func (p *PortForward) registerActions() { } func (p *PortForward) showBenchCmd(evt *tcell.EventKey) *tcell.EventKey { - p.app.gotoResource("be") + p.app.inject(NewBench("", "", nil)) return nil } @@ -213,26 +215,35 @@ func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (p *PortForward) hydrate() resource.TableData { - data := initHeader(len(p.app.forwarders)) - dc, dn := p.app.Bench.Benchmarks.Defaults.C, p.app.Bench.Benchmarks.Defaults.N - for _, f := range p.app.forwarders { - c, n, cfg := loadConfig(dc, dn, containerID(f.Path(), f.Container()), p.app.Bench.Benchmarks.Containers) +func (p *PortForward) hydrate() render.TableData { + var re render.Forward - ports := strings.Split(f.Ports()[0], ":") - ns, na := namespaced(f.Path()) - row := render.Row{ - ID: f.Path(), - Fields: render.Fields{ - ns, - na, - f.Container(), - strings.Join(f.Ports(), ","), - urlFor(cfg, ports[0]), - asNum(c), - asNum(n), - f.Age(), - }, + data := render.TableData{ + Header: re.Header(render.AllNamespaces), + RowEvents: make(render.RowEvents, 0, len(p.app.forwarders)), + Namespace: render.AllNamespaces, + } + + containers := p.app.Bench.Benchmarks.Containers + for _, f := range p.app.forwarders { + fqn := containerID(f.Path(), f.Container()) + cfg := benchCfg{ + c: p.app.Bench.Benchmarks.Defaults.C, + n: p.app.Bench.Benchmarks.Defaults.N, + } + if config, ok := containers[fqn]; ok { + cfg.c, cfg.n = config.C, config.N + cfg.host, cfg.path = config.HTTP.Host, config.HTTP.Path + } + + var row render.Row + fwd := forwarder{ + Forwarder: f, + BenchConfigurator: cfg, + } + if err := re.Render(fwd, render.AllNamespaces, &row); err != nil { + log.Error().Err(err).Msgf("PortForward render failed") + continue } data.RowEvents = append(data.RowEvents, render.RowEvent{Kind: render.EventAdd, Row: row}) } @@ -243,6 +254,23 @@ func (p *PortForward) hydrate() resource.TableData { // ---------------------------------------------------------------------------- // Helpers... +var _ render.PortForwarder = forwarder{} + +type forwarder struct { + render.Forwarder + render.BenchConfigurator +} + +type benchCfg struct { + c, n int + host, path string +} + +func (b benchCfg) C() int { return b.c } +func (b benchCfg) N() int { return b.n } +func (b benchCfg) Host() string { return b.host } +func (b benchCfg) HttpPath() string { return b.path } + func defaultConfig() config.BenchConfig { return config.BenchConfig{ C: config.DefaultC, @@ -254,33 +282,6 @@ func defaultConfig() config.BenchConfig { } } -func initHeader(rows int) resource.TableData { - return resource.TableData{ - // BOZO!! - // Header: resource.Row{"NAMESPACE", "NAME", "CONTAINER", "PORTS", "URL", "C", "N", "AGE"}, - // NumCols: map[string]bool{"C": true, "N": true}, - // Rows: make(resource.RowEvents, rows), - Namespace: resource.AllNamespaces, - } -} - -func loadConfig(dc, dn int, id string, cc map[string]config.BenchConfig) (int, int, config.BenchConfig) { - c, n := dc, dn - cfg, ok := cc[id] - if !ok { - return c, n, cfg - } - - if cfg.C != 0 { - c = cfg.C - } - if cfg.N != 0 { - n = cfg.N - } - - return c, n, cfg -} - func showModal(p *ui.Pages, msg string, ok func()) { m := tview.NewModal(). AddButtons([]string{"Cancel", "OK"}). @@ -313,7 +314,7 @@ func watchFS(ctx context.Context, app *App, dir, file string, cb func()) error { case evt := <-w.Events: log.Debug().Msgf("FS %s event %v", file, evt.Name) if file == "" || evt.Name == file { - log.Debug().Msgf("Capuring Event %#v", evt) + log.Debug().Msgf("Capturing Event %#v", evt) app.QueueUpdateDraw(func() { cb() }) diff --git a/internal/view/rbac.go b/internal/view/rbac.go index 8ed095d6..6f8eaaef 100644 --- a/internal/view/rbac.go +++ b/internal/view/rbac.go @@ -6,47 +6,37 @@ import ( "strings" "time" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" ) const ( ClusterRole roleKind = iota Role - all = "*" + clusterWide = "*" rbacTitle = "Rbac" - rbacTitleFmt = " [fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])" + rbacTitleFmt = " [fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] " ) var ( - rbacHeaderVerbs = resource.Row{ - "GET ", - "LIST ", - "DLIST ", - "WATCH ", - "CREATE", - "PATCH ", - "UPDATE", - "DELETE", - "EXTRAS", - } - - rbacHeader = append(resource.Row{"NAME", "API GROUP"}, rbacHeaderVerbs...) - k8sVerbs = []string{ "get", "list", - "deletecollection", "watch", "create", "patch", "update", "delete", + "deletecollection", } httpTok8sVerbs = map[string]string{ @@ -63,15 +53,17 @@ type Rbac struct { roleType roleKind roleName string - cache resource.RowEvents + path string + cache render.RowEvents } // NewRbac returns a new viewer. -func NewRbac(name string, kind roleKind) *Rbac { +func NewRbac(name string, kind roleKind, path string) *Rbac { return &Rbac{ Table: NewTable(rbacTitle), roleName: name, roleType: kind, + path: path, } } @@ -80,17 +72,18 @@ func (r *Rbac) Init(ctx context.Context) error { if err := r.Table.Init(ctx); err != nil { return err } - r.ActiveNS = r.app.Config.ActiveNamespace() - r.SetColorerFn(rbacColorer) + r.SetColorerFn(render.Rbac{}.ColorerFunc()) r.bindKeys() - - r.Start() - r.SetSortCol(1, len(rbacHeader), true) + r.SetSortCol(1, len(r.Header()), true) r.refresh() return nil } +func (r *Rbac) UpdateTitle() { + r.SetTitle(ui.SkinTitle(fmt.Sprintf(rbacTitleFmt, rbacTitle, r.path, r.GetRowCount()-1), r.app.Styles.Frame())) +} + // Start watches for viewer updates func (r *Rbac) Start() { if r.app.Conn() == nil { @@ -140,6 +133,7 @@ func (r *Rbac) refresh() { r.app.Flash().Err(err) } r.Update(data) + r.UpdateTitle() } func (r *Rbac) resetCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -152,7 +146,6 @@ func (r *Rbac) resetCmd(evt *tcell.EventKey) *tcell.EventKey { } func (r *Rbac) backCmd(evt *tcell.EventKey) *tcell.EventKey { - log.Debug().Msgf("!!!!RBAC back!!!") if r.cancelFn != nil { r.cancelFn() } @@ -165,53 +158,48 @@ func (r *Rbac) backCmd(evt *tcell.EventKey) *tcell.EventKey { return r.app.PrevCmd(evt) } -func (r *Rbac) reconcile(name string, kind roleKind) (resource.TableData, error) { - var table resource.TableData +func (r *Rbac) reconcile(name string, kind roleKind) (render.TableData, error) { + var table render.TableData - evts, err := r.rowEvents(name, kind) + rows, err := r.fetchRoles(name, kind) if err != nil { return table, err } - return buildTable(r, evts), nil + return buildTable(r, rows), nil } -func (r *Rbac) header() resource.Row { - return rbacHeader +func (r *Rbac) Header() render.HeaderRow { + return render.Rbac{}.Header(render.AllNamespaces) } -func (r *Rbac) getCache() resource.RowEvents { +func (r *Rbac) GetCache() render.RowEvents { return r.cache } -func (r *Rbac) setCache(evts resource.RowEvents) { +func (r *Rbac) SetCache(evts render.RowEvents) { r.cache = evts } -func (r *Rbac) rowEvents(name string, kind roleKind) (resource.RowEvents, error) { - var ( - evts resource.RowEvents - err error - ) - +func (r *Rbac) fetchRoles(name string, kind roleKind) (render.Rows, error) { switch kind { case ClusterRole: - evts, err = r.clusterPolicies(name) + return r.loadClusterRoles(name) case Role: - evts, err = r.namespacedPolicies(name) + return r.loadRoles(name) default: - return evts, fmt.Errorf("Expecting clusterrole/role but found %d", kind) + return nil, fmt.Errorf("Expecting clusterrole/role but found %d", kind) } - if err != nil { - log.Error().Err(err).Msg("Unable to load CR") - return evts, err - } - - return evts, nil } -func (r *Rbac) clusterPolicies(name string) (resource.RowEvents, error) { - cr, err := r.app.Conn().DialOrDie().RbacV1().ClusterRoles().Get(name, metav1.GetOptions{}) +func (r *Rbac) loadClusterRoles(name string) (render.Rows, error) { + o, err := r.app.factory.Get("-", "rbac.authorization.k8s.io/v1/clusterroles", name, labels.Everything()) + if err != nil { + return nil, err + } + + var cr rbacv1.ClusterRole + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr) if err != nil { return nil, err } @@ -219,71 +207,68 @@ func (r *Rbac) clusterPolicies(name string) (resource.RowEvents, error) { return r.parseRules(cr.Rules), nil } -func (r *Rbac) namespacedPolicies(path string) (resource.RowEvents, error) { - ns, na := namespaced(path) - cr, err := r.app.Conn().DialOrDie().RbacV1().Roles(ns).Get(na, metav1.GetOptions{}) +func (r *Rbac) loadRoles(path string) (render.Rows, error) { + ns, n := namespaced(path) + o, err := r.app.factory.Get(ns, "rbac.authorization.k8s.io/v1/roles", n, labels.Everything()) if err != nil { return nil, err } - return r.parseRules(cr.Rules), nil + var ro rbacv1.Role + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ro) + if err != nil { + return nil, err + } + + return r.parseRules(ro.Rules), nil } -func (r *Rbac) parseRules(rules []rbacv1.PolicyRule) resource.RowEvents { - m := make(resource.RowEvents, len(rules)) - for _, r := range rules { - for _, grp := range r.APIGroups { - for _, res := range r.Resources { +func (r *Rbac) parseRules(rules []rbacv1.PolicyRule) render.Rows { + m := make(render.Rows, 0, len(rules)) + for _, rule := range rules { + for _, grp := range rule.APIGroups { + for _, res := range rule.Resources { k := res if grp != "" { k = res + "." + grp } - for _, na := range r.ResourceNames { - n := fqn(k, na) - m[n] = &resource.RowEvent{ - Fields: prepRow(n, grp, r.Verbs), - } - } - m[k] = &resource.RowEvent{ - Fields: prepRow(k, grp, r.Verbs), + for _, na := range rule.ResourceNames { + m = m.Upsert(r.prepRow(fqn(k, na), grp, rule.Verbs)) } + m = m.Upsert(r.prepRow(k, grp, rule.Verbs)) } } - for _, nres := range r.NonResourceURLs { + for _, nres := range rule.NonResourceURLs { if nres[0] != '/' { nres = "/" + nres } - m[nres] = &resource.RowEvent{ - Fields: prepRow(nres, resource.NAValue, r.Verbs), - } + m = m.Upsert(r.prepRow(nres, "", rule.Verbs)) } } return m } -func prepRow(res, grp string, verbs []string) resource.Row { - if grp != resource.NAValue { +func (r *Rbac) prepRow(res, grp string, verbs []string) render.Row { + if grp != "" { grp = toGroup(grp) } - return makeRow(res, grp, asVerbs(verbs...)) + fields := make(render.Fields, 0, len(r.Header())) + fields = append(fields, res, group) + return render.Row{ + ID: res, + Fields: append(fields, verbs...), + } } -func makeRow(res, group string, verbs []string) resource.Row { - r := make(resource.Row, 0, len(rbacHeader)) - r = append(r, res, group) - - return append(r, verbs...) -} - -func asVerbs(verbs ...string) resource.Row { +func asVerbs(verbs ...string) []string { const ( verbLen = 4 unknownLen = 30 ) - r := make(resource.Row, 0, len(k8sVerbs)+1) + r := make([]string, 0, len(k8sVerbs)+1) for _, v := range k8sVerbs { r = append(r, toVerbIcon(hasVerb(verbs, v))) } @@ -293,7 +278,7 @@ func asVerbs(verbs ...string) resource.Row { if hv, ok := httpTok8sVerbs[v]; ok { v = hv } - if !hasVerb(k8sVerbs, v) && v != all { + if !hasVerb(k8sVerbs, v) && v != clusterWide { unknowns = append(unknowns, v) } } @@ -309,7 +294,7 @@ func toVerbIcon(ok bool) string { } func hasVerb(verbs []string, verb string) bool { - if len(verbs) == 1 && verbs[0] == all { + if len(verbs) == 1 && verbs[0] == clusterWide { return true } @@ -333,3 +318,42 @@ func toGroup(g string) string { } return g } + +func showRoleBinding(app *App, _, resource, selection string) { + ns, n := namespaced(selection) + rb, err := app.Conn().DialOrDie().RbacV1().RoleBindings(ns).Get(n, metav1.GetOptions{}) + if err != nil { + app.Flash().Errf("Unable to retrieve rolebindings for %s", selection) + return + } + app.inject(NewRbac(fqn(ns, rb.RoleRef.Name), Role, selection)) +} + +func showClusterRoleBinding(app *App, ns, resource, selection string) { + o, err := app.factory.Get("-", "rbac.authorization.k8s.io/v1/clusterrolebindings", selection, labels.Everything()) + if err != nil { + app.Flash().Err(err) + return + } + + var crb rbacv1.ClusterRoleBinding + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crb) + if err != nil { + app.Flash().Errf("Unable to retrieve clusterrolebindings for %s", selection) + return + } + + // BOZO!! Must make sure cluster roles are in cache prior to loading rbac view. + app.factory.ForResource("-", "rbac.authorization.k8s.io/v1/clusterroles") + app.factory.WaitForCacheSync() + + app.inject(NewRbac(crb.RoleRef.Name, ClusterRole, selection)) +} + +func showRBAC(app *App, ns, resource, selection string) { + kind := ClusterRole + if resource == "role" { + kind = Role + } + app.inject(NewRbac(selection, kind, selection)) +} diff --git a/internal/view/rbac_int_test.go b/internal/view/rbac_int_test.go index 095313b4..d0c9cd60 100644 --- a/internal/view/rbac_int_test.go +++ b/internal/view/rbac_int_test.go @@ -3,6 +3,7 @@ package view import ( "testing" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/resource" "github.com/stretchr/testify/assert" rbacv1 "k8s.io/api/rbac/v1" @@ -33,12 +34,12 @@ func TestAsVerbs(t *testing.T) { uu := []struct { vv []string - e resource.Row + e render.Row }{ - {[]string{"*"}, resource.Row{ok, ok, ok, ok, ok, ok, ok, ok, ""}}, - {[]string{"get", "list", "patch"}, resource.Row{ok, ok, nok, nok, nok, ok, nok, nok, ""}}, - {[]string{"get", "list", "deletecollection", "post"}, resource.Row{ok, ok, ok, nok, ok, nok, nok, nok, ""}}, - {[]string{"get", "list", "blee"}, resource.Row{ok, ok, nok, nok, nok, nok, nok, nok, "blee"}}, + {[]string{"*"}, render.Row{Fields: render.Fields{ok, ok, ok, ok, ok, ok, ok, ok, ""}}}, + {[]string{"get", "list", "patch"}, render.Row{Fields: render.Fields{ok, ok, nok, nok, nok, ok, nok, nok, ""}}}, + {[]string{"get", "list", "deletecollection", "post"}, render.Row{Fields: render.Fields{ok, ok, ok, nok, ok, nok, nok, nok, ""}}}, + {[]string{"get", "list", "blee"}, render.Row{Fields: render.Fields{ok, ok, nok, nok, nok, nok, nok, nok, "blee"}}}, } for _, u := range uu { @@ -52,55 +53,55 @@ func TestParseRules(t *testing.T) { uu := []struct { pp []rbacv1.PolicyRule - e map[string]resource.Row + e render.Rows }{ { []rbacv1.PolicyRule{ {APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"*"}}, }, - map[string]resource.Row{ - "*.*": {"*.*", "*", ok, ok, ok, ok, ok, ok, ok, ok, ""}, + render.Rows{ + render.Row{Fields: render.Fields{"*.*", "*", ok, ok, ok, ok, ok, ok, ok, ok, ""}}, }, }, { []rbacv1.PolicyRule{ {APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"get"}}, }, - map[string]resource.Row{ - "*.*": {"*.*", "*", ok, nok, nok, nok, nok, nok, nok, nok, ""}, + render.Rows{ + render.Row{Fields: render.Fields{"*.*", "*", ok, nok, nok, nok, nok, nok, nok, nok, ""}}, }, }, { []rbacv1.PolicyRule{ {APIGroups: []string{""}, Resources: []string{"*"}, Verbs: []string{"list"}}, }, - map[string]resource.Row{ - "*": {"*", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}, + render.Rows{ + render.Row{Fields: render.Fields{"*", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}}, }, }, { []rbacv1.PolicyRule{ {APIGroups: []string{""}, Resources: []string{"pods"}, Verbs: []string{"list"}, ResourceNames: []string{"fred"}}, }, - map[string]resource.Row{ - "pods": {"pods", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}, - "pods/fred": {"pods/fred", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}, + render.Rows{ + render.Row{Fields: render.Fields{"pods", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}}, + render.Row{Fields: render.Fields{"pods/fred", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}}, }, }, { []rbacv1.PolicyRule{ {APIGroups: []string{}, Resources: []string{}, Verbs: []string{"get"}, NonResourceURLs: []string{"/fred"}}, }, - map[string]resource.Row{ - "/fred": {"/fred", resource.NAValue, ok, nok, nok, nok, nok, nok, nok, nok, ""}, + render.Rows{ + render.Row{Fields: render.Fields{"/fred", resource.NAValue, ok, nok, nok, nok, nok, nok, nok, nok, ""}}, }, }, { []rbacv1.PolicyRule{ {APIGroups: []string{}, Resources: []string{}, Verbs: []string{"get"}, NonResourceURLs: []string{"fred"}}, }, - map[string]resource.Row{ - "/fred": {"/fred", resource.NAValue, ok, nok, nok, nok, nok, nok, nok, nok, ""}, + render.Rows{ + render.Row{Fields: render.Fields{"/fred", resource.NAValue, ok, nok, nok, nok, nok, nok, nok, nok, ""}}, }, }, } diff --git a/internal/view/rbac_test.go b/internal/view/rbac_test.go index f06f246f..b0b7cba6 100644 --- a/internal/view/rbac_test.go +++ b/internal/view/rbac_test.go @@ -8,7 +8,7 @@ import ( ) func TestRbacNew(t *testing.T) { - v := view.NewRbac("fred", view.ClusterRole) + v := view.NewRbac("fred", view.ClusterRole, "") v.Init(makeCtx()) assert.Equal(t, "Rbac", v.Name()) diff --git a/internal/view/registrar.go b/internal/view/registrar.go index ff7c98d9..5d7fbdc7 100644 --- a/internal/view/registrar.go +++ b/internal/view/registrar.go @@ -4,15 +4,10 @@ import ( "strings" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/k9s/internal/watch" "github.com/rs/zerolog/log" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" ) @@ -28,126 +23,43 @@ func ToResource(o *unstructured.Unstructured, obj interface{}) error { return runtime.DefaultUnstructuredConverter.FromUnstructured(o.Object, &obj) } -func allCRDs(f *watch.Factory, vv MetaViewers) { - log.Debug().Msgf(">>> Loading CRDS") - oo, err := f.List("", "apiextensions.k8s.io/v1beta1/customresourcedefinitions", labels.Everything()) - if err != nil { - log.Error().Err(err).Msg("CRDs load fail") - return - } - - var r render.CustomResourceDefinition - for _, o := range oo { - meta, err := r.Meta(o) - if err != nil { - log.Error().Err(err).Msgf("Error getting meta fields") - continue - } - - gvr := k8s.NewGVR(meta.Group, meta.Version, meta.Plural) - gvrs := gvr.String() - if meta.Plural != "" { - aliases.Define(gvrs, meta.Plural) - } - if meta.Singular != "" { - aliases.Define(gvrs, meta.Singular) - } - for _, a := range meta.ShortNames { - aliases.Define(gvrs, a) - } - - vv[gvrs] = MetaViewer{ - gvr: gvrs, - kind: meta.Kind, - viewFn: resourceFn(resource.NewCustomList(f.Client().(k8s.Connection), meta.Namespaced, "", gvrs)), - colorerFn: ui.DefaultColorer, - } - } -} - -func showRBAC(app *App, ns, resource, selection string) { - kind := ClusterRole - if resource == "role" { - kind = Role - } - app.inject(NewRbac(selection, kind)) -} - func showCRD(app *App, ns, resource, selection string) { log.Debug().Msgf("Launching CRD %q -- %q -- %q", ns, resource, selection) tokens := strings.Split(selection, ".") - if !app.gotoResource(tokens[0]) { - app.Flash().Errf("Goto %s failed", tokens[0]) - } + _ = tokens + panic("NYI") + // if !app.gotoResource(tokens[0]) { + // app.Flash().Errf("Goto %s failed", tokens[0]) + // } } -func showClusterRole(app *App, ns, resource, selection string) { - crb, err := app.Conn().DialOrDie().RbacV1().ClusterRoleBindings().Get(selection, metav1.GetOptions{}) - if err != nil { - app.Flash().Errf("Unable to retrieve clusterrolebindings for %s", selection) - return - } - app.inject(NewRbac(crb.RoleRef.Name, ClusterRole)) -} - -func showRole(app *App, _, resource, selection string) { - ns, n := namespaced(selection) - rb, err := app.Conn().DialOrDie().RbacV1().RoleBindings(ns).Get(n, metav1.GetOptions{}) - if err != nil { - app.Flash().Errf("Unable to retrieve rolebindings for %s", selection) - return - } - app.inject(NewRbac(fqn(ns, rb.RoleRef.Name), Role)) -} - -func showSAPolicy(app *App, _, _, selection string) { - _, n := namespaced(selection) - subject, err := mapFuSubject("ServiceAccount") - if err != nil { - app.Flash().Err(err) - return - } - app.inject(NewPolicy(app, subject, n)) -} - -func load(c k8s.Connection, vv MetaViewers) { +func loadAliases() error { if err := aliases.Load(); err != nil { - log.Error().Err(err).Msg("No custom aliases defined in config") + return err } - discovery, err := c.CachedDiscovery() - if err != nil { - log.Error().Err(err).Msgf("Error to get discovery client") - return - } - - rr, _ := discovery.ServerPreferredResources() - for _, r := range rr { - for _, res := range r.APIResources { - 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(gvrStr, strings.ToLower(res.Kind)) - aliases.Define(gvrStr, res.Name) - if len(res.SingularName) > 0 { - aliases.Define(gvrStr, res.SingularName) - } - for _, s := range res.ShortNames { - aliases.Define(gvrStr, s) - } + for _, gvr := range dao.AllGVRs() { + meta, err := dao.MetaFor(gvr) + if err != nil { + return err + } + if _, ok := aliases.Alias[meta.Kind]; ok { + continue + } + aliases.Define(string(gvr), strings.ToLower(meta.Kind), meta.Name) + if meta.SingularName != "" { + aliases.Define(string(gvr), meta.SingularName) + } + if meta.ShortNames != nil { + aliases.Define(string(gvr), meta.ShortNames...) } } + + return nil } -func resourceViews(c k8s.Connection, m MetaViewers) { +func loadCustomViewers() MetaViewers { + m := make(MetaViewers, 30) + coreRes(m) miscRes(m) appsRes(m) @@ -158,24 +70,20 @@ func resourceViews(c k8s.Connection, m MetaViewers) { policyRes(m) hpaRes(m) - load(c, m) + return m } func coreRes(vv MetaViewers) { vv["v1/nodes"] = MetaViewer{ - viewFn: NewNode, - listFn: resource.NewNodeList, - colorerFn: nsColorer, + viewFn: NewNode, + listFn: resource.NewNodeList, } vv["v1/namespaces"] = MetaViewer{ - viewFn: NewNamespace, - listFn: resource.NewNamespaceList, - colorerFn: nsColorer, + viewerFn: NewNamespace, } vv["v1/pods"] = MetaViewer{ - viewFn: NewPod, - listFn: resource.NewPodList, - colorerFn: podColorer, + viewFn: NewPod, + listFn: resource.NewPodList, } vv["v1/serviceaccounts"] = MetaViewer{ listFn: resource.NewServiceAccountList, @@ -189,12 +97,10 @@ func coreRes(vv MetaViewers) { listFn: resource.NewConfigMapList, } vv["v1/persistentvolumes"] = MetaViewer{ - listFn: resource.NewPersistentVolumeList, - colorerFn: pvColorer, + listFn: resource.NewPersistentVolumeList, } vv["v1/persistentvolumeclaims"] = MetaViewer{ - listFn: resource.NewPersistentVolumeClaimList, - colorerFn: pvcColorer, + listFn: resource.NewPersistentVolumeClaimList, } vv["v1/secrets"] = MetaViewer{ viewFn: NewSecret, @@ -204,13 +110,11 @@ func coreRes(vv MetaViewers) { listFn: resource.NewEndpointsList, } vv["v1/events"] = MetaViewer{ - listFn: resource.NewEventList, - colorerFn: evColorer, + listFn: resource.NewEventList, } vv["v1/replicationcontrollers"] = MetaViewer{ - viewFn: NewReplicationController, - listFn: resource.NewReplicationControllerList, - colorerFn: rsColorer, + viewFn: NewReplicationController, + listFn: resource.NewReplicationControllerList, } } @@ -219,54 +123,40 @@ func miscRes(vv MetaViewers) { listFn: resource.NewStorageClassList, } vv["contexts"] = MetaViewer{ - gvr: "contexts", - kind: "Contexts", - viewFn: NewContext, - listFn: resource.NewContextList, - colorerFn: ctxColorer, + viewerFn: NewContext, } vv["users"] = MetaViewer{ - gvr: "users", viewFn: NewSubject, } vv["groups"] = MetaViewer{ - gvr: "groups", viewFn: NewSubject, } vv["portforwards"] = MetaViewer{ - gvr: "portforwards", viewFn: NewPortForward, } vv["benchmarks"] = MetaViewer{ - gvr: "benchmarks", viewFn: NewBench, } vv["screendumps"] = MetaViewer{ - gvr: "screendumps", - viewFn: NewScreenDump, + viewerFn: NewScreenDump, } } func appsRes(vv MetaViewers) { vv["apps/v1/deployments"] = MetaViewer{ - viewFn: NewDeploy, - listFn: resource.NewDeploymentList, - colorerFn: dpColorer, + viewerFn: NewDeploy, } vv["apps/v1/replicasets"] = MetaViewer{ - viewFn: NewReplicaSet, - listFn: resource.NewReplicaSetList, - colorerFn: rsColorer, + viewerFn: NewReplicaSet, } vv["apps/v1/statefulsets"] = MetaViewer{ - viewFn: NewStatefulSet, - listFn: resource.NewStatefulSetList, - colorerFn: stsColorer, + viewerFn: NewStatefulSet, } vv["apps/v1/daemonsets"] = MetaViewer{ - viewFn: NewDaemonSet, - listFn: resource.NewDaemonSetList, - colorerFn: dpColorer, + viewerFn: NewDaemonSet, + } + vv["extensions/v1beta1/daemonsets"] = MetaViewer{ + viewerFn: NewDaemonSet, } } @@ -277,11 +167,11 @@ func authRes(vv MetaViewers) { } vv["rbac.authorization.k8s.io/v1/clusterrolebindings"] = MetaViewer{ listFn: resource.NewClusterRoleBindingList, - enterFn: showClusterRole, + enterFn: showClusterRoleBinding, } vv["rbac.authorization.k8s.io/v1/rolebindings"] = MetaViewer{ listFn: resource.NewRoleBindingList, - enterFn: showRole, + enterFn: showRoleBinding, } vv["rbac.authorization.k8s.io/v1/roles"] = MetaViewer{ listFn: resource.NewRoleList, @@ -322,8 +212,7 @@ func batchRes(vv MetaViewers) { func policyRes(vv MetaViewers) { vv["policy/v1beta1/poddisruptionbudgets"] = MetaViewer{ - listFn: resource.NewPDBList, - colorerFn: pdbColorer, + listFn: resource.NewPDBList, } } diff --git a/internal/view/resource.go b/internal/view/resource.go index 84fa2603..2fc64e6d 100644 --- a/internal/view/resource.go +++ b/internal/view/resource.go @@ -1,6 +1,7 @@ package view import ( + "bytes" "context" "errors" "fmt" @@ -8,12 +9,16 @@ import ( "time" "github.com/atotto/clipboard" + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/printers" ) // Resource represents a generic resource viewer. @@ -56,6 +61,13 @@ func (r *Resource) Init(ctx context.Context) error { return nil } +func (s *Resource) SetContextFn(ContextFunc) {} + +// GVR returns a resource descriptor. +func (r *Resource) GVR() string { + return r.gvr +} + // SetPath sets parent selector. func (r *Resource) SetPath(p string) { r.path = p @@ -153,11 +165,11 @@ func (r *Resource) refreshCmd(*tcell.EventKey) *tcell.EventKey { } func (r *Resource) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { - if !r.RowSelected() { + sel := r.GetSelectedItems() + if len(sel) == 0 { return evt } - sel := r.GetSelectedItems() var msg string if len(sel) > 1 { msg = fmt.Sprintf("Delete %d marked %s?", len(sel), r.list.GetName()) @@ -217,11 +229,23 @@ func (r *Resource) viewCmd(evt *tcell.EventKey) *tcell.EventKey { } sel := r.GetSelectedItem() - raw, err := r.list.Resource().Marshal(sel) + ns, n := resource.Namespaced(sel) + if ns == "" { + ns = r.list.GetNamespace() + } + log.Debug().Msgf("------ NAMESPACES %q vs %q", ns, r.list.GetNamespace()) + o, err := r.app.factory.Get(ns, r.gvr, n, labels.Everything()) + if err != nil { + r.app.Flash().Errf("Unable to get resource %s", err) + return nil + } + + raw, err := marshalObject(o) if err != nil { r.app.Flash().Errf("Unable to marshal resource %s", err) - return evt + return nil } + details := NewDetails("YAML") details.SetSubject(sel) details.SetTextColor(r.app.Styles.FgColor()) @@ -232,12 +256,27 @@ func (r *Resource) viewCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } +func marshalObject(o runtime.Object) (string, error) { + var ( + buff bytes.Buffer + p printers.YAMLPrinter + ) + err := p.PrintObj(o, &buff) + if err != nil { + log.Error().Msgf("Marshal Error %v", err) + return "", err + } + + return buff.String(), nil +} + func (r *Resource) editCmd(evt *tcell.EventKey) *tcell.EventKey { if !r.RowSelected() { return evt } r.Stop() + defer r.Start() { ns, po := namespaced(r.GetSelectedItem()) args := make([]string, 0, 10) @@ -252,7 +291,6 @@ func (r *Resource) editCmd(evt *tcell.EventKey) *tcell.EventKey { r.app.Flash().Err(errors.New("Edit exec failed")) } } - r.Start() return evt } @@ -298,29 +336,31 @@ func (r *Resource) refresh() { r.list.SetNamespace(r.currentNS) } - if r.app.Conn() != nil { - ctx := context.WithValue(context.Background(), resource.KeyFactory, r.app.factory) - if err := r.list.Reconcile(ctx, r.gvr, r.path); err != nil { - r.app.Flash().Err(err) - } + if r.app.Conn() == nil { + log.Error().Msg("No api connection") + return } + + ctx := context.WithValue(context.Background(), internal.KeyFactory, r.app.factory) + ctx = context.WithValue(ctx, internal.KeySelection, r.path) + if err := r.list.Reconcile(ctx, r.gvr); err != nil { + r.app.Flash().Err(err) + } + data := r.list.Data() - if r.decorateFn != nil { - data = r.decorateFn(data) - } + // BOZO!! + // if r.decorateFn != nil { + // data = r.decorateFn(data) + // } r.refreshActions() r.Update(data) } func (r *Resource) namespaceActions(aa ui.KeyActions) { - if !r.list.Access(resource.NamespaceAccess) { + if r.app.Conn() == nil || !r.list.Access(resource.NamespaceAccess) { return } r.namespaces = make(map[int]string, config.MaxFavoritesNS) - // User can't list namespace. Don't offer a choice. - if r.app.Conn() == nil || r.app.Conn().CheckListNSAccess() != nil { - return - } aa[tcell.Key(ui.NumKeys[0])] = ui.NewKeyAction(resource.AllNamespace, r.switchNamespaceCmd, true) r.namespaces[0] = resource.AllNamespace index := 1 diff --git a/internal/view/restart_extender.go b/internal/view/restart_extender.go index 07850e5a..4ec93739 100644 --- a/internal/view/restart_extender.go +++ b/internal/view/restart_extender.go @@ -3,7 +3,7 @@ package view import ( "errors" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" "github.com/gdamore/tcell" @@ -50,11 +50,16 @@ func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey { } func (r *RestartExtender) restartRollout(path string) error { - s, ok := r.List().Resource().(resource.Restartable) + ns, n := namespaced(path) + res, err := dao.AccessorFor(r.App().factory, dao.GVR(r.GVR())) + if err != nil { + return nil + } + + s, ok := res.(dao.Restartable) if !ok { return errors.New("resource is not restartable") } - ns, n := namespaced(path) return s.Restart(ns, n) } diff --git a/internal/view/rs.go b/internal/view/rs.go index b0175e8d..aae55e36 100644 --- a/internal/view/rs.go +++ b/internal/view/rs.go @@ -1,21 +1,23 @@ package view import ( - "context" "errors" "fmt" "strconv" "strings" - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/watch" "github.com/derailed/tview" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/kubectl/pkg/polymorphichelpers" ) @@ -26,21 +28,15 @@ type ReplicaSet struct { } // NewReplicaSet returns a new viewer. -func NewReplicaSet(title, gvr string, list resource.List) ResourceViewer { - return &ReplicaSet{ - ResourceViewer: NewResource(title, gvr, list), - } -} - -// Init initializes the component. -func (r *ReplicaSet) Init(ctx context.Context) error { - if err := r.ResourceViewer.Init(ctx); err != nil { - return err +func NewReplicaSet(gvr dao.GVR) ResourceViewer { + r := ReplicaSet{ + ResourceViewer: NewGeneric(gvr), } r.bindKeys() r.GetTable().SetEnterFn(r.showPods) + r.GetTable().SetColorerFn(render.ReplicaSet{}.ColorerFunc()) - return nil + return &r } func (r *ReplicaSet) bindKeys() { @@ -53,22 +49,19 @@ func (r *ReplicaSet) bindKeys() { func (r *ReplicaSet) showPods(app *App, _, res, sel string) { ns, n := namespaced(sel) - s, err := k8s.NewReplicaSet(app.Conn()).Get(ns, n) + o, err := app.factory.Get(ns, r.GVR(), n, labels.Everything()) if err != nil { - app.Flash().Errf("Replicaset failed %s", err) - } - - rs, ok := s.(*v1.ReplicaSet) - if !ok { - log.Fatal().Msg("Expecting a valid rs") - } - l, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector) - if err != nil { - app.Flash().Errf("Selector failed %s", err) + app.Flash().Err(err) return } - showPods(app, ns, l.String(), "") + var rs appsv1.ReplicaSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rs) + if err != nil { + app.Flash().Err(err) + } + + showPodsFromSelector(app, ns, rs.Spec.Selector) } func (r *ReplicaSet) rollbackCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -80,7 +73,7 @@ func (r *ReplicaSet) rollbackCmd(evt *tcell.EventKey) *tcell.EventKey { r.showModal(fmt.Sprintf("Rollback %s %s?", r.List().GetName(), sel), func(_ int, button string) { if button == "OK" { r.App().Flash().Infof("Rolling back %s %s", r.List().GetName(), sel) - if res, err := rollback(r.App().Conn(), sel); err != nil { + if res, err := rollback(r.App().factory, sel); err != nil { r.App().Flash().Err(err) } else { r.App().Flash().Info(res) @@ -110,21 +103,34 @@ func (r *ReplicaSet) showModal(msg string, done func(int, string)) { // ---------------------------------------------------------------------------- // Helpers... -func findRS(Conn k8s.Connection, ns, n string) (*v1.ReplicaSet, error) { - rset := k8s.NewReplicaSet(Conn) - r, err := rset.Get(ns, n) +func findRS(f *watch.Factory, ns, n string) (*v1.ReplicaSet, error) { + o, err := f.Get(ns, "apps/v1/replicasets", n, labels.Everything()) if err != nil { return nil, err } - return r.(*v1.ReplicaSet), nil + + var rs appsv1.ReplicaSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rs) + if err != nil { + return nil, err + } + + return &rs, nil } -func findDP(Conn k8s.Connection, ns, n string) (*appsv1.Deployment, error) { - dp, err := k8s.NewDeployment(Conn).Get(ns, n) +func findDP(f *watch.Factory, ns, n string) (*appsv1.Deployment, error) { + o, err := f.Get(ns, "apps/v1/deployments", n, labels.Everything()) if err != nil { return nil, err } - return dp.(*appsv1.Deployment), nil + + var dp appsv1.Deployment + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp) + if err != nil { + return nil, err + } + + return &dp, nil } func controllerInfo(rs *v1.ReplicaSet) (string, string, string, error) { @@ -146,19 +152,19 @@ func controllerInfo(rs *v1.ReplicaSet) (string, string, string, error) { func getRevision(rs *v1.ReplicaSet) (int64, error) { revision := rs.ObjectMeta.Annotations["deployment.kubernetes.io/revision"] if rs.Status.Replicas != 0 { - return 0, errors.New("Can not rollback current replica") + return 0, errors.New("can not rollback current replica") } vers, err := strconv.Atoi(revision) if err != nil { - return 0, errors.New("Revision conversion failed") + return 0, errors.New("revision conversion failed") } + return int64(vers), nil } -func rollback(Conn k8s.Connection, selectedItem string) (string, error) { +func rollback(f *watch.Factory, selectedItem string) (string, error) { ns, n := namespaced(selectedItem) - - rs, err := findRS(Conn, ns, n) + rs, err := findRS(f, ns, n) if err != nil { return "", err } @@ -171,11 +177,11 @@ func rollback(Conn k8s.Connection, selectedItem string) (string, error) { if err != nil { return "", err } - rb, err := polymorphichelpers.RollbackerFor(schema.GroupKind{Group: apiGroup, Kind: kind}, Conn.DialOrDie()) + rb, err := polymorphichelpers.RollbackerFor(schema.GroupKind{Group: apiGroup, Kind: kind}, f.Client().DialOrDie()) if err != nil { return "", err } - dp, err := findDP(Conn, ns, name) + dp, err := findDP(f, ns, name) if err != nil { return "", err } diff --git a/internal/view/scale_extender.go b/internal/view/scale_extender.go index a82dcf08..7f3d305c 100644 --- a/internal/view/scale_extender.go +++ b/internal/view/scale_extender.go @@ -1,15 +1,15 @@ package view import ( - "errors" "fmt" "strconv" "strings" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" ) type ScaleExtender struct { @@ -44,7 +44,7 @@ func (s *ScaleExtender) scaleCmd(evt *tcell.EventKey) *tcell.EventKey { func (s *ScaleExtender) showScaleDialog(path string) { confirm := tview.NewModalForm("", s.makeScaleForm(path)) - confirm.SetText(fmt.Sprintf("Scale %s %s", s.List().GetName(), path)) + confirm.SetText(fmt.Sprintf("Scale %s %s", s.GVR(), path)) confirm.SetDoneFunc(func(int, string) { s.dismissDialog() }) @@ -52,9 +52,11 @@ func (s *ScaleExtender) showScaleDialog(path string) { s.App().Content.ShowPage(scaleDialogKey) } -func (s *ScaleExtender) makeScaleForm(path string) *tview.Form { +func (s *ScaleExtender) makeScaleForm(sel string) *tview.Form { f := s.makeStyledForm() replicas := strings.TrimSpace(s.GetTable().GetCell(s.GetTable().GetSelectedRowIndex(), s.GetTable().NameColIndex()+1).Text) + tokens := strings.Split(replicas, "/") + replicas = tokens[1] f.AddInputField("Replicas:", replicas, 4, func(textToCheck string, lastChar rune) bool { _, err := strconv.Atoi(textToCheck) return err == nil @@ -69,10 +71,11 @@ func (s *ScaleExtender) makeScaleForm(path string) *tview.Form { s.App().Flash().Err(err) return } - if err := s.scale(path, count); err != nil { + if err := s.scale(sel, count); err != nil { + log.Error().Err(err).Msgf("DP %s scaling failed", sel) s.App().Flash().Err(err) } else { - s.App().Flash().Infof("Resource %s:%s scaled successfully", s.List().GetName(), path) + s.App().Flash().Infof("Resource %s:%s scaled successfully", s.GVR(), sel) } }) @@ -101,9 +104,14 @@ func (s *ScaleExtender) makeStyledForm() *tview.Form { func (s *ScaleExtender) scale(path string, replicas int) error { ns, n := namespaced(path) - scaler, ok := s.List().Resource().(resource.Scalable) + res, err := dao.AccessorFor(s.App().factory, dao.GVR(s.GVR())) + if err != nil { + return nil + } + log.Debug().Msgf("SCALER %#v", res) + scaler, ok := res.(dao.Scalable) if !ok { - return errors.New("Expecting a valid scalable resource") + return fmt.Errorf("expecting a scalable resource for %q", s.GVR()) } return scaler.Scale(ns, n, int32(replicas)) diff --git a/internal/view/screen_dump.go b/internal/view/screen_dump.go index fc50b8b3..2b55d085 100644 --- a/internal/view/screen_dump.go +++ b/internal/view/screen_dump.go @@ -3,13 +3,12 @@ package view import ( "context" "errors" - "fmt" - "os" "path/filepath" + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/render" "github.com/fsnotify/fsnotify" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" @@ -17,144 +16,55 @@ import ( const dumpTitle = "Screen Dumps" -var dumpHeader = resource.Row{"NAME", "AGE"} - // ScreenDump presents a directory listing viewer. type ScreenDump struct { - *Table + ResourceViewer } // NewScreenDump returns a new viewer. -func NewScreenDump(_, _ string, _ resource.List) ResourceViewer { - return &ScreenDump{ - Table: NewTable(dumpTitle), +func NewScreenDump(gvr dao.GVR) ResourceViewer { + s := ScreenDump{ + ResourceViewer: NewGeneric(gvr), } -} + // BOZO!! Rename Table + s.GetTable().SetBorderFocusColor(tcell.ColorSteelBlue) + s.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorRoyalBlue, tcell.AttrNone) + s.GetTable().SetColorerFn(render.ScreenDump{}.ColorerFunc()) + s.GetTable().SetSortCol(s.GetTable().NameColIndex(), 0, true) + s.GetTable().SelectRow(1, true) + s.GetTable().SetEnterFn(s.edit) + s.SetContextFn(s.dirContext) -// Init initializes the viewer. -func (s *ScreenDump) Init(ctx context.Context) error { - if err := s.Table.Init(ctx); err != nil { - return nil - } - s.bindKeys() - s.SetBorderFocusColor(tcell.ColorSteelBlue) - s.SetSelectedStyle(tcell.ColorWhite, tcell.ColorRoyalBlue, tcell.AttrNone) - s.SetColorerFn(dumpColorer) - s.ActiveNS = resource.AllNamespaces - s.SetSortCol(s.NameColIndex(), 0, true) - s.SelectRow(1, true) - - s.Start() - s.refresh() - - return nil -} - -// GetTable returns the table view. -func (r *ScreenDump) GetTable() *Table { return r.Table } - -// SetEnvFn sets up k9s env vars. -func (r *ScreenDump) SetEnvFn(EnvFunc) {} - -// SetPath sets parent selector. -func (p *ScreenDump) SetPath(s string) {} - -// List returns the resource lister. -func (s *ScreenDump) List() resource.List { - return nil + return &s } // Start starts the directory watcher. func (s *ScreenDump) Start() { + s.Stop() + + s.GetTable().Actions().Delete(tcell.KeyCtrlS) + + s.GetTable().Start() var ctx context.Context - ctx, s.cancelFn = context.WithCancel(context.Background()) + ctx, s.GetTable().cancelFn = context.WithCancel(context.Background()) if err := s.watchDumpDir(ctx); err != nil { - s.app.Flash().Errf("Unable to watch screen dumps directory %s", err) + s.App().Flash().Errf("Unable to watch screen dumps directory %s", err) } } -// Name returns the component name. -func (s *ScreenDump) Name() string { - return dumpTitle +func (s *ScreenDump) dirContext(ctx context.Context) context.Context { + dir := filepath.Join(config.K9sDumpDir, s.App().Config.K9s.CurrentCluster) + return context.WithValue(ctx, internal.KeyDir, dir) } -func (s *ScreenDump) refresh() { - s.Update(s.hydrate()) - s.UpdateTitle() -} +func (s *ScreenDump) edit(app *App, ns, resource, path string) { + log.Debug().Msgf("ScreenDump selection is %q", path) -func (s *ScreenDump) bindKeys() { - s.Actions().Add(ui.KeyActions{ - tcell.KeyEsc: ui.NewKeyAction("Back", s.app.PrevCmd, false), - tcell.KeyEnter: ui.NewKeyAction("View", s.enterCmd, true), - tcell.KeyCtrlD: ui.NewKeyAction("Delete", s.deleteCmd, true), - tcell.KeyCtrlS: ui.NewKeyAction("Save", noopCmd, false), - }) -} - -func (s *ScreenDump) enterCmd(evt *tcell.EventKey) *tcell.EventKey { - log.Debug().Msg("Dump enter!") - if s.SearchBuff().IsActive() { - return s.filterCmd(evt) + s.Stop() + defer s.Start() + if !edit(true, app, path) { + app.Flash().Err(errors.New("Failed to launch editor")) } - sel := s.GetSelectedItem() - if sel == "" { - return nil - } - - dir := filepath.Join(config.K9sDumpDir, s.app.Config.K9s.CurrentCluster) - if !edit(true, s.app, filepath.Join(dir, sel)) { - s.app.Flash().Err(errors.New("Failed to launch editor")) - } - - return nil -} - -func (s *ScreenDump) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { - sel := s.GetSelectedItem() - if sel == "" { - return nil - } - - dir := filepath.Join(config.K9sDumpDir, s.app.Config.K9s.CurrentCluster) - showModal(s.app.Content.Pages, fmt.Sprintf("Delete screen dump `%s?", sel), func() { - if err := os.Remove(filepath.Join(dir, sel)); err != nil { - s.app.Flash().Errf("Unable to delete file %s", err) - return - } - s.refresh() - s.app.Flash().Infof("ScreenDump file %s deleted!", sel) - }) - - return nil -} - -func (s *ScreenDump) hydrate() resource.TableData { - return resource.TableData{} - - // BOZO!! - // data := resource.TableData{ - // Header: dumpHeader, - // Rows: make(resource.RowEvents, 10), - // Namespace: resource.NotNamespaced, - // } - - // dir := filepath.Join(config.K9sDumpDir, s.app.Config.K9s.CurrentCluster) - // ff, err := ioutil.ReadDir(dir) - // if err != nil { - // s.app.Flash().Errf("Unable to read dump directory %s", err) - // } - - // for _, f := range ff { - // fields := resource.Row{f.Name(), time.Since(f.ModTime()).String()} - // data.Rows[f.Name()] = &resource.RowEvent{ - // Action: resource.New, - // Fields: fields, - // Deltas: fields, - // } - // } - - // return data } func (s *ScreenDump) watchDumpDir(ctx context.Context) error { @@ -167,15 +77,13 @@ func (s *ScreenDump) watchDumpDir(ctx context.Context) error { for { select { case evt := <-w.Events: - log.Debug().Msgf("Dump event %#v", evt) - s.app.QueueUpdateDraw(func() { - s.refresh() - }) + log.Debug().Msgf("ScreenDump event detected %#v", evt) + s.Refresh() case err := <-w.Errors: - log.Info().Err(err).Msg("Dir Watcher failed") + log.Error().Err(err).Msg("Dir Watcher failed") return case <-ctx.Done(): - log.Debug().Msg("!!!! FS WATCHER DONE!!") + log.Debug().Msg("!!!! ScreenDump WATCHER DONE!!") if err := w.Close(); err != nil { log.Error().Err(err).Msg("Closing dump watcher") } @@ -184,11 +92,5 @@ func (s *ScreenDump) watchDumpDir(ctx context.Context) error { } }() - return w.Add(filepath.Join(config.K9sDumpDir, s.app.Config.K9s.CurrentCluster)) -} - -// Helpers... - -func noopCmd(*tcell.EventKey) *tcell.EventKey { - return nil + return w.Add(filepath.Join(config.K9sDumpDir, s.App().Config.K9s.CurrentCluster)) } diff --git a/internal/view/screen_dump_test.go b/internal/view/screen_dump_test.go index 4da39e56..c2e1327a 100644 --- a/internal/view/screen_dump_test.go +++ b/internal/view/screen_dump_test.go @@ -12,5 +12,5 @@ func TestScreenDumpNew(t *testing.T) { po.Init(makeCtx()) assert.Equal(t, "Screen Dumps", po.Name()) - assert.Equal(t, 13, len(po.Hints())) + assert.Equal(t, 12, len(po.Hints())) } diff --git a/internal/view/sts.go b/internal/view/sts.go index c694c5ad..55257a62 100644 --- a/internal/view/sts.go +++ b/internal/view/sts.go @@ -1,11 +1,13 @@ package view import ( - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/apps/v1" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" ) // StatefulSet represents a statefulset viewer. @@ -14,12 +16,12 @@ type StatefulSet struct { } // NewStatefulSet returns a new viewer. -func NewStatefulSet(title, gvr string, list resource.List) ResourceViewer { +func NewStatefulSet(gvr dao.GVR) ResourceViewer { s := StatefulSet{ ResourceViewer: NewRestartExtender( NewScaleExtender( NewLogsExtender( - NewResource(title, gvr, list), + NewGeneric(gvr), func() string { return "" }, ), ), @@ -27,29 +29,31 @@ func NewStatefulSet(title, gvr string, list resource.List) ResourceViewer { } s.BindKeys() s.GetTable().SetEnterFn(s.showPods) + s.GetTable().SetColorerFn(render.StatefulSet{}.ColorerFunc()) return &s } -func (d *StatefulSet) BindKeys() { - d.Actions().Add(ui.KeyActions{ - ui.KeyShiftD: ui.NewKeyAction("Sort Desired", d.GetTable().SortColCmd(1, true), false), - ui.KeyShiftC: ui.NewKeyAction("Sort Current", d.GetTable().SortColCmd(2, true), false), +func (s *StatefulSet) BindKeys() { + s.Actions().Add(ui.KeyActions{ + ui.KeyShiftD: ui.NewKeyAction("Sort Desired", s.GetTable().SortColCmd(1, true), false), + ui.KeyShiftC: ui.NewKeyAction("Sort Current", s.GetTable().SortColCmd(2, true), false), }) } -func (s *StatefulSet) showPods(app *App, _, res, path string) { - ns, n := namespaced(path) - st, err := k8s.NewStatefulSet(app.Conn()).Get(ns, n) +func (s *StatefulSet) showPods(app *App, _, res, sel string) { + ns, n := namespaced(sel) + o, err := app.factory.Get(ns, s.GVR(), n, labels.Everything()) if err != nil { - log.Error().Err(err).Msgf("Fetching StatefulSet %s", path) - app.Flash().Errf("Unable to fetch statefulset %s", err) + app.Flash().Err(err) return } - sts, ok := st.(*v1.StatefulSet) - if !ok { - log.Fatal().Msg("Expecting a valid sts") + var sts appsv1.StatefulSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sts) + if err != nil { + app.Flash().Err(err) } + showPodsFromSelector(app, ns, sts.Spec.Selector) } diff --git a/internal/view/subject.go b/internal/view/subject.go index eb206345..c3c5e3fb 100644 --- a/internal/view/subject.go +++ b/internal/view/subject.go @@ -3,22 +3,25 @@ package view import ( "context" "fmt" + "reflect" "time" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" ) -var subjectHeader = resource.Row{"NAME", "KIND", "FIRST LOCATION"} - type ( - cachedEventer interface { - header() resource.Row - getCache() resource.RowEvents - setCache(resource.RowEvents) + TableInfo interface { + Header() render.HeaderRow + GetCache() render.RowEvents + SetCache(render.RowEvents) } // Subject presents a user/group viewer. @@ -26,7 +29,7 @@ type ( *Table subjectKind string - cache resource.RowEvents + cache render.RowEvents } ) @@ -35,6 +38,13 @@ func NewSubject(title, _ string, _ resource.List) ResourceViewer { return &Subject{Table: NewTable(title)} } +func (*Subject) SetContextFn(ContextFunc) {} + +// GVR returns a resource descriptor. +func (s *Subject) GVR() string { + return "n/a" +} + // GetTable returns the table view. func (s *Subject) GetTable() *Table { return s.Table } @@ -55,12 +65,11 @@ func (s *Subject) Init(ctx context.Context) error { } s.subjectKind = mapCmdSubject(app.Config.K9s.ActiveCluster().View.Active) s.Table = NewTable(s.subjectKind) - s.SetColorerFn(rbacColorer) + s.SetColorerFn(render.Subject{}.ColorerFunc()) if err := s.Table.Init(ctx); err != nil { return err } - s.ActiveNS = "*" - s.SetSortCol(1, len(rbacHeader), true) + s.SetSortCol(1, len(s.Header()), true) s.SelectRow(1, true) s.bindKeys() s.refresh() @@ -120,10 +129,6 @@ func (s *Subject) refresh() { } func (s *Subject) policyCmd(evt *tcell.EventKey) *tcell.EventKey { - log.Debug().Msgf("SUBJECT!!!!") - defer func(t time.Time) { - log.Debug().Msgf(">>>>>> Subject elapsed %v", time.Since(t)) - }(time.Now()) if !s.RowSelected() { return evt } @@ -134,9 +139,8 @@ func (s *Subject) policyCmd(evt *tcell.EventKey) *tcell.EventKey { s.app.Flash().Err(err) return nil } - log.Debug().Msgf(" INJECTING...") s.app.inject(NewPolicy(s.app, subject, n)) - log.Debug().Msgf(" DONE...") + return nil } @@ -158,135 +162,143 @@ func (s *Subject) backCmd(evt *tcell.EventKey) *tcell.EventKey { return s.app.PrevCmd(evt) } -func (s *Subject) reconcile() (resource.TableData, error) { - var table resource.TableData +func (s *Subject) reconcile() (render.TableData, error) { + var table render.TableData if s.app.Conn() == nil { return table, nil } - evts, err := s.clusterSubjects() + rows, err := s.fetchClusterRoleBindings() if err != nil { return table, err } - nevts, err := s.namespacedSubjects() + nrows, err := s.fetchRoleBindings() if err != nil { return table, err } - for k, v := range nevts { - evts[k] = v + for k, v := range nrows { + rows[k] = v } - return buildTable(s, evts), nil + return buildTable(s, rows), nil } -func (s *Subject) header() resource.Row { - return subjectHeader +func (s *Subject) Header() render.HeaderRow { + return render.Subject{}.Header(render.AllNamespaces) } -func (s *Subject) getCache() resource.RowEvents { +func (s *Subject) GetCache() render.RowEvents { return s.cache } -func (s *Subject) setCache(evts resource.RowEvents) { - s.cache = evts +func (s *Subject) SetCache(rows render.RowEvents) { + s.cache = rows } -func buildTable(c cachedEventer, evts resource.RowEvents) resource.TableData { - return resource.TableData{} +func buildTable(c TableInfo, rows render.Rows) render.TableData { + table := render.TableData{ + Header: c.Header(), + Namespace: "*", + } - // BOZO!! - // table := resource.TableData{ - // Header: c.header(), - // Rows: make(resource.RowEvents, len(evts)), - // Namespace: "*", - // } + cache := c.GetCache() + if len(cache) == 0 { + cache := make(render.RowEvents, 0, len(rows)) + for _, row := range rows { + cache = append(cache, render.RowEvent{Kind: render.EventAdd, Row: row}) + } + table.RowEvents = cache + return table + } - // noDeltas := make(resource.Row, len(c.header())) - // cache := c.getCache() - // if len(cache) == 0 { - // for k, ev := range evts { - // ev.Action = resource.New - // ev.Deltas = noDeltas - // table.Rows[k] = ev - // } - // c.setCache(evts) - // return table - // } + for _, row := range rows { + idx, ok := cache.FindIndex(row.ID) + if !ok { + cache = append(cache, render.RowEvent{Kind: render.EventAdd, Row: row}) + continue + } - // for k, ev := range evts { - // table.Rows[k] = ev + old := cache[idx].Row + deltas := make(render.DeltaRow, len(row.Fields)) + if reflect.DeepEqual(old, row) { + cache[idx].Kind = render.EventUnchanged + cache[idx].Deltas = deltas + continue + } - // newr := ev.Fields - // if _, ok := cache[k]; !ok { - // ev.Action, ev.Deltas = watch.Added, noDeltas - // continue - // } - // oldr := cache[k].Fields - // deltas := make(resource.Row, len(newr)) - // if !reflect.DeepEqual(oldr, newr) { - // ev.Action = watch.Modified - // for i, field := range oldr { - // if field != newr[i] { - // deltas[i] = field - // } - // } - // ev.Deltas = deltas - // } else { - // ev.Action = resource.Unchanged - // ev.Deltas = noDeltas - // } - // } + cache[idx].Kind = render.EventUpdate + for i, field := range old.Fields { + if field != row.Fields[i] { + deltas[i] = field + } + } + cache[idx].Deltas = deltas + } - // for k := range evts { - // if _, ok := table.Rows[k]; !ok { - // delete(evts, k) - // } - // } - // c.setCache(evts) + for _, row := range rows { + if _, ok := cache.FindIndex(row.ID); !ok { + cache.Delete(row.ID) + } + } + table.RowEvents = cache - // return table + return table } -func (s *Subject) clusterSubjects() (resource.RowEvents, error) { - crbs, err := s.app.Conn().DialOrDie().RbacV1().ClusterRoleBindings().List(metav1.ListOptions{}) +func (s *Subject) fetchClusterRoleBindings() (render.Rows, error) { + s.app.factory.Preload(render.ClusterWide, "rbac.authorization.k8s.io/v1/clusterroles") + oo, err := s.app.factory.List(render.ClusterWide, "rbac.authorization.k8s.io/v1/clusterrolebindings", labels.Everything()) if err != nil { return nil, err } - evts := make(resource.RowEvents, len(crbs.Items)) - for _, crb := range crbs.Items { + rows := make(render.Rows, 0, len(oo)) + for _, o := range oo { + var crb rbacv1.ClusterRoleBinding + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crb) + if err != nil { + return nil, err + } for _, subject := range crb.Subjects { if subject.Kind != s.subjectKind { continue } - evts[subject.Name] = &resource.RowEvent{ - Fields: resource.Row{subject.Name, "ClusterRoleBinding", crb.Name}, - } + rows = append(rows, render.Row{ + ID: subject.Name, + Fields: render.Fields{subject.Name, "ClusterRoleBinding", crb.Name}, + }) } } - return evts, nil + return rows, nil } -func (s *Subject) namespacedSubjects() (resource.RowEvents, error) { - rbs, err := s.app.Conn().DialOrDie().RbacV1().RoleBindings("").List(metav1.ListOptions{}) +func (s *Subject) fetchRoleBindings() (render.Rows, error) { + s.app.factory.Preload(render.ClusterWide, "rbac.authorization.k8s.io/v1/clusterroles") + oo, err := s.app.factory.List(render.ClusterWide, "rbac.authorization.k8s.io/v1/rolebindings", labels.Everything()) if err != nil { return nil, err } - evts := make(resource.RowEvents, len(rbs.Items)) - for _, rb := range rbs.Items { + rows := make(render.Rows, 0, len(oo)) + for _, o := range oo { + var rb rbacv1.RoleBinding + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rb) + if err != nil { + return nil, err + } for _, subject := range rb.Subjects { if subject.Kind == s.subjectKind { - evts[subject.Name] = &resource.RowEvent{ - Fields: resource.Row{subject.Name, "RoleBinding", rb.Name}, - } + rows = append(rows, render.Row{ + ID: subject.Name, + Fields: render.Fields{subject.Name, "RoleBinding", rb.Name}, + }) } } } - return evts, nil + return rows, nil } func mapCmdSubject(subject string) string { diff --git a/internal/view/table.go b/internal/view/table.go index 622f96a7..b7c39a35 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -12,11 +12,10 @@ import ( type Table struct { *ui.Table - app *App - filterFn func(string) - cancelFn context.CancelFunc - decorateFn DecorateFunc - enterFn EnterFunc + app *App + filterFn func(string) + cancelFn context.CancelFunc + enterFn EnterFunc } func NewTable(title string) *Table { @@ -33,7 +32,6 @@ func (t *Table) Init(ctx context.Context) (err error) { } ctx = context.WithValue(ctx, ui.KeyStyles, t.app.Styles) t.Table.Init(ctx) - t.bindKeys() return nil @@ -80,11 +78,6 @@ func (t *Table) SetEnterFn(f EnterFunc) { t.enterFn = f } -// SetDecorateFn specifies the default row decorator. -func (t *Table) SetDecorateFn(f DecorateFunc) { - t.decorateFn = f -} - // SetExtraActionsFn specifies custom keyboard behavior. func (t *Table) SetExtraActionsFn(BoostActionsFunc) {} diff --git a/internal/view/table_helper.go b/internal/view/table_helper.go index 82304a2c..269a236a 100644 --- a/internal/view/table_helper.go +++ b/internal/view/table_helper.go @@ -9,6 +9,7 @@ import ( "time" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" "github.com/rs/zerolog/log" @@ -41,7 +42,7 @@ func computeFilename(cluster, ns, title, path string) (string, error) { return strings.ToLower(filepath.Join(dir, fName)), nil } -func saveTable(cluster, title, path string, data resource.TableData) (string, error) { +func saveTable(cluster, title, path string, data render.TableData) (string, error) { ns := data.Namespace if ns == resource.AllNamespaces { ns = resource.AllNamespace @@ -84,5 +85,5 @@ func saveTable(cluster, title, path string, data resource.TableData) (string, er return "", err } - return path, nil + return fPath, nil } diff --git a/internal/view/table_int_test.go b/internal/view/table_int_test.go index 94b5de6b..5d28209e 100644 --- a/internal/view/table_int_test.go +++ b/internal/view/table_int_test.go @@ -8,7 +8,6 @@ import ( "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/render" - "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/stretchr/testify/assert" @@ -32,12 +31,12 @@ func TestTableNew(t *testing.T) { v := NewTable("test") v.Init(makeContext()) - data := resource.TableData{ + data := render.TableData{ Header: render.HeaderRow{ render.Header{Name: "NAMESPACE"}, render.Header{Name: "NAME", Align: tview.AlignRight}, render.Header{Name: "FRED"}, - render.Header{Name: "AGE"}, + render.Header{Name: "AGE", Decorator: ageDecorator}, }, RowEvents: render.RowEvents{ render.RowEvent{ @@ -61,12 +60,12 @@ func TestTableViewFilter(t *testing.T) { v := NewTable("test") v.Init(makeContext()) - data := resource.TableData{ + data := render.TableData{ Header: render.HeaderRow{ render.Header{Name: "NAMESPACE"}, render.Header{Name: "NAME", Align: tview.AlignRight}, render.Header{Name: "FRED"}, - render.Header{Name: "AGE"}, + render.Header{Name: "AGE", Decorator: ageDecorator}, }, RowEvents: render.RowEvents{ render.RowEvent{ @@ -95,12 +94,12 @@ func TestTableViewSort(t *testing.T) { v := NewTable("test") v.Init(makeContext()) - data := resource.TableData{ + data := render.TableData{ Header: render.HeaderRow{ render.Header{Name: "NAMESPACE"}, render.Header{Name: "NAME", Align: tview.AlignRight}, render.Header{Name: "FRED"}, - render.Header{Name: "AGE"}, + render.Header{Name: "AGE", Decorator: ageDecorator}, }, RowEvents: render.RowEvents{ render.RowEvent{ diff --git a/internal/view/types.go b/internal/view/types.go index ef78b4e2..3941b37a 100644 --- a/internal/view/types.go +++ b/internal/view/types.go @@ -1,10 +1,10 @@ package view import ( + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type ( @@ -23,9 +23,6 @@ type ( // EnterFunc represents an enter key action. EnterFunc func(app *App, ns, resource, selection string) - // DecorateFunc represents a row decorator. - DecorateFunc func(resource.TableData) resource.TableData - // ContainerFunc returns the active container name. ContainerFunc func() string ) @@ -68,6 +65,11 @@ type ResourceViewer interface { // SetPath set parents selector. SetPath(p string) + + // GVR returns a resource descriptor. + GVR() string + + SetContextFn(ContextFunc) } // TableViewer represents a tabular viewer. @@ -100,18 +102,15 @@ type SubjectViewer interface { SetSubject(s string) } +type ViewerFunc func(dao.GVR) ResourceViewer + // MetaViewer represents a registered meta viewer. type MetaViewer struct { - gvr string - kind string - namespaced bool - verbs metav1.Verbs - viewFn ViewFunc - listFn ListFunc - enterFn EnterFunc - colorerFn ui.ColorerFunc - decorateFn DecorateFunc + viewerFn ViewerFunc + viewFn ViewFunc + listFn ListFunc + enterFn EnterFunc } // MetaViewers represents a collection of meta viewers. -type MetaViewers map[string]MetaViewer +type MetaViewers map[dao.GVR]MetaViewer diff --git a/internal/views/mock_connection.go b/internal/views/mock_connection.go deleted file mode 100644 index 50847ad7..00000000 --- a/internal/views/mock_connection.go +++ /dev/null @@ -1,825 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/resource (interfaces: Connection) - -package views - -import ( - k8s "github.com/derailed/k9s/internal/k8s" - pegomock "github.com/petergtz/pegomock" - v1 "k8s.io/api/core/v1" - version "k8s.io/apimachinery/pkg/version" - disk "k8s.io/client-go/discovery/cached/disk" - dynamic "k8s.io/client-go/dynamic" - kubernetes "k8s.io/client-go/kubernetes" - rest "k8s.io/client-go/rest" - versioned "k8s.io/metrics/pkg/client/clientset/versioned" - "reflect" - "time" -) - -type MockConnection struct { - fail func(message string, callerSkip ...int) -} - -func NewMockConnection(options ...pegomock.Option) *MockConnection { - mock := &MockConnection{} - for _, option := range options { - option.Apply(mock) - } - return mock -} - -func (mock *MockConnection) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } -func (mock *MockConnection) FailHandler() pegomock.FailHandler { return mock.fail } - -func (mock *MockConnection) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CachedDiscovery", params, []reflect.Type{reflect.TypeOf((**disk.CachedDiscoveryClient)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *disk.CachedDiscoveryClient - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*disk.CachedDiscoveryClient) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) CanI(_param0 string, _param1 string, _param2 []string) (bool, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0, _param1, _param2} - result := pegomock.GetGenericMockFrom(mock).Invoke("CanI", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 bool - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) CheckListNSAccess() error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CheckListNSAccess", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(error) - } - } - return ret0 -} - -func (mock *MockConnection) CheckNSAccess(_param0 string) error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("CheckNSAccess", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(error) - } - } - return ret0 -} - -func (mock *MockConnection) Config() *k8s.Config { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("Config", params, []reflect.Type{reflect.TypeOf((**k8s.Config)(nil)).Elem()}) - var ret0 *k8s.Config - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*k8s.Config) - } - } - return ret0 -} - -func (mock *MockConnection) CurrentNamespaceName() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CurrentNamespaceName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) DialOrDie() kubernetes.Interface { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("DialOrDie", params, []reflect.Type{reflect.TypeOf((*kubernetes.Interface)(nil)).Elem()}) - var ret0 kubernetes.Interface - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(kubernetes.Interface) - } - } - return ret0 -} - -func (mock *MockConnection) DynDialOrDie() dynamic.Interface { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("DynDialOrDie", params, []reflect.Type{reflect.TypeOf((*dynamic.Interface)(nil)).Elem()}) - var ret0 dynamic.Interface - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(dynamic.Interface) - } - } - return ret0 -} - -func (mock *MockConnection) FetchNodes() (*v1.NodeList, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("FetchNodes", params, []reflect.Type{reflect.TypeOf((**v1.NodeList)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *v1.NodeList - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*v1.NodeList) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) HasMetrics() bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("HasMetrics", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) IsNamespaced(_param0 string) bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("IsNamespaced", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) MXDial() (*versioned.Clientset, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("MXDial", params, []reflect.Type{reflect.TypeOf((**versioned.Clientset)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *versioned.Clientset - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*versioned.Clientset) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) NSDialOrDie() dynamic.NamespaceableResourceInterface { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("NSDialOrDie", params, []reflect.Type{reflect.TypeOf((*dynamic.NamespaceableResourceInterface)(nil)).Elem()}) - var ret0 dynamic.NamespaceableResourceInterface - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(dynamic.NamespaceableResourceInterface) - } - } - return ret0 -} - -func (mock *MockConnection) NodePods(_param0 string) (*v1.PodList, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("NodePods", params, []reflect.Type{reflect.TypeOf((**v1.PodList)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *v1.PodList - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*v1.PodList) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) RestConfigOrDie() *rest.Config { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("RestConfigOrDie", params, []reflect.Type{reflect.TypeOf((**rest.Config)(nil)).Elem()}) - var ret0 *rest.Config - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*rest.Config) - } - } - return ret0 -} - -func (mock *MockConnection) ServerVersion() (*version.Info, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ServerVersion", params, []reflect.Type{reflect.TypeOf((**version.Info)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *version.Info - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*version.Info) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0, _param1} - result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 bool - var ret2 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(bool) - } - if result[2] != nil { - ret2 = result[2].(error) - } - } - return ret0, ret1, ret2 -} - -func (mock *MockConnection) SupportsResource(_param0 string) bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsResource", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) SwitchContextOrDie(_param0 string) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0} - pegomock.GetGenericMockFrom(mock).Invoke("SwitchContextOrDie", params, []reflect.Type{}) -} - -func (mock *MockConnection) ValidNamespaces() ([]v1.Namespace, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ValidNamespaces", params, []reflect.Type{reflect.TypeOf((*[]v1.Namespace)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 []v1.Namespace - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].([]v1.Namespace) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) VerifyWasCalledOnce() *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: pegomock.Times(1), - } -} - -func (mock *MockConnection) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - } -} - -func (mock *MockConnection) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - inOrderContext: inOrderContext, - } -} - -func (mock *MockConnection) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - timeout: timeout, - } -} - -type VerifierMockConnection struct { - mock *MockConnection - invocationCountMatcher pegomock.Matcher - inOrderContext *pegomock.InOrderContext - timeout time.Duration -} - -func (verifier *VerifierMockConnection) CachedDiscovery() *MockConnection_CachedDiscovery_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CachedDiscovery", params, verifier.timeout) - return &MockConnection_CachedDiscovery_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CachedDiscovery_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CachedDiscovery_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_CachedDiscovery_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) CanI(_param0 string, _param1 string, _param2 []string) *MockConnection_CanI_OngoingVerification { - params := []pegomock.Param{_param0, _param1, _param2} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CanI", params, verifier.timeout) - return &MockConnection_CanI_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CanI_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CanI_OngoingVerification) GetCapturedArguments() (string, string, []string) { - _param0, _param1, _param2 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1], _param2[len(_param2)-1] -} - -func (c *MockConnection_CanI_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 [][]string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - _param1 = make([]string, len(params[1])) - for u, param := range params[1] { - _param1[u] = param.(string) - } - _param2 = make([][]string, len(params[2])) - for u, param := range params[2] { - _param2[u] = param.([]string) - } - } - return -} - -func (verifier *VerifierMockConnection) CheckListNSAccess() *MockConnection_CheckListNSAccess_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckListNSAccess", params, verifier.timeout) - return &MockConnection_CheckListNSAccess_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CheckListNSAccess_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CheckListNSAccess_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_CheckListNSAccess_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) CheckNSAccess(_param0 string) *MockConnection_CheckNSAccess_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckNSAccess", params, verifier.timeout) - return &MockConnection_CheckNSAccess_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CheckNSAccess_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CheckNSAccess_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_CheckNSAccess_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockConnection) Config() *MockConnection_Config_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Config", params, verifier.timeout) - return &MockConnection_Config_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_Config_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_Config_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_Config_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) CurrentNamespaceName() *MockConnection_CurrentNamespaceName_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CurrentNamespaceName", params, verifier.timeout) - return &MockConnection_CurrentNamespaceName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CurrentNamespaceName_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CurrentNamespaceName_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_CurrentNamespaceName_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) DialOrDie() *MockConnection_DialOrDie_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DialOrDie", params, verifier.timeout) - return &MockConnection_DialOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_DialOrDie_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_DialOrDie_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_DialOrDie_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) DynDialOrDie() *MockConnection_DynDialOrDie_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DynDialOrDie", params, verifier.timeout) - return &MockConnection_DynDialOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_DynDialOrDie_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_DynDialOrDie_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_DynDialOrDie_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) FetchNodes() *MockConnection_FetchNodes_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "FetchNodes", params, verifier.timeout) - return &MockConnection_FetchNodes_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_FetchNodes_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_FetchNodes_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_FetchNodes_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) HasMetrics() *MockConnection_HasMetrics_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasMetrics", params, verifier.timeout) - return &MockConnection_HasMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_HasMetrics_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_HasMetrics_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_HasMetrics_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) IsNamespaced(_param0 string) *MockConnection_IsNamespaced_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "IsNamespaced", params, verifier.timeout) - return &MockConnection_IsNamespaced_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_IsNamespaced_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_IsNamespaced_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_IsNamespaced_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockConnection) MXDial() *MockConnection_MXDial_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "MXDial", params, verifier.timeout) - return &MockConnection_MXDial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_MXDial_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_MXDial_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_MXDial_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) NSDialOrDie() *MockConnection_NSDialOrDie_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "NSDialOrDie", params, verifier.timeout) - return &MockConnection_NSDialOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_NSDialOrDie_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_NSDialOrDie_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_NSDialOrDie_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) NodePods(_param0 string) *MockConnection_NodePods_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "NodePods", params, verifier.timeout) - return &MockConnection_NodePods_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_NodePods_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_NodePods_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_NodePods_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockConnection) RestConfigOrDie() *MockConnection_RestConfigOrDie_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RestConfigOrDie", params, verifier.timeout) - return &MockConnection_RestConfigOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_RestConfigOrDie_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_RestConfigOrDie_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_RestConfigOrDie_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) ServerVersion() *MockConnection_ServerVersion_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ServerVersion", params, verifier.timeout) - return &MockConnection_ServerVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ServerVersion_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ServerVersion_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ServerVersion_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) SupportsRes(_param0 string, _param1 []string) *MockConnection_SupportsRes_OngoingVerification { - params := []pegomock.Param{_param0, _param1} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SupportsRes", params, verifier.timeout) - return &MockConnection_SupportsRes_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_SupportsRes_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_SupportsRes_OngoingVerification) GetCapturedArguments() (string, []string) { - _param0, _param1 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1] -} - -func (c *MockConnection_SupportsRes_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 [][]string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - _param1 = make([][]string, len(params[1])) - for u, param := range params[1] { - _param1[u] = param.([]string) - } - } - return -} - -func (verifier *VerifierMockConnection) SupportsResource(_param0 string) *MockConnection_SupportsResource_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SupportsResource", params, verifier.timeout) - return &MockConnection_SupportsResource_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_SupportsResource_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_SupportsResource_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_SupportsResource_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockConnection) SwitchContextOrDie(_param0 string) *MockConnection_SwitchContextOrDie_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SwitchContextOrDie", params, verifier.timeout) - return &MockConnection_SwitchContextOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_SwitchContextOrDie_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_SwitchContextOrDie_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_SwitchContextOrDie_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockConnection) ValidNamespaces() *MockConnection_ValidNamespaces_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ValidNamespaces", params, verifier.timeout) - return &MockConnection_ValidNamespaces_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ValidNamespaces_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ValidNamespaces_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ValidNamespaces_OngoingVerification) GetAllCapturedArguments() { -} diff --git a/internal/watch/factory.go b/internal/watch/factory.go index 3e79a453..570398c8 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -1,12 +1,12 @@ package watch import ( - "context" "fmt" "strings" "time" - "github.com/derailed/k9s/internal/k9s" + "github.com/derailed/k9s/internal/k8s" + "github.com/derailed/k9s/internal/render" "github.com/rs/zerolog/log" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -18,17 +18,49 @@ import ( const defaultResync = 10 * time.Minute +// BOZO!! +// // Authorizer checks what a user can or cannot do to a resource. +// type Authorizer interface { +// // CanI returns true if the user can use these actions for a given resource. +// CanI(ns, gvr string, verbs []string) (bool, error) +// } + +// type Connection interface { +// Authorizer + +// // DialOrDie dials client api. +// DialOrDie() kubernetes.Interface + +// // MXDial dials metrics api. +// MXDial() (*versioned.Clientset, error) + +// // DynDialOrDie dials dynamic client api. +// DynDialOrDie() dynamic.Interface + +// // RestConfigOrDie return a client configuration. +// RestConfigOrDie() *restclient.Config + +// // Config returns the current kubeconfig. +// Config() *k8s.Config + +// // CachedDiscovery returns a cached client. +// CachedDiscovery() (*disk.CachedDiscoveryClient, error) + +// // SwithContextOrDie switch to a new kube context. +// SwitchContextOrDie(ctx string) +// } + // Factory tracks various resource informers. type Factory struct { factories map[string]di.DynamicSharedInformerFactory - client k9s.Connection + client k8s.Connection stopChan chan struct{} tweakListOptions internalinterfaces.TweakListOptionsFunc activeNS string } // NewFactory returns a new informers factory. -func NewFactory(client k9s.Connection) *Factory { +func NewFactory(client k8s.Connection) *Factory { return &Factory{ client: client, stopChan: make(chan struct{}), @@ -39,7 +71,16 @@ func NewFactory(client k9s.Connection) *Factory { func (f *Factory) Dump() { log.Debug().Msgf("----------- FACTORIES -------------") for ns := range f.factories { - log.Debug().Msgf("Factory for NS %q", ns) + log.Debug().Msgf(" Factory for NS %q", ns) + } + log.Debug().Msgf("-----------------------------------") +} + +func (f *Factory) Debug(gvr string) { + log.Debug().Msgf("----------- DEBUG FACTORY (%s) -------------", gvr) + inf := f.factories[render.AllNamespaces].ForResource(toGVR(gvr)) + for i, k := range inf.Informer().GetStore().ListKeys() { + log.Debug().Msgf("%d -- %s", i, k) } } @@ -66,11 +107,14 @@ func (f *Factory) List(ns, gvr string, sel labels.Selector) ([]runtime.Object, e return nil, fmt.Errorf("No resource for GVR %s", gvr) } + if ns == render.ClusterWide { + return inf.Lister().List(sel) + } return inf.Lister().ByNamespace(ns).List(sel) } func (f *Factory) Get(ns, gvr, name string, sel labels.Selector) (runtime.Object, error) { - log.Debug().Msgf("<<<<<<<<<<<<<<<<< FACTORY GET %q", gvr) + log.Debug().Msgf("<<<<<<<<<<<<<<<<< FACTORY GET %q --- %q:%q", gvr, ns, name) auth, err := f.Client().CanI(ns, gvr, []string{"get"}) if err != nil { return nil, err @@ -85,29 +129,28 @@ func (f *Factory) Get(ns, gvr, name string, sel labels.Selector) (runtime.Object return nil, fmt.Errorf("No resource for GVR %s", gvr) } + if ns == render.ClusterWide { + return inf.Lister().Get(name) + } return inf.Lister().ByNamespace(ns).Get(name) } func (f *Factory) WaitForCacheSync() map[schema.GroupVersionResource]bool { r := make(map[schema.GroupVersionResource]bool) for n, fac := range f.factories { - log.Debug().Msgf("Waiting for fac %q", n) + log.Debug().Msgf(">>> WAITING FOR FACTORY SYNC -- %q", n) res := fac.WaitForCacheSync(f.stopChan) - log.Debug().Msgf("DONE!") for k, v := range res { r[k] = v - log.Debug().Msgf("CACHE %v -- %v", k, v) + log.Debug().Msgf(" GVR resource %v -- %v", k, v) } + log.Debug().Msgf("<<< DONE!") } return r } -func (f *Factory) Init(ctx context.Context) { - go func() { - f.Start(f.stopChan) - <-ctx.Done() - f.Terminate() - }() +func (f *Factory) Init() { + f.Start(f.stopChan) } func (f *Factory) Terminate() { @@ -115,6 +158,9 @@ func (f *Factory) Terminate() { close(f.stopChan) f.stopChan = nil } + for k := range f.factories { + delete(f.factories, k) + } } // Start initializes the informers until caller cancels the context. @@ -127,20 +173,46 @@ func (f *Factory) Start(stopChan chan struct{}) { // BOZO!! Check ns access for resource?? func (f *Factory) SetActive(ns string) { - if !f.cluserWide() { + if !f.isClusterWide() { f.ensureFactory(ns) } f.activeNS = ns } -func (f *Factory) cluserWide() bool { - _, ok := f.factories[""] +func (f *Factory) isClusterWide() bool { + _, ok := f.factories[render.AllNamespaces] return ok } +func (f *Factory) preload(ns string) { + f.ForResource(ns, "v1/pods") + f.ForResource(render.AllNamespaces, "apiextensions.k8s.io/v1beta1/customresourcedefinitions") +} + +func (f *Factory) FactoryFor(ns string) di.DynamicSharedInformerFactory { + return f.factories[ns] +} + +func (f *Factory) Preload(ns, gvr string) { + _ = f.ForResource(ns, gvr) +} + +func (f *Factory) ForResource(ns, gvr string) informers.GenericInformer { + defer func(t time.Time) { + log.Debug().Msgf("ForResource Elapsed %v", time.Since(t)) + }(time.Now()) + + fact := f.ensureFactory(ns) + log.Debug().Msgf("--- FORRESOURCE %q -- %q -- %#v", ns, gvr, toGVR(gvr)) + inf := fact.ForResource(toGVR(gvr)) + fact.Start(f.stopChan) + + return inf +} + func (f *Factory) ensureFactory(ns string) di.DynamicSharedInformerFactory { - if f.cluserWide() { - ns = "" + if f.isClusterWide() { + ns = render.AllNamespaces } if fac, ok := f.factories[ns]; ok { return fac @@ -153,36 +225,12 @@ func (f *Factory) ensureFactory(ns string) di.DynamicSharedInformerFactory { nil, ) f.preload(ns) - f.WaitForCacheSync() + // f.WaitForCacheSync() f.Dump() return f.factories[ns] } -func (f *Factory) preload(ns string) { - f.ForResource(ns, "v1/pods") - f.ForResource("", "apiextensions.k8s.io/v1beta1/customresourcedefinitions") -} - -func (f *Factory) FactoryFor(ns string) di.DynamicSharedInformerFactory { - return f.factories[ns] -} - -func (f *Factory) ForResource(ns, gvr string) informers.GenericInformer { - log.Debug().Msgf("Loading resource %q", gvr) - fact := f.ensureFactory(ns) - log.Debug().Msgf("--- FORRESOURCE %q -- %#v", ns, toGVR(gvr)) - inf := fact.ForResource(toGVR(gvr)) - fact.Start(f.stopChan) - - // f.WaitForCacheSync() - // for i, k := range inf.Informer().GetStore().ListKeys() { - // log.Debug().Msgf("%d -- %s", i, k) - // } - - return inf -} - func (f *Factory) register(gvr, ns string, stopChan <-chan struct{}) error { log.Debug().Msgf("Registering GVR %q - %s", ns, gvr) f.factories[ns].ForResource(toGVR(gvr)) @@ -205,82 +253,6 @@ func toGVR(s string) schema.GroupVersionResource { } // Client return the factory connection. -func (f *Factory) Client() k9s.Connection { +func (f *Factory) Client() k8s.Connection { return f.client } - -// func (f *Factory) ForResource(res schema.GroupVersionResource) informers.GenericInformer { -// log.Debug().Msgf("ForResource %v", res) -// switch res { -// case schema.GroupVersionResource{"metrics.k8s.io", "v1beta1", "pods"}: -// return &genericInformer{ -// resource: res.GroupResource(), -// informer: f.MetricsV1Beta1("").PodMetricses().Informer(), -// } -// case schema.GroupVersionResource{"metrics.k8s.io", "v1beta1", "nodes"}: -// return &genericInformer{ -// resource: res.GroupResource(), -// informer: f.MetricsV1Beta1("").NodeMetricses().Informer(), -// } -// default: -// return f.factories[""].ForResource(res) -// } -// } - -// func (f *Factory) MetricsV1Beta1(ns string) v1beta1.Interface { -// return v1beta1.New(f.client, f, ns, f.tweakListOptions) -// } - -// type genericInformer struct { -// informer cache.SharedIndexInformer -// resource schema.GroupResource -// } - -// // Informer returns the SharedIndexInformer. -// func (f *genericInformer) Informer() cache.SharedIndexInformer { -// return f.informer -// } - -// // Lister returns the GenericLister. -// func (f *genericInformer) Lister() cache.GenericLister { -// return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) -// } - -// // InternalInformerFor returns the SharedIndexInformer for obj using an internal -// // client. -// func (f *Factory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { -// // var inf informers.GenericInformer - -// kind := reflect.TypeOf(obj) -// log.Debug().Msgf("Informer for %v", kind) -// // switch kind { -// // case v1beta1.PodMetrics: -// // inf = f.ForResource("", toGVR("metrics.k8s.io/v1beta1/pods")) -// // if inf, ok := f.informers[kind]; ok { -// // return inf -// // } -// // case v1beta1.NodeMetrics: -// // inf = f.ForResource("", toGVR("metrics.k8s.io/v1beta1/nodes")) -// // if inf, ok := f.informers[kind]; ok { -// // return inf -// // } -// // default: -// // panic(fmt.Errorf("Unknown type %#v", t)) -// // } -// // informerType := -// // informer, exists := f.informers[informerType] -// // if exists { -// // return informer -// // } - -// // resyncPeriod, exists := f.customResync[informerType] -// // if !exists { -// // resyncPeriod = f.defaultResync -// // } - -// // informer = newFunc(f.client, resyncPeriod) -// // f.informers[kind] = informer - -// // return informer -// return nil -// }