diff --git a/model/plugin.go b/model/plugin.go index f4bad678..18d66e30 100644 --- a/model/plugin.go +++ b/model/plugin.go @@ -23,6 +23,7 @@ type Plugins []Plugin type PluginRepository interface { ResourceRepository + ClearErrors() error CountAll(options ...QueryOptions) (int64, error) Delete(id string) error Get(id string) (*Plugin, error) diff --git a/persistence/plugin_repository.go b/persistence/plugin_repository.go index 466abb40..35c32de9 100644 --- a/persistence/plugin_repository.go +++ b/persistence/plugin_repository.go @@ -31,6 +31,14 @@ func (r *pluginRepository) isPermitted() bool { return user.IsAdmin } +func (r *pluginRepository) ClearErrors() error { + if !r.isPermitted() { + return rest.ErrPermissionDenied + } + _, err := r.db.NewQuery("UPDATE plugin SET last_error = '' WHERE last_error != ''").Execute() + return err +} + func (r *pluginRepository) CountAll(options ...model.QueryOptions) (int64, error) { if !r.isPermitted() { return 0, rest.ErrPermissionDenied diff --git a/persistence/plugin_repository_test.go b/persistence/plugin_repository_test.go index ee158a31..dc68b089 100644 --- a/persistence/plugin_repository_test.go +++ b/persistence/plugin_repository_test.go @@ -175,6 +175,30 @@ var _ = Describe("PluginRepository", func() { Expect(err.Error()).To(ContainSubstring("ID cannot be empty")) }) }) + + Describe("ClearErrors", func() { + It("clears last_error on all plugins with errors", func() { + _ = repo.Put(&model.Plugin{ID: "ok-plugin", Path: "/plugins/ok.wasm", Manifest: "{}", SHA256: "h1"}) + _ = repo.Put(&model.Plugin{ID: "err-plugin-1", Path: "/plugins/e1.wasm", Manifest: "{}", SHA256: "h2", LastError: "incompatible version"}) + _ = repo.Put(&model.Plugin{ID: "err-plugin-2", Path: "/plugins/e2.wasm", Manifest: "{}", SHA256: "h3", LastError: "missing export"}) + + err := repo.ClearErrors() + Expect(err).To(BeNil()) + + all, err := repo.GetAll() + Expect(err).To(BeNil()) + for _, p := range all { + Expect(p.LastError).To(BeEmpty(), "plugin %s should have no error", p.ID) + } + }) + + It("succeeds when no plugins have errors", func() { + _ = repo.Put(&model.Plugin{ID: "clean-plugin", Path: "/plugins/c.wasm", Manifest: "{}", SHA256: "h1"}) + + err := repo.ClearErrors() + Expect(err).To(BeNil()) + }) + }) }) Describe("Regular User", func() { diff --git a/plugins/manager.go b/plugins/manager.go index f148a706..bf6bab6e 100644 --- a/plugins/manager.go +++ b/plugins/manager.go @@ -146,6 +146,12 @@ func (m *Manager) Start(ctx context.Context) error { log.Info(ctx, "Starting plugin manager", "folder", folder) + // Clear previous error states so plugins can be retried on restart + adminCtx := adminContext(ctx) + if err := m.ds.Plugin(adminCtx).ClearErrors(); err != nil { + log.Error(ctx, "Error clearing plugin errors", err) + } + // Sync plugins folder with DB if err := m.syncPlugins(ctx, folder); err != nil { log.Error(ctx, "Error syncing plugins with DB", err) diff --git a/tests/mock_plugin_repo.go b/tests/mock_plugin_repo.go index dd08f6de..5d22c26a 100644 --- a/tests/mock_plugin_repo.go +++ b/tests/mock_plugin_repo.go @@ -29,6 +29,20 @@ func (m *MockPluginRepo) SetError(err bool) { m.Err = err } +func (m *MockPluginRepo) ClearErrors() error { + if m.Err { + return errors.New("unexpected error") + } + for i := range m.All { + m.All[i].LastError = "" + } + for k, p := range m.Data { + p.LastError = "" + m.Data[k] = p + } + return nil +} + func (m *MockPluginRepo) SetData(plugins model.Plugins) { m.Data = make(map[string]*model.Plugin, len(plugins)) m.All = plugins