diff --git a/app/services/notification/branch_updated.go b/app/services/notification/branch_updated.go new file mode 100644 index 000000000..bc87a155f --- /dev/null +++ b/app/services/notification/branch_updated.go @@ -0,0 +1,96 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package notification + +import ( + "context" + "fmt" + + pullreqevents "github.com/harness/gitness/app/events/pullreq" + "github.com/harness/gitness/events" + "github.com/harness/gitness/types" +) + +type PullReqBranchUpdatedPayload struct { + Base *BasePullReqPayload + Committer *types.PrincipalInfo + NewSHA string +} + +func (s *Service) notifyPullReqBranchUpdated( + ctx context.Context, + event *events.Event[*pullreqevents.BranchUpdatedPayload], +) error { + payload, reviewers, err := s.processPullReqBranchUpdatedEvent(ctx, event) + if err != nil { + return fmt.Errorf( + "failed to process %s event for pullReqID %d: %w", + pullreqevents.BranchUpdatedEvent, + event.Payload.PullReqID, + err, + ) + } + + if len(reviewers) == 0 { + return nil + } + + err = s.notificationClient.SendPullReqBranchUpdated(ctx, reviewers, payload) + if err != nil { + return fmt.Errorf( + "failed to send notification for event %s for pullReqID %d: %w", + pullreqevents.BranchUpdatedEvent, + event.Payload.PullReqID, + err, + ) + } + + return nil +} + +func (s *Service) processPullReqBranchUpdatedEvent( + ctx context.Context, + event *events.Event[*pullreqevents.BranchUpdatedPayload], +) (*PullReqBranchUpdatedPayload, []*types.PrincipalInfo, error) { + base, err := s.getBasePayload(ctx, event.Payload.Base) + if err != nil { + return nil, nil, fmt.Errorf("failed to get base payload: %w", err) + } + + committer, err := s.principalInfoCache.Get(ctx, event.Payload.PrincipalID) + if err != nil { + return nil, nil, fmt.Errorf("failed to get principal info for %d: %w", event.Payload.PrincipalID, err) + } + + reviewers, err := s.pullReqReviewersStore.List(ctx, event.Payload.PullReqID) + if err != nil { + return nil, nil, + fmt.Errorf("failed to get reviewers for pull request %d: %w", event.Payload.PullReqID, err) + } + + reviewerPrincipals := make([]*types.PrincipalInfo, len(reviewers)) + for i, reviewer := range reviewers { + reviewerPrincipals[i], err = s.principalInfoCache.Get(ctx, reviewer.PrincipalID) + if err != nil { + return nil, nil, fmt.Errorf("failed to get principal info for %d: %w", reviewer.PrincipalID, err) + } + } + + return &PullReqBranchUpdatedPayload{ + Base: base, + NewSHA: event.Payload.NewSHA, + Committer: committer, + }, reviewerPrincipals, nil +} diff --git a/app/services/notification/client_interface.go b/app/services/notification/client_interface.go new file mode 100644 index 000000000..d1da72370 --- /dev/null +++ b/app/services/notification/client_interface.go @@ -0,0 +1,33 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package notification + +import ( + "context" + + "github.com/harness/gitness/types" +) + +// Client is an interface for sending notifications, such as emails, Slack messages etc. +// It is implemented by MailClient and in future we can have other implementations for other channels like Slack etc. +type Client interface { + SendCommentCreated(ctx context.Context, recipients []*types.PrincipalInfo, payload *CommentCreatedPayload) error + SendReviewerAdded(ctx context.Context, recipients []*types.PrincipalInfo, payload *ReviewerAddedPayload) error + SendPullReqBranchUpdated( + ctx context.Context, + recipients []*types.PrincipalInfo, + payload *PullReqBranchUpdatedPayload, + ) error +} diff --git a/app/services/notification/comment_created.go b/app/services/notification/comment_created.go new file mode 100644 index 000000000..1291ff7c2 --- /dev/null +++ b/app/services/notification/comment_created.go @@ -0,0 +1,85 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package notification + +import ( + "context" + "fmt" + + pullreqevents "github.com/harness/gitness/app/events/pullreq" + "github.com/harness/gitness/events" + "github.com/harness/gitness/types" +) + +type CommentCreatedPayload struct { + Base *BasePullReqPayload + Commenter *types.PrincipalInfo + Text string +} + +func (s *Service) notifyCommentCreated( + ctx context.Context, + event *events.Event[*pullreqevents.CommentCreatedPayload], +) error { + payload, recipients, err := s.processCommentCreatedEvent(ctx, event) + if err != nil { + return fmt.Errorf( + "failed to process %s event for pullReqID %d: %w", + pullreqevents.CommentCreatedEvent, + event.Payload.PullReqID, + err, + ) + } + + err = s.notificationClient.SendCommentCreated(ctx, recipients, payload) + if err != nil { + return fmt.Errorf( + "failed to send notification for event %s for pullReqID %d: %w", + pullreqevents.CommentCreatedEvent, + event.Payload.PullReqID, + err, + ) + } + return nil +} + +func (s *Service) processCommentCreatedEvent( + ctx context.Context, + event *events.Event[*pullreqevents.CommentCreatedPayload], +) (*CommentCreatedPayload, []*types.PrincipalInfo, error) { + base, err := s.getBasePayload(ctx, event.Payload.Base) + if err != nil { + return nil, nil, fmt.Errorf("failed to get base payload: %w", err) + } + + activity, err := s.pullReqActivityStore.Find(ctx, event.Payload.ActivityID) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch activity from pullReqActivityStore: %w", err) + } + + commenter, err := s.principalInfoView.Find(ctx, activity.CreatedBy) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch commenter from principalInfoView: %w", err) + } + recipients := []*types.PrincipalInfo{ + base.Author, + } + + return &CommentCreatedPayload{ + Base: base, + Commenter: commenter, + Text: activity.Text, + }, recipients, nil +} diff --git a/app/services/notification/mail_client.go b/app/services/notification/mail_client.go new file mode 100644 index 000000000..d7d754f4a --- /dev/null +++ b/app/services/notification/mail_client.go @@ -0,0 +1,149 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package notification + +import ( + "bytes" + "context" + "fmt" + + pullreqevents "github.com/harness/gitness/app/events/pullreq" + "github.com/harness/gitness/app/services/notification/mailer" + "github.com/harness/gitness/types" +) + +const ( + TemplateReviewerAdded = "reviewer_added.html" + TemplateCommentCreated = "comment_created.html" + TemplatePullReqBranchUpdated = "pullreq_branch_updated.html" +) + +type MailClient struct { + mailer.Mailer +} + +func NewMailClient(mailer mailer.Mailer) MailClient { + return MailClient{ + Mailer: mailer, + } +} + +func (m MailClient) SendCommentCreated( + ctx context.Context, + recipients []*types.PrincipalInfo, + payload *CommentCreatedPayload, +) error { + mailPayload, err := GenerateEmailFromPayload( + TemplateCommentCreated, + recipients, + payload.Base, + payload, + ) + if err != nil { + return fmt.Errorf("failed to generate mail requests after processing %s event: %w", + pullreqevents.CommentCreatedEvent, err) + } + + return m.Mailer.Send(ctx, *mailPayload) +} + +func (m MailClient) SendReviewerAdded( + ctx context.Context, + recipients []*types.PrincipalInfo, + payload *ReviewerAddedPayload, +) error { + reviewerAddedMail, err := GenerateEmailFromPayload( + TemplateReviewerAdded, + recipients, + payload.Base, + payload, + ) + if err != nil { + return fmt.Errorf("failed to generate mail requests after processing %s event: %w", + pullreqevents.ReviewerAddedEvent, err) + } + + return m.Mailer.Send(ctx, *reviewerAddedMail) +} + +func (m MailClient) SendPullReqBranchUpdated( + ctx context.Context, + recipients []*types.PrincipalInfo, + payload *PullReqBranchUpdatedPayload, +) error { + mailPayload, err := GenerateEmailFromPayload( + TemplatePullReqBranchUpdated, + recipients, + payload.Base, + payload, + ) + if err != nil { + return fmt.Errorf("failed to generate mail requests after processing %s event: %w", + pullreqevents.BranchUpdatedEvent, err) + } + + return m.Mailer.Send(ctx, *mailPayload) +} + +func GetSubjectPullRequest( + repoUID string, + prNum int64, + prTitle string, +) string { + return fmt.Sprintf(subjectPullReqEvent, repoUID, prTitle, prNum) +} + +func GetHTMLBody(templateName string, data interface{}) ([]byte, error) { + tmpl := htmlTemplates[templateName] + tmplOutput := bytes.Buffer{} + err := tmpl.Execute(&tmplOutput, data) + if err != nil { + return nil, fmt.Errorf("failed to execute template %s", templateName) + } + + return tmplOutput.Bytes(), nil +} + +func GenerateEmailFromPayload( + templateName string, + recipients []*types.PrincipalInfo, + base *BasePullReqPayload, + payload interface{}, +) (*mailer.Payload, error) { + subject := GetSubjectPullRequest(base.Repo.UID, base.PullReq.Number, + base.PullReq.Title) + + body, err := GetHTMLBody(templateName, payload) + if err != nil { + return nil, err + } + + var mail mailer.Payload + mail.Body = string(body) + mail.Subject = subject + mail.RepoRef = base.Repo.Path + + recipientEmails := RetrieveEmailsFromPrincipals(recipients) + mail.ToRecipients = recipientEmails + return &mail, nil +} + +func RetrieveEmailsFromPrincipals(principals []*types.PrincipalInfo) []string { + emails := make([]string, len(principals)) + for i, principal := range principals { + emails[i] = principal.Email + } + return emails +} diff --git a/app/services/mailer/mail_client.go b/app/services/notification/mailer/mail.go similarity index 85% rename from app/services/mailer/mail_client.go rename to app/services/notification/mailer/mail.go index 61ebcd6f9..f5358bc15 100644 --- a/app/services/mailer/mail_client.go +++ b/app/services/notification/mailer/mail.go @@ -15,12 +15,13 @@ package mailer import ( + "context" "crypto/tls" gomail "gopkg.in/mail.v2" ) -type Service struct { +type GoMailClient struct { dialer *gomail.Dialer fromMail string } @@ -32,18 +33,18 @@ func NewMailClient( fromMail string, password string, insecure bool, -) *Service { +) GoMailClient { d := gomail.NewDialer(host, port, username, password) d.TLSConfig = &tls.Config{InsecureSkipVerify: insecure} // #nosec G402 (insecure TLS configuration) - return &Service{ + return GoMailClient{ dialer: d, fromMail: fromMail, } } -func (c *Service) SendMail(mailRequest *MailRequest) error { - mail := mailRequest.ToGoMail() +func (c GoMailClient) Send(_ context.Context, mailPayload Payload) error { + mail := ToGoMail(mailPayload) mail.SetHeader("From", c.fromMail) return c.dialer.DialAndSend(mail) } diff --git a/app/services/mailer/mail.go b/app/services/notification/mailer/mail_interface.go similarity index 65% rename from app/services/mailer/mail.go rename to app/services/notification/mailer/mail_interface.go index f0b8b8170..facdcc97b 100644 --- a/app/services/mailer/mail.go +++ b/app/services/notification/mailer/mail_interface.go @@ -14,21 +14,34 @@ package mailer -import gomail "gopkg.in/mail.v2" +import ( + "context" -type MailRequest struct { - CCRecipients []string `json:"cc_recipients"` - ToRecipients []string `json:"to_recipients"` - Subject string `json:"subject"` - Body string `json:"body"` - ContentType string `json:"content_type"` + gomail "gopkg.in/mail.v2" +) + +const ( + mailContentType = "text/html" +) + +type Mailer interface { + Send(ctx context.Context, mailPayload Payload) error } -func (dto *MailRequest) ToGoMail() *gomail.Message { +type Payload struct { + CCRecipients []string + ToRecipients []string + Subject string + Body string + ContentType string + RepoRef string +} + +func ToGoMail(dto Payload) *gomail.Message { mail := gomail.NewMessage() mail.SetHeader("To", dto.ToRecipients...) mail.SetHeader("Cc", dto.CCRecipients...) mail.SetHeader("Subject", dto.Subject) - mail.SetBody(dto.ContentType, dto.Body) + mail.SetBody(mailContentType, dto.Body) return mail } diff --git a/app/services/mailer/wire.go b/app/services/notification/mailer/wire.go similarity index 88% rename from app/services/mailer/wire.go rename to app/services/notification/mailer/wire.go index 265129bd8..8808d25bf 100644 --- a/app/services/mailer/wire.go +++ b/app/services/notification/mailer/wire.go @@ -21,11 +21,11 @@ import ( ) var WireSet = wire.NewSet( - ProvideMailService, + ProvideMailClient, ) -func ProvideMailService(config *types.Config) *Service { - mailSvc := NewMailClient( +func ProvideMailClient(config *types.Config) Mailer { + return NewMailClient( config.SMTP.Host, config.SMTP.Port, config.SMTP.Username, @@ -33,5 +33,4 @@ func ProvideMailService(config *types.Config) *Service { config.SMTP.Password, config.SMTP.Insecure, // #nosec G402 (insecure skipVerify configuration) ) - return mailSvc } diff --git a/app/services/notification/reviewer_added.go b/app/services/notification/reviewer_added.go new file mode 100644 index 000000000..7d389160e --- /dev/null +++ b/app/services/notification/reviewer_added.go @@ -0,0 +1,81 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package notification + +import ( + "context" + "fmt" + + pullreqevents "github.com/harness/gitness/app/events/pullreq" + "github.com/harness/gitness/events" + "github.com/harness/gitness/types" +) + +type ReviewerAddedPayload struct { + Base *BasePullReqPayload + Reviewer *types.PrincipalInfo +} + +func (s *Service) notifyReviewerAdded( + ctx context.Context, + event *events.Event[*pullreqevents.ReviewerAddedPayload], +) error { + payload, err := s.processReviewerAddedEvent(ctx, event) + if err != nil { + return fmt.Errorf( + "failed to process %s event for pullReqID %d: %w", + pullreqevents.ReviewerAddedEvent, + event.Payload.PullReqID, + err, + ) + } + + // Send notification to author and reviewer + recipients := []*types.PrincipalInfo{ + payload.Base.Author, + payload.Reviewer, + } + err = s.notificationClient.SendReviewerAdded(ctx, recipients, payload) + if err != nil { + return fmt.Errorf( + "failed to send notification for event %s for pullReqID %d: %w", + pullreqevents.ReviewerAddedEvent, + event.Payload.PullReqID, + err, + ) + } + + return nil +} + +func (s *Service) processReviewerAddedEvent( + ctx context.Context, + event *events.Event[*pullreqevents.ReviewerAddedPayload], +) (*ReviewerAddedPayload, error) { + base, err := s.getBasePayload(ctx, event.Payload.Base) + if err != nil { + return nil, fmt.Errorf("failed to get base payload: %w", err) + } + + reviewerPrincipal, err := s.principalInfoCache.Get(ctx, event.Payload.ReviewerID) + if err != nil { + return nil, fmt.Errorf("failed to get reviewer from principalInfoCache: %w", err) + } + + return &ReviewerAddedPayload{ + Base: base, + Reviewer: reviewerPrincipal, + }, nil +} diff --git a/app/services/notification/service.go b/app/services/notification/service.go new file mode 100644 index 000000000..2e57a5007 --- /dev/null +++ b/app/services/notification/service.go @@ -0,0 +1,179 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package notification + +import ( + "context" + "embed" + "fmt" + "html/template" + "io/fs" + "path" + + pullreqevents "github.com/harness/gitness/app/events/pullreq" + "github.com/harness/gitness/app/store" + "github.com/harness/gitness/app/url" + "github.com/harness/gitness/events" + "github.com/harness/gitness/stream" + "github.com/harness/gitness/types" +) + +const ( + eventReaderGroupName = "gitness:notification" + templatesDir = "templates" + subjectPullReqEvent = "[%s] %s (PR #%d)" +) + +var ( + //go:embed templates/* + files embed.FS + htmlTemplates map[string]*template.Template +) + +func init() { + err := LoadTemplates() + if err != nil { + panic(err) + } +} + +func LoadTemplates() error { + htmlTemplates = make(map[string]*template.Template) + tmplFiles, err := fs.ReadDir(files, templatesDir) + if err != nil { + return err + } + + for _, tmpl := range tmplFiles { + if tmpl.IsDir() { + continue + } + + pt, err := template.ParseFS(files, path.Join(templatesDir, tmpl.Name())) + if err != nil { + return err + } + + htmlTemplates[tmpl.Name()] = pt + } + return nil +} + +type BasePullReqPayload struct { + Repo *types.Repository + PullReq *types.PullReq + Author *types.PrincipalInfo + PullReqURL string +} + +type Config struct { + EventReaderName string + Concurrency int + MaxRetries int +} + +type Service struct { + config Config + notificationClient Client + prReaderFactory *events.ReaderFactory[*pullreqevents.Reader] + pullReqStore store.PullReqStore + repoStore store.RepoStore + principalInfoView store.PrincipalInfoView + principalInfoCache store.PrincipalInfoCache + pullReqReviewersStore store.PullReqReviewerStore + pullReqActivityStore store.PullReqActivityStore + spacePathStore store.SpacePathStore + urlProvider url.Provider +} + +func NewService( + ctx context.Context, + config Config, + notificationClient Client, + prReaderFactory *events.ReaderFactory[*pullreqevents.Reader], + pullReqStore store.PullReqStore, + repoStore store.RepoStore, + principalInfoView store.PrincipalInfoView, + principalInfoCache store.PrincipalInfoCache, + pullReqReviewersStore store.PullReqReviewerStore, + pullReqActivityStore store.PullReqActivityStore, + spacePathStore store.SpacePathStore, + urlProvider url.Provider, +) (*Service, error) { + service := &Service{ + config: config, + notificationClient: notificationClient, + prReaderFactory: prReaderFactory, + pullReqStore: pullReqStore, + repoStore: repoStore, + principalInfoView: principalInfoView, + principalInfoCache: principalInfoCache, + pullReqReviewersStore: pullReqReviewersStore, + pullReqActivityStore: pullReqActivityStore, + spacePathStore: spacePathStore, + urlProvider: urlProvider, + } + + _, err := service.prReaderFactory.Launch( + ctx, + eventReaderGroupName, + config.EventReaderName, + func(r *pullreqevents.Reader, + ) error { + r.Configure( + stream.WithConcurrency(config.Concurrency), + stream.WithHandlerOptions( + stream.WithMaxRetries(config.MaxRetries), + )) + + _ = r.RegisterReviewerAdded(service.notifyReviewerAdded) + _ = r.RegisterCommentCreated(service.notifyCommentCreated) + _ = r.RegisterBranchUpdated(service.notifyPullReqBranchUpdated) + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to launch event reader for %s: %w", eventReaderGroupName, err) + } + + return service, nil +} + +func (s *Service) getBasePayload( + ctx context.Context, + base pullreqevents.Base, +) (*BasePullReqPayload, error) { + repo, err := s.repoStore.Find(ctx, base.TargetRepoID) + if err != nil { + return nil, fmt.Errorf("failed to fetch repo from repoStore: %w", err) + } + pullReq, err := s.pullReqStore.Find(ctx, base.PullReqID) + if err != nil { + return nil, fmt.Errorf("failed to fetch pullreq from pullReqStore: %w", err) + } + + author, err := s.principalInfoCache.Get(ctx, pullReq.CreatedBy) + if err != nil { + return nil, + fmt.Errorf("failed to fetch author %d from principalInfoCache while building base notification: %w", + pullReq.CreatedBy, err) + } + + return &BasePullReqPayload{ + Repo: repo, + PullReq: pullReq, + Author: author, + PullReqURL: s.urlProvider.GenerateUIPRURL(repo.Path, pullReq.Number), + }, nil +} diff --git a/app/services/notification/templates/comment_created.html b/app/services/notification/templates/comment_created.html new file mode 100644 index 000000000..8aa27ee5c --- /dev/null +++ b/app/services/notification/templates/comment_created.html @@ -0,0 +1,17 @@ + + + + + + +

+ @{{.Commenter.DisplayName}} commented on pull request #{{.Base.PullReq.Number}}:{{.Base.PullReq.Title}} +

+

+ {{.Text}} +

+

+ View pull request #{{.Base.PullReq.Number}} +

+ + \ No newline at end of file diff --git a/app/services/notification/templates/pullreq_branch_updated.html b/app/services/notification/templates/pullreq_branch_updated.html new file mode 100644 index 000000000..5c5e7603a --- /dev/null +++ b/app/services/notification/templates/pullreq_branch_updated.html @@ -0,0 +1,17 @@ + + + + + + +

+ @{{.Committer.DisplayName}} pushed new commits to pull request #{{.Base.PullReq.Number}}:{{.Base.PullReq.Title}} +

+

+ Latest commit is {{.NewSHA}} +

+

+ View pull request #{{.Base.PullReq.Number}} +

+ + \ No newline at end of file diff --git a/app/services/notification/templates/reviewer_added.html b/app/services/notification/templates/reviewer_added.html new file mode 100644 index 000000000..494ed0e0a --- /dev/null +++ b/app/services/notification/templates/reviewer_added.html @@ -0,0 +1,14 @@ + + + + + + +

+ @{{.Reviewer.DisplayName}} was added as a reviewer for the Pull request: #{{.Base.PullReq.Number}}:{{.Base.PullReq.Title}} +

+

+ View pull request #{{.Base.PullReq.Number}} +

+ + diff --git a/app/services/notification/wire.go b/app/services/notification/wire.go new file mode 100644 index 000000000..cebff43d7 --- /dev/null +++ b/app/services/notification/wire.go @@ -0,0 +1,66 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package notification + +import ( + "context" + + pullreqevents "github.com/harness/gitness/app/events/pullreq" + "github.com/harness/gitness/app/services/notification/mailer" + "github.com/harness/gitness/app/store" + "github.com/harness/gitness/app/url" + "github.com/harness/gitness/events" + + "github.com/google/wire" +) + +var WireSet = wire.NewSet( + ProvideMailClient, + ProvideNotificationService, +) + +func ProvideNotificationService( + ctx context.Context, + notificationClient Client, + pullReqConfig Config, + prReaderFactory *events.ReaderFactory[*pullreqevents.Reader], + pullReqStore store.PullReqStore, + repoStore store.RepoStore, + principalInfoView store.PrincipalInfoView, + principalInfoCache store.PrincipalInfoCache, + pullReqReviewersStore store.PullReqReviewerStore, + pullReqActivityStore store.PullReqActivityStore, + spacePathStore store.SpacePathStore, + urlProvider url.Provider, +) (*Service, error) { + return NewService( + ctx, + pullReqConfig, + notificationClient, + prReaderFactory, + pullReqStore, + repoStore, + principalInfoView, + principalInfoCache, + pullReqReviewersStore, + pullReqActivityStore, + spacePathStore, + urlProvider, + ) +} + +func ProvideMailClient(mailer mailer.Mailer) Client { + return NewMailClient(mailer) +} diff --git a/app/services/wire.go b/app/services/wire.go index fa7319a9c..8c4166bca 100644 --- a/app/services/wire.go +++ b/app/services/wire.go @@ -19,6 +19,7 @@ import ( "github.com/harness/gitness/app/services/job" "github.com/harness/gitness/app/services/keywordsearch" "github.com/harness/gitness/app/services/metric" + "github.com/harness/gitness/app/services/notification" "github.com/harness/gitness/app/services/pullreq" "github.com/harness/gitness/app/services/trigger" "github.com/harness/gitness/app/services/webhook" @@ -37,6 +38,7 @@ type Services struct { JobScheduler *job.Scheduler MetricCollector *metric.Collector Cleanup *cleanup.Service + Notification *notification.Service Keywordsearch *keywordsearch.Service } @@ -47,6 +49,7 @@ func ProvideServices( jobScheduler *job.Scheduler, metricCollector *metric.Collector, cleanupSvc *cleanup.Service, + notificationSvc *notification.Service, keywordsearchSvc *keywordsearch.Service, ) Services { return Services{ @@ -56,6 +59,7 @@ func ProvideServices( JobScheduler: jobScheduler, MetricCollector: metricCollector, Cleanup: cleanupSvc, + Notification: notificationSvc, Keywordsearch: keywordsearchSvc, } } diff --git a/cli/server/config.go b/cli/server/config.go index 43244c541..6e74d17e0 100644 --- a/cli/server/config.go +++ b/cli/server/config.go @@ -25,6 +25,7 @@ import ( "github.com/harness/gitness/app/services/cleanup" "github.com/harness/gitness/app/services/codeowners" "github.com/harness/gitness/app/services/keywordsearch" + "github.com/harness/gitness/app/services/notification" "github.com/harness/gitness/app/services/trigger" "github.com/harness/gitness/app/services/webhook" "github.com/harness/gitness/blob" @@ -279,6 +280,14 @@ func ProvideWebhookConfig(config *types.Config) webhook.Config { } } +func ProvideNotificationConfig(config *types.Config) notification.Config { + return notification.Config{ + EventReaderName: config.InstanceID, + Concurrency: config.Notification.Concurrency, + MaxRetries: config.Notification.MaxRetries, + } +} + // ProvideTriggerConfig loads the trigger service config from the main config. func ProvideTriggerConfig(config *types.Config) trigger.Config { return trigger.Config{ diff --git a/cmd/gitness/wire.go b/cmd/gitness/wire.go index 7c97bbe07..be3602bff 100644 --- a/cmd/gitness/wire.go +++ b/cmd/gitness/wire.go @@ -9,7 +9,6 @@ package main import ( "context" - checkcontroller "github.com/harness/gitness/app/api/controller/check" "github.com/harness/gitness/app/api/controller/connector" "github.com/harness/gitness/app/api/controller/execution" @@ -56,6 +55,8 @@ import ( "github.com/harness/gitness/app/services/job" "github.com/harness/gitness/app/services/keywordsearch" "github.com/harness/gitness/app/services/metric" + "github.com/harness/gitness/app/services/notification" + "github.com/harness/gitness/app/services/notification/mailer" "github.com/harness/gitness/app/services/protection" pullreqservice "github.com/harness/gitness/app/services/pullreq" "github.com/harness/gitness/app/services/trigger" @@ -92,6 +93,8 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e cliserver.ProvideDatabaseConfig, database.WireSet, cliserver.ProvideBlobStoreConfig, + mailer.WireSet, + notification.WireSet, blob.WireSet, dbtx.WireSet, cache.WireSet, @@ -124,6 +127,7 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e cliserver.ProvideEventsConfig, events.WireSet, cliserver.ProvideWebhookConfig, + cliserver.ProvideNotificationConfig, webhook.WireSet, cliserver.ProvideTriggerConfig, trigger.WireSet, diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index 507fc0a32..be6dcfe72 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -55,6 +55,8 @@ import ( "github.com/harness/gitness/app/services/job" "github.com/harness/gitness/app/services/keywordsearch" "github.com/harness/gitness/app/services/metric" + "github.com/harness/gitness/app/services/notification" + "github.com/harness/gitness/app/services/notification/mailer" "github.com/harness/gitness/app/services/protection" "github.com/harness/gitness/app/services/pullreq" trigger2 "github.com/harness/gitness/app/services/trigger" @@ -286,12 +288,19 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro if err != nil { return nil, err } + mailerMailer := mailer.ProvideMailClient(config) + notificationClient := notification.ProvideMailClient(mailerMailer) + notificationConfig := server.ProvideNotificationConfig(config) + notificationService, err := notification.ProvideNotificationService(ctx, notificationClient, notificationConfig, eventsReaderFactory, pullReqStore, repoStore, principalInfoView, principalInfoCache, pullReqReviewerStore, pullReqActivityStore, spacePathStore, provider) + if err != nil { + return nil, err + } keywordsearchConfig := server.ProvideKeywordSearchConfig(config) keywordsearchService, err := keywordsearch.ProvideService(ctx, keywordsearchConfig, readerFactory, repoStore, indexer) if err != nil { return nil, err } - servicesServices := services.ProvideServices(webhookService, pullreqService, triggerService, jobScheduler, collector, cleanupService, keywordsearchService) + servicesServices := services.ProvideServices(webhookService, pullreqService, triggerService, jobScheduler, collector, cleanupService, notificationService, keywordsearchService) serverSystem := server.NewSystem(bootstrapBootstrap, serverServer, poller, pluginManager, servicesServices) return serverSystem, nil } diff --git a/types/config.go b/types/config.go index f7fb43b6d..9946e3203 100644 --- a/types/config.go +++ b/types/config.go @@ -324,6 +324,11 @@ type Config struct { Insecure bool `envconfig:"GITNESS_SMTP_INSECURE"` } + Notification struct { + MaxRetries int `envconfig:"GITNESS_NOTIFICATION_MAX_RETRIES" default:"3"` + Concurrency int `envconfig:"GITNESS_NOTIFICATION_CONCURRENCY" default:"4"` + } + KeywordSearch struct { Concurrency int `envconfig:"GITNESS_KEYWORD_SEARCH_CONCURRENCY" default:"4"` MaxRetries int `envconfig:"GITNESS_KEYWORD_SEARCH_MAX_RETRIES" default:"3"`