Skip to content

Commit

Permalink
opt(cloudevents-server): opt notify card UI (#69)
Browse files Browse the repository at this point in the history
Signed-off-by: wuhuizuo <wuhuizuo@126.com>

---------

Signed-off-by: wuhuizuo <wuhuizuo@126.com>
  • Loading branch information
wuhuizuo authored Jan 22, 2024
1 parent 8e81412 commit 6ff30ed
Show file tree
Hide file tree
Showing 2 changed files with 212 additions and 28 deletions.
193 changes: 165 additions & 28 deletions cloudevents-server/pkg/events/custom/tekton/lark.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package tekton

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"html/template"
"net/http"
"regexp"
"strings"

cloudevents "github.com/cloudevents/sdk-go/v2"
Expand All @@ -14,10 +18,59 @@ import (
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
"github.com/rs/zerolog/log"
tektoncloudevent "github.com/tektoncd/pipeline/pkg/reconciler/events/cloudevent"
"gopkg.in/yaml.v3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"knative.dev/pkg/apis"

"github.com/PingCAP-QE/ee-apps/cloudevents-server/pkg/config"

_ "embed"
)

//go:embed lark_templates/pipelinerun-notify.yaml.tmpl
var larkTemplateBytes string

// receiver formats:
// - Open ID: ou_......
// - Union ID: on_......
// - Chat ID: oc_......
// - Email: some email address.
// - User ID: I do not know
var (
reLarkOpenID = regexp.MustCompile(`^ou_\w+`)
reLarkUnionID = regexp.MustCompile(`^on_\w+`)
reLarkChatID = regexp.MustCompile(`^oc_\w+`)
reLarkEmail = regexp.MustCompile(`^\S+@\S+\.\S+$`)
)

func getReceiverIDType(id string) string {
switch {
case reLarkOpenID.MatchString(id):
return larkim.ReceiveIdTypeOpenId
case reLarkUnionID.MatchString(id):
return larkim.ReceiveIdTypeUnionId
case reLarkChatID.MatchString(id):
return larkim.ReceiveIdTypeChatId
case reLarkEmail.MatchString(id):
return larkim.ReceiveIdTypeEmail
default:
return larkim.ReceiveIdTypeUserId
}
}

func newMessageReq(receiver string, messageRawStr string) *larkim.CreateMessageReq {
return larkim.NewCreateMessageReqBuilder().
ReceiveIdType(getReceiverIDType(receiver)).
Body(
larkim.NewCreateMessageReqBodyBuilder().
MsgType(larkim.MsgTypeInteractive).
ReceiveId(receiver).
Content(messageRawStr).
Build(),
).
Build()
}

func newLarkClient(cfg config.Lark) *lark.Client {
// Disable certificate verification
tr := &http.Transport{
Expand All @@ -32,8 +85,8 @@ func newLarkClient(cfg config.Lark) *lark.Client {
)
}

func sendLarkMessages(client *lark.Client, receiveEmails []string, event cloudevents.Event, detailBaseUrl string) protocol.Result {
createMsgReqs, err := newLarkMessages(receiveEmails, event, detailBaseUrl)
func sendLarkMessages(client *lark.Client, receivers []string, event cloudevents.Event, detailBaseUrl string) protocol.Result {
createMsgReqs, err := newLarkMessages(receivers, event, detailBaseUrl)
if err != nil {
log.Error().Err(err).Msg("compose lark message failed")
return cloudevents.NewHTTPResult(http.StatusInternalServerError, "compose lark message failed: %v", err)
Expand All @@ -59,51 +112,135 @@ func sendLarkMessages(client *lark.Client, receiveEmails []string, event cloudev
return cloudevents.ResultACK
}

func newLarkMessages(receiveEmails []string, event cloudevents.Event, detailBaseUrl string) ([]*larkim.CreateMessageReq, error) {
messageCard := newLarkCard(event.Type(), event.Subject(), event.Source(), detailBaseUrl)
func newLarkMessages(receivers []string, event cloudevents.Event, detailBaseUrl string) ([]*larkim.CreateMessageReq, error) {
var eventData tektoncloudevent.TektonCloudEventData
if err := event.DataAs(&eventData); err != nil {
return nil, err
}

messageCard := newLarkCard(event.Type(), event.Subject(), event.Source(), detailBaseUrl, &eventData)
messageRawStr, err := messageCard.String()
if err != nil {
return nil, err
}

var reqs []*larkim.CreateMessageReq
for _, receiveEmail := range receiveEmails {
req := larkim.NewCreateMessageReqBuilder().
ReceiveIdType(larkim.ReceiveIdTypeEmail).
Body(
larkim.NewCreateMessageReqBodyBuilder().
MsgType(larkim.MsgTypeInteractive).
ReceiveId(receiveEmail).
Content(messageRawStr).
Build(),
).
Build()

reqs = append(reqs, req)
for _, r := range receivers {
reqs = append(reqs, newMessageReq(r, messageRawStr))
}

return reqs, nil
}

func newLarkCard(etype, subject, source, baseURL string) *larkcard.MessageCard {
func newLarkCard(etype, subject, source, baseURL string, data *tektoncloudevent.TektonCloudEventData) *larkcard.MessageCard {
title := newLarkTitle(etype, subject)
header := larkcard.NewMessageCardHeader().
Template(larkCardHeaderTemplates[tektoncloudevent.TektonEventType(etype)]).
Title(larkcard.NewMessageCardPlainText().Content(title))

detailLinkAction := larkcard.NewMessageCardAction().Actions([]larkcard.MessageCardActionElement{
larkcard.NewMessageCardEmbedButton().
Type(larkcard.MessageCardButtonTypeDefault).
Text(larkcard.NewMessageCardPlainText().Content("View")).
Url(newDetailURL(etype, source, baseURL)),
})

return larkcard.NewMessageCard().
Config(larkcard.NewMessageCardConfig().WideScreenMode(true)).
Header(header).
Elements([]larkcard.MessageCardElement{
detailLinkAction,
})
Elements(append(newMessageCardFieldFromTektonCloudEventData(data),
// detail link
larkcard.NewMessageCardAction().Actions([]larkcard.MessageCardActionElement{
larkcard.NewMessageCardEmbedButton().
Type(larkcard.MessageCardButtonTypeDefault).
Text(larkcard.NewMessageCardPlainText().Content("View")).
Url(newDetailURL(etype, source, baseURL)),
})),
)
}

func newMessageCardFieldFromTektonCloudEventData(data *tektoncloudevent.TektonCloudEventData) []larkcard.MessageCardElement {
var startTime, endTime *metav1.Time
var rerunCmd string
switch {
case data.PipelineRun != nil:
startTime = data.PipelineRun.Status.StartTime
endTime = data.PipelineRun.Status.CompletionTime
if data.PipelineRun.Status.GetCondition(apis.ConditionSucceeded).IsFalse() {
rerunCmd = fmt.Sprintf("tkn -n %s pipeline start %s --use-pipelinerun %s",
data.PipelineRun.Namespace, data.PipelineRun.Spec.PipelineRef.Name, data.PipelineRun.Name)
}
case data.TaskRun != nil:
startTime = data.TaskRun.Status.StartTime
endTime = data.TaskRun.Status.CompletionTime
if data.TaskRun.Status.GetCondition(apis.ConditionSucceeded).IsFalse() {
rerunCmd = fmt.Sprintf("tkn -n %s task start %s --use-taskrun %s",
data.TaskRun.Namespace, data.TaskRun.Spec.TaskRef.Name, data.TaskRun.Name)
}
case data.Run != nil:
startTime = data.Run.Status.StartTime
endTime = data.Run.Status.CompletionTime
}

var ret []larkcard.MessageCardElement
var infoFileds []*larkcard.MessageCardField
if startTime != nil {
content := fmt.Sprintf(`**Start time:** %s`, startTime.GoString())
infoFileds = append(infoFileds,
larkcard.NewMessageCardField().IsShort(true).Text(larkcard.NewMessageCardLarkMd().Content(content)))
}
if endTime != nil {
content := fmt.Sprintf(`**End time:** %s`, endTime.GoString())
infoFileds = append(infoFileds,
larkcard.NewMessageCardField().IsShort(true).Text(larkcard.NewMessageCardLarkMd().Content(content)))
}
if startTime != nil && endTime != nil {
content := fmt.Sprintf(`**Time cost:** %ds`, endTime.Unix()-startTime.Unix())
infoFileds = append(infoFileds,
larkcard.NewMessageCardField().IsShort(true).Text(larkcard.NewMessageCardLarkMd().Content(content)))
}

ret = append(ret, larkcard.NewMessageCardDiv().Fields(infoFileds))

if rerunCmd != "" {
content := fmt.Sprintf(`**Rerun command:** %s`, rerunCmd)
runRunFileds := []*larkcard.MessageCardField{
larkcard.NewMessageCardField().Text(larkcard.NewMessageCardLarkMd().Content(content)),
}

ret = append(ret, larkcard.NewMessageCardHr(), larkcard.NewMessageCardDiv().Fields(runRunFileds))
}

return ret
}

func newLarkCardWithGoTemplate(etype, subject, source, baseURL string) *larkcard.MessageCard {
// todo: replace with go template
tmpl, err := template.New("lark").Parse(larkTemplateBytes)
if err != nil {
return nil
}

tmplResult := new(bytes.Buffer)
if err := tmpl.Execute(tmplResult, nil); err != nil {
return nil
}

values := make(map[string]interface{})
if err := yaml.Unmarshal(tmplResult.Bytes(), &values); err != nil {
return nil
}

var yamlBytes []byte
data := make(map[string]interface{})
if err := yaml.Unmarshal(yamlBytes, data); err != nil {
return nil
}

jsonBytes, err := json.Marshal(data)
if err != nil {
return nil
}

ret := larkcard.NewMessageCard()
if err := json.Unmarshal(jsonBytes, ret); err != nil {
return nil
}

return ret
}

func newLarkTitle(etype, subject string) string {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
config:
wide_screen_mode: true
elements:
- tag: div
fields:
- is_short: true
text:
tag: lark_md
content: >-
**Start time:** {{ .StartTime }}
- is_short: true
text:
tag: lark_md
content: >-
**End time:** {{ .EndTime }}
- is_short: true
text:
tag: lark_md
content: >-
**Time cost: {{ .TimeCost }}
- tag: hr
- tag: markdown
content: |-
**Results:**
- abc: true
- sdfasf: true
- actions:
- tag: button
text:
content: View
tag: plain_text
type: primary
multi_url:
url: "{{ .ViewURL }}"
- tag: button
text:
content: Rerun
tag: plain_text
type: danger
multi_url:
url: "{{ .RerunURL }}"
tag: action
header:
template: {{ .TitleTemplate }}
title:
content: {{ .Title }}
tag: plain_text

0 comments on commit 6ff30ed

Please sign in to comment.