diff --git a/pkg/microservice/aslan/core/common/repository/models/dynamic_recipient.go b/pkg/microservice/aslan/core/common/repository/models/dynamic_recipient.go new file mode 100644 index 0000000000..bf68712ce7 --- /dev/null +++ b/pkg/microservice/aslan/core/common/repository/models/dynamic_recipient.go @@ -0,0 +1,63 @@ +package models + +import ( + "encoding/json" + "fmt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/bsontype" +) + +// DynamicRecipients keeps compatibility with the temporary PR schema +// [{"value":"{{.payload.user.email}}","identity_type":"email"}]. +type DynamicRecipients []string + +type legacyDynamicRecipient struct { + Value string `bson:"value" json:"value"` + IdentityType string `bson:"identity_type" json:"identity_type"` +} + +func (r *DynamicRecipients) UnmarshalJSON(data []byte) error { + var recipients []string + if err := json.Unmarshal(data, &recipients); err == nil { + *r = recipients + return nil + } + + var legacyRecipients []legacyDynamicRecipient + if err := json.Unmarshal(data, &legacyRecipients); err != nil { + return fmt.Errorf("failed to unmarshal dynamic recipients: %w", err) + } + + *r = legacyDynamicRecipientsToStrings(legacyRecipients) + return nil +} + +func (r *DynamicRecipients) UnmarshalBSONValue(t bsontype.Type, data []byte) error { + if t == bsontype.Null || t == bsontype.Undefined { + *r = nil + return nil + } + + var recipients []string + if err := (bson.RawValue{Type: t, Value: data}).Unmarshal(&recipients); err == nil { + *r = recipients + return nil + } + + var legacyRecipients []legacyDynamicRecipient + if err := (bson.RawValue{Type: t, Value: data}).Unmarshal(&legacyRecipients); err != nil { + return fmt.Errorf("failed to unmarshal dynamic recipients: %w", err) + } + + *r = legacyDynamicRecipientsToStrings(legacyRecipients) + return nil +} + +func legacyDynamicRecipientsToStrings(recipients []legacyDynamicRecipient) []string { + resp := make([]string, 0, len(recipients)) + for _, recipient := range recipients { + resp = append(resp, recipient.Value) + } + return resp +} diff --git a/pkg/microservice/aslan/core/common/repository/models/wokflow_task_v4.go b/pkg/microservice/aslan/core/common/repository/models/wokflow_task_v4.go index a1544c0a53..cb2a696d4d 100644 --- a/pkg/microservice/aslan/core/common/repository/models/wokflow_task_v4.go +++ b/pkg/microservice/aslan/core/common/repository/models/wokflow_task_v4.go @@ -783,6 +783,7 @@ type LarkChat struct { type JobTaskNotificationSpec struct { WebHookType setting.NotifyWebHookType `bson:"webhook_type" yaml:"webhook_type" json:"webhook_type"` + LarkHookNotificationConfig *LarkHookNotificationConfig `bson:"lark_hook_notification_config,omitempty" yaml:"lark_hook_notification_config,omitempty" json:"lark_hook_notification_config,omitempty"` LarkGroupNotificationConfig *LarkGroupNotificationConfig `bson:"lark_group_notification_config,omitempty" yaml:"lark_group_notification_config,omitempty" json:"lark_group_notification_config,omitempty"` LarkPersonNotificationConfig *LarkPersonNotificationConfig `bson:"lark_person_notification_config,omitempty" yaml:"lark_person_notification_config,omitempty" json:"lark_person_notification_config,omitempty"` WechatNotificationConfig *WechatNotificationConfig `bson:"wechat_notification_config,omitempty" yaml:"wechat_notification_config,omitempty" json:"wechat_notification_config,omitempty"` diff --git a/pkg/microservice/aslan/core/common/repository/models/workflow.go b/pkg/microservice/aslan/core/common/repository/models/workflow.go index d9756ba13b..d35d5a2272 100644 --- a/pkg/microservice/aslan/core/common/repository/models/workflow.go +++ b/pkg/microservice/aslan/core/common/repository/models/workflow.go @@ -478,14 +478,19 @@ type HookPayload struct { Owner string `bson:"owner" json:"owner,omitempty"` Repo string `bson:"repo" json:"repo,omitempty"` Branch string `bson:"branch" json:"branch,omitempty"` + TargetBranch string `bson:"target_branch" json:"target_branch,omitempty"` Ref string `bson:"ref" json:"ref,omitempty"` IsPr bool `bson:"is_pr" json:"is_pr,omitempty"` CheckRunID int64 `bson:"check_run_id" json:"check_run_id,omitempty"` MergeRequestID string `bson:"merge_request_id" json:"merge_request_id,omitempty"` CommitID string `bson:"commit_id" json:"commit_id,omitempty"` + CommitSHA string `bson:"commit_sha" json:"commit_sha,omitempty"` + CommitMessage string `bson:"commit_message" json:"commit_message,omitempty"` + Committer string `bson:"committer" json:"committer,omitempty"` DeliveryID string `bson:"delivery_id" json:"delivery_id,omitempty"` CodehostID int `bson:"codehost_id" json:"codehost_id"` EventType string `bson:"event_type" json:"event_type"` + RawPayload string `bson:"raw_payload" json:"raw_payload,omitempty"` } type TargetArgs struct { 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 dd7457eed5..21cf3004ae 100644 --- a/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go +++ b/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go @@ -1163,12 +1163,12 @@ type NotificationJobSpec struct { LarkGroupNotificationConfig *LarkGroupNotificationConfig `bson:"lark_group_notification_config,omitempty" yaml:"lark_group_notification_config,omitempty" json:"lark_group_notification_config,omitempty"` LarkPersonNotificationConfig *LarkPersonNotificationConfig `bson:"lark_person_notification_config,omitempty" yaml:"lark_person_notification_config,omitempty" json:"lark_person_notification_config,omitempty"` - //LarkHookNotificationConfig *LarkHookNotificationConfig `bson:"lark_hook_notification_config,omitempty" yaml:"lark_hook_notification_config,omitempty" json:"lark_hook_notification_config,omitempty"` - WechatNotificationConfig *WechatNotificationConfig `bson:"wechat_notification_config,omitempty" yaml:"wechat_notification_config,omitempty" json:"wechat_notification_config,omitempty"` - DingDingNotificationConfig *DingDingNotificationConfig `bson:"dingding_notification_config,omitempty" yaml:"dingding_notification_config,omitempty" json:"dingding_notification_config,omitempty"` - MSTeamsNotificationConfig *MSTeamsNotificationConfig `bson:"msteams_notification_config,omitempty" yaml:"msteams_notification_config,omitempty" json:"msteams_notification_config,omitempty"` - MailNotificationConfig *MailNotificationConfig `bson:"mail_notification_config,omitempty" yaml:"mail_notification_config,omitempty" json:"mail_notification_config,omitempty"` - WebhookNotificationConfig *WebhookNotificationConfig `bson:"webhook_notification_config,omitempty" yaml:"webhook_notification_config,omitempty" json:"webhook_notification_config,omitempty"` + LarkHookNotificationConfig *LarkHookNotificationConfig `bson:"lark_hook_notification_config,omitempty" yaml:"lark_hook_notification_config,omitempty" json:"lark_hook_notification_config,omitempty"` + WechatNotificationConfig *WechatNotificationConfig `bson:"wechat_notification_config,omitempty" yaml:"wechat_notification_config,omitempty" json:"wechat_notification_config,omitempty"` + DingDingNotificationConfig *DingDingNotificationConfig `bson:"dingding_notification_config,omitempty" yaml:"dingding_notification_config,omitempty" json:"dingding_notification_config,omitempty"` + MSTeamsNotificationConfig *MSTeamsNotificationConfig `bson:"msteams_notification_config,omitempty" yaml:"msteams_notification_config,omitempty" json:"msteams_notification_config,omitempty"` + MailNotificationConfig *MailNotificationConfig `bson:"mail_notification_config,omitempty" yaml:"mail_notification_config,omitempty" json:"mail_notification_config,omitempty"` + WebhookNotificationConfig *WebhookNotificationConfig `bson:"webhook_notification_config,omitempty" yaml:"webhook_notification_config,omitempty" json:"webhook_notification_config,omitempty"` Content string `bson:"content" yaml:"content" json:"content"` Title string `bson:"title" yaml:"title" json:"title"` @@ -1252,6 +1252,10 @@ func (n *NotificationJobSpec) GenerateNewNotifyConfigWithOldData() error { if n.LarkPersonNotificationConfig == nil { return fmt.Errorf("lark_person_notification_config cannot be empty for type feishu_person notification") } + case setting.NotifyWebHookTypeFeishu: + if n.LarkHookNotificationConfig == nil { + return fmt.Errorf("lark_hook_notification_config cannot be empty for type feishu notification") + } default: // TODO: this code is commented because of chagee old data. uncomment it if possible //return fmt.Errorf("unsupported notification type: %s", n.WebHookType) @@ -1262,42 +1266,50 @@ func (n *NotificationJobSpec) GenerateNewNotifyConfigWithOldData() error { // TODO: why is_at_all? it could be done in backend type LarkGroupNotificationConfig struct { - AppID string `bson:"app_id" json:"app_id" yaml:"app_id"` - Chat *LarkChat `bson:"chat" json:"chat" yaml:"chat"` - AtUsers []*lark.UserInfo `bson:"at_users" json:"at_users" yaml:"at_users"` - IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` + AppID string `bson:"app_id" json:"app_id" yaml:"app_id"` + Chat *LarkChat `bson:"chat" json:"chat" yaml:"chat"` + AtUsers []*lark.UserInfo `bson:"at_users" json:"at_users" yaml:"at_users"` + DynamicRecipients DynamicRecipients `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` + IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` } type LarkPersonNotificationConfig struct { - AppID string `bson:"app_id" json:"app_id" yaml:"app_id"` - TargetUsers []*lark.UserInfo `bson:"target_users" json:"target_users" yaml:"target_users"` + AppID string `bson:"app_id" json:"app_id" yaml:"app_id"` + TargetUsers []*lark.UserInfo `bson:"target_users" json:"target_users" yaml:"target_users"` + DynamicRecipients DynamicRecipients `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` } type LarkHookNotificationConfig struct { - HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` - AtUsers []string `bson:"at_users" json:"at_users" yaml:"at_users"` - IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` + AppID string `bson:"app_id" json:"app_id" yaml:"app_id"` + HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` + AtUsers []string `bson:"at_users" json:"at_users" yaml:"at_users"` + DynamicRecipients DynamicRecipients `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` + IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` } type WechatNotificationConfig struct { - HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` - AtUsers []string `bson:"at_users" json:"at_users" yaml:"at_users"` - IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` + HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` + AtUsers []string `bson:"at_users" json:"at_users" yaml:"at_users"` + DynamicRecipients DynamicRecipients `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` + IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` } type DingDingNotificationConfig struct { - HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` - AtMobiles []string `bson:"at_mobiles" json:"at_mobiles" yaml:"at_mobiles"` - IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` + HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` + AtMobiles []string `bson:"at_mobiles" json:"at_mobiles" yaml:"at_mobiles"` + DynamicRecipients DynamicRecipients `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` + IsAtAll bool `bson:"is_at_all" json:"is_at_all" yaml:"is_at_all"` } type MSTeamsNotificationConfig struct { - HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` - AtEmails []string `bson:"at_emails" json:"at_emails" yaml:"at_emails"` + HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"` + AtEmails []string `bson:"at_emails" json:"at_emails" yaml:"at_emails"` + DynamicRecipients DynamicRecipients `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` } type MailNotificationConfig struct { - TargetUsers []*User `bson:"target_users" json:"target_users" yaml:"target_users"` + TargetUsers []*User `bson:"target_users" json:"target_users" yaml:"target_users"` + DynamicRecipients DynamicRecipients `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"` } type WebhookNotificationConfig struct { diff --git a/pkg/microservice/aslan/core/common/service/dynamicrecipient/dynamic_recipient.go b/pkg/microservice/aslan/core/common/service/dynamicrecipient/dynamic_recipient.go new file mode 100644 index 0000000000..16731d1748 --- /dev/null +++ b/pkg/microservice/aslan/core/common/service/dynamicrecipient/dynamic_recipient.go @@ -0,0 +1,686 @@ +package dynamicrecipient + +import ( + "fmt" + "strings" + + "github.com/samber/lo" + + commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" + larkservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/lark" + "github.com/koderover/zadig/v2/pkg/setting" + userclient "github.com/koderover/zadig/v2/pkg/shared/client/user" + larktool "github.com/koderover/zadig/v2/pkg/tool/lark" + util2 "github.com/koderover/zadig/v2/pkg/util" +) + +type dynamicRecipientKind string + +const ( + dynamicRecipientKindEmail dynamicRecipientKind = "email" + dynamicRecipientKindMobile dynamicRecipientKind = "mobile" + dynamicRecipientKindAccount dynamicRecipientKind = "account" + dynamicRecipientKindUserID dynamicRecipientKind = "user_id" + dynamicRecipientKindOpenID dynamicRecipientKind = "open_id" + + searchAllIdentityType = "*" +) + +var supportedDynamicRecipientKinds = map[setting.NotifyWebHookType]map[dynamicRecipientKind]struct{}{ + setting.NotifyWebhookTypeFeishuApp: { + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, + dynamicRecipientKindAccount: {}, + dynamicRecipientKindUserID: {}, + }, + setting.NotifyWebHookTypeFeishuPerson: { + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, + dynamicRecipientKindAccount: {}, + dynamicRecipientKindUserID: {}, + dynamicRecipientKindOpenID: {}, + }, + setting.NotifyWebHookTypeFeishu: { + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, + dynamicRecipientKindAccount: {}, + dynamicRecipientKindUserID: {}, + }, + setting.NotifyWebHookTypeWechatWork: { + dynamicRecipientKindUserID: {}, + }, + setting.NotifyWebHookTypeDingDing: { + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, + dynamicRecipientKindAccount: {}, + }, + setting.NotifyWebHookTypeMSTeam: { + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, + dynamicRecipientKindAccount: {}, + }, + setting.NotifyWebHookTypeMail: { + dynamicRecipientKindEmail: {}, + dynamicRecipientKindMobile: {}, + dynamicRecipientKindAccount: {}, + }, +} + +type dynamicRecipientSpec struct { + raw string + key string + kind dynamicRecipientKind +} + +type Resolver struct { + keyMap map[string]string + + lookupUsersByAccount func(account string) ([]*userclient.User, error) + lookupUsersByEmail func(email string) ([]*userclient.User, error) + lookupUsersByPhone func(phone string) ([]*userclient.User, error) + + accountUsersCache map[string][]*userclient.User + emailUsersCache map[string][]*userclient.User + phoneUsersCache map[string][]*userclient.User + + larkClientCache map[string]*larktool.Client + larkUserIDCache map[string]string + larkUserMissCache map[string]bool +} + +func ValidateDynamicRecipientsForNotifyType(notifyType setting.NotifyWebHookType, recipients []string) error { + if len(recipients) == 0 { + return nil + } + + supportedKinds, ok := supportedDynamicRecipientKinds[notifyType] + if !ok { + return nil + } + + for _, recipient := range recipients { + spec, err := parseDynamicRecipient(recipient) + if err != nil { + return err + } + if _, ok := supportedKinds[spec.kind]; !ok { + return fmt.Errorf("dynamic recipient %s is not supported for notification type %s", recipient, notifyType) + } + } + + return nil +} + +func ValidateDynamicRecipientsForNotifyConfig(notifyType setting.NotifyWebHookType, appID string, recipients []string) error { + if err := ValidateDynamicRecipientsForNotifyType(notifyType, recipients); err != nil { + return err + } + if notifyType != setting.NotifyWebHookTypeFeishu || strings.TrimSpace(appID) != "" { + return nil + } + + for _, recipient := range recipients { + spec, err := parseDynamicRecipient(recipient) + if err != nil { + return err + } + if spec.kind != dynamicRecipientKindUserID { + return fmt.Errorf("dynamic recipient %s requires app_id for notification type %s", recipient, notifyType) + } + } + + return nil +} + +func NewResolver(keyMap map[string]string) *Resolver { + return &Resolver{ + keyMap: keyMap, + lookupUsersByAccount: func(account string) ([]*userclient.User, error) { + return searchUsersByAccount(account) + }, + lookupUsersByEmail: func(email string) ([]*userclient.User, error) { + return searchUsersByEmail(email) + }, + lookupUsersByPhone: func(phone string) ([]*userclient.User, error) { + return searchUsersByPhone(phone) + }, + accountUsersCache: make(map[string][]*userclient.User), + emailUsersCache: make(map[string][]*userclient.User), + phoneUsersCache: make(map[string][]*userclient.User), + larkClientCache: make(map[string]*larktool.Client), + larkUserIDCache: make(map[string]string), + larkUserMissCache: make(map[string]bool), + } +} + +func (r *Resolver) ResolveEmails(recipients []string) ([]string, error) { + resp := make([]string, 0) + for _, recipient := range recipients { + spec, value, ok, err := r.resolveRecipient(recipient) + if err != nil { + return nil, err + } + if !ok { + continue + } + + switch spec.kind { + case dynamicRecipientKindEmail: + resp = append(resp, value) + case dynamicRecipientKindMobile: + users, err := r.getUsersByPhone(value) + if err != nil { + return nil, err + } + for _, user := range users { + if user != nil && user.Email != "" { + resp = append(resp, user.Email) + } + } + case dynamicRecipientKindAccount: + users, err := r.getUsersByAccount(value) + if err != nil { + return nil, err + } + for _, user := range users { + if user != nil && user.Email != "" { + resp = append(resp, user.Email) + } + } + default: + return nil, fmt.Errorf("dynamic recipient %s cannot be resolved to email", recipient) + } + } + return UniqStrings(resp), nil +} + +func (r *Resolver) ResolveMobiles(recipients []string) ([]string, error) { + resp := make([]string, 0) + for _, recipient := range recipients { + spec, value, ok, err := r.resolveRecipient(recipient) + if err != nil { + return nil, err + } + if !ok { + continue + } + + switch spec.kind { + case dynamicRecipientKindMobile: + resp = append(resp, value) + case dynamicRecipientKindEmail: + users, err := r.getUsersByEmail(value) + if err != nil { + return nil, err + } + for _, user := range users { + if user != nil && user.Phone != "" { + resp = append(resp, user.Phone) + } + } + case dynamicRecipientKindAccount: + users, err := r.getUsersByAccount(value) + if err != nil { + return nil, err + } + for _, user := range users { + if user != nil && user.Phone != "" { + resp = append(resp, user.Phone) + } + } + default: + return nil, fmt.Errorf("dynamic recipient %s cannot be resolved to mobile", recipient) + } + } + return UniqStrings(resp), nil +} + +func (r *Resolver) ResolveDirectValues(recipients []string, supportedKinds ...dynamicRecipientKind) ([]string, error) { + supported := make(map[dynamicRecipientKind]struct{}, len(supportedKinds)) + for _, kind := range supportedKinds { + supported[kind] = struct{}{} + } + + resp := make([]string, 0) + for _, recipient := range recipients { + spec, value, ok, err := r.resolveRecipient(recipient) + if err != nil { + return nil, err + } + if !ok { + continue + } + if _, ok := supported[spec.kind]; !ok { + return nil, fmt.Errorf("dynamic recipient %s is not supported in this notification channel", recipient) + } + resp = append(resp, value) + } + return UniqStrings(resp), nil +} + +func (r *Resolver) ResolveUserIDs(recipients []string) ([]string, error) { + return r.ResolveDirectValues(recipients, dynamicRecipientKindUserID) +} + +func (r *Resolver) ResolveLarkUsers(recipients []string, appID string, allowOpenID bool) ([]*larktool.UserInfo, error) { + if len(recipients) == 0 { + return nil, nil + } + + resp := make([]*larktool.UserInfo, 0) + var client *larktool.Client + getClient := func() (*larktool.Client, error) { + if client != nil { + return client, nil + } + if strings.TrimSpace(appID) == "" { + return nil, fmt.Errorf("app_id is required to resolve lark dynamic recipients by email/mobile/account") + } + var err error + client, err = r.getLarkClient(appID) + if err != nil { + return nil, err + } + return client, nil + } + + for _, recipient := range recipients { + spec, value, ok, err := r.resolveRecipient(recipient) + if err != nil { + return nil, err + } + if !ok { + continue + } + + switch spec.kind { + case dynamicRecipientKindUserID: + resp = append(resp, &larktool.UserInfo{ID: value, IDType: setting.LarkUserID}) + case dynamicRecipientKindOpenID: + if !allowOpenID { + return nil, fmt.Errorf("dynamic recipient %s is not supported in this notification channel", recipient) + } + resp = append(resp, &larktool.UserInfo{ID: value, IDType: setting.LarkUserOpenID}) + case dynamicRecipientKindEmail: + client, err := getClient() + if err != nil { + return nil, err + } + ids, err := r.resolveLarkUserIDsByEmail(client, appID, value) + if err != nil { + return nil, err + } + for _, id := range ids { + resp = append(resp, &larktool.UserInfo{ID: id, IDType: setting.LarkUserID}) + } + case dynamicRecipientKindMobile: + client, err := getClient() + if err != nil { + return nil, err + } + ids, err := r.resolveLarkUserIDsByPhone(client, appID, value) + if err != nil { + return nil, err + } + for _, id := range ids { + resp = append(resp, &larktool.UserInfo{ID: id, IDType: setting.LarkUserID}) + } + case dynamicRecipientKindAccount: + client, err := getClient() + if err != nil { + return nil, err + } + ids, err := r.resolveLarkUserIDsByAccount(client, appID, value) + if err != nil { + return nil, err + } + for _, id := range ids { + resp = append(resp, &larktool.UserInfo{ID: id, IDType: setting.LarkUserID}) + } + default: + return nil, fmt.Errorf("dynamic recipient %s cannot be resolved to lark user", recipient) + } + } + + return UniqLarkUsers(resp), nil +} + +func (r *Resolver) resolveLarkUserIDsByEmail(client *larktool.Client, appID, email string) ([]string, error) { + if id, found, err := r.lookupLarkUserID(client, appID, larktool.QueryTypeEmail, email); err != nil { + return nil, err + } else if found { + return []string{id}, nil + } + + users, err := r.getUsersByEmail(email) + if err != nil { + return nil, err + } + + resp := make([]string, 0) + for _, user := range users { + if user == nil || user.Phone == "" { + continue + } + id, found, err := r.lookupLarkUserID(client, appID, larktool.QueryTypeMobile, user.Phone) + if err != nil { + return nil, err + } + if found { + resp = append(resp, id) + } + } + return UniqStrings(resp), nil +} + +func (r *Resolver) resolveLarkUserIDsByPhone(client *larktool.Client, appID, phone string) ([]string, error) { + if id, found, err := r.lookupLarkUserID(client, appID, larktool.QueryTypeMobile, phone); err != nil { + return nil, err + } else if found { + return []string{id}, nil + } + + users, err := r.getUsersByPhone(phone) + if err != nil { + return nil, err + } + + resp := make([]string, 0) + for _, user := range users { + if user == nil || user.Email == "" { + continue + } + id, found, err := r.lookupLarkUserID(client, appID, larktool.QueryTypeEmail, user.Email) + if err != nil { + return nil, err + } + if found { + resp = append(resp, id) + } + } + return UniqStrings(resp), nil +} + +func (r *Resolver) resolveLarkUserIDsByAccount(client *larktool.Client, appID, account string) ([]string, error) { + users, err := r.getUsersByAccount(account) + if err != nil { + return nil, err + } + + resp := make([]string, 0) + for _, user := range users { + if user == nil { + continue + } + if user.Email != "" { + id, found, err := r.lookupLarkUserID(client, appID, larktool.QueryTypeEmail, user.Email) + if err != nil { + return nil, err + } + if found { + resp = append(resp, id) + continue + } + } + if user.Phone != "" { + id, found, err := r.lookupLarkUserID(client, appID, larktool.QueryTypeMobile, user.Phone) + if err != nil { + return nil, err + } + if found { + resp = append(resp, id) + } + } + } + return UniqStrings(resp), nil +} + +func (r *Resolver) lookupLarkUserID(client *larktool.Client, appID, queryType, value string) (string, bool, error) { + cacheKey := strings.Join([]string{appID, queryType, value}, ":") + if cached, ok := r.larkUserIDCache[cacheKey]; ok { + return cached, true, nil + } + if r.larkUserMissCache[cacheKey] { + return "", false, nil + } + + userInfo, err := client.GetUserIDByEmailOrMobile(queryType, value, setting.LarkUserID) + if err != nil { + if isLarkUserNotFoundErr(err) { + r.larkUserMissCache[cacheKey] = true + return "", false, nil + } + return "", false, err + } + + userID := util2.GetStringFromPointer(userInfo.UserId) + if userID == "" { + r.larkUserMissCache[cacheKey] = true + return "", false, nil + } + + r.larkUserIDCache[cacheKey] = userID + return userID, true, nil +} + +func (r *Resolver) getUsersByAccount(account string) ([]*userclient.User, error) { + if users, ok := r.accountUsersCache[account]; ok { + return users, nil + } + users, err := r.lookupUsersByAccount(account) + if err != nil { + return nil, err + } + r.accountUsersCache[account] = users + return users, nil +} + +func (r *Resolver) getUsersByEmail(email string) ([]*userclient.User, error) { + if users, ok := r.emailUsersCache[email]; ok { + return users, nil + } + users, err := r.lookupUsersByEmail(email) + if err != nil { + return nil, err + } + r.emailUsersCache[email] = users + return users, nil +} + +func (r *Resolver) getUsersByPhone(phone string) ([]*userclient.User, error) { + if users, ok := r.phoneUsersCache[phone]; ok { + return users, nil + } + users, err := r.lookupUsersByPhone(phone) + if err != nil { + return nil, err + } + r.phoneUsersCache[phone] = users + return users, nil +} + +func (r *Resolver) getLarkClient(appID string) (*larktool.Client, error) { + if client, ok := r.larkClientCache[appID]; ok { + return client, nil + } + + client, err := larkservice.GetLarkClientByIMAppID(appID) + if err != nil { + return nil, err + } + + r.larkClientCache[appID] = client + return client, nil +} + +func (r *Resolver) resolveRecipient(raw string) (*dynamicRecipientSpec, string, bool, error) { + spec, err := parseDynamicRecipient(raw) + if err != nil { + return nil, "", false, err + } + + value := strings.TrimSpace(renderNotificationString(spec.raw, r.keyMap)) + if value == "" || strings.Contains(value, "{{.") { + return spec, "", false, nil + } + + return spec, value, true, nil +} + +func BuildMailUsersFromEmails(emails []string) []*commonmodels.User { + resp := make([]*commonmodels.User, 0, len(emails)) + for _, email := range lo.Uniq(emails) { + if email == "" { + continue + } + resp = append(resp, &commonmodels.User{ + Type: "email", + UserName: email, + }) + } + return resp +} + +func UniqMailUsers(users []*commonmodels.User) []*commonmodels.User { + seen := make(map[string]struct{}) + resp := make([]*commonmodels.User, 0, len(users)) + for _, user := range users { + if user == nil { + continue + } + key := user.Type + ":" + switch user.Type { + case "email": + key += user.UserName + case setting.UserTypeGroup: + key += user.GroupID + default: + key += user.UserID + } + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + resp = append(resp, user) + } + return resp +} + +func UniqLarkUsers(users []*larktool.UserInfo) []*larktool.UserInfo { + seen := make(map[string]struct{}) + resp := make([]*larktool.UserInfo, 0, len(users)) + for _, user := range users { + if user == nil || user.ID == "" { + continue + } + key := user.IDType + ":" + user.ID + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + resp = append(resp, user) + } + return resp +} + +func renderNotificationString(input string, keyMap map[string]string) string { + if len(keyMap) == 0 || !strings.Contains(input, "{{.") { + return input + } + pairs := make([]string, 0, len(keyMap)*2) + for key, value := range keyMap { + pairs = append(pairs, "{{."+key+"}}", value) + } + return strings.NewReplacer(pairs...).Replace(input) +} + +func parseDynamicRecipient(input string) (*dynamicRecipientSpec, error) { + input = strings.TrimSpace(input) + if !strings.HasPrefix(input, "{{.") || !strings.HasSuffix(input, "}}") { + return nil, fmt.Errorf("dynamic recipient must be a single template variable, got %s", input) + } + + key := strings.TrimSuffix(strings.TrimPrefix(input, "{{."), "}}") + if key == "" { + return nil, fmt.Errorf("dynamic recipient %s is invalid", input) + } + + parts := strings.Split(strings.ToLower(key), ".") + suffix := parts[len(parts)-1] + + var kind dynamicRecipientKind + switch suffix { + case "email": + kind = dynamicRecipientKindEmail + case "mobile", "phone": + kind = dynamicRecipientKindMobile + case "account": + kind = dynamicRecipientKindAccount + case "user_id", "userid": + kind = dynamicRecipientKindUserID + case "open_id": + kind = dynamicRecipientKindOpenID + default: + return nil, fmt.Errorf("dynamic recipient %s is not supported, only email/mobile(phone)/account/user_id(userid)/open_id are allowed", input) + } + + return &dynamicRecipientSpec{ + raw: input, + key: key, + kind: kind, + }, nil +} + +func searchUsersByAccount(account string) ([]*userclient.User, error) { + resp, err := userclient.New().SearchUser(&userclient.SearchUserArgs{ + Account: account, + IdentityType: searchAllIdentityType, + }) + if err != nil { + return nil, err + } + if resp == nil { + return nil, nil + } + return resp.Users, nil +} + +func searchUsersByEmail(email string) ([]*userclient.User, error) { + resp, err := userclient.New().SearchUser(&userclient.SearchUserArgs{ + Email: email, + }) + if err != nil { + return nil, err + } + if resp == nil { + return nil, nil + } + return resp.Users, nil +} + +func searchUsersByPhone(phone string) ([]*userclient.User, error) { + resp, err := userclient.New().SearchUser(&userclient.SearchUserArgs{ + Phone: phone, + }) + if err != nil { + return nil, err + } + if resp == nil { + return nil, nil + } + return resp.Users, nil +} + +func UniqStrings(items []string) []string { + items = lo.Filter(items, func(item string, _ int) bool { + return item != "" + }) + return lo.Uniq(items) +} + +func isLarkUserNotFoundErr(err error) bool { + if err == nil { + return false + } + return strings.Contains(strings.ToLower(err.Error()), "user not found") +} diff --git a/pkg/microservice/aslan/core/common/service/instantmessage/dingTalk.go b/pkg/microservice/aslan/core/common/service/instantmessage/dingTalk.go index d5bb4eda40..046b314293 100644 --- a/pkg/microservice/aslan/core/common/service/instantmessage/dingTalk.go +++ b/pkg/microservice/aslan/core/common/service/instantmessage/dingTalk.go @@ -19,6 +19,7 @@ package instantmessage import ( "fmt" "net/url" + "strings" ) type DingDingMessage struct { @@ -55,16 +56,38 @@ type DingDingAt struct { } const ( - DingDingMsgType = "actionCard" + DingDingMsgType = "actionCard" + DingDingMarkdownMsgType = "markdown" + dingDingAtContentPrefix = "##### **相关人员**:" ) func (w *Service) sendDingDingMessage(uri, title, content, actionURL string, atMobiles []string, isAtAll bool) error { + message := BuildDingDingMessage(title, content, actionURL, atMobiles, isAtAll) + _, err := w.SendMessageRequest(uri, message) + return err +} + +func BuildDingDingMessage(title, content, actionURL string, atMobiles []string, isAtAll bool) *DingDingMessage { + if len(atMobiles) > 0 || isAtAll { + return &DingDingMessage{ + MsgType: DingDingMarkdownMsgType, + MarkDown: &DingDingMarkDown{ + Title: title, + Text: buildDingDingMarkdownText(content, actionURL, atMobiles, isAtAll), + }, + At: &DingDingAt{ + AtMobiles: atMobiles, + IsAtAll: isAtAll, + }, + } + } + // reference: https://open.dingtalk.com/document/orgapp/message-link-description dingtalkRedirectURL := fmt.Sprintf("dingtalk://dingtalkclient/page/link?url=%s&pc_slide=false", url.QueryEscape(actionURL), ) - message := &DingDingMessage{ + return &DingDingMessage{ MsgType: DingDingMsgType, ActionCard: &DingDingActionCard{ HideAvatar: "0", @@ -78,12 +101,27 @@ func (w *Service) sendDingDingMessage(uri, title, content, actionURL string, atM }, }, }, + At: &DingDingAt{ + AtMobiles: atMobiles, + IsAtAll: isAtAll, + }, + } +} + +func buildDingDingMarkdownText(content, actionURL string, atMobiles []string, isAtAll bool) string { + text := strings.TrimSpace(content) + if actionURL != "" { + text = strings.TrimSpace(fmt.Sprintf("%s\n\n[点击查看更多信息](%s)", text, actionURL)) } - message.At = &DingDingAt{ - AtMobiles: atMobiles, - IsAtAll: isAtAll, + if strings.Contains(text, dingDingAtContentPrefix) { + return text } - _, err := w.SendMessageRequest(uri, message) - return err + if len(atMobiles) > 0 { + return strings.TrimSpace(fmt.Sprintf("%s\n%s @%s", text, dingDingAtContentPrefix, strings.Join(atMobiles, "@"))) + } + if isAtAll { + return strings.TrimSpace(fmt.Sprintf("%s\n%s @所有人", text, dingDingAtContentPrefix)) + } + return text } diff --git a/pkg/microservice/aslan/core/common/service/instantmessage/mail.go b/pkg/microservice/aslan/core/common/service/instantmessage/mail.go index a02128f19d..b6253eb9a5 100644 --- a/pkg/microservice/aslan/core/common/service/instantmessage/mail.go +++ b/pkg/microservice/aslan/core/common/service/instantmessage/mail.go @@ -42,6 +42,39 @@ func (w *Service) sendMailMessage(title, content string, users []*models.User) e log.Errorf("sendMailMessage GetEmailService error, error msg:%s", err) } + sentEmails := make(map[string]struct{}) + sendToEmail := func(to string) { + if to == "" { + return + } + if _, ok := sentEmails[to]; ok { + return + } + sentEmails[to] = struct{}{} + sendErr := mail.SendEmail(&mail.EmailParams{ + From: emailSvc.Address, + To: to, + Subject: title, + Host: email.Name, + UserName: email.UserName, + Password: email.Password, + Port: email.Port, + TlsSkipVerify: email.TlsSkipVerify, + Body: content, + }) + if sendErr != nil { + err = sendErr + log.Errorf("sendMailMessage SendEmail error, error msg:%s", sendErr) + } + } + + for _, u := range users { + if u == nil || u.Type != "email" { + continue + } + sendToEmail(u.UserName) + } + users, userMap := util.GeneFlatUsers(users) for _, u := range users { info, ok := userMap[u.UserID] @@ -57,21 +90,7 @@ func (w *Service) sendMailMessage(title, content string, users []*models.User) e log.Warnf("sendMailMessage user %s email is empty", info.Name) continue } - err = mail.SendEmail(&mail.EmailParams{ - From: emailSvc.Address, - To: info.Email, - Subject: title, - Host: email.Name, - UserName: email.UserName, - Password: email.Password, - Port: email.Port, - TlsSkipVerify: email.TlsSkipVerify, - Body: content, - }) - if err != nil { - log.Errorf("sendMailMessage SendEmail error, error msg:%s", err) - continue - } + sendToEmail(info.Email) } return err diff --git a/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go b/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go index b1d5b7b1c9..0f092c4a55 100644 --- a/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go +++ b/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go @@ -36,6 +36,7 @@ import ( "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" commonrepo "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb" templaterepo "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb/template" + "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/dynamicrecipient" larkservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/lark" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/webhooknotify" commonutil "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util" @@ -261,6 +262,11 @@ func (w *Service) SendWorkflowTaskApproveNotifications(workflowName string, task return errors.New(errMsg) } + if err := resolveWorkflowNotifyDynamicRecipients(task, notify); err != nil { + log.Errorf("failed to resolve workflow notification dynamic recipients, err: %s", err) + continue + } + if notify.WebHookType == setting.NotifyWebHookTypeMail { if task.TaskCreatorID != "" { for _, user := range notify.MailUsers { @@ -371,6 +377,11 @@ func (w *Service) SendWorkflowTaskNotifications(task *models.WorkflowTask) error return errors.New(errMsg) } + if err := resolveWorkflowNotifyDynamicRecipients(task, notify); err != nil { + log.Errorf("failed to resolve workflow notification dynamic recipients, err: %s", err) + continue + } + if notify.WebHookType == setting.NotifyWebHookTypeMail { if task.TaskCreatorID != "" { for _, user := range notify.MailNotificationConfig.TargetUsers { @@ -440,6 +451,90 @@ func (w *Service) SendWorkflowTaskNotifications(task *models.WorkflowTask) error return nil } +func resolveWorkflowNotifyDynamicRecipients(task *models.WorkflowTask, notify *models.NotifyCtl) error { + if task == nil || notify == nil { + return nil + } + + workflowArgs := task.WorkflowArgs + if workflowArgs == nil { + workflowArgs = task.OriginWorkflowArgs + } + if workflowArgs == nil { + return nil + } + + keyMap := commonutil.KeyValsToMap(commonutil.BuildWorkflowRuntimeVariableKVs( + workflowArgs, + task.ProjectName, + task.ProjectDisplayName, + task.TaskID, + task.TaskCreator, + task.TaskCreatorAccount, + task.TaskCreatorID, + time.Unix(task.StartTime, 0), + )) + resolver := dynamicrecipient.NewResolver(keyMap) + + if cfg := notify.LarkHookNotificationConfig; cfg != nil { + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, false) + if err != nil { + return err + } + for _, user := range users { + if user == nil || user.ID == "" { + continue + } + cfg.AtUsers = append(cfg.AtUsers, user.ID) + } + cfg.AtUsers = dynamicrecipient.UniqStrings(cfg.AtUsers) + } + if cfg := notify.LarkGroupNotificationConfig; cfg != nil { + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, false) + if err != nil { + return err + } + cfg.AtUsers = dynamicrecipient.UniqLarkUsers(append(cfg.AtUsers, users...)) + } + if cfg := notify.LarkPersonNotificationConfig; cfg != nil { + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, true) + if err != nil { + return err + } + cfg.TargetUsers = dynamicrecipient.UniqLarkUsers(append(cfg.TargetUsers, users...)) + } + if cfg := notify.MSTeamsNotificationConfig; cfg != nil { + emails, err := resolver.ResolveEmails([]string(cfg.DynamicRecipients)) + if err != nil { + return err + } + cfg.AtEmails = dynamicrecipient.UniqStrings(append(cfg.AtEmails, emails...)) + } + if cfg := notify.MailNotificationConfig; cfg != nil { + emails, err := resolver.ResolveEmails([]string(cfg.DynamicRecipients)) + if err != nil { + return err + } + cfg.TargetUsers = dynamicrecipient.UniqMailUsers(append(cfg.TargetUsers, dynamicrecipient.BuildMailUsersFromEmails(emails)...)) + } + if cfg := notify.DingDingNotificationConfig; cfg != nil { + mobiles, err := resolver.ResolveMobiles([]string(cfg.DynamicRecipients)) + if err != nil { + return err + } + cfg.AtMobiles = dynamicrecipient.UniqStrings(append(cfg.AtMobiles, mobiles...)) + } + if cfg := notify.WechatNotificationConfig; cfg != nil { + users, err := resolver.ResolveUserIDs([]string(cfg.DynamicRecipients)) + if err != nil { + return err + } + cfg.AtUsers = dynamicrecipient.UniqStrings(append(cfg.AtUsers, users...)) + } + + return nil +} + func (w *Service) SendManualExecStageNotifications(workflowCtx *models.WorkflowTaskCtx, stage *models.StageTask) error { if workflowCtx == nil || stage == nil || stage.ManualExec == nil { return nil @@ -909,7 +1004,7 @@ func (w *Service) getApproveNotificationContent(notify *models.NotifyCtl, task * } else if notify.WebHookType != setting.NotifyWebHookTypeFeishu && notify.WebHookType != setting.NotifyWebhookTypeFeishuApp && notify.WebHookType != setting.NotifyWebHookTypeFeishuPerson { tplcontent := strings.Join(tplBaseInfo, "") tplcontent += strings.Join(jobContents, "") - tplcontent = tplcontent + getNotifyAtContent(notify) + tplcontent = appendInlineNotifyAtContent(tplcontent, notify) tplcontent = fmt.Sprintf("%s%s", title, tplcontent) if notify.WebHookType == setting.NotifyWebHookTypeWechatWork { tplcontent = fmt.Sprintf("%s%s", tplcontent, moreInformation) @@ -1166,7 +1261,7 @@ func (w *Service) getNotificationContentWithOptions(notify *models.NotifyCtl, ta } else if notify.WebHookType != setting.NotifyWebHookTypeFeishu && notify.WebHookType != setting.NotifyWebhookTypeFeishuApp && notify.WebHookType != setting.NotifyWebHookTypeFeishuPerson { tplcontent := strings.Join(tplBaseInfo, "") tplcontent += strings.Join(jobContents, "") - tplcontent = tplcontent + getNotifyAtContent(notify) + tplcontent = appendInlineNotifyAtContent(tplcontent, notify) tplcontent = fmt.Sprintf("%s%s", title, tplcontent) if notify.WebHookType == setting.NotifyWebHookTypeWechatWork { tplcontent = fmt.Sprintf("%s%s", tplcontent, moreInformation) @@ -1489,6 +1584,13 @@ func genSonartMetricsText(jobSpec *models.JobTaskFreestyleSpec, language string) return result, mailResult, nil } +func appendInlineNotifyAtContent(content string, notify *models.NotifyCtl) string { + if notify == nil || notify.WebHookType == setting.NotifyWebHookTypeDingDing { + return content + } + return content + getNotifyAtContent(notify) +} + func (w *Service) sendNotification(title, content string, notify *models.NotifyCtl, card *LarkCard, webhookNotify *webhooknotify.WorkflowNotify, taskStatus config.Status) error { link := "" if notify.WebHookType == setting.NotifyWebHookTypeDingDing || notify.WebHookType == setting.NotifyWebHookTypeWechatWork || notify.WebHookType == setting.NotifyWebHookTypeMSTeam { diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job.go index ce6c3199cb..b44a1258e3 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job.go @@ -46,6 +46,140 @@ type JobCtl interface { SaveInfo(ctx context.Context) error } +type notificationRuntimeRenderFields struct { + Title string + Content string + + LarkHookAtUsers []string + WechatAtUsers []string + DingDingMobiles []string + MSTeamsAtEmails []string + LarkHookDynamic commonmodels.DynamicRecipients + LarkGroupDynamic commonmodels.DynamicRecipients + LarkPersonDynamic commonmodels.DynamicRecipients + WechatDynamic commonmodels.DynamicRecipients + DingDingDynamic commonmodels.DynamicRecipients + MSTeamsDynamic commonmodels.DynamicRecipients + MailDynamic commonmodels.DynamicRecipients +} + +func cloneNotificationStrings(items []string) []string { + if items == nil { + return nil + } + resp := make([]string, len(items)) + copy(resp, items) + return resp +} + +func cloneNotificationDynamicRecipients(items commonmodels.DynamicRecipients) commonmodels.DynamicRecipients { + if items == nil { + return nil + } + resp := make(commonmodels.DynamicRecipients, len(items)) + copy(resp, items) + return resp +} + +func backupNotificationRuntimeRenderFields(job *commonmodels.JobTask) (*notificationRuntimeRenderFields, error) { + if job == nil || job.JobType != string(config.JobNotification) { + return nil, nil + } + + spec, err := decodeNotificationJobTaskSpec(job.Spec) + if err != nil { + return nil, err + } + + resp := ¬ificationRuntimeRenderFields{ + Title: spec.Title, + Content: spec.Content, + } + + if cfg := spec.LarkHookNotificationConfig; cfg != nil { + resp.LarkHookAtUsers = cloneNotificationStrings(cfg.AtUsers) + resp.LarkHookDynamic = cloneNotificationDynamicRecipients(cfg.DynamicRecipients) + } + if cfg := spec.LarkGroupNotificationConfig; cfg != nil { + resp.LarkGroupDynamic = cloneNotificationDynamicRecipients(cfg.DynamicRecipients) + } + if cfg := spec.LarkPersonNotificationConfig; cfg != nil { + resp.LarkPersonDynamic = cloneNotificationDynamicRecipients(cfg.DynamicRecipients) + } + if cfg := spec.WechatNotificationConfig; cfg != nil { + resp.WechatAtUsers = cloneNotificationStrings(cfg.AtUsers) + resp.WechatDynamic = cloneNotificationDynamicRecipients(cfg.DynamicRecipients) + } + if cfg := spec.DingDingNotificationConfig; cfg != nil { + resp.DingDingMobiles = cloneNotificationStrings(cfg.AtMobiles) + resp.DingDingDynamic = cloneNotificationDynamicRecipients(cfg.DynamicRecipients) + } + if cfg := spec.MSTeamsNotificationConfig; cfg != nil { + resp.MSTeamsAtEmails = cloneNotificationStrings(cfg.AtEmails) + resp.MSTeamsDynamic = cloneNotificationDynamicRecipients(cfg.DynamicRecipients) + } + if cfg := spec.MailNotificationConfig; cfg != nil { + resp.MailDynamic = cloneNotificationDynamicRecipients(cfg.DynamicRecipients) + } + + return resp, nil +} + +func restoreNotificationRuntimeRenderFields(job *commonmodels.JobTask, fields *notificationRuntimeRenderFields) (*commonmodels.JobTaskNotificationSpec, error) { + if job == nil || fields == nil || job.JobType != string(config.JobNotification) { + return nil, nil + } + + spec, err := decodeNotificationJobTaskSpec(job.Spec) + if err != nil { + return nil, err + } + + spec.Title = fields.Title + spec.Content = fields.Content + + if cfg := spec.LarkHookNotificationConfig; cfg != nil { + cfg.AtUsers = cloneNotificationStrings(fields.LarkHookAtUsers) + cfg.DynamicRecipients = cloneNotificationDynamicRecipients(fields.LarkHookDynamic) + } + if cfg := spec.LarkGroupNotificationConfig; cfg != nil { + cfg.DynamicRecipients = cloneNotificationDynamicRecipients(fields.LarkGroupDynamic) + } + if cfg := spec.LarkPersonNotificationConfig; cfg != nil { + cfg.DynamicRecipients = cloneNotificationDynamicRecipients(fields.LarkPersonDynamic) + } + if cfg := spec.WechatNotificationConfig; cfg != nil { + cfg.AtUsers = cloneNotificationStrings(fields.WechatAtUsers) + cfg.DynamicRecipients = cloneNotificationDynamicRecipients(fields.WechatDynamic) + } + if cfg := spec.DingDingNotificationConfig; cfg != nil { + cfg.AtMobiles = cloneNotificationStrings(fields.DingDingMobiles) + cfg.DynamicRecipients = cloneNotificationDynamicRecipients(fields.DingDingDynamic) + } + if cfg := spec.MSTeamsNotificationConfig; cfg != nil { + cfg.AtEmails = cloneNotificationStrings(fields.MSTeamsAtEmails) + cfg.DynamicRecipients = cloneNotificationDynamicRecipients(fields.MSTeamsDynamic) + } + if cfg := spec.MailNotificationConfig; cfg != nil { + cfg.DynamicRecipients = cloneNotificationDynamicRecipients(fields.MailDynamic) + } + + job.Spec = spec + return spec, nil +} + +func decodeNotificationJobTaskSpec(raw interface{}) (*commonmodels.JobTaskNotificationSpec, error) { + if spec, ok := raw.(*commonmodels.JobTaskNotificationSpec); ok && spec != nil { + return spec, nil + } + + spec := &commonmodels.JobTaskNotificationSpec{} + if err := commonmodels.IToi(raw, spec); err != nil { + return nil, err + } + return spec, nil +} + func initJobCtl(job *commonmodels.JobTask, workflowCtx *commonmodels.WorkflowTaskCtx, logger *zap.SugaredLogger, ack func()) JobCtl { var jobCtl JobCtl switch job.JobType { @@ -173,6 +307,14 @@ func runJob(ctx context.Context, job *commonmodels.JobTask, workflowCtx *commonm return true }) + notificationFields, err := backupNotificationRuntimeRenderFields(job) + if err != nil { + logger.Errorf("backup notification runtime fields error: %v", err) + job.Status = config.StatusFailed + job.Error = err.Error() + return + } + // remove all the unrendered variable, replacing then with empty string b, _ := json.Marshal(job) variableRegexp := regexp.MustCompile(config.VariableRegEx) @@ -183,6 +325,17 @@ func runJob(ctx context.Context, job *commonmodels.JobTask, workflowCtx *commonm job.Error = err.Error() return } + if restoredSpec, err := restoreNotificationRuntimeRenderFields(job, notificationFields); err != nil { + logger.Errorf("restore notification runtime fields error: %v", err) + job.Status = config.StatusFailed + job.Error = err.Error() + return + } else if restoredSpec != nil { + if ctl, ok := jobCtl.(*NotificationJobCtl); ok { + ctl.jobTaskSpec = restoredSpec + ctl.job.Spec = restoredSpec + } + } // Check execute policy before running the job if !shouldExecuteJob(job) { diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go index dd2053f0e7..6d7ec3704d 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_notification.go @@ -33,6 +33,7 @@ import ( "github.com/koderover/zadig/v2/pkg/microservice/aslan/config" commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb" + "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/dynamicrecipient" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/instantmessage" larkservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/lark" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util" @@ -75,6 +76,14 @@ func (c *NotificationJobCtl) Run(ctx context.Context) { c.job.Status = config.StatusRunning c.ack() + if err := c.prepareRuntimeNotificationFields(); err != nil { + c.logger.Error(err) + c.job.Status = config.StatusFailed + c.job.Error = err.Error() + c.ack() + return + } + if c.jobTaskSpec.WebHookType == setting.NotifyWebhookTypeFeishuApp { larkAtUserIDs := make([]string, 0) @@ -109,6 +118,15 @@ func (c *NotificationJobCtl) Run(ctx context.Context) { c.ack() return } + } else if c.jobTaskSpec.WebHookType == setting.NotifyWebHookTypeFeishu { + err := sendLarkHookMessage(c.workflowCtx.ProjectName, c.workflowCtx.WorkflowName, c.workflowCtx.WorkflowDisplayName, c.workflowCtx.TaskID, c.jobTaskSpec.LarkHookNotificationConfig.HookAddress, c.jobTaskSpec.Title, c.jobTaskSpec.Content, c.jobTaskSpec.LarkHookNotificationConfig.AtUsers, c.jobTaskSpec.LarkHookNotificationConfig.IsAtAll) + if err != nil { + c.logger.Error(err) + c.job.Status = config.StatusFailed + c.job.Error = err.Error() + c.ack() + return + } } else if c.jobTaskSpec.WebHookType == setting.NotifyWebHookTypeMSTeam { err := sendMSTeamsMessage(c.workflowCtx.ProjectName, c.workflowCtx.WorkflowName, c.workflowCtx.WorkflowDisplayName, c.workflowCtx.TaskID, c.jobTaskSpec.MSTeamsNotificationConfig.HookAddress, c.jobTaskSpec.Title, c.jobTaskSpec.Content, c.jobTaskSpec.MSTeamsNotificationConfig.AtEmails) if err != nil { @@ -207,6 +225,145 @@ func (c *NotificationJobCtl) Run(ctx context.Context) { return } +func (c *NotificationJobCtl) prepareRuntimeNotificationFields() error { + keyMap := c.buildRuntimeNotificationKeyMap() + recipientKeyMap := c.buildRuntimeNotificationRecipientKeyMap() + + c.jobTaskSpec.Title = renderNotificationString(c.jobTaskSpec.Title, keyMap) + c.jobTaskSpec.Content = renderNotificationString(c.jobTaskSpec.Content, keyMap) + + if cfg := c.jobTaskSpec.LarkHookNotificationConfig; cfg != nil { + cfg.AtUsers = renderNotificationStrings(cfg.AtUsers, recipientKeyMap) + } + if cfg := c.jobTaskSpec.DingDingNotificationConfig; cfg != nil { + cfg.AtMobiles = renderNotificationStrings(cfg.AtMobiles, recipientKeyMap) + } + if cfg := c.jobTaskSpec.WechatNotificationConfig; cfg != nil { + cfg.AtUsers = renderNotificationStrings(cfg.AtUsers, recipientKeyMap) + } + if cfg := c.jobTaskSpec.MSTeamsNotificationConfig; cfg != nil { + cfg.AtEmails = renderNotificationStrings(cfg.AtEmails, recipientKeyMap) + } + return c.resolveDynamicRecipients(recipientKeyMap) +} + +func (c *NotificationJobCtl) buildRuntimeNotificationKeyMap() map[string]string { + keyMap := util.KeyValsToMap(c.workflowCtx.WorkflowKeyVals) + for key := range keyMap { + if strings.HasPrefix(key, "payload.") { + delete(keyMap, key) + } + } + return keyMap +} + +func (c *NotificationJobCtl) buildRuntimeNotificationRecipientKeyMap() map[string]string { + return util.KeyValsToMap(c.workflowCtx.WorkflowKeyVals) +} + +func renderNotificationStrings(inputs []string, keyMap map[string]string) []string { + if len(keyMap) == 0 { + return inputs + } + pairs := make([]string, 0, len(keyMap)*2) + for key, value := range keyMap { + pairs = append(pairs, "{{."+key+"}}", value) + } + replacer := strings.NewReplacer(pairs...) + + resp := make([]string, 0, len(inputs)) + for _, item := range inputs { + resp = append(resp, replacer.Replace(item)) + } + return resp +} + +func (c *NotificationJobCtl) resolveDynamicRecipients(keyMap map[string]string) error { + resolver := dynamicrecipient.NewResolver(keyMap) + + if cfg := c.jobTaskSpec.LarkHookNotificationConfig; cfg != nil { + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, false) + if err != nil { + return err + } + for _, user := range users { + if user == nil || user.ID == "" { + continue + } + cfg.AtUsers = append(cfg.AtUsers, user.ID) + } + cfg.AtUsers = lo.Uniq(cfg.AtUsers) + } + if cfg := c.jobTaskSpec.LarkGroupNotificationConfig; cfg != nil { + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, false) + if err != nil { + return err + } + cfg.AtUsers = dynamicrecipient.UniqLarkUsers(append(cfg.AtUsers, users...)) + } + if cfg := c.jobTaskSpec.LarkPersonNotificationConfig; cfg != nil { + users, err := resolver.ResolveLarkUsers([]string(cfg.DynamicRecipients), cfg.AppID, true) + if err != nil { + return err + } + cfg.TargetUsers = dynamicrecipient.UniqLarkUsers(append(cfg.TargetUsers, users...)) + } + if cfg := c.jobTaskSpec.MSTeamsNotificationConfig; cfg != nil { + emails, err := resolver.ResolveEmails([]string(cfg.DynamicRecipients)) + if err != nil { + return err + } + cfg.AtEmails = lo.Uniq(append(cfg.AtEmails, emails...)) + } + if cfg := c.jobTaskSpec.MailNotificationConfig; cfg != nil { + emails, err := resolver.ResolveEmails([]string(cfg.DynamicRecipients)) + if err != nil { + return err + } + cfg.TargetUsers = dynamicrecipient.UniqMailUsers(append(cfg.TargetUsers, dynamicrecipient.BuildMailUsersFromEmails(emails)...)) + } + if cfg := c.jobTaskSpec.DingDingNotificationConfig; cfg != nil { + mobiles, err := resolver.ResolveMobiles([]string(cfg.DynamicRecipients)) + if err != nil { + return err + } + cfg.AtMobiles = lo.Uniq(append(cfg.AtMobiles, mobiles...)) + } + if cfg := c.jobTaskSpec.WechatNotificationConfig; cfg != nil { + users, err := resolver.ResolveUserIDs([]string(cfg.DynamicRecipients)) + if err != nil { + return err + } + cfg.AtUsers = lo.Uniq(append(cfg.AtUsers, users...)) + } + + return nil +} + +func buildLarkAtMessage(idList []string, isAtAll bool) string { + idList = lo.Filter(idList, func(s string, _ int) bool { return s != "All" }) + atUserList := make([]string, 0, len(idList)) + for _, userID := range idList { + atUserList = append(atUserList, fmt.Sprintf("", userID)) + } + atMessage := strings.Join(atUserList, " ") + if isAtAll { + atMessage += "" + } + return atMessage +} + +func renderNotificationString(input string, keyMap map[string]string) string { + if len(keyMap) == 0 || !strings.Contains(input, "{{.") { + return input + } + pairs := make([]string, 0, len(keyMap)*2) + for key, value := range keyMap { + pairs = append(pairs, "{{."+key+"}}", value) + } + return strings.NewReplacer(pairs...).Replace(input) +} + func sendLarkMessage(client *lark.Client, productName, workflowName, workflowDisplayName string, taskID int64, receiverType, receiverID, title, message string, idList []string, isAtAll bool) error { // first generate lark card card := instantmessage.NewLarkCard() @@ -268,43 +425,71 @@ func sendLarkMessage(client *lark.Client, productName, workflowName, workflowDis return nil } -func sendDingDingMessage(productName, workflowName, workflowDisplayName string, taskID int64, uri, title, message string, idList []string, isAtAll bool) error { - processedMessage := generateDingDingNotificationMessage(title, message, idList) +func sendLarkHookMessage(productName, workflowName, workflowDisplayName string, taskID int64, uri, title, message string, idList []string, isAtAll bool) error { + card := instantmessage.NewLarkCard() + card.SetConfig(true) + card.SetHeader("blue", title, "plain_text") + card.AddI18NElementsZhcnFeild(message, true) - actionURL := fmt.Sprintf("%s/v1/projects/detail/%s/pipelines/custom/%s/%d?display_name=%s", + detailURL := fmt.Sprintf("%s/v1/projects/detail/%s/pipelines/custom/%s/%d?display_name=%s", configbase.SystemAddress(), productName, workflowName, taskID, - url.PathEscape(workflowDisplayName), + workflowDisplayName, ) + card.AddI18NElementsZhcnAction("点击查看更多信息", detailURL) - // reference: https://open.dingtalk.com/document/orgapp/message-link-description - dingtalkRedirectURL := fmt.Sprintf("dingtalk://dingtalkclient/page/link?url=%s&pc_slide=false", - url.QueryEscape(actionURL), - ) + messageReq := instantmessage.LarkCardReq{ + MsgType: "interactive", + Card: card, + } + if _, err := httpclient.New().Post(uri, httpclient.SetBody(messageReq)); err != nil { + return err + } + + if len(idList) == 0 && !isAtAll { + return nil + } - messageReq := instantmessage.DingDingMessage{ - MsgType: instantmessage.DingDingMsgType, - ActionCard: &instantmessage.DingDingActionCard{ - HideAvatar: "0", - ButtonOrientation: "0", - Text: processedMessage, - Title: title, - Buttons: []*instantmessage.DingDingButton{ - { - Title: "点击查看更多信息", - ActionURL: dingtalkRedirectURL, - }, - }, - }, + atUserList := make([]string, 0, len(idList)) + idList = lo.Filter(idList, func(s string, _ int) bool { return s != "All" }) + for _, userID := range idList { + atUserList = append(atUserList, fmt.Sprintf("", userID)) + } + atMessage := strings.Join(atUserList, " ") + if isAtAll { + atMessage += "" } - messageReq.At = &instantmessage.DingDingAt{ - AtMobiles: idList, - IsAtAll: isAtAll, + if strings.Contains(uri, "bot/v2/hook") { + _, err := httpclient.New().Post(uri, httpclient.SetBody(&instantmessage.FeiShuMessageV2{ + MsgType: "text", + Content: instantmessage.FeiShuContentV2{Text: atMessage}, + })) + return err } + _, err := httpclient.New().Post(uri, httpclient.SetBody(&instantmessage.FeiShuMessage{ + Title: "", + Text: atMessage, + })) + return err +} + +func sendDingDingMessage(productName, workflowName, workflowDisplayName string, taskID int64, uri, title, message string, idList []string, isAtAll bool) error { + processedMessage := generateDingDingNotificationMessage(title, message, idList) + + actionURL := fmt.Sprintf("%s/v1/projects/detail/%s/pipelines/custom/%s/%d?display_name=%s", + configbase.SystemAddress(), + productName, + workflowName, + taskID, + url.PathEscape(workflowDisplayName), + ) + + messageReq := instantmessage.BuildDingDingMessage(title, processedMessage, actionURL, idList, isAtAll) + // TODO: if required, add proxy to it c := httpclient.New() @@ -438,7 +623,35 @@ func sendMailMessage(title, message string, users []*commonmodels.User, callerID return err } - users, userMap := util.GeneFlatUsersWithCaller(users, callerID) + directEmailUsers := make([]*commonmodels.User, 0) + lookupUsers := make([]*commonmodels.User, 0) + for _, u := range users { + if u != nil && u.Type == "email" { + directEmailUsers = append(directEmailUsers, u) + continue + } + lookupUsers = append(lookupUsers, u) + } + + users, userMap := util.GeneFlatUsersWithCaller(lookupUsers, callerID) + for _, u := range directEmailUsers { + log.Infof("Sending Mail to email: %s", u.UserName) + err = mail.SendEmail(&mail.EmailParams{ + From: emailSvc.Address, + To: u.UserName, + Subject: title, + Host: email.Name, + UserName: email.UserName, + Password: email.Password, + Port: email.Port, + TlsSkipVerify: email.TlsSkipVerify, + Body: message, + }) + if err != nil { + log.Errorf("sendMailMessage SendEmail error, error msg:%s", err) + } + } + for _, u := range users { log.Infof("Sending Mail to user: %s", u.UserName) info, ok := userMap[u.UserID] diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/workflow.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/workflow.go index 9ef0ec1b5d..45cf6e42e4 100644 --- a/pkg/microservice/aslan/core/common/service/workflowcontroller/workflow.go +++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/workflow.go @@ -25,11 +25,6 @@ import ( "time" "github.com/google/uuid" - "go.uber.org/zap" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/util/rand" - config2 "github.com/koderover/zadig/v2/pkg/config" "github.com/koderover/zadig/v2/pkg/microservice/aslan/config" commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" @@ -39,6 +34,7 @@ import ( "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/scmnotify" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/workflowstat" + commonutil "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util" "github.com/koderover/zadig/v2/pkg/setting" "github.com/koderover/zadig/v2/pkg/tool/cache" "github.com/koderover/zadig/v2/pkg/tool/clientmanager" @@ -47,6 +43,10 @@ import ( "github.com/koderover/zadig/v2/pkg/tool/kube/podexec" "github.com/koderover/zadig/v2/pkg/tool/kube/updater" "github.com/koderover/zadig/v2/pkg/tool/log" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/rand" ) const ( @@ -239,6 +239,7 @@ func (c *workflowCtl) Run(ctx context.Context, concurrency int) { WorkflowTaskCreatorUserID: c.workflowTask.TaskCreatorID, WorkflowTaskCreatorMobile: c.workflowTask.TaskCreatorPhone, WorkflowTaskCreatorEmail: c.workflowTask.TaskCreatorEmail, + WorkflowKeyVals: commonutil.BuildWorkflowRuntimeVariableKVs(c.workflowTask.WorkflowArgs, c.workflowTask.ProjectName, c.workflowTask.ProjectDisplayName, c.workflowTask.TaskID, c.workflowTask.TaskCreator, c.workflowTask.TaskCreatorAccount, c.workflowTask.TaskCreatorID, time.Unix(c.workflowTask.StartTime, 0)), Workspace: "/workspace", DistDir: fmt.Sprintf("%s/%s/dist/%d", config.S3StoragePath(), c.workflowTask.WorkflowName, c.workflowTask.TaskID), DockerMountDir: fmt.Sprintf("/tmp/%s/docker/%d", uuid.NewString(), time.Now().Unix()), diff --git a/pkg/microservice/aslan/core/common/util/workflow_variables.go b/pkg/microservice/aslan/core/common/util/workflow_variables.go new file mode 100644 index 0000000000..8dc18e17e0 --- /dev/null +++ b/pkg/microservice/aslan/core/common/util/workflow_variables.go @@ -0,0 +1,178 @@ +package util + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + "strconv" + "strings" + "time" + + configbase "github.com/koderover/zadig/v2/pkg/config" + commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" +) + +func BuildPayloadVariables(rawPayload string) []*commonmodels.KeyVal { + if rawPayload == "" { + return nil + } + + var payload interface{} + if err := json.Unmarshal([]byte(rawPayload), &payload); err != nil { + return nil + } + + resp := make([]*commonmodels.KeyVal, 0) + flattenPayloadValue("payload", payload, &resp) + return resp +} + +func flattenPayloadValue(prefix string, value interface{}, resp *[]*commonmodels.KeyVal) { + switch val := value.(type) { + case map[string]interface{}: + for key, item := range val { + flattenPayloadValue(prefix+"."+key, item, resp) + } + case []interface{}: + for index, item := range val { + flattenPayloadValue(fmt.Sprintf("%s.%d", prefix, index), item, resp) + } + case string: + *resp = append(*resp, &commonmodels.KeyVal{Key: prefix, Value: val, IsCredential: false}) + case float64: + *resp = append(*resp, &commonmodels.KeyVal{Key: prefix, Value: strconv.FormatFloat(val, 'f', -1, 64), IsCredential: false}) + case bool: + *resp = append(*resp, &commonmodels.KeyVal{Key: prefix, Value: strconv.FormatBool(val), IsCredential: false}) + case nil: + return + default: + *resp = append(*resp, &commonmodels.KeyVal{Key: prefix, Value: fmt.Sprint(val), IsCredential: false}) + } +} + +func BuildWorkflowSystemVariableKVs(workflow *commonmodels.WorkflowV4, projectName, projectDisplayName string, taskID int64, creator, account, uid string, now time.Time) []*commonmodels.KeyVal { + if workflow == nil { + return nil + } + + resp := []*commonmodels.KeyVal{ + {Key: "project", Value: projectName, IsCredential: false}, + {Key: "project.id", Value: projectName, IsCredential: false}, + {Key: "project.name", Value: projectDisplayName, IsCredential: false}, + {Key: "workflow.id", Value: workflow.Name, IsCredential: false}, + {Key: "workflow.name", Value: workflow.DisplayName, IsCredential: false}, + {Key: "workflow.task.id", Value: fmt.Sprintf("%d", taskID), IsCredential: false}, + {Key: "workflow.task.creator", Value: creator, IsCredential: false}, + {Key: "workflow.task.creator.id", Value: account, IsCredential: false}, + {Key: "workflow.task.creator.userId", Value: uid, IsCredential: false}, + {Key: "workflow.task.timestamp", Value: fmt.Sprintf("%d", now.Unix()), IsCredential: false}, + {Key: "workflow.task.datetime", Value: now.Format(time.DateTime), IsCredential: false}, + { + Key: "workflow.task.url", + Value: fmt.Sprintf("%s/v1/projects/detail/%s/pipelines/custom/%s/%d?display_name=%s", configbase.SystemAddress(), projectName, workflow.Name, taskID, url.QueryEscape(workflow.DisplayName)), + IsCredential: false, + }, + } + + for _, param := range workflow.Params { + if param == nil { + continue + } + value := param.Value + if param.ParamsType == string(commonmodels.MultiSelectType) { + value = strings.Join(param.ChoiceValue, ",") + } else if param.ParamsType == string(commonmodels.FileType) { + continue + } + resp = append(resp, &commonmodels.KeyVal{ + Key: strings.Join([]string{"workflow", "params", param.Name}, "."), + Value: value, + IsCredential: false, + }) + } + if workflow.HookPayload != nil { + resp = append(resp, BuildWorkflowTriggerVariableKVs(workflow.HookPayload)...) + resp = append(resp, BuildPayloadVariables(workflow.HookPayload.RawPayload)...) + } + + return resp +} + +func BuildWorkflowTriggerVariableKVs(hookPayload *commonmodels.HookPayload) []*commonmodels.KeyVal { + if hookPayload == nil { + return nil + } + + resp := make([]*commonmodels.KeyVal, 0, 8) + appendIfNotEmpty := func(key, value string) { + if value == "" { + return + } + resp = append(resp, &commonmodels.KeyVal{Key: key, Value: value, IsCredential: false}) + } + + appendIfNotEmpty("workflow.trigger.branch", hookPayload.Branch) + appendIfNotEmpty("workflow.trigger.target_branch", hookPayload.TargetBranch) + appendIfNotEmpty("workflow.trigger.pr", hookPayload.MergeRequestID) + appendIfNotEmpty("workflow.trigger.commit_id", hookPayload.CommitID) + appendIfNotEmpty("workflow.trigger.commit_sha", inferWorkflowTriggerCommitSHA(hookPayload)) + appendIfNotEmpty("workflow.trigger.commit_message", hookPayload.CommitMessage) + appendIfNotEmpty("workflow.trigger.committer", hookPayload.Committer) + appendIfNotEmpty("workflow.trigger.event", hookPayload.EventType) + + return resp +} + +var commitSHARegex = regexp.MustCompile(`^[0-9a-fA-F]{40}$`) + +func inferWorkflowTriggerCommitSHA(hookPayload *commonmodels.HookPayload) string { + if hookPayload == nil { + return "" + } + if hookPayload.CommitSHA != "" { + return hookPayload.CommitSHA + } + if commitSHARegex.MatchString(hookPayload.CommitID) { + return hookPayload.CommitID + } + + type patchSetPayload struct { + Revision string `json:"revision"` + } + type gerritPayload struct { + NewRev string `json:"newRev"` + PatchSet patchSetPayload `json:"patchSet"` + } + + if hookPayload.RawPayload == "" { + return "" + } + + payload := new(gerritPayload) + if err := json.Unmarshal([]byte(hookPayload.RawPayload), payload); err != nil { + return "" + } + if commitSHARegex.MatchString(payload.NewRev) { + return payload.NewRev + } + if commitSHARegex.MatchString(payload.PatchSet.Revision) { + return payload.PatchSet.Revision + } + return "" +} + +func BuildWorkflowRuntimeVariableKVs(workflow *commonmodels.WorkflowV4, projectName, projectDisplayName string, taskID int64, creator, account, uid string, now time.Time) []*commonmodels.KeyVal { + return BuildWorkflowSystemVariableKVs(workflow, projectName, projectDisplayName, taskID, creator, account, uid, now) +} + +func KeyValsToMap(kvs []*commonmodels.KeyVal) map[string]string { + resp := make(map[string]string) + for _, kv := range kvs { + if kv == nil || kv.Key == "" || kv.GetValue() == "" { + continue + } + resp[kv.Key] = kv.GetValue() + } + return resp +} diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go index ff7117c8c0..b5b2921d56 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go @@ -173,6 +173,11 @@ func (gruem *gerritChangeMergedEventMatcherForWorkflowV4) GetHookRepo(hookRepo * RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: hookRepo.Branch, + PR: gruem.Event.Change.Number, + CommitID: gruem.Event.NewRev, + CommitMessage: gruem.Event.Change.CommitMessage, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -223,7 +228,11 @@ func (gpcem *gerritPatchsetCreatedEventMatcherForWorkflowV4) GetHookRepo(hookRep RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: hookRepo.Branch, PR: gpcem.Event.Change.Number, + CommitID: gpcem.Event.PatchSet.Revision, + CommitMessage: gpcem.Event.Change.CommitMessage, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -267,7 +276,6 @@ func TriggerWorkflowV4ByGerritEvent(event *gerritTypeEvent, body []byte, uri, ba return fmt.Errorf(errMsg) } var errorList = &multierror.Error{} - var hookPayload *commonmodels.HookPayload var notification *commonmodels.Notification for _, workflow := range workflows { gitHooks, err := commonrepo.NewWorkflowV4GitHookColl().List(internalhandler.NewBackgroupContext(), workflow.Name) @@ -290,6 +298,7 @@ func TriggerWorkflowV4ByGerritEvent(event *gerritTypeEvent, body []byte, uri, ba continue } for _, item := range gitHooks { + var hookPayload *commonmodels.HookPayload if !item.Enabled { continue } @@ -321,7 +330,9 @@ func TriggerWorkflowV4ByGerritEvent(event *gerritTypeEvent, body []byte, uri, ba eventRepo := matcher.GetHookRepo(item.MainRepo) var mergeRequestID, commitID string - if m, ok := matcher.(*gerritPatchsetCreatedEventMatcherForWorkflowV4); ok { + var commitSHA string + switch m := matcher.(type) { + case *gerritPatchsetCreatedEventMatcherForWorkflowV4: if item.CheckPatchSetChange { // for different patch sets under the same pr, if the updated contents of the two patch sets are exactly the same, and the task triggered by the previous patch set is executed successfully, the new patch set will no longer trigger the task. // TODO THE OLD IMPLEMENTATION DOES NOT WORK FOR A LONG TIME, SO I DELETED THEM. REWITE IT WHEN NECESSARY. @@ -329,6 +340,7 @@ func TriggerWorkflowV4ByGerritEvent(event *gerritTypeEvent, body []byte, uri, ba mergeRequestID = strconv.Itoa(m.Event.Change.Number) commitID = strconv.Itoa(m.Event.PatchSet.Number) + commitSHA = m.Event.PatchSet.Revision autoCancelOpt := &AutoCancelOpt{ MergeRequestID: mergeRequestID, CommitID: commitID, @@ -352,16 +364,25 @@ func TriggerWorkflowV4ByGerritEvent(event *gerritTypeEvent, body []byte, uri, ba mainRepo, m.Event.Change.Number, baseURI, false, false, false, true, log, ) } - - hookPayload = &commonmodels.HookPayload{ - Owner: eventRepo.RepoOwner, - Repo: eventRepo.RepoName, - Branch: eventRepo.Branch, - IsPr: true, - CodehostID: item.MainRepo.CodehostID, - MergeRequestID: mergeRequestID, - CommitID: commitID, - } + case *gerritChangeMergedEventMatcherForWorkflowV4: + mergeRequestID = strconv.Itoa(m.Event.Change.Number) + commitID = eventRepo.CommitID + commitSHA = m.Event.NewRev + } + hookPayload = &commonmodels.HookPayload{ + Owner: eventRepo.RepoOwner, + Repo: eventRepo.RepoName, + Branch: eventRepo.Branch, + TargetBranch: eventRepo.TargetBranch, + IsPr: true, + CodehostID: item.MainRepo.CodehostID, + MergeRequestID: mergeRequestID, + CommitID: commitID, + CommitSHA: commitSHA, + CommitMessage: eventRepo.CommitMessage, + Committer: eventRepo.Committer, + EventType: event.Type, + RawPayload: string(body), } workflowController := controller.CreateWorkflowController(item.WorkflowArg) if err := workflowController.UpdateWithLatestWorkflow(nil); err != nil { diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitee.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitee.go index 418c817baa..44d445a5a6 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitee.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitee.go @@ -116,7 +116,7 @@ func ProcessGiteeHook(payload []byte, req *http.Request, requestID string, log * wg.Add(1) go func() { defer wg.Done() - if err = TriggerWorkflowV4ByGiteeEvent(event, baseURI, requestID, log); err != nil { + if err = TriggerWorkflowV4ByGiteeEvent(event, string(payload), baseURI, requestID, log); err != nil { errorList = multierror.Append(errorList, err) } }() @@ -150,7 +150,7 @@ func ProcessGiteeHook(payload []byte, req *http.Request, requestID string, log * wg.Add(1) go func() { defer wg.Done() - if err = TriggerWorkflowV4ByGiteeEvent(event, baseURI, requestID, log); err != nil { + if err = TriggerWorkflowV4ByGiteeEvent(event, string(payload), baseURI, requestID, log); err != nil { errorList = multierror.Append(errorList, err) } }() @@ -176,7 +176,7 @@ func ProcessGiteeHook(payload []byte, req *http.Request, requestID string, log * wg.Add(1) go func() { defer wg.Done() - if err = TriggerWorkflowV4ByGiteeEvent(event, baseURI, requestID, log); err != nil { + if err = TriggerWorkflowV4ByGiteeEvent(event, string(payload), baseURI, requestID, log); err != nil { errorList = multierror.Append(errorList, err) } }() diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitee_testing_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitee_testing_task.go index 6bd2c876ee..132f41e4ac 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitee_testing_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitee_testing_task.go @@ -299,6 +299,7 @@ func TriggerTestByGiteeEvent(event interface{}, baseURI, requestID string, log * IsPr: true, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitSHA: commitID, EventType: eventType, } case *gitee.PushEvent: @@ -317,6 +318,7 @@ func TriggerTestByGiteeEvent(event interface{}, baseURI, requestID string, log * Ref: ref, IsPr: false, CommitID: commitID, + CommitSHA: commitID, EventType: eventType, } case *gitee.TagPushEvent: diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go index c5bf677f35..a723bc4633 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go @@ -81,12 +81,20 @@ func (gpem *giteePushEventMatcherForWorkflowV4) Match(hookRepo *commonmodels.Mai } func (gpem *giteePushEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmodels.MainHookRepo) *types.Repository { + commitMessage := "" + if len(gpem.event.Commits) > 0 { + commitMessage = gpem.event.Commits[len(gpem.event.Commits)-1].Message + } return &types.Repository{ CodehostID: hookRepo.CodehostID, RepoName: hookRepo.RepoName, RepoNamespace: hookRepo.GetRepoNamespace(), RepoOwner: hookRepo.RepoOwner, Branch: hookRepo.Branch, + TargetBranch: hookRepo.Branch, + CommitID: gpem.event.After, + CommitMessage: commitMessage, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -139,7 +147,11 @@ func (gmem *giteeMergeEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmod RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: gmem.event.PullRequest.Base.Ref, PR: gmem.event.PullRequest.Number, + CommitID: gmem.event.PullRequest.Head.Sha, + CommitMessage: gmem.event.PullRequest.Title, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -173,7 +185,9 @@ func (gtem *giteeTagEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmodel RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: hookRepo.Branch, Tag: hookRepo.Tag, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -206,7 +220,7 @@ func createGiteeEventMatcherForWorkflowV4( return nil } -func TriggerWorkflowV4ByGiteeEvent(event interface{}, baseURI, requestID string, log *zap.SugaredLogger) error { +func TriggerWorkflowV4ByGiteeEvent(event interface{}, rawPayload, baseURI, requestID string, log *zap.SugaredLogger) error { workflows, _, err := commonrepo.NewWorkflowV4Coll().List(&commonrepo.ListWorkflowV4Option{}, 0, 0) if err != nil { errMsg := fmt.Sprintf("list workflow v4 error: %v", err) @@ -276,10 +290,15 @@ func TriggerWorkflowV4ByGiteeEvent(event interface{}, baseURI, requestID string, Repo: eventRepo.RepoName, CodehostID: item.MainRepo.CodehostID, Branch: eventRepo.Branch, + TargetBranch: eventRepo.TargetBranch, IsPr: true, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitSHA: commitID, + CommitMessage: ev.PullRequest.Title, + Committer: ev.PullRequest.User.Login, EventType: eventType, + RawPayload: rawPayload, } case *gitee.PushEvent: eventType = EventTypePush @@ -289,19 +308,28 @@ func TriggerWorkflowV4ByGiteeEvent(event interface{}, baseURI, requestID string, autoCancelOpt.CommitID = commitID autoCancelOpt.Ref = ref hookPayload = &commonmodels.HookPayload{ - Owner: eventRepo.RepoOwner, - Repo: eventRepo.RepoName, - CodehostID: item.MainRepo.CodehostID, - Branch: eventRepo.Branch, - Ref: ref, - IsPr: false, - CommitID: commitID, - EventType: eventType, + Owner: eventRepo.RepoOwner, + Repo: eventRepo.RepoName, + CodehostID: item.MainRepo.CodehostID, + Branch: eventRepo.Branch, + TargetBranch: eventRepo.TargetBranch, + Ref: ref, + IsPr: false, + CommitID: commitID, + CommitSHA: commitID, + CommitMessage: eventRepo.CommitMessage, + Committer: eventRepo.Committer, + EventType: eventType, + RawPayload: rawPayload, } case *gitee.TagPushEvent: eventType = EventTypeTag hookPayload = &commonmodels.HookPayload{ - EventType: eventType, + Branch: eventRepo.Branch, + TargetBranch: eventRepo.TargetBranch, + Committer: eventRepo.Committer, + EventType: eventType, + RawPayload: rawPayload, } } if autoCancelOpt.Type != "" { diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/github.go b/pkg/microservice/aslan/core/workflow/service/webhook/github.go index 8a9ee2b9b1..360f452d43 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/github.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/github.go @@ -429,19 +429,19 @@ func ProcessGithubWebHookForWorkflowV4(payload []byte, req *http.Request, reques if *et.Action != "opened" && *et.Action != "synchronize" { return nil } - err = TriggerWorkflowV4ByGithubEvent(et, baseURI, deliveryID, requestID, log) + err = TriggerWorkflowV4ByGithubEvent(et, string(payload), baseURI, deliveryID, requestID, log) if err != nil { log.Errorf("prEventToPipelineTasks error: %v", err) return e.ErrGithubWebHook.AddErr(err) } case *github.PushEvent: - err = TriggerWorkflowV4ByGithubEvent(et, baseURI, deliveryID, requestID, log) + err = TriggerWorkflowV4ByGithubEvent(et, string(payload), baseURI, deliveryID, requestID, log) if err != nil { log.Infof("pushEventToPipelineTasks error: %v", err) return e.ErrGithubWebHook.AddErr(err) } case *github.CreateEvent: - err = TriggerWorkflowV4ByGithubEvent(et, baseURI, deliveryID, requestID, log) + err = TriggerWorkflowV4ByGithubEvent(et, string(payload), baseURI, deliveryID, requestID, log) if err != nil { log.Errorf("tagEventToPipelineTasks error: %s", err) return e.ErrGithubWebHook.AddErr(err) diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/github_scanning_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/github_scanning_task.go index 1699b3b67b..d87f6be173 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/github_scanning_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/github_scanning_task.go @@ -96,6 +96,7 @@ func TriggerScanningByGithubEvent(event interface{}, requestID string, log *zap. CodehostID: mainRepo.CodehostID, MergeRequestID: strconv.Itoa(mergeRequestID), CommitID: commitID, + CommitSHA: commitID, EventType: eventType, } case *github.PushEvent: @@ -113,6 +114,7 @@ func TriggerScanningByGithubEvent(event interface{}, requestID string, log *zap. Ref: ref, IsPr: false, CommitID: commitID, + CommitSHA: commitID, EventType: eventType, CodehostID: mainRepo.CodehostID, } diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/github_testing_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/github_testing_task.go index f9f46ffd8e..d667e73e13 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/github_testing_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/github_testing_task.go @@ -87,6 +87,7 @@ func TriggerTestByGithubEvent(event interface{}, requestID string, log *zap.Suga CodehostID: item.MainRepo.CodehostID, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitSHA: commitID, EventType: eventType, } case *github.PushEvent: @@ -104,6 +105,7 @@ func TriggerTestByGithubEvent(event interface{}, requestID string, log *zap.Suga Ref: ref, IsPr: false, CommitID: commitID, + CommitSHA: commitID, EventType: eventType, CodehostID: item.MainRepo.CodehostID, } diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go index 699358b3a6..4a03d425b0 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go @@ -25,7 +25,6 @@ import ( "github.com/hashicorp/go-multierror" "go.uber.org/zap" - internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler" "github.com/koderover/zadig/v2/pkg/microservice/aslan/config" commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" commonrepo "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb" @@ -33,6 +32,7 @@ import ( workflowservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/workflow/service/workflow" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/workflow/service/workflow/controller" "github.com/koderover/zadig/v2/pkg/setting" + internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler" "github.com/koderover/zadig/v2/pkg/types" ) @@ -86,8 +86,10 @@ func (gpem *githubPushEventMatcheForWorkflowV4) GetHookRepo(hookRepo *commonmode RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: hookRepo.Branch, CommitID: *gpem.event.HeadCommit.ID, CommitMessage: *gpem.event.HeadCommit.Message, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -143,9 +145,11 @@ func (gmem *githubMergeEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmo RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: *gmem.event.PullRequest.Base.Ref, PR: *gmem.event.PullRequest.Number, CommitID: *gmem.event.PullRequest.Head.SHA, CommitMessage: *gmem.event.PullRequest.Title, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -182,7 +186,9 @@ func (gtem *githubTagEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmode RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: hookRepo.Branch, Tag: hookRepo.Tag, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -215,7 +221,7 @@ func createGithubEventMatcherForWorkflowV4( return nil } -func TriggerWorkflowV4ByGithubEvent(event interface{}, baseURI, deliveryID, requestID string, log *zap.SugaredLogger) error { +func TriggerWorkflowV4ByGithubEvent(event interface{}, rawPayload, baseURI, deliveryID, requestID string, log *zap.SugaredLogger) error { workflows, _, err := commonrepo.NewWorkflowV4Coll().List(&commonrepo.ListWorkflowV4Option{}, 0, 0) if err != nil { errMsg := fmt.Sprintf("list workflow v4 error: %v", err) @@ -277,13 +283,18 @@ func TriggerWorkflowV4ByGithubEvent(event interface{}, baseURI, deliveryID, requ Owner: *ev.Repo.Owner.Login, Repo: *ev.Repo.Name, Branch: *ev.PullRequest.Base.Ref, + TargetBranch: *ev.PullRequest.Base.Ref, Ref: *ev.PullRequest.Head.SHA, IsPr: true, CodehostID: item.MainRepo.CodehostID, DeliveryID: deliveryID, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitSHA: commitID, + CommitMessage: *ev.PullRequest.Title, + Committer: *ev.PullRequest.User.Login, EventType: eventType, + RawPayload: rawPayload, } case *github.PushEvent: if ev.GetRef() != "" && ev.GetHeadCommit().GetID() != "" { @@ -294,20 +305,30 @@ func TriggerWorkflowV4ByGithubEvent(event interface{}, baseURI, deliveryID, requ autoCancelOpt.Ref = ref autoCancelOpt.CommitID = commitID hookPayload = &commonmodels.HookPayload{ - Owner: *ev.Repo.Owner.Login, - Repo: *ev.Repo.Name, - Ref: ref, - IsPr: false, - CodehostID: item.MainRepo.CodehostID, - DeliveryID: deliveryID, - CommitID: commitID, - EventType: eventType, + Owner: *ev.Repo.Owner.Login, + Repo: *ev.Repo.Name, + Branch: getBranchFromRef(ref), + TargetBranch: getBranchFromRef(ref), + Ref: ref, + IsPr: false, + CodehostID: item.MainRepo.CodehostID, + DeliveryID: deliveryID, + CommitID: commitID, + CommitSHA: commitID, + CommitMessage: ev.GetHeadCommit().GetMessage(), + Committer: ev.GetPusher().GetName(), + EventType: eventType, + RawPayload: rawPayload, } } case *github.CreateEvent: eventType = EventTypeTag hookPayload = &commonmodels.HookPayload{ - EventType: eventType, + Branch: item.MainRepo.Branch, + TargetBranch: item.MainRepo.Branch, + Committer: item.MainRepo.Committer, + EventType: eventType, + RawPayload: rawPayload, } } if autoCancelOpt.Type != "" { diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab.go index e733b47c72..b3963515d7 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab.go @@ -163,7 +163,7 @@ func ProcessGitlabHook(payload []byte, req *http.Request, requestID string, log go func() { defer wg.Done() triggerWorkflowV4Start := time.Now() - if err = TriggerWorkflowV4ByGitlabEvent(pushEvent, baseURI, requestID, log); err != nil { + if err = TriggerWorkflowV4ByGitlabEvent(pushEvent, string(payload), baseURI, requestID, log); err != nil { errorList = multierror.Append(errorList, err) } log.Infof("gitlab webhook TriggerWorkflowV4ByGitlabEvent push cost %s", time.Since(triggerWorkflowV4Start)) @@ -196,7 +196,7 @@ func ProcessGitlabHook(payload []byte, req *http.Request, requestID string, log go func() { defer wg.Done() triggerWorkflowV4Start := time.Now() - if err = TriggerWorkflowV4ByGitlabEvent(mergeEvent, baseURI, requestID, log); err != nil { + if err = TriggerWorkflowV4ByGitlabEvent(mergeEvent, string(payload), baseURI, requestID, log); err != nil { errorList = multierror.Append(errorList, err) } log.Infof("gitlab webhook TriggerWorkflowV4ByGitlabEvent merge cost %s", time.Since(triggerWorkflowV4Start)) @@ -229,7 +229,7 @@ func ProcessGitlabHook(payload []byte, req *http.Request, requestID string, log go func() { defer wg.Done() triggerWorkflowV4Start := time.Now() - if err = TriggerWorkflowV4ByGitlabEvent(tagEvent, baseURI, requestID, log); err != nil { + if err = TriggerWorkflowV4ByGitlabEvent(tagEvent, string(payload), baseURI, requestID, log); err != nil { errorList = multierror.Append(errorList, err) } log.Infof("gitlab webhook TriggerWorkflowV4ByGitlabEvent tag cost %s", time.Since(triggerWorkflowV4Start)) diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_scanning_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_scanning_task.go index 77c102a423..467c76f5d4 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_scanning_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_scanning_task.go @@ -93,6 +93,7 @@ func TriggerScanningByGitlabEvent(event interface{}, baseURI, requestID string, IsPr: true, MergeRequestID: strconv.Itoa(mergeRequestID), CommitID: commitID, + CommitSHA: commitID, CodehostID: eventRepo.CodehostID, EventType: eventType, } @@ -111,6 +112,7 @@ func TriggerScanningByGitlabEvent(event interface{}, baseURI, requestID string, Ref: ref, IsPr: false, CommitID: commitID, + CommitSHA: commitID, CodehostID: eventRepo.CodehostID, EventType: eventType, } diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_testing_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_testing_task.go index 711eefa487..ddaebac794 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_testing_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_testing_task.go @@ -224,6 +224,7 @@ func TriggerTestByGitlabEvent(event interface{}, baseURI, requestID string, log IsPr: true, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitSHA: commitID, CodehostID: eventRepo.CodehostID, EventType: eventType, } @@ -242,6 +243,7 @@ func TriggerTestByGitlabEvent(event interface{}, baseURI, requestID string, log Ref: ref, IsPr: false, CommitID: commitID, + CommitSHA: commitID, CodehostID: eventRepo.CodehostID, EventType: eventType, } diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go index 1c896aa053..f8bd3cc91b 100644 --- a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go +++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go @@ -108,7 +108,11 @@ func (gmem *gitlabMergeEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmo RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: gmem.event.ObjectAttributes.TargetBranch, PR: gmem.event.ObjectAttributes.IID, + CommitID: gmem.event.ObjectAttributes.LastCommit.ID, + CommitMessage: gmem.event.ObjectAttributes.Title, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -225,12 +229,20 @@ func (gpem *gitlabPushEventMatcherForWorkflowV4) Match(hookRepo *commonmodels.Ma } func (gpem *gitlabPushEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmodels.MainHookRepo) *types.Repository { + commitMessage := "" + if len(gpem.event.Commits) > 0 { + commitMessage = gpem.event.Commits[len(gpem.event.Commits)-1].Message + } return &types.Repository{ CodehostID: hookRepo.CodehostID, RepoName: hookRepo.RepoName, RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: hookRepo.Branch, + CommitID: gpem.event.After, + CommitMessage: commitMessage, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } @@ -268,12 +280,14 @@ func (gpem *gitlabTagEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmode RepoOwner: hookRepo.RepoOwner, RepoNamespace: hookRepo.GetRepoNamespace(), Branch: hookRepo.Branch, + TargetBranch: hookRepo.Branch, Tag: hookRepo.Tag, + Committer: hookRepo.Committer, Source: hookRepo.Source, } } -func TriggerWorkflowV4ByGitlabEvent(event interface{}, baseURI, requestID string, log *zap.SugaredLogger) error { +func TriggerWorkflowV4ByGitlabEvent(event interface{}, rawPayload, baseURI, requestID string, log *zap.SugaredLogger) error { // TODO: cache workflow // 1. find configured workflow workflows, _, err := commonrepo.NewWorkflowV4Coll().List(&commonrepo.ListWorkflowV4Option{}, 0, 0) @@ -365,11 +379,16 @@ func TriggerWorkflowV4ByGitlabEvent(event interface{}, baseURI, requestID string Owner: eventRepo.RepoOwner, Repo: eventRepo.RepoName, Branch: eventRepo.Branch, + TargetBranch: eventRepo.TargetBranch, IsPr: true, MergeRequestID: mergeRequestID, CommitID: commitID, + CommitSHA: commitID, + CommitMessage: ev.ObjectAttributes.Title, + Committer: ev.User.Username, CodehostID: eventRepo.CodehostID, EventType: eventType, + RawPayload: rawPayload, } case *gitlab.PushEvent: eventType = EventTypePush @@ -379,19 +398,28 @@ func TriggerWorkflowV4ByGitlabEvent(event interface{}, baseURI, requestID string autoCancelOpt.Ref = ref autoCancelOpt.CommitID = commitID hookPayload = &commonmodels.HookPayload{ - Owner: eventRepo.RepoOwner, - Repo: eventRepo.RepoName, - Branch: eventRepo.Branch, - Ref: ref, - IsPr: false, - CommitID: commitID, - CodehostID: eventRepo.CodehostID, - EventType: eventType, + Owner: eventRepo.RepoOwner, + Repo: eventRepo.RepoName, + Branch: eventRepo.Branch, + TargetBranch: eventRepo.TargetBranch, + Ref: ref, + IsPr: false, + CommitID: commitID, + CommitSHA: commitID, + CommitMessage: eventRepo.CommitMessage, + Committer: eventRepo.Committer, + CodehostID: eventRepo.CodehostID, + EventType: eventType, + RawPayload: rawPayload, } case *gitlab.TagEvent: eventType = EventTypeTag hookPayload = &commonmodels.HookPayload{ - EventType: eventType, + Branch: eventRepo.Branch, + TargetBranch: eventRepo.TargetBranch, + Committer: eventRepo.Committer, + EventType: eventType, + RawPayload: rawPayload, } } if autoCancelOpt.Type != "" { diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_build.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_build.go index 848cea549b..d0e64aeba8 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_build.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_build.go @@ -1128,6 +1128,10 @@ func mergeRepos(targetRepos, sourceRepos []*types.Repository) []*types.Repositor targetRepo.PRs = sourceRepo.PRs targetRepo.CommitID = sourceRepo.CommitID targetRepo.CommitMessage = sourceRepo.CommitMessage + targetRepo.AuthorName = sourceRepo.AuthorName + targetRepo.Committer = sourceRepo.Committer + targetRepo.TargetBranch = sourceRepo.TargetBranch + targetRepo.CheckoutRef = sourceRepo.CheckoutRef } else { // Add new repo from source repos targetRepos = append(targetRepos, sourceRepo) diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go index 4f8550be86..c83bb484d6 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_notification.go @@ -22,7 +22,9 @@ import ( "github.com/koderover/zadig/v2/pkg/microservice/aslan/config" commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" + "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/dynamicrecipient" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util" + "github.com/koderover/zadig/v2/pkg/setting" e "github.com/koderover/zadig/v2/pkg/tool/errors" "github.com/koderover/zadig/v2/pkg/types" ) @@ -65,6 +67,15 @@ func (j NotificationJobController) Validate(isExecution bool) error { if err := util.CheckZadigProfessionalLicense(); err != nil { return e.ErrLicenseInvalid.AddDesc("") } + if j.jobSpec.WebHookType == setting.NotifyWebHookTypeFeishu && j.jobSpec.LarkHookNotificationConfig != nil { + if err := dynamicrecipient.ValidateDynamicRecipientsForNotifyConfig( + j.jobSpec.WebHookType, + j.jobSpec.LarkHookNotificationConfig.AppID, + []string(j.jobSpec.LarkHookNotificationConfig.DynamicRecipients), + ); err != nil { + return e.ErrLintWorkflow.AddDesc(err.Error()) + } + } return nil } @@ -86,30 +97,42 @@ func (j NotificationJobController) Update(useUserInput bool, ticket *commonmodel j.jobSpec.Source = currJobSpec.Source if currJobSpec.Source == "runtime" { + if currJobSpec.LarkHookNotificationConfig != nil && j.jobSpec.LarkHookNotificationConfig != nil { + currJobSpec.LarkHookNotificationConfig.AtUsers = j.jobSpec.LarkHookNotificationConfig.AtUsers + currJobSpec.LarkHookNotificationConfig.DynamicRecipients = j.jobSpec.LarkHookNotificationConfig.DynamicRecipients + currJobSpec.LarkHookNotificationConfig.IsAtAll = j.jobSpec.LarkHookNotificationConfig.IsAtAll + } if currJobSpec.LarkGroupNotificationConfig != nil && j.jobSpec.LarkGroupNotificationConfig != nil { currJobSpec.LarkGroupNotificationConfig.AtUsers = j.jobSpec.LarkGroupNotificationConfig.AtUsers + currJobSpec.LarkGroupNotificationConfig.DynamicRecipients = j.jobSpec.LarkGroupNotificationConfig.DynamicRecipients currJobSpec.LarkGroupNotificationConfig.IsAtAll = j.jobSpec.LarkGroupNotificationConfig.IsAtAll } if currJobSpec.LarkPersonNotificationConfig != nil && j.jobSpec.LarkPersonNotificationConfig != nil { currJobSpec.LarkPersonNotificationConfig.TargetUsers = j.jobSpec.LarkPersonNotificationConfig.TargetUsers + currJobSpec.LarkPersonNotificationConfig.DynamicRecipients = j.jobSpec.LarkPersonNotificationConfig.DynamicRecipients } if currJobSpec.WechatNotificationConfig != nil && j.jobSpec.WechatNotificationConfig != nil { currJobSpec.WechatNotificationConfig.AtUsers = j.jobSpec.WechatNotificationConfig.AtUsers + currJobSpec.WechatNotificationConfig.DynamicRecipients = j.jobSpec.WechatNotificationConfig.DynamicRecipients currJobSpec.WechatNotificationConfig.IsAtAll = j.jobSpec.WechatNotificationConfig.IsAtAll } if currJobSpec.DingDingNotificationConfig != nil && j.jobSpec.DingDingNotificationConfig != nil { currJobSpec.DingDingNotificationConfig.AtMobiles = j.jobSpec.DingDingNotificationConfig.AtMobiles + currJobSpec.DingDingNotificationConfig.DynamicRecipients = j.jobSpec.DingDingNotificationConfig.DynamicRecipients currJobSpec.DingDingNotificationConfig.IsAtAll = j.jobSpec.DingDingNotificationConfig.IsAtAll } if currJobSpec.MSTeamsNotificationConfig != nil && j.jobSpec.MSTeamsNotificationConfig != nil { currJobSpec.MSTeamsNotificationConfig.AtEmails = j.jobSpec.MSTeamsNotificationConfig.AtEmails + currJobSpec.MSTeamsNotificationConfig.DynamicRecipients = j.jobSpec.MSTeamsNotificationConfig.DynamicRecipients } if currJobSpec.MailNotificationConfig != nil && j.jobSpec.MailNotificationConfig != nil { currJobSpec.MailNotificationConfig.TargetUsers = j.jobSpec.MailNotificationConfig.TargetUsers + currJobSpec.MailNotificationConfig.DynamicRecipients = j.jobSpec.MailNotificationConfig.DynamicRecipients } } // use the latest webhook settings, except for title and content + j.jobSpec.LarkHookNotificationConfig = currJobSpec.LarkHookNotificationConfig j.jobSpec.LarkGroupNotificationConfig = currJobSpec.LarkGroupNotificationConfig j.jobSpec.LarkPersonNotificationConfig = currJobSpec.LarkPersonNotificationConfig j.jobSpec.WechatNotificationConfig = currJobSpec.WechatNotificationConfig @@ -218,6 +241,7 @@ func generateNotificationJobSpec(spec *commonmodels.NotificationJobSpec) (*commo return nil, err } + resp.LarkHookNotificationConfig = spec.LarkHookNotificationConfig resp.MailNotificationConfig = spec.MailNotificationConfig resp.WechatNotificationConfig = spec.WechatNotificationConfig resp.LarkPersonNotificationConfig = spec.LarkPersonNotificationConfig diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/utils.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/utils.go index 03f6f8bad7..da50168b79 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/utils.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/utils.go @@ -246,6 +246,10 @@ func applyRepos(base, input []*types.Repository) []*types.Repository { item.FilterRegexp = cv.FilterRegexp item.CommitID = cv.CommitID item.CommitMessage = cv.CommitMessage + item.AuthorName = cv.AuthorName + item.Committer = cv.Committer + item.TargetBranch = cv.TargetBranch + item.CheckoutRef = cv.CheckoutRef item.SSHKey = cv.SSHKey item.PrivateAccessToken = cv.PrivateAccessToken diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go index cc48b3b19e..e753911584 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go @@ -48,6 +48,264 @@ func CreateWorkflowController(wf *commonmodels.WorkflowV4) *Workflow { return &Workflow{wf} } +type notificationDynamicRecipients struct { + LarkHook commonmodels.DynamicRecipients + LarkGroup commonmodels.DynamicRecipients + LarkPerson commonmodels.DynamicRecipients + Wechat commonmodels.DynamicRecipients + DingDing commonmodels.DynamicRecipients + MSTeams commonmodels.DynamicRecipients + Mail commonmodels.DynamicRecipients +} + +type workflowNotificationSpecBackup struct { + StageIndex int + JobIndex int + Recipients *notificationDynamicRecipients +} + +func cloneDynamicRecipients(items commonmodels.DynamicRecipients) commonmodels.DynamicRecipients { + if items == nil { + return nil + } + resp := make(commonmodels.DynamicRecipients, len(items)) + copy(resp, items) + return resp +} + +func backupNotificationDynamicRecipients( + larkHook *commonmodels.LarkHookNotificationConfig, + larkGroup *commonmodels.LarkGroupNotificationConfig, + larkPerson *commonmodels.LarkPersonNotificationConfig, + wechat *commonmodels.WechatNotificationConfig, + dingDing *commonmodels.DingDingNotificationConfig, + msTeams *commonmodels.MSTeamsNotificationConfig, + mail *commonmodels.MailNotificationConfig, +) *notificationDynamicRecipients { + resp := ¬ificationDynamicRecipients{} + if larkHook != nil { + resp.LarkHook = cloneDynamicRecipients(larkHook.DynamicRecipients) + } + if larkGroup != nil { + resp.LarkGroup = cloneDynamicRecipients(larkGroup.DynamicRecipients) + } + if larkPerson != nil { + resp.LarkPerson = cloneDynamicRecipients(larkPerson.DynamicRecipients) + } + if wechat != nil { + resp.Wechat = cloneDynamicRecipients(wechat.DynamicRecipients) + } + if dingDing != nil { + resp.DingDing = cloneDynamicRecipients(dingDing.DynamicRecipients) + } + if msTeams != nil { + resp.MSTeams = cloneDynamicRecipients(msTeams.DynamicRecipients) + } + if mail != nil { + resp.Mail = cloneDynamicRecipients(mail.DynamicRecipients) + } + return resp +} + +func restoreNotificationDynamicRecipients( + recipients *notificationDynamicRecipients, + larkHook *commonmodels.LarkHookNotificationConfig, + larkGroup *commonmodels.LarkGroupNotificationConfig, + larkPerson *commonmodels.LarkPersonNotificationConfig, + wechat *commonmodels.WechatNotificationConfig, + dingDing *commonmodels.DingDingNotificationConfig, + msTeams *commonmodels.MSTeamsNotificationConfig, + mail *commonmodels.MailNotificationConfig, +) { + if recipients == nil { + return + } + if larkHook != nil { + larkHook.DynamicRecipients = cloneDynamicRecipients(recipients.LarkHook) + } + if larkGroup != nil { + larkGroup.DynamicRecipients = cloneDynamicRecipients(recipients.LarkGroup) + } + if larkPerson != nil { + larkPerson.DynamicRecipients = cloneDynamicRecipients(recipients.LarkPerson) + } + if wechat != nil { + wechat.DynamicRecipients = cloneDynamicRecipients(recipients.Wechat) + } + if dingDing != nil { + dingDing.DynamicRecipients = cloneDynamicRecipients(recipients.DingDing) + } + if msTeams != nil { + msTeams.DynamicRecipients = cloneDynamicRecipients(recipients.MSTeams) + } + if mail != nil { + mail.DynamicRecipients = cloneDynamicRecipients(recipients.Mail) + } +} + +func backupNotificationDynamicRecipientsFromWorkflowSpec(spec *commonmodels.NotificationJobSpec) *notificationDynamicRecipients { + if spec == nil { + return nil + } + return backupNotificationDynamicRecipients( + spec.LarkHookNotificationConfig, + spec.LarkGroupNotificationConfig, + spec.LarkPersonNotificationConfig, + spec.WechatNotificationConfig, + spec.DingDingNotificationConfig, + spec.MSTeamsNotificationConfig, + spec.MailNotificationConfig, + ) +} + +func restoreNotificationDynamicRecipientsToWorkflowSpec(spec *commonmodels.NotificationJobSpec, recipients *notificationDynamicRecipients) { + if spec == nil || recipients == nil { + return + } + restoreNotificationDynamicRecipients( + recipients, + spec.LarkHookNotificationConfig, + spec.LarkGroupNotificationConfig, + spec.LarkPersonNotificationConfig, + spec.WechatNotificationConfig, + spec.DingDingNotificationConfig, + spec.MSTeamsNotificationConfig, + spec.MailNotificationConfig, + ) +} + +func backupWorkflowNotificationRuntimeRenderFields(workflow *commonmodels.WorkflowV4) ([]*workflowNotificationSpecBackup, error) { + if workflow == nil { + return nil, nil + } + + resp := make([]*workflowNotificationSpecBackup, 0) + for stageIndex, stage := range workflow.Stages { + if stage == nil { + continue + } + for jobIndex, job := range stage.Jobs { + if job == nil || job.JobType != config.JobNotification { + continue + } + spec := &commonmodels.NotificationJobSpec{} + if err := commonmodels.IToi(job.Spec, spec); err != nil { + return nil, fmt.Errorf("failed to decode notification job spec for job %s, error: %w", job.Name, err) + } + resp = append(resp, &workflowNotificationSpecBackup{ + StageIndex: stageIndex, + JobIndex: jobIndex, + Recipients: backupNotificationDynamicRecipientsFromWorkflowSpec(spec), + }) + } + } + return resp, nil +} + +func restoreWorkflowNotificationRuntimeRenderFields(workflow *commonmodels.WorkflowV4, backups []*workflowNotificationSpecBackup) error { + if workflow == nil { + return nil + } + + for _, backup := range backups { + if backup == nil || backup.Recipients == nil { + continue + } + if backup.StageIndex >= len(workflow.Stages) || workflow.Stages[backup.StageIndex] == nil { + continue + } + stage := workflow.Stages[backup.StageIndex] + if backup.JobIndex >= len(stage.Jobs) || stage.Jobs[backup.JobIndex] == nil { + continue + } + job := stage.Jobs[backup.JobIndex] + spec := &commonmodels.NotificationJobSpec{} + if err := commonmodels.IToi(job.Spec, spec); err != nil { + return fmt.Errorf("failed to restore notification job spec for job %s, error: %w", job.Name, err) + } + restoreNotificationDynamicRecipientsToWorkflowSpec(spec, backup.Recipients) + job.Spec = spec + } + return nil +} + +func backupNotificationDynamicRecipientsFromTaskSpec(spec *commonmodels.JobTaskNotificationSpec) *notificationDynamicRecipients { + if spec == nil { + return nil + } + return backupNotificationDynamicRecipients( + spec.LarkHookNotificationConfig, + spec.LarkGroupNotificationConfig, + spec.LarkPersonNotificationConfig, + spec.WechatNotificationConfig, + spec.DingDingNotificationConfig, + spec.MSTeamsNotificationConfig, + spec.MailNotificationConfig, + ) +} + +func restoreNotificationDynamicRecipientsToTaskSpec(spec *commonmodels.JobTaskNotificationSpec, recipients *notificationDynamicRecipients) { + if spec == nil || recipients == nil { + return + } + restoreNotificationDynamicRecipients( + recipients, + spec.LarkHookNotificationConfig, + spec.LarkGroupNotificationConfig, + spec.LarkPersonNotificationConfig, + spec.WechatNotificationConfig, + spec.DingDingNotificationConfig, + spec.MSTeamsNotificationConfig, + spec.MailNotificationConfig, + ) +} + +func RenderJobTaskWithGlobalVariables(task *commonmodels.JobTask, globalKeyMap map[string]string) error { + if task == nil { + return nil + } + + var notificationRecipients *notificationDynamicRecipients + if task.JobType == string(config.JobNotification) { + spec := &commonmodels.JobTaskNotificationSpec{} + if err := commonmodels.IToi(task.Spec, spec); err != nil { + return fmt.Errorf("failed to decode notification task spec for task %s, error: %w", task.Name, err) + } + notificationRecipients = backupNotificationDynamicRecipientsFromTaskSpec(spec) + } + + taskBytes, err := json.Marshal(task) + if err != nil { + return fmt.Errorf("failed to marshal task %s, error: %w", task.Name, err) + } + taskString := string(taskBytes) + for k, v := range globalKeyMap { + // Use json.Marshal to properly escape the value as it would appear in JSON. + escapedValueBytes, _ := json.Marshal(v) + escapedValue := string(escapedValueBytes) + // Remove the surrounding quotes since we're replacing within a JSON string. + escapedValue = strings.Trim(escapedValue, `"`) + + taskString = strings.ReplaceAll(taskString, fmt.Sprintf("{{.%s}}", k), escapedValue) + } + + if err := json.Unmarshal([]byte(taskString), task); err != nil { + return fmt.Errorf("failed to replace input variable for task: %s, error: %w", task.Name, err) + } + + if notificationRecipients == nil { + return nil + } + + spec := &commonmodels.JobTaskNotificationSpec{} + if err := commonmodels.IToi(task.Spec, spec); err != nil { + return fmt.Errorf("failed to restore notification task spec for task %s, error: %w", task.Name, err) + } + restoreNotificationDynamicRecipientsToTaskSpec(spec, notificationRecipients) + task.Spec = spec + return nil +} + func (w *Workflow) SetPreset(ticket *commonmodels.ApprovalTicket) error { for _, stage := range w.Stages { for _, job := range stage.Jobs { @@ -209,22 +467,8 @@ func (w *Workflow) ToJobTasks(taskID int64, creator, account, uid string, releas } for _, task := range tasks { - taskBytes, _ := json.Marshal(task) - taskString := string(taskBytes) - for k, v := range globalKeyMap { - // Use json.Marshal to properly escape the value as it would appear in JSON - escapedValueBytes, _ := json.Marshal(v) - escapedValue := string(escapedValueBytes) - // Remove the surrounding quotes since we're replacing within a JSON string - escapedValue = strings.Trim(escapedValue, `"`) - - taskString = strings.ReplaceAll(taskString, fmt.Sprintf("{{.%s}}", k), escapedValue) - log.Debugf("replacing key %s with value: %s", fmt.Sprintf("{{.%s}}", k), v) - } - - err := json.Unmarshal([]byte(taskString), &task) - if err != nil { - return nil, fmt.Errorf("failed to replace input variable for task: %s, error: %s", task.Name, err) + if err := RenderJobTaskWithGlobalVariables(task, globalKeyMap); err != nil { + return nil, err } } @@ -346,12 +590,19 @@ func (w *Workflow) RenderWorkflowDefaultParams(taskID int64, creator, account, u if err != nil { return fmt.Errorf("marshal workflow error: %v", err) } + notificationBackups, err := backupWorkflowNotificationRuntimeRenderFields(w.WorkflowV4) + if err != nil { + return err + } globalParams, err := w.getWorkflowDefaultParams(taskID, creator, account, uid, releasePlan) if err != nil { return fmt.Errorf("get workflow default params error: %v", err) } replacedString := renderMultiLineString(string(b), globalParams) - return json.Unmarshal([]byte(replacedString), &w.WorkflowV4) + if err := json.Unmarshal([]byte(replacedString), &w.WorkflowV4); err != nil { + return err + } + return restoreWorkflowNotificationRuntimeRenderFields(w.WorkflowV4, notificationBackups) } func (w *Workflow) getWorkflowDefaultParams(taskID int64, creator, account, uid string, releasePlan *commonmodels.ReleasePlanRef) ([]*commonmodels.Param, error) { @@ -391,9 +642,88 @@ func (w *Workflow) getWorkflowDefaultParams(taskID int64, creator, account, uid } resp = append(resp, newParam) } + if w.HookPayload != nil { + for _, kv := range commonutil.BuildWorkflowTriggerVariableKVs(w.HookPayload) { + resp = append(resp, &commonmodels.Param{ + Name: kv.Key, + Value: kv.Value, + ParamsType: "string", + IsCredential: kv.IsCredential, + }) + } + for _, kv := range commonutil.BuildPayloadVariables(w.HookPayload.RawPayload) { + resp = append(resp, &commonmodels.Param{ + Name: kv.Key, + Value: kv.Value, + ParamsType: "string", + IsCredential: kv.IsCredential, + }) + } + } return resp, nil } +func buildRuntimeReferableVariables(workflow *commonmodels.WorkflowV4) []*commonmodels.KeyVal { + resp := make([]*commonmodels.KeyVal, 0) + resp = append(resp, &commonmodels.KeyVal{ + Key: "workflow.task.creator", + Value: "", + Type: "string", + IsCredential: false, + }) + resp = append(resp, &commonmodels.KeyVal{ + Key: "workflow.task.creator.id", + Value: "", + Type: "string", + IsCredential: false, + }) + resp = append(resp, &commonmodels.KeyVal{ + Key: "workflow.task.creator.userId", + Value: "", + Type: "string", + IsCredential: false, + }) + resp = append(resp, &commonmodels.KeyVal{ + Key: "workflow.task.is_release_plan_trigger", + Value: "", + Type: "string", + IsCredential: false, + }) + resp = append(resp, &commonmodels.KeyVal{ + Key: "workflow.task.timestamp", + Value: "", + Type: "string", + IsCredential: false, + }) + resp = append(resp, &commonmodels.KeyVal{ + Key: "workflow.task.datetime", + Value: "", + Type: "string", + IsCredential: false, + }) + resp = append(resp, &commonmodels.KeyVal{ + Key: "workflow.task.id", + Value: "", + Type: "string", + IsCredential: false, + }) + resp = append(resp, &commonmodels.KeyVal{ + Key: "workflow.task.url", + Value: workflow.Name, + Type: "string", + IsCredential: false, + }) + resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.branch", Type: "string", IsCredential: false}) + resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.target_branch", Type: "string", IsCredential: false}) + resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.pr", Type: "string", IsCredential: false}) + resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.commit_id", Type: "string", IsCredential: false}) + resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.commit_sha", Type: "string", IsCredential: false}) + resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.commit_message", Type: "string", IsCredential: false}) + resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.committer", Type: "string", IsCredential: false}) + resp = append(resp, &commonmodels.KeyVal{Key: "workflow.trigger.event", Type: "string", IsCredential: false}) + return resp +} + func (w *Workflow) Validate(isExecution bool) error { if w.Project == "" { err := fmt.Errorf("project should not be empty") @@ -633,61 +963,7 @@ func (w *Workflow) GetReferableVariables(currentJobName string, option GetWorkfl }) if option.GetRuntimeVariables { - resp = append(resp, &commonmodels.KeyVal{ - Key: "workflow.task.creator", - Value: "", - Type: "string", - IsCredential: false, - }) - - resp = append(resp, &commonmodels.KeyVal{ - Key: "workflow.task.creator.id", - Value: "", - Type: "string", - IsCredential: false, - }) - - resp = append(resp, &commonmodels.KeyVal{ - Key: "workflow.task.creator.userId", - Value: "", - Type: "string", - IsCredential: false, - }) - - resp = append(resp, &commonmodels.KeyVal{ - Key: "workflow.task.is_release_plan_trigger", - Value: "", - Type: "string", - IsCredential: false, - }) - - resp = append(resp, &commonmodels.KeyVal{ - Key: "workflow.task.timestamp", - Value: "", - Type: "string", - IsCredential: false, - }) - - resp = append(resp, &commonmodels.KeyVal{ - Key: "workflow.task.datetime", - Value: "", - Type: "string", - IsCredential: false, - }) - - resp = append(resp, &commonmodels.KeyVal{ - Key: "workflow.task.id", - Value: "", - Type: "string", - IsCredential: false, - }) - - resp = append(resp, &commonmodels.KeyVal{ - Key: "workflow.task.url", - Value: w.Name, - Type: "string", - IsCredential: false, - }) + resp = append(resp, buildRuntimeReferableVariables(w.WorkflowV4)...) } for _, param := range w.Params { diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/types.go b/pkg/microservice/aslan/core/workflow/service/workflow/types.go index ddc0b45bea..5a5c7858b3 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/types.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/types.go @@ -169,36 +169,43 @@ type CreateCustomTaskLarkUserInfo struct { } type CreateCustomTaskLarkGroupNotificationConfig struct { - ChatID string `json:"chat_id"` - AtUsers []CreateCustomTaskLarkUserInfo `json:"at_users"` + ChatID string `json:"chat_id"` + AtUsers []CreateCustomTaskLarkUserInfo `json:"at_users"` + DynamicRecipients []string `json:"dynamic_recipients"` } type CreateCustomTaskLarkPersonNotificationConfig struct { - Users []CreateCustomTaskLarkUserInfo `json:"users"` + Users []CreateCustomTaskLarkUserInfo `json:"users"` + DynamicRecipients []string `json:"dynamic_recipients"` } type CreateCustomTaskLarkHookNotificationConfig struct { - AtUsers []string `json:"at_users"` - IsAtAll bool `json:"is_at_all"` + AtUsers []string `json:"at_users"` + DynamicRecipients []string `json:"dynamic_recipients"` + IsAtAll bool `json:"is_at_all"` } type CreateCustomTaskWechatNotificationConfig struct { - AtUsers []string `json:"at_users"` - IsAtAll bool `json:"is_at_all"` + AtUsers []string `json:"at_users"` + DynamicRecipients []string `json:"dynamic_recipients"` + IsAtAll bool `json:"is_at_all"` } type CreateCustomTaskDingDingNotificationConfig struct { - AtMobiles []string `json:"at_mobiles"` - IsAtAll bool `json:"is_at_all"` + AtMobiles []string `json:"at_mobiles"` + DynamicRecipients []string `json:"dynamic_recipients"` + IsAtAll bool `json:"is_at_all"` } type CreateCustomTaskMSTeamsNotificationConfig struct { - AtEmails []string `json:"at_emails"` + AtEmails []string `json:"at_emails"` + DynamicRecipients []string `json:"dynamic_recipients"` } type CreateCustomTaskMailNotificationConfig struct { - UserIDs []string `json:"user_ids"` - Users []*commonmodels.User `json:"users"` + UserIDs []string `json:"user_ids"` + Users []*commonmodels.User `json:"users"` + DynamicRecipients []string `json:"dynamic_recipients"` } type CreateCustomTaskParam struct { diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go index 3bff0a6920..c82da003d6 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go @@ -44,6 +44,7 @@ import ( "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service" commonservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/dingtalk" + "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/dynamicrecipient" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/instantmessage" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/lark" "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/s3" @@ -558,7 +559,10 @@ func CreateWorkflowTaskV4(args *CreateWorkflowTaskV4Args, workflow *commonmodels // use the latest workflow's notification settings workflow.NotifyCtls = originalWorkflow.NotifyCtls } else { - workflow.NotifyCtls = updateNotifyCtls(workflow.NotifyCtls, args.NotifyInput) + workflow.NotifyCtls, err = updateNotifyCtls(workflow.NotifyCtls, args.NotifyInput) + if err != nil { + return resp, e.ErrCreateTask.AddDesc(err.Error()) + } } workflowTask.Hash = originalWorkflow.Hash } else { @@ -668,6 +672,7 @@ func CreateWorkflowTaskV4(args *CreateWorkflowTaskV4Args, workflow *commonmodels log.Errorf("fill serviceModules to jobs error: %v", err) return resp, e.ErrCreateTask.AddDesc(err.Error()) } + workflowTask.GlobalContext = buildWorkflowTaskRuntimeContext(workflowTask) if err := instantmessage.NewWeChatClient().SendWorkflowTaskNotifications(workflowTask); err != nil { log.Errorf("send workflow task notification failed, error: %v", err) @@ -691,12 +696,27 @@ func CreateWorkflowTaskV4(args *CreateWorkflowTaskV4Args, workflow *commonmodels return resp, nil } -func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*CreateCustomTaskNotifyInput) []*commonmodels.NotifyCtl { +func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*CreateCustomTaskNotifyInput) ([]*commonmodels.NotifyCtl, error) { notifyInputsMap := make(map[int]*CreateCustomTaskNotifyInput) for _, notifyInput := range notifyInputs { notifyInputsMap[notifyInput.ID] = notifyInput } + toDynamicRecipients := func(notifyType setting.NotifyWebHookType, appID string, inputs []string) ([]string, error) { + resp := make([]string, 0, len(inputs)) + for _, input := range inputs { + input = strings.TrimSpace(input) + if input == "" { + continue + } + resp = append(resp, input) + } + if err := dynamicrecipient.ValidateDynamicRecipientsForNotifyConfig(notifyType, appID, resp); err != nil { + return nil, err + } + return resp, nil + } + for i, notifyCtl := range notifyCtls { notifyInput, ok := notifyInputsMap[i] if ok && notifyCtl.WebHookType == notifyInput.Type { @@ -710,11 +730,17 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("lark hook notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyCtl.LarkHookNotificationConfig.AppID, notifyInput.LarkHookNotificationConfig.DynamicRecipients) + if err != nil { + return nil, err + } config := &commonmodels.LarkHookNotificationConfig{ - HookAddress: notifyCtl.LarkHookNotificationConfig.HookAddress, - AtUsers: notifyInput.LarkHookNotificationConfig.AtUsers, - IsAtAll: notifyInput.LarkHookNotificationConfig.IsAtAll, + AppID: notifyCtl.LarkHookNotificationConfig.AppID, + HookAddress: notifyCtl.LarkHookNotificationConfig.HookAddress, + AtUsers: notifyInput.LarkHookNotificationConfig.AtUsers, + DynamicRecipients: commonmodels.DynamicRecipients(dynamicRecipients), + IsAtAll: notifyInput.LarkHookNotificationConfig.IsAtAll, } notifyCtl.LarkHookNotificationConfig = config @@ -723,9 +749,14 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("lark person notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyCtl.LarkPersonNotificationConfig.AppID, notifyInput.LarkPersonNotificationConfig.DynamicRecipients) + if err != nil { + return nil, err + } config := &commonmodels.LarkPersonNotificationConfig{ - AppID: notifyCtl.LarkPersonNotificationConfig.AppID, + AppID: notifyCtl.LarkPersonNotificationConfig.AppID, + DynamicRecipients: commonmodels.DynamicRecipients(dynamicRecipients), } targetUsers := make([]*larktool.UserInfo, 0) @@ -745,9 +776,14 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("lark group notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, notifyCtl.LarkGroupNotificationConfig.AppID, notifyInput.LarkGroupNotificationConfig.DynamicRecipients) + if err != nil { + return nil, err + } config := &commonmodels.LarkGroupNotificationConfig{ - AppID: notifyCtl.LarkGroupNotificationConfig.AppID, + AppID: notifyCtl.LarkGroupNotificationConfig.AppID, + DynamicRecipients: commonmodels.DynamicRecipients(dynamicRecipients), Chat: &commonmodels.LarkChat{ ChatID: notifyInput.LarkGroupNotificationConfig.ChatID, }, @@ -768,11 +804,16 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("wechat notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, "", notifyInput.WechatNotificationConfig.DynamicRecipients) + if err != nil { + return nil, err + } config := &commonmodels.WechatNotificationConfig{ - HookAddress: notifyCtl.WechatNotificationConfig.HookAddress, - AtUsers: notifyInput.WechatNotificationConfig.AtUsers, - IsAtAll: notifyInput.WechatNotificationConfig.IsAtAll, + HookAddress: notifyCtl.WechatNotificationConfig.HookAddress, + AtUsers: notifyInput.WechatNotificationConfig.AtUsers, + DynamicRecipients: commonmodels.DynamicRecipients(dynamicRecipients), + IsAtAll: notifyInput.WechatNotificationConfig.IsAtAll, } notifyCtl.WechatNotificationConfig = config @@ -781,11 +822,16 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("dingding notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, "", notifyInput.DingDingNotificationConfig.DynamicRecipients) + if err != nil { + return nil, err + } config := &commonmodels.DingDingNotificationConfig{ - HookAddress: notifyCtl.DingDingNotificationConfig.HookAddress, - AtMobiles: notifyInput.DingDingNotificationConfig.AtMobiles, - IsAtAll: notifyInput.DingDingNotificationConfig.IsAtAll, + HookAddress: notifyCtl.DingDingNotificationConfig.HookAddress, + AtMobiles: notifyInput.DingDingNotificationConfig.AtMobiles, + DynamicRecipients: commonmodels.DynamicRecipients(dynamicRecipients), + IsAtAll: notifyInput.DingDingNotificationConfig.IsAtAll, } notifyCtl.DingDingNotificationConfig = config @@ -794,10 +840,15 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("msteams notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, "", notifyInput.MSTeamsNotificationConfig.DynamicRecipients) + if err != nil { + return nil, err + } config := &commonmodels.MSTeamsNotificationConfig{ - HookAddress: notifyCtl.MSTeamsNotificationConfig.HookAddress, - AtEmails: notifyInput.MSTeamsNotificationConfig.AtEmails, + HookAddress: notifyCtl.MSTeamsNotificationConfig.HookAddress, + AtEmails: notifyInput.MSTeamsNotificationConfig.AtEmails, + DynamicRecipients: commonmodels.DynamicRecipients(dynamicRecipients), } notifyCtl.MSTeamsNotificationConfig = config @@ -806,9 +857,14 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea log.Errorf("mail notification config is nil for notify type: %s", notifyCtl.WebHookType) continue } + dynamicRecipients, err := toDynamicRecipients(notifyCtl.WebHookType, "", notifyInput.MailNotificationConfig.DynamicRecipients) + if err != nil { + return nil, err + } config := &commonmodels.MailNotificationConfig{ - TargetUsers: make([]*commonmodels.User, 0), + TargetUsers: make([]*commonmodels.User, 0), + DynamicRecipients: commonmodels.DynamicRecipients(dynamicRecipients), } if len(notifyInput.MailNotificationConfig.Users) > 0 { @@ -839,7 +895,43 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea } } } - return notifyCtls + return notifyCtls, nil +} + +func buildWorkflowTaskRuntimeContext(task *commonmodels.WorkflowTask) map[string]string { + if task == nil { + return nil + } + + resp := make(map[string]string) + for key, value := range task.GlobalContext { + resp[key] = value + } + + if task.WorkflowArgs == nil { + return resp + } + + keyMap := commonutil.KeyValsToMap(commonutil.BuildWorkflowRuntimeVariableKVs( + task.WorkflowArgs, + task.ProjectName, + task.ProjectDisplayName, + task.TaskID, + task.TaskCreator, + task.TaskCreatorAccount, + task.TaskCreatorID, + time.Unix(task.StartTime, 0), + )) + + for key, value := range keyMap { + // Payload variables are resolved from HookPayload.RawPayload on demand; + // they don't need to be duplicated into GlobalContext. + if strings.HasPrefix(key, "payload.") { + continue + } + resp[runtimeWorkflowController.GetContextKey(fmt.Sprintf("{{.%s}}", key))] = value + } + return resp } func GetManualExecWorkflowTaskV4Info(workflowName string, taskID int64, logger *zap.SugaredLogger) (*commonmodels.WorkflowV4, error) { @@ -919,7 +1011,16 @@ func RetryWorkflowTaskV4(workflowName string, taskID int64, logger *zap.SugaredL task.RetryNum++ - globalKeyMap := make(map[string]string) + globalKeyMap := commonutil.KeyValsToMap(commonutil.BuildWorkflowRuntimeVariableKVs( + task.WorkflowArgs, + task.ProjectName, + task.ProjectDisplayName, + task.TaskID, + task.TaskCreator, + task.TaskCreatorAccount, + task.TaskCreatorID, + time.Unix(task.StartTime, 0), + )) jobTaskMap := make(map[string]*commonmodels.JobTask) for _, stage := range task.WorkflowArgs.Stages { for _, job := range stage.Jobs { @@ -971,6 +1072,7 @@ func RetryWorkflowTaskV4(workflowName string, taskID int64, logger *zap.SugaredL globalKeyMap[key] = item.Value } } + task.GlobalContext = buildWorkflowTaskRuntimeContext(task) for _, stage := range task.Stages { if stage.Status == config.StatusPassed || stage.Status == config.StatusSkipped { @@ -990,15 +1092,8 @@ func RetryWorkflowTaskV4(workflowName string, taskID int64, logger *zap.SugaredL jobTask.EndTime = 0 jobTask.Error = "" if t, ok := jobTaskMap[jobTask.Name]; ok { - taskBytes, _ := json.Marshal(t) - taskString := string(taskBytes) - for k, v := range globalKeyMap { - taskString = strings.ReplaceAll(taskString, fmt.Sprintf("{{.%s}}", k), v) - log.Debugf("replacing key %s with value: %s", fmt.Sprintf("{{.%s}}", k), v) - } - err := json.Unmarshal([]byte(taskString), &t) - if err != nil { - return fmt.Errorf("failed to replace input variable for task: %s, error: %s", t.Name, err) + if err := workflowController.RenderJobTaskWithGlobalVariables(t, globalKeyMap); err != nil { + return err } jobTask.Spec = t.Spec } else { @@ -1052,7 +1147,16 @@ func ManualExecWorkflowTaskV4(workflowName string, taskID int64, stageName strin return e.ErrCreateTask.AddErr(fmt.Errorf("save original jobs error: %v", err)) } - globalKeyMap := make(map[string]string) + globalKeyMap := commonutil.KeyValsToMap(commonutil.BuildWorkflowRuntimeVariableKVs( + task.WorkflowArgs, + task.ProjectName, + task.ProjectDisplayName, + task.TaskID, + task.TaskCreator, + task.TaskCreatorAccount, + task.TaskCreatorID, + time.Unix(task.StartTime, 0), + )) for _, stage := range task.WorkflowArgs.Stages { if stage.Name == stageName { @@ -1125,6 +1229,7 @@ func ManualExecWorkflowTaskV4(workflowName string, taskID int64, stageName strin globalKeyMap[key] = item.Value } } + task.GlobalContext = buildWorkflowTaskRuntimeContext(task) for _, stage := range task.OriginWorkflowArgs.Stages { if stage.Name == stageName { @@ -1182,15 +1287,8 @@ func ManualExecWorkflowTaskV4(workflowName string, taskID int64, stageName strin job.Spec = ctrl.GetSpec() for _, task := range jobTasks { - taskBytes, _ := json.Marshal(task) - taskString := string(taskBytes) - for k, v := range globalKeyMap { - taskString = strings.ReplaceAll(taskString, fmt.Sprintf("{{.%s}}", k), v) - log.Debugf("replacing key %s with value: %s", fmt.Sprintf("{{.%s}}", k), v) - } - err := json.Unmarshal([]byte(taskString), &task) - if err != nil { - return fmt.Errorf("failed to replace input variable for task: %s, error: %s", task.Name, err) + if err := workflowController.RenderJobTaskWithGlobalVariables(task, globalKeyMap); err != nil { + return err } } diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go index 4a3e14b654..a167795253 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go @@ -2617,6 +2617,14 @@ func getDefaultVars(workflow *commonmodels.WorkflowV4, currentJobName string) [] vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.task.timestamp")) vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.task.datetime")) vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.task.id")) + vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.branch")) + vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.target_branch")) + vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.pr")) + vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.commit_id")) + vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.commit_sha")) + vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.commit_message")) + vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.committer")) + vars = append(vars, fmt.Sprintf(setting.RenderValueTemplate, "workflow.trigger.event")) for _, param := range workflow.Params { if param.ParamsType == "repo" || param.ParamsType == "file" { continue diff --git a/pkg/microservice/user/core/handler/user/user.go b/pkg/microservice/user/core/handler/user/user.go index 0f037b9cce..eb2d42d3aa 100644 --- a/pkg/microservice/user/core/handler/user/user.go +++ b/pkg/microservice/user/core/handler/user/user.go @@ -275,6 +275,10 @@ func ListUsers(c *gin.Context) { if len(args.UIDs) > 0 { ctx.Resp, ctx.RespErr = permission.SearchUsersByUIDs(args.UIDs, ctx.Logger) + } else if len(args.Email) > 0 { + ctx.Resp, ctx.RespErr = permission.SearchUsersByEmail(args, ctx.Logger) + } else if len(args.Phone) > 0 { + ctx.Resp, ctx.RespErr = permission.SearchUsersByPhone(args, ctx.Logger) } else if len(args.Account) > 0 { if len(args.IdentityType) == 0 { args.IdentityType = config.SystemIdentityType @@ -384,6 +388,10 @@ func ListUsersBrief(c *gin.Context) { var resp *types.UsersResp if len(args.UIDs) > 0 { resp, err = permission.SearchUsersByUIDs(args.UIDs, ctx.Logger) + } else if len(args.Email) > 0 { + resp, err = permission.SearchUsersByEmail(args, ctx.Logger) + } else if len(args.Phone) > 0 { + resp, err = permission.SearchUsersByPhone(args, ctx.Logger) } else if len(args.Account) > 0 { if len(args.IdentityType) == 0 { args.IdentityType = config.SystemIdentityType diff --git a/pkg/microservice/user/core/init/dm_mysql.sql b/pkg/microservice/user/core/init/dm_mysql.sql index a06dee9bf2..1eed155595 100644 --- a/pkg/microservice/user/core/init/dm_mysql.sql +++ b/pkg/microservice/user/core/init/dm_mysql.sql @@ -42,6 +42,8 @@ CREATE TABLE IF NOT EXISTS "user"( ) ; CREATE UNIQUE INDEX IF NOT EXISTS account ON "user"(account,identity_type); +CREATE INDEX IF NOT EXISTS idx_email ON "user"(email); +CREATE INDEX IF NOT EXISTS idx_phone ON "user"(phone); CREATE TABLE IF NOT EXISTS user_group ( group_id varchar(64) NOT NULL COMMENT '用户组ID', diff --git a/pkg/microservice/user/core/init/mysql.sql b/pkg/microservice/user/core/init/mysql.sql index 68d491b991..14a926951a 100644 --- a/pkg/microservice/user/core/init/mysql.sql +++ b/pkg/microservice/user/core/init/mysql.sql @@ -38,6 +38,8 @@ CREATE TABLE IF NOT EXISTS `user`( `created_at` int(11) unsigned NOT NULL COMMENT '创建时间', `updated_at` int(11) unsigned NOT NULL COMMENT '修改时间', UNIQUE KEY `account` (`account`,`identity_type`), + KEY `idx_email` (`email`) USING BTREE, + KEY `idx_phone` (`phone`) USING BTREE, PRIMARY KEY (`uid`) ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户信息表' ROW_FORMAT = Compact; diff --git a/pkg/microservice/user/core/repository/models/user.go b/pkg/microservice/user/core/repository/models/user.go index 24f87967ec..f93dcb9cca 100644 --- a/pkg/microservice/user/core/repository/models/user.go +++ b/pkg/microservice/user/core/repository/models/user.go @@ -21,8 +21,8 @@ type User struct { UID string `gorm:"primary" json:"uid"` Name string `json:"name"` IdentityType string `gorm:"default:'unknown'" json:"identity_type"` - Email string `json:"email"` - Phone string `json:"phone"` + Email string `gorm:"index:idx_email" json:"email"` + Phone string `gorm:"index:idx_phone" json:"phone"` Account string `json:"account"` APIToken string `gorm:"api_token" json:"api_token"` APITokenEnabled bool `gorm:"column:api_token_enabled;default:0" json:"api_token_enabled"` diff --git a/pkg/microservice/user/core/repository/orm/user.go b/pkg/microservice/user/core/repository/orm/user.go index c45755a5e3..eb9658b325 100644 --- a/pkg/microservice/user/core/repository/orm/user.go +++ b/pkg/microservice/user/core/repository/orm/user.go @@ -46,6 +46,33 @@ func GetUser(account string, identityType string, db *gorm.DB) (*models.User, er return &user, nil } +func ListUsersByAccount(account string, db *gorm.DB) ([]models.User, error) { + users := make([]models.User, 0) + err := db.Where("account = ?", account).Find(&users).Error + if err != nil && err != gorm.ErrRecordNotFound { + return nil, err + } + return users, nil +} + +func ListUsersByEmail(email string, db *gorm.DB) ([]models.User, error) { + users := make([]models.User, 0) + err := db.Where("email = ?", email).Find(&users).Error + if err != nil && err != gorm.ErrRecordNotFound { + return nil, err + } + return users, nil +} + +func ListUsersByPhone(phone string, db *gorm.DB) ([]models.User, error) { + users := make([]models.User, 0) + err := db.Where("phone = ?", phone).Find(&users).Error + if err != nil && err != gorm.ErrRecordNotFound { + return nil, err + } + return users, nil +} + // GetUserByUid Get a user based on uid func GetUserByUid(uid string, db *gorm.DB) (*models.User, error) { var user models.User diff --git a/pkg/microservice/user/core/service/permission/user.go b/pkg/microservice/user/core/service/permission/user.go index 27510fa060..330b020d8d 100644 --- a/pkg/microservice/user/core/service/permission/user.go +++ b/pkg/microservice/user/core/service/permission/user.go @@ -79,6 +79,8 @@ type OpenAPIQueryArgs struct { type QueryArgs struct { Name string `json:"name,omitempty"` Account string `json:"account,omitempty" form:"account"` + Email string `json:"email,omitempty"` + Phone string `json:"phone,omitempty"` IdentityType string `json:"identity_type,omitempty"` UIDs []string `json:"uids,omitempty"` PerPage int `json:"per_page,omitempty" form:"perPage"` @@ -89,6 +91,8 @@ type QueryArgs struct { Order setting.ListUserOrder `json:"order,omitempty" form:"order"` } +const allIdentityTypes = "*" + type Password struct { Uid string `json:"uid"` OldPassword string `json:"oldPassword"` @@ -424,6 +428,15 @@ func GetUserSetting(uid string, logger *zap.SugaredLogger) (*types.UserSetting, } func SearchUserByAccount(args *QueryArgs, logger *zap.SugaredLogger) (*types.UsersResp, error) { + if args.IdentityType == allIdentityTypes { + users, err := orm.ListUsersByAccount(args.Account, repository.DB) + if err != nil { + logger.Errorf("SearchUserByAccount ListUsersByAccount account:%s error, error msg:%s", args.Account, err.Error()) + return nil, err + } + return buildUsersRespFromModels(users, logger) + } + user, err := orm.GetUser(args.Account, args.IdentityType, repository.DB) if err != nil { logger.Errorf("SearchUserByAccount GetUser By account:%s error, error msg:%s", args.Account, err.Error()) @@ -435,34 +448,49 @@ func SearchUserByAccount(args *QueryArgs, logger *zap.SugaredLogger) (*types.Use TotalCount: 0, }, nil } - userLogins, err := orm.ListUserLogins([]string{user.UID}, repository.DB) + + return buildUsersRespFromModels([]models.User{*user}, logger) +} + +func SearchUsersByEmail(args *QueryArgs, logger *zap.SugaredLogger) (*types.UsersResp, error) { + users, err := orm.ListUsersByEmail(args.Email, repository.DB) if err != nil { - logger.Errorf("SearchUserByAccount ListUserLogins By uid:%s error, error msg:%s", user.UID, err.Error()) + logger.Errorf("SearchUsersByEmail ListUsersByEmail email:%s error, error msg:%s", args.Email, err.Error()) return nil, err } - usersInfo := mergeUserLogin([]models.User{*user}, *userLogins, logger) + return buildUsersRespFromModels(users, logger) +} - for _, uInfo := range usersInfo { - roles, err := ListRolesByNamespaceAndUserID("*", uInfo.Uid, logger) - if err != nil { - logger.Errorf("failed to get user role info for user: %s[%s], error: %s", uInfo.Name, uInfo.Account, err) - return nil, err - } - rolebindings := make([]*types.RoleBinding, 0) - for _, role := range roles { - rolebindings = append(rolebindings, &types.RoleBinding{ - UID: uInfo.Uid, - Role: role.Name, - }) - if role.Name == string(setting.SystemAdmin) { - uInfo.Admin = true - uInfo.APITokenEnabled = true - } - } - uInfo.SystemRoleBindings = rolebindings +func SearchUsersByPhone(args *QueryArgs, logger *zap.SugaredLogger) (*types.UsersResp, error) { + users, err := orm.ListUsersByPhone(args.Phone, repository.DB) + if err != nil { + logger.Errorf("SearchUsersByPhone ListUsersByPhone phone:%s error, error msg:%s", args.Phone, err.Error()) + return nil, err } - if err := fillUsersMFAEnabled(usersInfo); err != nil { - logger.Errorf("SearchUserByAccount fillUsersMFAEnabled error, error msg:%s", err.Error()) + return buildUsersRespFromModels(users, logger) +} + +func buildUsersRespFromModels(users []models.User, logger *zap.SugaredLogger) (*types.UsersResp, error) { + if len(users) == 0 { + return &types.UsersResp{ + Users: nil, + TotalCount: 0, + }, nil + } + + uids := make([]string, 0, len(users)) + for _, user := range users { + uids = append(uids, user.UID) + } + + userLogins, err := orm.ListUserLogins(uids, repository.DB) + if err != nil { + logger.Errorf("buildUsersRespFromModels ListUserLogins By uids:%s error, error msg:%s", uids, err.Error()) + return nil, err + } + + usersInfo := mergeUserLogin(users, *userLogins, logger) + if err := enrichUsersInfo(usersInfo, logger); err != nil { return nil, err } @@ -566,27 +594,7 @@ func SearchUsers(args *QueryArgs, logger *zap.SugaredLogger) (*types.UsersResp, usersInfo = mergeUserLoginWithLoginTime(users, *userLogins, logger) } - for _, uInfo := range usersInfo { - roles, err := ListRolesByNamespaceAndUserID("*", uInfo.Uid, logger) - if err != nil { - logger.Errorf("failed to get user role info for user: %s[%s], error: %s", uInfo.Name, uInfo.Account, err) - return nil, err - } - rolebindings := make([]*types.RoleBinding, 0) - for _, role := range roles { - rolebindings = append(rolebindings, &types.RoleBinding{ - UID: uInfo.Uid, - Role: role.Name, - }) - if role.Name == string(setting.SystemAdmin) { - uInfo.Admin = true - uInfo.APITokenEnabled = true - } - } - uInfo.SystemRoleBindings = rolebindings - } - if err := fillUsersMFAEnabled(usersInfo); err != nil { - logger.Errorf("SearchUsers fillUsersMFAEnabled error, error msg:%s", err.Error()) + if err := enrichUsersInfo(usersInfo, logger); err != nil { return nil, err } @@ -634,6 +642,33 @@ func fillUsersMFAEnabled(usersInfo []*types.UserInfo) error { return nil } +func enrichUsersInfo(usersInfo []*types.UserInfo, logger *zap.SugaredLogger) error { + for _, uInfo := range usersInfo { + roles, err := ListRolesByNamespaceAndUserID("*", uInfo.Uid, logger) + if err != nil { + logger.Errorf("failed to get user role info for user: %s[%s], error: %s", uInfo.Name, uInfo.Account, err) + return err + } + rolebindings := make([]*types.RoleBinding, 0) + for _, role := range roles { + rolebindings = append(rolebindings, &types.RoleBinding{ + UID: uInfo.Uid, + Role: role.Name, + }) + if role.Name == string(setting.SystemAdmin) { + uInfo.Admin = true + uInfo.APITokenEnabled = true + } + } + uInfo.SystemRoleBindings = rolebindings + } + if err := fillUsersMFAEnabled(usersInfo); err != nil { + logger.Errorf("enrichUsersInfo fillUsersMFAEnabled error, error msg:%s", err.Error()) + return err + } + return nil +} + func mergeUserLoginWithLoginTime(users []models.UserWithLoginTime, userLogins []models.UserLogin, logger *zap.SugaredLogger) []*types.UserInfo { userLoginMap := make(map[string]models.UserLogin) for _, userLogin := range userLogins { @@ -691,41 +726,7 @@ func SearchUsersByUIDs(uids []string, logger *zap.SugaredLogger) (*types.UsersRe logger.Errorf("SearchUsersByUIDs SeachUsers By uids:%s error, error msg:%s", uids, err.Error()) return nil, err } - userLogins, err := orm.ListUserLogins(uids, repository.DB) - if err != nil { - logger.Errorf("SearchUsersByUIDs ListUserLogins By uids:%s error, error msg:%s", uids, err.Error()) - return nil, err - } - usersInfo := mergeUserLogin(users, *userLogins, logger) - - for _, uInfo := range usersInfo { - roles, err := ListRolesByNamespaceAndUserID("*", uInfo.Uid, logger) - if err != nil { - logger.Errorf("failed to get user role info for user: %s[%s], error: %s", uInfo.Name, uInfo.Account, err) - return nil, err - } - rolebindings := make([]*types.RoleBinding, 0) - for _, role := range roles { - rolebindings = append(rolebindings, &types.RoleBinding{ - UID: uInfo.Uid, - Role: role.Name, - }) - if role.Name == string(setting.SystemAdmin) { - uInfo.Admin = true - uInfo.APITokenEnabled = true - } - } - uInfo.SystemRoleBindings = rolebindings - } - if err := fillUsersMFAEnabled(usersInfo); err != nil { - logger.Errorf("SearchUsersByUIDs fillUsersMFAEnabled error, error msg:%s", err.Error()) - return nil, err - } - - return &types.UsersResp{ - Users: usersInfo, - TotalCount: int64(len(usersInfo)), - }, nil + return buildUsersRespFromModels(users, logger) } func getLoginId(user *models.User, loginType config.LoginType) string { diff --git a/pkg/shared/client/user/user.go b/pkg/shared/client/user/user.go index 167d00cfd7..801c9d0a0a 100644 --- a/pkg/shared/client/user/user.go +++ b/pkg/shared/client/user/user.go @@ -82,6 +82,8 @@ func (c *Client) CreateUser(args *CreateUserArgs) (*CreateUserResp, error) { type SearchUserArgs struct { Name string `json:"name,omitempty"` Account string `json:"account,omitempty"` + Email string `json:"email,omitempty"` + Phone string `json:"phone,omitempty"` IdentityType string `json:"identity_type,omitempty"` UIDs []string `json:"uids,omitempty"` PerPage int `json:"per_page,omitempty"` diff --git a/pkg/types/repo.go b/pkg/types/repo.go index 794451353c..8d27a32efe 100644 --- a/pkg/types/repo.go +++ b/pkg/types/repo.go @@ -49,10 +49,12 @@ type Repository struct { IsPrimary bool `bson:"is_primary" json:"is_primary" yaml:"is_primary"` CodehostID int `bson:"codehost_id" json:"codehost_id" yaml:"codehost_id"` // add - OauthToken string `bson:"oauth_token" json:"oauth_token" yaml:"oauth_token"` - Address string `bson:"address" json:"address" yaml:"address"` - AuthorName string `bson:"author_name,omitempty" json:"author_name,omitempty" yaml:"author_name,omitempty"` - CheckoutRef string `bson:"checkout_ref,omitempty" json:"checkout_ref,omitempty" yaml:"checkout_ref,omitempty"` + OauthToken string `bson:"oauth_token" json:"oauth_token" yaml:"oauth_token"` + Address string `bson:"address" json:"address" yaml:"address"` + AuthorName string `bson:"author_name,omitempty" json:"author_name,omitempty" yaml:"author_name,omitempty"` + Committer string `bson:"committer,omitempty" json:"committer,omitempty" yaml:"committer,omitempty"` + TargetBranch string `bson:"target_branch,omitempty" json:"target_branch,omitempty" yaml:"target_branch,omitempty"` + CheckoutRef string `bson:"checkout_ref,omitempty" json:"checkout_ref,omitempty" yaml:"checkout_ref,omitempty"` // username/password authorization for git/perforce Username string `bson:"username,omitempty" json:"username,omitempty" yaml:"username,omitempty"` Password string `bson:"password,omitempty" json:"password,omitempty" yaml:"password,omitempty"`