diff --git a/pkg/cli/upgradeassistant/cmd/migrate/430.go b/pkg/cli/upgradeassistant/cmd/migrate/430.go
index 99cbb550be..142298fb3f 100644
--- a/pkg/cli/upgradeassistant/cmd/migrate/430.go
+++ b/pkg/cli/upgradeassistant/cmd/migrate/430.go
@@ -117,15 +117,21 @@ func V421ToV430() error {
updateMigrationError(migrationInfo.ID, err)
}()
- // 这次迁移分三段:
+ // 这次迁移分四段:
// 1. MySQL: user 表新增 api_token_enabled
- // 2. MySQL: permission action + role/template 绑定
- // 3. Mongo: collaboration mode / instance verbs
+ // 2. MySQL: user 表新增 email/phone 索引
+ // 3. MySQL: permission action + role/template 绑定
+ // 4. Mongo: collaboration mode / instance verbs
err = migrateUserAPITokenEnabledColumn(ctx, migrationInfo)
if err != nil {
return err
}
+ err = migrateUserContactIndexes(migrationInfo)
+ if err != nil {
+ return err
+ }
+
err = migrateGlobalReadOnlyRole(ctx, migrationInfo)
if err != nil {
return err
@@ -144,6 +150,29 @@ func V421ToV430() error {
return nil
}
+// migrateUserContactIndexes adds indexes for dynamic notification recipient lookups.
+func migrateUserContactIndexes(migrationInfo *internalmodels.Migration) error {
+ if !migrationInfo.Migration430UserContactIndexes {
+ if !repository.DB.Migrator().HasIndex(&usermodels.User{}, "idx_email") {
+ if err := repository.DB.Migrator().CreateIndex(&usermodels.User{}, "idx_email"); err != nil {
+ return fmt.Errorf("failed to add idx_email index for user table, err: %s", err)
+ }
+ }
+
+ if !repository.DB.Migrator().HasIndex(&usermodels.User{}, "idx_phone") {
+ if err := repository.DB.Migrator().CreateIndex(&usermodels.User{}, "idx_phone"); err != nil {
+ return fmt.Errorf("failed to add idx_phone index for user table, err: %s", err)
+ }
+ }
+ }
+
+ _ = internalmongodb.NewMigrationColl().UpdateMigrationStatus(migrationInfo.ID, map[string]interface{}{
+ getMigrationFieldBsonTag(migrationInfo, &migrationInfo.Migration430UserContactIndexes): true,
+ })
+
+ return nil
+}
+
// migrateUserAPITokenEnabledColumn adds api_token_enabled column for user table.
func migrateUserAPITokenEnabledColumn(_ *internalhandler.Context, migrationInfo *internalmodels.Migration) error {
if !migrationInfo.Migration430UserAPITokenEnabled {
diff --git a/pkg/cli/upgradeassistant/internal/repository/models/migration.go b/pkg/cli/upgradeassistant/internal/repository/models/migration.go
index 9cb27d1062..a8e675e7e3 100644
--- a/pkg/cli/upgradeassistant/internal/repository/models/migration.go
+++ b/pkg/cli/upgradeassistant/internal/repository/models/migration.go
@@ -41,6 +41,7 @@ type Migration struct {
Migration421CollaborationRollbackPermission bool `bson:"migration_421_collaboration_rollback_permission"`
Migration421WorkflowDeploySpec bool `bson:"migration_421_workflow_deploy_spec"`
Migration430UserAPITokenEnabled bool `bson:"migration_430_user_api_token_enabled"`
+ Migration430UserContactIndexes bool `bson:"migration_430_user_contact_indexes"`
Migration430GlobalReadOnlyRole bool `bson:"migration_430_global_read_only_role"`
Migration430ScalePermission bool `bson:"migration_430_scale_permission"`
Migration430CollaborationScalePermission bool `bson:"migration_430_collaboration_scale_permission"`
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 f68200a438..78d0ee39ea 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
@@ -794,6 +794,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 20ffe6e6c9..57d56b7845 100644
--- a/pkg/microservice/aslan/core/common/repository/models/workflow.go
+++ b/pkg/microservice/aslan/core/common/repository/models/workflow.go
@@ -485,14 +485,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 913082a42a..4db3aae9ad 100644
--- a/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go
+++ b/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go
@@ -1173,12 +1173,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"`
@@ -1262,6 +1262,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)
@@ -1272,42 +1276,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..3812f925ae
--- /dev/null
+++ b/pkg/microservice/aslan/core/common/service/dynamicrecipient/dynamic_recipient.go
@@ -0,0 +1,541 @@
+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"
+)
+
+var supportedDynamicRecipientKinds = map[setting.NotifyWebHookType]map[dynamicRecipientKind]struct{}{
+ setting.NotifyWebhookTypeFeishuApp: {
+ dynamicRecipientKindEmail: {},
+ dynamicRecipientKindMobile: {},
+ },
+ setting.NotifyWebHookTypeFeishuPerson: {
+ dynamicRecipientKindEmail: {},
+ dynamicRecipientKindMobile: {},
+ },
+ setting.NotifyWebHookTypeFeishu: {
+ dynamicRecipientKindEmail: {},
+ dynamicRecipientKindMobile: {},
+ },
+ setting.NotifyWebHookTypeWechatWork: {},
+ setting.NotifyWebHookTypeDingDing: {
+ dynamicRecipientKindEmail: {},
+ dynamicRecipientKindMobile: {},
+ },
+ setting.NotifyWebHookTypeMSTeam: {
+ dynamicRecipientKindEmail: {},
+ dynamicRecipientKindMobile: {},
+ },
+ setting.NotifyWebHookTypeMail: {
+ dynamicRecipientKindEmail: {},
+ dynamicRecipientKindMobile: {},
+ },
+}
+
+type dynamicRecipientSpec struct {
+ raw string
+ key string
+ kind dynamicRecipientKind
+}
+
+type Resolver struct {
+ keyMap map[string]string
+
+ lookupUsersByEmail func(email string) ([]*userclient.User, error)
+ lookupUsersByPhone func(phone string) ([]*userclient.User, error)
+
+ 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 len(recipients) == 0 || !isLarkNotifyType(notifyType) || strings.TrimSpace(appID) != "" {
+ return nil
+ }
+
+ return fmt.Errorf("app_id is required to resolve dynamic recipients for notification type %s", notifyType)
+}
+
+func NewResolver(keyMap map[string]string) *Resolver {
+ return &Resolver{
+ keyMap: keyMap,
+ lookupUsersByEmail: func(email string) ([]*userclient.User, error) {
+ return searchUsersByEmail(email)
+ },
+ lookupUsersByPhone: func(phone string) ([]*userclient.User, error) {
+ return searchUsersByPhone(phone)
+ },
+ 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)
+ }
+ }
+ 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)
+ }
+ }
+ default:
+ return nil, fmt.Errorf("dynamic recipient %s cannot be resolved to mobile", recipient)
+ }
+ }
+ return UniqStrings(resp), nil
+}
+
+func (r *Resolver) ResolveUserIDs(recipients []string) ([]string, error) {
+ for _, recipient := range recipients {
+ _, _, ok, err := r.resolveRecipient(recipient)
+ if err != nil {
+ return nil, err
+ }
+ if !ok {
+ continue
+ }
+ return nil, fmt.Errorf("dynamic recipient %s cannot be resolved to user_id", recipient)
+ }
+ return nil, nil
+}
+
+func (r *Resolver) ResolveLarkUsers(recipients []string, appID string) ([]*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")
+ }
+ 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 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})
+ }
+ 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) 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) 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
+ default:
+ return nil, fmt.Errorf("dynamic recipient %s is not supported, only email/mobile(phone) are allowed", input)
+ }
+
+ return &dynamicRecipientSpec{
+ raw: input,
+ key: key,
+ kind: kind,
+ }, 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")
+}
+
+func isLarkNotifyType(notifyType setting.NotifyWebHookType) bool {
+ return notifyType == setting.NotifyWebHookTypeFeishu ||
+ notifyType == setting.NotifyWebhookTypeFeishuApp ||
+ notifyType == setting.NotifyWebHookTypeFeishuPerson
+}
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/lark.go b/pkg/microservice/aslan/core/common/service/instantmessage/lark.go
index 0d39b9a447..0e8f733f67 100644
--- a/pkg/microservice/aslan/core/common/service/instantmessage/lark.go
+++ b/pkg/microservice/aslan/core/common/service/instantmessage/lark.go
@@ -198,6 +198,10 @@ func (w *Service) sendFeishuMessage(uri string, lcMsg *LarkCard) error {
return err
}
+func (w *Service) SendFeishuHookCard(uri string, lcMsg *LarkCard) error {
+ return w.sendFeishuMessage(uri, lcMsg)
+}
+
func (w *Service) sendFeishuMessageFromClient(client *lark.Client, receiverType, receiverID, messageType, messageBody string) error {
err := client.SendMessage(receiverType, messageType, receiverID, messageBody)
@@ -229,6 +233,10 @@ func (w *Service) sendFeishuMessageOfSingleType(title, uri, content string) erro
return err
}
+func (w *Service) SendFeishuHookText(uri, content string) error {
+ return w.sendFeishuMessageOfSingleType("", uri, content)
+}
+
func getColorTemplateWithStatus(status config.Status) string {
if status == config.StatusPassed || status == config.StatusCreated {
return feishuHeaderTemplateGreen
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 dc19bf35bc..8c6016dd59 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 {
@@ -375,6 +381,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 {
@@ -465,6 +476,90 @@ func shouldSkipFeishuPersonPauseNotification(task *models.WorkflowTask, notify *
return false
}
+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)
+ 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)
+ 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)
+ 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
@@ -934,7 +1029,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)
@@ -1194,7 +1289,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)
@@ -1521,6 +1616,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 ef9299e3bf..21dfa3b685 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 {
@@ -177,6 +311,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)
@@ -187,6 +329,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..bf74e7a4c9 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)
+ 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)
+ 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)
+ 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()
@@ -223,7 +380,7 @@ func sendLarkMessage(client *lark.Client, productName, workflowName, workflowDis
productName,
workflowName,
taskID,
- workflowDisplayName,
+ url.QueryEscape(workflowDisplayName),
)
card.AddI18NElementsZhcnAction("点击查看更多信息", url)
@@ -240,18 +397,8 @@ func sendLarkMessage(client *lark.Client, productName, workflowName, workflowDis
// then send @ message
if len(idList) > 0 || isAtAll {
- atUserList := []string{}
- 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 += ""
- }
-
larkAtMessage := &instantmessage.FeiShuMessage{
- Text: atMessage,
+ Text: buildLarkAtMessage(idList, isAtAll),
}
atMessageContent, err := json.Marshal(larkAtMessage)
@@ -268,6 +415,34 @@ func sendLarkMessage(client *lark.Client, productName, workflowName, workflowDis
return nil
}
+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)
+
+ detailURL := fmt.Sprintf("%s/v1/projects/detail/%s/pipelines/custom/%s/%d?display_name=%s",
+ configbase.SystemAddress(),
+ productName,
+ workflowName,
+ taskID,
+ url.QueryEscape(workflowDisplayName),
+ )
+ card.AddI18NElementsZhcnAction("点击查看更多信息", detailURL)
+
+ imService := instantmessage.NewWeChatClient()
+ if err := imService.SendFeishuHookCard(uri, card); err != nil {
+ return err
+ }
+
+ if len(idList) == 0 && !isAtAll {
+ return nil
+ }
+
+ atMessage := buildLarkAtMessage(idList, isAtAll)
+ return imService.SendFeishuHookText(uri, atMessage)
+}
+
func sendDingDingMessage(productName, workflowName, workflowDisplayName string, taskID int64, uri, title, message string, idList []string, isAtAll bool) error {
processedMessage := generateDingDingNotificationMessage(title, message, idList)
@@ -279,31 +454,7 @@ func sendDingDingMessage(productName, workflowName, workflowDisplayName string,
url.PathEscape(workflowDisplayName),
)
- // 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.DingDingMessage{
- MsgType: instantmessage.DingDingMsgType,
- ActionCard: &instantmessage.DingDingActionCard{
- HideAvatar: "0",
- ButtonOrientation: "0",
- Text: processedMessage,
- Title: title,
- Buttons: []*instantmessage.DingDingButton{
- {
- Title: "点击查看更多信息",
- ActionURL: dingtalkRedirectURL,
- },
- },
- },
- }
-
- messageReq.At = &instantmessage.DingDingAt{
- AtMobiles: idList,
- IsAtAll: isAtAll,
- }
+ messageReq := instantmessage.BuildDingDingMessage(title, processedMessage, actionURL, idList, isAtAll)
// TODO: if required, add proxy to it
c := httpclient.New()
@@ -438,7 +589,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 71446019d9..90841358ab 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 (
@@ -261,16 +261,18 @@ func (c *workflowCtl) Run(ctx context.Context, concurrency int) {
WorkflowTaskCreatorUserID: c.workflowTask.TaskCreatorID,
WorkflowTaskCreatorMobile: c.workflowTask.TaskCreatorPhone,
WorkflowTaskCreatorEmail: c.workflowTask.TaskCreatorEmail,
- 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()),
- ConfigMapMountDir: fmt.Sprintf("/tmp/%s/cm/%d", uuid.NewString(), time.Now().Unix()),
- GlobalContextGetAll: c.getGlobalContextAll,
- GlobalContextGet: c.getGlobalContext,
- GlobalContextSet: c.setGlobalContext,
- GlobalContextEach: c.globalContextEach,
- ClusterIDAdd: c.addClusterID,
- StartTime: time.Now(),
+ // NotificationJobCtl uses these variables to render notification titles, content and recipient templates at send time.
+ 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()),
+ ConfigMapMountDir: fmt.Sprintf("/tmp/%s/cm/%d", uuid.NewString(), time.Now().Unix()),
+ GlobalContextGetAll: c.getGlobalContextAll,
+ GlobalContextGet: c.getGlobalContext,
+ GlobalContextSet: c.setGlobalContext,
+ GlobalContextEach: c.globalContextEach,
+ ClusterIDAdd: c.addClusterID,
+ StartTime: time.Now(),
}
defer jobcontroller.CleanWorkflowJobs(ctx, c.workflowTask, workflowCtx, c.logger, c.ack)
if err := scmnotify.NewService().UpdateWebhookCommentForWorkflowV4(c.workflowTask, c.logger); err != nil {
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 3084f186a1..aaf293761a 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
@@ -164,6 +164,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,
}
}
@@ -206,7 +211,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,
}
}
@@ -250,7 +259,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)
@@ -273,6 +281,7 @@ func TriggerWorkflowV4ByGerritEvent(event *gerritTypeEvent, body []byte, uri, ba
continue
}
for _, item := range gitHooks {
+ var hookPayload *commonmodels.HookPayload
if !item.Enabled {
continue
}
@@ -304,7 +313,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.
@@ -312,6 +323,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,
@@ -335,16 +347,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 3cd08ad6d7..f8546c59c9 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
@@ -280,6 +280,7 @@ func TriggerTestByGiteeEvent(event interface{}, baseURI, requestID string, log *
IsPr: true,
MergeRequestID: mergeRequestID,
CommitID: commitID,
+ CommitSHA: commitID,
EventType: eventType,
}
case *gitee.PushEvent:
@@ -298,6 +299,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 e00e12e37d..14a6af529a 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
@@ -73,12 +73,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,
}
}
@@ -125,7 +133,10 @@ 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,
+ Committer: hookRepo.Committer,
Source: hookRepo.Source,
}
}
@@ -164,7 +175,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,
}
}
@@ -197,7 +210,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)
@@ -267,10 +280,14 @@ 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,
+ Committer: ev.PullRequest.User.Login,
EventType: eventType,
+ RawPayload: rawPayload,
}
case *gitee.PushEvent:
eventType = EventTypePush
@@ -280,19 +297,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 97ac352835..73ba35b070 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
@@ -95,6 +95,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:
@@ -112,6 +113,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 0f03e48546..3d41514b1e 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
@@ -86,6 +86,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:
@@ -103,6 +104,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 81594ed0cc..05eb6a571d 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
@@ -79,8 +79,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,
}
}
@@ -131,9 +133,10 @@ 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,
}
}
@@ -175,7 +178,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,
}
}
@@ -208,7 +213,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)
@@ -270,13 +275,17 @@ 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,
+ Committer: *ev.PullRequest.User.Login,
EventType: eventType,
+ RawPayload: rawPayload,
}
case *github.PushEvent:
if ev.GetRef() != "" && ev.GetHeadCommit().GetID() != "" {
@@ -287,20 +296,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 8544abe263..712f0f294f 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
@@ -92,6 +92,7 @@ func TriggerScanningByGitlabEvent(event interface{}, baseURI, requestID string,
IsPr: true,
MergeRequestID: strconv.Itoa(mergeRequestID),
CommitID: commitID,
+ CommitSHA: commitID,
CodehostID: eventRepo.CodehostID,
EventType: eventType,
}
@@ -110,6 +111,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 43e735e4ad..1ad3a22ae3 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
@@ -215,6 +215,7 @@ func TriggerTestByGitlabEvent(event interface{}, baseURI, requestID string, log
IsPr: true,
MergeRequestID: mergeRequestID,
CommitID: commitID,
+ CommitSHA: commitID,
CodehostID: eventRepo.CodehostID,
EventType: eventType,
}
@@ -233,6 +234,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 652199458d..8c70a6d9d7 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
@@ -102,7 +102,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.LastCommit.Message,
+ Committer: hookRepo.Committer,
Source: hookRepo.Source,
}
}
@@ -213,12 +217,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,
}
}
@@ -261,12 +273,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)
@@ -358,11 +372,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.LastCommit.Message,
+ Committer: ev.User.Username,
CodehostID: eventRepo.CodehostID,
EventType: eventType,
+ RawPayload: rawPayload,
}
case *gitlab.PushEvent:
eventType = EventTypePush
@@ -372,19 +391,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 05bba925a4..2a94d9090e 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
@@ -1131,6 +1131,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..3273382256 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,10 +67,63 @@ func (j NotificationJobController) Validate(isExecution bool) error {
if err := util.CheckZadigProfessionalLicense(); err != nil {
return e.ErrLicenseInvalid.AddDesc("")
}
+ if err := validateNotificationJobDynamicRecipients(j.jobSpec); err != nil {
+ return e.ErrLintWorkflow.AddDesc(err.Error())
+ }
return nil
}
+func validateNotificationJobDynamicRecipients(spec *commonmodels.NotificationJobSpec) error {
+ if spec == nil {
+ return nil
+ }
+
+ validate := func(appID string, recipients commonmodels.DynamicRecipients) error {
+ return dynamicrecipient.ValidateDynamicRecipientsForNotifyConfig(spec.WebHookType, appID, []string(recipients))
+ }
+
+ switch spec.WebHookType {
+ case setting.NotifyWebHookTypeFeishu:
+ if spec.LarkHookNotificationConfig == nil {
+ return nil
+ }
+ return validate(spec.LarkHookNotificationConfig.AppID, spec.LarkHookNotificationConfig.DynamicRecipients)
+ case setting.NotifyWebhookTypeFeishuApp:
+ if spec.LarkGroupNotificationConfig == nil {
+ return nil
+ }
+ return validate(spec.LarkGroupNotificationConfig.AppID, spec.LarkGroupNotificationConfig.DynamicRecipients)
+ case setting.NotifyWebHookTypeFeishuPerson:
+ if spec.LarkPersonNotificationConfig == nil {
+ return nil
+ }
+ return validate(spec.LarkPersonNotificationConfig.AppID, spec.LarkPersonNotificationConfig.DynamicRecipients)
+ case setting.NotifyWebHookTypeWechatWork:
+ if spec.WechatNotificationConfig == nil {
+ return nil
+ }
+ return validate("", spec.WechatNotificationConfig.DynamicRecipients)
+ case setting.NotifyWebHookTypeDingDing:
+ if spec.DingDingNotificationConfig == nil {
+ return nil
+ }
+ return validate("", spec.DingDingNotificationConfig.DynamicRecipients)
+ case setting.NotifyWebHookTypeMSTeam:
+ if spec.MSTeamsNotificationConfig == nil {
+ return nil
+ }
+ return validate("", spec.MSTeamsNotificationConfig.DynamicRecipients)
+ case setting.NotifyWebHookTypeMail:
+ if spec.MailNotificationConfig == nil {
+ return nil
+ }
+ return validate("", spec.MailNotificationConfig.DynamicRecipients)
+ default:
+ return nil
+ }
+}
+
func (j NotificationJobController) Update(useUserInput bool, ticket *commonmodels.ApprovalTicket) error {
currJob, err := j.workflow.FindJob(j.name, j.jobType)
if err != nil {
@@ -86,30 +141,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 +285,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 a8f7d1110a..0ab13488b2 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
@@ -248,6 +248,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 7d6854e97c..b22184747c 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,267 @@ 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,
+ )
+}
+
+// RenderJobTaskWithGlobalVariables replays a task spec with persisted GlobalContext during retry/manual execution.
+func RenderJobTaskWithGlobalVariables(task *commonmodels.JobTask, globalKeyMap map[string]string) error {
+ if task == nil {
+ return nil
+ }
+
+ var notificationRecipients *notificationDynamicRecipients
+ if task.JobType == string(config.JobNotification) {
+ // DynamicRecipients must stay as templates until NotificationJobCtl resolves them with payload/user mapping.
+ // Rendering them here would turn {{.payload.user.email}} into a raw value and lose the identity suffix.
+ 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 +470,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 +593,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) {
@@ -399,6 +653,24 @@ 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
}
@@ -517,6 +789,67 @@ func (w *Workflow) GetWorkflowParamDynamicValues(taskID int64, creator, account,
return nil, fmt.Errorf("workflow param %s not found", key)
}
+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")
@@ -802,61 +1135,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.workflowID(),
- 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 ce74991774..57033f0d74 100644
--- a/pkg/microservice/aslan/core/workflow/service/workflow/types.go
+++ b/pkg/microservice/aslan/core/workflow/service/workflow/types.go
@@ -171,36 +171,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 5dae8fa0df..39a95d8976 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"
@@ -598,7 +599,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 {
@@ -708,6 +712,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)
@@ -731,12 +736,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 {
@@ -750,11 +770,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
@@ -763,9 +789,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)
@@ -785,9 +816,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,
},
@@ -808,11 +844,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
@@ -821,11 +862,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
@@ -834,10 +880,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
@@ -846,9 +897,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 {
@@ -879,7 +935,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) {
@@ -959,7 +1051,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 {
@@ -1011,6 +1112,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 {
@@ -1030,15 +1132,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 {
@@ -1092,7 +1187,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 {
@@ -1165,6 +1269,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 {
@@ -1222,15 +1327,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 faff2421f8..5ede3a77ba 100644
--- a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go
+++ b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go
@@ -2619,6 +2619,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"`