diff --git a/Wire.go b/Wire.go index 0467650a5d..bf581e611f 100644 --- a/Wire.go +++ b/Wire.go @@ -374,6 +374,10 @@ func InitializeApp() (*App, error) { wire.Bind(new(notifier.SlackNotificationService), new(*notifier.SlackNotificationServiceImpl)), repository.NewSlackNotificationRepositoryImpl, wire.Bind(new(repository.SlackNotificationRepository), new(*repository.SlackNotificationRepositoryImpl)), + notifier.NewWebhookNotificationServiceImpl, + wire.Bind(new(notifier.WebhookNotificationService), new(*notifier.WebhookNotificationServiceImpl)), + repository.NewWebhookNotificationRepositoryImpl, + wire.Bind(new(repository.WebhookNotificationRepository), new(*repository.WebhookNotificationRepositoryImpl)), notifier.NewNotificationConfigServiceImpl, wire.Bind(new(notifier.NotificationConfigService), new(*notifier.NotificationConfigServiceImpl)), diff --git a/api/restHandler/NotificationRestHandler.go b/api/restHandler/NotificationRestHandler.go index 586deedd50..f047942dc3 100644 --- a/api/restHandler/NotificationRestHandler.go +++ b/api/restHandler/NotificationRestHandler.go @@ -45,19 +45,21 @@ import ( ) const ( - SLACK_CONFIG_DELETE_SUCCESS_RESP = "Slack config deleted successfully." - SES_CONFIG_DELETE_SUCCESS_RESP = "SES config deleted successfully." - SMTP_CONFIG_DELETE_SUCCESS_RESP = "SMTP config deleted successfully." + SLACK_CONFIG_DELETE_SUCCESS_RESP = "Slack config deleted successfully." + WEBHOOK_CONFIG_DELETE_SUCCESS_RESP = "Webhook config deleted successfully." + SES_CONFIG_DELETE_SUCCESS_RESP = "SES config deleted successfully." + SMTP_CONFIG_DELETE_SUCCESS_RESP = "SMTP config deleted successfully." ) type NotificationRestHandler interface { SaveNotificationSettings(w http.ResponseWriter, r *http.Request) UpdateNotificationSettings(w http.ResponseWriter, r *http.Request) - SaveNotificationChannelConfig(w http.ResponseWriter, r *http.Request) FindSESConfig(w http.ResponseWriter, r *http.Request) FindSlackConfig(w http.ResponseWriter, r *http.Request) FindSMTPConfig(w http.ResponseWriter, r *http.Request) + FindWebhookConfig(w http.ResponseWriter, r *http.Request) + GetWebhookVariables(w http.ResponseWriter, r *http.Request) FindAllNotificationConfig(w http.ResponseWriter, r *http.Request) GetAllNotificationSettings(w http.ResponseWriter, r *http.Request) DeleteNotificationSettings(w http.ResponseWriter, r *http.Request) @@ -76,6 +78,7 @@ type NotificationRestHandlerImpl struct { validator *validator.Validate notificationService notifier.NotificationConfigService slackService notifier.SlackNotificationService + webhookService notifier.WebhookNotificationService sesService notifier.SESNotificationService smtpService notifier.SMTPNotificationService enforcer casbin.Enforcer @@ -93,7 +96,7 @@ func NewNotificationRestHandlerImpl(dockerRegistryConfig pipeline.DockerRegistry logger *zap.SugaredLogger, gitRegistryConfig pipeline.GitRegistryConfig, dbConfigService pipeline.DbConfigService, userAuthService user.UserService, validator *validator.Validate, notificationService notifier.NotificationConfigService, - slackService notifier.SlackNotificationService, sesService notifier.SESNotificationService, smtpService notifier.SMTPNotificationService, + slackService notifier.SlackNotificationService, webhookService notifier.WebhookNotificationService, sesService notifier.SESNotificationService, smtpService notifier.SMTPNotificationService, enforcer casbin.Enforcer, teamService team.TeamService, environmentService cluster.EnvironmentService, pipelineBuilder pipeline.PipelineBuilder, enforcerUtil rbac.EnforcerUtil) *NotificationRestHandlerImpl { return &NotificationRestHandlerImpl{ @@ -105,6 +108,7 @@ func NewNotificationRestHandlerImpl(dockerRegistryConfig pipeline.DockerRegistry validator: validator, notificationService: notificationService, slackService: slackService, + webhookService: webhookService, sesService: sesService, smtpService: smtpService, enforcer: enforcer, @@ -551,13 +555,46 @@ func (impl NotificationRestHandlerImpl) SaveNotificationChannelConfig(w http.Res } w.Header().Set("Content-Type", "application/json") common.WriteJsonResp(w, nil, res, http.StatusOK) + } else if util.Webhook == channelReq.Channel { + var webhookReq *notifier.WebhookChannelConfig + err = json.NewDecoder(ioutil.NopCloser(bytes.NewBuffer(data))).Decode(&webhookReq) + if err != nil { + impl.logger.Errorw("request err, SaveNotificationChannelConfig", "err", err, "webhookReq", webhookReq) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + + err = impl.validator.Struct(webhookReq) + if err != nil { + impl.logger.Errorw("validation err, SaveNotificationChannelConfig", "err", err, "webhookReq", webhookReq) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + + // RBAC enforcer applying + token := r.Header.Get("token") + if ok := impl.enforcer.Enforce(token, casbin.ResourceNotification, casbin.ActionCreate, "*"); !ok { + response.WriteResponse(http.StatusForbidden, "FORBIDDEN", w, errors.New("unauthorized")) + return + } + //RBAC enforcer Ends + + res, cErr := impl.webhookService.SaveOrEditNotificationConfig(*webhookReq.WebhookConfigDtos, userId) + if cErr != nil { + impl.logger.Errorw("service err, SaveNotificationChannelConfig", "err", err, "webhookReq", webhookReq) + common.WriteJsonResp(w, cErr, nil, http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + common.WriteJsonResp(w, nil, res, http.StatusOK) } } type ChannelResponseDTO struct { - SlackConfigs []*notifier.SlackConfigDto `json:"slackConfigs"` - SESConfigs []*notifier.SESConfigDto `json:"sesConfigs"` - SMTPConfigs []*notifier.SMTPConfigDto `json:"smtpConfigs"` + SlackConfigs []*notifier.SlackConfigDto `json:"slackConfigs"` + WebhookConfigs []*notifier.WebhookConfigDto `json:"webhookConfigs"` + SESConfigs []*notifier.SESConfigDto `json:"sesConfigs"` + SMTPConfigs []*notifier.SMTPConfigDto `json:"smtpConfigs"` } func (impl NotificationRestHandlerImpl) FindAllNotificationConfig(w http.ResponseWriter, r *http.Request) { @@ -607,6 +644,18 @@ func (impl NotificationRestHandlerImpl) FindAllNotificationConfig(w http.Respons if pass { channelsResponse.SlackConfigs = slackConfigs } + webhookConfigs, fErr := impl.webhookService.FetchAllWebhookNotificationConfig() + if fErr != nil && fErr != pg.ErrNoRows { + impl.logger.Errorw("service err, FindAllNotificationConfig", "err", err) + common.WriteJsonResp(w, fErr, nil, http.StatusInternalServerError) + return + } + if webhookConfigs == nil { + webhookConfigs = make([]*notifier.WebhookConfigDto, 0) + } + if pass { + channelsResponse.WebhookConfigs = webhookConfigs + } sesConfigs, fErr := impl.sesService.FetchAllSESNotificationConfig() if fErr != nil && fErr != pg.ErrNoRows { impl.logger.Errorw("service err, FindAllNotificationConfig", "err", err) @@ -717,6 +766,47 @@ func (impl NotificationRestHandlerImpl) FindSMTPConfig(w http.ResponseWriter, r w.Header().Set("Content-Type", "application/json") common.WriteJsonResp(w, fErr, smtpConfig, http.StatusOK) } +func (impl NotificationRestHandlerImpl) FindWebhookConfig(w http.ResponseWriter, r *http.Request) { + userId, err := impl.userAuthService.GetLoggedInUser(r) + if userId == 0 || err != nil { + common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized) + return + } + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + impl.logger.Errorw("request err, FindWebhookConfig", "err", err, "id", id) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + + webhookConfig, fErr := impl.webhookService.FetchWebhookNotificationConfigById(id) + if fErr != nil && fErr != pg.ErrNoRows { + impl.logger.Errorw("service err, FindWebhookConfig, cannot find webhook config", "err", fErr, "id", id) + common.WriteJsonResp(w, fErr, nil, http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + common.WriteJsonResp(w, fErr, webhookConfig, http.StatusOK) +} +func (impl NotificationRestHandlerImpl) GetWebhookVariables(w http.ResponseWriter, r *http.Request) { + userId, err := impl.userAuthService.GetLoggedInUser(r) + if userId == 0 || err != nil { + common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized) + return + } + + webhookVariables, fErr := impl.webhookService.GetWebhookVariables() + if fErr != nil && fErr != pg.ErrNoRows { + impl.logger.Errorw("service err, GetWebhookVariables, cannot find webhook Variables", "err", fErr) + common.WriteJsonResp(w, fErr, nil, http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + common.WriteJsonResp(w, fErr, webhookVariables, http.StatusOK) +} func (impl NotificationRestHandlerImpl) RecipientListingSuggestion(w http.ResponseWriter, r *http.Request) { userId, err := impl.userAuthService.GetLoggedInUser(r) @@ -783,6 +873,17 @@ func (impl NotificationRestHandlerImpl) FindAllNotificationConfigAutocomplete(w } } + } else if cType == string(util.Webhook) { + if ok := impl.enforcer.Enforce(token, casbin.ResourceNotification, casbin.ActionGet, "*"); !ok { + response.WriteResponse(http.StatusForbidden, "FORBIDDEN", w, errors.New("unauthorized")) + return + } + channelsResponse, err = impl.webhookService.FetchAllWebhookNotificationConfigAutocomplete() + if err != nil && err != pg.ErrNoRows { + impl.logger.Errorw("service err, FindAllNotificationConfigAutocomplete", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } } else if cType == string(util.SES) { if ok := impl.enforcer.Enforce(token, casbin.ResourceNotification, casbin.ActionGet, "*"); !ok { response.WriteResponse(http.StatusForbidden, "FORBIDDEN", w, errors.New("unauthorized")) @@ -929,6 +1030,37 @@ func (impl NotificationRestHandlerImpl) DeleteNotificationChannelConfig(w http.R return } common.WriteJsonResp(w, nil, SLACK_CONFIG_DELETE_SUCCESS_RESP, http.StatusOK) + } else if util.Webhook == channelReq.Channel { + var deleteReq *notifier.WebhookConfigDto + err = json.NewDecoder(ioutil.NopCloser(bytes.NewBuffer(data))).Decode(&deleteReq) + if err != nil { + impl.logger.Errorw("request err, DeleteNotificationChannelConfig", "err", err, "deleteReq", deleteReq) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + + err = impl.validator.Struct(deleteReq) + if err != nil { + impl.logger.Errorw("validation err, DeleteNotificationChannelConfig", "err", err, "deleteReq", deleteReq) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + + // RBAC enforcer applying + token := r.Header.Get("token") + if ok := impl.enforcer.Enforce(token, casbin.ResourceNotification, casbin.ActionCreate, "*"); !ok { + response.WriteResponse(http.StatusForbidden, "FORBIDDEN", w, errors.New("unauthorized")) + return + } + //RBAC enforcer Ends + + cErr := impl.webhookService.DeleteNotificationConfig(deleteReq, userId) + if cErr != nil { + impl.logger.Errorw("service err, DeleteNotificationChannelConfig", "err", err, "deleteReq", deleteReq) + common.WriteJsonResp(w, cErr, nil, http.StatusInternalServerError) + return + } + common.WriteJsonResp(w, nil, WEBHOOK_CONFIG_DELETE_SUCCESS_RESP, http.StatusOK) } else if util.SES == channelReq.Channel { var deleteReq *notifier.SESConfigDto err = json.NewDecoder(ioutil.NopCloser(bytes.NewBuffer(data))).Decode(&deleteReq) diff --git a/api/router/NotificationRouter.go b/api/router/NotificationRouter.go index 1cc7bc82e7..f920021274 100644 --- a/api/router/NotificationRouter.go +++ b/api/router/NotificationRouter.go @@ -63,6 +63,13 @@ func (impl NotificationRouterImpl) InitNotificationRegRouter(configRouter *mux.R configRouter.Path("/channel/smtp/{id}"). HandlerFunc(impl.notificationRestHandler.FindSMTPConfig). Methods("GET") + configRouter.Path("/channel/webhook/{id}"). + HandlerFunc(impl.notificationRestHandler.FindWebhookConfig). + Methods("GET") + configRouter.Path("/variables"). + HandlerFunc(impl.notificationRestHandler.GetWebhookVariables). + Methods("GET") + configRouter.Path("/channel"). HandlerFunc(impl.notificationRestHandler.DeleteNotificationChannelConfig). Methods("DELETE") diff --git a/internal/sql/repository/WebhookNotificationRepository.go b/internal/sql/repository/WebhookNotificationRepository.go new file mode 100644 index 0000000000..86be8b4ff5 --- /dev/null +++ b/internal/sql/repository/WebhookNotificationRepository.go @@ -0,0 +1,81 @@ +package repository + +import ( + "github.com/devtron-labs/devtron/pkg/sql" + "github.com/go-pg/pg" +) + +type WebhookNotificationRepository interface { + FindOne(id int) (*WebhookConfig, error) + UpdateWebhookConfig(webhookConfig *WebhookConfig) (*WebhookConfig, error) + SaveWebhookConfig(webhookConfig *WebhookConfig) (*WebhookConfig, error) + FindAll() ([]WebhookConfig, error) + FindByName(value string) ([]WebhookConfig, error) + FindByIds(ids []*int) ([]*WebhookConfig, error) + MarkWebhookConfigDeleted(webhookConfig *WebhookConfig) error +} + +type WebhookNotificationRepositoryImpl struct { + dbConnection *pg.DB +} + +func NewWebhookNotificationRepositoryImpl(dbConnection *pg.DB) *WebhookNotificationRepositoryImpl { + return &WebhookNotificationRepositoryImpl{dbConnection: dbConnection} +} + +type WebhookConfig struct { + tableName struct{} `sql:"webhook_config" pg:",discard_unknown_columns"` + Id int `sql:"id,pk"` + WebHookUrl string `sql:"web_hook_url"` + ConfigName string `sql:"config_name"` + Header map[string]interface{} `sql:"header"` + Payload map[string]interface{} `sql:"payload"` + Description string `sql:"description"` + OwnerId int32 `sql:"owner_id"` + Active bool `sql:"active"` + Deleted bool `sql:"deleted,notnull"` + sql.AuditLog +} + +func (impl *WebhookNotificationRepositoryImpl) FindOne(id int) (*WebhookConfig, error) { + details := &WebhookConfig{} + err := impl.dbConnection.Model(details).Where("id = ?", id). + Where("deleted = ?", false).Select() + + return details, err +} + +func (impl *WebhookNotificationRepositoryImpl) FindAll() ([]WebhookConfig, error) { + var webhookConfigs []WebhookConfig + err := impl.dbConnection.Model(&webhookConfigs). + Where("deleted = ?", false).Select() + return webhookConfigs, err +} + +func (impl *WebhookNotificationRepositoryImpl) UpdateWebhookConfig(webhookConfig *WebhookConfig) (*WebhookConfig, error) { + return webhookConfig, impl.dbConnection.Update(webhookConfig) +} + +func (impl *WebhookNotificationRepositoryImpl) SaveWebhookConfig(webhookConfig *WebhookConfig) (*WebhookConfig, error) { + return webhookConfig, impl.dbConnection.Insert(webhookConfig) +} + +func (impl *WebhookNotificationRepositoryImpl) FindByName(value string) ([]WebhookConfig, error) { + var webhookConfigs []WebhookConfig + err := impl.dbConnection.Model(&webhookConfigs).Where(`config_name like ?`, "%"+value+"%"). + Where("deleted = ?", false).Select() + return webhookConfigs, err + +} + +func (repo *WebhookNotificationRepositoryImpl) FindByIds(ids []*int) ([]*WebhookConfig, error) { + var objects []*WebhookConfig + err := repo.dbConnection.Model(&objects).Where("id in (?)", pg.In(ids)). + Where("deleted = ?", false).Select() + return objects, err +} + +func (impl *WebhookNotificationRepositoryImpl) MarkWebhookConfigDeleted(webhookConfig *WebhookConfig) error { + webhookConfig.Deleted = true + return impl.dbConnection.Update(webhookConfig) +} diff --git a/internal/sql/repository/mocks/NotificationSettingsRepository.go b/internal/sql/repository/mocks/NotificationSettingsRepository.go new file mode 100644 index 0000000000..f64a06e437 --- /dev/null +++ b/internal/sql/repository/mocks/NotificationSettingsRepository.go @@ -0,0 +1,438 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + pg "github.com/go-pg/pg" + mock "github.com/stretchr/testify/mock" + + repository "github.com/devtron-labs/devtron/internal/sql/repository" +) + +// NotificationSettingsRepository is an autogenerated mock type for the NotificationSettingsRepository type +type NotificationSettingsRepository struct { + mock.Mock +} + +// DeleteNotificationSettingsByConfigId provides a mock function with given fields: viewId, tx +func (_m *NotificationSettingsRepository) DeleteNotificationSettingsByConfigId(viewId int, tx *pg.Tx) (int, error) { + ret := _m.Called(viewId, tx) + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func(int, *pg.Tx) (int, error)); ok { + return rf(viewId, tx) + } + if rf, ok := ret.Get(0).(func(int, *pg.Tx) int); ok { + r0 = rf(viewId, tx) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func(int, *pg.Tx) error); ok { + r1 = rf(viewId, tx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeleteNotificationSettingsViewById provides a mock function with given fields: id, tx +func (_m *NotificationSettingsRepository) DeleteNotificationSettingsViewById(id int, tx *pg.Tx) (int, error) { + ret := _m.Called(id, tx) + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func(int, *pg.Tx) (int, error)); ok { + return rf(id, tx) + } + if rf, ok := ret.Get(0).(func(int, *pg.Tx) int); ok { + r0 = rf(id, tx) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func(int, *pg.Tx) error); ok { + r1 = rf(id, tx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FetchNotificationSettingGroupBy provides a mock function with given fields: viewId +func (_m *NotificationSettingsRepository) FetchNotificationSettingGroupBy(viewId int) ([]repository.NotificationSettings, error) { + ret := _m.Called(viewId) + + var r0 []repository.NotificationSettings + var r1 error + if rf, ok := ret.Get(0).(func(int) ([]repository.NotificationSettings, error)); ok { + return rf(viewId) + } + if rf, ok := ret.Get(0).(func(int) []repository.NotificationSettings); ok { + r0 = rf(viewId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]repository.NotificationSettings) + } + } + + if rf, ok := ret.Get(1).(func(int) error); ok { + r1 = rf(viewId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FindAll provides a mock function with given fields: offset, size +func (_m *NotificationSettingsRepository) FindAll(offset int, size int) ([]*repository.NotificationSettingsView, error) { + ret := _m.Called(offset, size) + + var r0 []*repository.NotificationSettingsView + var r1 error + if rf, ok := ret.Get(0).(func(int, int) ([]*repository.NotificationSettingsView, error)); ok { + return rf(offset, size) + } + if rf, ok := ret.Get(0).(func(int, int) []*repository.NotificationSettingsView); ok { + r0 = rf(offset, size) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*repository.NotificationSettingsView) + } + } + + if rf, ok := ret.Get(1).(func(int, int) error); ok { + r1 = rf(offset, size) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FindNSViewCount provides a mock function with given fields: +func (_m *NotificationSettingsRepository) FindNSViewCount() (int, error) { + ret := _m.Called() + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func() (int, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FindNotificationSettingBuildOptions provides a mock function with given fields: settingRequest +func (_m *NotificationSettingsRepository) FindNotificationSettingBuildOptions(settingRequest *repository.SearchRequest) ([]*repository.SettingOptionDTO, error) { + ret := _m.Called(settingRequest) + + var r0 []*repository.SettingOptionDTO + var r1 error + if rf, ok := ret.Get(0).(func(*repository.SearchRequest) ([]*repository.SettingOptionDTO, error)); ok { + return rf(settingRequest) + } + if rf, ok := ret.Get(0).(func(*repository.SearchRequest) []*repository.SettingOptionDTO); ok { + r0 = rf(settingRequest) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*repository.SettingOptionDTO) + } + } + + if rf, ok := ret.Get(1).(func(*repository.SearchRequest) error); ok { + r1 = rf(settingRequest) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FindNotificationSettingDeploymentOptions provides a mock function with given fields: settingRequest +func (_m *NotificationSettingsRepository) FindNotificationSettingDeploymentOptions(settingRequest *repository.SearchRequest) ([]*repository.SettingOptionDTO, error) { + ret := _m.Called(settingRequest) + + var r0 []*repository.SettingOptionDTO + var r1 error + if rf, ok := ret.Get(0).(func(*repository.SearchRequest) ([]*repository.SettingOptionDTO, error)); ok { + return rf(settingRequest) + } + if rf, ok := ret.Get(0).(func(*repository.SearchRequest) []*repository.SettingOptionDTO); ok { + r0 = rf(settingRequest) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*repository.SettingOptionDTO) + } + } + + if rf, ok := ret.Get(1).(func(*repository.SearchRequest) error); ok { + r1 = rf(settingRequest) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FindNotificationSettingsByConfigIdAndConfigType provides a mock function with given fields: configId, configType +func (_m *NotificationSettingsRepository) FindNotificationSettingsByConfigIdAndConfigType(configId int, configType string) ([]*repository.NotificationSettings, error) { + ret := _m.Called(configId, configType) + + var r0 []*repository.NotificationSettings + var r1 error + if rf, ok := ret.Get(0).(func(int, string) ([]*repository.NotificationSettings, error)); ok { + return rf(configId, configType) + } + if rf, ok := ret.Get(0).(func(int, string) []*repository.NotificationSettings); ok { + r0 = rf(configId, configType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*repository.NotificationSettings) + } + } + + if rf, ok := ret.Get(1).(func(int, string) error); ok { + r1 = rf(configId, configType) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FindNotificationSettingsByViewId provides a mock function with given fields: viewId +func (_m *NotificationSettingsRepository) FindNotificationSettingsByViewId(viewId int) ([]repository.NotificationSettings, error) { + ret := _m.Called(viewId) + + var r0 []repository.NotificationSettings + var r1 error + if rf, ok := ret.Get(0).(func(int) ([]repository.NotificationSettings, error)); ok { + return rf(viewId) + } + if rf, ok := ret.Get(0).(func(int) []repository.NotificationSettings); ok { + r0 = rf(viewId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]repository.NotificationSettings) + } + } + + if rf, ok := ret.Get(1).(func(int) error); ok { + r1 = rf(viewId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FindNotificationSettingsViewById provides a mock function with given fields: id +func (_m *NotificationSettingsRepository) FindNotificationSettingsViewById(id int) (*repository.NotificationSettingsView, error) { + ret := _m.Called(id) + + var r0 *repository.NotificationSettingsView + var r1 error + if rf, ok := ret.Get(0).(func(int) (*repository.NotificationSettingsView, error)); ok { + return rf(id) + } + if rf, ok := ret.Get(0).(func(int) *repository.NotificationSettingsView); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*repository.NotificationSettingsView) + } + } + + if rf, ok := ret.Get(1).(func(int) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FindNotificationSettingsViewByIds provides a mock function with given fields: id +func (_m *NotificationSettingsRepository) FindNotificationSettingsViewByIds(id []*int) ([]*repository.NotificationSettingsView, error) { + ret := _m.Called(id) + + var r0 []*repository.NotificationSettingsView + var r1 error + if rf, ok := ret.Get(0).(func([]*int) ([]*repository.NotificationSettingsView, error)); ok { + return rf(id) + } + if rf, ok := ret.Get(0).(func([]*int) []*repository.NotificationSettingsView); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*repository.NotificationSettingsView) + } + } + + if rf, ok := ret.Get(1).(func([]*int) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SaveAllNotificationSettings provides a mock function with given fields: notificationSettings, tx +func (_m *NotificationSettingsRepository) SaveAllNotificationSettings(notificationSettings []repository.NotificationSettings, tx *pg.Tx) (int, error) { + ret := _m.Called(notificationSettings, tx) + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func([]repository.NotificationSettings, *pg.Tx) (int, error)); ok { + return rf(notificationSettings, tx) + } + if rf, ok := ret.Get(0).(func([]repository.NotificationSettings, *pg.Tx) int); ok { + r0 = rf(notificationSettings, tx) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]repository.NotificationSettings, *pg.Tx) error); ok { + r1 = rf(notificationSettings, tx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SaveNotificationSetting provides a mock function with given fields: notificationSettings, tx +func (_m *NotificationSettingsRepository) SaveNotificationSetting(notificationSettings *repository.NotificationSettings, tx *pg.Tx) (*repository.NotificationSettings, error) { + ret := _m.Called(notificationSettings, tx) + + var r0 *repository.NotificationSettings + var r1 error + if rf, ok := ret.Get(0).(func(*repository.NotificationSettings, *pg.Tx) (*repository.NotificationSettings, error)); ok { + return rf(notificationSettings, tx) + } + if rf, ok := ret.Get(0).(func(*repository.NotificationSettings, *pg.Tx) *repository.NotificationSettings); ok { + r0 = rf(notificationSettings, tx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*repository.NotificationSettings) + } + } + + if rf, ok := ret.Get(1).(func(*repository.NotificationSettings, *pg.Tx) error); ok { + r1 = rf(notificationSettings, tx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SaveNotificationSettingsConfig provides a mock function with given fields: notificationSettingsView, tx +func (_m *NotificationSettingsRepository) SaveNotificationSettingsConfig(notificationSettingsView *repository.NotificationSettingsView, tx *pg.Tx) (*repository.NotificationSettingsView, error) { + ret := _m.Called(notificationSettingsView, tx) + + var r0 *repository.NotificationSettingsView + var r1 error + if rf, ok := ret.Get(0).(func(*repository.NotificationSettingsView, *pg.Tx) (*repository.NotificationSettingsView, error)); ok { + return rf(notificationSettingsView, tx) + } + if rf, ok := ret.Get(0).(func(*repository.NotificationSettingsView, *pg.Tx) *repository.NotificationSettingsView); ok { + r0 = rf(notificationSettingsView, tx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*repository.NotificationSettingsView) + } + } + + if rf, ok := ret.Get(1).(func(*repository.NotificationSettingsView, *pg.Tx) error); ok { + r1 = rf(notificationSettingsView, tx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateNotificationSettings provides a mock function with given fields: notificationSettings, tx +func (_m *NotificationSettingsRepository) UpdateNotificationSettings(notificationSettings *repository.NotificationSettings, tx *pg.Tx) (*repository.NotificationSettings, error) { + ret := _m.Called(notificationSettings, tx) + + var r0 *repository.NotificationSettings + var r1 error + if rf, ok := ret.Get(0).(func(*repository.NotificationSettings, *pg.Tx) (*repository.NotificationSettings, error)); ok { + return rf(notificationSettings, tx) + } + if rf, ok := ret.Get(0).(func(*repository.NotificationSettings, *pg.Tx) *repository.NotificationSettings); ok { + r0 = rf(notificationSettings, tx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*repository.NotificationSettings) + } + } + + if rf, ok := ret.Get(1).(func(*repository.NotificationSettings, *pg.Tx) error); ok { + r1 = rf(notificationSettings, tx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateNotificationSettingsView provides a mock function with given fields: notificationSettingsView, tx +func (_m *NotificationSettingsRepository) UpdateNotificationSettingsView(notificationSettingsView *repository.NotificationSettingsView, tx *pg.Tx) (*repository.NotificationSettingsView, error) { + ret := _m.Called(notificationSettingsView, tx) + + var r0 *repository.NotificationSettingsView + var r1 error + if rf, ok := ret.Get(0).(func(*repository.NotificationSettingsView, *pg.Tx) (*repository.NotificationSettingsView, error)); ok { + return rf(notificationSettingsView, tx) + } + if rf, ok := ret.Get(0).(func(*repository.NotificationSettingsView, *pg.Tx) *repository.NotificationSettingsView); ok { + r0 = rf(notificationSettingsView, tx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*repository.NotificationSettingsView) + } + } + + if rf, ok := ret.Get(1).(func(*repository.NotificationSettingsView, *pg.Tx) error); ok { + r1 = rf(notificationSettingsView, tx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewNotificationSettingsRepository interface { + mock.TestingT + Cleanup(func()) +} + +// NewNotificationSettingsRepository creates a new instance of NotificationSettingsRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewNotificationSettingsRepository(t mockConstructorTestingTNewNotificationSettingsRepository) *NotificationSettingsRepository { + mock := &NotificationSettingsRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/sql/repository/mocks/WebhookNotificationRepository.go b/internal/sql/repository/mocks/WebhookNotificationRepository.go new file mode 100644 index 0000000000..e48ce9ce1d --- /dev/null +++ b/internal/sql/repository/mocks/WebhookNotificationRepository.go @@ -0,0 +1,198 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + repository "github.com/devtron-labs/devtron/internal/sql/repository" + mock "github.com/stretchr/testify/mock" +) + +// WebhookNotificationRepository is an autogenerated mock type for the WebhookNotificationRepository type +type WebhookNotificationRepository struct { + mock.Mock +} + +// FindAll provides a mock function with given fields: +func (_m *WebhookNotificationRepository) FindAll() ([]repository.WebhookConfig, error) { + ret := _m.Called() + + var r0 []repository.WebhookConfig + var r1 error + if rf, ok := ret.Get(0).(func() ([]repository.WebhookConfig, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() []repository.WebhookConfig); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]repository.WebhookConfig) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FindByIds provides a mock function with given fields: ids +func (_m *WebhookNotificationRepository) FindByIds(ids []*int) ([]*repository.WebhookConfig, error) { + ret := _m.Called(ids) + + var r0 []*repository.WebhookConfig + var r1 error + if rf, ok := ret.Get(0).(func([]*int) ([]*repository.WebhookConfig, error)); ok { + return rf(ids) + } + if rf, ok := ret.Get(0).(func([]*int) []*repository.WebhookConfig); ok { + r0 = rf(ids) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*repository.WebhookConfig) + } + } + + if rf, ok := ret.Get(1).(func([]*int) error); ok { + r1 = rf(ids) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FindByName provides a mock function with given fields: value +func (_m *WebhookNotificationRepository) FindByName(value string) ([]repository.WebhookConfig, error) { + ret := _m.Called(value) + + var r0 []repository.WebhookConfig + var r1 error + if rf, ok := ret.Get(0).(func(string) ([]repository.WebhookConfig, error)); ok { + return rf(value) + } + if rf, ok := ret.Get(0).(func(string) []repository.WebhookConfig); ok { + r0 = rf(value) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]repository.WebhookConfig) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(value) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FindOne provides a mock function with given fields: id +func (_m *WebhookNotificationRepository) FindOne(id int) (*repository.WebhookConfig, error) { + ret := _m.Called(id) + + var r0 *repository.WebhookConfig + var r1 error + if rf, ok := ret.Get(0).(func(int) (*repository.WebhookConfig, error)); ok { + return rf(id) + } + if rf, ok := ret.Get(0).(func(int) *repository.WebhookConfig); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*repository.WebhookConfig) + } + } + + if rf, ok := ret.Get(1).(func(int) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MarkWebhookConfigDeleted provides a mock function with given fields: webhookConfig +func (_m *WebhookNotificationRepository) MarkWebhookConfigDeleted(webhookConfig *repository.WebhookConfig) error { + ret := _m.Called(webhookConfig) + + var r0 error + if rf, ok := ret.Get(0).(func(*repository.WebhookConfig) error); ok { + r0 = rf(webhookConfig) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SaveWebhookConfig provides a mock function with given fields: webhookConfig +func (_m *WebhookNotificationRepository) SaveWebhookConfig(webhookConfig *repository.WebhookConfig) (*repository.WebhookConfig, error) { + ret := _m.Called(webhookConfig) + + var r0 *repository.WebhookConfig + var r1 error + if rf, ok := ret.Get(0).(func(*repository.WebhookConfig) (*repository.WebhookConfig, error)); ok { + return rf(webhookConfig) + } + if rf, ok := ret.Get(0).(func(*repository.WebhookConfig) *repository.WebhookConfig); ok { + r0 = rf(webhookConfig) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*repository.WebhookConfig) + } + } + + if rf, ok := ret.Get(1).(func(*repository.WebhookConfig) error); ok { + r1 = rf(webhookConfig) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateWebhookConfig provides a mock function with given fields: webhookConfig +func (_m *WebhookNotificationRepository) UpdateWebhookConfig(webhookConfig *repository.WebhookConfig) (*repository.WebhookConfig, error) { + ret := _m.Called(webhookConfig) + + var r0 *repository.WebhookConfig + var r1 error + if rf, ok := ret.Get(0).(func(*repository.WebhookConfig) (*repository.WebhookConfig, error)); ok { + return rf(webhookConfig) + } + if rf, ok := ret.Get(0).(func(*repository.WebhookConfig) *repository.WebhookConfig); ok { + r0 = rf(webhookConfig) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*repository.WebhookConfig) + } + } + + if rf, ok := ret.Get(1).(func(*repository.WebhookConfig) error); ok { + r1 = rf(webhookConfig) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewWebhookNotificationRepository interface { + mock.TestingT + Cleanup(func()) +} + +// NewWebhookNotificationRepository creates a new instance of WebhookNotificationRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewWebhookNotificationRepository(t mockConstructorTestingTNewWebhookNotificationRepository) *WebhookNotificationRepository { + mock := &WebhookNotificationRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/notifier/NotificationConfigService.go b/pkg/notifier/NotificationConfigService.go index 20e50fc574..c78b3184f3 100644 --- a/pkg/notifier/NotificationConfigService.go +++ b/pkg/notifier/NotificationConfigService.go @@ -51,6 +51,7 @@ type NotificationConfigServiceImpl struct { ciPipelineRepository pipelineConfig.CiPipelineRepository pipelineRepository pipelineConfig.PipelineRepository slackRepository repository.SlackNotificationRepository + webhookRepository repository.WebhookNotificationRepository sesRepository repository.SESNotificationRepository smtpRepository repository.SMTPNotificationRepository teamRepository repository2.TeamRepository @@ -163,7 +164,7 @@ type ProvidersConfig struct { } func NewNotificationConfigServiceImpl(logger *zap.SugaredLogger, notificationSettingsRepository repository.NotificationSettingsRepository, notificationConfigBuilder NotificationConfigBuilder, ciPipelineRepository pipelineConfig.CiPipelineRepository, - pipelineRepository pipelineConfig.PipelineRepository, slackRepository repository.SlackNotificationRepository, + pipelineRepository pipelineConfig.PipelineRepository, slackRepository repository.SlackNotificationRepository, webhookRepository repository.WebhookNotificationRepository, sesRepository repository.SESNotificationRepository, smtpRepository repository.SMTPNotificationRepository, teamRepository repository2.TeamRepository, environmentRepository repository3.EnvironmentRepository, appRepository app.AppRepository, @@ -176,6 +177,7 @@ func NewNotificationConfigServiceImpl(logger *zap.SugaredLogger, notificationSet ciPipelineRepository: ciPipelineRepository, sesRepository: sesRepository, slackRepository: slackRepository, + webhookRepository: webhookRepository, smtpRepository: smtpRepository, teamRepository: teamRepository, environmentRepository: environmentRepository, @@ -365,6 +367,7 @@ func (impl *NotificationConfigServiceImpl) BuildNotificationSettingsResponse(not if config.Providers != nil && len(config.Providers) > 0 { var slackIds []*int + var webhookIds []*int var sesUserIds []int32 var smtpUserIds []int32 var providerConfigs []*ProvidersConfig @@ -377,6 +380,8 @@ func (impl *NotificationConfigServiceImpl) BuildNotificationSettingsResponse(not sesUserIds = append(sesUserIds, int32(item.ConfigId)) } else if item.Destination == util.SMTP { smtpUserIds = append(smtpUserIds, int32(item.ConfigId)) + } else if item.Destination == util.Webhook { + webhookIds = append(webhookIds, &item.ConfigId) } } else { providerConfigs = append(providerConfigs, &ProvidersConfig{Dest: string(item.Destination), Recipient: item.Recipient}) @@ -392,6 +397,16 @@ func (impl *NotificationConfigServiceImpl) BuildNotificationSettingsResponse(not providerConfigs = append(providerConfigs, &ProvidersConfig{Id: item.Id, ConfigName: item.ConfigName, Dest: string(util.Slack)}) } } + if len(webhookIds) > 0 { + webhookConfigs, err := impl.webhookRepository.FindByIds(webhookIds) + if err != nil && err != pg.ErrNoRows { + impl.logger.Errorw("error in fetching webhook config", "err", err) + return notificationSettingsResponses, deletedItemCount, err + } + for _, item := range webhookConfigs { + providerConfigs = append(providerConfigs, &ProvidersConfig{Id: item.Id, ConfigName: item.ConfigName, Dest: string(util.Webhook)}) + } + } if len(sesUserIds) > 0 { sesConfigs, err := impl.userRepository.GetByIds(sesUserIds) diff --git a/pkg/notifier/SlackNotificationService.go b/pkg/notifier/SlackNotificationService.go index db2c49d882..d891e98dcb 100644 --- a/pkg/notifier/SlackNotificationService.go +++ b/pkg/notifier/SlackNotificationService.go @@ -33,6 +33,8 @@ import ( ) const SLACK_CONFIG_TYPE = "slack" +const SLACK_URL = "https://hooks.slack.com/" +const WEBHOOK_URL = "https://" type SlackNotificationService interface { SaveOrEditNotificationConfig(channelReq []SlackConfigDto, userId int32) ([]int, error) @@ -47,6 +49,7 @@ type SlackNotificationServiceImpl struct { logger *zap.SugaredLogger teamService team.TeamService slackRepository repository.SlackNotificationRepository + webhookRepository repository.WebhookNotificationRepository userRepository repository2.UserRepository notificationSettingsRepository repository.NotificationSettingsRepository } @@ -65,12 +68,13 @@ type SlackConfigDto struct { Id int `json:"id" validate:"number"` } -func NewSlackNotificationServiceImpl(logger *zap.SugaredLogger, slackRepository repository.SlackNotificationRepository, teamService team.TeamService, +func NewSlackNotificationServiceImpl(logger *zap.SugaredLogger, slackRepository repository.SlackNotificationRepository, webhookRepository repository.WebhookNotificationRepository, teamService team.TeamService, userRepository repository2.UserRepository, notificationSettingsRepository repository.NotificationSettingsRepository) *SlackNotificationServiceImpl { return &SlackNotificationServiceImpl{ logger: logger, teamService: teamService, slackRepository: slackRepository, + webhookRepository: webhookRepository, userRepository: userRepository, notificationSettingsRepository: notificationSettingsRepository, } @@ -202,19 +206,31 @@ func (impl *SlackNotificationServiceImpl) buildConfigUpdateModel(slackConfig *re func (impl *SlackNotificationServiceImpl) RecipientListingSuggestion(value string) ([]*NotificationRecipientListingResponse, error) { var results []*NotificationRecipientListingResponse - sesConfigs, err := impl.slackRepository.FindByName(value) + slackConfigs, err := impl.slackRepository.FindByName(value) if err != nil && !util.IsErrNoRows(err) { impl.logger.Errorw("cannot find all slack config", "err", err) return []*NotificationRecipientListingResponse{}, err } - for _, sesConfig := range sesConfigs { + for _, slackConfig := range slackConfigs { result := &NotificationRecipientListingResponse{ - ConfigId: sesConfig.Id, - Recipient: sesConfig.ConfigName, + ConfigId: slackConfig.Id, + Recipient: slackConfig.ConfigName, Dest: util2.Slack} results = append(results, result) } + webhookConfigs, err := impl.webhookRepository.FindByName(value) + if err != nil && !util.IsErrNoRows(err) { + impl.logger.Errorw("cannot find all webhook config", "err", err) + return []*NotificationRecipientListingResponse{}, err + } + for _, webhookConfig := range webhookConfigs { + result := &NotificationRecipientListingResponse{ + ConfigId: webhookConfig.Id, + Recipient: webhookConfig.ConfigName, + Dest: util2.Webhook} + results = append(results, result) + } userList, err := impl.userRepository.FetchUserMatchesByEmailIdExcludingApiTokenUser(value) if err != nil && !util.IsErrNoRows(err) { impl.logger.Errorw("cannot find all slack config", "err", err) @@ -250,8 +266,10 @@ func (impl *SlackNotificationServiceImpl) RecipientListingSuggestion(value strin result := &NotificationRecipientListingResponse{ Recipient: v.(string), } - if strings.Contains(v.(string), "https://") { + if strings.Contains(v.(string), SLACK_URL) { result.Dest = util2.Slack + } else if strings.Contains(v.(string), WEBHOOK_URL) { + result.Dest = util2.Webhook } else { result.Dest = util2.SES } diff --git a/pkg/notifier/WebhookNotificationService.go b/pkg/notifier/WebhookNotificationService.go new file mode 100644 index 0000000000..7c6b0066d4 --- /dev/null +++ b/pkg/notifier/WebhookNotificationService.go @@ -0,0 +1,236 @@ +package notifier + +import ( + "fmt" + "github.com/devtron-labs/devtron/internal/sql/repository" + "github.com/devtron-labs/devtron/internal/util" + "github.com/devtron-labs/devtron/pkg/sql" + "github.com/devtron-labs/devtron/pkg/team" + repository2 "github.com/devtron-labs/devtron/pkg/user/repository" + util2 "github.com/devtron-labs/devtron/util/event" + "github.com/go-pg/pg" + "go.uber.org/zap" + "time" +) + +const WEBHOOK_CONFIG_TYPE = "webhook" + +type WebhookVariable string + +const ( + // these fields will be configurable in future + DevtronContainerImageTag WebhookVariable = "{{devtronContainerImageTag}}" + DevtronAppName WebhookVariable = "{{devtronAppName}}" + DevtronAppId WebhookVariable = "{{devtronAppId}}" + DevtronEnvName WebhookVariable = "{{devtronEnvName}}" + DevtronEnvId WebhookVariable = "{{devtronEnvId}}" + DevtronCiPipelineId WebhookVariable = "{{devtronCiPipelineId}}" + DevtronCdPipelineId WebhookVariable = "{{devtronCdPipelineId}}" + DevtronTriggeredByEmail WebhookVariable = "{{devtronTriggeredByEmail}}" + EventType WebhookVariable = "{{eventType}}" +) + +type WebhookNotificationService interface { + SaveOrEditNotificationConfig(channelReq []WebhookConfigDto, userId int32) ([]int, error) + FetchWebhookNotificationConfigById(id int) (*WebhookConfigDto, error) + GetWebhookVariables() (map[string]WebhookVariable, error) + FetchAllWebhookNotificationConfig() ([]*WebhookConfigDto, error) + FetchAllWebhookNotificationConfigAutocomplete() ([]*NotificationChannelAutoResponse, error) + DeleteNotificationConfig(deleteReq *WebhookConfigDto, userId int32) error +} + +type WebhookNotificationServiceImpl struct { + logger *zap.SugaredLogger + webhookRepository repository.WebhookNotificationRepository + teamService team.TeamService + userRepository repository2.UserRepository + notificationSettingsRepository repository.NotificationSettingsRepository +} +type WebhookChannelConfig struct { + Channel util2.Channel `json:"channel" validate:"required"` + WebhookConfigDtos *[]WebhookConfigDto `json:"configs"` +} + +type WebhookConfigDto struct { + OwnerId int32 `json:"userId" validate:"number"` + WebhookUrl string `json:"webhookUrl" validate:"required"` + ConfigName string `json:"configName" validate:"required"` + Header map[string]interface{} `json:"header"` + Payload map[string]interface{} `json:"payload"` + Description string `json:"description"` + Id int `json:"id" validate:"number"` +} + +func NewWebhookNotificationServiceImpl(logger *zap.SugaredLogger, webhookRepository repository.WebhookNotificationRepository, teamService team.TeamService, + userRepository repository2.UserRepository, notificationSettingsRepository repository.NotificationSettingsRepository) *WebhookNotificationServiceImpl { + return &WebhookNotificationServiceImpl{ + logger: logger, + webhookRepository: webhookRepository, + teamService: teamService, + userRepository: userRepository, + notificationSettingsRepository: notificationSettingsRepository, + } +} + +func (impl *WebhookNotificationServiceImpl) SaveOrEditNotificationConfig(channelReq []WebhookConfigDto, userId int32) ([]int, error) { + var responseIds []int + webhookConfigs := buildWebhookNewConfigs(channelReq, userId) + for _, config := range webhookConfigs { + if config.Id != 0 { + model, err := impl.webhookRepository.FindOne(config.Id) + if err != nil && !util.IsErrNoRows(err) { + impl.logger.Errorw("err while fetching webhook config", "err", err) + return []int{}, err + } + impl.buildConfigUpdateModel(config, model, userId) + model, uErr := impl.webhookRepository.UpdateWebhookConfig(model) + if uErr != nil { + impl.logger.Errorw("err while updating webhook config", "err", err) + return []int{}, uErr + } + } else { + _, iErr := impl.webhookRepository.SaveWebhookConfig(config) + if iErr != nil { + impl.logger.Errorw("err while inserting webhook config", "err", iErr) + return []int{}, iErr + } + + } + responseIds = append(responseIds, config.Id) + } + return responseIds, nil +} + +func (impl *WebhookNotificationServiceImpl) FetchWebhookNotificationConfigById(id int) (*WebhookConfigDto, error) { + webhookConfig, err := impl.webhookRepository.FindOne(id) + if err != nil && !util.IsErrNoRows(err) { + impl.logger.Errorw("cannot find all webhoook config", "err", err) + return nil, err + } + webhoookConfigDto := impl.adaptWebhookConfig(*webhookConfig) + return &webhoookConfigDto, nil +} + +func (impl *WebhookNotificationServiceImpl) GetWebhookVariables() (map[string]WebhookVariable, error) { + variables := map[string]WebhookVariable{ + "devtronContainerImageTag": DevtronContainerImageTag, + "devtronAppName": DevtronAppName, + "devtronAppId": DevtronAppId, + "devtronEnvName": DevtronEnvName, + "devtronEnvId": DevtronEnvId, + "devtronCiPipelineId": DevtronCiPipelineId, + "devtronCdPipelineId": DevtronCdPipelineId, + "devtronTriggeredByEmail": DevtronTriggeredByEmail, + "eventType": EventType, + } + + return variables, nil +} + +func (impl *WebhookNotificationServiceImpl) FetchAllWebhookNotificationConfig() ([]*WebhookConfigDto, error) { + var responseDto []*WebhookConfigDto + webhookConfigs, err := impl.webhookRepository.FindAll() + if err != nil && !util.IsErrNoRows(err) { + impl.logger.Errorw("cannot find all webhoook config", "err", err) + return []*WebhookConfigDto{}, err + } + for _, webhookConfig := range webhookConfigs { + webhookConfigDto := impl.adaptWebhookConfig(webhookConfig) + responseDto = append(responseDto, &webhookConfigDto) + } + if responseDto == nil { + responseDto = make([]*WebhookConfigDto, 0) + } + return responseDto, nil +} + +func (impl *WebhookNotificationServiceImpl) FetchAllWebhookNotificationConfigAutocomplete() ([]*NotificationChannelAutoResponse, error) { + var responseDto []*NotificationChannelAutoResponse + webhookConfigs, err := impl.webhookRepository.FindAll() + if err != nil && !util.IsErrNoRows(err) { + impl.logger.Errorw("cannot find all webhoook config", "err", err) + return []*NotificationChannelAutoResponse{}, err + } + for _, webhookConfig := range webhookConfigs { + webhookConfigDto := &NotificationChannelAutoResponse{ + Id: webhookConfig.Id, + ConfigName: webhookConfig.ConfigName, + } + responseDto = append(responseDto, webhookConfigDto) + } + return responseDto, nil +} + +func (impl *WebhookNotificationServiceImpl) adaptWebhookConfig(webhookConfig repository.WebhookConfig) WebhookConfigDto { + webhookConfigDto := WebhookConfigDto{ + OwnerId: webhookConfig.OwnerId, + WebhookUrl: webhookConfig.WebHookUrl, + ConfigName: webhookConfig.ConfigName, + Header: webhookConfig.Header, + Payload: webhookConfig.Payload, + Description: webhookConfig.Description, + Id: webhookConfig.Id, + } + return webhookConfigDto +} + +func buildWebhookNewConfigs(webhookReq []WebhookConfigDto, userId int32) []*repository.WebhookConfig { + var webhookConfigs []*repository.WebhookConfig + for _, c := range webhookReq { + webhookConfig := &repository.WebhookConfig{ + Id: c.Id, + ConfigName: c.ConfigName, + WebHookUrl: c.WebhookUrl, + Header: c.Header, + Payload: c.Payload, + Description: c.Description, + AuditLog: sql.AuditLog{ + CreatedBy: userId, + CreatedOn: time.Now(), + UpdatedOn: time.Now(), + UpdatedBy: userId, + }, + } + webhookConfig.OwnerId = userId + webhookConfigs = append(webhookConfigs, webhookConfig) + } + return webhookConfigs +} + +func (impl *WebhookNotificationServiceImpl) buildConfigUpdateModel(webhookConfig *repository.WebhookConfig, model *repository.WebhookConfig, userId int32) { + model.WebHookUrl = webhookConfig.WebHookUrl + model.ConfigName = webhookConfig.ConfigName + model.Description = webhookConfig.Description + model.Payload = webhookConfig.Payload + model.Header = webhookConfig.Header + model.OwnerId = webhookConfig.OwnerId + model.UpdatedOn = time.Now() + model.UpdatedBy = userId +} + +func (impl *WebhookNotificationServiceImpl) DeleteNotificationConfig(deleteReq *WebhookConfigDto, userId int32) error { + existingConfig, err := impl.webhookRepository.FindOne(deleteReq.Id) + if err != nil { + impl.logger.Errorw("No matching entry found for delete", "err", err, "id", deleteReq.Id) + return err + } + notifications, err := impl.notificationSettingsRepository.FindNotificationSettingsByConfigIdAndConfigType(deleteReq.Id, WEBHOOK_CONFIG_TYPE) + if err != nil && err != pg.ErrNoRows { + impl.logger.Errorw("error in deleting webhook config", "config", deleteReq) + return err + } + if len(notifications) > 0 { + impl.logger.Errorw("found notifications using this config, cannot delete", "config", deleteReq) + return fmt.Errorf(" Please delete all notifications using this config before deleting") + } + + existingConfig.UpdatedOn = time.Now() + existingConfig.UpdatedBy = userId + //deleting webhook config + err = impl.webhookRepository.MarkWebhookConfigDeleted(existingConfig) + if err != nil { + impl.logger.Errorw("error in deleting webhook config", "err", err, "id", existingConfig.Id) + return err + } + return nil +} diff --git a/pkg/notifier/WebhookNotificationService_test.go b/pkg/notifier/WebhookNotificationService_test.go new file mode 100644 index 0000000000..4504b67ad5 --- /dev/null +++ b/pkg/notifier/WebhookNotificationService_test.go @@ -0,0 +1,143 @@ +package notifier + +import ( + "fmt" + "github.com/devtron-labs/devtron/internal/sql/repository" + mocks2 "github.com/devtron-labs/devtron/internal/sql/repository/mocks" + util2 "github.com/devtron-labs/devtron/internal/util" + "github.com/devtron-labs/devtron/pkg/team/mocks" + "github.com/stretchr/testify/mock" + + //"github.com/devtron-labs/devtron/pkg/user/repository" + mocks3 "github.com/devtron-labs/devtron/pkg/user/repository/mocks" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_buildWebhookNewConfigs(t *testing.T) { + type args struct { + webhookReq []WebhookConfigDto + userId int32 + } + tests := []struct { + name string + args args + want []*repository.WebhookConfig + }{ + { + name: "test1", + args: args{ + webhookReq: []WebhookConfigDto{ + { + WebhookUrl: "dfcd nmc dc", + ConfigName: "aditya", + Payload: map[string]interface{}{"text": "final"}, + Header: map[string]interface{}{"Content-type": "application/json"}, + }, + }, + userId: 1, + }, + want: []*repository.WebhookConfig{ + { + WebHookUrl: "dfcd nmc dc", + ConfigName: "aditya", + Payload: map[string]interface{}{"text": "final"}, + Header: map[string]interface{}{"Content-type": "application/json"}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := buildWebhookNewConfigs(tt.args.webhookReq, tt.args.userId) + + assert.Equal(t, len(tt.want), len(got), "Number of webhook configs mismatch") + + for i, want := range tt.want { + assert.Equal(t, want.WebHookUrl, got[i].WebHookUrl, "WebHookUrl mismatch") + assert.Equal(t, want.ConfigName, got[i].ConfigName, "ConfigName mismatch") + assert.Equal(t, want.Payload, got[i].Payload, "Payload mismatch") + assert.Equal(t, want.Header, got[i].Header, "Header mismatch") + + } + }) + } +} + +func TestWebhookNotificationServiceImpl_SaveOrEditNotificationConfig(t *testing.T) { + sugaredLogger, err := util2.NewSugardLogger() + assert.Nil(t, err) + mockedTeamService := mocks.NewTeamService(t) + mockedWebhookNotfRep := mocks2.NewWebhookNotificationRepository(t) + mockedUserRepo := mocks3.NewUserRepository(t) + mockedNotfSetRepo := mocks2.NewNotificationSettingsRepository(t) + + type args struct { + channelReq []WebhookConfigDto + userId int32 + } + + tests := []struct { + name string + args args + want []int + wantErr assert.ErrorAssertionFunc + }{ + { + name: "SaveOrUpdate_ExistingConfig", + args: args{ + channelReq: []WebhookConfigDto{ + { + WebhookUrl: "djfndgfbd,gds", + ConfigName: "aditya", + Payload: map[string]interface{}{"text": "final"}, + Header: map[string]interface{}{"Content-type": "application/json"}, + }, + }, + userId: 2, + }, + want: []int{0}, + wantErr: assert.NoError, + }, + { + name: "SaveOrUpdate_NewConfig", + args: args{ + channelReq: []WebhookConfigDto{ + { + WebhookUrl: "d,fm sdfd", + ConfigName: "aditya", + Payload: map[string]interface{}{"text": "final"}, + Header: map[string]interface{}{"Content-type": "application/json"}, + }, + }, + userId: 2, + }, + want: []int{0}, + wantErr: assert.NoError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + impl := &WebhookNotificationServiceImpl{ + logger: sugaredLogger, + webhookRepository: mockedWebhookNotfRep, + teamService: mockedTeamService, + userRepository: mockedUserRepo, + notificationSettingsRepository: mockedNotfSetRepo, + } + + mockConfig := &repository.WebhookConfig{Id: 1} + mockError := error(nil) + mockedWebhookNotfRep.On("SaveWebhookConfig", mock.Anything).Return(mockConfig, mockError) + + got, err := impl.SaveOrEditNotificationConfig(tt.args.channelReq, tt.args.userId) + + if !tt.wantErr(t, err, fmt.Sprintf("SaveOrEditNotificationConfig(%v, %v)", tt.args.channelReq, tt.args.userId)) { + return + } + assert.Equalf(t, tt.want, got, "SaveOrEditNotificationConfig(%v, %v)", tt.args.channelReq, tt.args.userId) + }) + } +} diff --git a/pkg/team/mocks/TeamService.go b/pkg/team/mocks/TeamService.go new file mode 100644 index 0000000000..1c72cb8edb --- /dev/null +++ b/pkg/team/mocks/TeamService.go @@ -0,0 +1,224 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + team "github.com/devtron-labs/devtron/pkg/team" + mock "github.com/stretchr/testify/mock" +) + +// TeamService is an autogenerated mock type for the TeamService type +type TeamService struct { + mock.Mock +} + +// Create provides a mock function with given fields: request +func (_m *TeamService) Create(request *team.TeamRequest) (*team.TeamRequest, error) { + ret := _m.Called(request) + + var r0 *team.TeamRequest + var r1 error + if rf, ok := ret.Get(0).(func(*team.TeamRequest) (*team.TeamRequest, error)); ok { + return rf(request) + } + if rf, ok := ret.Get(0).(func(*team.TeamRequest) *team.TeamRequest); ok { + r0 = rf(request) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*team.TeamRequest) + } + } + + if rf, ok := ret.Get(1).(func(*team.TeamRequest) error); ok { + r1 = rf(request) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: request +func (_m *TeamService) Delete(request *team.TeamRequest) error { + ret := _m.Called(request) + + var r0 error + if rf, ok := ret.Get(0).(func(*team.TeamRequest) error); ok { + r0 = rf(request) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// FetchAllActive provides a mock function with given fields: +func (_m *TeamService) FetchAllActive() ([]team.TeamRequest, error) { + ret := _m.Called() + + var r0 []team.TeamRequest + var r1 error + if rf, ok := ret.Get(0).(func() ([]team.TeamRequest, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() []team.TeamRequest); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]team.TeamRequest) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FetchForAutocomplete provides a mock function with given fields: +func (_m *TeamService) FetchForAutocomplete() ([]team.TeamRequest, error) { + ret := _m.Called() + + var r0 []team.TeamRequest + var r1 error + if rf, ok := ret.Get(0).(func() ([]team.TeamRequest, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() []team.TeamRequest); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]team.TeamRequest) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FetchOne provides a mock function with given fields: id +func (_m *TeamService) FetchOne(id int) (*team.TeamRequest, error) { + ret := _m.Called(id) + + var r0 *team.TeamRequest + var r1 error + if rf, ok := ret.Get(0).(func(int) (*team.TeamRequest, error)); ok { + return rf(id) + } + if rf, ok := ret.Get(0).(func(int) *team.TeamRequest); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*team.TeamRequest) + } + } + + if rf, ok := ret.Get(1).(func(int) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FindByIds provides a mock function with given fields: ids +func (_m *TeamService) FindByIds(ids []*int) ([]*team.TeamBean, error) { + ret := _m.Called(ids) + + var r0 []*team.TeamBean + var r1 error + if rf, ok := ret.Get(0).(func([]*int) ([]*team.TeamBean, error)); ok { + return rf(ids) + } + if rf, ok := ret.Get(0).(func([]*int) []*team.TeamBean); ok { + r0 = rf(ids) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*team.TeamBean) + } + } + + if rf, ok := ret.Get(1).(func([]*int) error); ok { + r1 = rf(ids) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FindByTeamName provides a mock function with given fields: teamName +func (_m *TeamService) FindByTeamName(teamName string) (*team.TeamRequest, error) { + ret := _m.Called(teamName) + + var r0 *team.TeamRequest + var r1 error + if rf, ok := ret.Get(0).(func(string) (*team.TeamRequest, error)); ok { + return rf(teamName) + } + if rf, ok := ret.Get(0).(func(string) *team.TeamRequest); ok { + r0 = rf(teamName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*team.TeamRequest) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(teamName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Update provides a mock function with given fields: request +func (_m *TeamService) Update(request *team.TeamRequest) (*team.TeamRequest, error) { + ret := _m.Called(request) + + var r0 *team.TeamRequest + var r1 error + if rf, ok := ret.Get(0).(func(*team.TeamRequest) (*team.TeamRequest, error)); ok { + return rf(request) + } + if rf, ok := ret.Get(0).(func(*team.TeamRequest) *team.TeamRequest); ok { + r0 = rf(request) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*team.TeamRequest) + } + } + + if rf, ok := ret.Get(1).(func(*team.TeamRequest) error); ok { + r1 = rf(request) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewTeamService interface { + mock.TestingT + Cleanup(func()) +} + +// NewTeamService creates a new instance of TeamService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewTeamService(t mockConstructorTestingTNewTeamService) *TeamService { + mock := &TeamService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/user/repository/mocks/UserRepository.go b/pkg/user/repository/mocks/UserRepository.go new file mode 100644 index 0000000000..326e421ba0 --- /dev/null +++ b/pkg/user/repository/mocks/UserRepository.go @@ -0,0 +1,330 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + bean "github.com/devtron-labs/devtron/api/bean" + mock "github.com/stretchr/testify/mock" + + pg "github.com/go-pg/pg" + + repository "github.com/devtron-labs/devtron/pkg/user/repository" +) + +// UserRepository is an autogenerated mock type for the UserRepository type +type UserRepository struct { + mock.Mock +} + +// CreateUser provides a mock function with given fields: userModel, tx +func (_m *UserRepository) CreateUser(userModel *repository.UserModel, tx *pg.Tx) (*repository.UserModel, error) { + ret := _m.Called(userModel, tx) + + var r0 *repository.UserModel + var r1 error + if rf, ok := ret.Get(0).(func(*repository.UserModel, *pg.Tx) (*repository.UserModel, error)); ok { + return rf(userModel, tx) + } + if rf, ok := ret.Get(0).(func(*repository.UserModel, *pg.Tx) *repository.UserModel); ok { + r0 = rf(userModel, tx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*repository.UserModel) + } + } + + if rf, ok := ret.Get(1).(func(*repository.UserModel, *pg.Tx) error); ok { + r1 = rf(userModel, tx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FetchActiveOrDeletedUserByEmail provides a mock function with given fields: email +func (_m *UserRepository) FetchActiveOrDeletedUserByEmail(email string) (*repository.UserModel, error) { + ret := _m.Called(email) + + var r0 *repository.UserModel + var r1 error + if rf, ok := ret.Get(0).(func(string) (*repository.UserModel, error)); ok { + return rf(email) + } + if rf, ok := ret.Get(0).(func(string) *repository.UserModel); ok { + r0 = rf(email) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*repository.UserModel) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(email) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FetchActiveUserByEmail provides a mock function with given fields: email +func (_m *UserRepository) FetchActiveUserByEmail(email string) (bean.UserInfo, error) { + ret := _m.Called(email) + + var r0 bean.UserInfo + var r1 error + if rf, ok := ret.Get(0).(func(string) (bean.UserInfo, error)); ok { + return rf(email) + } + if rf, ok := ret.Get(0).(func(string) bean.UserInfo); ok { + r0 = rf(email) + } else { + r0 = ret.Get(0).(bean.UserInfo) + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(email) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FetchUserDetailByEmail provides a mock function with given fields: email +func (_m *UserRepository) FetchUserDetailByEmail(email string) (bean.UserInfo, error) { + ret := _m.Called(email) + + var r0 bean.UserInfo + var r1 error + if rf, ok := ret.Get(0).(func(string) (bean.UserInfo, error)); ok { + return rf(email) + } + if rf, ok := ret.Get(0).(func(string) bean.UserInfo); ok { + r0 = rf(email) + } else { + r0 = ret.Get(0).(bean.UserInfo) + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(email) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FetchUserMatchesByEmailIdExcludingApiTokenUser provides a mock function with given fields: email +func (_m *UserRepository) FetchUserMatchesByEmailIdExcludingApiTokenUser(email string) ([]repository.UserModel, error) { + ret := _m.Called(email) + + var r0 []repository.UserModel + var r1 error + if rf, ok := ret.Get(0).(func(string) ([]repository.UserModel, error)); ok { + return rf(email) + } + if rf, ok := ret.Get(0).(func(string) []repository.UserModel); ok { + r0 = rf(email) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]repository.UserModel) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(email) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetAllExcludingApiTokenUser provides a mock function with given fields: +func (_m *UserRepository) GetAllExcludingApiTokenUser() ([]repository.UserModel, error) { + ret := _m.Called() + + var r0 []repository.UserModel + var r1 error + if rf, ok := ret.Get(0).(func() ([]repository.UserModel, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() []repository.UserModel); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]repository.UserModel) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetById provides a mock function with given fields: id +func (_m *UserRepository) GetById(id int32) (*repository.UserModel, error) { + ret := _m.Called(id) + + var r0 *repository.UserModel + var r1 error + if rf, ok := ret.Get(0).(func(int32) (*repository.UserModel, error)); ok { + return rf(id) + } + if rf, ok := ret.Get(0).(func(int32) *repository.UserModel); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*repository.UserModel) + } + } + + if rf, ok := ret.Get(1).(func(int32) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetByIdIncludeDeleted provides a mock function with given fields: id +func (_m *UserRepository) GetByIdIncludeDeleted(id int32) (*repository.UserModel, error) { + ret := _m.Called(id) + + var r0 *repository.UserModel + var r1 error + if rf, ok := ret.Get(0).(func(int32) (*repository.UserModel, error)); ok { + return rf(id) + } + if rf, ok := ret.Get(0).(func(int32) *repository.UserModel); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*repository.UserModel) + } + } + + if rf, ok := ret.Get(1).(func(int32) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetByIds provides a mock function with given fields: ids +func (_m *UserRepository) GetByIds(ids []int32) ([]repository.UserModel, error) { + ret := _m.Called(ids) + + var r0 []repository.UserModel + var r1 error + if rf, ok := ret.Get(0).(func([]int32) ([]repository.UserModel, error)); ok { + return rf(ids) + } + if rf, ok := ret.Get(0).(func([]int32) []repository.UserModel); ok { + r0 = rf(ids) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]repository.UserModel) + } + } + + if rf, ok := ret.Get(1).(func([]int32) error); ok { + r1 = rf(ids) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetConnection provides a mock function with given fields: +func (_m *UserRepository) GetConnection() *pg.DB { + ret := _m.Called() + + var r0 *pg.DB + if rf, ok := ret.Get(0).(func() *pg.DB); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*pg.DB) + } + } + + return r0 +} + +// UpdateRoleIdForUserRolesMappings provides a mock function with given fields: roleId, newRoleId +func (_m *UserRepository) UpdateRoleIdForUserRolesMappings(roleId int, newRoleId int) (*repository.UserRoleModel, error) { + ret := _m.Called(roleId, newRoleId) + + var r0 *repository.UserRoleModel + var r1 error + if rf, ok := ret.Get(0).(func(int, int) (*repository.UserRoleModel, error)); ok { + return rf(roleId, newRoleId) + } + if rf, ok := ret.Get(0).(func(int, int) *repository.UserRoleModel); ok { + r0 = rf(roleId, newRoleId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*repository.UserRoleModel) + } + } + + if rf, ok := ret.Get(1).(func(int, int) error); ok { + r1 = rf(roleId, newRoleId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateUser provides a mock function with given fields: userModel, tx +func (_m *UserRepository) UpdateUser(userModel *repository.UserModel, tx *pg.Tx) (*repository.UserModel, error) { + ret := _m.Called(userModel, tx) + + var r0 *repository.UserModel + var r1 error + if rf, ok := ret.Get(0).(func(*repository.UserModel, *pg.Tx) (*repository.UserModel, error)); ok { + return rf(userModel, tx) + } + if rf, ok := ret.Get(0).(func(*repository.UserModel, *pg.Tx) *repository.UserModel); ok { + r0 = rf(userModel, tx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*repository.UserModel) + } + } + + if rf, ok := ret.Get(1).(func(*repository.UserModel, *pg.Tx) error); ok { + r1 = rf(userModel, tx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewUserRepository interface { + mock.TestingT + Cleanup(func()) +} + +// NewUserRepository creates a new instance of UserRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewUserRepository(t mockConstructorTestingTNewUserRepository) *UserRepository { + mock := &UserRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/scripts/sql/150_webhook_config_notification.down.sql b/scripts/sql/150_webhook_config_notification.down.sql new file mode 100644 index 0000000000..fe8c0c0d70 --- /dev/null +++ b/scripts/sql/150_webhook_config_notification.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS "public"."webhook_config"; +DROP SEQUENCE IF EXISTS public.id_seq_webhook_config; \ No newline at end of file diff --git a/scripts/sql/150_webhook_config_notification.up.sql b/scripts/sql/150_webhook_config_notification.up.sql new file mode 100644 index 0000000000..5dccbad8ee --- /dev/null +++ b/scripts/sql/150_webhook_config_notification.up.sql @@ -0,0 +1,18 @@ +CREATE SEQUENCE IF NOT EXISTS id_seq_webhook_config; + +CREATE TABLE "public"."webhook_config" ( + "id" integer NOT NULL DEFAULT nextval('id_seq_webhook_config'::regclass), + "web_hook_url" VARCHAR(100), + "config_name" VARCHAR(100), + "header" jsonb, + "payload" jsonb, + "description" text, + "owner_id" integer, + "active" bool, + "deleted" bool NOT NULL DEFAULT FALSE, + "created_on" timestamptz, + "created_by" int4, + "updated_on" timestamptz, + "updated_by" int4, + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/util/event/Event.go b/util/event/Event.go index 8e8a0bb167..e8f83ede90 100644 --- a/util/event/Event.go +++ b/util/event/Event.go @@ -33,9 +33,10 @@ type Level string type Channel string const ( - Slack Channel = "slack" - SES Channel = "ses" - SMTP Channel = "smtp" + Slack Channel = "slack" + SES Channel = "ses" + SMTP Channel = "smtp" + Webhook Channel = "webhook" ) type UpdateType string diff --git a/wire_gen.go b/wire_gen.go index c9ebcfb6d4..99d31c2a3b 100644 --- a/wire_gen.go +++ b/wire_gen.go @@ -537,13 +537,15 @@ func InitializeApp() (*App, error) { notificationSettingsRepositoryImpl := repository.NewNotificationSettingsRepositoryImpl(db) notificationConfigBuilderImpl := notifier.NewNotificationConfigBuilderImpl(sugaredLogger) slackNotificationRepositoryImpl := repository.NewSlackNotificationRepositoryImpl(db) + webhookNotificationRepositoryImpl := repository.NewWebhookNotificationRepositoryImpl(db) sesNotificationRepositoryImpl := repository.NewSESNotificationRepositoryImpl(db) smtpNotificationRepositoryImpl := repository.NewSMTPNotificationRepositoryImpl(db) - notificationConfigServiceImpl := notifier.NewNotificationConfigServiceImpl(sugaredLogger, notificationSettingsRepositoryImpl, notificationConfigBuilderImpl, ciPipelineRepositoryImpl, pipelineRepositoryImpl, slackNotificationRepositoryImpl, sesNotificationRepositoryImpl, smtpNotificationRepositoryImpl, teamRepositoryImpl, environmentRepositoryImpl, appRepositoryImpl, userRepositoryImpl, ciPipelineMaterialRepositoryImpl) - slackNotificationServiceImpl := notifier.NewSlackNotificationServiceImpl(sugaredLogger, slackNotificationRepositoryImpl, teamServiceImpl, userRepositoryImpl, notificationSettingsRepositoryImpl) + notificationConfigServiceImpl := notifier.NewNotificationConfigServiceImpl(sugaredLogger, notificationSettingsRepositoryImpl, notificationConfigBuilderImpl, ciPipelineRepositoryImpl, pipelineRepositoryImpl, slackNotificationRepositoryImpl, webhookNotificationRepositoryImpl, sesNotificationRepositoryImpl, smtpNotificationRepositoryImpl, teamRepositoryImpl, environmentRepositoryImpl, appRepositoryImpl, userRepositoryImpl, ciPipelineMaterialRepositoryImpl) + slackNotificationServiceImpl := notifier.NewSlackNotificationServiceImpl(sugaredLogger, slackNotificationRepositoryImpl, webhookNotificationRepositoryImpl, teamServiceImpl, userRepositoryImpl, notificationSettingsRepositoryImpl) + webhookNotificationServiceImpl := notifier.NewWebhookNotificationServiceImpl(sugaredLogger, webhookNotificationRepositoryImpl, teamServiceImpl, userRepositoryImpl, notificationSettingsRepositoryImpl) sesNotificationServiceImpl := notifier.NewSESNotificationServiceImpl(sugaredLogger, sesNotificationRepositoryImpl, teamServiceImpl, notificationSettingsRepositoryImpl) smtpNotificationServiceImpl := notifier.NewSMTPNotificationServiceImpl(sugaredLogger, smtpNotificationRepositoryImpl, teamServiceImpl, notificationSettingsRepositoryImpl) - notificationRestHandlerImpl := restHandler.NewNotificationRestHandlerImpl(dockerRegistryConfigImpl, sugaredLogger, gitRegistryConfigImpl, dbConfigServiceImpl, userServiceImpl, validate, notificationConfigServiceImpl, slackNotificationServiceImpl, sesNotificationServiceImpl, smtpNotificationServiceImpl, enforcerImpl, teamServiceImpl, environmentServiceImpl, pipelineBuilderImpl, enforcerUtilImpl) + notificationRestHandlerImpl := restHandler.NewNotificationRestHandlerImpl(dockerRegistryConfigImpl, sugaredLogger, gitRegistryConfigImpl, dbConfigServiceImpl, userServiceImpl, validate, notificationConfigServiceImpl, slackNotificationServiceImpl, webhookNotificationServiceImpl, sesNotificationServiceImpl, smtpNotificationServiceImpl, enforcerImpl, teamServiceImpl, environmentServiceImpl, pipelineBuilderImpl, enforcerUtilImpl) notificationRouterImpl := router.NewNotificationRouterImpl(notificationRestHandlerImpl) teamRestHandlerImpl := team2.NewTeamRestHandlerImpl(sugaredLogger, teamServiceImpl, userServiceImpl, enforcerImpl, validate, userAuthServiceImpl, deleteServiceExtendedImpl) teamRouterImpl := team2.NewTeamRouterImpl(teamRestHandlerImpl)