From 5564b1a1c48fdf3ba1bb8e7480bc2f0e718aea4e Mon Sep 17 00:00:00 2001 From: Patrick Zhao Date: Wed, 25 Dec 2024 18:12:37 +0800 Subject: [PATCH] support redirect in dingtalk and lark approval Signed-off-by: Patrick Zhao --- go.mod | 4 +- go.sum | 4 + pkg/microservice/aslan/config/consts.go | 8 +- .../common/repository/models/workflow_v4.go | 52 ++--- .../core/common/service/dingtalk/webhook.go | 1 - .../aslan/core/common/service/lark/lark.go | 21 +- .../aslan/core/common/service/lark/webhook.go | 1 + .../jobcontroller/job_approval.go | 205 ++++++++++++++++-- .../aslan/core/common/util/workflow.go | 3 +- .../core/release_plan/service/approval.go | 6 +- pkg/microservice/cron/core/service/types.go | 18 +- pkg/tool/dingtalk/approval.go | 1 + pkg/tool/lark/approval.go | 57 ++++- 13 files changed, 302 insertions(+), 79 deletions(-) diff --git a/go.mod b/go.mod index 382f7c2975..82173bff8a 100644 --- a/go.mod +++ b/go.mod @@ -77,6 +77,7 @@ require ( github.com/robfig/cron/v3 v3.0.1 github.com/samber/lo v1.37.0 github.com/sashabaranov/go-openai v1.24.0 + github.com/segmentio/encoding v0.4.1 github.com/shirou/gopsutil v3.21.11+incompatible github.com/shirou/gopsutil/v3 v3.22.8 github.com/spf13/cobra v1.8.0 @@ -100,6 +101,7 @@ require ( google.golang.org/grpc v1.53.0 google.golang.org/protobuf v1.30.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df + gopkg.in/ini.v1 v1.67.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 @@ -283,6 +285,7 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rubenv/sql-migrate v1.3.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/segmentio/asm v1.1.3 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/afero v1.9.2 // indirect @@ -329,7 +332,6 @@ require ( google.golang.org/appengine v1.6.7 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect k8s.io/apiserver v0.27.7 // indirect k8s.io/component-base v0.27.7 // indirect diff --git a/go.sum b/go.sum index 5ea75c7e3c..fccdb536b1 100644 --- a/go.sum +++ b/go.sum @@ -1016,6 +1016,10 @@ github.com/sashabaranov/go-openai v1.24.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adO github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.4.1 h1:KLGaLSW0jrmhB58Nn4+98spfvPvmo4Ci1P/WIQ9wn7w= +github.com/segmentio/encoding v0.4.1/go.mod h1:/d03Cd8PoaDeceuhUUUQWjU0KhWjrmYrWPgtJHYZSnI= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= diff --git a/pkg/microservice/aslan/config/consts.go b/pkg/microservice/aslan/config/consts.go index 199868f4ef..a4b866ce23 100644 --- a/pkg/microservice/aslan/config/consts.go +++ b/pkg/microservice/aslan/config/consts.go @@ -299,11 +299,13 @@ const ( SAEBatchReleaseTypeManual = "manual" ) -type ApproveOrReject string +type ApprovalStatus string const ( - Approve ApproveOrReject = "approve" - Reject ApproveOrReject = "reject" + Approve ApprovalStatus = "approve" + Reject ApprovalStatus = "reject" + Redirect ApprovalStatus = "redirect" + Done ApprovalStatus = "done" ) type DeploySourceType string diff --git a/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go b/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go index 23a7db2fd9..83a81cdd15 100644 --- a/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go +++ b/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go @@ -137,11 +137,11 @@ type Approval struct { } type NativeApproval struct { - Timeout int `bson:"timeout" yaml:"timeout" json:"timeout"` - ApproveUsers []*User `bson:"approve_users" yaml:"approve_users" json:"approve_users"` - FloatApproveUsers []*User `bson:"-" yaml:"flat_approve_users" json:"flat_approve_users"` - NeededApprovers int `bson:"needed_approvers" yaml:"needed_approvers" json:"needed_approvers"` - RejectOrApprove config.ApproveOrReject `bson:"reject_or_approve" yaml:"-" json:"reject_or_approve"` + Timeout int `bson:"timeout" yaml:"timeout" json:"timeout"` + ApproveUsers []*User `bson:"approve_users" yaml:"approve_users" json:"approve_users"` + FloatApproveUsers []*User `bson:"-" yaml:"flat_approve_users" json:"flat_approve_users"` + NeededApprovers int `bson:"needed_approvers" yaml:"needed_approvers" json:"needed_approvers"` + RejectOrApprove config.ApprovalStatus `bson:"reject_or_approve" yaml:"-" json:"reject_or_approve"` // InstanceCode: native approval instance code, save for working after restart aslan InstanceCode string `bson:"instance_code" yaml:"instance_code" json:"instance_code"` } @@ -160,16 +160,16 @@ type DingTalkApproval struct { type DingTalkApprovalNode struct { ApproveUsers []*DingTalkApprovalUser `bson:"approve_users" yaml:"approve_users" json:"approve_users"` Type dingtalk.ApprovalAction `bson:"type" yaml:"type" json:"type"` - RejectOrApprove config.ApproveOrReject `bson:"reject_or_approve" yaml:"-" json:"reject_or_approve"` + RejectOrApprove config.ApprovalStatus `bson:"reject_or_approve" yaml:"-" json:"reject_or_approve"` } type DingTalkApprovalUser struct { - ID string `bson:"id" yaml:"id" json:"id"` - Name string `bson:"name" yaml:"name" json:"name"` - Avatar string `bson:"avatar" yaml:"avatar" json:"avatar"` - RejectOrApprove config.ApproveOrReject `bson:"reject_or_approve,omitempty" yaml:"-" json:"reject_or_approve,omitempty"` - Comment string `bson:"comment,omitempty" yaml:"-" json:"comment,omitempty"` - OperationTime int64 `bson:"operation_time,omitempty" yaml:"-" json:"operation_time,omitempty"` + ID string `bson:"id" yaml:"id" json:"id"` + Name string `bson:"name" yaml:"name" json:"name"` + Avatar string `bson:"avatar" yaml:"avatar" json:"avatar"` + RejectOrApprove config.ApprovalStatus `bson:"reject_or_approve,omitempty" yaml:"-" json:"reject_or_approve,omitempty"` + Comment string `bson:"comment,omitempty" yaml:"-" json:"comment,omitempty"` + OperationTime int64 `bson:"operation_time,omitempty" yaml:"-" json:"operation_time,omitempty"` } type LarkApproval struct { @@ -211,16 +211,16 @@ func (l LarkApproval) GetLarkApprovalNode() (resp []*lark.ApprovalNode) { } type LarkApprovalNode struct { - ApproveUsers []*LarkApprovalUser `bson:"approve_users" yaml:"approve_users" json:"approve_users"` - Type lark.ApproveType `bson:"type" yaml:"type" json:"type"` - RejectOrApprove config.ApproveOrReject `bson:"reject_or_approve" yaml:"-" json:"reject_or_approve"` + ApproveUsers []*LarkApprovalUser `bson:"approve_users" yaml:"approve_users" json:"approve_users"` + Type lark.ApproveType `bson:"type" yaml:"type" json:"type"` + RejectOrApprove config.ApprovalStatus `bson:"reject_or_approve" yaml:"-" json:"reject_or_approve"` } type LarkApprovalUser struct { lark.UserInfo `bson:",inline" yaml:",inline" json:",inline"` - RejectOrApprove config.ApproveOrReject `bson:"reject_or_approve,omitempty" yaml:"-" json:"reject_or_approve,omitempty"` - Comment string `bson:"comment,omitempty" yaml:"-" json:"comment,omitempty"` - OperationTime int64 `bson:"operation_time,omitempty" yaml:"-" json:"operation_time,omitempty"` + RejectOrApprove config.ApprovalStatus `bson:"reject_or_approve,omitempty" yaml:"-" json:"reject_or_approve,omitempty"` + Comment string `bson:"comment,omitempty" yaml:"-" json:"comment,omitempty"` + OperationTime int64 `bson:"operation_time,omitempty" yaml:"-" json:"operation_time,omitempty"` } type WorkWXApproval struct { @@ -235,14 +235,14 @@ type WorkWXApproval struct { } type User struct { - Type string `bson:"type" yaml:"type" json:"type"` - UserID string `bson:"user_id,omitempty" yaml:"user_id,omitempty" json:"user_id,omitempty"` - UserName string `bson:"user_name,omitempty" yaml:"user_name,omitempty" json:"user_name,omitempty"` - GroupID string `bson:"group_id,omitempty" yaml:"group_id,omitempty" json:"group_id,omitempty"` - GroupName string `bson:"group_name,omitempty" yaml:"group_name,omitempty" json:"group_name,omitempty"` - RejectOrApprove config.ApproveOrReject `bson:"reject_or_approve,omitempty" yaml:"-" json:"reject_or_approve,omitempty"` - Comment string `bson:"comment,omitempty" yaml:"-" json:"comment,omitempty"` - OperationTime int64 `bson:"operation_time,omitempty" yaml:"-" json:"operation_time,omitempty"` + Type string `bson:"type" yaml:"type" json:"type"` + UserID string `bson:"user_id,omitempty" yaml:"user_id,omitempty" json:"user_id,omitempty"` + UserName string `bson:"user_name,omitempty" yaml:"user_name,omitempty" json:"user_name,omitempty"` + GroupID string `bson:"group_id,omitempty" yaml:"group_id,omitempty" json:"group_id,omitempty"` + GroupName string `bson:"group_name,omitempty" yaml:"group_name,omitempty" json:"group_name,omitempty"` + RejectOrApprove config.ApprovalStatus `bson:"reject_or_approve,omitempty" yaml:"-" json:"reject_or_approve,omitempty"` + Comment string `bson:"comment,omitempty" yaml:"-" json:"comment,omitempty"` + OperationTime int64 `bson:"operation_time,omitempty" yaml:"-" json:"operation_time,omitempty"` } type Job struct { diff --git a/pkg/microservice/aslan/core/common/service/dingtalk/webhook.go b/pkg/microservice/aslan/core/common/service/dingtalk/webhook.go index 25c8366ebc..6a00525eee 100644 --- a/pkg/microservice/aslan/core/common/service/dingtalk/webhook.go +++ b/pkg/microservice/aslan/core/common/service/dingtalk/webhook.go @@ -139,7 +139,6 @@ func EventHandler(appKey string, body []byte, signature, ts, nonce string) (*Eve eventType := gjson.Get(data, "EventType").String() log.Infof("receive dingtalk event type: %s instanceID: %s", eventType, gjson.Get(data, "processInstanceId").String()) - log.Debugf("receive dingtalk event data: %s", data) switch eventType { case EventTaskChange: diff --git a/pkg/microservice/aslan/core/common/service/lark/lark.go b/pkg/microservice/aslan/core/common/service/lark/lark.go index f79d969387..2379a7dcfc 100644 --- a/pkg/microservice/aslan/core/common/service/lark/lark.go +++ b/pkg/microservice/aslan/core/common/service/lark/lark.go @@ -39,11 +39,13 @@ const ( // ApprovalStatusNotFound not defined by lark open api, it just means not found in local manager. ApprovalStatusNotFound = "NOTFOUND" - ApprovalStatusPending = "PENDING" - ApprovalStatusApproved = "APPROVED" - ApprovalStatusRejected = "REJECTED" - ApprovalStatusCanceled = "CANCELED" - ApprovalStatusDeleted = "DELETED" + ApprovalStatusPending = "PENDING" + ApprovalStatusApproved = "APPROVED" + ApprovalStatusRejected = "REJECTED" + ApprovalStatusTransferred = "TRANSFERRED" + ApprovalStatusDone = "DONE" + ApprovalStatusCanceled = "CANCELED" + ApprovalStatusDeleted = "DELETED" ) type DepartmentInfo struct { @@ -292,7 +294,7 @@ type ApprovalManager struct { type UserApprovalResult struct { Result string - ApproveOrReject config.ApproveOrReject + ApproveOrReject config.ApprovalStatus OperationTime int64 } @@ -382,11 +384,18 @@ func (l *ApprovalManager) updateNodeUserApprovalResult(nodeID, userID string, re case ApprovalStatusRejected: l.NodeMap[nodeID][userID] = result result.ApproveOrReject = config.Reject + case ApprovalStatusTransferred: + l.NodeMap[nodeID][userID] = result + result.ApproveOrReject = config.Redirect + case ApprovalStatusDone: + l.NodeMap[nodeID][userID] = result + result.ApproveOrReject = config.Done } } return } +// Node Custom Key => Node Key func (l *ApprovalManager) GetNodeKeyMap() map[string]string { m := make(map[string]string) for k, v := range l.NodeKeyMap { diff --git a/pkg/microservice/aslan/core/common/service/lark/webhook.go b/pkg/microservice/aslan/core/common/service/lark/webhook.go index d78bdcd8b8..1fbc1c08ea 100644 --- a/pkg/microservice/aslan/core/common/service/lark/webhook.go +++ b/pkg/microservice/aslan/core/common/service/lark/webhook.go @@ -117,6 +117,7 @@ func EventHandler(appID, sign, ts, nonce, body string) (*EventHandlerResponse, e log.Infof("get unknown callback event type %s, ignored", eventType) return nil, nil } + log.Debugf("event data: %s", string(callback.Event)) event := ApprovalTaskEvent{} err = json.Unmarshal(callback.Event, &event) if err != nil { diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_approval.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_approval.go index 1903113793..88491427e7 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_approval.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_approval.go @@ -18,6 +18,7 @@ package jobcontroller import ( "context" + "encoding/json" "errors" "fmt" "net/url" @@ -239,7 +240,7 @@ func waitForLarkApprove(ctx context.Context, spec *commonmodels.JobTaskApprovalS } } - checkNodeStatus := func(node *commonmodels.LarkApprovalNode) (config.ApproveOrReject, error) { + checkNodeStatus := func(node *commonmodels.LarkApprovalNode) (config.ApprovalStatus, error) { switch node.Type { case "AND": result := config.Approve @@ -264,14 +265,41 @@ func waitForLarkApprove(ctx context.Context, spec *commonmodels.JobTaskApprovalS } } + checkApprovalFinished := func(nodes []*commonmodels.LarkApprovalNode) (bool, bool) { + count := len(nodes) + finishedNode := 0 + approval := true + for _, node := range nodes { + if node.RejectOrApprove == config.Approve || node.RejectOrApprove == config.Redirect || node.RejectOrApprove == config.Done { + finishedNode++ + } else { + approval = false + } + } + + if finishedNode == count { + return true, approval + } + + return false, approval + } + // approvalUpdate is used to update the approval status approvalUpdate := func(larkApproval *commonmodels.LarkApproval) (done, isApprove bool, err error) { + approvalUserMap := map[string]*commonmodels.LarkApprovalUser{} + for _, node := range approval.ApprovalNodes { + for _, approveUser := range node.ApproveUsers { + approvalUserMap[approveUser.ID] = approveUser + } + } + // userUpdated represents whether the user status has been updated userUpdated := false for i, node := range larkApproval.ApprovalNodes { if node.RejectOrApprove != "" { continue } + resultMap := larkservice.GetNodeUserApprovalResults(instance, lark.ApprovalNodeIDKey(i)) for _, user := range node.ApproveUsers { if result, ok := resultMap[user.ID]; ok && user.RejectOrApprove == "" { @@ -283,7 +311,13 @@ func waitForLarkApprove(ctx context.Context, spec *commonmodels.JobTaskApprovalS comment := "" // nodeKeyMap is used to get the node key from the custom node key nodeKeyMap := larkservice.GetLarkApprovalInstanceManager(instance).GetNodeKeyMap() + log.Debugf("nodeKeyMap: %+v", nodeKeyMap) + log.Debugf("nodeKeyMap[%s]: %s", lark.ApprovalNodeIDKey(i), nodeKeyMap[lark.ApprovalNodeIDKey(i)]) + bytes, _ := json.Marshal(instanceData.ApproverInfoWithNode) + log.Debugf("instanceData.ApproverInfoWithNode: %s", string(bytes)) if nodeData, ok := instanceData.ApproverInfoWithNode[nodeKeyMap[lark.ApprovalNodeIDKey(i)]]; ok { + log.Debugf("nodeData: %+v", nodeData) + log.Debugf("user.ID: %s", user.ID) if userData, ok := nodeData[user.ID]; ok { comment = userData.Comment } @@ -292,8 +326,36 @@ func waitForLarkApprove(ctx context.Context, spec *commonmodels.JobTaskApprovalS user.RejectOrApprove = result.ApproveOrReject user.OperationTime = result.OperationTime userUpdated = true + + if user.RejectOrApprove == config.Redirect { + for customNodeKey, taskMap := range instanceData.ApproverTaskWithNode { + if customNodeKey == lark.ApprovalNodeIDKey(i) { + for userID, task := range taskMap { + if approvalUserMap[userID] == nil { + userInfo, err := client.GetUserInfoByID(userID, setting.LarkUserOpenID) + if err != nil { + return false, false, fmt.Errorf("get user info %s failed, error: %s", userID, err) + } + + redirectedUser := &commonmodels.LarkApprovalUser{ + UserInfo: lark.UserInfo{ + ID: task.UserID, + Name: userInfo.Name, + Avatar: userInfo.Avatar, + }, + RejectOrApprove: task.Status, + } + node.ApproveUsers = append(node.ApproveUsers, redirectedUser) + + userUpdated = true + } + } + } + } + } } } + node.RejectOrApprove, err = checkNodeStatus(node) if err != nil { return false, false, err @@ -311,8 +373,8 @@ func waitForLarkApprove(ctx context.Context, spec *commonmodels.JobTaskApprovalS } } - finalResult := larkApproval.ApprovalNodes[len(larkApproval.ApprovalNodes)-1].RejectOrApprove - return finalResult != "", finalResult == config.Approve, nil + finished, approved := checkApprovalFinished(larkApproval.ApprovalNodes) + return finished, approved, nil } defer func() { @@ -330,7 +392,7 @@ func waitForLarkApprove(ctx context.Context, spec *commonmodels.JobTaskApprovalS cancelApproval() return config.StatusTimeout, fmt.Errorf("workflow timeout") default: - done, isApprove, err := approvalUpdate(approval) + done, _, err := approvalUpdate(approval) if err != nil { cancelApproval() return config.StatusFailed, fmt.Errorf("failed to check approval status, error: %s", err) @@ -341,13 +403,14 @@ func waitForLarkApprove(ctx context.Context, spec *commonmodels.JobTaskApprovalS if err != nil { return config.StatusFailed, fmt.Errorf("get approval final instance, error: %s", err) } - if finalInstance.ApproveOrReject == config.Approve && isApprove { + + log.Debugf("final approval instance: %+v", finalInstance) + if finalInstance.ApproveOrReject == config.Approve { return config.StatusPassed, nil } - if finalInstance.ApproveOrReject == config.Reject && !isApprove { + if finalInstance.ApproveOrReject == config.Reject { return config.StatusReject, nil } - return config.StatusFailed, errors.New("check final approval status failed") } } } @@ -431,12 +494,13 @@ func waitForDingTalkApprove(ctx context.Context, spec *commonmodels.JobTaskAppro dingservice.RemoveDingTalkApprovalManager(instanceID) }() - resultMap := map[string]config.ApproveOrReject{ - "agree": config.Approve, - "refuse": config.Reject, + resultMap := map[string]config.ApprovalStatus{ + "agree": config.Approve, + "refuse": config.Reject, + "redirect": config.Redirect, } - checkNodeStatus := func(node *commonmodels.DingTalkApprovalNode) (config.ApproveOrReject, error) { + checkNodeStatus := func(node *commonmodels.DingTalkApprovalNode) (config.ApprovalStatus, error) { users := node.ApproveUsers switch node.Type { case "AND": @@ -462,6 +526,22 @@ func waitForDingTalkApprove(ctx context.Context, spec *commonmodels.JobTaskAppro } } + checkApprovalFinished := func(nodes []*commonmodels.DingTalkApprovalNode) bool { + count := len(nodes) + finishedNode := 0 + for _, node := range nodes { + if node.RejectOrApprove == config.Approve || node.RejectOrApprove == config.Redirect { + finishedNode++ + } + } + + if finishedNode == count { + return true + } + + return false + } + timeoutChan := time.After(time.Duration(timeout) * time.Minute) for { time.Sleep(1 * time.Second) @@ -471,28 +551,119 @@ func waitForDingTalkApprove(ctx context.Context, spec *commonmodels.JobTaskAppro case <-timeoutChan: return config.StatusTimeout, fmt.Errorf("workflow timeout") default: - userApprovalResult := dingservice.GetAllUserApprovalResults(instanceID) userUpdated := false + userApprovalResult := dingservice.GetAllUserApprovalResults(instanceID) + + approvalUserMap := map[string]*commonmodels.DingTalkApprovalUser{} + for _, node := range approval.ApprovalNodes { + for _, approveUser := range node.ApproveUsers { + approvalUserMap[approveUser.ID] = approveUser + } + } + for _, node := range approval.ApprovalNodes { if node.RejectOrApprove != "" { continue } + + isRedirected := false for _, user := range node.ApproveUsers { - if result := userApprovalResult[user.ID]; result != nil && user.RejectOrApprove == "" { + if result := userApprovalResult[user.ID]; result != nil { + if user.RejectOrApprove == resultMap[result.Result] && + user.Comment == result.Remark && + user.OperationTime == result.OperationTime { + continue + } + user.RejectOrApprove = resultMap[result.Result] user.Comment = result.Remark user.OperationTime = result.OperationTime userUpdated = true + + if user.RejectOrApprove == config.Redirect { + isRedirected = true + } + } + } + + if isRedirected { + instanceInfo, err := client.GetApprovalInstance(instanceID) + if err != nil { + log.Errorf("get instance final info failed: %v", err) + return config.StatusFailed, fmt.Errorf("get instance final info error: %s", err) + } + + timeLayout := "2006-01-02T15:04Z" + operationRecordMap := map[string]*dingtalk.OperationRecord{} + for _, record := range instanceInfo.OperationRecords { + oldRecord, ok := operationRecordMap[record.UserID] + if !ok { + operationRecordMap[record.UserID] = record + } else { + oldTime, err := time.Parse(timeLayout, oldRecord.Date) + if err != nil { + return config.StatusFailed, fmt.Errorf("parse operation time failed: %s", err) + } + newTime, err := time.Parse(timeLayout, record.Date) + if err != nil { + return config.StatusFailed, fmt.Errorf("parse operation time failed: %s", err) + } + + if newTime.After(oldTime) { + operationRecordMap[record.UserID] = record + } + } + } + + for _, task := range instanceInfo.Tasks { + if _, ok := approvalUserMap[task.UserID]; !ok { + operationTime := int64(0) + record, ok := operationRecordMap[task.UserID] + if !ok { + record = &dingtalk.OperationRecord{ + UserID: task.UserID, + Result: task.Result, + } + } else { + t, err := time.Parse(timeLayout, record.Date) + if err != nil { + return config.StatusFailed, fmt.Errorf("parse operation time failed: %s", err) + } + operationTime = t.Unix() + } + + userInfo, err := client.GetUserInfo(task.UserID) + if err != nil { + err = fmt.Errorf("get user info %s failed: %s", task.UserID, err) + log.Error(err) + return config.StatusFailed, err + } + + redirectedUser := &commonmodels.DingTalkApprovalUser{ + ID: task.UserID, + Name: userInfo.Name, + RejectOrApprove: resultMap[task.Result], + Comment: record.Remark, + OperationTime: operationTime, + } + node.ApproveUsers = append(node.ApproveUsers, redirectedUser) + + userUpdated = true + } } } + node.RejectOrApprove, err = checkNodeStatus(node) if err != nil { log.Errorf("check node failed: %v", err) return config.StatusFailed, fmt.Errorf("check node failed, error: %s", err) } + switch node.RejectOrApprove { case config.Approve: ack() + case config.Redirect: + ack() case config.Reject: return config.StatusReject, fmt.Errorf("Approval has been rejected") default: @@ -502,7 +673,8 @@ func waitForDingTalkApprove(ctx context.Context, spec *commonmodels.JobTaskAppro } break } - if approval.ApprovalNodes[len(approval.ApprovalNodes)-1].RejectOrApprove == config.Approve { + + if checkApprovalFinished(approval.ApprovalNodes) { instanceInfo, err := client.GetApprovalInstance(instanceID) if err != nil { log.Errorf("get instance final info failed: %v", err) @@ -511,8 +683,9 @@ func waitForDingTalkApprove(ctx context.Context, spec *commonmodels.JobTaskAppro if instanceInfo.Status == "COMPLETED" && instanceInfo.Result == "agree" { return config.StatusPassed, nil } else { - log.Errorf("Unexpect instance final status is %s, result is %s", instanceInfo.Status, instanceInfo.Result) - return config.StatusFailed, fmt.Errorf("get unexpected instance final info, error: %s", err) + err = fmt.Errorf("Unexpect instance final status is %s, result is %s", instanceInfo.Status, instanceInfo.Result) + log.Error(err) + return config.StatusFailed, err } } } diff --git a/pkg/microservice/aslan/core/common/util/workflow.go b/pkg/microservice/aslan/core/common/util/workflow.go index 588bc0322a..15213f8bf2 100644 --- a/pkg/microservice/aslan/core/common/util/workflow.go +++ b/pkg/microservice/aslan/core/common/util/workflow.go @@ -21,7 +21,6 @@ import ( commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" "github.com/koderover/zadig/v2/pkg/setting" - "github.com/koderover/zadig/v2/pkg/tool/log" ) func CalcWorkflowTaskRunningTime(task *commonmodels.WorkflowTask) int64 { @@ -31,7 +30,7 @@ func CalcWorkflowTaskRunningTime(task *commonmodels.WorkflowTask) int64 { runningTime += 0 } else if task.EndTime == 0 { runningTime += 0 - log.Errorf("workflow task %s/%d stage %s end time is 0", task.WorkflowName, task.TaskID, stage.Name) + // log.Errorf("workflow task %s/%d stage %s end time is 0", task.WorkflowName, task.TaskID, stage.Name) } else { runningTime += stage.EndTime - stage.StartTime } diff --git a/pkg/microservice/aslan/core/release_plan/service/approval.go b/pkg/microservice/aslan/core/release_plan/service/approval.go index 741f406fb1..8bad881f16 100644 --- a/pkg/microservice/aslan/core/release_plan/service/approval.go +++ b/pkg/microservice/aslan/core/release_plan/service/approval.go @@ -205,12 +205,12 @@ func updateDingTalkApproval(ctx context.Context, approvalInfo *models.Approval) } client := dingtalk.NewClient(data.DingTalkAppKey, data.DingTalkAppSecret) - resultMap := map[string]config.ApproveOrReject{ + resultMap := map[string]config.ApprovalStatus{ "agree": config.Approve, "refuse": config.Reject, } - checkNodeStatus := func(node *models.DingTalkApprovalNode) (config.ApproveOrReject, error) { + checkNodeStatus := func(node *models.DingTalkApprovalNode) (config.ApprovalStatus, error) { users := node.ApproveUsers switch node.Type { case "AND": @@ -516,7 +516,7 @@ func updateLarkApproval(ctx context.Context, approval *models.Approval) error { } client := lark.NewClient(data.AppID, data.AppSecret) - checkNodeStatus := func(node *models.LarkApprovalNode) (config.ApproveOrReject, error) { + checkNodeStatus := func(node *models.LarkApprovalNode) (config.ApprovalStatus, error) { switch node.Type { case "AND": result := config.Approve diff --git a/pkg/microservice/cron/core/service/types.go b/pkg/microservice/cron/core/service/types.go index 3795d62dfe..559ddee83d 100644 --- a/pkg/microservice/cron/core/service/types.go +++ b/pkg/microservice/cron/core/service/types.go @@ -513,10 +513,10 @@ type Approval struct { } type NativeApproval struct { - Timeout int `bson:"timeout" yaml:"timeout" json:"timeout"` - ApproveUsers []*User `bson:"approve_users" yaml:"approve_users" json:"approve_users"` - NeededApprovers int `bson:"needed_approvers" yaml:"needed_approvers" json:"needed_approvers"` - RejectOrApprove config.ApproveOrReject `bson:"reject_or_approve" yaml:"-" json:"reject_or_approve"` + Timeout int `bson:"timeout" yaml:"timeout" json:"timeout"` + ApproveUsers []*User `bson:"approve_users" yaml:"approve_users" json:"approve_users"` + NeededApprovers int `bson:"needed_approvers" yaml:"needed_approvers" json:"needed_approvers"` + RejectOrApprove config.ApprovalStatus `bson:"reject_or_approve" yaml:"-" json:"reject_or_approve"` } type LarkApproval struct { @@ -526,11 +526,11 @@ type LarkApproval struct { } type User struct { - UserID string `bson:"user_id" yaml:"user_id" json:"user_id"` - UserName string `bson:"user_name" yaml:"user_name" json:"user_name"` - RejectOrApprove config.ApproveOrReject `bson:"reject_or_approve" yaml:"-" json:"reject_or_approve"` - Comment string `bson:"comment" yaml:"-" json:"comment"` - OperationTime int64 `bson:"operation_time" yaml:"-" json:"operation_time"` + UserID string `bson:"user_id" yaml:"user_id" json:"user_id"` + UserName string `bson:"user_name" yaml:"user_name" json:"user_name"` + RejectOrApprove config.ApprovalStatus `bson:"reject_or_approve" yaml:"-" json:"reject_or_approve"` + Comment string `bson:"comment" yaml:"-" json:"comment"` + OperationTime int64 `bson:"operation_time" yaml:"-" json:"operation_time"` } type Job struct { diff --git a/pkg/tool/dingtalk/approval.go b/pkg/tool/dingtalk/approval.go index 4809a42a50..a438ae6844 100644 --- a/pkg/tool/dingtalk/approval.go +++ b/pkg/tool/dingtalk/approval.go @@ -182,6 +182,7 @@ type ApprovalInstanceInfo struct { Title string `json:"title"` Status string `json:"status"` Result string `json:"result"` + ApproverUserIds []string `json:"approverUserIds"` OperationRecords []*OperationRecord `json:"operationRecords"` Tasks []*ApprovalInstanceTask `json:"tasks"` } diff --git a/pkg/tool/lark/approval.go b/pkg/tool/lark/approval.go index f50194a3ae..3879452c1f 100644 --- a/pkg/tool/lark/approval.go +++ b/pkg/tool/lark/approval.go @@ -33,13 +33,20 @@ const ( // ApprovalStatusNotFound not defined by lark open api, it just means not found in local manager. ApprovalStatusNotFound = "NOTFOUND" - ApprovalStatusPending = "PENDING" - ApprovalStatusApproved = "APPROVED" - ApprovalStatusRejected = "REJECTED" - ApprovalStatusCanceled = "CANCELED" - ApprovalStatusDeleted = "DELETED" + ApprovalStatusPending = "PENDING" + ApprovalStatusApproved = "APPROVED" + ApprovalStatusRejected = "REJECTED" + ApprovalStatusTransferred = "TRANSFERRED" + ApprovalStatusCanceled = "CANCELED" + ApprovalStatusDeleted = "DELETED" ) +var approvalStatusMap = map[string]config.ApprovalStatus{ + ApprovalStatusApproved: config.Approve, + ApprovalStatusRejected: config.Reject, + ApprovalStatusTransferred: config.Redirect, +} + type CreateApprovalDefinitionArgs struct { Name string Description string @@ -232,7 +239,14 @@ type UserApprovalComment struct { type ApprovalInstanceInfo struct { // key1 is node id, key2 is user open id ApproverInfoWithNode map[string]map[string]*UserApprovalComment - ApproveOrReject config.ApproveOrReject + ApproverTaskWithNode map[string]map[string]*ApprovalTask + ApproveOrReject config.ApprovalStatus +} + +type ApprovalTask struct { + UserID string + Status config.ApprovalStatus + NodeID string } func (client *Client) GetApprovalInstance(args *GetApprovalInstanceArgs) (*ApprovalInstanceInfo, error) { @@ -249,11 +263,31 @@ func (client *Client) GetApprovalInstance(args *GetApprovalInstanceArgs) (*Appro return nil, resp.CodeError } + bytes, _ := json.Marshal(resp) + log.Debugf("get approval instance response: %s", string(bytes)) + + taskMap := make(map[string]map[string]*ApprovalTask) + for _, task := range resp.Data.TaskList { + customNodeKey, openID := getStringFromPointer(task.CustomNodeId), getStringFromPointer(task.OpenId) + if customNodeKey == "" { + log.Warn("custom node key is empty") + continue + } + + if taskMap[customNodeKey] == nil { + taskMap[customNodeKey] = make(map[string]*ApprovalTask) + } + taskMap[customNodeKey][openID] = &ApprovalTask{ + UserID: openID, + Status: approvalStatusMap[getStringFromPointer(task.Status)], + } + } + userCommentMap := make(map[string]map[string]*UserApprovalComment) for _, timeline := range resp.Data.Timeline { status := getStringFromPointer(timeline.Type) - if status == "PASS" || status == "REJECT" { - nodeKey, userID := getStringFromPointer(timeline.NodeKey), getStringFromPointer(timeline.OpenId) + if status == "PASS" || status == "REJECT" || status == "TRANSFER" || status == "ADD_APPROVER_AFTER" { + nodeKey, openID := getStringFromPointer(timeline.NodeKey), getStringFromPointer(timeline.OpenId) if nodeKey == "" { log.Warn("node key is empty") continue @@ -261,16 +295,15 @@ func (client *Client) GetApprovalInstance(args *GetApprovalInstanceArgs) (*Appro if userCommentMap[nodeKey] == nil { userCommentMap[nodeKey] = make(map[string]*UserApprovalComment) } - userCommentMap[nodeKey][userID] = &UserApprovalComment{ + userCommentMap[nodeKey][openID] = &UserApprovalComment{ Comment: getStringFromPointer(timeline.Comment), } } } return &ApprovalInstanceInfo{ ApproverInfoWithNode: userCommentMap, - ApproveOrReject: map[string]config.ApproveOrReject{ - ApprovalStatusApproved: config.Approve, - ApprovalStatusRejected: config.Reject}[getStringFromPointer(resp.Data.Status)], + ApproverTaskWithNode: taskMap, + ApproveOrReject: approvalStatusMap[getStringFromPointer(resp.Data.Status)], }, nil }