diff --git a/internal/dao/registry.go b/internal/dao/registry.go index 6ebaf4e0..0728d941 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -135,7 +135,7 @@ func (m *Meta) LoadResources(f Factory) error { return nil } -// BOZO!! Need contermeasure for direct commands! +// BOZO!! Need countermeasures for direct commands! func loadNonResource(m ResourceMetas) { loadK9s(m) loadRBAC(m) diff --git a/internal/model/describe.go b/internal/model/describe.go index a500da06..69a00eb6 100644 --- a/internal/model/describe.go +++ b/internal/model/describe.go @@ -26,7 +26,7 @@ type ResourceViewer interface { Filter(string) ClearFilter() Peek() []string - Watch(context.Context) + Watch(context.Context) error AddListener(ResourceViewerListener) RemoveListener(ResourceViewerListener) } @@ -117,9 +117,12 @@ func (d *Describe) Peek() []string { } // Watch watches for describe data changes. -func (d *Describe) Watch(ctx context.Context) { - d.refresh(ctx) +func (d *Describe) Watch(ctx context.Context) error { + if err := d.refresh(ctx); err != nil { + return err + } go d.updater(ctx) + return nil } func (d *Describe) updater(ctx context.Context) { diff --git a/internal/model/table.go b/internal/model/table.go index f496afa3..7aed9b2f 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -7,6 +7,7 @@ import ( "sync/atomic" "time" + backoff "github.com/cenkalti/backoff/v4" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" @@ -84,14 +85,18 @@ func (t *Table) RemoveListener(l TableListener) { } // Watch initiates model updates. -func (t *Table) Watch(ctx context.Context) { - t.refresh(ctx) +func (t *Table) Watch(ctx context.Context) error { + if err := t.refresh(ctx); err != nil { + return err + } go t.updater(ctx) + + return nil } // Refresh updates the table content. -func (t *Table) Refresh(ctx context.Context) { - t.refresh(ctx) +func (t *Table) Refresh(ctx context.Context) error { + return t.refresh(ctx) } // Get returns a resource instance if found, else an error. @@ -119,36 +124,6 @@ func (t *Table) Delete(ctx context.Context, path string, cascade, force bool) er return nuker.Delete(path, cascade, force) } -// Describe describes a given resource. -func (t *Table) Describe(ctx context.Context, path string) (string, error) { - meta, err := getMeta(ctx, t.gvr) - if err != nil { - return "", err - } - - desc, ok := meta.DAO.(dao.Describer) - if !ok { - return "", fmt.Errorf("no describer for %q", meta.DAO.GVR()) - } - - return desc.Describe(path) -} - -// ToYAML returns a resource yaml. -func (t *Table) ToYAML(ctx context.Context, path string) (string, error) { - meta, err := getMeta(ctx, t.gvr) - if err != nil { - return "", err - } - - desc, ok := meta.DAO.(dao.Describer) - if !ok { - return "", fmt.Errorf("no describer for %q", meta.DAO.GVR()) - } - - return desc.ToYAML(path, false) -} - // GetNamespace returns the model namespace. func (t *Table) GetNamespace() string { return t.namespace @@ -191,6 +166,8 @@ func (t *Table) Peek() render.TableData { func (t *Table) updater(ctx context.Context) { defer log.Debug().Msgf("TABLE-MODEL canceled -- %q", t.gvr) + bf := backoff.NewExponentialBackOff() + bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxRetryInterval rate := initRefreshRate for { select { @@ -198,24 +175,31 @@ func (t *Table) updater(ctx context.Context) { return case <-time.After(rate): rate = t.refreshRate - t.refresh(ctx) + err := backoff.Retry(func() error { + return t.refresh(ctx) + }, backoff.WithContext(bf, ctx)) + if err != nil { + log.Error().Err(err).Msgf("Retry failed") + t.fireTableLoadFailed(err) + return + } } } } -func (t *Table) refresh(ctx context.Context) { +func (t *Table) refresh(ctx context.Context) error { if !atomic.CompareAndSwapInt32(&t.inUpdate, 0, 1) { log.Debug().Msgf("Dropping update...") - return + return nil } defer atomic.StoreInt32(&t.inUpdate, 0) if err := t.reconcile(ctx); err != nil { - log.Error().Err(err).Msgf("reconcile failed %q::%q", t.gvr, t.instance) - t.fireTableLoadFailed(err) - return + return err } t.fireTableChanged(t.Peek()) + + return nil } func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, error) { diff --git a/internal/model/yaml.go b/internal/model/yaml.go index 40d1a3b3..f4808537 100644 --- a/internal/model/yaml.go +++ b/internal/model/yaml.go @@ -108,12 +108,13 @@ func (y *YAML) Peek() []string { } // Watch watches for YAML changes. -func (y *YAML) Watch(ctx context.Context) { +func (y *YAML) Watch(ctx context.Context) error { if err := y.refresh(ctx); err != nil { - log.Error().Err(err).Msgf("YAML Refresh failed") - return + return err } go y.updater(ctx) + + return nil } func (y *YAML) updater(ctx context.Context) { @@ -148,8 +149,6 @@ func (y *YAML) refresh(ctx context.Context) error { defer atomic.StoreInt32(&y.inUpdate, 0) if err := y.reconcile(ctx); err != nil { - log.Error().Err(err).Msgf("reconcile failed %q", y.gvr) - y.fireResourceFailed(err) return err } diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index 0fa8a387..3b902c1b 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -66,14 +66,14 @@ func (t *mockModel) SetLabelFilter(string) {} func (t *mockModel) Empty() bool { return false } func (t *mockModel) HasMetrics() bool { return true } func (t *mockModel) Peek() render.TableData { return makeTableData() } -func (t *mockModel) Refresh(context.Context) {} +func (t *mockModel) Refresh(context.Context) error { return nil } func (t *mockModel) ClusterWide() bool { return false } func (t *mockModel) GetNamespace() string { return "blee" } func (t *mockModel) SetNamespace(string) {} func (t *mockModel) ToggleToast() {} func (t *mockModel) AddListener(model.TableListener) {} func (t *mockModel) RemoveListener(model.TableListener) {} -func (t *mockModel) Watch(context.Context) {} +func (t *mockModel) Watch(context.Context) error { return nil } func (t *mockModel) Get(ctx context.Context, path string) (runtime.Object, error) { return nil, nil } diff --git a/internal/ui/types.go b/internal/ui/types.go index 76b8bc2c..578cb00c 100644 --- a/internal/ui/types.go +++ b/internal/ui/types.go @@ -39,12 +39,6 @@ type Namespaceable interface { type Lister interface { // Get returns a resource instance. Get(ctx context.Context, path string) (runtime.Object, error) - - // ToYAML returns a resource yaml representation. - ToYAML(ctx context.Context, path string) (string, error) - - // Describes describes a given resource. - Describe(ctx context.Context, path string) (string, error) } // Tabular represents a tabular model. @@ -65,10 +59,10 @@ type Tabular interface { Peek() render.TableData // Watch watches a given resource for changes. - Watch(context.Context) + Watch(context.Context) error // Refresh forces a new refresh. - Refresh(context.Context) + Refresh(context.Context) error // SetRefreshRate sets the model watch loop rate. SetRefreshRate(time.Duration) diff --git a/internal/view/alias_test.go b/internal/view/alias_test.go index f3aee749..3e25cd1f 100644 --- a/internal/view/alias_test.go +++ b/internal/view/alias_test.go @@ -116,8 +116,8 @@ func (t *mockModel) SetNamespace(string) {} func (t *mockModel) ToggleToast() {} func (t *mockModel) AddListener(model.TableListener) {} func (t *mockModel) RemoveListener(model.TableListener) {} -func (t *mockModel) Watch(context.Context) {} -func (t *mockModel) Refresh(context.Context) {} +func (t *mockModel) Watch(context.Context) error { return nil } +func (t *mockModel) Refresh(context.Context) error { return nil } func (t *mockModel) Get(context.Context, string) (runtime.Object, error) { return nil, nil diff --git a/internal/view/table_int_test.go b/internal/view/table_int_test.go index 3c68831a..14125846 100644 --- a/internal/view/table_int_test.go +++ b/internal/view/table_int_test.go @@ -97,14 +97,14 @@ func (t *mockTableModel) SetLabelFilter(string) {} func (t *mockTableModel) Empty() bool { return false } func (t *mockTableModel) HasMetrics() bool { return true } func (t *mockTableModel) Peek() render.TableData { return makeTableData() } -func (t *mockTableModel) Refresh(context.Context) {} +func (t *mockTableModel) Refresh(context.Context) error { return nil } func (t *mockTableModel) ClusterWide() bool { return false } func (t *mockTableModel) GetNamespace() string { return "blee" } func (t *mockTableModel) SetNamespace(string) {} func (t *mockTableModel) ToggleToast() {} func (t *mockTableModel) AddListener(model.TableListener) {} func (t *mockTableModel) RemoveListener(model.TableListener) {} -func (t *mockTableModel) Watch(context.Context) {} +func (t *mockTableModel) Watch(context.Context) error { return nil } func (t *mockTableModel) Get(context.Context, string) (runtime.Object, error) { return nil, nil }