diff --git a/.changeset/good-chairs-boil.md b/.changeset/good-chairs-boil.md new file mode 100644 index 00000000000..ead73f68103 --- /dev/null +++ b/.changeset/good-chairs-boil.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#added Enable and Disable Feeds Manager mutations diff --git a/core/logger/audit/audit_types.go b/core/logger/audit/audit_types.go index 8ce2017f107..c12ed574d5a 100644 --- a/core/logger/audit/audit_types.go +++ b/core/logger/audit/audit_types.go @@ -23,6 +23,9 @@ const ( FeedsManCreated EventID = "FEEDS_MAN_CREATED" FeedsManUpdated EventID = "FEEDS_MAN_UPDATED" + FeedsManEnabled EventID = "FEEDS_MAN_ENABLED" + FeedsManDisabled EventID = "FEEDS_MAN_DISABLED" + FeedsManChainConfigCreated EventID = "FEEDS_MAN_CHAIN_CONFIG_CREATED" FeedsManChainConfigUpdated EventID = "FEEDS_MAN_CHAIN_CONFIG_UPDATED" FeedsManChainConfigDeleted EventID = "FEEDS_MAN_CHAIN_CONFIG_DELETED" diff --git a/core/services/feeds/mocks/orm.go b/core/services/feeds/mocks/orm.go index d6cae81dc6c..8fde65e1870 100644 --- a/core/services/feeds/mocks/orm.go +++ b/core/services/feeds/mocks/orm.go @@ -684,6 +684,124 @@ func (_c *ORM_DeleteProposal_Call) RunAndReturn(run func(context.Context, int64) return _c } +// DisableManager provides a mock function with given fields: ctx, id +func (_m *ORM) DisableManager(ctx context.Context, id int64) (*feeds.FeedsManager, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for DisableManager") + } + + var r0 *feeds.FeedsManager + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (*feeds.FeedsManager, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) *feeds.FeedsManager); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*feeds.FeedsManager) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ORM_DisableManager_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisableManager' +type ORM_DisableManager_Call struct { + *mock.Call +} + +// DisableManager is a helper method to define mock.On call +// - ctx context.Context +// - id int64 +func (_e *ORM_Expecter) DisableManager(ctx interface{}, id interface{}) *ORM_DisableManager_Call { + return &ORM_DisableManager_Call{Call: _e.mock.On("DisableManager", ctx, id)} +} + +func (_c *ORM_DisableManager_Call) Run(run func(ctx context.Context, id int64)) *ORM_DisableManager_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *ORM_DisableManager_Call) Return(_a0 *feeds.FeedsManager, _a1 error) *ORM_DisableManager_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ORM_DisableManager_Call) RunAndReturn(run func(context.Context, int64) (*feeds.FeedsManager, error)) *ORM_DisableManager_Call { + _c.Call.Return(run) + return _c +} + +// EnableManager provides a mock function with given fields: ctx, id +func (_m *ORM) EnableManager(ctx context.Context, id int64) (*feeds.FeedsManager, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for EnableManager") + } + + var r0 *feeds.FeedsManager + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (*feeds.FeedsManager, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) *feeds.FeedsManager); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*feeds.FeedsManager) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ORM_EnableManager_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnableManager' +type ORM_EnableManager_Call struct { + *mock.Call +} + +// EnableManager is a helper method to define mock.On call +// - ctx context.Context +// - id int64 +func (_e *ORM_Expecter) EnableManager(ctx interface{}, id interface{}) *ORM_EnableManager_Call { + return &ORM_EnableManager_Call{Call: _e.mock.On("EnableManager", ctx, id)} +} + +func (_c *ORM_EnableManager_Call) Run(run func(ctx context.Context, id int64)) *ORM_EnableManager_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *ORM_EnableManager_Call) Return(_a0 *feeds.FeedsManager, _a1 error) *ORM_EnableManager_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ORM_EnableManager_Call) RunAndReturn(run func(context.Context, int64) (*feeds.FeedsManager, error)) *ORM_EnableManager_Call { + _c.Call.Return(run) + return _c +} + // ExistsSpecByJobProposalIDAndVersion provides a mock function with given fields: ctx, jpID, version func (_m *ORM) ExistsSpecByJobProposalIDAndVersion(ctx context.Context, jpID int64, version int32) (bool, error) { ret := _m.Called(ctx, jpID, version) diff --git a/core/services/feeds/mocks/service.go b/core/services/feeds/mocks/service.go index d84879bb700..b4c184f6be2 100644 --- a/core/services/feeds/mocks/service.go +++ b/core/services/feeds/mocks/service.go @@ -391,6 +391,124 @@ func (_c *Service_DeleteJob_Call) RunAndReturn(run func(context.Context, *feeds. return _c } +// DisableManager provides a mock function with given fields: ctx, id +func (_m *Service) DisableManager(ctx context.Context, id int64) (*feeds.FeedsManager, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for DisableManager") + } + + var r0 *feeds.FeedsManager + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (*feeds.FeedsManager, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) *feeds.FeedsManager); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*feeds.FeedsManager) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Service_DisableManager_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisableManager' +type Service_DisableManager_Call struct { + *mock.Call +} + +// DisableManager is a helper method to define mock.On call +// - ctx context.Context +// - id int64 +func (_e *Service_Expecter) DisableManager(ctx interface{}, id interface{}) *Service_DisableManager_Call { + return &Service_DisableManager_Call{Call: _e.mock.On("DisableManager", ctx, id)} +} + +func (_c *Service_DisableManager_Call) Run(run func(ctx context.Context, id int64)) *Service_DisableManager_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *Service_DisableManager_Call) Return(_a0 *feeds.FeedsManager, _a1 error) *Service_DisableManager_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Service_DisableManager_Call) RunAndReturn(run func(context.Context, int64) (*feeds.FeedsManager, error)) *Service_DisableManager_Call { + _c.Call.Return(run) + return _c +} + +// EnableManager provides a mock function with given fields: ctx, id +func (_m *Service) EnableManager(ctx context.Context, id int64) (*feeds.FeedsManager, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for EnableManager") + } + + var r0 *feeds.FeedsManager + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (*feeds.FeedsManager, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) *feeds.FeedsManager); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*feeds.FeedsManager) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Service_EnableManager_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnableManager' +type Service_EnableManager_Call struct { + *mock.Call +} + +// EnableManager is a helper method to define mock.On call +// - ctx context.Context +// - id int64 +func (_e *Service_Expecter) EnableManager(ctx interface{}, id interface{}) *Service_EnableManager_Call { + return &Service_EnableManager_Call{Call: _e.mock.On("EnableManager", ctx, id)} +} + +func (_c *Service_EnableManager_Call) Run(run func(ctx context.Context, id int64)) *Service_EnableManager_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *Service_EnableManager_Call) Return(_a0 *feeds.FeedsManager, _a1 error) *Service_EnableManager_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Service_EnableManager_Call) RunAndReturn(run func(context.Context, int64) (*feeds.FeedsManager, error)) *Service_EnableManager_Call { + _c.Call.Return(run) + return _c +} + // GetChainConfig provides a mock function with given fields: ctx, id func (_m *Service) GetChainConfig(ctx context.Context, id int64) (*feeds.ChainConfig, error) { ret := _m.Called(ctx, id) diff --git a/core/services/feeds/models.go b/core/services/feeds/models.go index a4fbe8745f0..93dddd86dae 100644 --- a/core/services/feeds/models.go +++ b/core/services/feeds/models.go @@ -118,6 +118,7 @@ type FeedsManager struct { IsConnectionActive bool CreatedAt time.Time UpdatedAt time.Time + DisabledAt *time.Time } // ChainConfig defines the chain configuration for a Feeds Manager. diff --git a/core/services/feeds/orm.go b/core/services/feeds/orm.go index 82d1114a23d..e5ce57456b1 100644 --- a/core/services/feeds/orm.go +++ b/core/services/feeds/orm.go @@ -23,6 +23,8 @@ type ORM interface { ListManagers(ctx context.Context) (mgrs []FeedsManager, err error) ListManagersByIDs(ctx context.Context, ids []int64) ([]FeedsManager, error) UpdateManager(ctx context.Context, mgr FeedsManager) error + EnableManager(ctx context.Context, id int64) (*FeedsManager, error) + DisableManager(ctx context.Context, id int64) (*FeedsManager, error) CreateBatchChainConfig(ctx context.Context, cfgs []ChainConfig) ([]int64, error) CreateChainConfig(ctx context.Context, cfg ChainConfig) (int64, error) @@ -268,7 +270,7 @@ RETURNING id; // GetManager gets a feeds manager by id. func (o *orm) GetManager(ctx context.Context, id int64) (mgr *FeedsManager, err error) { stmt := ` -SELECT id, name, uri, public_key, created_at, updated_at +SELECT id, name, uri, public_key, created_at, updated_at, disabled_at FROM feeds_managers WHERE id = $1 ` @@ -281,7 +283,7 @@ WHERE id = $1 // ListManager lists all feeds managers. func (o *orm) ListManagers(ctx context.Context) (mgrs []FeedsManager, err error) { stmt := ` -SELECT id, name, uri, public_key, created_at, updated_at +SELECT id, name, uri, public_key, created_at, updated_at, disabled_at FROM feeds_managers ORDER BY created_at; ` @@ -293,7 +295,7 @@ ORDER BY created_at; // ListManagersByIDs gets feeds managers by ids. func (o *orm) ListManagersByIDs(ctx context.Context, ids []int64) (managers []FeedsManager, err error) { stmt := ` -SELECT id, name, uri, public_key, created_at, updated_at +SELECT id, name, uri, public_key, created_at, updated_at, disabled_at FROM feeds_managers WHERE id = ANY($1) ORDER BY created_at, id;` @@ -326,6 +328,36 @@ WHERE id = $4; return nil } +func (o *orm) EnableManager(ctx context.Context, id int64) (*FeedsManager, error) { + stmt := ` + UPDATE feeds_managers + SET disabled_at = NULL + WHERE id = $1 + RETURNING *; +` + mgr := new(FeedsManager) + err := o.ds.GetContext(ctx, mgr, stmt, id) + if err != nil { + return nil, errors.Wrap(err, "EnableManager failed") + } + return mgr, nil +} + +func (o *orm) DisableManager(ctx context.Context, id int64) (*FeedsManager, error) { + stmt := ` + UPDATE feeds_managers + SET disabled_at = NOW() + WHERE id = $1 + RETURNING *; +` + mgr := new(FeedsManager) + err := o.ds.GetContext(ctx, mgr, stmt, id) + if err != nil { + return nil, errors.Wrap(err, "DisableManager failed") + } + return mgr, nil +} + // CreateJobProposal creates a job proposal. func (o *orm) CreateJobProposal(ctx context.Context, jp *JobProposal) (id int64, err error) { stmt := ` diff --git a/core/services/feeds/orm_test.go b/core/services/feeds/orm_test.go index cfe6cef37b9..30e738ff89e 100644 --- a/core/services/feeds/orm_test.go +++ b/core/services/feeds/orm_test.go @@ -129,6 +129,7 @@ func Test_ORM_GetManager(t *testing.T) { assert.Equal(t, uri, actual.URI) assert.Equal(t, name, actual.Name) assert.Equal(t, publicKey, actual.PublicKey) + assert.Nil(t, actual.DisabledAt) _, err = orm.GetManager(ctx, -1) require.Error(t, err) @@ -159,6 +160,7 @@ func Test_ORM_ListManagers(t *testing.T) { assert.Equal(t, uri, actual.URI) assert.Equal(t, name, actual.Name) assert.Equal(t, publicKey, actual.PublicKey) + assert.Nil(t, actual.DisabledAt) } func Test_ORM_ListManagersByIDs(t *testing.T) { @@ -186,6 +188,7 @@ func Test_ORM_ListManagersByIDs(t *testing.T) { assert.Equal(t, uri, actual.URI) assert.Equal(t, name, actual.Name) assert.Equal(t, publicKey, actual.PublicKey) + assert.Nil(t, actual.DisabledAt) } func Test_ORM_UpdateManager(t *testing.T) { @@ -222,6 +225,34 @@ func Test_ORM_UpdateManager(t *testing.T) { assert.Equal(t, updatedMgr.PublicKey, actual.PublicKey) } +func Test_ORM_EnableAndDisableManager(t *testing.T) { + t.Parallel() + ctx := testutils.Context(t) + + var ( + orm = setupORM(t) + mgr = &feeds.FeedsManager{ + URI: uri, + Name: name, + PublicKey: publicKey, + } + ) + id, err := orm.CreateManager(ctx, mgr) + require.NoError(t, err) + + mgr, err = orm.GetManager(ctx, id) + require.NoError(t, err) + require.Nil(t, mgr.DisabledAt) + + mgr, err = orm.DisableManager(ctx, id) + require.NoError(t, err) + require.NotNil(t, mgr.DisabledAt) + + mgr, err = orm.EnableManager(ctx, id) + require.NoError(t, err) + require.Nil(t, mgr.DisabledAt) +} + // Chain Config func Test_ORM_CreateChainConfig(t *testing.T) { diff --git a/core/services/feeds/service.go b/core/services/feeds/service.go index 7479f10abb5..ad02658ec9d 100644 --- a/core/services/feeds/service.go +++ b/core/services/feeds/service.go @@ -86,6 +86,8 @@ type Service interface { ListManagersByIDs(ctx context.Context, ids []int64) ([]FeedsManager, error) RegisterManager(ctx context.Context, params RegisterManagerParams) (int64, error) UpdateManager(ctx context.Context, mgr FeedsManager) error + EnableManager(ctx context.Context, id int64) (*FeedsManager, error) + DisableManager(ctx context.Context, id int64) (*FeedsManager, error) CreateChainConfig(ctx context.Context, cfg ChainConfig) (int64, error) DeleteChainConfig(ctx context.Context, id int64) (int64, error) @@ -298,6 +300,36 @@ func (s *service) UpdateManager(ctx context.Context, mgr FeedsManager) error { return nil } +func (s *service) EnableManager(ctx context.Context, id int64) (*FeedsManager, error) { + mgr, err := s.orm.EnableManager(ctx, id) + if err != nil || mgr == nil { + return nil, errors.Wrap(err, "could not enable manager") + } + + if err := s.restartConnection(ctx, *mgr); err != nil { + s.lggr.Errorf("could not restart FMS connection: %v", err) + } + + mgr.IsConnectionActive = s.connMgr.IsConnected(mgr.ID) + + return mgr, nil +} + +func (s *service) DisableManager(ctx context.Context, id int64) (*FeedsManager, error) { + mgr, err := s.orm.DisableManager(ctx, id) + if err != nil || mgr == nil { + return nil, errors.Wrap(err, "could not disable manager") + } + + if err := s.connMgr.Disconnect(mgr.ID); err != nil { + s.lggr.Info("Error disconnecting manager", "err", err) + } + + mgr.IsConnectionActive = s.connMgr.IsConnected(mgr.ID) + + return mgr, nil +} + // ListManagerServices lists all the manager services. func (s *service) ListManagers(ctx context.Context) ([]FeedsManager, error) { managers, err := s.orm.ListManagers(ctx) @@ -1049,7 +1081,9 @@ func (s *service) Start(ctx context.Context) error { if s.featCfg.MultiFeedsManagers() { s.lggr.Infof("starting connection to %d feeds managers", len(mgrs)) for _, mgr := range mgrs { - s.connectFeedManager(ctx, mgr, privkey) + if mgr.DisabledAt == nil { + s.connectFeedManager(ctx, mgr, privkey) + } } } else { s.connectFeedManager(ctx, mgrs[0], privkey) @@ -1530,6 +1564,12 @@ func (ns NullService) SyncNodeInfo(ctx context.Context, id int64) error { return func (ns NullService) UpdateManager(ctx context.Context, mgr FeedsManager) error { return ErrFeedsManagerDisabled } +func (ns NullService) EnableManager(ctx context.Context, id int64) (*FeedsManager, error) { + return nil, ErrFeedsManagerDisabled +} +func (ns NullService) DisableManager(ctx context.Context, id int64) (*FeedsManager, error) { + return nil, ErrFeedsManagerDisabled +} func (ns NullService) IsJobManaged(ctx context.Context, jobID int64) (bool, error) { return false, nil } diff --git a/core/services/feeds/service_test.go b/core/services/feeds/service_test.go index e7472a5ae54..115695d8514 100644 --- a/core/services/feeds/service_test.go +++ b/core/services/feeds/service_test.go @@ -463,6 +463,38 @@ func Test_Service_UpdateFeedsManager(t *testing.T) { require.NoError(t, err) } +func Test_Service_EnableFeedsManager(t *testing.T) { + key := cltest.DefaultCSAKey + + mgr := feeds.FeedsManager{ID: 1} + + svc := setupTestService(t) + + svc.orm.On("EnableManager", mock.Anything, mgr.ID).Return(&mgr, nil) + svc.connMgr.On("IsConnected", mgr.ID).Return(false) + svc.csaKeystore.On("GetAll").Return([]csakey.KeyV2{key}, nil) + svc.connMgr.On("Disconnect", mgr.ID).Return(nil) + svc.connMgr.On("Connect", mock.IsType(feeds.ConnectOpts{})).Return(nil) + + actual, err := svc.EnableManager(testutils.Context(t), 1) + require.NoError(t, err) + require.NotNil(t, actual) +} + +func Test_Service_DisableFeedsManager(t *testing.T) { + mgr := feeds.FeedsManager{ID: 1} + + svc := setupTestService(t) + + svc.orm.On("DisableManager", mock.Anything, mgr.ID).Return(&mgr, nil) + svc.connMgr.On("IsConnected", mgr.ID).Return(false) + svc.connMgr.On("Disconnect", mgr.ID).Return(nil) + + actual, err := svc.DisableManager(testutils.Context(t), 1) + require.NoError(t, err) + require.NotNil(t, actual) +} + func Test_Service_ListManagersByIDs(t *testing.T) { t.Parallel() ctx := testutils.Context(t) diff --git a/core/store/migrate/migrations/0258_add_disabled_at_column_to_feeds_manager.sql b/core/store/migrate/migrations/0258_add_disabled_at_column_to_feeds_manager.sql new file mode 100644 index 00000000000..7a7e4a15542 --- /dev/null +++ b/core/store/migrate/migrations/0258_add_disabled_at_column_to_feeds_manager.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE feeds_managers +ADD COLUMN disabled_at TIMESTAMP WITH TIME ZONE; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE feeds_managers +DROP COLUMN IF EXISTS disabled_at; +-- +goose StatementEnd diff --git a/core/web/resolver/feeds_manager.go b/core/web/resolver/feeds_manager.go index 0711cb1354b..520765d8636 100644 --- a/core/web/resolver/feeds_manager.go +++ b/core/web/resolver/feeds_manager.go @@ -77,6 +77,13 @@ func (r *FeedsManagerResolver) CreatedAt() graphql.Time { return graphql.Time{Time: r.mgr.CreatedAt} } +func (r *FeedsManagerResolver) DisabledAt() *graphql.Time { + if r.mgr.DisabledAt == nil { + return nil + } + return &graphql.Time{Time: *r.mgr.DisabledAt} +} + // -- FeedsManager Query -- type FeedsManagerPayloadResolver struct { @@ -283,3 +290,79 @@ func NewUpdateFeedsManagerSuccessResolver(mgr feeds.FeedsManager) *UpdateFeedsMa func (r *UpdateFeedsManagerSuccessResolver) FeedsManager() *FeedsManagerResolver { return NewFeedsManager(r.mgr) } + +// -- EnableFeedsManager Mutation -- + +type EnableFeedsManagerPayloadResolver struct { + mgr *feeds.FeedsManager + NotFoundErrorUnionType +} + +func NewEnableFeedsManagerPayload(mgr *feeds.FeedsManager, err error) *EnableFeedsManagerPayloadResolver { + e := NotFoundErrorUnionType{err: err, message: "feeds manager not found", isExpectedErrorFn: nil} + + return &EnableFeedsManagerPayloadResolver{ + mgr: mgr, + NotFoundErrorUnionType: e, + } +} + +func (r *EnableFeedsManagerPayloadResolver) ToEnableFeedsManagerSuccess() (*EnableFeedsManagerSuccessResolver, bool) { + if r.mgr != nil { + return NewEnableFeedsManagerSuccessResolver(*r.mgr), true + } + + return nil, false +} + +type EnableFeedsManagerSuccessResolver struct { + mgr feeds.FeedsManager +} + +func NewEnableFeedsManagerSuccessResolver(mgr feeds.FeedsManager) *EnableFeedsManagerSuccessResolver { + return &EnableFeedsManagerSuccessResolver{ + mgr: mgr, + } +} + +func (r *EnableFeedsManagerSuccessResolver) FeedsManager() *FeedsManagerResolver { + return NewFeedsManager(r.mgr) +} + +// -- DisableFeedsManager Mutation -- + +type DisableFeedsManagerPayloadResolver struct { + mgr *feeds.FeedsManager + NotFoundErrorUnionType +} + +func NewDisableFeedsManagerPayload(mgr *feeds.FeedsManager, err error) *DisableFeedsManagerPayloadResolver { + e := NotFoundErrorUnionType{err: err, message: "feeds manager not found", isExpectedErrorFn: nil} + + return &DisableFeedsManagerPayloadResolver{ + mgr: mgr, + NotFoundErrorUnionType: e, + } +} + +func (r *DisableFeedsManagerPayloadResolver) ToDisableFeedsManagerSuccess() (*DisableFeedsManagerSuccessResolver, bool) { + if r.mgr != nil { + return NewDisableFeedsManagerSuccessResolver(*r.mgr), true + } + + return nil, false +} + +type DisableFeedsManagerSuccessResolver struct { + mgr feeds.FeedsManager +} + +func NewDisableFeedsManagerSuccessResolver(mgr feeds.FeedsManager) *DisableFeedsManagerSuccessResolver { + return &DisableFeedsManagerSuccessResolver{ + mgr: mgr, + } +} + +func (r *DisableFeedsManagerSuccessResolver) FeedsManager() *FeedsManagerResolver { + return NewFeedsManager(r.mgr) +} diff --git a/core/web/resolver/feeds_manager_test.go b/core/web/resolver/feeds_manager_test.go index 4237c6a7749..697c91162fd 100644 --- a/core/web/resolver/feeds_manager_test.go +++ b/core/web/resolver/feeds_manager_test.go @@ -5,6 +5,7 @@ import ( "database/sql" "testing" + "github.com/pkg/errors" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -460,3 +461,217 @@ func Test_UpdateFeedsManager(t *testing.T) { RunGQLTests(t, testCases) } + +func Test_EnableFeedsManager(t *testing.T) { + var ( + mgrID = int64(1) + name = "manager1" + uri = "localhost:2000" + pubKeyHex = "3b0f149627adb7b6fafe1497a9dfc357f22295a5440786c3bc566dfdb0176808" + + mutation = ` + mutation EnableFeedsManager($id: ID!) { + enableFeedsManager(id: $id) { + ... on EnableFeedsManagerSuccess { + feedsManager { + id + name + uri + publicKey + isConnectionActive + createdAt + disabledAt + } + } + ... on NotFoundError { + message + code + } + } + }` + variables = map[string]interface{}{ + "id": "1", + } + ) + + pubKey, err := crypto.PublicKeyFromHex(pubKeyHex) + require.NoError(t, err) + + testCases := []GQLTestCase{ + unauthorizedTestCase(GQLTestCase{query: mutation, variables: variables}, "enableFeedsManager"), + { + name: "success", + authenticated: true, + before: func(ctx context.Context, f *gqlTestFramework) { + disabledAt := f.Timestamp() + f.App.On("GetFeedsService").Return(f.Mocks.feedsSvc) + f.Mocks.feedsSvc.On("EnableManager", mock.Anything, mgrID).Return(&feeds.FeedsManager{ + ID: mgrID, + Name: name, + URI: uri, + IsConnectionActive: false, + PublicKey: *pubKey, + CreatedAt: f.Timestamp(), + DisabledAt: &disabledAt, + }, nil) + }, + query: mutation, + variables: variables, + result: ` + { + "enableFeedsManager": { + "feedsManager": { + "id": "1", + "name": "manager1", + "uri": "localhost:2000", + "publicKey": "3b0f149627adb7b6fafe1497a9dfc357f22295a5440786c3bc566dfdb0176808", + "isConnectionActive": false, + "createdAt": "2021-01-01T00:00:00Z", + "disabledAt": "2021-01-01T00:00:00Z" + } + } + }`, + }, + { + name: "not found", + authenticated: true, + before: func(ctx context.Context, f *gqlTestFramework) { + f.App.On("GetFeedsService").Return(f.Mocks.feedsSvc) + f.Mocks.feedsSvc.On("EnableManager", mock.Anything, mgrID).Return(nil, sql.ErrNoRows) + }, + query: mutation, + variables: variables, + result: ` + { + "enableFeedsManager": { + "message": "feeds manager not found", + "code": "NOT_FOUND" + } + }`, + }, + { + name: "db query error", + authenticated: true, + before: func(ctx context.Context, f *gqlTestFramework) { + f.App.On("GetFeedsService").Return(f.Mocks.feedsSvc) + f.Mocks.feedsSvc.On("EnableManager", mock.Anything, mgrID).Return(nil, errors.New("db error")) + }, + query: mutation, + variables: variables, + result: ` + { + "enableFeedsManager": { + } + }`, + }, + } + + RunGQLTests(t, testCases) +} + +func Test_DisableFeedsManager(t *testing.T) { + var ( + mgrID = int64(1) + name = "manager1" + uri = "localhost:2000" + pubKeyHex = "3b0f149627adb7b6fafe1497a9dfc357f22295a5440786c3bc566dfdb0176808" + + mutation = ` + mutation DisableFeedsManager($id: ID!) { + disableFeedsManager(id: $id) { + ... on DisableFeedsManagerSuccess { + feedsManager { + id + name + uri + publicKey + isConnectionActive + createdAt + disabledAt + } + } + ... on NotFoundError { + message + code + } + } + }` + variables = map[string]interface{}{ + "id": "1", + } + ) + + pubKey, err := crypto.PublicKeyFromHex(pubKeyHex) + require.NoError(t, err) + + testCases := []GQLTestCase{ + unauthorizedTestCase(GQLTestCase{query: mutation, variables: variables}, "disableFeedsManager"), + { + name: "success", + authenticated: true, + before: func(ctx context.Context, f *gqlTestFramework) { + disabledAt := f.Timestamp() + f.App.On("GetFeedsService").Return(f.Mocks.feedsSvc) + f.Mocks.feedsSvc.On("DisableManager", mock.Anything, mgrID).Return(&feeds.FeedsManager{ + ID: mgrID, + Name: name, + URI: uri, + IsConnectionActive: false, + PublicKey: *pubKey, + CreatedAt: f.Timestamp(), + DisabledAt: &disabledAt, + }, nil) + }, + query: mutation, + variables: variables, + result: ` + { + "disableFeedsManager": { + "feedsManager": { + "id": "1", + "name": "manager1", + "uri": "localhost:2000", + "publicKey": "3b0f149627adb7b6fafe1497a9dfc357f22295a5440786c3bc566dfdb0176808", + "isConnectionActive": false, + "createdAt": "2021-01-01T00:00:00Z", + "disabledAt": "2021-01-01T00:00:00Z" + } + } + }`, + }, + { + name: "not found", + authenticated: true, + before: func(ctx context.Context, f *gqlTestFramework) { + f.App.On("GetFeedsService").Return(f.Mocks.feedsSvc) + f.Mocks.feedsSvc.On("DisableManager", mock.Anything, mgrID).Return(nil, sql.ErrNoRows) + }, + query: mutation, + variables: variables, + result: ` + { + "disableFeedsManager": { + "message": "feeds manager not found", + "code": "NOT_FOUND" + } + }`, + }, + { + name: "db query error", + authenticated: true, + before: func(ctx context.Context, f *gqlTestFramework) { + f.App.On("GetFeedsService").Return(f.Mocks.feedsSvc) + f.Mocks.feedsSvc.On("DisableManager", mock.Anything, mgrID).Return(nil, errors.New("db error")) + }, + query: mutation, + variables: variables, + result: ` + { + "disableFeedsManager": { + } + }`, + }, + } + + RunGQLTests(t, testCases) +} diff --git a/core/web/resolver/mutation.go b/core/web/resolver/mutation.go index 4388bd5a701..92d30ca65af 100644 --- a/core/web/resolver/mutation.go +++ b/core/web/resolver/mutation.go @@ -570,6 +570,50 @@ func (r *Resolver) UpdateFeedsManager(ctx context.Context, args struct { return NewUpdateFeedsManagerPayload(mgr, nil, nil), nil } +func (r *Resolver) EnableFeedsManager(ctx context.Context, args struct { + ID graphql.ID +}, +) (*EnableFeedsManagerPayloadResolver, error) { + if err := authenticateUserCanEdit(ctx); err != nil { + return nil, err + } + + id, err := stringutils.ToInt64(string(args.ID)) + if err != nil { + return nil, err + } + + feedsService := r.App.GetFeedsService() + + mgr, err := feedsService.EnableManager(ctx, id) + + mgrj, _ := json.Marshal(mgr) + r.App.GetAuditLogger().Audit(audit.FeedsManEnabled, map[string]interface{}{"mgrj": mgrj}) + return NewEnableFeedsManagerPayload(mgr, err), nil +} + +func (r *Resolver) DisableFeedsManager(ctx context.Context, args struct { + ID graphql.ID +}, +) (*DisableFeedsManagerPayloadResolver, error) { + if err := authenticateUserCanEdit(ctx); err != nil { + return nil, err + } + + id, err := stringutils.ToInt64(string(args.ID)) + if err != nil { + return nil, err + } + + feedsService := r.App.GetFeedsService() + + mgr, err := feedsService.DisableManager(ctx, id) + + mgrj, _ := json.Marshal(mgr) + r.App.GetAuditLogger().Audit(audit.FeedsManDisabled, map[string]interface{}{"mgrj": mgrj}) + return NewDisableFeedsManagerPayload(mgr, err), nil +} + func (r *Resolver) CreateOCRKeyBundle(ctx context.Context) (*CreateOCRKeyBundlePayloadResolver, error) { if err := authenticateUserCanEdit(ctx); err != nil { return nil, err diff --git a/core/web/schema/schema.graphql b/core/web/schema/schema.graphql index 415be25b77d..0bf9ae9e71a 100644 --- a/core/web/schema/schema.graphql +++ b/core/web/schema/schema.graphql @@ -70,6 +70,8 @@ type Mutation { setSQLLogging(input: SetSQLLoggingInput!): SetSQLLoggingPayload! updateBridge(id: ID!, input: UpdateBridgeInput!): UpdateBridgePayload! updateFeedsManager(id: ID!, input: UpdateFeedsManagerInput!): UpdateFeedsManagerPayload! + enableFeedsManager(id: ID!): EnableFeedsManagerPayload! + disableFeedsManager(id: ID!): DisableFeedsManagerPayload! updateFeedsManagerChainConfig(id: ID!, input: UpdateFeedsManagerChainConfigInput!): UpdateFeedsManagerChainConfigPayload! updateJobProposalSpecDefinition(id: ID!, input: UpdateJobProposalSpecDefinitionInput!): UpdateJobProposalSpecDefinitionPayload! updateUserPassword(input: UpdatePasswordInput!): UpdatePasswordPayload! diff --git a/core/web/schema/type/feeds_manager.graphql b/core/web/schema/type/feeds_manager.graphql index 9da8f64e1c2..daad488fac7 100644 --- a/core/web/schema/type/feeds_manager.graphql +++ b/core/web/schema/type/feeds_manager.graphql @@ -20,6 +20,7 @@ type FeedsManager { jobProposals: [JobProposal!]! isConnectionActive: Boolean! createdAt: Time! + disabledAt: Time chainConfigs: [FeedsManagerChainConfig!]! } @@ -188,3 +189,17 @@ type UpdateFeedsManagerChainConfigSuccess { union UpdateFeedsManagerChainConfigPayload = UpdateFeedsManagerChainConfigSuccess | NotFoundError | InputErrors + +type EnableFeedsManagerSuccess { + feedsManager: FeedsManager! +} + +union EnableFeedsManagerPayload = EnableFeedsManagerSuccess + | NotFoundError + +type DisableFeedsManagerSuccess { + feedsManager: FeedsManager! +} + +union DisableFeedsManagerPayload = DisableFeedsManagerSuccess + | NotFoundError \ No newline at end of file