From a95ec9ecddc84bce4c29ab746158c8ec779bf53d Mon Sep 17 00:00:00 2001 From: wuhuizuo Date: Fri, 15 Mar 2024 11:50:11 +0800 Subject: [PATCH] feat(cloudevents-server): improve notify info (#98) ## Improvements 1. avoid notify for failed task run when it's created by a pipeline run. 2. add more details about failed tasks and step details for failed pipeline run. ## Tests - [x] Tested with real lark IM. ## Effects image image Signed-off-by: wuhuizuo --- .../pkg/events/custom/tekton/handler.go | 149 ++++++++++++++++++ .../custom/tekton/handler_pipelinerun.go | 5 +- .../events/custom/tekton/handler_taskrun.go | 15 +- .../pkg/events/custom/tekton/lark.go | 99 ------------ .../tekton-run-notify.yaml.tmpl | 23 +++ .../pkg/events/custom/tekton/types.go | 6 +- .../pkg/events/custom/testcaserun/test.sh | 13 -- 7 files changed, 192 insertions(+), 118 deletions(-) delete mode 100755 cloudevents-server/pkg/events/custom/testcaserun/test.sh diff --git a/cloudevents-server/pkg/events/custom/tekton/handler.go b/cloudevents-server/pkg/events/custom/tekton/handler.go index 4bcb9021..14f69b54 100644 --- a/cloudevents-server/pkg/events/custom/tekton/handler.go +++ b/cloudevents-server/pkg/events/custom/tekton/handler.go @@ -2,8 +2,17 @@ package tekton import ( "encoding/json" + "fmt" + "strings" + "time" + cloudevents "github.com/cloudevents/sdk-go/v2" larksdk "github.com/larksuite/oapi-sdk-go/v3" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + tektoncloudevent "github.com/tektoncd/pipeline/pkg/reconciler/events/cloudevent" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/pkg/apis" "github.com/PingCAP-QE/ee-apps/cloudevents-server/pkg/config" "github.com/PingCAP-QE/ee-apps/cloudevents-server/pkg/events/handler" @@ -36,3 +45,143 @@ func getTriggerUser(run AnnotationsGetter) string { return contextData[eventContextAnnotationInnerKeyUser] } + +// /#/namespaces//s/ +// source: /apis///namespaces/// +// https://tekton.abc.com/tekton/apis/tekton.dev/v1beta1/namespaces/ee-cd/pipelineruns/auto-compose-multi-arch-image-run-g5hqv +// +// "source": "/apis///namespaces/ee-cd//build-package-tikv-tikv-linux-9bn55-build-binaries", +func newDetailURL(etype, source, baseURL string) string { + words := strings.Split(source, "/") + runName := words[len(words)-1] + runType := words[len(words)-2] + runNamespace := words[len(words)-3] + + if runType == "" { + runType = strings.Split(etype, ".")[3] + "s" + } + + return fmt.Sprintf("%s/#/namespaces/%s/%s/%s", baseURL, runNamespace, runType, runName) +} + +func extractLarkInfosFromEvent(event cloudevents.Event, baseURL string) (*cardMessageInfos, error) { + var data tektoncloudevent.TektonCloudEventData + if err := event.DataAs(&data); err != nil { + return nil, err + } + + ret := cardMessageInfos{ + Title: newLarkTitle(event.Type(), event.Subject()), + TitleTemplate: larkCardHeaderTemplates[tektoncloudevent.TektonEventType(event.Type())], + ViewURL: newDetailURL(event.Type(), event.Source(), baseURL), + } + + switch { + case data.PipelineRun != nil: + fillInfosWithPipelineRun(data.PipelineRun, &ret) + if event.Type() == string(tektoncloudevent.PipelineRunFailedEventV1) { + ret.FailedTasks = getFailedTasks(data.PipelineRun) + } + case data.TaskRun != nil: + fillInfosWithTaskRun(data.TaskRun, &ret) + if event.Type() == string(tektoncloudevent.TaskRunFailedEventV1) { + ret.StepStatuses = getStepStatuses(&data.TaskRun.Status) + } + case data.Run != nil: + fillInfosWithCustomRun(data.Run, &ret) + } + + return &ret, nil +} + +func fillInfosWithCustomRun(data *v1alpha1.Run, ret *cardMessageInfos) { + fillTimeFileds(ret, data.Status.StartTime, data.Status.CompletionTime) + + for _, p := range data.Spec.Params { + v, _ := p.Value.MarshalJSON() + ret.Params = append(ret.Params, [2]string{p.Name, string(v)}) + } + if results := data.Status.Results; len(results) > 0 { + var parts []string + for _, r := range results { + parts = append(parts, fmt.Sprintf("**%s**:", r.Name), r.Value, "---") + ret.Results = append(ret.Results, [2]string{r.Name, r.Value}) + + } + } +} + +func fillInfosWithTaskRun(data *v1beta1.TaskRun, ret *cardMessageInfos) { + fillTimeFileds(ret, data.Status.StartTime, data.Status.CompletionTime) + + for _, p := range data.Spec.Params { + v, _ := p.Value.MarshalJSON() + ret.Params = append(ret.Params, [2]string{p.Name, string(v)}) + } + if data.Status.GetCondition(apis.ConditionSucceeded).IsFalse() { + ret.RerunURL = fmt.Sprintf("tkn -n %s task start %s --use-taskrun %s", + data.Namespace, data.Spec.TaskRef.Name, data.Name) + } + if results := data.Status.TaskRunResults; len(results) > 0 { + for _, r := range results { + v, _ := r.Value.MarshalJSON() + ret.Results = append(ret.Results, [2]string{r.Name, string(v)}) + } + } + + getStepStatuses(&data.Status) +} + +func fillInfosWithPipelineRun(data *v1beta1.PipelineRun, ret *cardMessageInfos) { + fillTimeFileds(ret, data.Status.StartTime, data.Status.CompletionTime) + + for _, p := range data.Spec.Params { + v, _ := p.Value.MarshalJSON() + ret.Params = append(ret.Params, [2]string{p.Name, string(v)}) + } + if data.Status.GetCondition(apis.ConditionSucceeded).IsFalse() { + ret.RerunURL = fmt.Sprintf("tkn -n %s pipeline start %s --use-pipelinerun %s", + data.Namespace, data.Spec.PipelineRef.Name, data.Name) + } + if results := data.Status.PipelineResults; len(results) > 0 { + for _, r := range results { + ret.Results = append(ret.Results, [2]string{r.Name, r.Value.StringVal}) + } + } + +} + +func getFailedTasks(data *v1beta1.PipelineRun) map[string][][2]string { + ret := make(map[string][][2]string) + for _, v := range data.Status.TaskRuns { + succeededCondition := v.Status.GetCondition(apis.ConditionSucceeded) + if !succeededCondition.IsTrue() { + ret[v.PipelineTaskName] = getStepStatuses(v.Status) + } + } + + return ret +} + +func getStepStatuses(status *v1beta1.TaskRunStatus) [][2]string { + var ret [][2]string + for _, s := range status.Steps { + if s.Terminated != nil { + ret = append(ret, [2]string{s.Name, s.Terminated.Reason}) + } + } + + return ret +} + +func fillTimeFileds(ret *cardMessageInfos, startTime, endTime *metav1.Time) { + if startTime != nil { + ret.StartTime = startTime.Format(time.RFC3339) + } + if endTime != nil { + ret.EndTime = endTime.Format(time.RFC3339) + } + if startTime != nil && endTime != nil { + ret.TimeCost = time.Duration(endTime.UnixNano() - startTime.UnixNano()).String() + } +} diff --git a/cloudevents-server/pkg/events/custom/tekton/handler_pipelinerun.go b/cloudevents-server/pkg/events/custom/tekton/handler_pipelinerun.go index 0d083b35..60a61de8 100644 --- a/cloudevents-server/pkg/events/custom/tekton/handler_pipelinerun.go +++ b/cloudevents-server/pkg/events/custom/tekton/handler_pipelinerun.go @@ -59,7 +59,10 @@ func (h *pipelineRunHandler) Handle(event cloudevents.Event) cloudevents.Result return sendLarkMessages(h.LarkClient, receivers, event, h.DashboardBaseURL) default: - log.Debug().Str("ce-type", event.Type()).Msg("skip notifing for the event type.") + log.Debug(). + Str("handler", "pipelineRunHandler"). + Str("ce-type", event.Type()). + Msg("skip notifing for the event type.") return cloudevents.ResultACK } } diff --git a/cloudevents-server/pkg/events/custom/tekton/handler_taskrun.go b/cloudevents-server/pkg/events/custom/tekton/handler_taskrun.go index c0d4a232..09243821 100644 --- a/cloudevents-server/pkg/events/custom/tekton/handler_taskrun.go +++ b/cloudevents-server/pkg/events/custom/tekton/handler_taskrun.go @@ -8,6 +8,7 @@ import ( cloudevents "github.com/cloudevents/sdk-go/v2" lark "github.com/larksuite/oapi-sdk-go/v3" "github.com/rs/zerolog/log" + "github.com/tektoncd/pipeline/pkg/apis/pipeline" tektoncloudevent "github.com/tektoncd/pipeline/pkg/reconciler/events/cloudevent" ) @@ -34,6 +35,11 @@ func (h *taskRunHandler) Handle(event cloudevents.Event) cloudevents.Result { switch event.Type() { case string(tektoncloudevent.TaskRunFailedEventV1): + // Skip notify the taskrun when it created by a pipelineRun. + if data.TaskRun.Labels[pipeline.PipelineRunLabelKey] != "" { + break + } + var receivers []string // send notify to the trigger user if it's existed, else send to the receivers configurated by type. if receiver := getTriggerUser(data.TaskRun); receiver != "" { @@ -48,8 +54,11 @@ func (h *taskRunHandler) Handle(event cloudevents.Event) cloudevents.Result { Msg("send notification for the event type.") return sendLarkMessages(h.LarkClient, receivers, event, h.DashboardBaseURL) - default: - log.Debug().Str("ce-type", event.Type()).Msg("skip notifing for the event type.") - return cloudevents.ResultACK } + + log.Debug(). + Str("handler", "taskRunHandler"). + Str("ce-type", event.Type()). + Msg("skip notifing for the event type.") + return cloudevents.ResultACK } diff --git a/cloudevents-server/pkg/events/custom/tekton/lark.go b/cloudevents-server/pkg/events/custom/tekton/lark.go index aab6e8b5..7afa3023 100644 --- a/cloudevents-server/pkg/events/custom/tekton/lark.go +++ b/cloudevents-server/pkg/events/custom/tekton/lark.go @@ -9,7 +9,6 @@ import ( "regexp" "strings" "text/template" - "time" "github.com/Masterminds/sprig/v3" cloudevents "github.com/cloudevents/sdk-go/v2" @@ -19,8 +18,6 @@ import ( "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" _ "embed" ) @@ -115,84 +112,6 @@ func newLarkMessages(receivers []string, event cloudevents.Event, detailBaseUrl return reqs, nil } -func extractLarkInfosFromEvent(event cloudevents.Event, baseURL string) (*cardMessageInfos, error) { - var data tektoncloudevent.TektonCloudEventData - if err := event.DataAs(&data); err != nil { - return nil, err - } - - ret := cardMessageInfos{ - Title: newLarkTitle(event.Type(), event.Subject()), - TitleTemplate: larkCardHeaderTemplates[tektoncloudevent.TektonEventType(event.Type())], - ViewURL: newDetailURL(event.Type(), event.Source(), baseURL), - } - - var startTime, endTime *metav1.Time - switch { - case data.PipelineRun != nil: - startTime = data.PipelineRun.Status.StartTime - endTime = data.PipelineRun.Status.CompletionTime - for _, p := range data.PipelineRun.Spec.Params { - v, _ := p.Value.MarshalJSON() - ret.Params = append(ret.Params, [2]string{p.Name, string(v)}) - } - if data.PipelineRun.Status.GetCondition(apis.ConditionSucceeded).IsFalse() { - ret.RerunURL = fmt.Sprintf("tkn -n %s pipeline start %s --use-pipelinerun %s", - data.PipelineRun.Namespace, data.PipelineRun.Spec.PipelineRef.Name, data.PipelineRun.Name) - } - if results := data.PipelineRun.Status.PipelineResults; len(results) > 0 { - for _, r := range results { - ret.Results = append(ret.Results, [2]string{r.Name, r.Value.StringVal}) - } - } - case data.TaskRun != nil: - for _, p := range data.TaskRun.Spec.Params { - v, _ := p.Value.MarshalJSON() - ret.Params = append(ret.Params, [2]string{p.Name, string(v)}) - } - startTime = data.TaskRun.Status.StartTime - endTime = data.TaskRun.Status.CompletionTime - if data.TaskRun.Status.GetCondition(apis.ConditionSucceeded).IsFalse() { - ret.RerunURL = fmt.Sprintf("tkn -n %s task start %s --use-taskrun %s", - data.TaskRun.Namespace, data.TaskRun.Spec.TaskRef.Name, data.TaskRun.Name) - } - - if results := data.TaskRun.Status.TaskRunResults; len(results) > 0 { - for _, r := range results { - v, _ := r.Value.MarshalJSON() - ret.Results = append(ret.Results, [2]string{r.Name, string(v)}) - } - } - case data.Run != nil: - startTime = data.Run.Status.StartTime - endTime = data.Run.Status.CompletionTime - for _, p := range data.Run.Spec.Params { - v, _ := p.Value.MarshalJSON() - ret.Params = append(ret.Params, [2]string{p.Name, string(v)}) - } - if results := data.Run.Status.Results; len(results) > 0 { - var parts []string - for _, r := range results { - parts = append(parts, fmt.Sprintf("**%s**:", r.Name), r.Value, "---") - ret.Results = append(ret.Results, [2]string{r.Name, r.Value}) - - } - } - } - - if startTime != nil { - ret.StartTime = startTime.Format(time.RFC3339) - } - if endTime != nil { - ret.EndTime = endTime.Format(time.RFC3339) - } - if startTime != nil && endTime != nil { - ret.TimeCost = time.Duration(endTime.UnixNano() - startTime.UnixNano()).String() - } - - return &ret, nil -} - func newLarkCardWithGoTemplate(event cloudevents.Event, baseURL string) (string, error) { infos, err := extractLarkInfosFromEvent(event, baseURL) if err != nil { @@ -232,21 +151,3 @@ func newLarkTitle(etype, subject string) string { return fmt.Sprintf("%s [%s] %s is %s ", larkCardHeaderEmojis[tektoncloudevent.TektonEventType(etype)], runType, subject, runState) } - -// /#/namespaces//s/ -// source: /apis///namespaces/// -// https://tekton.abc.com/tekton/apis/tekton.dev/v1beta1/namespaces/ee-cd/pipelineruns/auto-compose-multi-arch-image-run-g5hqv -// -// "source": "/apis///namespaces/ee-cd//build-package-tikv-tikv-linux-9bn55-build-binaries", -func newDetailURL(etype, source, baseURL string) string { - words := strings.Split(source, "/") - runName := words[len(words)-1] - runType := words[len(words)-2] - runNamespace := words[len(words)-3] - - if runType == "" { - runType = strings.Split(etype, ".")[3] + "s" - } - - return fmt.Sprintf("%s/#/namespaces/%s/%s/%s", baseURL, runNamespace, runType, runName) -} diff --git a/cloudevents-server/pkg/events/custom/tekton/lark_templates/tekton-run-notify.yaml.tmpl b/cloudevents-server/pkg/events/custom/tekton/lark_templates/tekton-run-notify.yaml.tmpl index 62c6fa2e..e49b9f71 100644 --- a/cloudevents-server/pkg/events/custom/tekton/lark_templates/tekton-run-notify.yaml.tmpl +++ b/cloudevents-server/pkg/events/custom/tekton/lark_templates/tekton-run-notify.yaml.tmpl @@ -32,6 +32,29 @@ elements: {{- end }} {{- end }} + {{- with .FailedTasks }} + - tag: hr + - tag: markdown + content: |- + **Failed tasks:** + {{- range $t, $ss := . }} + - {{ $t }}: + {{- range $ss }} + 1. {{ index . 0 }}: {{ index . 1 }} + {{- end }} + {{- end }} + {{- end }} + + {{- with .StepStatuses }} + - tag: hr + - tag: markdown + content: |- + **Steps:** + {{- range . }} + 1. {{ index . 0 }}: {{ index . 1 }} + {{- end }} + {{- end }} + {{- with .Results }} - tag: hr - tag: markdown diff --git a/cloudevents-server/pkg/events/custom/tekton/types.go b/cloudevents-server/pkg/events/custom/tekton/types.go index c8347796..9298c6c7 100644 --- a/cloudevents-server/pkg/events/custom/tekton/types.go +++ b/cloudevents-server/pkg/events/custom/tekton/types.go @@ -13,8 +13,10 @@ type cardMessageInfos struct { StartTime string EndTime string TimeCost string - Params [][2]string // key-value pairs. - Results [][2]string // Key-Value pairs. + Params [][2]string // key-value pairs. + Results [][2]string // Key-Value pairs. + StepStatuses [][2]string // name-status pairs. + FailedTasks map[string][][2]string // task id => step statuses. } var larkCardHeaderTemplates = map[tektoncloudevent.TektonEventType]string{ diff --git a/cloudevents-server/pkg/events/custom/testcaserun/test.sh b/cloudevents-server/pkg/events/custom/testcaserun/test.sh deleted file mode 100755 index 433b2cdf..00000000 --- a/cloudevents-server/pkg/events/custom/testcaserun/test.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -curl --verbose --request POST \ - --url http://127.0.0.1:8080/events \ - --header "ce-id: $(uuidgen)" \ - --header 'ce-source: https://do.pingcap.net/jenkins' \ - --header 'ce-type: test-case-run-report' \ - --header 'ce-repo: pingcap/tidb' \ - --header 'ce-branch: master' \ - --header 'ce-buildurl: https://do.pingcap.net/jenkins/job/pingcap/job/tidb/job/ghpr_unit_test/35602/' \ - --header 'ce-specversion: 1.0' \ - --header 'content-type: application/json; charset=UTF-8' \ - --data @bazel-go-test-problem-cases.json