Merge pull request #11030 from mmpei/webhook-dev-slack

add support slack in webhook
This commit is contained in:
Wenkai Yin(尹文开) 2020-03-11 18:20:58 +08:00 committed by GitHub
commit c2826d0368
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 236 additions and 2 deletions

View File

@ -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")
}

View File

@ -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:* <!date^{{.OccurAt}}^{date} at {time}|February 18th, 2014 at 6:39 AM PST>"
}
},
{ "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
}

View File

@ -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())
}

View File

@ -13,5 +13,6 @@ const (
EventTypeTestEndpoint = "testEndpoint"
EventTypeProjectQuota = "projectQuota"
NotifyTypeHTTP = "http"
NotifyTypeHTTP = "http"
NotifyTypeSlack = "slack"
)

View File

@ -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"
)

View File

@ -31,6 +31,7 @@ func init() {
model.ScanningCompletedTopic: {&notification.ScanImagePreprocessHandler{}},
model.ScanningFailedTopic: {&notification.ScanImagePreprocessHandler{}},
model.QuotaExceedTopic: {&notification.QuotaPreprocessHandler{}},
model.SlackTopic: {&notification.SlackHandler{}},
}
for t, handlers := range handlersMap {