diff --git a/src/pkg/notification/notification.go b/src/pkg/notification/notification.go index 57c4b4134..cc645f345 100755 --- a/src/pkg/notification/notification.go +++ b/src/pkg/notification/notification.go @@ -45,7 +45,7 @@ func Init() { model.EventTypeScanningCompleted, model.EventTypeScanningFailed, model.EventTypeProjectQuota, ) - initSupportedNotifyType(model.NotifyTypeHTTP) + initSupportedNotifyType(model.NotifyTypeHTTP, model.NotifyTypeSlack) log.Info("notification initialization completed") } diff --git a/src/pkg/notifier/handler/notification/slack_handler.go b/src/pkg/notifier/handler/notification/slack_handler.go new file mode 100644 index 000000000..7cf7a2ac8 --- /dev/null +++ b/src/pkg/notifier/handler/notification/slack_handler.go @@ -0,0 +1,129 @@ +package notification + +import ( + "errors" + "fmt" + + "bytes" + "encoding/json" + "github.com/goharbor/harbor/src/common/job/models" + "github.com/goharbor/harbor/src/jobservice/job" + "github.com/goharbor/harbor/src/pkg/notification" + "github.com/goharbor/harbor/src/pkg/notifier/model" + "strings" + "text/template" +) + +const ( + // SlackBodyTemplate defines Slack request body template + SlackBodyTemplate = `{ + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Harbor webhook events*" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*event_type:* {{.Type}}" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*occur_at:* " + } + }, + { "type": "section", + "text": { + "type": "mrkdwn", + "text": "*operator:* {{.Operator}}" + } + }, + { "type": "section", + "text": { + "type": "mrkdwn", + "text": "*event_data:*" + } + }, + { "type": "section", + "text": { + "type": "mrkdwn", + "text": "{{.EventData}}" + } + } + ]}` +) + +// SlackHandler preprocess event data to slack and start the hook processing +type SlackHandler struct { +} + +// Handle handles event to slack +func (s *SlackHandler) Handle(value interface{}) error { + if value == nil { + return errors.New("SlackHandler cannot handle nil value") + } + + event, ok := value.(*model.HookEvent) + if !ok || event == nil { + return errors.New("invalid notification slack event") + } + + return s.process(event) +} + +// IsStateful ... +func (s *SlackHandler) IsStateful() bool { + return false +} + +func (s *SlackHandler) process(event *model.HookEvent) error { + j := &models.JobData{ + Metadata: &models.JobMetadata{ + JobKind: job.KindGeneric, + }, + } + // Create a webhookJob to send message to slack + j.Name = job.WebhookJob + + // Convert payload to slack format + payload, err := s.convert(event.Payload) + if err != nil { + return fmt.Errorf("convert payload to slack body failed: %v", err) + } + + j.Parameters = map[string]interface{}{ + "payload": payload, + "address": event.Target.Address, + // Users can define a auth header in http statement in notification(webhook) policy. + // So it will be sent in header in http request. + "auth_header": event.Target.AuthHeader, + "skip_cert_verify": event.Target.SkipCertVerify, + } + return notification.HookManager.StartHook(event, j) +} + +func (s *SlackHandler) convert(payLoad *model.Payload) (string, error) { + data := make(map[string]interface{}) + data["Type"] = payLoad.Type + data["OccurAt"] = payLoad.OccurAt + data["Operator"] = payLoad.Operator + eventData, err := json.MarshalIndent(payLoad.EventData, "", "\t") + if err != nil { + return "", fmt.Errorf("marshal from eventData %v failed: %v", payLoad.EventData, err) + } + data["EventData"] = "```" + strings.Replace(string(eventData), `"`, `\"`, -1) + "```" + + st, _ := template.New("slack").Parse(SlackBodyTemplate) + var slackBuf bytes.Buffer + if err := st.Execute(&slackBuf, data); err != nil { + return "", fmt.Errorf("%v", err) + } + return slackBuf.String(), nil +} diff --git a/src/pkg/notifier/handler/notification/slack_handler_test.go b/src/pkg/notifier/handler/notification/slack_handler_test.go new file mode 100644 index 000000000..6fc726c1d --- /dev/null +++ b/src/pkg/notifier/handler/notification/slack_handler_test.go @@ -0,0 +1,101 @@ +package notification + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + cModels "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/pkg/notification" + "github.com/goharbor/harbor/src/pkg/notifier/event" + "github.com/goharbor/harbor/src/pkg/notifier/model" + "github.com/stretchr/testify/require" +) + +func TestSlackHandler_Handle(t *testing.T) { + hookMgr := notification.HookManager + defer func() { + notification.HookManager = hookMgr + }() + notification.HookManager = &fakedHookManager{} + + handler := &SlackHandler{} + + type args struct { + event *event.Event + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "SlackHandler_Handle Want Error 1", + args: args{ + event: &event.Event{ + Topic: "slack", + Data: nil, + }, + }, + wantErr: true, + }, + { + name: "SlackHandler_Handle Want Error 2", + args: args{ + event: &event.Event{ + Topic: "slack", + Data: &model.ImageEvent{}, + }, + }, + wantErr: true, + }, + { + name: "SlackHandler_Handle 1", + args: args{ + event: &event.Event{ + Topic: "slack", + Data: &model.HookEvent{ + PolicyID: 1, + EventType: "pushImage", + Target: &cModels.EventTarget{ + Type: "slack", + Address: "http://127.0.0.1:8080", + }, + Payload: &model.Payload{ + OccurAt: time.Now().Unix(), + Type: "pushImage", + Operator: "admin", + EventData: &model.EventData{ + Resources: []*model.Resource{ + { + Tag: "v9.0", + }, + }, + Repository: &model.Repository{ + Name: "library/debian", + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := handler.Handle(tt.args.event.Data) + if tt.wantErr { + require.NotNil(t, err, "Error: %s", err) + return + } + }) + } +} + +func TestSlackHandler_IsStateful(t *testing.T) { + handler := &SlackHandler{} + assert.False(t, handler.IsStateful()) +} diff --git a/src/pkg/notifier/model/const.go b/src/pkg/notifier/model/const.go index 8bdcc21db..386f9132f 100644 --- a/src/pkg/notifier/model/const.go +++ b/src/pkg/notifier/model/const.go @@ -13,5 +13,6 @@ const ( EventTypeTestEndpoint = "testEndpoint" EventTypeProjectQuota = "projectQuota" - NotifyTypeHTTP = "http" + NotifyTypeHTTP = "http" + NotifyTypeSlack = "slack" ) diff --git a/src/pkg/notifier/model/topic.go b/src/pkg/notifier/model/topic.go index 9094274e9..0bab0e8f6 100644 --- a/src/pkg/notifier/model/topic.go +++ b/src/pkg/notifier/model/topic.go @@ -42,6 +42,8 @@ const ( // WebhookTopic is topic for sending webhook payload WebhookTopic = "http" + // SlackTopic is topic for sending slack payload + SlackTopic = "slack" // EmailTopic is topic for sending email payload EmailTopic = "email" ) diff --git a/src/pkg/notifier/topic/topics.go b/src/pkg/notifier/topic/topics.go index 1493c09a4..c6b643086 100644 --- a/src/pkg/notifier/topic/topics.go +++ b/src/pkg/notifier/topic/topics.go @@ -31,6 +31,7 @@ func init() { model.ScanningCompletedTopic: {¬ification.ScanImagePreprocessHandler{}}, model.ScanningFailedTopic: {¬ification.ScanImagePreprocessHandler{}}, model.QuotaExceedTopic: {¬ification.QuotaPreprocessHandler{}}, + model.SlackTopic: {¬ification.SlackHandler{}}, } for t, handlers := range handlersMap {