From 48869f9f51f6c9c4470c04165239df82a0bb2f44 Mon Sep 17 00:00:00 2001 From: derailed Date: Thu, 2 Jan 2020 18:15:38 -0700 Subject: [PATCH] fix #463 --- README.md | 6 +- change_logs/release_0.10.10.md | 25 +++++ internal/model/generic.go | 27 ++++- internal/model/resource.go | 5 + internal/model/table.go | 27 ++++- internal/model/types.go | 3 + internal/render/generic.go | 42 ++++++-- internal/render/generic_test.go | 171 +++++++++++++++++++++++++++++--- internal/ui/select_table.go | 40 -------- internal/ui/table_test.go | 8 +- internal/ui/types.go | 47 ++++++++- internal/view/alias_test.go | 8 +- internal/view/browser.go | 27 +++++ internal/view/table.go | 27 ----- internal/view/table_int_test.go | 8 +- internal/watch/factory.go | 17 ++-- internal/watch/helper.go | 6 +- 17 files changed, 375 insertions(+), 119 deletions(-) create mode 100644 change_logs/release_0.10.10.md diff --git a/README.md b/README.md index 3291d1d8..13c4dddb 100644 --- a/README.md +++ b/README.md @@ -569,12 +569,12 @@ to make this project a reality! ## Meet The Core Team! -* [Gustavo Silva Paiva](https://github.com/paivagustavo) - * guustavo.paiva@gmail.com - * [@paivagustavodev](https://twitter.com/paivagustavodev) * [Fernand Galiana](https://github.com/derailed) * fernand@imhotep.io * [@kitesurfer](https://twitter.com/kitesurfer?lang=en) +* [Gustavo Silva Paiva](https://github.com/paivagustavo) + * guustavo.paiva@gmail.com + * [@paivagustavodev](https://twitter.com/paivagustavodev) --- diff --git a/change_logs/release_0.10.10.md b/change_logs/release_0.10.10.md new file mode 100644 index 00000000..6f69a4ee --- /dev/null +++ b/change_logs/release_0.10.10.md @@ -0,0 +1,25 @@ + + +# Release v0.10.10 + +## Notes + +Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated! + +Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +--- + +## Change Logs + +Maintenance release! + +--- + +## Resolved Bugs/Features + +* [Issue #463](https://github.com/derailed/k9s/issues/463) + +--- + + © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/internal/model/generic.go b/internal/model/generic.go index 5070a9da..972d7bfb 100644 --- a/internal/model/generic.go +++ b/internal/model/generic.go @@ -6,6 +6,7 @@ import ( "github.com/derailed/k9s/internal/client" "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" @@ -23,6 +24,20 @@ type Generic struct { table *metav1beta1.Table } +// List returns a collection of node resources. +func (g *Generic) Get(ctx context.Context, path string) (runtime.Object, error) { + var opts metav1.GetOptions + + ns, n := client.Namespaced(path) + gvr := client.NewGVR(g.gvr) + req := g.factory.Client().DynDialOrDie().Resource(gvr.AsGVR()) + if ns == "" { + return req.Get(n, opts) + } + + return req.Namespace(ns).Get(n, opts) +} + // List returns a collection of node resources. func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) { // Ensures the factory is tracking this resource @@ -39,13 +54,19 @@ func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) { return nil, err } - // BOZO!! Need to know if gvr is namespaced or not + ns := g.namespace + if g.namespace == render.ClusterScope { + ns = render.AllNamespaces + } + + log.Debug().Msgf("GENERIC LIST %q:%q", g.namespace, g.gvr) 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() + Namespace(ns). + Do(). + Get() if err != nil { return nil, err } diff --git a/internal/model/resource.go b/internal/model/resource.go index 7bc21aa9..b32af0dd 100644 --- a/internal/model/resource.go +++ b/internal/model/resource.go @@ -21,6 +21,11 @@ func (r *Resource) Init(ns, gvr string, f dao.Factory) { r.namespace, r.gvr, r.factory = ns, gvr, f } +// Get returns a resource instance if found, else an error. +func (r *Resource) Get(ctx context.Context, path string) (runtime.Object, error) { + return r.factory.Get(r.gvr, path, true, labels.Everything()) +} + // List returns a collection of nodes. func (r *Resource) List(ctx context.Context) ([]runtime.Object, error) { strLabel, ok := ctx.Value(internal.KeyLabels).(string) diff --git a/internal/model/table.go b/internal/model/table.go index 5afb22f9..ccf6f7eb 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -47,6 +47,18 @@ func (t *Table) Watch(ctx context.Context) { go t.updater(ctx) } +// Get returns a resource instance if found, else an error. +func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { + meta := t.resourceMeta() + factory, ok := ctx.Value(internal.KeyFactory).(dao.Factory) + if !ok { + return nil, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory)) + } + meta.Model.Init(t.namespace, t.gvr, factory) + + return meta.Model.Get(ctx, path) +} + // Refresh update the model now. func (t *Table) Refresh(ctx context.Context) { t.refresh(ctx) @@ -158,11 +170,7 @@ func (t *Table) list(ctx context.Context, l Lister) ([]runtime.Object, error) { return l.List(ctx) } -func (t *Table) reconcile(ctx context.Context) error { - defer func(t time.Time) { - log.Debug().Msgf("RECONCILE elapsed %v", time.Since(t)) - }(time.Now()) - +func (t *Table) resourceMeta() ResourceMeta { meta, ok := Registry[t.gvr] if !ok { log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr) @@ -175,6 +183,15 @@ func (t *Table) reconcile(ctx context.Context) error { meta.Model = &Resource{} } + return meta +} + +func (t *Table) reconcile(ctx context.Context) error { + defer func(t time.Time) { + log.Debug().Msgf("RECONCILE elapsed %v", time.Since(t)) + }(time.Now()) + + meta := t.resourceMeta() oo, err := t.list(ctx, meta.Model) if err != nil { return err diff --git a/internal/model/types.go b/internal/model/types.go index e7576bde..e7c0dae8 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -62,6 +62,9 @@ type Lister interface { // List returns a collection of resources. List(context.Context) ([]runtime.Object, error) + // Get returns a resource instance. + Get(ctx context.Context, path string) (runtime.Object, error) + // Hydrate converts resource rows into tabular data. Hydrate(oo []runtime.Object, rr render.Rows, r Renderer) error } diff --git a/internal/render/generic.go b/internal/render/generic.go index 5a36c0de..94fe4118 100644 --- a/internal/render/generic.go +++ b/internal/render/generic.go @@ -9,9 +9,13 @@ import ( metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" ) +const ageTableCol = "Age" + // Generic renders a generic resource to screen. type Generic struct { table *metav1beta1.Table + + ageIndex int } // SetTable sets the tabular resource. @@ -34,9 +38,16 @@ func (g *Generic) Header(ns string) HeaderRow { if ns == "" { h = append(h, Header{Name: "NAMESPACE"}) } - for _, c := range g.table.ColumnDefinitions { + for i, c := range g.table.ColumnDefinitions { + if c.Name == ageTableCol { + g.ageIndex = i + continue + } h = append(h, Header{Name: strings.ToUpper(c.Name)}) } + if g.ageIndex > 0 { + h = append(h, Header{Name: "AGE"}) + } return h } @@ -48,24 +59,35 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error { return fmt.Errorf("expecting a TableRow but got %T", o) } - r.ID, ok = row.Cells[0].(string) + var nns = AllNamespaces + if ns != ClusterScope { + var err error + nns, err = extractNamespace(row.Object.Raw) + if err != nil { + return err + } + } + n, ok := row.Cells[0].(string) if !ok { return fmt.Errorf("expecting row id to be a string but got %#v", row.Cells[0]) } - rns, err := extractNamespace(row.Object.Raw) - if err != nil { - return err - } - - r.ID = FQN(rns, r.ID) + r.ID = FQN(nns, n) r.Fields = make(Fields, 0, len(g.Header(ns))) if isAllNamespace(ns) { - r.Fields = append(r.Fields, rns) + r.Fields = append(r.Fields, nns) } - for _, c := range row.Cells { + var ageCell interface{} + for i, c := range row.Cells { + if g.ageIndex > 0 && i == g.ageIndex { + ageCell = c + continue + } r.Fields = append(r.Fields, fmt.Sprintf("%v", c)) } + if ageCell != nil { + r.Fields = append(r.Fields, fmt.Sprintf("%v", ageCell)) + } return nil } diff --git a/internal/render/generic_test.go b/internal/render/generic_test.go index 7fc7de92..df89a058 100644 --- a/internal/render/generic_test.go +++ b/internal/render/generic_test.go @@ -10,24 +10,83 @@ import ( ) func TestGenericRender(t *testing.T) { - var g render.Generic + uu := map[string]struct { + ns string + table *metav1beta1.Table + eID string + eFields render.Fields + eHeader render.HeaderRow + }{ + "specific_ns": { + ns: "blee", + table: makeNSGeneric(), + eID: "ns1/c1", + eFields: render.Fields{"c1", "c2", "c3"}, + eHeader: render.HeaderRow{ + render.Header{Name: "A"}, + render.Header{Name: "B"}, + render.Header{Name: "C"}, + }, + }, + "all_ns": { + ns: "", + table: makeAllNSGeneric(), + eID: "ns1/c1", + eFields: render.Fields{"ns1", "c1", "c2", "c3"}, + eHeader: render.HeaderRow{ + render.Header{Name: "NAMESPACE"}, + render.Header{Name: "A"}, + render.Header{Name: "B"}, + render.Header{Name: "C"}, + }, + }, + "cluster": { + ns: "-", + table: makeClusterGeneric(), + eID: "c1", + eFields: render.Fields{"c1", "c2", "c3"}, + eHeader: render.HeaderRow{ + render.Header{Name: "A"}, + render.Header{Name: "B"}, + render.Header{Name: "C"}, + }, + }, + "age": { + ns: "-", + table: makeAgeGeneric(), + eID: "c1", + eFields: render.Fields{"c1", "c2", "Age"}, + eHeader: render.HeaderRow{ + render.Header{Name: "A"}, + render.Header{Name: "C"}, + render.Header{Name: "AGE"}, + }, + }, + } - var r render.Row - row := makeGeneric().Rows[0] - assert.Nil(t, g.Render(&row, "blee", &r)) - - assert.Equal(t, "blee/a", r.ID) - assert.Equal(t, render.Fields{"a", "b", "c"}, r.Fields) + var re render.Generic + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + var r render.Row + re.SetTable(u.table) + assert.Nil(t, re.Render(&u.table.Rows[0], u.ns, &r)) + assert.Equal(t, u.eID, r.ID) + assert.Equal(t, u.eFields, r.Fields) + assert.Equal(t, u.eHeader, re.Header(u.ns)) + }) + } } +// ---------------------------------------------------------------------------- // Helpers... -func makeGeneric() *metav1beta1.Table { +func makeNSGeneric() *metav1beta1.Table { return &metav1beta1.Table{ ColumnDefinitions: []metav1beta1.TableColumnDefinition{ - {Name: "A"}, - {Name: "B"}, - {Name: "C"}, + {Name: "a"}, + {Name: "b"}, + {Name: "c"}, }, Rows: []metav1beta1.TableRow{ { @@ -36,14 +95,96 @@ func makeGeneric() *metav1beta1.Table { "kind": "fred", "apiVersion": "v1", "metadata": { - "namespace": "blee", + "namespace": "ns1", "name": "fred" }}`), }, Cells: []interface{}{ - "a", - "b", - "c", + "c1", + "c2", + "c3", + }, + }, + }, + } +} + +func makeAllNSGeneric() *metav1beta1.Table { + return &metav1beta1.Table{ + ColumnDefinitions: []metav1beta1.TableColumnDefinition{ + {Name: "a"}, + {Name: "b"}, + {Name: "c"}, + }, + Rows: []metav1beta1.TableRow{ + { + Object: runtime.RawExtension{ + Raw: []byte(`{ + "kind": "fred", + "apiVersion": "v1", + "metadata": { + "namespace": "ns1", + "name": "fred" + }}`), + }, + Cells: []interface{}{ + "c1", + "c2", + "c3", + }, + }, + }, + } +} + +func makeClusterGeneric() *metav1beta1.Table { + return &metav1beta1.Table{ + ColumnDefinitions: []metav1beta1.TableColumnDefinition{ + {Name: "a"}, + {Name: "b"}, + {Name: "c"}, + }, + Rows: []metav1beta1.TableRow{ + { + Object: runtime.RawExtension{ + Raw: []byte(`{ + "kind": "fred", + "apiVersion": "v1", + "metadata": { + "name": "fred" + }}`), + }, + Cells: []interface{}{ + "c1", + "c2", + "c3", + }, + }, + }, + } +} + +func makeAgeGeneric() *metav1beta1.Table { + return &metav1beta1.Table{ + ColumnDefinitions: []metav1beta1.TableColumnDefinition{ + {Name: "a"}, + {Name: "Age"}, + {Name: "c"}, + }, + Rows: []metav1beta1.TableRow{ + { + Object: runtime.RawExtension{ + Raw: []byte(`{ + "kind": "fred", + "apiVersion": "v1", + "metadata": { + "name": "fred" + }}`), + }, + Cells: []interface{}{ + "c1", + "Age", + "c2", }, }, }, diff --git a/internal/ui/select_table.go b/internal/ui/select_table.go index 1bf4e6d1..a56b4d2c 100644 --- a/internal/ui/select_table.go +++ b/internal/ui/select_table.go @@ -1,50 +1,10 @@ package ui import ( - "context" - "time" - - "github.com/derailed/k9s/internal/model" - "github.com/derailed/k9s/internal/render" "github.com/derailed/tview" "github.com/gdamore/tcell" ) -// Namespaceable represents a namespaceable model. -type Namespaceable interface { - // ClusterWide returns true if the model represents resource in all namespaces. - ClusterWide() bool - - // GetNamespace returns the model namespace. - GetNamespace() string - - // SetNamespace changes the model namespace. - SetNamespace(string) - - // InNamespace check if current namespace matches models. - InNamespace(string) bool -} - -// Tabular represents a tabular model. -type Tabular interface { - Namespaceable - - // Empty returns true if model has no data. - Empty() bool - - // Peek returns current model data. - Peek() render.TableData - - // Watch watches a given resource for changes. - Watch(context.Context) - - // SetRefreshRate sets the model watch loop rate. - SetRefreshRate(time.Duration) - - // AddListener registers a model listener. - AddListener(model.TableListener) -} - // SelectTable represents a table with selections. type SelectTable struct { *tview.Table diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index 5ee46d68..6258c59a 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -11,6 +11,7 @@ import ( "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/runtime" ) func TestTableNew(t *testing.T) { @@ -66,8 +67,11 @@ func (t *testModel) GetNamespace() string { return "blee" } func (t *testModel) SetNamespace(string) {} func (t *testModel) AddListener(model.TableListener) {} func (t *testModel) Watch(context.Context) {} -func (t *testModel) InNamespace(string) bool { return true } -func (t *testModel) SetRefreshRate(time.Duration) {} +func (t *testModel) Get(ctx context.Context, path string) (runtime.Object, error) { + return nil, nil +} +func (t *testModel) InNamespace(string) bool { return true } +func (t *testModel) SetRefreshRate(time.Duration) {} func makeTableData() render.TableData { t := render.NewTableData() diff --git a/internal/ui/types.go b/internal/ui/types.go index 1026cb67..add25e57 100644 --- a/internal/ui/types.go +++ b/internal/ui/types.go @@ -1,6 +1,13 @@ package ui -import "github.com/derailed/k9s/internal/render" +import ( + "context" + "time" + + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" + "k8s.io/apimachinery/pkg/runtime" +) type ( // SortFn represent a function that can sort columnar data. @@ -13,3 +20,41 @@ type ( asc bool } ) + +// Namespaceable represents a namespaceable model. +type Namespaceable interface { + // ClusterWide returns true if the model represents resource in all namespaces. + ClusterWide() bool + + // GetNamespace returns the model namespace. + GetNamespace() string + + // SetNamespace changes the model namespace. + SetNamespace(string) + + // InNamespace check if current namespace matches models. + InNamespace(string) bool +} + +// Tabular represents a tabular model. +type Tabular interface { + Namespaceable + + // Empty returns true if model has no data. + Empty() bool + + // Peek returns current model data. + Peek() render.TableData + + // Watch watches a given resource for changes. + Watch(context.Context) + + // SetRefreshRate sets the model watch loop rate. + SetRefreshRate(time.Duration) + + // AddListener registers a model listener. + AddListener(model.TableListener) + + // Get returns a resource instance. + Get(ctx context.Context, path string) (runtime.Object, error) +} diff --git a/internal/view/alias_test.go b/internal/view/alias_test.go index 31e64c15..0b72cc91 100644 --- a/internal/view/alias_test.go +++ b/internal/view/alias_test.go @@ -15,6 +15,7 @@ import ( "github.com/gdamore/tcell" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" ) func TestAliasNew(t *testing.T) { @@ -105,8 +106,11 @@ func (t *testModel) GetNamespace() string { return "blee" } func (t *testModel) SetNamespace(string) {} func (t *testModel) AddListener(model.TableListener) {} func (t *testModel) Watch(context.Context) {} -func (t *testModel) InNamespace(string) bool { return true } -func (t *testModel) SetRefreshRate(time.Duration) {} +func (t *testModel) Get(ctx context.Context, path string) (runtime.Object, error) { + return nil, nil +} +func (t *testModel) InNamespace(string) bool { return true } +func (t *testModel) SetRefreshRate(time.Duration) {} func makeTableData() render.TableData { return render.TableData{ diff --git a/internal/view/browser.go b/internal/view/browser.go index 6cbef436..3556c3f3 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -151,6 +151,33 @@ func (b *Browser) TableLoadFailed(err error) { // ---------------------------------------------------------------------------- // Actions... +func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey { + path := b.GetSelectedItem() + if path == "" { + return evt + } + + ctx := b.defaultContext() + o, err := b.GetModel().Get(ctx, path) + if err != nil { + b.App().Flash().Errf("unable to get resource %q -- %s", b.gvr, err) + return nil + } + + raw, err := toYAML(o) + if err != nil { + b.App().Flash().Errf("unable to marshal resource %s", err) + return nil + } + + details := NewDetails(b.app, "YAML", path).Update(raw) + if err := b.App().inject(details); err != nil { + b.App().Flash().Err(err) + } + + return nil +} + func (b *Browser) resetCmd(evt *tcell.EventKey) *tcell.EventKey { if !b.SearchBuff().InCmdMode() { b.SearchBuff().Reset() diff --git a/internal/view/table.go b/internal/view/table.go index dc320d65..8a8c13f2 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -10,7 +10,6 @@ import ( "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" - "k8s.io/apimachinery/pkg/labels" ) // Table represents a table viewer. @@ -128,32 +127,6 @@ func (t *Table) bindKeys() { }) } -func (t *Table) viewCmd(evt *tcell.EventKey) *tcell.EventKey { - path := t.GetSelectedItem() - if path == "" { - return evt - } - - o, err := t.app.factory.Get(t.GVR(), path, true, labels.Everything()) - if err != nil { - t.app.Flash().Errf("Unable to get resource %q -- %s", t.gvr, err) - return nil - } - - raw, err := toYAML(o) - if err != nil { - t.app.Flash().Errf("Unable to marshal resource %s", err) - return nil - } - - details := NewDetails(t.app, "YAML", path).Update(raw) - if err := t.app.inject(details); err != nil { - t.App().Flash().Err(err) - } - - return nil -} - func (t *Table) cpCmd(evt *tcell.EventKey) *tcell.EventKey { path := t.GetSelectedItem() if path == "" { diff --git a/internal/view/table_int_test.go b/internal/view/table_int_test.go index da645171..4991645c 100644 --- a/internal/view/table_int_test.go +++ b/internal/view/table_int_test.go @@ -16,6 +16,7 @@ import ( "github.com/derailed/tview" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" ) func TestTableSave(t *testing.T) { @@ -97,8 +98,11 @@ func (t *testTableModel) GetNamespace() string { return "blee" } func (t *testTableModel) SetNamespace(string) {} func (t *testTableModel) AddListener(model.TableListener) {} func (t *testTableModel) Watch(context.Context) {} -func (t *testTableModel) InNamespace(string) bool { return true } -func (t *testTableModel) SetRefreshRate(time.Duration) {} +func (t *testTableModel) Get(ctx context.Context, path string) (runtime.Object, error) { + return nil, nil +} +func (t *testTableModel) InNamespace(string) bool { return true } +func (t *testTableModel) SetRefreshRate(time.Duration) {} func makeTableData() render.TableData { t := render.NewTableData() diff --git a/internal/watch/factory.go b/internal/watch/factory.go index fb4be077..ba382c44 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -62,35 +62,36 @@ func (f *Factory) Terminate() { // List returns a resource collection. func (f *Factory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) { + if ns == clusterScope { + ns = allNamespaces + } log.Debug().Msgf("List %q:%q", ns, gvr) inf, err := f.CanForResource(ns, gvr, []string{"list", "watch"}) if err != nil { return nil, err } - if ns == clusterScope { - ns = allNamespaces - } - if wait { f.waitForCacheSync(ns) } + return inf.Lister().ByNamespace(ns).List(sel) } // Get retrieves a given resource. func (f *Factory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) { ns, n := namespaced(path) + if ns == clusterScope { + ns = allNamespaces + } + log.Debug().Msgf("GET %q:%q::%q", ns, gvr, n) inf, err := f.CanForResource(ns, gvr, []string{"get"}) if err != nil { return nil, err } - if ns == clusterScope { - ns = allNamespaces - } - if wait { f.waitForCacheSync(ns) } + return inf.Lister().ByNamespace(ns).Get(n) } diff --git a/internal/watch/helper.go b/internal/watch/helper.go index 59254fc9..abd988d1 100644 --- a/internal/watch/helper.go +++ b/internal/watch/helper.go @@ -39,7 +39,11 @@ func Dump(f *Factory) { // Debug for debug. func Debug(f *Factory, ns string, gvr string) { log.Debug().Msgf("----------- DEBUG FACTORY (%s) -------------", gvr) - inf := f.factories[ns].ForResource(toGVR(gvr)) + fac, ok := f.factories[ns] + if !ok { + return + } + inf := fac.ForResource(toGVR(gvr)) for i, k := range inf.Informer().GetStore().ListKeys() { log.Debug().Msgf("%d -- %s", i, k) }