diff --git a/pkg/cli/upgradeassistant/cmd/check.go b/pkg/cli/upgradeassistant/cmd/check.go
new file mode 100644
index 0000000000..ce2d6e10ad
--- /dev/null
+++ b/pkg/cli/upgradeassistant/cmd/check.go
@@ -0,0 +1,81 @@
+/*
+Copyright 2021 The KodeRover Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package cmd
+
+import (
+ "fmt"
+
+ "github.com/spf13/cobra"
+
+ "github.com/koderover/zadig/v2/pkg/shared/client/plutusenterprise"
+ e "github.com/koderover/zadig/v2/pkg/tool/errors"
+ "github.com/koderover/zadig/v2/pkg/tool/log"
+)
+
+func init() {
+ rootCmd.AddCommand(checkUpgradeCmd)
+
+ checkUpgradeCmd.PersistentFlags().StringP("from-version", "f", oldestVersion, "current version to migrate from")
+ checkUpgradeCmd.PersistentFlags().StringP("to-version", "t", "", "target version to migrate to")
+}
+
+var checkUpgradeCmd = &cobra.Command{
+ Use: "check",
+ Short: "check if the upgrade is possible",
+ Long: `check if the upgrade is possible.`,
+ PreRunE: func(cmd *cobra.Command, args []string) error {
+ return preRun()
+ },
+ Run: func(cmd *cobra.Command, args []string) {
+ from, _ := cmd.Flags().GetString("from-version")
+ to, _ := cmd.Flags().GetString("to-version")
+ if err := runCheckUpgrade(from, to); err != nil {
+ log.Fatal(err)
+ }
+ },
+ PostRun: func(cmd *cobra.Command, args []string) {
+ if err := postRun(); err != nil {
+ fmt.Println(err)
+ }
+ },
+}
+
+func runCheckUpgrade(from, to string) error {
+ if len(from) == 0 {
+ from = oldestVersion
+ }
+ if len(to) == 0 {
+ return fmt.Errorf("target version not assigned")
+ }
+
+ log.Infof("Checking upgrade from %s to %s", from, to)
+
+ resp, err := plutusenterprise.New().CheckUpgrade(from, to)
+ if err != nil {
+ nerr := e.ErrUpgradeNotAllowed.AddErr(err)
+ log.Error(nerr)
+ return nerr
+ }
+ if !resp.AllowUpgrade {
+ nerr := e.ErrUpgradeNotAllowed
+ log.Error(nerr)
+ return nerr
+ }
+
+ log.Info("Upgrade check passed")
+ return nil
+}
diff --git a/pkg/cli/upgradeassistant/cmd/migrate.go b/pkg/cli/upgradeassistant/cmd/migrate.go
index 07c38771f0..38ae87052a 100644
--- a/pkg/cli/upgradeassistant/cmd/migrate.go
+++ b/pkg/cli/upgradeassistant/cmd/migrate.go
@@ -40,8 +40,6 @@ func init() {
migrateCmd.PersistentFlags().StringP("from-version", "f", oldestVersion, "current version to migrate from")
migrateCmd.PersistentFlags().StringP("to-version", "t", "", "target version to migrate to")
- _ = viper.BindPFlag("fromVersion", migrateCmd.PersistentFlags().Lookup("from-version"))
- _ = viper.BindPFlag("toVersion", migrateCmd.PersistentFlags().Lookup("to-version"))
}
var migrateCmd = &cobra.Command{
@@ -52,7 +50,9 @@ var migrateCmd = &cobra.Command{
return preRun()
},
Run: func(cmd *cobra.Command, args []string) {
- if err := run(); err != nil {
+ from, _ := cmd.Flags().GetString("from-version")
+ to, _ := cmd.Flags().GetString("to-version")
+ if err := run(from, to); err != nil {
log.Fatal(err)
}
},
@@ -90,10 +90,7 @@ func nextVersionFromList(targetVersion semver.Version, versionList semver.Versio
return targetVersion.FinalizeVersion()
}
-func run() error {
- from := viper.GetString("fromVersion")
- to := viper.GetString("toVersion")
-
+func run(from, to string) error {
log.Infof("Migrating from %s to %s", from, to)
versionSets := sets.NewString()
@@ -159,6 +156,18 @@ func run() error {
upgradepath.AddHandler(upgradepath.VersionDatas.VersionIndex(rh.FromVersion), upgradepath.VersionDatas.VersionIndex(rh.ToVersion), rh.Fn)
}
+ // resp, err := plutusenterprise.New().CheckUpgrade(from, to)
+ // if err != nil {
+ // nerr := e.ErrUpgradeNotAllowed.AddErr(err)
+ // log.Error(nerr)
+ // return nerr
+ // }
+ // if !resp.AllowUpgrade {
+ // nerr := e.ErrUpgradeNotAllowed
+ // log.Error(nerr)
+ // return nerr
+ // }
+
err := upgradepath.UpgradeWithBestPath(from, to)
if err == nil {
log.Info("Migration finished")
diff --git a/pkg/cli/upgradeassistant/cmd/migrate/430.go b/pkg/cli/upgradeassistant/cmd/migrate/430.go
index 3bc95cb8d7..99cbb550be 100644
--- a/pkg/cli/upgradeassistant/cmd/migrate/430.go
+++ b/pkg/cli/upgradeassistant/cmd/migrate/430.go
@@ -22,17 +22,90 @@ import (
internalmodels "github.com/koderover/zadig/v2/pkg/cli/upgradeassistant/internal/repository/models"
internalmongodb "github.com/koderover/zadig/v2/pkg/cli/upgradeassistant/internal/repository/mongodb"
"github.com/koderover/zadig/v2/pkg/cli/upgradeassistant/internal/upgradepath"
+ collaborationmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/collaboration/repository/models"
+ collaborationmongodb "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/collaboration/repository/mongodb"
"github.com/koderover/zadig/v2/pkg/microservice/user/core/repository"
usermodels "github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/models"
+ "github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/orm"
+ userorm "github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/orm"
+ permissionservice "github.com/koderover/zadig/v2/pkg/microservice/user/core/service/permission"
+ "github.com/koderover/zadig/v2/pkg/setting"
internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler"
+ "github.com/koderover/zadig/v2/pkg/tool/log"
+ pkgtypes "github.com/koderover/zadig/v2/pkg/types"
+ "gorm.io/gorm"
+ "k8s.io/apimachinery/pkg/util/sets"
)
+type permissionActionSeed430 struct {
+ Name string
+ Action string
+ Resource string
+ Scope int
+}
+
+var businessDirectoryActionSeeds430 = []permissionActionSeed430{
+ {Name: "查看", Action: permissionservice.VerbGetBusinessDirectory, Resource: "BusinessDirectory", Scope: pkgtypes.DBSystemScope},
+ {Name: "新建", Action: permissionservice.VerbCreateBusinessDirectory, Resource: "BusinessDirectory", Scope: pkgtypes.DBSystemScope},
+ {Name: "编辑", Action: permissionservice.VerbEditBusinessDirectory, Resource: "BusinessDirectory", Scope: pkgtypes.DBSystemScope},
+ {Name: "删除", Action: permissionservice.VerbDeleteBusinessDirectory, Resource: "BusinessDirectory", Scope: pkgtypes.DBSystemScope},
+}
+
+type permissionBackfillRule430 struct {
+ Source string
+ Targets []string
+}
+
+type listActionBindings430 func(uint, *gorm.DB) ([]*usermodels.Action, error)
+type createActionBindings430 func(uint, []uint, *gorm.DB) error
+
+// 新版本新增的 action。历史实例升级时,先保证这些 action 已经存在,再去补 role/template 绑定。
+var permissionActionSeeds430 = []permissionActionSeed430{
+ {Name: "调整副本", Action: permissionservice.VerbScaleEnvironment, Resource: "Environment"},
+ {Name: "调整副本", Action: permissionservice.VerbScaleProductionEnv, Resource: "ProductionEnvironment"},
+}
+
+// 回填规则只认历史权限:
+// 旧环境管理权限 -> 新环境调整副本
+// 旧生产环境管理权限 -> 新生产环境调整副本
+var permissionBackfillRules430 = []permissionBackfillRule430{
+ {
+ Source: permissionservice.VerbManageEnvironment,
+ Targets: []string{
+ permissionservice.VerbScaleEnvironment,
+ },
+ },
+ {
+ Source: permissionservice.VerbEditProductionEnv,
+ Targets: []string{
+ permissionservice.VerbScaleProductionEnv,
+ },
+ },
+}
+
+// collaborationBackfillRules430 backfills collaboration scale permissions.
+var collaborationBackfillRules430 = []permissionBackfillRule430{
+ {
+ Source: pkgtypes.EnvActionManagePod,
+ Targets: []string{
+ pkgtypes.EnvActionScale,
+ },
+ },
+ {
+ Source: pkgtypes.ProductionEnvActionManagePod,
+ Targets: []string{
+ pkgtypes.ProductionEnvActionScale,
+ },
+ },
+}
+
func init() {
- upgradepath.RegisterHandler("4.2.0", "4.3.0", V420ToV430)
- upgradepath.RegisterHandler("4.3.0", "4.2.0", V430ToV420)
+ upgradepath.RegisterHandler("4.2.1", "4.3.0", V421ToV430)
+ upgradepath.RegisterHandler("4.3.0", "4.2.1", V430ToV421)
}
-func V420ToV430() error {
+// V421ToV430 executes all 4.3.0 upgrade steps for user/collaboration permissions.
+func V421ToV430() error {
ctx := internalhandler.NewBackgroupContext()
migrationInfo, err := getMigrationInfo()
@@ -44,14 +117,34 @@ func V420ToV430() error {
updateMigrationError(migrationInfo.ID, err)
}()
+ // 这次迁移分三段:
+ // 1. MySQL: user 表新增 api_token_enabled
+ // 2. MySQL: permission action + role/template 绑定
+ // 3. Mongo: collaboration mode / instance verbs
err = migrateUserAPITokenEnabledColumn(ctx, migrationInfo)
if err != nil {
return err
}
+ err = migrateGlobalReadOnlyRole(ctx, migrationInfo)
+ if err != nil {
+ return err
+ }
+
+ err = migrateScalePermissions(migrationInfo)
+ if err != nil {
+ return err
+ }
+
+ err = migrateCollaborationScalePermissions(migrationInfo)
+ if err != nil {
+ return err
+ }
+
return nil
}
+// migrateUserAPITokenEnabledColumn adds api_token_enabled column for user table.
func migrateUserAPITokenEnabledColumn(_ *internalhandler.Context, migrationInfo *internalmodels.Migration) error {
if !migrationInfo.Migration430UserAPITokenEnabled {
if !repository.DB.Migrator().HasColumn(&usermodels.User{}, "APITokenEnabled") {
@@ -68,6 +161,492 @@ func migrateUserAPITokenEnabledColumn(_ *internalhandler.Context, migrationInfo
return nil
}
-func V430ToV420() error {
+// check global read only role column
+func migrateGlobalReadOnlyRole(_ *internalhandler.Context, migrationInfo *internalmodels.Migration) error {
+ if !migrationInfo.Migration430GlobalReadOnlyRole {
+ if !repository.DB.Migrator().HasColumn(&usermodels.NewRole{}, "GlobalReadOnly") {
+ if err := repository.DB.Migrator().AddColumn(&usermodels.NewRole{}, "GlobalReadOnly"); err != nil {
+ return fmt.Errorf("failed to add global_read_only column for role table, err: %s", err)
+ }
+ }
+ }
+
+ // write globalreadonly role into system roles
+ err := backfillGlobalReadOnlyRole()
+ if err != nil {
+ return err
+ }
+ // Ensure business-directory actions exist for upgraded instances.
+ if err := ensureBusinessDirectoryActions430(); err != nil {
+ return err
+ }
+ // Fallback backfill:
+ // - if a role already has get_business_directory, append create/edit/delete
+ if err := backfillBusinessDirectoryRolePermissions430(); err != nil {
+ return err
+ }
+
+ _ = internalmongodb.NewMigrationColl().UpdateMigrationStatus(migrationInfo.ID, map[string]interface{}{
+ getMigrationFieldBsonTag(migrationInfo, &migrationInfo.Migration430GlobalReadOnlyRole): true,
+ })
+
+ return nil
+}
+
+func ensureBusinessDirectoryActions430() error {
+ tx := repository.DB.Begin()
+ if tx.Error != nil {
+ return fmt.Errorf("failed to begin tx for business directory action migration, err: %s", tx.Error)
+ }
+
+ for _, seed := range businessDirectoryActionSeeds430 {
+ action, err := orm.GetActionByVerb(seed.Action, tx)
+ if err != nil {
+ tx.Rollback()
+ return fmt.Errorf("failed to query action %s, err: %s", seed.Action, err)
+ }
+ if action != nil && action.ID != 0 {
+ continue
+ }
+
+ action = &usermodels.Action{
+ Name: seed.Name,
+ Action: seed.Action,
+ Resource: seed.Resource,
+ Scope: seed.Scope,
+ }
+ if err := orm.CreateAction(action, tx); err != nil {
+ tx.Rollback()
+ return fmt.Errorf("failed to create action %s, err: %s", seed.Action, err)
+ }
+ }
+
+ if err := tx.Commit().Error; err != nil {
+ return fmt.Errorf("failed to commit business directory action migration tx, err: %s", err)
+ }
+ return nil
+}
+
+// backfillBusinessDirectoryRolePermissions430 provides a migration fallback for
+// historical system roles:
+// 1) If a role already has get_business_directory, only append write verbs.
+func backfillBusinessDirectoryRolePermissions430() error {
+ tx := repository.DB.Begin()
+ if tx.Error != nil {
+ return fmt.Errorf("failed to begin tx for business directory permission backfill, err: %s", tx.Error)
+ }
+
+ actionIDMap := make(map[string]uint, len(businessDirectoryActionSeeds430))
+ for _, seed := range businessDirectoryActionSeeds430 {
+ action, err := orm.GetActionByVerb(seed.Action, tx)
+ if err != nil {
+ tx.Rollback()
+ return fmt.Errorf("failed to query action %s for backfill, err: %s", seed.Action, err)
+ }
+ if action == nil || action.ID == 0 {
+ tx.Rollback()
+ return fmt.Errorf("action %s is missing while backfilling business directory permissions", seed.Action)
+ }
+ actionIDMap[seed.Action] = action.ID
+ }
+
+ roles, err := orm.ListRoleByNamespace(permissionservice.GeneralNamespace, tx)
+ if err != nil {
+ tx.Rollback()
+ return fmt.Errorf("failed to list system roles for business directory backfill, err: %s", err)
+ }
+
+ for _, role := range roles {
+ if role == nil || role.ID == 0 {
+ continue
+ }
+ // Keep global-read-only role as readonly.
+ if role.GlobalReadOnly {
+ continue
+ }
+
+ actions, err := orm.ListActionByRole(role.ID, tx)
+ if err != nil {
+ tx.Rollback()
+ return fmt.Errorf("failed to list actions for role %d during business directory backfill, err: %s", role.ID, err)
+ }
+
+ existingVerbs := map[string]struct{}{}
+ for _, action := range actions {
+ if action == nil {
+ continue
+ }
+ existingVerbs[action.Action] = struct{}{}
+ }
+
+ // Only backfill write verbs for roles that already have get_business_directory.
+ if _, hasGet := existingVerbs[permissionservice.VerbGetBusinessDirectory]; !hasGet {
+ continue
+ }
+ targetVerbs := []string{
+ permissionservice.VerbCreateBusinessDirectory,
+ permissionservice.VerbEditBusinessDirectory,
+ permissionservice.VerbDeleteBusinessDirectory,
+ }
+
+ missingActionIDs := make([]uint, 0)
+ for _, verb := range targetVerbs {
+ if _, ok := existingVerbs[verb]; ok {
+ continue
+ }
+ if actionID, ok := actionIDMap[verb]; ok {
+ missingActionIDs = append(missingActionIDs, actionID)
+ }
+ }
+
+ if len(missingActionIDs) == 0 {
+ continue
+ }
+ if err := orm.BulkCreateRoleActionBindings(role.ID, missingActionIDs, tx); err != nil {
+ tx.Rollback()
+ return fmt.Errorf("failed to backfill business directory permissions for role %d, err: %s", role.ID, err)
+ }
+ }
+
+ if err := tx.Commit().Error; err != nil {
+ return fmt.Errorf("failed to commit business directory permission backfill tx, err: %s", err)
+ }
+ return nil
+}
+
+// backfill global read only role
+func backfillGlobalReadOnlyRole() error {
+ tx := repository.DB.Begin()
+ role := &usermodels.NewRole{
+ Name: "global-read-only",
+ Description: "拥有系统全局只读的权限",
+ Type: int64(setting.RoleTypeSystem),
+ Namespace: "*",
+ GlobalReadOnly: true,
+ }
+
+ // Check if role already exists
+ existingRole, err := orm.GetRole("global-read-only", "*", tx)
+ if err == nil && existingRole != nil && existingRole.ID != 0 {
+ tx.Commit()
+ return nil
+ }
+
+ if err := orm.CreateRole(role, tx); err != nil {
+ tx.Rollback()
+ return fmt.Errorf("failed to create global-read-only role in backfill, error: %s", err)
+ }
+
+ if err := tx.Commit().Error; err != nil {
+ return fmt.Errorf("failed to commit tx, err: %s", err)
+ }
+
+ return nil
+}
+
+func migrateScalePermissions(migrationInfo *internalmodels.Migration) error {
+ alreadyMigrated := migrationInfo.Migration430ScalePermission
+
+ tx := repository.DB.Begin()
+ if tx.Error != nil {
+ return fmt.Errorf("failed to begin migration 4.3.0 transaction, err: %s", tx.Error)
+ }
+
+ // 先补 action,再根据历史权限补 role 和 role template 绑定。
+ actionIDs, err := ensurePermissionActions430(tx)
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ templateCount, err := backfillRoleTemplatePermissions430(tx, actionIDs)
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ roleCount, err := backfillRolePermissions430(tx, actionIDs)
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ if err := tx.Commit().Error; err != nil {
+ return fmt.Errorf("failed to commit migration 4.3.0 permissions, err: %s", err)
+ }
+
+ log.Infof("migration 4.3.0 backfilled scale permissions for %d role templates and %d roles", templateCount, roleCount)
+
+ if alreadyMigrated {
+ return nil
+ }
+
+ return internalmongodb.NewMigrationColl().UpdateMigrationStatus(migrationInfo.ID, map[string]interface{}{
+ getMigrationFieldBsonTag(migrationInfo, &migrationInfo.Migration430ScalePermission): true,
+ })
+}
+
+// ensurePermissionActions430 ensures permission actions exist.
+func ensurePermissionActions430(tx *gorm.DB) (map[string]uint, error) {
+ actionIDs := make(map[string]uint, len(permissionActionSeeds430))
+ for _, seed := range permissionActionSeeds430 {
+ actionID, err := ensureAction430(tx, seed)
+ if err != nil {
+ return nil, err
+ }
+ actionIDs[seed.Action] = actionID
+ }
+
+ return actionIDs, nil
+}
+
+// ensureAction430 保证某个 action 在表里存在。
+// 如果是重复执行迁移,会直接复用已有数据。
+func ensureAction430(tx *gorm.DB, seed permissionActionSeed430) (uint, error) {
+ action, err := userorm.GetActionByVerb(seed.Action, tx)
+ if err != nil {
+ return 0, fmt.Errorf("failed to query action %s, err: %s", seed.Action, err)
+ }
+ if action != nil && action.ID != 0 {
+ return action.ID, nil
+ }
+
+ action = &usermodels.Action{
+ Name: seed.Name,
+ Action: seed.Action,
+ Resource: seed.Resource,
+ Scope: pkgtypes.DBProjectScope,
+ }
+ if err := userorm.CreateAction(action, tx); err != nil {
+ action, err = userorm.GetActionByVerb(seed.Action, tx)
+ if err != nil {
+ return 0, fmt.Errorf("failed to create action %s, err: %s", seed.Action, err)
+ }
+ }
+
+ if action == nil || action.ID == 0 {
+ return 0, fmt.Errorf("action %s still missing after migration", seed.Action)
+ }
+
+ return action.ID, nil
+}
+
+// backfillRoleTemplatePermissions430 backfills role template permissions
+func backfillRoleTemplatePermissions430(tx *gorm.DB, actionIDs map[string]uint) (int, error) {
+ roleTemplates, err := userorm.ListRoleTemplates(tx)
+ if err != nil {
+ return 0, fmt.Errorf("failed to list role templates, err: %s", err)
+ }
+ ids := make([]uint, 0, len(roleTemplates))
+ for _, roleTemplate := range roleTemplates {
+ ids = append(ids, roleTemplate.ID)
+ }
+
+ return backfillActionBindings430(tx, ids, actionIDs, userorm.ListActionByRoleTemplate, userorm.BulkCreateRoleTemplateActionBindings)
+}
+
+// backfillRolePermissions430 backfills role permissions
+func backfillRolePermissions430(tx *gorm.DB, actionIDs map[string]uint) (int, error) {
+ roles := make([]*usermodels.NewRole, 0)
+ if err := tx.Where("namespace <> ?", permissionservice.GeneralNamespace).Find(&roles).Error; err != nil {
+ return 0, fmt.Errorf("failed to list project roles, err: %s", err)
+ }
+
+ ids := make([]uint, 0, len(roles))
+ for _, role := range roles {
+ binding, err := userorm.GetRoleTemplateBindingByRoleID(role.ID, tx)
+ if err != nil {
+ return 0, fmt.Errorf("failed to query role template binding for role %d, err: %s", role.ID, err)
+ }
+ if binding != nil {
+ continue
+ }
+ ids = append(ids, role.ID)
+ }
+
+ return backfillActionBindings430(tx, ids, actionIDs, userorm.ListActionByRole, userorm.BulkCreateRoleActionBindings)
+}
+
+func backfillActionBindings430(tx *gorm.DB, ids []uint, actionIDs map[string]uint, listActions listActionBindings430, createBindings createActionBindings430) (int, error) {
+ updatedCount := 0
+ for _, id := range ids {
+ actions, err := listActions(id, tx)
+ if err != nil {
+ return updatedCount, fmt.Errorf("failed to list actions by id %d, err: %s", id, err)
+ }
+
+ missingActionIDs := collectMissingActionIDs430(actions, actionIDs)
+ if len(missingActionIDs) == 0 {
+ continue
+ }
+
+ if err := createBindings(id, missingActionIDs, tx); err != nil {
+ return updatedCount, fmt.Errorf("failed to backfill action bindings by id %d, err: %s", id, err)
+ }
+ updatedCount++
+ }
+
+ return updatedCount, nil
+}
+
+func collectMissingActionIDs430(actions []*usermodels.Action, actionIDs map[string]uint) []uint {
+ missingVerbs := collectMissingBackfillTargets430(actionVerbs430(actions))
+ missingActionIDs := make([]uint, 0, len(missingVerbs))
+ for _, verb := range missingVerbs {
+ actionID, ok := actionIDs[verb]
+ if ok {
+ missingActionIDs = append(missingActionIDs, actionID)
+ }
+ }
+ return missingActionIDs
+}
+
+func actionVerbs430(actions []*usermodels.Action) []string {
+ verbs := make([]string, 0, len(actions))
+ for _, action := range actions {
+ verbs = append(verbs, action.Action)
+ }
+ return verbs
+}
+
+func collectMissingBackfillTargets430(verbs []string) []string {
+ return collectMissingBackfillTargetsByRules430(verbs, permissionBackfillRules430)
+}
+
+func collectMissingBackfillTargetsByRules430(verbs []string, rules []permissionBackfillRule430) []string {
+ verbSet := sets.NewString(verbs...)
+ missingVerbs := make([]string, 0)
+ for _, rule := range rules {
+ if !verbSet.Has(rule.Source) {
+ continue
+ }
+ for _, target := range rule.Targets {
+ if verbSet.Has(target) {
+ continue
+ }
+ missingVerbs = append(missingVerbs, target)
+ verbSet.Insert(target)
+ }
+ }
+ return missingVerbs
+}
+
+func migrateCollaborationScalePermissions(migrationInfo *internalmodels.Migration) error {
+ alreadyMigrated := migrationInfo.Migration430CollaborationScalePermission
+
+ modeColl := collaborationmongodb.NewCollaborationModeColl()
+ instanceColl := collaborationmongodb.NewCollaborationInstanceColl()
+
+ modes, err := modeColl.List(&collaborationmongodb.CollaborationModeListOptions{})
+ if err != nil {
+ return fmt.Errorf("failed to list collaboration modes, err: %s", err)
+ }
+
+ modeRevisionMap := make(map[string]int64, len(modes))
+ modeUpdatedCount := 0
+ for _, mode := range modes {
+ changed := appendBackfillTargetsToMode430(mode)
+
+ revision := mode.Revision
+ if changed {
+ if err := modeColl.Update("system", mode); err != nil {
+ return fmt.Errorf("failed to update collaboration mode %s/%s, err: %s", mode.ProjectName, mode.Name, err)
+ }
+ revision++
+ modeUpdatedCount++
+ }
+ modeRevisionMap[collaborationModeKey430(mode.ProjectName, mode.Name)] = revision
+ }
+
+ instances, err := instanceColl.List(&collaborationmongodb.CollaborationInstanceFindOptions{})
+ if err != nil {
+ return fmt.Errorf("failed to list collaboration instances, err: %s", err)
+ }
+
+ instanceUpdatedCount := 0
+ for _, instance := range instances {
+ changed := appendBackfillTargetsToInstance430(instance)
+
+ if revision, ok := modeRevisionMap[collaborationModeKey430(instance.ProjectName, instance.CollaborationName)]; ok && instance.Revision != revision {
+ instance.Revision = revision
+ changed = true
+ }
+
+ if !changed {
+ continue
+ }
+
+ if err := instanceColl.Update(instance.UserUID, instance); err != nil {
+ return fmt.Errorf("failed to update collaboration instance %s/%s/%s, err: %s", instance.ProjectName, instance.CollaborationName, instance.UserUID, err)
+ }
+ instanceUpdatedCount++
+ }
+
+ log.Infof("migration 4.3.0 backfilled collaboration scale permissions for %d modes and %d instances", modeUpdatedCount, instanceUpdatedCount)
+
+ if alreadyMigrated {
+ return nil
+ }
+
+ return internalmongodb.NewMigrationColl().UpdateMigrationStatus(migrationInfo.ID, map[string]interface{}{
+ getMigrationFieldBsonTag(migrationInfo, &migrationInfo.Migration430CollaborationScalePermission): true,
+ })
+}
+
+func collaborationModeKey430(projectName, modeName string) string {
+ return fmt.Sprintf("%s/%s", projectName, modeName)
+}
+
+func appendBackfillTargets430(verbs []string) (bool, []string) {
+ missingVerbs := collectMissingBackfillTargetsByRules430(verbs, collaborationBackfillRules430)
+ if len(missingVerbs) == 0 {
+ return false, verbs
+ }
+
+ updatedVerbs := append([]string{}, verbs...)
+ updatedVerbs = append(updatedVerbs, missingVerbs...)
+ return true, updatedVerbs
+}
+
+func appendBackfillTargetsToMode430(mode *collaborationmodels.CollaborationMode) bool {
+ changed := false
+ for i := range mode.Workflows {
+ itemChanged, verbs := appendBackfillTargets430(mode.Workflows[i].Verbs)
+ if itemChanged {
+ mode.Workflows[i].Verbs = verbs
+ changed = true
+ }
+ }
+ for i := range mode.Products {
+ itemChanged, verbs := appendBackfillTargets430(mode.Products[i].Verbs)
+ if itemChanged {
+ mode.Products[i].Verbs = verbs
+ changed = true
+ }
+ }
+ return changed
+}
+
+// appendBackfillTargetsToInstance430 backfills collaboration scale permissions for instance
+func appendBackfillTargetsToInstance430(instance *collaborationmodels.CollaborationInstance) bool {
+ changed := false
+ for i := range instance.Workflows {
+ itemChanged, verbs := appendBackfillTargets430(instance.Workflows[i].Verbs)
+ if itemChanged {
+ instance.Workflows[i].Verbs = verbs
+ changed = true
+ }
+ }
+ for i := range instance.Products {
+ itemChanged, verbs := appendBackfillTargets430(instance.Products[i].Verbs)
+ if itemChanged {
+ instance.Products[i].Verbs = verbs
+ changed = true
+ }
+ }
+ return changed
+}
+
+func V430ToV421() error {
return nil
}
diff --git a/pkg/cli/upgradeassistant/internal/repository/models/migration.go b/pkg/cli/upgradeassistant/internal/repository/models/migration.go
index a4774c9496..7437c2de92 100644
--- a/pkg/cli/upgradeassistant/internal/repository/models/migration.go
+++ b/pkg/cli/upgradeassistant/internal/repository/models/migration.go
@@ -41,6 +41,9 @@ 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"`
+ Migration430GlobalReadOnlyRole bool `bson:"migration_430_global_read_only_role"`
+ Migration430ScalePermission bool `bson:"migration_430_scale_permission"`
+ Migration430CollaborationScalePermission bool `bson:"migration_430_collaboration_scale_permission"`
Error string `bson:"error"`
}
diff --git a/pkg/cli/zadig-agent/internal/network/connect.go b/pkg/cli/zadig-agent/internal/network/connect.go
index 661e2441dd..f01ebf02b9 100644
--- a/pkg/cli/zadig-agent/internal/network/connect.go
+++ b/pkg/cli/zadig-agent/internal/network/connect.go
@@ -142,6 +142,7 @@ type HeartbeatParameters struct {
DiskSpace uint64 `json:"disk_space"`
FreeDiskSpace uint64 `json:"free_disk_space"`
Hostname string `json:"hostname"`
+ AgentVersion string `json:"agent_version"`
}
type HeartbeatServerRequest struct {
diff --git a/pkg/cli/zadig-agent/pkg/monitor/heart_beat.go b/pkg/cli/zadig-agent/pkg/monitor/heart_beat.go
index a9192493b2..e59f1ab57b 100644
--- a/pkg/cli/zadig-agent/pkg/monitor/heart_beat.go
+++ b/pkg/cli/zadig-agent/pkg/monitor/heart_beat.go
@@ -116,6 +116,7 @@ func Heartbeat(agentCtl *agent.AgentController, errChan chan error, successChan
if err != nil {
panic(fmt.Errorf("failed to convert platform parameters to register agent parameters: %v", err))
}
+ params.AgentVersion = agentconfig.GetAgentVersion()
config := &network.AgentConfig{
Token: agentconfig.GetAgentToken(),
diff --git a/pkg/config/config.go b/pkg/config/config.go
index f9ba76f058..b410f7f803 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -161,6 +161,15 @@ func VendorServiceAddress() string {
return GetServiceAddress(s.Name, s.Port)
}
+func EnterpriseServiceInfo() *setting.ServiceInfo {
+ return GetServiceByCode(setting.Enterprise)
+}
+
+func EnterpriseServiceAddress() string {
+ s := EnterpriseServiceInfo()
+ return GetServiceAddress(s.Name, s.Port)
+}
+
func GetServiceAddress(name string, port int32) string {
return fmt.Sprintf("http://%s:%d", name, port)
}
@@ -232,10 +241,6 @@ func MongoDatabase() string {
return viper.GetString(setting.ENVAslanDBName)
}
-func PolicyDatabase() string {
- return MongoDatabase() + "_policy"
-}
-
func MysqlUser() string {
return viper.GetString(setting.ENVMysqlUser)
}
diff --git a/pkg/microservice/aslan/config/config.go b/pkg/microservice/aslan/config/config.go
index be13fc484d..5671484419 100644
--- a/pkg/microservice/aslan/config/config.go
+++ b/pkg/microservice/aslan/config/config.go
@@ -22,10 +22,9 @@ import (
"strconv"
"strings"
- "github.com/spf13/viper"
-
configbase "github.com/koderover/zadig/v2/pkg/config"
"github.com/koderover/zadig/v2/pkg/setting"
+ "github.com/spf13/viper"
)
func DefaultIngressClass() string {
@@ -246,3 +245,19 @@ func DindImage() string {
func Features() string {
return viper.GetString(setting.FeatureFlag)
}
+
+func GetZadigAgentVersion() (string, error) {
+ version := viper.GetString(setting.ZadigAgentVersion)
+ if version != "" {
+ return strings.TrimPrefix(version, "v"), nil
+ }
+ return "", fmt.Errorf("zadig-agent version not found")
+}
+
+func GetRepoURL() (string, error) {
+ url := viper.GetString(setting.ZadigAgentRepoURL)
+ if url != "" {
+ return url, nil
+ }
+ return "", fmt.Errorf("zadig-agent repo URL not found")
+}
diff --git a/pkg/microservice/aslan/config/consts.go b/pkg/microservice/aslan/config/consts.go
index 1a39ad3745..e003742410 100644
--- a/pkg/microservice/aslan/config/consts.go
+++ b/pkg/microservice/aslan/config/consts.go
@@ -276,6 +276,13 @@ const (
DBInstanceTypeMariaDB DBInstanceType = "mariadb"
)
+type DMSJobExecuteMode string
+
+const (
+ DMSJobExecuteModeParallel DMSJobExecuteMode = "parallel"
+ DMSJobExecuteModeSerial DMSJobExecuteMode = "serial"
+)
+
type ObservabilityType string
const (
diff --git a/pkg/microservice/aslan/core/application/handler/application.go b/pkg/microservice/aslan/core/application/handler/application.go
index a9150812c0..f1091b1c04 100644
--- a/pkg/microservice/aslan/core/application/handler/application.go
+++ b/pkg/microservice/aslan/core/application/handler/application.go
@@ -39,6 +39,10 @@ func CreateApplication(c *gin.Context) {
ctx.UnAuthorized = true
return
}
+ if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.Create {
+ ctx.UnAuthorized = true
+ return
+ }
args := new(commonmodels.Application)
data, _ := c.GetRawData()
c.Request.Body = io.NopCloser(bytes.NewBuffer(data))
@@ -58,6 +62,11 @@ func BulkCreateApplications(c *gin.Context) {
return
}
+ if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.Create {
+ ctx.UnAuthorized = true
+ return
+ }
+
var args []*commonmodels.Application
data, _ := c.GetRawData()
c.Request.Body = io.NopCloser(bytes.NewBuffer(data))
@@ -74,8 +83,17 @@ func BulkCreateApplications(c *gin.Context) {
}
func GetApplication(c *gin.Context) {
- ctx := internalhandler.NewContext(c)
+ ctx, err := internalhandler.NewContextWithAuthorization(c)
defer func() { internalhandler.JSONResponse(c, ctx) }()
+ if err != nil {
+ ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err)
+ ctx.UnAuthorized = true
+ return
+ }
+ if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.View {
+ ctx.UnAuthorized = true
+ return
+ }
ctx.Resp, ctx.RespErr = service.GetApplication(c.Param("id"), ctx.Logger)
}
@@ -87,6 +105,10 @@ func UpdateApplication(c *gin.Context) {
ctx.UnAuthorized = true
return
}
+ if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.Edit {
+ ctx.UnAuthorized = true
+ return
+ }
args := new(commonmodels.Application)
data, _ := c.GetRawData()
if err := json.Unmarshal(data, args); err != nil {
@@ -104,12 +126,25 @@ func DeleteApplication(c *gin.Context) {
ctx.UnAuthorized = true
return
}
+ if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.Delete {
+ ctx.UnAuthorized = true
+ return
+ }
ctx.RespErr = service.DeleteApplication(c.Param("id"), ctx.Logger)
}
func SearchApplications(c *gin.Context) {
- ctx := internalhandler.NewContext(c)
+ ctx, err := internalhandler.NewContextWithAuthorization(c)
defer func() { internalhandler.JSONResponse(c, ctx) }()
+ if err != nil {
+ ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err)
+ ctx.UnAuthorized = true
+ return
+ }
+ if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.View {
+ ctx.UnAuthorized = true
+ return
+ }
var req service.SearchApplicationsRequest
if err := c.ShouldBindJSON(&req); err != nil {
ctx.RespErr = e.ErrInvalidParam.AddErr(err)
@@ -124,8 +159,17 @@ func SearchApplications(c *gin.Context) {
}
func ListApplicationEnvs(c *gin.Context) {
- ctx := internalhandler.NewContext(c)
+ ctx, err := internalhandler.NewContextWithAuthorization(c)
defer func() { internalhandler.JSONResponse(c, ctx) }()
+ if err != nil {
+ ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err)
+ ctx.UnAuthorized = true
+ return
+ }
+ if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.View {
+ ctx.UnAuthorized = true
+ return
+ }
resp, err := service.ListApplicationEnvs(c.Param("id"), ctx.Logger)
if err != nil {
ctx.RespErr = err
diff --git a/pkg/microservice/aslan/core/application/handler/field_definition.go b/pkg/microservice/aslan/core/application/handler/field_definition.go
index ae3e8efa94..eb410bdf50 100644
--- a/pkg/microservice/aslan/core/application/handler/field_definition.go
+++ b/pkg/microservice/aslan/core/application/handler/field_definition.go
@@ -39,6 +39,10 @@ func CreateFieldDefinition(c *gin.Context) {
ctx.UnAuthorized = true
return
}
+ if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.Edit {
+ ctx.UnAuthorized = true
+ return
+ }
args := new(commonmodels.ApplicationFieldDefinition)
data, _ := c.GetRawData()
c.Request.Body = io.NopCloser(bytes.NewBuffer(data))
@@ -50,8 +54,17 @@ func CreateFieldDefinition(c *gin.Context) {
}
func ListFieldDefinitions(c *gin.Context) {
- ctx := internalhandler.NewContext(c)
+ ctx, err := internalhandler.NewContextWithAuthorization(c)
defer func() { internalhandler.JSONResponse(c, ctx) }()
+ if err != nil {
+ ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err)
+ ctx.UnAuthorized = true
+ return
+ }
+ if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.View {
+ ctx.UnAuthorized = true
+ return
+ }
ctx.Resp, ctx.RespErr = service.ListFieldDefinitions(ctx.Logger)
}
@@ -63,6 +76,10 @@ func UpdateFieldDefinition(c *gin.Context) {
ctx.UnAuthorized = true
return
}
+ if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.Edit {
+ ctx.UnAuthorized = true
+ return
+ }
args := new(commonmodels.ApplicationFieldDefinition)
data, _ := c.GetRawData()
if err := json.Unmarshal(data, args); err != nil {
@@ -80,5 +97,9 @@ func DeleteFieldDefinition(c *gin.Context) {
ctx.UnAuthorized = true
return
}
+ if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.Delete {
+ ctx.UnAuthorized = true
+ return
+ }
ctx.RespErr = service.DeleteFieldDefinition(c.Param("id"), ctx.Logger)
}
diff --git a/pkg/microservice/aslan/core/code/handler/codehost.go b/pkg/microservice/aslan/core/code/handler/codehost.go
index 362fe74d2c..c536f9c031 100644
--- a/pkg/microservice/aslan/core/code/handler/codehost.go
+++ b/pkg/microservice/aslan/core/code/handler/codehost.go
@@ -24,6 +24,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/code/service"
+ "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util"
"github.com/koderover/zadig/v2/pkg/setting"
"github.com/koderover/zadig/v2/pkg/shared/client/systemconfig"
internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler"
@@ -60,7 +61,12 @@ func CodeHostGetNamespaceList(c *gin.Context) {
return
}
chID, _ := strconv.Atoi(codehostID)
- ctx.Resp, ctx.RespErr = service.CodeHostListNamespaces(chID, keyword, ctx.Logger)
+ namespaces, err := service.CodeHostListNamespaces(chID, keyword, ctx.Logger)
+ if err != nil {
+ ctx.RespErr = e.NewWithDesc(e.ErrCodehostListNamespaces, util.FormatCodeHostErrorWithDefault("Failed to connect to the code host or the configuration is invalid. Please check your code host settings", err))
+ return
+ }
+ ctx.Resp = namespaces
}
type CodeHostListProjectsArgs struct {
@@ -110,7 +116,7 @@ func CodeHostGetProjectsList(c *gin.Context) {
args.Key,
ctx.Logger)
if err != nil {
- ctx.RespErr = err
+ ctx.RespErr = e.NewWithDesc(e.ErrCodehostListProjects, util.FormatCodeHostErrorWithDefault("Failed to fetch repositories. Please verify the code host connection and namespace permissions", err))
return
}
@@ -168,7 +174,7 @@ func CodeHostGetBranchList(c *gin.Context) {
}
chID, _ := strconv.Atoi(codehostID)
- ctx.Resp, ctx.RespErr = service.CodeHostListBranches(
+ branches, err := service.CodeHostListBranches(
chID,
repoName,
strings.Replace(repoOwner, "%2F", "/", -1),
@@ -176,6 +182,11 @@ func CodeHostGetBranchList(c *gin.Context) {
args.Page,
args.PerPage,
ctx.Logger)
+ if err != nil {
+ ctx.RespErr = e.NewWithDesc(e.ErrCodehostListBranches, util.FormatCodeHostErrorWithDefault("Failed to fetch branches. Please check if the repository exists and you have access permissions", err))
+ return
+ }
+ ctx.Resp = branches
}
// @Summary 获取代码仓库标签列表
@@ -217,7 +228,12 @@ func CodeHostGetTagList(c *gin.Context) {
}
chID, _ := strconv.Atoi(codehostID)
- ctx.Resp, ctx.RespErr = service.CodeHostListTags(chID, repoName, strings.Replace(repoOwner, "%2F", "/", -1), args.Key, args.Page, args.PerPage, ctx.Logger)
+ tags, err := service.CodeHostListTags(chID, repoName, strings.Replace(repoOwner, "%2F", "/", -1), args.Key, args.Page, args.PerPage, ctx.Logger)
+ if err != nil {
+ ctx.RespErr = e.NewWithDesc(e.ErrCodehostListTags, util.FormatCodeHostErrorWithDefault("Failed to fetch tags. Please check if the repository exists and you have access permissions", err))
+ return
+ }
+ ctx.Resp = tags
}
func CodeHostGetPRList(c *gin.Context) {
@@ -250,7 +266,12 @@ func CodeHostGetPRList(c *gin.Context) {
targetBr := c.Query("targetBranch")
chID, _ := strconv.Atoi(codehostID)
- ctx.Resp, ctx.RespErr = service.CodeHostListPRs(chID, repoName, strings.Replace(repoOwner, "%2F", "/", -1), targetBr, args.Key, args.Page, args.PerPage, ctx.Logger)
+ prs, err := service.CodeHostListPRs(chID, repoName, strings.Replace(repoOwner, "%2F", "/", -1), targetBr, args.Key, args.Page, args.PerPage, ctx.Logger)
+ if err != nil {
+ ctx.RespErr = e.NewWithDesc(e.ErrCodehostListPrs, util.FormatCodeHostErrorWithDefault("Failed to fetch pull requests. Please verify repository access and permissions", err))
+ return
+ }
+ ctx.Resp = prs
}
func CodeHostGetCommits(c *gin.Context) {
@@ -283,7 +304,12 @@ func CodeHostGetCommits(c *gin.Context) {
targetBr := c.Query("branchName")
chID, _ := strconv.Atoi(codehostID)
- ctx.Resp, ctx.RespErr = service.CodeHostListCommits(chID, repoName, strings.Replace(repoNamespace, "%2F", "/", -1), targetBr, args.Page, args.PerPage, ctx.Logger)
+ commits, err := service.CodeHostListCommits(chID, repoName, strings.Replace(repoNamespace, "%2F", "/", -1), targetBr, args.Page, args.PerPage, ctx.Logger)
+ if err != nil {
+ ctx.RespErr = e.NewWithDesc(e.ErrCodehostListCommits, util.FormatCodeHostErrorWithDefault("Failed to fetch commits. Please check if the branch exists and you have access permissions", err))
+ return
+ }
+ ctx.Resp = commits
}
func ListRepoInfos(c *gin.Context) {
@@ -306,7 +332,12 @@ func ListRepoInfos(c *gin.Context) {
return
}
- ctx.Resp, ctx.RespErr = service.ListRepoInfos(args.Infos, page, perPage, ctx.Logger)
+ repoInfos, err := service.ListRepoInfos(args.Infos, page, perPage, ctx.Logger)
+ if err != nil {
+ ctx.RespErr = e.NewWithDesc(e.ErrCodehostListProjects, util.FormatCodeHostErrorWithDefault("Failed to fetch repository information. Please verify the repository configuration and permissions", err))
+ return
+ }
+ ctx.Resp = repoInfos
}
type MatchBranchesListRequest struct {
@@ -343,7 +374,7 @@ func MatchRegularList(c *gin.Context) {
}
chID, _ := strconv.Atoi(codehostID)
- ctx.Resp, ctx.RespErr = service.MatchRegularList(
+ branches, err := service.MatchRegularList(
chID,
req.RepoName,
strings.Replace(req.RepoOwner, "%2F", "/", -1),
@@ -352,4 +383,9 @@ func MatchRegularList(c *gin.Context) {
perPage,
req.Regular,
ctx.Logger)
+ if err != nil {
+ ctx.RespErr = e.NewWithDesc(e.ErrCodehostListBranches, util.FormatCodeHostErrorWithDefault("Failed to match branches. Please check if the repository exists and you have access permissions", err))
+ return
+ }
+ ctx.Resp = branches
}
diff --git a/pkg/microservice/aslan/core/code/service/repo.go b/pkg/microservice/aslan/core/code/service/repo.go
index 0f80e472b7..9f5f0b2707 100644
--- a/pkg/microservice/aslan/core/code/service/repo.go
+++ b/pkg/microservice/aslan/core/code/service/repo.go
@@ -21,12 +21,12 @@ import (
"strings"
"sync"
- "github.com/hashicorp/go-multierror"
"github.com/koderover/zadig/v2/pkg/types"
"go.uber.org/zap"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/code/client"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/code/client/open"
+ "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util"
"github.com/koderover/zadig/v2/pkg/shared/client/systemconfig"
)
@@ -63,20 +63,37 @@ func (repo *GitRepoInfo) GetNamespace() string {
// ListRepoInfos ...
func ListRepoInfos(infos []*GitRepoInfo, page, perPage int, log *zap.SugaredLogger) ([]*GitRepoInfo, error) {
var wg sync.WaitGroup
- var errList *multierror.Error
for _, info := range infos {
ch, err := systemconfig.New().GetCodeHost(info.CodehostID)
if err != nil {
log.Errorf("get code host info err:%s", err)
- return nil, err
+ info.ErrorMsg = util.FormatRepoInfoError(err)
+ info.Branches = []*client.Branch{}
+ info.Tags = []*client.Tag{}
+ info.PRs = []*client.PullRequest{}
+ continue
}
if ch.Type == types.ProviderPerforce {
continue
}
codehostClient, err := open.OpenClient(ch, log)
if err != nil {
- return nil, err
+ log.Errorf("open client err:%s", err)
+ info.ErrorMsg = util.FormatRepoInfoError(err)
+ info.Branches = []*client.Branch{}
+ info.Tags = []*client.Tag{}
+ info.PRs = []*client.PullRequest{}
+ continue
+ }
+ var setErrorOnce sync.Once
+ setRepoInfoError := func(err error) {
+ if err == nil {
+ return
+ }
+ setErrorOnce.Do(func() {
+ info.ErrorMsg = util.FormatRepoInfoError(err)
+ })
}
wg.Add(1)
go func(info *GitRepoInfo) {
@@ -88,18 +105,19 @@ func ListRepoInfos(infos []*GitRepoInfo, page, perPage int, log *zap.SugaredLogg
return
}
- info.PRs, err = codehostClient.ListPrs(client.ListOpt{
+ prs, err := codehostClient.ListPrs(client.ListOpt{
Namespace: strings.Replace(info.GetNamespace(), "%2F", "/", -1),
ProjectName: info.Repo,
Page: page,
PerPage: perPage,
})
if err != nil {
- errList = multierror.Append(errList, err)
- info.ErrorMsg = err.Error()
+ log.Errorf("list pr err:%s", err)
+ setRepoInfoError(err)
info.PRs = []*client.PullRequest{}
return
}
+ info.PRs = prs
}(info)
wg.Add(1)
@@ -108,7 +126,7 @@ func ListRepoInfos(infos []*GitRepoInfo, page, perPage int, log *zap.SugaredLogg
wg.Done()
}()
projectName := info.Repo
- info.Branches, err = codehostClient.ListBranches(client.ListOpt{
+ branches, err := codehostClient.ListBranches(client.ListOpt{
Namespace: strings.Replace(info.GetNamespace(), "%2F", "/", -1),
ProjectName: projectName,
Key: info.Key,
@@ -117,11 +135,12 @@ func ListRepoInfos(infos []*GitRepoInfo, page, perPage int, log *zap.SugaredLogg
MatchBranches: true,
})
if err != nil {
- errList = multierror.Append(errList, err)
- info.ErrorMsg = err.Error()
+ log.Errorf("list branch err:%s", err)
+ setRepoInfoError(err)
info.Branches = []*client.Branch{}
return
}
+ info.Branches = branches
if info.DefaultBranch != "" {
foundDefaultBranch := false
@@ -139,8 +158,8 @@ func ListRepoInfos(infos []*GitRepoInfo, page, perPage int, log *zap.SugaredLogg
Key: info.DefaultBranch,
})
if err != nil {
- errList = multierror.Append(errList, err)
- info.ErrorMsg = err.Error()
+ log.Errorf("list default branch err:%s", err)
+ setRepoInfoError(err)
info.Branches = []*client.Branch{}
return
}
@@ -162,7 +181,7 @@ func ListRepoInfos(infos []*GitRepoInfo, page, perPage int, log *zap.SugaredLogg
}()
projectName := info.Repo
- info.Tags, err = codehostClient.ListTags(client.ListOpt{
+ tags, err := codehostClient.ListTags(client.ListOpt{
Namespace: strings.Replace(info.GetNamespace(), "%2F", "/", -1),
ProjectName: projectName,
Key: info.Key,
@@ -170,11 +189,12 @@ func ListRepoInfos(infos []*GitRepoInfo, page, perPage int, log *zap.SugaredLogg
PerPage: perPage,
})
if err != nil {
- errList = multierror.Append(errList, err)
- info.ErrorMsg = err.Error()
+ log.Errorf("list tag err:%s", err)
+ setRepoInfoError(err)
info.Tags = []*client.Tag{}
return
}
+ info.Tags = tags
}(info)
}
@@ -216,10 +236,6 @@ func ListRepoInfos(infos []*GitRepoInfo, page, perPage int, log *zap.SugaredLogg
}
}
}
- if err := errList.ErrorOrNil(); err != nil {
- log.Errorf("list repo info error: %v", err)
- return nil, err
- }
return infos, nil
}
diff --git a/pkg/microservice/aslan/core/collaboration/handler/collaboration_mode.go b/pkg/microservice/aslan/core/collaboration/handler/collaboration_mode.go
index d469143153..b3359319cc 100644
--- a/pkg/microservice/aslan/core/collaboration/handler/collaboration_mode.go
+++ b/pkg/microservice/aslan/core/collaboration/handler/collaboration_mode.go
@@ -183,12 +183,14 @@ func generateCollaborationDetailLog(username string, args *commonmodels.Collabor
"manage_environment": "管理服务实例",
"restart_environment": "重启",
"rollback_environment": "回滚",
+ "scale_environment": "调整副本",
"debug_pod": "服务调试",
"get_production_environment": "查看",
"config_production_environment": "配置",
"edit_production_environment": "管理服务实例",
"restart_production_environment": "重启",
"rollback_production_environment": "回滚",
+ "scale_production_environment": "调整副本",
"production_debug_pod": "服务调试",
}
diff --git a/pkg/microservice/aslan/core/collaboration/service/collaboration_mode.go b/pkg/microservice/aslan/core/collaboration/service/collaboration_mode.go
index 740cfdc60e..454991ac5a 100644
--- a/pkg/microservice/aslan/core/collaboration/service/collaboration_mode.go
+++ b/pkg/microservice/aslan/core/collaboration/service/collaboration_mode.go
@@ -27,22 +27,11 @@ import (
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/collaboration/repository/mongodb"
)
-func validateMemberInfo(collaborationMode *models.CollaborationMode) bool {
- if len(collaborationMode.Members) != len(collaborationMode.MemberInfo) {
- return false
- }
- memberSet := sets.NewString(collaborationMode.Members...)
- memberInfoSet := sets.NewString()
- for _, memberInfo := range collaborationMode.MemberInfo {
- memberInfoSet.Insert(memberInfo.GetID())
- }
- return memberSet.Equal(memberInfoSet)
-}
-
func CreateCollaborationMode(userName string, collaborationMode *models.CollaborationMode, logger *zap.SugaredLogger) error {
if !validateMemberInfo(collaborationMode) {
return fmt.Errorf("members and member_info not match")
}
+
err := mongodb.NewCollaborationModeColl().Create(userName, collaborationMode)
if err != nil {
logger.Errorf("CreateCollaborationMode error, err msg:%s", err)
@@ -55,6 +44,7 @@ func UpdateCollaborationMode(userName string, collaborationMode *models.Collabor
if !validateMemberInfo(collaborationMode) {
return fmt.Errorf("members and member_info not match")
}
+
err := mongodb.NewCollaborationModeColl().Update(userName, collaborationMode)
if err != nil {
logger.Errorf("UpdateCollaborationMode error, err msg:%s", err)
@@ -88,3 +78,16 @@ func GetCollaborationMode(username, projectName, name string, logger *zap.Sugare
}
return resp, true, nil
}
+
+func validateMemberInfo(collaborationMode *models.CollaborationMode) bool {
+ if len(collaborationMode.Members) != len(collaborationMode.MemberInfo) {
+ return false
+ }
+ memberSet := sets.NewString(collaborationMode.Members...)
+ memberInfoSet := sets.NewString()
+ for _, memberInfo := range collaborationMode.MemberInfo {
+ memberInfoSet.Insert(memberInfo.GetID())
+ }
+ return memberSet.Equal(memberInfoSet)
+}
+
diff --git a/pkg/microservice/aslan/core/common/repository/models/private_key.go b/pkg/microservice/aslan/core/common/repository/models/private_key.go
index 11a7f130eb..e62b358d8a 100644
--- a/pkg/microservice/aslan/core/common/repository/models/private_key.go
+++ b/pkg/microservice/aslan/core/common/repository/models/private_key.go
@@ -21,7 +21,7 @@ import (
"github.com/koderover/zadig/v2/pkg/microservice/aslan/config"
"github.com/koderover/zadig/v2/pkg/setting"
- "github.com/koderover/zadig/v2/pkg/shared/client/plutusvendor"
+ "github.com/koderover/zadig/v2/pkg/shared/client/plutusenterprise"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
"github.com/koderover/zadig/v2/pkg/types"
"go.mongodb.org/mongo-driver/bson/primitive"
@@ -83,13 +83,13 @@ func (PrivateKey) TableName() string {
}
func (args *PrivateKey) Validate() error {
- licenseStatus, err := plutusvendor.New().CheckZadigXLicenseStatus()
+ licenseStatus, err := plutusenterprise.New().CheckZadigXLicenseStatus()
if err != nil {
return fmt.Errorf("failed to validate zadig license status, error: %s", err)
}
- if !((licenseStatus.Type == plutusvendor.ZadigSystemTypeProfessional ||
- licenseStatus.Type == plutusvendor.ZadigSystemTypeEnterprise) &&
- licenseStatus.Status == plutusvendor.ZadigXLicenseStatusNormal) {
+ if !((licenseStatus.Type == plutusenterprise.ZadigSystemTypeProfessional ||
+ licenseStatus.Type == plutusenterprise.ZadigSystemTypeEnterprise) &&
+ licenseStatus.Status == plutusenterprise.ZadigXLicenseStatusNormal) {
if args.Provider == config.VMProviderAmazon {
return e.ErrLicenseInvalid.AddDesc("")
}
diff --git a/pkg/microservice/aslan/core/common/repository/models/registry_namespace.go b/pkg/microservice/aslan/core/common/repository/models/registry_namespace.go
index bd7e5ff033..19e3e6ab39 100644
--- a/pkg/microservice/aslan/core/common/repository/models/registry_namespace.go
+++ b/pkg/microservice/aslan/core/common/repository/models/registry_namespace.go
@@ -25,7 +25,7 @@ import (
"go.mongodb.org/mongo-driver/bson/primitive"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/config"
- "github.com/koderover/zadig/v2/pkg/shared/client/plutusvendor"
+ "github.com/koderover/zadig/v2/pkg/shared/client/plutusenterprise"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
)
@@ -87,7 +87,7 @@ func (ns *RegistryNamespace) GetRegistryAddress() (string, error) {
}
func (args *RegistryNamespace) LicenseValidate() error {
- licenseStatus, err := plutusvendor.New().CheckZadigXLicenseStatus()
+ licenseStatus, err := plutusenterprise.New().CheckZadigXLicenseStatus()
if err != nil {
return fmt.Errorf("failed to validate zadig license status, error: %s", err)
}
@@ -95,9 +95,9 @@ func (args *RegistryNamespace) LicenseValidate() error {
args.RegProvider == config.RegistryProviderTCREnterprise ||
args.RegProvider == config.RegistryProviderECR ||
args.RegProvider == config.RegistryProviderJFrog {
- if !((licenseStatus.Type == plutusvendor.ZadigSystemTypeProfessional ||
- licenseStatus.Type == plutusvendor.ZadigSystemTypeEnterprise) &&
- licenseStatus.Status == plutusvendor.ZadigXLicenseStatusNormal) {
+ if !((licenseStatus.Type == plutusenterprise.ZadigSystemTypeProfessional ||
+ licenseStatus.Type == plutusenterprise.ZadigSystemTypeEnterprise) &&
+ licenseStatus.Status == plutusenterprise.ZadigXLicenseStatusNormal) {
return e.ErrLicenseInvalid.AddDesc("")
}
}
diff --git a/pkg/microservice/aslan/core/common/repository/models/release_plan.go b/pkg/microservice/aslan/core/common/repository/models/release_plan.go
index b0d167c3d0..7cdc25ed7f 100644
--- a/pkg/microservice/aslan/core/common/repository/models/release_plan.go
+++ b/pkg/microservice/aslan/core/common/repository/models/release_plan.go
@@ -60,6 +60,7 @@ type ReleasePlan struct {
WaitForExecuteExternalCheckTime int64 `bson:"wait_for_execute_external_check_time" yaml:"wait_for_execute_external_check_time" json:"wait_for_execute_external_check_time"`
WaitForAllDoneExternalCheckTime int64 `bson:"wait_for_all_done_external_check_time" yaml:"wait_for_all_done_external_check_time" json:"wait_for_all_done_external_check_time"`
ExternalCheckFailedReason string `bson:"external_check_failed_reason" yaml:"external_check_failed_reason" json:"external_check_failed_reason"`
+ CallbackDescription string `bson:"callback_description" yaml:"callback_description" json:"callback_description"`
}
type HookSettings struct {
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 af2aafb7b3..9da2ad93e3 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
@@ -234,7 +234,7 @@ type JobTaskDeploySpec struct {
ReplaceResources []Resource `bson:"replace_resources" json:"replace_resources" yaml:"replace_resources"`
RelatedPodLabels []map[string]string `bson:"-" json:"-" yaml:"-"`
// overrideResource is used to do a full yaml override instead of a 2-way merge patching for all the resources
- OverrideResource bool `bson:"override_resource" json:"override_resource" yaml:"override_resource"`
+ OverrideResource bool `bson:"override_resource" json:"override_resource" yaml:"override_resource"`
// for compatibility
ServiceModule string `bson:"service_module" json:"service_module" yaml:"-"`
Image string `bson:"image" json:"image" yaml:"-"`
@@ -266,7 +266,7 @@ type JobTaskDeployRevertSpec struct {
OverrideKVs string `bson:"override_kvs" json:"override_kvs" yaml:"override_kvs"`
Revision int64 `bson:"revision" json:"revision" yaml:"revision"`
RevisionCreateTime int64 `bson:"revision_create_time" json:"revision_create_time" yaml:"revision_create_time"`
- OverrideResource bool `bson:"override_resource" json:"override_resource" yaml:"override_resource"`
+ OverrideResource bool `bson:"override_resource" json:"override_resource" yaml:"override_resource"`
}
type DeployServiceModule struct {
@@ -614,8 +614,9 @@ type SQLExecResult struct {
}
type JobTaskDMSSpec struct {
- ID string `bson:"id" json:"id" yaml:"id"`
- Orders []*DMSTaskOrder `bson:"orders" json:"orders" yaml:"orders"`
+ ID string `bson:"id" json:"id" yaml:"id"`
+ ExecuteMode string `bson:"execute_mode" json:"execute_mode" yaml:"execute_mode"`
+ Orders []*DMSTaskOrder `bson:"orders" json:"orders" yaml:"orders"`
}
type DMSTaskOrder struct {
@@ -773,6 +774,7 @@ type LarkChat struct {
type JobTaskNotificationSpec struct {
WebHookType setting.NotifyWebHookType `bson:"webhook_type" yaml:"webhook_type" json:"webhook_type"`
+ LarkHookNotificationConfig *LarkHookNotificationConfig `bson:"lark_hook_notification_config,omitempty" yaml:"lark_hook_notification_config,omitempty" json:"lark_hook_notification_config,omitempty"`
LarkGroupNotificationConfig *LarkGroupNotificationConfig `bson:"lark_group_notification_config,omitempty" yaml:"lark_group_notification_config,omitempty" json:"lark_group_notification_config,omitempty"`
LarkPersonNotificationConfig *LarkPersonNotificationConfig `bson:"lark_person_notification_config,omitempty" yaml:"lark_person_notification_config,omitempty" json:"lark_person_notification_config,omitempty"`
WechatNotificationConfig *WechatNotificationConfig `bson:"wechat_notification_config,omitempty" yaml:"wechat_notification_config,omitempty" json:"wechat_notification_config,omitempty"`
diff --git a/pkg/microservice/aslan/core/common/repository/models/workflow.go b/pkg/microservice/aslan/core/common/repository/models/workflow.go
index d9756ba13b..eb02584075 100644
--- a/pkg/microservice/aslan/core/common/repository/models/workflow.go
+++ b/pkg/microservice/aslan/core/common/repository/models/workflow.go
@@ -486,6 +486,7 @@ type HookPayload struct {
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 427c1961b8..a763af15f1 100644
--- a/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go
+++ b/pkg/microservice/aslan/core/common/repository/models/workflow_v4.go
@@ -134,12 +134,14 @@ type WorkflowStage struct {
}
type ManualExec struct {
- Enabled bool `bson:"enabled" yaml:"enabled" json:"enabled"`
- ModifyParams bool `bson:"modify_params" yaml:"modify_params" json:"modify_params"`
- Excuted bool `bson:"excuted,omitempty" yaml:"excuted,omitempty" json:"excuted,omitempty"`
- ManualExecUsers []*User `bson:"manual_exec_users" yaml:"manual_exec_users" json:"manual_exec_users"`
- ManualExectorID string `bson:"manual_exector_id,omitempty" yaml:"manual_exector_id,omitempty" json:"manual_exector_id,omitempty"`
- ManualExectorName string `bson:"manual_exector_name,omitempty" yaml:"manual_exector_name,omitempty" json:"manual_exector_name,omitempty"`
+ Enabled bool `bson:"enabled" yaml:"enabled" json:"enabled"`
+ ModifyParams bool `bson:"modify_params" yaml:"modify_params" json:"modify_params"`
+ Excuted bool `bson:"excuted,omitempty" yaml:"excuted,omitempty" json:"excuted,omitempty"`
+ ManualExecUsers []*User `bson:"manual_exec_users" yaml:"manual_exec_users" json:"manual_exec_users"`
+ LarkPersonNotificationConfig *LarkPersonNotificationConfig `bson:"lark_person_notification_config,omitempty" yaml:"lark_person_notification_config,omitempty" json:"lark_person_notification_config,omitempty"`
+ NotificationSent bool `bson:"notification_sent,omitempty" yaml:"notification_sent,omitempty" json:"notification_sent,omitempty"`
+ ManualExectorID string `bson:"manual_exector_id,omitempty" yaml:"manual_exector_id,omitempty" json:"manual_exector_id,omitempty"`
+ ManualExectorName string `bson:"manual_exector_name,omitempty" yaml:"manual_exector_name,omitempty" json:"manual_exector_name,omitempty"`
}
type Approval struct {
@@ -1011,6 +1013,7 @@ type SQLJobSpec struct {
type DMSJobSpec struct {
ID string `bson:"id" json:"id" yaml:"id"`
RemarkTemplate string `bson:"remark_template" json:"remark_template" yaml:"remark_template"`
+ ExecuteMode string `bson:"execute_mode" json:"execute_mode" yaml:"execute_mode"`
Orders []*DMSOrder `bson:"orders" json:"orders" yaml:"orders"`
}
@@ -1155,12 +1158,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"`
@@ -1244,6 +1247,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)
@@ -1252,44 +1259,56 @@ func (n *NotificationJobSpec) GenerateNewNotifyConfigWithOldData() error {
return nil
}
+type DynamicRecipient struct {
+ Value string `bson:"value" json:"value" yaml:"value"`
+ IdentityType string `bson:"identity_type" json:"identity_type" yaml:"identity_type"`
+}
+
// 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 []*DynamicRecipient `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 []*DynamicRecipient `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"`
+ HookAddress string `bson:"hook_address" json:"hook_address" yaml:"hook_address"`
+ AtUsers []string `bson:"at_users" json:"at_users" yaml:"at_users"`
+ DynamicRecipients []*DynamicRecipient `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 []*DynamicRecipient `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 []*DynamicRecipient `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 []*DynamicRecipient `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 []*DynamicRecipient `bson:"dynamic_recipients" json:"dynamic_recipients" yaml:"dynamic_recipients"`
}
type WebhookNotificationConfig struct {
diff --git a/pkg/microservice/aslan/core/common/repository/models/yaml_template.go b/pkg/microservice/aslan/core/common/repository/models/yaml_template.go
index 175d1722fb..076ffc8567 100644
--- a/pkg/microservice/aslan/core/common/repository/models/yaml_template.go
+++ b/pkg/microservice/aslan/core/common/repository/models/yaml_template.go
@@ -29,6 +29,16 @@ type YamlTemplate struct {
VariableYaml string `bson:"variable_yaml" json:"variable_yaml"`
ServiceVariableKVs []*commontypes.ServiceVariableKV `bson:"service_variable_kvs" json:"service_variable_kvs"`
ServiceVars []string `bson:"service_vars" json:"service_vars"` // Deprecated since 1.18.0
+ Source string `bson:"source,omitempty" json:"source,omitempty"`
+ RepoOwner string `bson:"repo_owner,omitempty" json:"repo_owner,omitempty"`
+ Namespace string `bson:"namespace,omitempty" json:"namespace,omitempty"`
+ RepoName string `bson:"repo_name,omitempty" json:"repo_name,omitempty"`
+ Path string `bson:"path,omitempty" json:"path,omitempty"`
+ BranchName string `bson:"branch_name,omitempty" json:"branch_name,omitempty"`
+ RemoteName string `bson:"remote_name,omitempty" json:"remote_name,omitempty"`
+ CodeHostID int `bson:"codehost_id,omitempty" json:"codeHostID,omitempty"`
+ LoadFromDir bool `bson:"load_from_dir,omitempty" json:"load_from_dir,omitempty"`
+ Commit *Commit `bson:"commit,omitempty" json:"commit,omitempty"`
}
type Variable struct {
diff --git a/pkg/microservice/aslan/core/common/repository/mongodb/release_plan.go b/pkg/microservice/aslan/core/common/repository/mongodb/release_plan.go
index e0b706ec9e..6c4bb0d328 100644
--- a/pkg/microservice/aslan/core/common/repository/mongodb/release_plan.go
+++ b/pkg/microservice/aslan/core/common/repository/mongodb/release_plan.go
@@ -148,6 +148,8 @@ type ListReleasePlanOption struct {
SuccessTimeEnd int64
UpdateTimeStart int64
UpdateTimeEnd int64
+ StartTime int64
+ EndTime int64
IsSort bool
SortBy SortReleasePlanBy
ExcludedFields []string
@@ -190,6 +192,12 @@ func (c *ReleasePlanColl) ListByOptions(opt *ListReleasePlanOption) ([]*models.R
if opt.UpdateTimeStart > 0 && opt.UpdateTimeEnd > 0 {
query["update_time"] = bson.M{"$gte": opt.UpdateTimeStart, "$lte": opt.UpdateTimeEnd}
}
+ if opt.StartTime > 0 && opt.EndTime > 0 {
+ query["$or"] = []bson.M{
+ {"start_time": bson.M{"$gte": opt.StartTime, "$lte": opt.EndTime}},
+ {"end_time": bson.M{"$gte": opt.StartTime, "$lte": opt.EndTime}},
+ }
+ }
if opt.Status != "" {
query["status"] = opt.Status
}
diff --git a/pkg/microservice/aslan/core/common/repository/mongodb/workflow_v4_general_hook.go b/pkg/microservice/aslan/core/common/repository/mongodb/workflow_v4_general_hook.go
index add7d24bd0..32bbdb1821 100644
--- a/pkg/microservice/aslan/core/common/repository/mongodb/workflow_v4_general_hook.go
+++ b/pkg/microservice/aslan/core/common/repository/mongodb/workflow_v4_general_hook.go
@@ -127,7 +127,14 @@ func (c *WorkflowV4GeneralHookColl) Get(ctx *internalhandler.Context, workflowNa
}
func (c *WorkflowV4GeneralHookColl) Exists(ctx *internalhandler.Context, workflowName, hookName string) (bool, error) {
- return singleResultExists(c.Collection.FindOne(ctx, bson.M{"workflow_name": workflowName, "name": hookName}))
+ if err := c.Collection.FindOne(ctx, bson.M{"workflow_name": workflowName, "name": hookName}); err != nil {
+ if err.Err() == mongo.ErrNoDocuments {
+ return false, nil
+ }
+ return false, err.Err()
+ }
+
+ return true, nil
}
func (c *WorkflowV4GeneralHookColl) Update(ctx *internalhandler.Context, id string, obj *models.WorkflowV4GeneralHook) error {
diff --git a/pkg/microservice/aslan/core/common/repository/mongodb/workflow_v4_git_hook.go b/pkg/microservice/aslan/core/common/repository/mongodb/workflow_v4_git_hook.go
index 50cf2d4e2a..8121f2ce9d 100644
--- a/pkg/microservice/aslan/core/common/repository/mongodb/workflow_v4_git_hook.go
+++ b/pkg/microservice/aslan/core/common/repository/mongodb/workflow_v4_git_hook.go
@@ -127,17 +127,13 @@ func (c *WorkflowV4GitHookColl) Get(ctx *internalhandler.Context, workflowName,
}
func (c *WorkflowV4GitHookColl) Exists(ctx *internalhandler.Context, workflowName, hookName string) (bool, error) {
- return singleResultExists(c.Collection.FindOne(ctx, bson.M{"workflow_name": workflowName, "name": hookName}))
-}
-
-func singleResultExists(result *mongo.SingleResult) (bool, error) {
- err := result.Err()
- if err == mongo.ErrNoDocuments {
- return false, nil
- }
- if err != nil {
- return false, err
+ if err := c.Collection.FindOne(ctx, bson.M{"workflow_name": workflowName, "name": hookName}); err != nil {
+ if err.Err() == mongo.ErrNoDocuments {
+ return false, nil
+ }
+ return false, err.Err()
}
+
return true, nil
}
diff --git a/pkg/microservice/aslan/core/common/repository/mongodb/workflow_v4_jira_hook.go b/pkg/microservice/aslan/core/common/repository/mongodb/workflow_v4_jira_hook.go
index 883f6fbad1..6ad4e73192 100644
--- a/pkg/microservice/aslan/core/common/repository/mongodb/workflow_v4_jira_hook.go
+++ b/pkg/microservice/aslan/core/common/repository/mongodb/workflow_v4_jira_hook.go
@@ -99,7 +99,14 @@ func (c *WorkflowV4JiraHookColl) Create(ctx *internalhandler.Context, obj *model
}
func (c *WorkflowV4JiraHookColl) Exists(ctx *internalhandler.Context, workflowName, hookName string) (bool, error) {
- return singleResultExists(c.Collection.FindOne(ctx, bson.M{"workflow_name": workflowName, "name": hookName}))
+ if err := c.Collection.FindOne(ctx, bson.M{"workflow_name": workflowName, "name": hookName}); err != nil {
+ if err.Err() == mongo.ErrNoDocuments {
+ return false, nil
+ }
+ return false, err.Err()
+ }
+
+ return true, nil
}
func (c *WorkflowV4JiraHookColl) List(ctx *internalhandler.Context, workflowName string) ([]*models.WorkflowV4JiraHook, error) {
diff --git a/pkg/microservice/aslan/core/common/repository/mongodb/workflow_v4_meego_hook.go b/pkg/microservice/aslan/core/common/repository/mongodb/workflow_v4_meego_hook.go
index 615dd8c8e3..c47a93e80b 100644
--- a/pkg/microservice/aslan/core/common/repository/mongodb/workflow_v4_meego_hook.go
+++ b/pkg/microservice/aslan/core/common/repository/mongodb/workflow_v4_meego_hook.go
@@ -99,7 +99,14 @@ func (c *WorkflowV4MeegoHookColl) Create(ctx *internalhandler.Context, obj *mode
}
func (c *WorkflowV4MeegoHookColl) Exists(ctx *internalhandler.Context, workflowName, hookName string) (bool, error) {
- return singleResultExists(c.Collection.FindOne(ctx, bson.M{"workflow_name": workflowName, "name": hookName}))
+ if err := c.Collection.FindOne(ctx, bson.M{"workflow_name": workflowName, "name": hookName}); err != nil {
+ if err.Err() == mongo.ErrNoDocuments {
+ return false, nil
+ }
+ return false, err.Err()
+ }
+
+ return true, nil
}
func (c *WorkflowV4MeegoHookColl) List(ctx *internalhandler.Context, workflowName string) ([]*models.WorkflowV4MeegoHook, error) {
diff --git a/pkg/microservice/aslan/core/common/repository/mongodb/yaml_template.go b/pkg/microservice/aslan/core/common/repository/mongodb/yaml_template.go
index 3bd6db492f..6400753a95 100644
--- a/pkg/microservice/aslan/core/common/repository/mongodb/yaml_template.go
+++ b/pkg/microservice/aslan/core/common/repository/mongodb/yaml_template.go
@@ -123,6 +123,24 @@ func (c *YamlTemplateColl) List(pageNum, pageSize int) ([]*models.YamlTemplate,
return resp, int(count), nil
}
+func (c *YamlTemplateColl) ListBySource(source string) ([]*models.YamlTemplate, error) {
+ resp := make([]*models.YamlTemplate, 0)
+ query := bson.M{}
+ if source != "" {
+ query["source"] = source
+ }
+
+ cursor, err := c.Collection.Find(context.TODO(), query)
+ if err != nil {
+ return nil, err
+ }
+ err = cursor.All(context.TODO(), &resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
func (c *YamlTemplateColl) GetById(idstring string) (*models.YamlTemplate, error) {
resp := new(models.YamlTemplate)
id, err := primitive.ObjectIDFromHex(idstring)
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 89545e69d4..abba6e0759 100644
--- a/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go
+++ b/pkg/microservice/aslan/core/common/service/instantmessage/workflow_task.go
@@ -39,13 +39,14 @@ import (
templaterepo "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb/template"
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"
+ workflownotifyutil "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util/workflownotify"
"github.com/koderover/zadig/v2/pkg/setting"
userclient "github.com/koderover/zadig/v2/pkg/shared/client/user"
"github.com/koderover/zadig/v2/pkg/tool/lark"
"github.com/koderover/zadig/v2/pkg/tool/log"
"github.com/koderover/zadig/v2/pkg/tool/sonar"
"github.com/koderover/zadig/v2/pkg/types"
- jobspec "github.com/koderover/zadig/v2/pkg/types/job"
"github.com/koderover/zadig/v2/pkg/types/step"
"github.com/koderover/zadig/v2/pkg/util"
)
@@ -117,8 +118,12 @@ var (
"notificationTextWorkflow": "工作流",
"notificationTextWaitingForApproval": "等待审批",
+ "notificationTextManualExecPending": "等待手动执行",
"notificationTextExecutor": "执行用户",
+ "notificationTextNotifiedUsers": "被通知人",
+ "notificationTextJobs": "任务信息",
"notificationTextProjectName": "项目名称",
+ "notificationTextStageName": "阶段名称",
"notificationTextStartTime": "开始时间",
"notificationTextDuration": "持续时间",
"notificationTextRemark": "备注",
@@ -197,8 +202,12 @@ var (
"notificationTextWorkflow": "Workflow",
"notificationTextWaitingForApproval": "waiting for approval",
+ "notificationTextManualExecPending": "Waiting for Manual Execution",
"notificationTextExecutor": "Executor",
+ "notificationTextNotifiedUsers": "Notified Users",
+ "notificationTextJobs": "Jobs",
"notificationTextProjectName": "Project Name",
+ "notificationTextStageName": "Stage Name",
"notificationTextStartTime": "Start Time",
"notificationTextDuration": "Duration",
"notificationTextRemark": "Remark",
@@ -296,7 +305,7 @@ func (w *Service) SendWorkflowTaskApproveNotifications(workflowName string, task
return fmt.Errorf("executor phone not configured")
}
- client, err := larkservice.GetLarkClientByIMAppID(notify.LarkGroupNotificationConfig.AppID)
+ client, err := larkservice.GetLarkClientByIMAppID(notify.LarkPersonNotificationConfig.AppID)
if err != nil {
return fmt.Errorf("failed to get notify target info: create feishu client error: %s", err)
}
@@ -435,6 +444,456 @@ func (w *Service) SendWorkflowTaskNotifications(task *models.WorkflowTask) error
}
return nil
}
+
+func (w *Service) SendManualExecStageNotifications(workflowCtx *models.WorkflowTaskCtx, stage *models.StageTask) error {
+ if workflowCtx == nil || stage == nil || stage.ManualExec == nil {
+ return nil
+ }
+
+ systemSetting, err := commonrepo.NewSystemSettingColl().Get()
+ if err != nil {
+ return fmt.Errorf("get system language error: %w", err)
+ }
+ language := systemSetting.Language
+
+ jobContents := []string{}
+ taskForNotification := &models.WorkflowTask{
+ WorkflowName: workflowCtx.WorkflowName,
+ TaskID: workflowCtx.TaskID,
+ Stages: []*models.StageTask{stage},
+ }
+ stageForNotification := stage
+ if taskInColl, findErr := w.workflowTaskV4Coll.Find(workflowCtx.WorkflowName, workflowCtx.TaskID); findErr == nil && taskInColl != nil {
+ taskForNotification = taskInColl
+ if matchedStage := getStageTaskByName(taskInColl.Stages, stage.Name); matchedStage != nil {
+ stageForNotification = matchedStage
+ }
+ }
+ notifyCtls := getManualExecStageNotifyCtls(taskForNotification)
+ if len(notifyCtls) == 0 {
+ return nil
+ }
+
+ respErr := new(multierror.Error)
+ for _, notify := range notifyCtls {
+ jobContents, err = w.buildManualExecStageJobContents(taskForNotification, stageForNotification, notify.WebHookType, language)
+ if err != nil {
+ respErr = multierror.Append(respErr, fmt.Errorf("build manual exec stage notification jobs error: %w", err))
+ continue
+ }
+
+ switch notify.WebHookType {
+ case setting.NotifyWebHookTypeFeishuPerson:
+ resolvedTargets, notifiedUsers, err := w.resolveManualExecStageLarkTargets(taskForNotification, stageForNotification, notify)
+ if err != nil {
+ respErr = multierror.Append(respErr, err)
+ continue
+ }
+ if len(resolvedTargets) == 0 {
+ continue
+ }
+
+ notifyToSend := *notify
+ notifyToSend.LarkPersonNotificationConfig = &models.LarkPersonNotificationConfig{
+ AppID: notify.LarkPersonNotificationConfig.AppID,
+ TargetUsers: resolvedTargets,
+ }
+
+ card := w.getManualExecStageLarkCard(workflowCtx, stageForNotification, language, notifiedUsers, jobContents)
+ if err := w.sendNotification("", "", ¬ifyToSend, card, nil, config.StatusPause); err != nil {
+ respErr = multierror.Append(respErr, err)
+ }
+
+ case setting.NotifyWebHookTypeMail:
+ resolvedUsers, notifiedUsers, err := w.resolveManualExecStageMailUsers(taskForNotification, stageForNotification, notify)
+ if err != nil {
+ respErr = multierror.Append(respErr, err)
+ continue
+ }
+ if len(resolvedUsers) == 0 {
+ continue
+ }
+
+ title, content, err := w.getManualExecStageMailContent(workflowCtx, stageForNotification, language, notifiedUsers, jobContents)
+ if err != nil {
+ respErr = multierror.Append(respErr, err)
+ continue
+ }
+
+ notifyToSend := *notify
+ notifyToSend.MailNotificationConfig = &models.MailNotificationConfig{TargetUsers: resolvedUsers}
+ if err := w.sendNotification(title, content, ¬ifyToSend, nil, nil, config.StatusPause); err != nil {
+ respErr = multierror.Append(respErr, err)
+ }
+ }
+ }
+
+ return respErr.ErrorOrNil()
+}
+
+func getManualExecStageNotifyCtls(task *models.WorkflowTask) []*models.NotifyCtl {
+ if task == nil {
+ return nil
+ }
+
+ var notifyCtls []*models.NotifyCtl
+ switch {
+ case task.OriginWorkflowArgs != nil:
+ notifyCtls = task.OriginWorkflowArgs.NotifyCtls
+ case task.WorkflowArgs != nil:
+ notifyCtls = task.WorkflowArgs.NotifyCtls
+ }
+
+ ret := make([]*models.NotifyCtl, 0, len(notifyCtls))
+ for _, notify := range notifyCtls {
+ if notify == nil || !notify.Enabled {
+ continue
+ }
+ if err := notify.GenerateNewNotifyConfigWithOldData(); err != nil {
+ log.Errorf("failed to parse notification config for workflow %s task %d: %v", task.WorkflowName, task.TaskID, err)
+ continue
+ }
+ if !sets.NewString(notify.NotifyTypes...).Has(string(config.StatusPause)) {
+ continue
+ }
+ if notify.WebHookType != setting.NotifyWebHookTypeFeishuPerson && notify.WebHookType != setting.NotifyWebHookTypeMail {
+ continue
+ }
+ ret = append(ret, notify)
+ }
+
+ return ret
+}
+
+func (w *Service) buildManualExecStageJobContents(task *models.WorkflowTask, stage *models.StageTask, webHookType setting.NotifyWebHookType, language string) ([]string, error) {
+ jobContents, _, err := workflownotifyutil.BuildWorkflowJobContents(&workflownotifyutil.BuildJobContentsArgs{
+ Task: task,
+ Stages: []*models.StageTask{stage},
+ WebHookType: webHookType,
+ RenderTemplate: func(tpl string, job *models.JobTask) (string, error) {
+ return getJobTaskTplExec(tpl, &jobTaskNotification{Job: job, WebHookType: webHookType}, language)
+ },
+ GetTestResult: func(jobName string) (string, error) {
+ return genTestResultText(task.WorkflowName, jobName, task.TaskID, language)
+ },
+ GetSonarMetrics: func(jobSpec *models.JobTaskFreestyleSpec) (string, string, error) {
+ return genSonartMetricsText(jobSpec, language)
+ },
+ })
+ return jobContents, err
+}
+
+func (w *Service) resolveManualExecStageLarkTargets(task *models.WorkflowTask, stage *models.StageTask, notify *models.NotifyCtl) ([]*lark.UserInfo, string, error) {
+ if notify == nil || notify.LarkPersonNotificationConfig == nil || notify.LarkPersonNotificationConfig.AppID == "" {
+ return nil, "", nil
+ }
+
+ client, err := larkservice.GetLarkClientByIMAppID(notify.LarkPersonNotificationConfig.AppID)
+ if err != nil {
+ return nil, "", fmt.Errorf("create feishu client error: %w", err)
+ }
+
+ respErr := new(multierror.Error)
+ targets := make([]*lark.UserInfo, 0, len(notify.LarkPersonNotificationConfig.TargetUsers))
+ targetSet := sets.NewString()
+ nameSet := sets.NewString()
+ notifiedUsers := make([]string, 0, len(notify.LarkPersonNotificationConfig.TargetUsers))
+ stageUsers, stageUserInfoMap := resolveManualExecStageUsers(stage, task.TaskCreatorID)
+
+ for _, target := range notify.LarkPersonNotificationConfig.TargetUsers {
+ if target == nil {
+ continue
+ }
+
+ if target.IsExecutor {
+ if task.TaskCreatorID == "" {
+ respErr = multierror.Append(respErr, fmt.Errorf("executor id is empty, cannot send message"))
+ continue
+ }
+ userInfo, err := userclient.New().GetUserByID(task.TaskCreatorID)
+ if err != nil {
+ respErr = multierror.Append(respErr, fmt.Errorf("failed to find user %s, error: %w", task.TaskCreatorID, err))
+ continue
+ }
+ resolvedTarget, displayName, resolveErr := w.resolveManualExecStageLarkTargetFromUser(client, userInfo.Uid, userInfo.Name)
+ if resolveErr != nil {
+ respErr = multierror.Append(respErr, resolveErr)
+ continue
+ }
+ targets, notifiedUsers = appendManualExecStageLarkTarget(targets, targetSet, notifiedUsers, nameSet, resolvedTarget, displayName)
+ continue
+ }
+
+ if target.IsStageExecutor {
+ for _, stageUser := range stageUsers {
+ if stageUser == nil || stageUser.UserID == "" {
+ continue
+ }
+ displayName := stageUser.UserName
+ if info, ok := stageUserInfoMap[stageUser.UserID]; ok && info != nil && info.Name != "" {
+ displayName = info.Name
+ }
+ resolvedTarget, resolvedDisplayName, resolveErr := w.resolveManualExecStageLarkTargetFromUser(client, stageUser.UserID, displayName)
+ if resolveErr != nil {
+ respErr = multierror.Append(respErr, fmt.Errorf("stage executor %s: %w", stageUser.UserID, resolveErr))
+ continue
+ }
+ targets, notifiedUsers = appendManualExecStageLarkTarget(targets, targetSet, notifiedUsers, nameSet, resolvedTarget, resolvedDisplayName)
+ }
+ continue
+ }
+
+ resolvedTarget := &lark.UserInfo{
+ ID: target.ID,
+ IDType: target.IDType,
+ Name: target.Name,
+ Avatar: target.Avatar,
+ IsExecutor: target.IsExecutor,
+ IsStageExecutor: target.IsStageExecutor,
+ }
+ displayName := target.Name
+
+ if resolvedTarget.ID == "" {
+ continue
+ }
+ if resolvedTarget.IDType == "" {
+ resolvedTarget.IDType = setting.LarkUserID
+ }
+ targets, notifiedUsers = appendManualExecStageLarkTarget(targets, targetSet, notifiedUsers, nameSet, resolvedTarget, displayName)
+ }
+
+ return targets, strings.Join(notifiedUsers, ", "), respErr.ErrorOrNil()
+}
+
+func (w *Service) resolveManualExecStageMailUsers(task *models.WorkflowTask, stage *models.StageTask, notify *models.NotifyCtl) ([]*models.User, string, error) {
+ if notify == nil || notify.MailNotificationConfig == nil {
+ return nil, "", nil
+ }
+
+ usersToExpand := make([]*models.User, 0, len(notify.MailNotificationConfig.TargetUsers))
+ for _, user := range notify.MailNotificationConfig.TargetUsers {
+ if user == nil {
+ continue
+ }
+ if user.Type == setting.UserTypeStageExecutor {
+ if stage == nil || stage.ManualExec == nil {
+ continue
+ }
+ usersToExpand = append(usersToExpand, stage.ManualExec.ManualExecUsers...)
+ continue
+ }
+ usersToExpand = append(usersToExpand, user)
+ }
+
+ var users []*models.User
+ var userInfoMap map[string]*types.UserInfo
+ if task.TaskCreatorID != "" {
+ users, userInfoMap = commonutil.GeneFlatUsersWithCaller(usersToExpand, task.TaskCreatorID)
+ } else {
+ users, userInfoMap = commonutil.GeneFlatUsers(usersToExpand)
+ }
+
+ return users, formatManualExecNotifiedUsers(users, userInfoMap), nil
+}
+
+func resolveManualExecStageUsers(stage *models.StageTask, taskCreatorID string) ([]*models.User, map[string]*types.UserInfo) {
+ if stage == nil || stage.ManualExec == nil || len(stage.ManualExec.ManualExecUsers) == 0 {
+ return nil, map[string]*types.UserInfo{}
+ }
+
+ if taskCreatorID != "" {
+ return commonutil.GeneFlatUsersWithCaller(stage.ManualExec.ManualExecUsers, taskCreatorID)
+ }
+
+ return commonutil.GeneFlatUsers(stage.ManualExec.ManualExecUsers)
+}
+
+func (w *Service) resolveManualExecStageLarkTargetFromUser(client *lark.Client, userID, userName string) (*lark.UserInfo, string, error) {
+ userInfo, err := userclient.New().GetUserByID(userID)
+ if err != nil {
+ return nil, "", fmt.Errorf("failed to find user %s, error: %w", userID, err)
+ }
+ if len(userInfo.Phone) == 0 {
+ return nil, "", fmt.Errorf("phone not configured")
+ }
+
+ larkUser, err := client.GetUserIDByEmailOrMobile(lark.QueryTypeMobile, userInfo.Phone, setting.LarkUserID)
+ if err != nil {
+ return nil, "", fmt.Errorf("find lark user with phone %s error: %w", userInfo.Phone, err)
+ }
+
+ userDetailedInfo, err := client.GetUserInfoByID(util.GetStringFromPointer(larkUser.UserId), setting.LarkUserID)
+ if err != nil {
+ return nil, "", fmt.Errorf("find lark user info for userID %s error: %w", util.GetStringFromPointer(larkUser.UserId), err)
+ }
+
+ displayName := userDetailedInfo.Name
+ if displayName == "" {
+ displayName = userName
+ }
+
+ return &lark.UserInfo{
+ ID: util.GetStringFromPointer(larkUser.UserId),
+ IDType: setting.LarkUserID,
+ Name: userDetailedInfo.Name,
+ Avatar: userDetailedInfo.Avatar,
+ }, displayName, nil
+}
+
+func appendManualExecStageLarkTarget(targets []*lark.UserInfo, targetSet sets.String, notifiedUsers []string, nameSet sets.String, target *lark.UserInfo, displayName string) ([]*lark.UserInfo, []string) {
+ if target == nil || target.ID == "" {
+ return targets, notifiedUsers
+ }
+ if target.IDType == "" {
+ target.IDType = setting.LarkUserID
+ }
+
+ targetKey := fmt.Sprintf("%s:%s", target.IDType, target.ID)
+ if !targetSet.Has(targetKey) {
+ targetSet.Insert(targetKey)
+ targets = append(targets, target)
+ }
+
+ if displayName == "" {
+ displayName = target.Name
+ }
+ if displayName == "" {
+ displayName = target.ID
+ }
+ if !nameSet.Has(displayName) {
+ nameSet.Insert(displayName)
+ notifiedUsers = append(notifiedUsers, displayName)
+ }
+
+ return targets, notifiedUsers
+}
+
+func formatManualExecNotifiedUsers(users []*models.User, userInfoMap map[string]*types.UserInfo) string {
+ if len(users) == 0 {
+ return ""
+ }
+
+ names := make([]string, 0, len(users))
+ nameSet := sets.NewString()
+ for _, user := range users {
+ if user == nil || user.UserID == "" {
+ continue
+ }
+
+ name := user.UserName
+ if info, ok := userInfoMap[user.UserID]; ok && info != nil && info.Name != "" {
+ name = info.Name
+ }
+ if name == "" {
+ name = user.UserID
+ }
+ if nameSet.Has(name) {
+ continue
+ }
+ nameSet.Insert(name)
+ names = append(names, name)
+ }
+
+ sort.Strings(names)
+ return strings.Join(names, ", ")
+}
+
+func getStageTaskByName(stages []*models.StageTask, stageName string) *models.StageTask {
+ for _, stage := range stages {
+ if stage != nil && stage.Name == stageName {
+ return stage
+ }
+ }
+ return nil
+}
+
+func (w *Service) getManualExecStageLarkCard(workflowCtx *models.WorkflowTaskCtx, stage *models.StageTask, language, notifiedUsers string, jobContents []string) *LarkCard {
+ title := fmt.Sprintf("%s %s #%d %s", getText("notificationTextWorkflow", language), workflowCtx.WorkflowDisplayName, workflowCtx.TaskID, getText("notificationTextManualExecPending", language))
+ detailURL := fmt.Sprintf("%s/v1/projects/detail/%s/pipelines/custom/%s/%d?display_name=%s",
+ configbase.SystemAddress(),
+ workflowCtx.ProjectName,
+ workflowCtx.WorkflowName,
+ workflowCtx.TaskID,
+ url.QueryEscape(workflowCtx.WorkflowDisplayName),
+ )
+
+ lc := NewLarkCard()
+ lc.SetConfig(true)
+ lc.SetHeader(feishuHeaderTemplateTurquoise, title, feiShuTagText)
+ lc.AddI18NElementsZhcnFeild(fmt.Sprintf("**%s**:%s", getText("notificationTextProjectName", language), workflowCtx.ProjectDisplayName), true)
+ lc.AddI18NElementsZhcnFeild(fmt.Sprintf("**%s**:%s", getText("notificationTextStageName", language), stage.Name), true)
+ lc.AddI18NElementsZhcnFeild(fmt.Sprintf("**%s**:%s", getText("notificationTextExecutor", language), workflowCtx.WorkflowTaskCreatorUsername), true)
+ if notifiedUsers != "" {
+ lc.AddI18NElementsZhcnFeild(fmt.Sprintf("**%s**:%s", getText("notificationTextNotifiedUsers", language), notifiedUsers), true)
+ }
+ for idx, jobContent := range jobContents {
+ if jobContent == "" {
+ continue
+ }
+ if idx == 0 {
+ lc.AddI18NElementsZhcnFeild(fmt.Sprintf("**%s**:\n%s", getText("notificationTextJobs", language), jobContent), true)
+ continue
+ }
+ lc.AddI18NElementsZhcnFeild(jobContent, true)
+ }
+ lc.AddI18NElementsZhcnFeild(fmt.Sprintf("**%s**:%s", getText("notificationTextStartTime", language), workflowCtx.StartTime.Format(time.DateTime)), true)
+ lc.AddI18NElementsZhcnFeild(fmt.Sprintf("**%s**:%s", getText("notificationTextRemark", language), workflowCtx.Remark), true)
+ lc.AddI18NElementsZhcnAction(getText("notificationTextClickForMore", language), detailURL)
+ return lc
+}
+
+func (w *Service) getManualExecStageMailContent(workflowCtx *models.WorkflowTaskCtx, stage *models.StageTask, language, notifiedUsers string, jobContents []string) (string, string, error) {
+ title := fmt.Sprintf("%s %s #%d %s", getText("notificationTextWorkflow", language), workflowCtx.WorkflowDisplayName, workflowCtx.TaskID, getText("notificationTextManualExecPending", language))
+ detailURL := fmt.Sprintf("%s/v1/projects/detail/%s/pipelines/custom/%s/%d?display_name=%s",
+ configbase.SystemAddress(),
+ workflowCtx.ProjectName,
+ workflowCtx.WorkflowName,
+ workflowCtx.TaskID,
+ url.QueryEscape(workflowCtx.WorkflowDisplayName),
+ )
+
+ lines := []string{
+ fmt.Sprintf("%s:%s \n", getText("notificationTextProjectName", language), workflowCtx.ProjectDisplayName),
+ fmt.Sprintf("%s:%s \n", getText("notificationTextStageName", language), stage.Name),
+ fmt.Sprintf("%s:%s \n", getText("notificationTextExecutor", language), workflowCtx.WorkflowTaskCreatorUsername),
+ }
+ if notifiedUsers != "" {
+ lines = append(lines, fmt.Sprintf("%s:%s \n", getText("notificationTextNotifiedUsers", language), notifiedUsers))
+ }
+ if len(jobContents) > 0 {
+ lines = append(lines, fmt.Sprintf("%s:\n", getText("notificationTextJobs", language)))
+ lines = append(lines, strings.Join(jobContents, ""))
+ }
+ lines = append(lines,
+ fmt.Sprintf("%s:%s \n", getText("notificationTextStartTime", language), workflowCtx.StartTime.Format(time.DateTime)),
+ fmt.Sprintf("%s:%s \n", getText("notificationTextRemark", language), workflowCtx.Remark),
+ )
+
+ content := strings.TrimSpace(strings.Join(lines, ""))
+ t, err := template.New("workflow_notification").Parse(getMailTemplate(language))
+ if err != nil {
+ return "", "", fmt.Errorf("workflow notification template parse error, error msg:%s", err)
+ }
+
+ var buf bytes.Buffer
+ err = t.Execute(&buf, struct {
+ WorkflowName string
+ WorkflowTaskID int64
+ Content string
+ Url string
+ }{
+ WorkflowName: workflowCtx.WorkflowDisplayName,
+ WorkflowTaskID: workflowCtx.TaskID,
+ Content: content,
+ Url: detailURL,
+ })
+ if err != nil {
+ return "", "", fmt.Errorf("workflow notification template execute error, error msg:%s", err)
+ }
+
+ return title, buf.String(), nil
+}
+
func (w *Service) getApproveNotificationContent(notify *models.NotifyCtl, task *models.WorkflowTask) (string, string, *LarkCard, *webhooknotify.WorkflowNotify, error) {
project, err := templaterepo.NewProductColl().Find(task.ProjectName)
if err != nil {
@@ -678,247 +1137,22 @@ func (w *Service) getNotificationContent(notify *models.NotifyCtl, task *models.
"{{getText \"notificationTextRemark\"}}:{{ .Task.Remark}} \n",
}
- jobContents := []string{}
- workflowNotifyStages := []*webhooknotify.WorkflowNotifyStage{}
- for _, stage := range task.Stages {
- workflowNotifyStage := &webhooknotify.WorkflowNotifyStage{
- Name: stage.Name,
- Status: stage.Status,
- StartTime: stage.StartTime,
- EndTime: stage.EndTime,
- Error: stage.Error,
- }
-
- for _, job := range stage.Jobs {
- workflowNotifyJob := &webhooknotify.WorkflowNotifyJobTask{
- Name: job.Name,
- DisplayName: job.DisplayName,
- JobType: job.JobType,
- Status: job.Status,
- StartTime: job.StartTime,
- EndTime: job.EndTime,
- Error: job.Error,
- }
-
- jobTplcontent := "{{if and (ne .WebHookType \"feishu\") (ne .WebHookType \"feishu_app\") (ne .WebHookType \"feishu_person\")}}\n\n{{end}}{{if eq .WebHookType \"dingding\"}}---\n\n##### {{end}}**{{jobType .Job.JobType }}**: {{.Job.DisplayName}} **{{getText \"notificationTextStatus\"}}**: {{taskStatus .Job.Status }} \n"
- mailJobTplcontent := "{{jobType .Job.JobType }}:{{.Job.DisplayName}} {{getText \"notificationTextStatus\"}}:{{taskStatus .Job.Status }} \n"
- switch job.JobType {
- case string(config.JobZadigBuild):
- fallthrough
- case string(config.JobFreestyle):
- jobSpec := &models.JobTaskFreestyleSpec{}
- models.IToi(job.Spec, jobSpec)
-
- workflowNotifyJobTaskSpec := &webhooknotify.WorkflowNotifyJobTaskBuildSpec{}
-
- repos := []*types.Repository{}
- for _, stepTask := range jobSpec.Steps {
- if stepTask.StepType == config.StepGit {
- stepSpec := &step.StepGitSpec{}
- models.IToi(stepTask.Spec, stepSpec)
- repos = stepSpec.Repos
- }
- }
-
- branchTag, commitID, gitCommitURL := "", "", ""
- commitMsgs := []string{}
- var prInfoList []string
- var prInfo string
- for idx, buildRepo := range repos {
- workflowNotifyRepository := &webhooknotify.WorkflowNotifyRepository{
- Source: buildRepo.Source,
- RepoOwner: buildRepo.RepoOwner,
- RepoNamespace: buildRepo.RepoNamespace,
- RepoName: buildRepo.RepoName,
- Branch: buildRepo.Branch,
- Tag: buildRepo.Tag,
- AuthorName: buildRepo.AuthorName,
- CommitID: buildRepo.CommitID,
- CommitMessage: buildRepo.CommitMessage,
- }
- if idx == 0 || buildRepo.IsPrimary {
- branchTag = buildRepo.Branch
- if buildRepo.Tag != "" {
- branchTag = buildRepo.Tag
- }
- if len(buildRepo.CommitID) > 8 {
- commitID = buildRepo.CommitID[0:8]
- }
- var prLinkBuilder func(baseURL, owner, repoName string, prID int) string
- switch buildRepo.Source {
- case types.ProviderGithub:
- prLinkBuilder = func(baseURL, owner, repoName string, prID int) string {
- return fmt.Sprintf("%s/%s/%s/pull/%d", baseURL, owner, repoName, prID)
- }
- case types.ProviderGitee:
- prLinkBuilder = func(baseURL, owner, repoName string, prID int) string {
- return fmt.Sprintf("%s/%s/%s/pulls/%d", baseURL, owner, repoName, prID)
- }
- case types.ProviderGitlab:
- prLinkBuilder = func(baseURL, owner, repoName string, prID int) string {
- return fmt.Sprintf("%s/%s/%s/merge_requests/%d", baseURL, owner, repoName, prID)
- }
- case types.ProviderGerrit:
- prLinkBuilder = func(baseURL, owner, repoName string, prID int) string {
- return fmt.Sprintf("%s/%d", baseURL, prID)
- }
- default:
- prLinkBuilder = func(baseURL, owner, repoName string, prID int) string {
- return ""
- }
- }
- prInfoList = []string{}
- sort.Ints(buildRepo.PRs)
- for _, id := range buildRepo.PRs {
- link := prLinkBuilder(buildRepo.Address, buildRepo.RepoOwner, buildRepo.RepoName, id)
- if link != "" {
- prInfoList = append(prInfoList, fmt.Sprintf("[#%d](%s)", id, link))
- }
- }
- commitMsg := strings.Trim(buildRepo.CommitMessage, "\n")
- commitMsgs = strings.Split(commitMsg, "\n")
- gitCommitURL = fmt.Sprintf("%s/%s/%s/commit/%s", buildRepo.Address, buildRepo.RepoOwner, buildRepo.RepoName, commitID)
- workflowNotifyRepository.CommitURL = gitCommitURL
- }
-
- workflowNotifyJobTaskSpec.Repositories = append(workflowNotifyJobTaskSpec.Repositories, workflowNotifyRepository)
- }
- if len(prInfoList) != 0 {
- // need an extra space at the end
- prInfo = strings.Join(prInfoList, " ") + " "
- }
- image := ""
- imageContextKey := strings.Join(strings.Split(jobspec.GetJobOutputKey(job.Key, "IMAGE"), "."), "@?")
- if task.GlobalContext != nil {
- image = task.GlobalContext[imageContextKey]
- }
- if len(commitID) > 0 {
- jobTplcontent += fmt.Sprintf("{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextRepositoryInfo\"}}**:%s %s[%s](%s) ", branchTag, prInfo, commitID, gitCommitURL)
- jobTplcontent += "{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextCommitMessage\"}}**:"
- mailJobTplcontent += fmt.Sprintf("{{getText \"notificationTextRepositoryInfo\"}}:%s %s[%s]( %s ) ", branchTag, prInfo, commitID, gitCommitURL)
- if len(commitMsgs) == 1 {
- jobTplcontent += fmt.Sprintf("%s \n", commitMsgs[0])
- } else {
- jobTplcontent += "\n"
- for _, commitMsg := range commitMsgs {
- jobTplcontent += fmt.Sprintf("%s \n", commitMsg)
- }
- }
- }
- if job.Status == config.StatusPassed && image != "" && !strings.HasPrefix(image, "{{.") && !strings.Contains(image, "}}") {
- jobTplcontent += fmt.Sprintf("{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextImageInfo\"}}**:%s \n", image)
- mailJobTplcontent += fmt.Sprintf("{{getText \"notificationTextImageInfo\"}}:%s \n", image)
- workflowNotifyJobTaskSpec.Image = image
- }
-
- workflowNotifyJob.Spec = workflowNotifyJobTaskSpec
- case string(config.JobZadigDeploy):
- jobSpec := &models.JobTaskDeploySpec{}
- models.IToi(job.Spec, jobSpec)
- jobTplcontent += fmt.Sprintf("{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextEnvironment\"}}**:%s \n", jobSpec.Env)
- mailJobTplcontent += fmt.Sprintf("{{getText \"notificationTextEnvironment\"}}:%s \n", jobSpec.Env)
-
- if job.Status == config.StatusPassed && len(jobSpec.ServiceAndImages) > 0 {
- jobTplcontent += fmt.Sprintf("{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextImageInfo\"}}**: \n")
- mailJobTplcontent += fmt.Sprintf("{{getText \"notificationTextImageInfo\"}}: \n")
- }
-
- serviceModules := []*webhooknotify.WorkflowNotifyDeployServiceModule{}
- for _, serviceAndImage := range jobSpec.ServiceAndImages {
- if job.Status == config.StatusPassed && !strings.HasPrefix(serviceAndImage.Image, "{{.") && !strings.Contains(serviceAndImage.Image, "}}") {
- jobTplcontent += fmt.Sprintf("%s \n", serviceAndImage.Image)
- mailJobTplcontent += fmt.Sprintf("%s \n", serviceAndImage.Image)
- }
-
- serviceModule := &webhooknotify.WorkflowNotifyDeployServiceModule{
- ServiceModule: serviceAndImage.ServiceModule,
- Image: serviceAndImage.Image,
- }
- serviceModules = append(serviceModules, serviceModule)
- }
-
- workflowNotifyJobTaskSpec := &webhooknotify.WorkflowNotifyJobTaskDeploySpec{
- Env: jobSpec.Env,
- ServiceName: jobSpec.ServiceName,
- ServiceModules: serviceModules,
- }
- workflowNotifyJob.Spec = workflowNotifyJobTaskSpec
- case string(config.JobZadigHelmDeploy):
- jobSpec := &models.JobTaskHelmDeploySpec{}
- models.IToi(job.Spec, jobSpec)
- jobTplcontent += fmt.Sprintf("{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextEnvironment\"}}**:%s \n", jobSpec.Env)
- mailJobTplcontent += fmt.Sprintf("{{getText \"notificationTextEnvironment\"}}:%s \n", jobSpec.Env)
-
- if job.Status == config.StatusPassed && len(jobSpec.ImageAndModules) > 0 {
- jobTplcontent += fmt.Sprintf("{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextImageInfo\"}}**: \n")
- mailJobTplcontent += fmt.Sprintf("{{getText \"notificationTextImageInfo\"}}: \n")
- }
-
- serviceModules := []*webhooknotify.WorkflowNotifyDeployServiceModule{}
- for _, serviceAndImage := range jobSpec.ImageAndModules {
- if !strings.HasPrefix(serviceAndImage.Image, "{{.") && !strings.Contains(serviceAndImage.Image, "}}") {
- jobTplcontent += fmt.Sprintf("%s \n", serviceAndImage.Image)
- mailJobTplcontent += fmt.Sprintf("%s \n", serviceAndImage.Image)
- }
-
- serviceModule := &webhooknotify.WorkflowNotifyDeployServiceModule{
- ServiceModule: serviceAndImage.ServiceModule,
- Image: serviceAndImage.Image,
- }
- serviceModules = append(serviceModules, serviceModule)
- }
-
- workflowNotifyJobTaskSpec := &webhooknotify.WorkflowNotifyJobTaskDeploySpec{
- Env: jobSpec.Env,
- ServiceName: jobSpec.ServiceName,
- ServiceModules: serviceModules,
- }
- workflowNotifyJob.Spec = workflowNotifyJobTaskSpec
- case string(config.JobZadigTesting):
- testResult, err := genTestResultText(task.WorkflowName, job.Name, task.TaskID, language)
- if err != nil {
- log.Errorf("genTestResultText err:%s", err)
- return "", "", nil, nil, fmt.Errorf("genTestResultText err:%s", err)
- }
-
- jobTplcontent += fmt.Sprintf("{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextTestResult\"}}**: %s \n", testResult)
- mailJobTplcontent += fmt.Sprintf("{{getText \"notificationTextTestResult\"}}: %s \n", testResult)
- case string(config.JobZadigScanning):
- jobSpec := &models.JobTaskFreestyleSpec{}
- models.IToi(job.Spec, jobSpec)
- sonarMetricsText, mailSonarMetricsText, err := genSonartMetricsText(jobSpec, language)
- if err != nil {
- log.Errorf("genTestResultText err:%s", err)
- return "", "", nil, nil, fmt.Errorf("genTestResultText err:%s", err)
- }
-
- if sonarMetricsText != "" {
- jobTplcontent += fmt.Sprintf("{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextSonarMetrics\"}}**: %s \n", sonarMetricsText)
- mailJobTplcontent += fmt.Sprintf("{{getText \"notificationTextSonarMetrics\"}}: %s \n", mailSonarMetricsText)
- }
- }
- jobNotifaication := &jobTaskNotification{
- Job: job,
- WebHookType: notify.WebHookType,
- }
-
- if notify.WebHookType == setting.NotifyWebHookTypeMail {
- jobContent, err := getJobTaskTplExec(mailJobTplcontent, jobNotifaication, language)
- if err != nil {
- return "", "", nil, nil, err
- }
- jobContents = append(jobContents, jobContent)
- } else {
- jobContent, err := getJobTaskTplExec(jobTplcontent, jobNotifaication, language)
- if err != nil {
- return "", "", nil, nil, err
- }
- jobContents = append(jobContents, jobContent)
- }
-
- workflowNotifyStage.Jobs = append(workflowNotifyStage.Jobs, workflowNotifyJob)
- }
- workflowNotifyStages = append(workflowNotifyStages, workflowNotifyStage)
+ jobContents, workflowNotifyStages, err := workflownotifyutil.BuildWorkflowJobContents(&workflownotifyutil.BuildJobContentsArgs{
+ Task: task,
+ Stages: task.Stages,
+ WebHookType: notify.WebHookType,
+ RenderTemplate: func(tpl string, job *models.JobTask) (string, error) {
+ return getJobTaskTplExec(tpl, &jobTaskNotification{Job: job, WebHookType: notify.WebHookType}, language)
+ },
+ GetTestResult: func(jobName string) (string, error) {
+ return genTestResultText(task.WorkflowName, jobName, task.TaskID, language)
+ },
+ GetSonarMetrics: func(jobSpec *models.JobTaskFreestyleSpec) (string, string, error) {
+ return genSonartMetricsText(jobSpec, language)
+ },
+ })
+ if err != nil {
+ return "", "", nil, nil, err
}
webhookNotify.Stages = workflowNotifyStages
@@ -1396,6 +1630,9 @@ func (w *Service) sendNotification(title, content string, notify *models.NotifyC
respErr := new(multierror.Error)
for _, target := range notify.LarkPersonNotificationConfig.TargetUsers {
+ if target == nil || target.ID == "" {
+ continue
+ }
err = w.sendFeishuMessageFromClient(client, target.IDType, target.ID, LarkMessageTypeCard, string(messageContent))
if err != nil {
respErr = multierror.Append(respErr, err)
diff --git a/pkg/microservice/aslan/core/common/service/kube/helm.go b/pkg/microservice/aslan/core/common/service/kube/helm.go
index cff0f82891..b814edef69 100644
--- a/pkg/microservice/aslan/core/common/service/kube/helm.go
+++ b/pkg/microservice/aslan/core/common/service/kube/helm.go
@@ -27,6 +27,7 @@ import (
"github.com/cenkalti/backoff/v4"
"github.com/hashicorp/go-multierror"
+ "github.com/koderover/zadig/v2/pkg/setting"
"github.com/koderover/zadig/v2/pkg/tool/clientmanager"
mongotool "github.com/koderover/zadig/v2/pkg/tool/mongo"
helmclient "github.com/mittwald/go-helm-client"
@@ -35,10 +36,12 @@ import (
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/releaseutil"
versionedclient "istio.io/client-go/pkg/clientset/versioned"
+ appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
+ "k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/config"
@@ -53,6 +56,7 @@ import (
commonutil "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util"
"github.com/koderover/zadig/v2/pkg/tool/cache"
helmtool "github.com/koderover/zadig/v2/pkg/tool/helmclient"
+ "github.com/koderover/zadig/v2/pkg/tool/kube/getter"
kubeutil "github.com/koderover/zadig/v2/pkg/tool/kube/util"
"github.com/koderover/zadig/v2/pkg/tool/log"
"github.com/koderover/zadig/v2/pkg/types"
@@ -106,6 +110,11 @@ func InstallOrUpgradeHelmChartWithValues(param *ReleaseInstallParam, isRetry boo
chartSpec.Timeout = time.Second * time.Duration(param.Timeout)
}
+ stuckDeployments, stuckStatefulSets, err := getStuckWorkload(helmClient, chartSpec)
+ if err != nil {
+ return fmt.Errorf("failed to get stuck workloads: %s", err)
+ }
+
// If the target environment is a shared environment and a sub env, we need to clear the deployed K8s Service.
ctx := context.TODO()
err = EnsureDeletePreCreatedServices(ctx, param.ProductName, param.Namespace, chartSpec, helmClient)
@@ -125,14 +134,94 @@ func InstallOrUpgradeHelmChartWithValues(param *ReleaseInstallParam, isRetry boo
err,
"failed to install or upgrade helm chart %s/%s",
namespace, serviceObj.ServiceName)
+ return err
} else {
err = EnsureZadigServiceByManifest(ctx, param.ProductName, param.Namespace, release.Manifest)
if err != nil {
err = errors.WithMessagef(err, "failed to ensure Zadig Service, err: %s", err)
+ return err
}
}
- return err
+ restCfg := helmClient.RestConfig
+ if restCfg == nil {
+ return fmt.Errorf("failed to get kubernetes client set, err: helm client rest config is nil")
+ }
+
+ clientSet, err := kubernetes.NewForConfig(restCfg)
+ if err != nil {
+ return fmt.Errorf("failed to get kubernetes client set, err: %v", err)
+ }
+ err = cleanupStuckWorkloads(clientSet, stuckDeployments, stuckStatefulSets, log.SugaredLogger())
+ if err != nil {
+ return fmt.Errorf("failed to cleanup stuck workloads, err: %v", err)
+ }
+
+ return nil
+}
+
+func getStuckWorkload(helmClient *helmtool.HelmClient, chartSpec *helmclient.ChartSpec) ([]*appsv1.Deployment, []*appsv1.StatefulSet, error) {
+ manifestBytes, err := helmClient.TemplateChart(chartSpec, nil)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to template chart %s/%s, err: %s", chartSpec.ReleaseName, chartSpec.ChartName, err)
+ }
+
+ _, resourceMap, err := ManifestToUnstructured(string(manifestBytes))
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to convert manifest to unstructured: %v", err)
+ }
+
+ kubeClient, err := helmClient.GetKubeClient()
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to get kube client: %s", err)
+ }
+
+ stuckDeployments := make([]*appsv1.Deployment, 0)
+ stuckStatefulSets := make([]*appsv1.StatefulSet, 0)
+ for _, resource := range resourceMap {
+ if resource.Unstructured.GetKind() == setting.Deployment {
+ existingDeploy, deployExists, getErr := getter.GetDeployment(chartSpec.Namespace, resource.Unstructured.GetName(), kubeClient)
+ isStuck := false
+ if getErr != nil {
+ log.Warnf("Failed to get existing Deployment %s/%s: %v", chartSpec.Namespace, resource.Unstructured.GetName(), getErr)
+ } else if deployExists {
+ isStuck = IsDeploymentStuckInUpdate(existingDeploy, log.SugaredLogger())
+ if isStuck {
+ stuckDeployments = append(stuckDeployments, existingDeploy)
+ }
+ }
+ } else if resource.Unstructured.GetKind() == setting.StatefulSet {
+ existingSts, stsExists, getErr := getter.GetStatefulSet(chartSpec.Namespace, resource.Unstructured.GetName(), kubeClient)
+ if getErr != nil {
+ log.Warnf("Failed to get existing StatefulSet %s/%s: %v", chartSpec.Namespace, resource.Unstructured.GetName(), getErr)
+ } else if stsExists {
+ isStuck := IsStatefulSetStuckInUpdate(existingSts, log.SugaredLogger())
+ if isStuck {
+ stuckStatefulSets = append(stuckStatefulSets, existingSts)
+ }
+ }
+ }
+ }
+ return stuckDeployments, stuckStatefulSets, nil
+}
+
+func cleanupStuckWorkloads(clientSet *kubernetes.Clientset, stuckDeployments []*appsv1.Deployment, stuckStatefulSets []*appsv1.StatefulSet, logger *zap.SugaredLogger) error {
+ var err error
+ for _, deploy := range stuckDeployments {
+ err = HandleStuckDeployment(deploy, clientSet, logger)
+ if err != nil {
+ err = fmt.Errorf("failed to handle stuck deployment, name: %s, error: %v", deploy.Name, err)
+ return err
+ }
+ }
+ for _, sts := range stuckStatefulSets {
+ err = HandleStuckStatefulSet(sts, clientSet, logger)
+ if err != nil {
+ err = fmt.Errorf("failed to handle stuck statefulset, name: %s, error: %v", sts.Name, err)
+ return err
+ }
+ }
+ return nil
}
type chartInstantiateDeploy struct {
diff --git a/pkg/microservice/aslan/core/common/service/template/types.go b/pkg/microservice/aslan/core/common/service/template/types.go
index 01fdfa934f..5eb0e3b7a1 100644
--- a/pkg/microservice/aslan/core/common/service/template/types.go
+++ b/pkg/microservice/aslan/core/common/service/template/types.go
@@ -62,11 +62,32 @@ type YamlTemplate struct {
Content string `json:"content"`
VariableYaml string `json:"variable_yaml"`
ServiceVariableKVs []*commontypes.ServiceVariableKV `json:"service_variable_kvs"`
+
+ Source string `json:"source,omitempty"`
+ CodehostID int `json:"codehostID,omitempty"`
+ RepoOwner string `json:"repo_owner,omitempty"`
+ Namespace string `json:"namespace,omitempty"`
+ RepoName string `json:"repo,omitempty"`
+ Path string `json:"path,omitempty"`
+ BranchName string `json:"branch,omitempty"`
+ RemoteName string `json:"remote_name,omitempty"`
+ LoadFromDir bool `json:"load_from_dir,omitempty"`
+ Commit *models.Commit `json:"commit,omitempty"`
}
type YamlListObject struct {
- ID string `json:"id"`
- Name string `json:"name"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Source string `json:"source,omitempty"`
+ CodehostID int `json:"codehostID,omitempty"`
+ RepoOwner string `json:"repo_owner,omitempty"`
+ Namespace string `json:"namespace,omitempty"`
+ Repo string `json:"repo,omitempty"`
+ Path string `json:"path,omitempty"`
+ Branch string `json:"branch,omitempty"`
+ RemoteName string `json:"remote_name,omitempty"`
+ LoadFromDir bool `json:"load_from_dir,omitempty"`
+ Commit *models.Commit `json:"commit,omitempty"`
}
type YamlDetail struct {
@@ -75,6 +96,16 @@ type YamlDetail struct {
Content string `json:"content"`
VariableYaml string `json:"variable_yaml"`
ServiceVariableKVs []*commontypes.ServiceVariableKV `json:"service_variable_kvs"`
+ Source string `json:"source,omitempty"`
+ CodehostID int `json:"codehostID,omitempty"`
+ RepoOwner string `json:"repo_owner,omitempty"`
+ Namespace string `json:"namespace,omitempty"`
+ RepoName string `json:"repo_name,omitempty"`
+ Path string `json:"path,omitempty"`
+ BranchName string `json:"branch_name,omitempty"`
+ RemoteName string `json:"remote_name,omitempty"`
+ LoadFromDir bool `json:"load_from_dir,omitempty"`
+ Commit *models.Commit `json:"commit,omitempty"`
}
type ServiceReference struct {
diff --git a/pkg/microservice/aslan/core/common/service/template_store.go b/pkg/microservice/aslan/core/common/service/template_store.go
index 3d1b864d72..77fe62595d 100644
--- a/pkg/microservice/aslan/core/common/service/template_store.go
+++ b/pkg/microservice/aslan/core/common/service/template_store.go
@@ -16,7 +16,18 @@ limitations under the License.
package service
-import "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb"
+import (
+ "fmt"
+
+ "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/webhook"
+ "github.com/koderover/zadig/v2/pkg/setting"
+ "github.com/koderover/zadig/v2/pkg/shared/client/systemconfig"
+ "github.com/koderover/zadig/v2/pkg/tool/gerrit"
+ "github.com/koderover/zadig/v2/pkg/tool/httpclient"
+ "go.uber.org/zap"
+)
func GetDockerfileTemplateContent(id string) (string, error) {
dockerfileTemplate, err := mongodb.NewDockerfileTemplateColl().GetById(id)
@@ -25,3 +36,80 @@ func GetDockerfileTemplateContent(id string) (string, error) {
}
return dockerfileTemplate.Content, nil
}
+
+func ProcessYamlTemplateWebhook(updated, current *models.YamlTemplate, logger *zap.SugaredLogger) error {
+ var updatedHooks, currentHooks []*webhook.WebHook
+ if current != nil {
+ namespace := current.Namespace
+ if namespace == "" {
+ namespace = current.RepoOwner
+ }
+ switch current.Source {
+ case setting.SourceFromGithub, setting.SourceFromGitlab, setting.SourceFromGitee, setting.SourceFromGiteeEE:
+ currentHooks = append(currentHooks, &webhook.WebHook{
+ Owner: current.RepoOwner,
+ Namespace: namespace,
+ Repo: current.RepoName,
+ Name: "trigger",
+ CodeHostID: current.CodeHostID,
+ })
+ case setting.SourceFromGerrit:
+ detail, err := systemconfig.New().GetCodeHost(current.CodeHostID)
+ if err != nil {
+ return err
+ }
+
+ cl := gerrit.NewHTTPClient(detail.Address, detail.AccessToken)
+ if err := cl.DeleteWebhook(current.RepoName, webhook.YamlTemplatePrefix+current.Name); err != nil {
+ logger.Errorf("failed to delete gerrit webhook for yaml template %s, err: %s", current.Name, err)
+ }
+ }
+ }
+ if updated != nil {
+ namespace := updated.Namespace
+ if namespace == "" {
+ namespace = updated.RepoOwner
+ }
+ switch updated.Source {
+ case setting.SourceFromGithub, setting.SourceFromGitlab, setting.SourceFromGitee, setting.SourceFromGiteeEE:
+ updatedHooks = append(updatedHooks, &webhook.WebHook{
+ Owner: updated.RepoOwner,
+ Namespace: namespace,
+ Repo: updated.RepoName,
+ Name: "trigger",
+ CodeHostID: updated.CodeHostID,
+ })
+ case setting.SourceFromGerrit:
+ detail, err := systemconfig.New().GetCodeHost(updated.CodeHostID)
+ if err != nil {
+ return err
+ }
+
+ cl := gerrit.NewHTTPClient(detail.Address, detail.AccessToken)
+ webhookName := webhook.YamlTemplatePrefix + updated.Name
+ webhookURL := fmt.Sprintf("/%s/%s/%s/%s", "a/config/server/webhooks~projects", gerrit.Escape(updated.RepoName), "remotes", webhookName)
+ if _, err := cl.Get(webhookURL); err != nil {
+ gerritWebhook := &gerrit.Webhook{
+ URL: fmt.Sprintf("%s?name=%s", WebHookURL(), webhookName),
+ MaxTries: setting.MaxTries,
+ SslVerify: false,
+ }
+ if _, err = cl.Put(webhookURL, httpclient.SetBody(gerritWebhook)); err != nil {
+ return err
+ }
+ }
+ }
+ }
+
+ name := ""
+ if updated != nil {
+ name = webhook.YamlTemplatePrefix + updated.Name
+ } else if current != nil {
+ name = webhook.YamlTemplatePrefix + current.Name
+ }
+ if len(updatedHooks) == 0 && len(currentHooks) == 0 {
+ return nil
+ }
+
+ return ProcessWebhook(updatedHooks, currentHooks, name, logger)
+}
diff --git a/pkg/microservice/aslan/core/common/service/webhook/client.go b/pkg/microservice/aslan/core/common/service/webhook/client.go
index df6ca43f10..687c0e93a8 100644
--- a/pkg/microservice/aslan/core/common/service/webhook/client.go
+++ b/pkg/microservice/aslan/core/common/service/webhook/client.go
@@ -22,13 +22,14 @@ import (
)
const (
- WorkflowPrefix = "workflow-"
- WorkflowV4Prefix = "workflowv4-"
- PipelinePrefix = "pipeline-"
- ColliePrefix = "collie-"
- ServicePrefix = "service-"
- TestingPrefix = "testing-"
- ScannerPrefix = "scanning-"
+ WorkflowPrefix = "workflow-"
+ WorkflowV4Prefix = "workflowv4-"
+ PipelinePrefix = "pipeline-"
+ ColliePrefix = "collie-"
+ ServicePrefix = "service-"
+ YamlTemplatePrefix = "yaml-template-"
+ TestingPrefix = "testing-"
+ ScannerPrefix = "scanning-"
taskTimeoutSecond = 10
)
diff --git a/pkg/microservice/aslan/core/common/service/webhooknotify/types.go b/pkg/microservice/aslan/core/common/service/webhooknotify/types.go
index ca618d1bf7..2c898d62e9 100644
--- a/pkg/microservice/aslan/core/common/service/webhooknotify/types.go
+++ b/pkg/microservice/aslan/core/common/service/webhooknotify/types.go
@@ -657,3 +657,35 @@ type OpenAPIWorkflowDistributeTarget struct {
// 如果 UpdateTag 为 false,则使用源标签作为目标标签
UpdateTag bool `json:"update_tag"`
}
+
+// TapdJobSpec
+
+type OpenAPIWorkflowTapdJobSpec struct {
+ // Tapd ID
+ TapdID string `json:"tapd_id"`
+ // 类型
+ Type string `json:"type"`
+ // 项目 ID
+ ProjectID string `json:"project_id"`
+ // 项目名称
+ ProjectName string `json:"project_name"`
+ // 源状态
+ SourceStatus config.TapdIterationStatus `json:"source_status"`
+ // 目标状态
+ Status config.TapdIterationStatus `json:"status"`
+ // 迭代列表
+ Iterations []*OpenAPIWorkflowTapdIteration `json:"iterations"`
+}
+
+type OpenAPIWorkflowTapdIteration struct {
+ // 迭代 ID
+ IterationID string `json:"iteration_id"`
+ // 迭代名称
+ IterationName string `json:"iteration_name"`
+ // 开始日期
+ StartDate string `json:"start_date"`
+ // 结束日期
+ EndDate string `json:"end_date"`
+ // 错误信息
+ Error string `json:"error"`
+}
diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_dms.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_dms.go
index cc540a6a0d..8a4aa7126d 100644
--- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_dms.go
+++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_dms.go
@@ -18,6 +18,7 @@ package jobcontroller
import (
"context"
+ "strings"
"time"
dms "github.com/alibabacloud-go/dms-enterprise-20181101/v3/client"
@@ -72,9 +73,18 @@ func (c *DMSJobCtl) Run(ctx context.Context) {
return
}
+ switch normalizeDMSJobExecuteMode(c.jobTaskSpec.ExecuteMode) {
+ case config.DMSJobExecuteModeSerial:
+ c.runSerial(ctx, client)
+ default:
+ c.runParallel(ctx, client)
+ }
+}
+
+func (c *DMSJobCtl) runParallel(ctx context.Context, client *dms.Client) {
failed := false
for _, order := range c.jobTaskSpec.Orders {
- err = execDMSDataCorrectOrder(ctx, client, order.ID)
+ err := execDMSDataCorrectOrder(ctx, client, order.ID)
if err != nil {
failed = true
order.Error = err.Error()
@@ -86,21 +96,18 @@ func (c *DMSJobCtl) Run(ctx context.Context) {
for {
c.ack()
- select {
- case <-ctx.Done():
- c.job.Status = config.StatusCancelled
- logError(c.job, "job cancelled", c.logger)
+ if c.checkCancelled(ctx) {
return
- default:
}
allDone := true
for _, order := range c.jobTaskSpec.Orders {
if order.Error != "" {
+ failed = true
continue
}
- if order.JobStatus == "FAIL" || order.JobStatus == "SUCCESS" || order.JobStatus == "DELETE" {
+ if isDMSOrderDone(order.JobStatus) {
if order.JobStatus == "FAIL" {
failed = true
}
@@ -118,6 +125,9 @@ func (c *DMSJobCtl) Run(ctx context.Context) {
}
order.JobStatus = tea.StringValue(taskDetail.GetJobStatus())
+ if order.JobStatus == "FAIL" {
+ failed = true
+ }
}
if allDone {
@@ -133,6 +143,74 @@ func (c *DMSJobCtl) Run(ctx context.Context) {
}
}
+func (c *DMSJobCtl) runSerial(ctx context.Context, client *dms.Client) {
+ for _, order := range c.jobTaskSpec.Orders {
+ if c.checkCancelled(ctx) {
+ return
+ }
+
+ err := execDMSDataCorrectOrder(ctx, client, order.ID)
+ if err != nil {
+ order.Error = err.Error()
+ logError(c.job, err.Error(), c.logger)
+ c.job.Status = config.StatusFailed
+ return
+ }
+
+ for {
+ c.ack()
+
+ if c.checkCancelled(ctx) {
+ return
+ }
+
+ taskDetail, err := getDMSDataCorrectTaskDetail(ctx, client, order.ID)
+ if err != nil {
+ order.Error = err.Error()
+ logError(c.job, err.Error(), c.logger)
+ c.job.Status = config.StatusFailed
+ return
+ }
+
+ order.JobStatus = tea.StringValue(taskDetail.GetJobStatus())
+ if !isDMSOrderDone(order.JobStatus) {
+ time.Sleep(time.Second * 3)
+ continue
+ }
+ if order.JobStatus == "FAIL" {
+ c.job.Status = config.StatusFailed
+ return
+ }
+ break
+ }
+ }
+ c.job.Status = config.StatusPassed
+}
+
+func (c *DMSJobCtl) checkCancelled(ctx context.Context) bool {
+ select {
+ case <-ctx.Done():
+ c.job.Status = config.StatusCancelled
+ logError(c.job, "job cancelled", c.logger)
+ return true
+ default:
+ return false
+ }
+}
+
+func isDMSOrderDone(status string) bool {
+ return status == "FAIL" || status == "SUCCESS" || status == "DELETE"
+}
+
+func normalizeDMSJobExecuteMode(mode string) config.DMSJobExecuteMode {
+ switch strings.ToLower(mode) {
+ case string(config.DMSJobExecuteModeSerial):
+ return config.DMSJobExecuteModeSerial
+ default:
+ return config.DMSJobExecuteModeParallel
+ }
+}
+
func (c *DMSJobCtl) SaveInfo(ctx context.Context) error {
return mongodb.NewJobInfoColl().Create(context.TODO(), &commonmodels.JobInfo{
Type: c.job.JobType,
diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_helm_chart_deploy.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_helm_chart_deploy.go
index 8ba3c18341..b0ee554ff8 100644
--- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_helm_chart_deploy.go
+++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_helm_chart_deploy.go
@@ -153,6 +153,7 @@ func (c *HelmChartDeployJobCtl) Run(ctx context.Context) {
logError(c.job, err.Error(), c.logger)
return
}
+
break
case <-time.After(time.Second*time.Duration(timeOut) + time.Minute):
err = fmt.Errorf("failed to upgrade relase for service: %s, timeout", deploy.ReleaseName)
diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_helm_deploy.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_helm_deploy.go
index fddbde49ed..b76a8afcdf 100644
--- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_helm_deploy.go
+++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_helm_deploy.go
@@ -276,6 +276,7 @@ func (c *HelmDeployJobCtl) Run(ctx context.Context) {
jobLogManager.SaveJobLog(fmt.Sprintf("Deleting %s %s/%s", resource.Unstructured.GetKind(), c.namespace, resource.Unstructured.GetName()))
}
}
+
for key, newManifest := range newResourceMap {
currentManifest, ok := currentResourceMap[key]
if !ok {
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..5c42464f8a 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
@@ -75,6 +75,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 +117,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 +224,330 @@ func (c *NotificationJobCtl) Run(ctx context.Context) {
return
}
+func (c *NotificationJobCtl) prepareRuntimeNotificationFields() error {
+ keyMap := c.buildRuntimeNotificationKeyMap()
+
+ 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, keyMap)
+ }
+ if cfg := c.jobTaskSpec.DingDingNotificationConfig; cfg != nil {
+ cfg.AtMobiles = renderNotificationStrings(cfg.AtMobiles, keyMap)
+ }
+ if cfg := c.jobTaskSpec.WechatNotificationConfig; cfg != nil {
+ cfg.AtUsers = renderNotificationStrings(cfg.AtUsers, keyMap)
+ }
+ if cfg := c.jobTaskSpec.MSTeamsNotificationConfig; cfg != nil {
+ cfg.AtEmails = renderNotificationStrings(cfg.AtEmails, keyMap)
+ }
+ return c.resolveDynamicRecipients(keyMap)
+}
+
+func (c *NotificationJobCtl) buildRuntimeNotificationKeyMap() map[string]string {
+ keyMap := make(map[string]string)
+
+ insertKVs := func(kvs []*commonmodels.KeyVal) {
+ for _, kv := range kvs {
+ if kv == nil || kv.Key == "" || kv.GetValue() == "" {
+ continue
+ }
+ keyMap[kv.Key] = kv.GetValue()
+ }
+ }
+
+ insertKVs(c.workflowCtx.WorkflowKeyVals)
+ return keyMap
+}
+
+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 {
+ if cfg := c.jobTaskSpec.LarkHookNotificationConfig; cfg != nil {
+ users := c.resolveDynamicRecipientsToDirectValues(cfg.DynamicRecipients, keyMap, "open_id", "user_id", "id")
+ cfg.AtUsers = lo.Uniq(append(cfg.AtUsers, users...))
+ }
+ if cfg := c.jobTaskSpec.LarkGroupNotificationConfig; cfg != nil {
+ users, err := c.resolveDynamicRecipientsToLarkUsers(cfg.DynamicRecipients, cfg.AppID, keyMap)
+ if err != nil {
+ return err
+ }
+ cfg.AtUsers = uniqLarkUsers(append(cfg.AtUsers, users...))
+ }
+ if cfg := c.jobTaskSpec.LarkPersonNotificationConfig; cfg != nil {
+ users, err := c.resolveDynamicRecipientsToLarkUsers(cfg.DynamicRecipients, cfg.AppID, keyMap)
+ if err != nil {
+ return err
+ }
+ cfg.TargetUsers = uniqLarkUsers(append(cfg.TargetUsers, users...))
+ }
+ if cfg := c.jobTaskSpec.MSTeamsNotificationConfig; cfg != nil {
+ emails, err := c.resolveDynamicRecipientsToEmails(cfg.DynamicRecipients, keyMap)
+ if err != nil {
+ return err
+ }
+ cfg.AtEmails = lo.Uniq(append(cfg.AtEmails, emails...))
+ }
+ if cfg := c.jobTaskSpec.MailNotificationConfig; cfg != nil {
+ emails, err := c.resolveDynamicRecipientsToEmails(cfg.DynamicRecipients, keyMap)
+ if err != nil {
+ return err
+ }
+ cfg.TargetUsers = uniqMailUsers(append(cfg.TargetUsers, buildMailUsersFromEmails(emails)...))
+ }
+ if cfg := c.jobTaskSpec.DingDingNotificationConfig; cfg != nil {
+ mobiles, err := c.resolveDynamicRecipientsToMobiles(cfg.DynamicRecipients, keyMap)
+ if err != nil {
+ return err
+ }
+ cfg.AtMobiles = lo.Uniq(append(cfg.AtMobiles, mobiles...))
+ }
+ if cfg := c.jobTaskSpec.WechatNotificationConfig; cfg != nil {
+ users := c.resolveDynamicRecipientsToDirectValues(cfg.DynamicRecipients, keyMap, "user_id", "userid", "id")
+ cfg.AtUsers = lo.Uniq(append(cfg.AtUsers, users...))
+ }
+
+ return nil
+}
+
+func (c *NotificationJobCtl) resolveDynamicRecipientsToLarkUsers(recipients []*commonmodels.DynamicRecipient, appID string, keyMap map[string]string) ([]*lark.UserInfo, error) {
+ if len(recipients) == 0 {
+ return nil, nil
+ }
+
+ client, err := larkservice.GetLarkClientByIMAppID(appID)
+ if err != nil {
+ return nil, err
+ }
+
+ resp := make([]*lark.UserInfo, 0)
+ for _, recipient := range recipients {
+ value := renderNotificationString(recipient.Value, keyMap)
+ if value == "" {
+ continue
+ }
+
+ idType, id, err := resolveLarkRecipient(client, recipient.IdentityType, value)
+ if err != nil {
+ return nil, err
+ }
+ if id == "" {
+ continue
+ }
+ resp = append(resp, &lark.UserInfo{ID: id, IDType: idType})
+ }
+
+ return uniqLarkUsers(resp), nil
+}
+
+func (c *NotificationJobCtl) resolveDynamicRecipientsToEmails(recipients []*commonmodels.DynamicRecipient, keyMap map[string]string) ([]string, error) {
+ resp := make([]string, 0)
+ for _, recipient := range recipients {
+ value := renderNotificationString(recipient.Value, keyMap)
+ if value == "" {
+ continue
+ }
+
+ switch recipient.IdentityType {
+ case "", "email":
+ resp = append(resp, value)
+ case "account":
+ userInfo, err := searchUserByAccount(value)
+ if err != nil {
+ return nil, err
+ }
+ if userInfo != nil && userInfo.Email != "" {
+ resp = append(resp, userInfo.Email)
+ }
+ }
+ }
+ return lo.Uniq(resp), nil
+}
+
+func (c *NotificationJobCtl) resolveDynamicRecipientsToMobiles(recipients []*commonmodels.DynamicRecipient, keyMap map[string]string) ([]string, error) {
+ resp := make([]string, 0)
+ for _, recipient := range recipients {
+ value := renderNotificationString(recipient.Value, keyMap)
+ if value == "" {
+ continue
+ }
+
+ switch recipient.IdentityType {
+ case "mobile":
+ resp = append(resp, value)
+ case "account":
+ userInfo, err := searchUserByAccount(value)
+ if err != nil {
+ return nil, err
+ }
+ if userInfo != nil && userInfo.Phone != "" {
+ resp = append(resp, userInfo.Phone)
+ }
+ }
+ }
+ return lo.Uniq(resp), nil
+}
+
+func (c *NotificationJobCtl) resolveDynamicRecipientsToDirectValues(recipients []*commonmodels.DynamicRecipient, keyMap map[string]string, supportedTypes ...string) []string {
+ if len(recipients) == 0 {
+ return nil
+ }
+ supported := make(map[string]struct{}, len(supportedTypes))
+ for _, identityType := range supportedTypes {
+ supported[identityType] = struct{}{}
+ }
+ resp := make([]string, 0)
+ for _, recipient := range recipients {
+ if _, ok := supported[recipient.IdentityType]; !ok {
+ continue
+ }
+ value := renderNotificationString(recipient.Value, keyMap)
+ if value == "" {
+ continue
+ }
+ resp = append(resp, value)
+ }
+ return lo.Uniq(resp)
+}
+
+func resolveLarkRecipient(client *lark.Client, identityType, value string) (string, string, error) {
+ switch identityType {
+ case "", "email":
+ userInfo, err := client.GetUserIDByEmailOrMobile(lark.QueryTypeEmail, value, setting.LarkUserID)
+ if err != nil {
+ return "", "", err
+ }
+ return setting.LarkUserID, util2.GetStringFromPointer(userInfo.UserId), nil
+ case "mobile":
+ userInfo, err := client.GetUserIDByEmailOrMobile(lark.QueryTypeMobile, value, setting.LarkUserID)
+ if err != nil {
+ return "", "", err
+ }
+ return setting.LarkUserID, util2.GetStringFromPointer(userInfo.UserId), nil
+ case "account":
+ userInfo, err := searchUserByAccount(value)
+ if err != nil {
+ return "", "", err
+ }
+ if userInfo == nil {
+ return "", "", nil
+ }
+ if userInfo.Email != "" {
+ larkUser, err := client.GetUserIDByEmailOrMobile(lark.QueryTypeEmail, userInfo.Email, setting.LarkUserID)
+ if err == nil {
+ return setting.LarkUserID, util2.GetStringFromPointer(larkUser.UserId), nil
+ }
+ }
+ if userInfo.Phone != "" {
+ larkUser, err := client.GetUserIDByEmailOrMobile(lark.QueryTypeMobile, userInfo.Phone, setting.LarkUserID)
+ if err == nil {
+ return setting.LarkUserID, util2.GetStringFromPointer(larkUser.UserId), nil
+ }
+ }
+ return "", "", nil
+ default:
+ return "", "", fmt.Errorf("unsupported lark dynamic recipient identity type: %s", identityType)
+ }
+}
+
+func searchUserByAccount(account string) (*user.User, error) {
+ resp, err := user.New().SearchUser(&user.SearchUserArgs{
+ Account: account,
+ Page: 1,
+ PerPage: 1,
+ })
+ if err != nil {
+ return nil, err
+ }
+ if resp == nil || len(resp.Users) == 0 {
+ return nil, nil
+ }
+ return resp.Users[0], nil
+}
+
+func uniqLarkUsers(users []*lark.UserInfo) []*lark.UserInfo {
+ seen := make(map[string]struct{})
+ resp := make([]*lark.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 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 renderNotificationString(input string, keyMap map[string]string) string {
+ if len(keyMap) == 0 || !strings.Contains(input, "{{.") {
+ return input
+ }
+ pairs := make([]string, 0, len(keyMap)*2)
+ for key, value := range keyMap {
+ pairs = append(pairs, "{{."+key+"}}", value)
+ }
+ return strings.NewReplacer(pairs...).Replace(input)
+}
+
func sendLarkMessage(client *lark.Client, productName, workflowName, workflowDisplayName string, taskID int64, receiverType, receiverID, title, message string, idList []string, isAtAll bool) error {
// first generate lark card
card := instantmessage.NewLarkCard()
@@ -268,6 +609,58 @@ 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,
+ workflowDisplayName,
+ )
+ card.AddI18NElementsZhcnAction("点击查看更多信息", detailURL)
+
+ messageReq := instantmessage.LarkCardReq{
+ MsgType: "interactive",
+ Card: card,
+ }
+ if _, err := httpclient.New().Post(uri, httpclient.SetBody(messageReq)); err != nil {
+ return err
+ }
+
+ if len(idList) == 0 && !isAtAll {
+ return nil
+ }
+
+ atUserList := make([]string, 0, len(idList))
+ idList = lo.Filter(idList, func(s string, _ int) bool { return s != "All" })
+ for _, userID := range idList {
+ atUserList = append(atUserList, fmt.Sprintf("", userID))
+ }
+ atMessage := strings.Join(atUserList, " ")
+ if isAtAll {
+ atMessage += ""
+ }
+
+ if strings.Contains(uri, "bot/v2/hook") {
+ _, err := httpclient.New().Post(uri, httpclient.SetBody(&instantmessage.FeiShuMessageV2{
+ MsgType: "text",
+ Content: instantmessage.FeiShuContentV2{Text: atMessage},
+ }))
+ return err
+ }
+
+ _, err := httpclient.New().Post(uri, httpclient.SetBody(&instantmessage.FeiShuMessage{
+ Title: "",
+ Text: atMessage,
+ }))
+ return err
+}
+
func sendDingDingMessage(productName, workflowName, workflowDisplayName string, taskID int64, uri, title, message string, idList []string, isAtAll bool) error {
processedMessage := generateDingDingNotificationMessage(title, message, idList)
@@ -438,7 +831,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/jobcontroller/job_plugin.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_plugin.go
index e2e3dad2aa..3e5bcf2ff1 100644
--- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_plugin.go
+++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/job_plugin.go
@@ -179,7 +179,7 @@ func (c *PluginJobCtl) complete(ctx context.Context) {
}()
// get job outputs info from pod terminate message.
- if err := getJobOutputFromTerminalMsg(c.jobTaskSpec.Properties.Namespace, c.job.Name, c.job, c.workflowCtx, c.kubeclient); err != nil {
+ if err := getJobOutputFromTerminalMsg(c.jobTaskSpec.Properties.Namespace, c.job, c.workflowCtx, c.kubeclient); err != nil {
c.logger.Error(err)
c.job.Error = err.Error()
}
diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/kubernetes.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/kubernetes.go
index 340cd8509d..e5bd0cce37 100644
--- a/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/kubernetes.go
+++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/jobcontroller/kubernetes.go
@@ -78,6 +78,7 @@ const (
defaultSecretEmail = "bot@koderover.com"
registrySecretSuffix = "-registry-secret"
workflowConfigMapRoleSA = "workflow-cm-sa"
+ outputCollectorContainerName = "job-output-collector"
defaultRetryCount = 3
defaultRetryInterval = time.Second * 3
@@ -215,31 +216,63 @@ func getBaseImage(buildOS, imageFrom string) string {
}
func buildPlainJob(jobName string, resReq setting.Request, resReqSpec setting.RequestSpec, jobTask *commonmodels.JobTask, jobTaskSpec *commonmodels.JobTaskPluginSpec, workflowCtx *commonmodels.WorkflowTaskCtx, customLabels, customAnnotations map[string]string) (*batchv1.Job, error) {
- collectJobOutput := `OLD_IFS=$IFS
-export IFS=","
-files='%s'
-outputs='%s'
-file_arr=($files)
-output_arr=($outputs)
-IFS="$OLD_IFS"
-result="{"
-for i in ${!file_arr[@]};
-do
- file_value=$(cat ${file_arr[$i]})
- output_value=${output_arr[$i]}
- result="$result\"$output_value\":\"$file_value\","
+ collectJobOutput := `output_pairs='%s'
+
+json_escape() {
+ printf '%%s' "$1" | sed ':a;N;$!ba;s/\\/\\\\/g;s/"/\\"/g;s/\n/\\n/g'
+}
+
+result="["
+old_ifs=$IFS
+IFS=','
+for output_pair in $output_pairs; do
+ name=${output_pair%%%%::*}
+ file=${output_pair#*::}
+ value=""
+ [ -f "$file" ] && value=$(cat "$file")
+ result="${result}{\"name\":\"$(json_escape "$name")\",\"value\":\"$(json_escape "$value")\"},"
done
-result=$(sed 's/,$/}/' <<< $result)
-echo $result > %s
-`
- files := []string{}
- outputs := []string{}
+IFS=$old_ifs
+
+[ "$result" = "[" ] && result="[]" || result="${result%%,}]"
+printf '%%s' "$result" > %s
+ `
+ outputFilePairs := []string{}
for _, output := range jobTask.Outputs {
outputFile := path.Join(job.JobOutputDir, output.Name)
- files = append(files, outputFile)
- outputs = append(outputs, output.Name)
- }
- collectJobOutputCommand := fmt.Sprintf(collectJobOutput, strings.Join(files, ","), strings.Join(outputs, ","), job.JobTerminationFile)
+ outputFilePairs = append(outputFilePairs, fmt.Sprintf("%s::%s", output.Name, outputFile))
+ }
+ collectJobOutputFromSidecar := fmt.Sprintf(collectJobOutput, strings.Join(outputFilePairs, ","), job.JobTerminationFile)
+ collectorScript := `output_pairs='%s'
+max_wait=%d
+output_file=%s
+
+if [ -z "$output_pairs" ]; then
+ printf '[]' > "$output_file"
+ exit 0
+fi
+
+all_exist() {
+ old_ifs=$IFS
+ IFS=','
+ for output_pair in $output_pairs; do
+ file=${output_pair#*::}
+ [ -f "$file" ] || { IFS=$old_ifs; return 1; }
+ done
+ IFS=$old_ifs
+ return 0
+}
+
+for _ in $(seq 1 "$max_wait"); do
+ all_exist && break
+ sleep 1
+done
+
+# Give producers a short window to finish file writes.
+sleep 1
+
+%s`
+ collectorScript = fmt.Sprintf(collectorScript, strings.Join(outputFilePairs, ","), jobTaskSpec.Properties.Timeout*60+120, job.JobTerminationFile, collectJobOutputFromSidecar)
labels := getJobLabelsWithCustomizeData(&JobLabel{
JobType: string(jobTask.JobType),
@@ -295,14 +328,7 @@ echo $result > %s
Image: jobTaskSpec.Plugin.Image,
Args: jobTaskSpec.Plugin.Args,
Command: jobTaskSpec.Plugin.Cmds,
- Lifecycle: &corev1.Lifecycle{
- PreStop: &corev1.LifecycleHandler{
- Exec: &corev1.ExecAction{
- Command: []string{"/bin/sh", "-c", collectJobOutputCommand},
- },
- },
- },
- Env: envs,
+ Env: envs,
VolumeMounts: []corev1.VolumeMount{
{
Name: "zadig-context",
@@ -321,6 +347,21 @@ echo $result > %s
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
TerminationMessagePath: job.JobTerminationFile,
},
+ {
+ Name: outputCollectorContainerName,
+ Image: BusyBoxImage,
+ ImagePullPolicy: util.ToPullPolicy(configbase.ImagePullPolicy()),
+ Command: []string{"/bin/sh", "-c"},
+ Args: []string{collectorScript},
+ VolumeMounts: []corev1.VolumeMount{
+ {
+ Name: "zadig-output",
+ MountPath: job.JobOutputDir,
+ },
+ },
+ TerminationMessagePolicy: corev1.TerminationMessageReadFile,
+ TerminationMessagePath: job.JobTerminationFile,
+ },
},
Volumes: []corev1.Volume{
{
@@ -1042,27 +1083,27 @@ func waitJobEndByCheckingConfigMap(ctx context.Context, taskTimeout <-chan time.
}
}
-func getJobOutputFromTerminalMsg(namespace, containerName string, jobTask *commonmodels.JobTask, workflowCtx *commonmodels.WorkflowTaskCtx, kubeClient crClient.Client) error {
+func getJobOutputFromTerminalMsg(namespace string, jobTask *commonmodels.JobTask, workflowCtx *commonmodels.WorkflowTaskCtx, kubeClient crClient.Client) error {
jobLabel := &JobLabel{
JobType: string(jobTask.JobType),
JobName: jobTask.K8sJobName,
}
+
outputs := []*job.JobOutput{}
ls := getJobLabels(jobLabel)
pods, err := getter.ListPods(namespace, labels.Set(ls).AsSelector(), kubeClient)
if err != nil {
return err
}
+
for _, pod := range pods {
ipod := wrapper.Pod(pod)
// only collect succeeed job outputs.
if !ipod.Succeeded() {
return nil
}
+
for _, containerStatus := range pod.Status.ContainerStatuses {
- if containerStatus.Name != containerName {
- continue
- }
if containerStatus.State.Terminated != nil && len(containerStatus.State.Terminated.Message) != 0 {
if err := json.Unmarshal([]byte(containerStatus.State.Terminated.Message), &outputs); err != nil {
return err
@@ -1070,6 +1111,7 @@ func getJobOutputFromTerminalMsg(namespace, containerName string, jobTask *commo
}
}
}
+
writeOutputs(outputs, jobTask.Key, workflowCtx)
return nil
}
diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/stage.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/stage.go
index 7b34234e4f..33c68544b3 100644
--- a/pkg/microservice/aslan/core/common/service/workflowcontroller/stage.go
+++ b/pkg/microservice/aslan/core/common/service/workflowcontroller/stage.go
@@ -26,6 +26,7 @@ import (
"github.com/koderover/zadig/v2/pkg/microservice/aslan/config"
commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models"
approvalservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/approval"
+ "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/instantmessage"
)
type StageCtl interface {
@@ -94,6 +95,13 @@ func waitForManualExec(ctx context.Context, stage *commonmodels.StageTask, workf
}
stage.Status = config.StatusPause
+ if !stage.ManualExec.NotificationSent {
+ if err := instantmessage.NewWeChatClient().SendManualExecStageNotifications(workflowCtx, stage); err != nil {
+ logger.Errorf("failed to send manual execution stage notification for stage %s: %v", stage.Name, err)
+ } else {
+ stage.ManualExec.NotificationSent = true
+ }
+ }
return true, err
}
diff --git a/pkg/microservice/aslan/core/common/service/workflowcontroller/workflow.go b/pkg/microservice/aslan/core/common/service/workflowcontroller/workflow.go
index 27bc169d66..2312025619 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"
- "github.com/koderover/zadig/v2/pkg/tool/clientmanager"
- "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,13 +34,19 @@ 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"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
"github.com/koderover/zadig/v2/pkg/tool/kube/getter"
"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 (
@@ -238,6 +239,7 @@ func (c *workflowCtl) Run(ctx context.Context, concurrency int) {
WorkflowTaskCreatorUserID: c.workflowTask.TaskCreatorID,
WorkflowTaskCreatorMobile: c.workflowTask.TaskCreatorPhone,
WorkflowTaskCreatorEmail: c.workflowTask.TaskCreatorEmail,
+ WorkflowKeyVals: commonutil.BuildWorkflowRuntimeVariableKVs(c.workflowTask.WorkflowArgs, c.workflowTask.ProjectName, c.workflowTask.ProjectDisplayName, c.workflowTask.TaskID, c.workflowTask.TaskCreator, c.workflowTask.TaskCreatorAccount, c.workflowTask.TaskCreatorID, time.Unix(c.workflowTask.StartTime, 0)),
Workspace: "/workspace",
DistDir: fmt.Sprintf("%s/%s/dist/%d", config.S3StoragePath(), c.workflowTask.WorkflowName, c.workflowTask.TaskID),
DockerMountDir: fmt.Sprintf("/tmp/%s/docker/%d", uuid.NewString(), time.Now().Unix()),
diff --git a/pkg/microservice/aslan/core/common/util/error_formatter.go b/pkg/microservice/aslan/core/common/util/error_formatter.go
new file mode 100644
index 0000000000..958ffd9adc
--- /dev/null
+++ b/pkg/microservice/aslan/core/common/util/error_formatter.go
@@ -0,0 +1,117 @@
+/*
+Copyright 2026 The KodeRover Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package util
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "go.mongodb.org/mongo-driver/mongo"
+
+ e "github.com/koderover/zadig/v2/pkg/tool/errors"
+)
+
+// FormatCodeHostError formats code host errors into generic user-friendly messages.
+// It identifies common error types (401, 403, 404, 429, timeout, etc.) and
+// returns appropriate descriptions for frontend display.
+func FormatCodeHostError(err error) string {
+ if err == nil {
+ return ""
+ }
+
+ if errors.Is(err, mongo.ErrNoDocuments) {
+ return "Code host is not configured or does not exist"
+ }
+
+ var httpErr *e.HTTPError
+ if errors.As(err, &httpErr) && httpErr.Desc() != "" {
+ raw := strings.ToLower(httpErr.Desc())
+ if msg := matchErrorPattern(raw, "Resource not found"); msg != "" {
+ return msg
+ }
+ }
+
+ raw := strings.ToLower(err.Error())
+ if msg := matchErrorPattern(raw, "Resource not found"); msg != "" {
+ return msg
+ }
+
+ return ""
+}
+
+// FormatRepoInfoError formats repo info lookup errors into repo-specific user-facing messages.
+func FormatRepoInfoError(err error) string {
+ if err == nil {
+ return ""
+ }
+
+ if errors.Is(err, mongo.ErrNoDocuments) {
+ return "Code host is not configured or does not exist"
+ }
+
+ var httpErr *e.HTTPError
+ if errors.As(err, &httpErr) && httpErr.Desc() != "" {
+ raw := strings.ToLower(httpErr.Desc())
+ if msg := matchErrorPattern(raw, "Repository not found or access denied"); msg != "" {
+ return msg
+ }
+ }
+
+ raw := strings.ToLower(err.Error())
+ if msg := matchErrorPattern(raw, "Repository not found or access denied"); msg != "" {
+ return msg
+ }
+
+ return "Failed to fetch repository metadata"
+}
+
+// FormatCodeHostErrorWithDefault formats code host errors with a default description.
+// It appends the specific error detail to the default description.
+func FormatCodeHostErrorWithDefault(defaultDesc string, err error) string {
+ if err == nil {
+ return defaultDesc
+ }
+
+ detail := FormatCodeHostError(err)
+ if detail == "" {
+ return defaultDesc
+ }
+
+ base := strings.TrimSpace(strings.TrimRight(defaultDesc, "."))
+ return fmt.Sprintf("%s. %s", base, detail)
+}
+
+// matchErrorPattern matches common error patterns and returns user-friendly messages.
+func matchErrorPattern(raw, notFoundMsg string) string {
+ switch {
+ case strings.Contains(raw, "401"), strings.Contains(raw, "unauthorized"):
+ return "Authentication failed, please check the access credentials"
+ case strings.Contains(raw, "403"), strings.Contains(raw, "forbidden"):
+ return "Permission denied"
+ case strings.Contains(raw, "404"), strings.Contains(raw, "not found"):
+ return notFoundMsg
+ case strings.Contains(raw, "429"), strings.Contains(raw, "rate limit"):
+ return "Rate limit exceeded"
+ case strings.Contains(raw, "deadline exceeded"), strings.Contains(raw, "timeout"):
+ return "Connection timeout"
+ case strings.Contains(raw, "no documents"), strings.Contains(raw, "not exist"):
+ return "Code host is not configured or does not exist"
+ default:
+ return ""
+ }
+}
diff --git a/pkg/microservice/aslan/core/common/util/lark.go b/pkg/microservice/aslan/core/common/util/lark.go
new file mode 100644
index 0000000000..1634dca961
--- /dev/null
+++ b/pkg/microservice/aslan/core/common/util/lark.go
@@ -0,0 +1,51 @@
+package util
+
+import (
+ "fmt"
+
+ "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"
+ "github.com/koderover/zadig/v2/pkg/tool/lark"
+ "k8s.io/apimachinery/pkg/util/sets"
+)
+
+func ConvertLarkUserGroupToUser(larkApprovalID string, groups []*models.LarkApprovalGroup) ([]*lark.UserInfo, error) {
+ userSet := sets.NewString()
+ users := make([]*lark.UserInfo, 0)
+ for _, group := range groups {
+ userGroup, err := larkservice.GetLarkUserGroup(larkApprovalID, group.GroupID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get lark user group: %s", err)
+ }
+
+ if userGroup.MemberUserCount > 0 {
+ userInfos, err := larkservice.GetLarkUserGroupMembersInfo(larkApprovalID, group.GroupID, "user", setting.LarkUserOpenID, "")
+ if err != nil {
+ return nil, fmt.Errorf("failed to get lark department user infos: %s", err)
+ }
+
+ for _, user := range userInfos {
+ if !userSet.Has(user.ID) {
+ users = append(users, user)
+ userSet.Insert(user.ID)
+ }
+ }
+ }
+
+ if userGroup.MemberDepartmentCount > 0 {
+ userInfos, err := larkservice.GetLarkUserGroupMembersInfo(larkApprovalID, group.GroupID, "department", setting.LarkDepartmentID, "")
+ if err != nil {
+ return nil, fmt.Errorf("failed to get lark department user infos: %s", err)
+ }
+
+ for _, user := range userInfos {
+ if !userSet.Has(user.ID) {
+ users = append(users, user)
+ userSet.Insert(user.ID)
+ }
+ }
+ }
+ }
+ return users, nil
+}
diff --git a/pkg/microservice/aslan/core/common/util/validate.go b/pkg/microservice/aslan/core/common/util/validate.go
index 23fc1d37bb..8a6efbdec6 100644
--- a/pkg/microservice/aslan/core/common/util/validate.go
+++ b/pkg/microservice/aslan/core/common/util/validate.go
@@ -23,7 +23,7 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
"github.com/koderover/zadig/v2/pkg/setting"
- "github.com/koderover/zadig/v2/pkg/shared/client/plutusvendor"
+ "github.com/koderover/zadig/v2/pkg/shared/client/plutusenterprise"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
)
@@ -57,7 +57,7 @@ func CheckDefineResourceParam(req setting.Request, reqSpec setting.RequestSpec)
}
func CheckZadigProfessionalLicense() error {
- licenseStatus, err := plutusvendor.New().CheckZadigXLicenseStatus()
+ licenseStatus, err := plutusenterprise.New().CheckZadigXLicenseStatus()
if err != nil {
return fmt.Errorf("failed to validate zadig license status, error: %s", err)
}
@@ -68,7 +68,7 @@ func CheckZadigProfessionalLicense() error {
}
func CheckZadigEnterpriseLicense() error {
- licenseStatus, err := plutusvendor.New().CheckZadigXLicenseStatus()
+ licenseStatus, err := plutusenterprise.New().CheckZadigXLicenseStatus()
if err != nil {
return fmt.Errorf("failed to validate zadig license status, error: %s", err)
}
@@ -78,25 +78,25 @@ func CheckZadigEnterpriseLicense() error {
return nil
}
-func ValidateZadigProfessionalLicense(licenseStatus *plutusvendor.ZadigXLicenseStatus) bool {
- if !((licenseStatus.Type == plutusvendor.ZadigSystemTypeProfessional ||
- licenseStatus.Type == plutusvendor.ZadigSystemTypeEnterprise) &&
- licenseStatus.Status == plutusvendor.ZadigXLicenseStatusNormal) {
+func ValidateZadigProfessionalLicense(licenseStatus *plutusenterprise.ZadigXLicenseStatus) bool {
+ if !((licenseStatus.Type == plutusenterprise.ZadigSystemTypeProfessional ||
+ licenseStatus.Type == plutusenterprise.ZadigSystemTypeEnterprise) &&
+ licenseStatus.Status == plutusenterprise.ZadigXLicenseStatusNormal) {
return false
}
return true
}
-func ValidateZadigEnterpriseLicense(licenseStatus *plutusvendor.ZadigXLicenseStatus) bool {
- if !(licenseStatus.Type == plutusvendor.ZadigSystemTypeEnterprise &&
- licenseStatus.Status == plutusvendor.ZadigXLicenseStatusNormal) {
+func ValidateZadigEnterpriseLicense(licenseStatus *plutusenterprise.ZadigXLicenseStatus) bool {
+ if !(licenseStatus.Type == plutusenterprise.ZadigSystemTypeEnterprise &&
+ licenseStatus.Status == plutusenterprise.ZadigXLicenseStatusNormal) {
return false
}
return true
}
func CheckZadigLicenseFeatureSae() error {
- licenseStatus, err := plutusvendor.New().CheckZadigXLicenseStatus()
+ licenseStatus, err := plutusenterprise.New().CheckZadigXLicenseStatus()
if err != nil {
return fmt.Errorf("failed to validate zadig license status, error: %s", err)
}
@@ -104,7 +104,7 @@ func CheckZadigLicenseFeatureSae() error {
return e.ErrLicenseInvalid.AddDesc("")
}
for _, feature := range licenseStatus.Features {
- if feature == plutusvendor.ZadigLicenseFeatureSae {
+ if feature == plutusenterprise.ZadigLicenseFeatureSae {
return nil
}
}
@@ -112,7 +112,7 @@ func CheckZadigLicenseFeatureSae() error {
}
func CheckZadigLicenseFeatureDelivery() error {
- licenseStatus, err := plutusvendor.New().CheckZadigXLicenseStatus()
+ licenseStatus, err := plutusenterprise.New().CheckZadigXLicenseStatus()
if err != nil {
return fmt.Errorf("failed to validate zadig license status, error: %s", err)
}
@@ -120,7 +120,7 @@ func CheckZadigLicenseFeatureDelivery() error {
return e.ErrLicenseInvalid.AddDesc("")
}
for _, feature := range licenseStatus.Features {
- if feature == plutusvendor.ZadigLicenseFeatureDelivery {
+ if feature == plutusenterprise.ZadigLicenseFeatureDelivery {
return nil
}
}
diff --git a/pkg/microservice/aslan/core/common/util/workflow.go b/pkg/microservice/aslan/core/common/util/workflow.go
index 15213f8bf2..e7207c6ff3 100644
--- a/pkg/microservice/aslan/core/common/util/workflow.go
+++ b/pkg/microservice/aslan/core/common/util/workflow.go
@@ -18,11 +18,15 @@ package util
import (
"fmt"
+ "regexp"
+ "strings"
commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models"
"github.com/koderover/zadig/v2/pkg/setting"
)
+var workflowJobNameRegx = regexp.MustCompile(setting.JobNameRegx)
+
func CalcWorkflowTaskRunningTime(task *commonmodels.WorkflowTask) int64 {
runningTime := int64(0)
for _, stage := range task.Stages {
@@ -44,3 +48,22 @@ func GenScanningWorkflowName(scanningID string) string {
func GenTestingWorkflowName(testingName string) string {
return fmt.Sprintf(setting.TestWorkflowNamingConvention, testingName)
}
+
+func GenerateTestingModuleJobName(name string) string {
+ return strings.ToLower(name)
+}
+
+func GenerateScanningModuleJobName(name string) string {
+ if len(name) >= 32 {
+ return strings.TrimSuffix(name[:31], "-")
+ }
+ return name
+}
+
+func ValidateGeneratedWorkflowJobName(name string, generator func(string) string) error {
+ jobName := generator(name)
+ if !workflowJobNameRegx.MatchString(jobName) {
+ return fmt.Errorf("name [%s] cannot be used to generate a workflow job name, generated job name [%s] did not match %s", name, jobName, setting.JobNameRegx)
+ }
+ return 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..b66f703ac0
--- /dev/null
+++ b/pkg/microservice/aslan/core/common/util/workflow_variables.go
@@ -0,0 +1,249 @@
+package util
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ configbase "github.com/koderover/zadig/v2/pkg/config"
+ commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models"
+ "github.com/koderover/zadig/v2/pkg/types"
+)
+
+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 RepoVariableKVs(repos []*types.Repository) []*commonmodels.KeyVal {
+ ret := make([]*commonmodels.KeyVal, 0)
+ for index, repo := range repos {
+ repoNameIndex := fmt.Sprintf("REPONAME_%d", index)
+ ret = append(ret, &commonmodels.KeyVal{Key: repoNameIndex, Value: repo.RepoName, IsCredential: false})
+
+ repoIndex := fmt.Sprintf("REPO_%d", index)
+ repoName := RepoNameToRepoIndex(repo.RepoName)
+ ret = append(ret, &commonmodels.KeyVal{Key: repoIndex, Value: repoName, IsCredential: false})
+
+ if len(repo.Branch) > 0 {
+ ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_BRANCH", repoName), Value: repo.Branch, IsCredential: false})
+ }
+
+ if len(repo.Tag) > 0 {
+ ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_TAG", repoName), Value: repo.Tag, IsCredential: false})
+ }
+
+ if repo.PR > 0 {
+ ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PR", repoName), Value: strconv.Itoa(repo.PR), IsCredential: false})
+ }
+
+ ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PRE_MERGE_BRANCHES", repoName), Value: repo.GetPreMergeBranches(), IsCredential: false})
+ ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_ORG", repoName), Value: repo.RepoOwner, IsCredential: false})
+
+ if len(repo.PRs) > 0 {
+ prStrs := []string{}
+ for _, pr := range repo.PRs {
+ prStrs = append(prStrs, strconv.Itoa(pr))
+ }
+ ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PR", repoName), Value: strings.Join(prStrs, ","), IsCredential: false})
+ }
+
+ if len(repo.CommitID) > 0 {
+ ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_COMMIT_ID", repoName), Value: repo.CommitID, IsCredential: false})
+ }
+ if len(repo.AuthorName) > 0 {
+ ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_AUTHOR", repoName), Value: repo.AuthorName, IsCredential: false})
+ }
+ if len(repo.Committer) > 0 {
+ ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_COMMITTER", repoName), Value: repo.Committer, IsCredential: false})
+ }
+ if len(repo.CommitMessage) > 0 {
+ ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_COMMIT_MESSAGE", repoName), Value: repo.CommitMessage, IsCredential: false})
+ }
+ if len(repo.TargetBranch) > 0 {
+ ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_TARGET_BRANCH", repoName), Value: repo.TargetBranch, IsCredential: false})
+ }
+ }
+ return ret
+}
+
+func RepoNameToRepoIndex(repoName string) string {
+ words := map[rune]string{
+ '0': "A", '1': "B", '2': "C", '3': "D", '4': "E",
+ '5': "F", '6': "G", '7': "H", '8': "I", '9': "J",
+ }
+ result := ""
+ for i, digit := range repoName {
+ if word, ok := words[digit]; ok {
+ result += word
+ } else {
+ result += repoName[i:]
+ break
+ }
+ }
+
+ result = strings.ReplaceAll(result, "-", "_")
+ result = strings.ReplaceAll(result, ".", "_")
+ return result
+}
+
+func CollectWorkflowRepos(workflow *commonmodels.WorkflowV4) []*types.Repository {
+ if workflow == nil {
+ return nil
+ }
+
+ resp := make([]*types.Repository, 0)
+ repoKeySet := make(map[string]struct{})
+ appendRepo := func(repo *types.Repository) {
+ if repo == nil {
+ return
+ }
+ key := fmt.Sprintf("%d/%s/%s/%s/%s/%d", repo.CodehostID, repo.RepoOwner, repo.RepoNamespace, repo.RepoName, repo.Branch, repo.PR)
+ if _, ok := repoKeySet[key]; ok {
+ return
+ }
+ repoKeySet[key] = struct{}{}
+ resp = append(resp, repo)
+ }
+
+ for _, stage := range workflow.Stages {
+ for _, jobInfo := range stage.Jobs {
+ switch spec := jobInfo.Spec.(type) {
+ case *commonmodels.ZadigBuildJobSpec:
+ for _, build := range spec.ServiceAndBuilds {
+ for _, repo := range build.Repos {
+ appendRepo(repo)
+ }
+ }
+ case *commonmodels.ZadigTestingJobSpec:
+ for _, testModule := range spec.TestModules {
+ for _, repo := range testModule.Repos {
+ appendRepo(repo)
+ }
+ }
+ for _, serviceTest := range spec.ServiceAndTests {
+ for _, repo := range serviceTest.Repos {
+ appendRepo(repo)
+ }
+ }
+ case *commonmodels.ZadigScanningJobSpec:
+ for _, scanning := range spec.Scannings {
+ for _, repo := range scanning.Repos {
+ appendRepo(repo)
+ }
+ }
+ for _, serviceScanning := range spec.ServiceAndScannings {
+ for _, repo := range serviceScanning.Repos {
+ appendRepo(repo)
+ }
+ }
+ }
+ }
+ }
+
+ return resp
+}
+
+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, BuildPayloadVariables(workflow.HookPayload.RawPayload)...)
+ }
+
+ return resp
+}
+
+func BuildWorkflowRuntimeVariableKVs(workflow *commonmodels.WorkflowV4, projectName, projectDisplayName string, taskID int64, creator, account, uid string, now time.Time) []*commonmodels.KeyVal {
+ resp := BuildWorkflowSystemVariableKVs(workflow, projectName, projectDisplayName, taskID, creator, account, uid, now)
+ if workflow == nil {
+ return resp
+ }
+ resp = append(resp, RepoVariableKVs(CollectWorkflowRepos(workflow))...)
+
+ return resp
+}
+
+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/common/util/workflownotify/job_content.go b/pkg/microservice/aslan/core/common/util/workflownotify/job_content.go
new file mode 100644
index 0000000000..fffb0a57fd
--- /dev/null
+++ b/pkg/microservice/aslan/core/common/util/workflownotify/job_content.go
@@ -0,0 +1,300 @@
+/*
+Copyright 2026 The KodeRover Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package workflownotify
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/koderover/zadig/v2/pkg/microservice/aslan/config"
+ "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models"
+ "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/webhooknotify"
+ "github.com/koderover/zadig/v2/pkg/setting"
+ "github.com/koderover/zadig/v2/pkg/types"
+ jobspec "github.com/koderover/zadig/v2/pkg/types/job"
+ "github.com/koderover/zadig/v2/pkg/types/step"
+)
+
+type RenderJobTemplate func(tpl string, job *models.JobTask) (string, error)
+type TestResultGetter func(jobName string) (string, error)
+type SonarMetricsGetter func(jobSpec *models.JobTaskFreestyleSpec) (string, string, error)
+
+type BuildJobContentsArgs struct {
+ Task *models.WorkflowTask
+ Stages []*models.StageTask
+ WebHookType setting.NotifyWebHookType
+ RenderTemplate RenderJobTemplate
+ GetTestResult TestResultGetter
+ GetSonarMetrics SonarMetricsGetter
+}
+
+func BuildWorkflowJobContents(args *BuildJobContentsArgs) ([]string, []*webhooknotify.WorkflowNotifyStage, error) {
+ if args == nil || args.Task == nil || len(args.Stages) == 0 {
+ return nil, nil, nil
+ }
+ if args.RenderTemplate == nil {
+ return nil, nil, fmt.Errorf("render template callback is required")
+ }
+
+ jobContents := make([]string, 0)
+ workflowNotifyStages := make([]*webhooknotify.WorkflowNotifyStage, 0, len(args.Stages))
+
+ for _, stage := range args.Stages {
+ if stage == nil {
+ continue
+ }
+
+ workflowNotifyStage := &webhooknotify.WorkflowNotifyStage{
+ Name: stage.Name,
+ Status: stage.Status,
+ StartTime: stage.StartTime,
+ EndTime: stage.EndTime,
+ Error: stage.Error,
+ }
+
+ for _, job := range stage.Jobs {
+ if job == nil {
+ continue
+ }
+
+ workflowNotifyJob := &webhooknotify.WorkflowNotifyJobTask{
+ Name: job.Name,
+ DisplayName: job.DisplayName,
+ JobType: job.JobType,
+ Status: job.Status,
+ StartTime: job.StartTime,
+ EndTime: job.EndTime,
+ Error: job.Error,
+ }
+
+ jobTplcontent := "{{if and (ne .WebHookType \"feishu\") (ne .WebHookType \"feishu_app\") (ne .WebHookType \"feishu_person\")}}\n\n{{end}}{{if eq .WebHookType \"dingding\"}}---\n\n##### {{end}}**{{jobType .Job.JobType }}**: {{.Job.DisplayName}} **{{getText \"notificationTextStatus\"}}**: {{taskStatus .Job.Status }} \n"
+ mailJobTplcontent := "{{jobType .Job.JobType }}:{{.Job.DisplayName}} {{getText \"notificationTextStatus\"}}:{{taskStatus .Job.Status }} \n"
+
+ switch job.JobType {
+ case string(config.JobZadigBuild), string(config.JobFreestyle):
+ jobSpec := &models.JobTaskFreestyleSpec{}
+ models.IToi(job.Spec, jobSpec)
+
+ workflowNotifyJobTaskSpec := &webhooknotify.WorkflowNotifyJobTaskBuildSpec{}
+
+ repos := []*types.Repository{}
+ for _, stepTask := range jobSpec.Steps {
+ if stepTask.StepType == config.StepGit {
+ stepSpec := &step.StepGitSpec{}
+ models.IToi(stepTask.Spec, stepSpec)
+ repos = stepSpec.Repos
+ }
+ }
+
+ branchTag, commitID, gitCommitURL := "", "", ""
+ commitMsgs := []string{}
+ var prInfoList []string
+ var prInfo string
+ for idx, buildRepo := range repos {
+ workflowNotifyRepository := &webhooknotify.WorkflowNotifyRepository{
+ Source: buildRepo.Source,
+ RepoOwner: buildRepo.RepoOwner,
+ RepoNamespace: buildRepo.RepoNamespace,
+ RepoName: buildRepo.RepoName,
+ Branch: buildRepo.Branch,
+ Tag: buildRepo.Tag,
+ AuthorName: buildRepo.AuthorName,
+ CommitID: buildRepo.CommitID,
+ CommitMessage: buildRepo.CommitMessage,
+ }
+ if idx == 0 || buildRepo.IsPrimary {
+ branchTag = buildRepo.Branch
+ if buildRepo.Tag != "" {
+ branchTag = buildRepo.Tag
+ }
+ if len(buildRepo.CommitID) > 8 {
+ commitID = buildRepo.CommitID[0:8]
+ }
+ var prLinkBuilder func(baseURL, owner, repoName string, prID int) string
+ switch buildRepo.Source {
+ case types.ProviderGithub:
+ prLinkBuilder = func(baseURL, owner, repoName string, prID int) string {
+ return fmt.Sprintf("%s/%s/%s/pull/%d", baseURL, owner, repoName, prID)
+ }
+ case types.ProviderGitee:
+ prLinkBuilder = func(baseURL, owner, repoName string, prID int) string {
+ return fmt.Sprintf("%s/%s/%s/pulls/%d", baseURL, owner, repoName, prID)
+ }
+ case types.ProviderGitlab:
+ prLinkBuilder = func(baseURL, owner, repoName string, prID int) string {
+ return fmt.Sprintf("%s/%s/%s/merge_requests/%d", baseURL, owner, repoName, prID)
+ }
+ case types.ProviderGerrit:
+ prLinkBuilder = func(baseURL, owner, repoName string, prID int) string {
+ return fmt.Sprintf("%s/%d", baseURL, prID)
+ }
+ default:
+ prLinkBuilder = func(baseURL, owner, repoName string, prID int) string {
+ return ""
+ }
+ }
+ prInfoList = []string{}
+ sort.Ints(buildRepo.PRs)
+ for _, id := range buildRepo.PRs {
+ link := prLinkBuilder(buildRepo.Address, buildRepo.RepoOwner, buildRepo.RepoName, id)
+ if link != "" {
+ prInfoList = append(prInfoList, fmt.Sprintf("[#%d](%s)", id, link))
+ }
+ }
+ commitMsg := strings.Trim(buildRepo.CommitMessage, "\n")
+ commitMsgs = strings.Split(commitMsg, "\n")
+ gitCommitURL = fmt.Sprintf("%s/%s/%s/commit/%s", buildRepo.Address, buildRepo.RepoOwner, buildRepo.RepoName, commitID)
+ workflowNotifyRepository.CommitURL = gitCommitURL
+ }
+
+ workflowNotifyJobTaskSpec.Repositories = append(workflowNotifyJobTaskSpec.Repositories, workflowNotifyRepository)
+ }
+ if len(prInfoList) != 0 {
+ prInfo = strings.Join(prInfoList, " ") + " "
+ }
+
+ image := ""
+ imageContextKey := strings.Join(strings.Split(jobspec.GetJobOutputKey(job.Key, "IMAGE"), "."), "@?")
+ if args.Task.GlobalContext != nil {
+ image = args.Task.GlobalContext[imageContextKey]
+ }
+ if len(commitID) > 0 {
+ jobTplcontent += fmt.Sprintf("{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextRepositoryInfo\"}}**:%s %s[%s](%s) ", branchTag, prInfo, commitID, gitCommitURL)
+ jobTplcontent += "{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextCommitMessage\"}}**:"
+ mailJobTplcontent += fmt.Sprintf("{{getText \"notificationTextRepositoryInfo\"}}:%s %s[%s]( %s ) ", branchTag, prInfo, commitID, gitCommitURL)
+ if len(commitMsgs) == 1 {
+ jobTplcontent += fmt.Sprintf("%s \n", commitMsgs[0])
+ } else {
+ jobTplcontent += "\n"
+ for _, commitMsg := range commitMsgs {
+ jobTplcontent += fmt.Sprintf("%s \n", commitMsg)
+ }
+ }
+ }
+ if job.Status == config.StatusPassed && image != "" && !strings.HasPrefix(image, "{{.") && !strings.Contains(image, "}}") {
+ jobTplcontent += fmt.Sprintf("{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextImageInfo\"}}**:%s \n", image)
+ mailJobTplcontent += fmt.Sprintf("{{getText \"notificationTextImageInfo\"}}:%s \n", image)
+ workflowNotifyJobTaskSpec.Image = image
+ }
+
+ workflowNotifyJob.Spec = workflowNotifyJobTaskSpec
+
+ case string(config.JobZadigDeploy):
+ jobSpec := &models.JobTaskDeploySpec{}
+ models.IToi(job.Spec, jobSpec)
+ jobTplcontent += fmt.Sprintf("{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextEnvironment\"}}**:%s \n", jobSpec.Env)
+ mailJobTplcontent += fmt.Sprintf("{{getText \"notificationTextEnvironment\"}}:%s \n", jobSpec.Env)
+
+ if job.Status == config.StatusPassed && len(jobSpec.ServiceAndImages) > 0 {
+ jobTplcontent += fmt.Sprintf("{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextImageInfo\"}}**: \n")
+ mailJobTplcontent += fmt.Sprintf("{{getText \"notificationTextImageInfo\"}}: \n")
+ }
+
+ serviceModules := make([]*webhooknotify.WorkflowNotifyDeployServiceModule, 0, len(jobSpec.ServiceAndImages))
+ for _, serviceAndImage := range jobSpec.ServiceAndImages {
+ if job.Status == config.StatusPassed && !strings.HasPrefix(serviceAndImage.Image, "{{.") && !strings.Contains(serviceAndImage.Image, "}}") {
+ jobTplcontent += fmt.Sprintf("%s \n", serviceAndImage.Image)
+ mailJobTplcontent += fmt.Sprintf("%s \n", serviceAndImage.Image)
+ }
+
+ serviceModules = append(serviceModules, &webhooknotify.WorkflowNotifyDeployServiceModule{
+ ServiceModule: serviceAndImage.ServiceModule,
+ Image: serviceAndImage.Image,
+ })
+ }
+
+ workflowNotifyJob.Spec = &webhooknotify.WorkflowNotifyJobTaskDeploySpec{
+ Env: jobSpec.Env,
+ ServiceName: jobSpec.ServiceName,
+ ServiceModules: serviceModules,
+ }
+
+ case string(config.JobZadigHelmDeploy):
+ jobSpec := &models.JobTaskHelmDeploySpec{}
+ models.IToi(job.Spec, jobSpec)
+ jobTplcontent += fmt.Sprintf("{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextEnvironment\"}}**:%s \n", jobSpec.Env)
+ mailJobTplcontent += fmt.Sprintf("{{getText \"notificationTextEnvironment\"}}:%s \n", jobSpec.Env)
+
+ if job.Status == config.StatusPassed && len(jobSpec.ImageAndModules) > 0 {
+ jobTplcontent += fmt.Sprintf("{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextImageInfo\"}}**: \n")
+ mailJobTplcontent += fmt.Sprintf("{{getText \"notificationTextImageInfo\"}}: \n")
+ }
+
+ serviceModules := make([]*webhooknotify.WorkflowNotifyDeployServiceModule, 0, len(jobSpec.ImageAndModules))
+ for _, serviceAndImage := range jobSpec.ImageAndModules {
+ if !strings.HasPrefix(serviceAndImage.Image, "{{.") && !strings.Contains(serviceAndImage.Image, "}}") {
+ jobTplcontent += fmt.Sprintf("%s \n", serviceAndImage.Image)
+ mailJobTplcontent += fmt.Sprintf("%s \n", serviceAndImage.Image)
+ }
+
+ serviceModules = append(serviceModules, &webhooknotify.WorkflowNotifyDeployServiceModule{
+ ServiceModule: serviceAndImage.ServiceModule,
+ Image: serviceAndImage.Image,
+ })
+ }
+
+ workflowNotifyJob.Spec = &webhooknotify.WorkflowNotifyJobTaskDeploySpec{
+ Env: jobSpec.Env,
+ ServiceName: jobSpec.ServiceName,
+ ServiceModules: serviceModules,
+ }
+
+ case string(config.JobZadigTesting):
+ if args.GetTestResult == nil {
+ return nil, nil, fmt.Errorf("test result callback is required")
+ }
+ testResult, err := args.GetTestResult(job.Name)
+ if err != nil {
+ return nil, nil, err
+ }
+ jobTplcontent += fmt.Sprintf("{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextTestResult\"}}**: %s \n", testResult)
+ mailJobTplcontent += fmt.Sprintf("{{getText \"notificationTextTestResult\"}}: %s \n", testResult)
+
+ case string(config.JobZadigScanning):
+ if args.GetSonarMetrics == nil {
+ return nil, nil, fmt.Errorf("sonar metrics callback is required")
+ }
+ jobSpec := &models.JobTaskFreestyleSpec{}
+ models.IToi(job.Spec, jobSpec)
+ sonarMetricsText, mailSonarMetricsText, err := args.GetSonarMetrics(jobSpec)
+ if err != nil {
+ return nil, nil, err
+ }
+ if sonarMetricsText != "" {
+ jobTplcontent += fmt.Sprintf("{{if eq .WebHookType \"dingding\"}}##### {{end}}**{{getText \"notificationTextSonarMetrics\"}}**: %s \n", sonarMetricsText)
+ mailJobTplcontent += fmt.Sprintf("{{getText \"notificationTextSonarMetrics\"}}: %s \n", mailSonarMetricsText)
+ }
+ }
+
+ tplToRender := jobTplcontent
+ if args.WebHookType == setting.NotifyWebHookTypeMail {
+ tplToRender = mailJobTplcontent
+ }
+ jobContent, err := args.RenderTemplate(tplToRender, job)
+ if err != nil {
+ return nil, nil, err
+ }
+ jobContents = append(jobContents, jobContent)
+ workflowNotifyStage.Jobs = append(workflowNotifyStage.Jobs, workflowNotifyJob)
+ }
+
+ workflowNotifyStages = append(workflowNotifyStages, workflowNotifyStage)
+ }
+
+ return jobContents, workflowNotifyStages, nil
+}
diff --git a/pkg/microservice/aslan/core/common/util/yaml.go b/pkg/microservice/aslan/core/common/util/yaml.go
new file mode 100644
index 0000000000..e0e4c36b0c
--- /dev/null
+++ b/pkg/microservice/aslan/core/common/util/yaml.go
@@ -0,0 +1,71 @@
+package util
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/koderover/zadig/v2/pkg/microservice/aslan/config"
+ "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/git"
+ githubservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/github"
+ gitlabservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/gitlab"
+ "github.com/koderover/zadig/v2/pkg/setting"
+ "github.com/koderover/zadig/v2/pkg/shared/client/systemconfig"
+ "github.com/koderover/zadig/v2/pkg/tool/log"
+)
+
+type YAMLLoader interface {
+ GetYAMLContents(owner, repo, path, branch string, isDir, split bool) ([]string, error)
+ GetLatestRepositoryCommit(owner, repo, path, branch string) (*git.RepositoryCommit, error)
+ GetTree(owner, repo, path, branch string) ([]*git.TreeNode, error)
+}
+
+func GetYAMLLoader(ch *systemconfig.CodeHost) (YAMLLoader, error) {
+ switch ch.Type {
+ case setting.SourceFromGithub:
+ return githubservice.NewClient(ch.AccessToken, config.ProxyHTTPSAddr(), ch.EnableProxy), nil
+ case setting.SourceFromGitlab:
+ return gitlabservice.NewClient(ch.ID, ch.Address, ch.AccessToken, config.ProxyHTTPSAddr(), ch.EnableProxy, ch.DisableSSL)
+ default:
+ // should not have happened here
+ log.DPanicf("invalid source: %s", ch.Type)
+ return nil, fmt.Errorf("invalid source: %s", ch.Type)
+ }
+}
+
+func GetFoldersAndYAMLFiles(treeNodes []*git.TreeNode) ([]*git.TreeNode, []*git.TreeNode) {
+ var folders, files []*git.TreeNode
+ for _, tn := range treeNodes {
+ if tn.IsDir {
+ folders = append(folders, tn)
+ } else if IsYaml(tn.Name) {
+ files = append(files, tn)
+ }
+ }
+
+ return folders, files
+}
+
+func IsYaml(filename string) bool {
+ filename = strings.ToLower(filename)
+ return strings.HasSuffix(filename, ".yaml") || strings.HasSuffix(filename, ".yml")
+}
+
+func HasYAMLFiles(treeNodes []*git.TreeNode) bool {
+ for _, tn := range treeNodes {
+ if !tn.IsDir && IsYaml(tn.Name) {
+ return true
+ }
+ }
+
+ return false
+}
+
+func IsValidServiceDir(child []os.FileInfo) bool {
+ for _, file := range child {
+ if !file.IsDir() && IsYaml(file.Name()) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/pkg/microservice/aslan/core/delivery/service/version_v2.go b/pkg/microservice/aslan/core/delivery/service/version_v2.go
index 2d76a43d8f..092be56cbe 100644
--- a/pkg/microservice/aslan/core/delivery/service/version_v2.go
+++ b/pkg/microservice/aslan/core/delivery/service/version_v2.go
@@ -473,6 +473,8 @@ func RetryCreateK8SDeliveryVersionV2(deliveryVersion *commonmodels.DeliveryVersi
// update status
deliveryVersion.Status = setting.DeliveryVersionStatusRetrying
updateVersionStatusV2(deliveryVersion.Version, deliveryVersion.ProjectName, deliveryVersion.Status, deliveryVersion.Error)
+
+ go waitK8SImageVersionDoneV2(deliveryVersion)
} else {
return fmt.Errorf("no workflow task found for version: %s", deliveryVersion.Version)
}
@@ -670,6 +672,9 @@ func RetryCreateHelmDeliveryVersionV2(deliveryVersion *commonmodels.DeliveryVers
deliveryVersion.Status = setting.DeliveryVersionStatusRetrying
updateVersionStatusV2(deliveryVersion.Version, deliveryVersion.ProjectName, deliveryVersion.Status, deliveryVersion.Error)
+ // start a new routine to check task results
+ go waitHelmChartVersionDoneV2(deliveryVersion)
+
return nil
}
diff --git a/pkg/microservice/aslan/core/environment/handler/environment.go b/pkg/microservice/aslan/core/environment/handler/environment.go
index f43ad9f853..9e0d269cc1 100644
--- a/pkg/microservice/aslan/core/environment/handler/environment.go
+++ b/pkg/microservice/aslan/core/environment/handler/environment.go
@@ -41,7 +41,7 @@ import (
commonutil "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/environment/service"
"github.com/koderover/zadig/v2/pkg/setting"
- "github.com/koderover/zadig/v2/pkg/shared/client/plutusvendor"
+ "github.com/koderover/zadig/v2/pkg/shared/client/plutusenterprise"
internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler"
"github.com/koderover/zadig/v2/pkg/shared/kube/resource"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
@@ -860,7 +860,7 @@ func UpdateHelmProductDefaultValues(c *gin.Context) {
return
}
- licenseStatus, err := plutusvendor.New().CheckZadigXLicenseStatus()
+ licenseStatus, err := plutusenterprise.New().CheckZadigXLicenseStatus()
if err != nil {
ctx.RespErr = fmt.Errorf("failed to validate zadig license status, error: %s", err)
return
@@ -1206,7 +1206,7 @@ func updateMultiK8sEnv(c *gin.Context, request *service.UpdateEnvRequest, produc
return
}
- licenseStatus, err := plutusvendor.New().CheckZadigXLicenseStatus()
+ licenseStatus, err := plutusenterprise.New().CheckZadigXLicenseStatus()
if err != nil {
ctx.RespErr = fmt.Errorf("failed to validate zadig license status, error: %s", err)
return
@@ -1215,9 +1215,9 @@ func updateMultiK8sEnv(c *gin.Context, request *service.UpdateEnvRequest, produc
for _, arg := range args {
for _, service := range arg.Services {
if service.DeployStrategy == setting.ServiceDeployStrategyImport {
- if !((licenseStatus.Type == plutusvendor.ZadigSystemTypeProfessional ||
- licenseStatus.Type == plutusvendor.ZadigSystemTypeEnterprise) &&
- licenseStatus.Status == plutusvendor.ZadigXLicenseStatusNormal) {
+ if !((licenseStatus.Type == plutusenterprise.ZadigSystemTypeProfessional ||
+ licenseStatus.Type == plutusenterprise.ZadigSystemTypeEnterprise) &&
+ licenseStatus.Status == plutusenterprise.ZadigXLicenseStatusNormal) {
ctx.RespErr = e.ErrLicenseInvalid.AddDesc("")
return
}
@@ -1381,7 +1381,7 @@ func updateMultiHelmChartEnv(c *gin.Context, request *service.UpdateEnvRequest,
return
}
- licenseStatus, err := plutusvendor.New().CheckZadigXLicenseStatus()
+ licenseStatus, err := plutusenterprise.New().CheckZadigXLicenseStatus()
if err != nil {
ctx.RespErr = fmt.Errorf("failed to validate zadig license status, error: %s", err)
return
@@ -3277,8 +3277,8 @@ func RescaleSAEApp(c *gin.Context) {
if production {
if !ctx.Resources.ProjectAuthInfo[projectKey].IsProjectAdmin &&
- !ctx.Resources.ProjectAuthInfo[projectKey].ProductionEnv.Rollback {
- permitted, err := internalhandler.GetCollaborationModePermission(ctx.UserID, projectKey, types.ResourceTypeEnvironment, envName, types.ProductionEnvActionRollback)
+ !ctx.Resources.ProjectAuthInfo[projectKey].ProductionEnv.Scale {
+ permitted, err := internalhandler.GetCollaborationModePermission(ctx.UserID, projectKey, types.ResourceTypeEnvironment, envName, types.ProductionEnvActionScale)
if err != nil || !permitted {
ctx.UnAuthorized = true
return
@@ -3286,8 +3286,8 @@ func RescaleSAEApp(c *gin.Context) {
}
} else {
if !ctx.Resources.ProjectAuthInfo[projectKey].IsProjectAdmin &&
- !ctx.Resources.ProjectAuthInfo[projectKey].Env.Rollback {
- permitted, err := internalhandler.GetCollaborationModePermission(ctx.UserID, projectKey, types.ResourceTypeEnvironment, envName, types.EnvActionRollback)
+ !ctx.Resources.ProjectAuthInfo[projectKey].Env.Scale {
+ permitted, err := internalhandler.GetCollaborationModePermission(ctx.UserID, projectKey, types.ResourceTypeEnvironment, envName, types.EnvActionScale)
if err != nil || !permitted {
ctx.UnAuthorized = true
return
diff --git a/pkg/microservice/aslan/core/environment/handler/openapi.go b/pkg/microservice/aslan/core/environment/handler/openapi.go
index 50534309fa..7e37bb6472 100644
--- a/pkg/microservice/aslan/core/environment/handler/openapi.go
+++ b/pkg/microservice/aslan/core/environment/handler/openapi.go
@@ -110,8 +110,8 @@ func OpenAPIScaleWorkloads(c *gin.Context) {
return
}
if !ctx.Resources.ProjectAuthInfo[req.ProjectKey].IsProjectAdmin &&
- !ctx.Resources.ProjectAuthInfo[req.ProjectKey].Env.ManagePods {
- permitted, err := internalhandler.GetCollaborationModePermission(ctx.UserID, req.ProjectKey, types.ResourceTypeEnvironment, req.EnvName, types.EnvActionManagePod)
+ !ctx.Resources.ProjectAuthInfo[req.ProjectKey].Env.Scale {
+ permitted, err := internalhandler.GetCollaborationModePermission(ctx.UserID, req.ProjectKey, types.ResourceTypeEnvironment, req.EnvName, types.EnvActionScale)
if err != nil || !permitted {
ctx.UnAuthorized = true
return
diff --git a/pkg/microservice/aslan/core/environment/handler/product.go b/pkg/microservice/aslan/core/environment/handler/product.go
index 8daca62f22..ef20c6c5c6 100644
--- a/pkg/microservice/aslan/core/environment/handler/product.go
+++ b/pkg/microservice/aslan/core/environment/handler/product.go
@@ -98,6 +98,7 @@ func GetInitProduct(c *gin.Context) {
if !ctx.Resources.SystemActions.Project.Create &&
// this api is also used in creating testing env for some reason
!(ctx.Resources.ProjectAuthInfo[projectKey].Env.Create ||
+ ctx.Resources.ProjectAuthInfo[projectKey].Env.View ||
ctx.Resources.ProjectAuthInfo[projectKey].IsProjectAdmin) {
ctx.UnAuthorized = true
return
diff --git a/pkg/microservice/aslan/core/environment/handler/service.go b/pkg/microservice/aslan/core/environment/handler/service.go
index 63d73ce859..924a808573 100644
--- a/pkg/microservice/aslan/core/environment/handler/service.go
+++ b/pkg/microservice/aslan/core/environment/handler/service.go
@@ -531,8 +531,8 @@ func ScaleNewService(c *gin.Context) {
if production {
if !ctx.Resources.ProjectAuthInfo[projectKey].IsProjectAdmin &&
- !ctx.Resources.ProjectAuthInfo[projectKey].ProductionEnv.ManagePods {
- permitted, err := internalhandler.GetCollaborationModePermission(ctx.UserID, projectKey, types.ResourceTypeEnvironment, envName, types.ProductionEnvActionManagePod)
+ !ctx.Resources.ProjectAuthInfo[projectKey].ProductionEnv.Scale {
+ permitted, err := internalhandler.GetCollaborationModePermission(ctx.UserID, projectKey, types.ResourceTypeEnvironment, envName, types.ProductionEnvActionScale)
if err != nil || !permitted {
ctx.UnAuthorized = true
return
@@ -545,8 +545,8 @@ func ScaleNewService(c *gin.Context) {
}
} else {
if !ctx.Resources.ProjectAuthInfo[projectKey].IsProjectAdmin &&
- !ctx.Resources.ProjectAuthInfo[projectKey].Env.ManagePods {
- permitted, err := internalhandler.GetCollaborationModePermission(ctx.UserID, projectKey, types.ResourceTypeEnvironment, envName, types.EnvActionManagePod)
+ !ctx.Resources.ProjectAuthInfo[projectKey].Env.Scale {
+ permitted, err := internalhandler.GetCollaborationModePermission(ctx.UserID, projectKey, types.ResourceTypeEnvironment, envName, types.EnvActionScale)
if err != nil || !permitted {
ctx.UnAuthorized = true
return
diff --git a/pkg/microservice/aslan/core/environment/service/replica_scale.go b/pkg/microservice/aslan/core/environment/service/replica_scale.go
index e070bcf2e0..009cea40f8 100644
--- a/pkg/microservice/aslan/core/environment/service/replica_scale.go
+++ b/pkg/microservice/aslan/core/environment/service/replica_scale.go
@@ -176,6 +176,67 @@ func updateRenderVariableReplicaValue(renderVars []*commontypes.RenderVariableKV
return nil, fmt.Errorf("failed to find render variable %s", rootKey)
}
+func cloneGlobalVariableKVs(kvs []*commontypes.GlobalVariableKV) []*commontypes.GlobalVariableKV {
+ ret := make([]*commontypes.GlobalVariableKV, 0, len(kvs))
+ for _, kv := range kvs {
+ if kv == nil {
+ continue
+ }
+ copied := *kv
+ copied.Options = append([]string{}, kv.Options...)
+ copied.RelatedServices = append([]string{}, kv.RelatedServices...)
+ ret = append(ret, &copied)
+ }
+ return ret
+}
+
+func updateGlobalVariableReplicaValue(globalVars []*commontypes.GlobalVariableKV, rootKey, subPath string, replicas int) ([]*commontypes.GlobalVariableKV, error) {
+ cloned := cloneGlobalVariableKVs(globalVars)
+ for _, kv := range cloned {
+ if kv == nil || kv.Key != rootKey {
+ continue
+ }
+ if subPath == "" {
+ if kv.Type == commontypes.ServiceVariableKVTypeYaml {
+ renderedValue, err := yaml.Marshal(replicas)
+ if err != nil {
+ return nil, err
+ }
+ kv.Value = strings.TrimSpace(string(renderedValue))
+ return cloned, nil
+ }
+ kv.Value = replicas
+ return cloned, nil
+ }
+
+ if kv.Type != commontypes.ServiceVariableKVTypeYaml {
+ return nil, fmt.Errorf("global variable %s does not support nested replica path %s", kv.Key, subPath)
+ }
+ yamlValue, ok := kv.Value.(string)
+ if !ok {
+ return nil, fmt.Errorf("global variable %s is not a valid yaml value", kv.Key)
+ }
+
+ flatMap, err := converter.YamlToFlatMap([]byte(yamlValue))
+ if err != nil {
+ return nil, fmt.Errorf("failed to flatten global variable %s: %w", kv.Key, err)
+ }
+ flatMap[subPath] = replicas
+
+ expanded, err := converter.Expand(flatMap)
+ if err != nil {
+ return nil, fmt.Errorf("failed to expand global variable %s: %w", kv.Key, err)
+ }
+ renderedValue, err := yaml.Marshal(expanded)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal global variable %s: %w", kv.Key, err)
+ }
+ kv.Value = string(renderedValue)
+ return cloned, nil
+ }
+ return nil, fmt.Errorf("failed to find global variable %s", rootKey)
+}
+
// buildPreviewCandidateOverrides 仅用于预览:基于候选变量/版本计算预期的副本 override,不修改当前环境状态。
func buildPreviewCandidateOverrides(prod *commonmodels.Product, serviceName string, updateServiceRevision bool, variableKVs []*commontypes.RenderVariableKV) ([]*commonmodels.WorkLoad, error) {
currentSvc := cloneProductService(prod.GetServiceMap()[serviceName])
diff --git a/pkg/microservice/aslan/core/environment/service/service_scale.go b/pkg/microservice/aslan/core/environment/service/service_scale.go
index eb6a196a95..8878b5db4e 100644
--- a/pkg/microservice/aslan/core/environment/service/service_scale.go
+++ b/pkg/microservice/aslan/core/environment/service/service_scale.go
@@ -3,6 +3,7 @@ package service
import (
"context"
"fmt"
+ "reflect"
"time"
"github.com/koderover/zadig/v2/pkg/tool/clientmanager"
@@ -71,9 +72,6 @@ func Scale(args *ScaleArgs, updateBy string, logger *zap.SugaredLogger) error {
if err != nil {
return e.ErrScaleService.AddErr(err)
}
- if source.Kind == replicaSourceGlobal {
- return e.ErrScaleService.AddErr(fmt.Errorf("replicas of workload %s/%s is sourced from environment global variables and cannot be updated by scale", args.Type, args.Name))
- }
liveReplica, err := getWorkloadLiveReplica(prod.Namespace, args.Type, args.Name, kubeClient)
if err != nil {
@@ -81,6 +79,7 @@ func Scale(args *ScaleArgs, updateBy string, logger *zap.SugaredLogger) error {
}
candidateSvc := cloneProductService(currentSvc)
+ globalVariableChanged := false
switch source.Kind {
case replicaSourceLiteral:
case replicaSourceService:
@@ -97,6 +96,24 @@ func Scale(args *ScaleArgs, updateBy string, logger *zap.SugaredLogger) error {
if err != nil {
return e.ErrScaleService.AddErr(err)
}
+ case replicaSourceGlobal:
+ updatedGlobalVars, err := updateGlobalVariableReplicaValue(prod.GlobalVariables, source.RootKey, source.SubPath, args.Number)
+ if err != nil {
+ return e.ErrScaleService.AddErr(err)
+ }
+ globalVariableChanged = !reflect.DeepEqual(prod.GlobalVariables, updatedGlobalVars)
+ prod.GlobalVariables = updatedGlobalVars
+
+ mergedRenderKVs, err := mergeServiceRenderVariableKVs(currentTmpl.ServiceVariableKVs, currentSvc.GetServiceRender().OverrideYaml.RenderVariableKVs)
+ if err != nil {
+ return e.ErrScaleService.AddErr(err)
+ }
+ updatedRenderKVs := commontypes.UpdateRenderVariable(updatedGlobalVars, mergedRenderKVs)
+ candidateSvc.GetServiceRender().OverrideYaml.RenderVariableKVs = updatedRenderKVs
+ candidateSvc.GetServiceRender().OverrideYaml.YamlContent, err = commontypes.RenderVariableKVToYaml(updatedRenderKVs, true)
+ if err != nil {
+ return e.ErrScaleService.AddErr(err)
+ }
default:
return e.ErrScaleService.AddErr(fmt.Errorf("unsupported replicas source for workload %s/%s", args.Type, args.Name))
}
@@ -108,7 +125,7 @@ func Scale(args *ScaleArgs, updateBy string, logger *zap.SugaredLogger) error {
envStateChanged := serviceReplicaStateChanged(currentSvc, candidateSvc)
targetReplica := int32(args.Number)
- if liveReplica == targetReplica && !envStateChanged {
+ if liveReplica == targetReplica && !envStateChanged && !globalVariableChanged {
return nil
}
diff --git a/pkg/microservice/aslan/core/multicluster/service/clusters.go b/pkg/microservice/aslan/core/multicluster/service/clusters.go
index 32147a321e..14a9781400 100644
--- a/pkg/microservice/aslan/core/multicluster/service/clusters.go
+++ b/pkg/microservice/aslan/core/multicluster/service/clusters.go
@@ -53,7 +53,7 @@ import (
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/kube"
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/shared/client/plutusvendor"
+ "github.com/koderover/zadig/v2/pkg/shared/client/plutusenterprise"
"github.com/koderover/zadig/v2/pkg/shared/handler"
"github.com/koderover/zadig/v2/pkg/tool/clientmanager"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
@@ -115,13 +115,13 @@ type ScheduleStrategy struct {
}
func (args *K8SCluster) Validate() error {
- licenseStatus, err := plutusvendor.New().CheckZadigXLicenseStatus()
+ licenseStatus, err := plutusenterprise.New().CheckZadigXLicenseStatus()
if err != nil {
return fmt.Errorf("failed to validate zadig license status, error: %s", err)
}
- if !((licenseStatus.Type == plutusvendor.ZadigSystemTypeProfessional ||
- licenseStatus.Type == plutusvendor.ZadigSystemTypeEnterprise) &&
- licenseStatus.Status == plutusvendor.ZadigXLicenseStatusNormal) {
+ if !((licenseStatus.Type == plutusenterprise.ZadigSystemTypeProfessional ||
+ licenseStatus.Type == plutusenterprise.ZadigSystemTypeEnterprise) &&
+ licenseStatus.Status == plutusenterprise.ZadigXLicenseStatusNormal) {
if args.Provider == config.ClusterProviderTKEServerless || args.Provider == config.ClusterProviderAmazonEKS || args.Production {
return e.ErrLicenseInvalid.AddDesc("")
}
diff --git a/pkg/microservice/aslan/core/plugin/service/lark_v2.go b/pkg/microservice/aslan/core/plugin/service/lark_v2.go
index 6120b571cc..bbc68104ef 100644
--- a/pkg/microservice/aslan/core/plugin/service/lark_v2.go
+++ b/pkg/microservice/aslan/core/plugin/service/lark_v2.go
@@ -455,8 +455,11 @@ func ExecuteLarkWorkitemWorkflowV2(ctx *internalhandler.Context, workspaceID, wo
if err != nil {
return fmt.Errorf("failed to get input configs: %w", err)
}
- for _, inputConfig := range inputConfigs {
- inputConfig.Branch = stageConfig.TargetBranch
+ // if target branch is set and branch filter is not set, set the branch for all input configs
+ if stageConfig.TargetBranch != "" && len(stageConfig.BranchFilter) == 0 {
+ for _, inputConfig := range inputConfigs {
+ inputConfig.Branch = stageConfig.TargetBranch
+ }
}
} else {
binds, err := mongodb.NewLarkPluginReleaseWorkItemBindColl().ListReleaseBindItems(workspaceID, workItemID)
diff --git a/pkg/microservice/aslan/core/release_plan/service/lint.go b/pkg/microservice/aslan/core/release_plan/service/lint.go
index 042fc21dbc..25f3ea73be 100644
--- a/pkg/microservice/aslan/core/release_plan/service/lint.go
+++ b/pkg/microservice/aslan/core/release_plan/service/lint.go
@@ -19,6 +19,7 @@ package service
import (
"fmt"
+ commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util"
"github.com/koderover/zadig/v2/pkg/tool/lark"
"github.com/pkg/errors"
@@ -105,12 +106,44 @@ func lintApproval(approval *models.Approval) error {
if len(approval.LarkApproval.ApprovalNodes) == 0 {
return errors.New("num of approval-node is 0")
}
+
for i, node := range approval.LarkApproval.ApprovalNodes {
- if node.Type == lark.ApproveTypeStart || node.Type == lark.ApproveTypeEnd {
- continue
- }
- if len(node.ApproveUsers) == 0 {
- return errors.Errorf("num of approval-node %d approver is 0", i)
+ if node.ApproveNodeType == lark.ApproveNodeTypeUser {
+ if node.Type == lark.ApproveTypeStart || node.Type == lark.ApproveTypeEnd {
+ continue
+ }
+ if len(node.ApproveUsers) == 0 {
+ return errors.Errorf("num of approval-node %d approver is 0", i)
+ }
+ } else if node.ApproveNodeType == lark.ApproveNodeTypeUserGroup {
+ if node.Type == lark.ApproveTypeStart || node.Type == lark.ApproveTypeEnd {
+ users, err := util.ConvertLarkUserGroupToUser(approval.LarkApproval.ID, node.CcGroups)
+ if err != nil {
+ return errors.Errorf("failed to convert lark user group to user: %s", err)
+ }
+ node.CcUsers = users
+ approval.LarkApproval.ApprovalNodes[i] = node
+ continue
+ }
+
+ if len(node.ApproveGroups) == 0 {
+ return errors.Errorf("num of approval-node %d approver is 0", i)
+ }
+
+ users, err := util.ConvertLarkUserGroupToUser(approval.LarkApproval.ID, node.ApproveGroups)
+ if err != nil {
+ return errors.Errorf("failed to convert lark user group to user: %s", err)
+ }
+
+ approveUsers := make([]*commonmodels.LarkApprovalUser, 0)
+ for _, user := range users {
+ approveUsers = append(approveUsers, &commonmodels.LarkApprovalUser{
+ UserInfo: *user,
+ })
+ }
+ node.ApproveUsers = approveUsers
+
+ approval.LarkApproval.ApprovalNodes[i] = node
}
if !lo.Contains([]string{"AND", "OR"}, string(node.Type)) {
return errors.Errorf("approval-node %d type should be AND or OR", i)
diff --git a/pkg/microservice/aslan/core/release_plan/service/release_plan.go b/pkg/microservice/aslan/core/release_plan/service/release_plan.go
index 19323f5738..e1f519b2fe 100644
--- a/pkg/microservice/aslan/core/release_plan/service/release_plan.go
+++ b/pkg/microservice/aslan/core/release_plan/service/release_plan.go
@@ -645,6 +645,8 @@ func RetryReleaseJob(c *handler.Context, planID string, args *RetryReleaseJobArg
plan.UpdatedBy = c.UserName
plan.UpdateTime = time.Now().Unix()
+ plan.Status = config.ReleasePlanStatusExecuting
+ plan.SuccessTime = 0
sendWebhook := false
hookSetting, err := mongodb.NewSystemSettingColl().GetReleasePlanHookSetting()
@@ -1355,10 +1357,12 @@ const (
)
type ListReleasePlanOption struct {
- PageNum int64 `form:"pageNum" binding:"required"`
- PageSize int64 `form:"pageSize" binding:"required"`
- Type ListReleasePlanType `form:"type" binding:"required"`
- Keyword string `form:"keyword"`
+ PageNum int64 `form:"pageNum" binding:"required"`
+ PageSize int64 `form:"pageSize" binding:"required"`
+ StartTime int64 `form:"startTime"`
+ EndTime int64 `form:"endTime"`
+ Type ListReleasePlanType `form:"type" binding:"required"`
+ Keyword string `form:"keyword"`
}
type ListReleasePlanResp struct {
@@ -1379,6 +1383,8 @@ func ListReleasePlans(opt *ListReleasePlanOption) (*ListReleasePlanResp, error)
IsSort: true,
PageNum: opt.PageNum,
PageSize: opt.PageSize,
+ StartTime: opt.StartTime,
+ EndTime: opt.EndTime,
ExcludedFields: []string{"jobs", "logs"},
})
case ListReleasePlanTypeManager:
@@ -1387,6 +1393,8 @@ func ListReleasePlans(opt *ListReleasePlanOption) (*ListReleasePlanResp, error)
IsSort: true,
PageNum: opt.PageNum,
PageSize: opt.PageSize,
+ StartTime: opt.StartTime,
+ EndTime: opt.EndTime,
ExcludedFields: []string{"jobs", "logs"},
})
case ListReleasePlanTypeSuccessTime:
@@ -1413,6 +1421,8 @@ func ListReleasePlans(opt *ListReleasePlanOption) (*ListReleasePlanResp, error)
SortBy: mongodb.SortReleasePlanByUpdateTime,
PageNum: opt.PageNum,
PageSize: opt.PageSize,
+ StartTime: opt.StartTime,
+ EndTime: opt.EndTime,
ExcludedFields: []string{"jobs", "logs"},
})
case ListReleasePlanTypeUpdateTime:
@@ -1439,6 +1449,8 @@ func ListReleasePlans(opt *ListReleasePlanOption) (*ListReleasePlanResp, error)
SortBy: mongodb.SortReleasePlanByUpdateTime,
PageNum: opt.PageNum,
PageSize: opt.PageSize,
+ StartTime: opt.StartTime,
+ EndTime: opt.EndTime,
ExcludedFields: []string{"jobs", "logs"},
})
case ListReleasePlanTypeStatus:
@@ -1483,10 +1495,11 @@ type ReleasePlanCallBackBody struct {
HookEvent models.ReleasePlanHookEvent `json:"hook_event"`
Result setting.ReleasePlanCallBackResultType `json:"result"`
FailedReason string `json:"failed_reason"`
+ Description string `json:"description"`
}
func ReleasePlanHookCallback(c *handler.Context, callback *ReleasePlanCallBackBody) error {
- log.Infof("release plan hook callback, id: %s, instance code: %s, hook event: %s, result: %s, failed reason: %s", callback.ReleasePlanID, callback.InstanceCode, callback.HookEvent, callback.Result, callback.FailedReason)
+ log.Infof("release plan hook callback, id: %s, instance code: %s, hook event: %s, result: %s, failed reason: %s, description: %s", callback.ReleasePlanID, callback.InstanceCode, callback.HookEvent, callback.Result, callback.FailedReason, callback.Description)
hookSetting, err := mongodb.NewSystemSettingColl().GetReleasePlanHookSetting()
if err != nil {
@@ -1619,6 +1632,14 @@ func ReleasePlanHookCallback(c *handler.Context, callback *ReleasePlanCallBackBo
}
releasePlan.ExternalCheckFailedReason = callback.FailedReason
+ if err := mongodb.NewReleasePlanColl().UpdateByID(c, releasePlan.ID.Hex(), releasePlan); err != nil {
+ fmtErr := fmt.Errorf("failed update release plan, id: %s, err: %v", releasePlan.ID.Hex(), err)
+ log.Error(fmtErr)
+ return fmtErr
+ }
+ } else if callback.Result == setting.ReleasePlanCallBackResultTypeExecuting {
+ releasePlan.CallbackDescription = callback.Description
+
if err := mongodb.NewReleasePlanColl().UpdateByID(c, releasePlan.ID.Hex(), releasePlan); err != nil {
fmtErr := fmt.Errorf("failed update release plan, id: %s, err: %v", releasePlan.ID.Hex(), err)
log.Error(fmtErr)
@@ -2135,6 +2156,35 @@ func convertWorkflowV4ToOpenAPIWorkflowV4(workflow *commonmodels.WorkflowV4) (*w
Source: spec.Source,
NacosDatas: spec.NacosDatas,
}
+ case config.JobTapd:
+ spec := new(commonmodels.TapdJobSpec)
+ err := models.IToi(job.Spec, spec)
+ if err != nil {
+ fmtErr := fmt.Errorf("failed convert job spec to nacos job spec, job: %+v, err: %v", job, err)
+ log.Error(fmtErr)
+ return nil, fmtErr
+ }
+
+ iterations := []*webhooknotify.OpenAPIWorkflowTapdIteration{}
+ for _, iteration := range spec.Iterations {
+ iterations = append(iterations, &webhooknotify.OpenAPIWorkflowTapdIteration{
+ IterationID: iteration.IterationID,
+ IterationName: iteration.IterationName,
+ StartDate: iteration.StartDate,
+ EndDate: iteration.EndDate,
+ Error: iteration.Error,
+ })
+ }
+
+ hookSpec = &webhooknotify.OpenAPIWorkflowTapdJobSpec{
+ TapdID: spec.TapdID,
+ Type: spec.Type,
+ ProjectID: spec.ProjectID,
+ ProjectName: spec.ProjectName,
+ SourceStatus: spec.SourceStatus,
+ Status: spec.Status,
+ Iterations: iterations,
+ }
case config.JobZadigDistributeImage:
spec := new(commonmodels.ZadigDistributeImageJobSpec)
err := models.IToi(job.Spec, spec)
diff --git a/pkg/microservice/aslan/core/service/service/loader.go b/pkg/microservice/aslan/core/service/service/loader.go
index b98e339bc3..a23b8258b4 100644
--- a/pkg/microservice/aslan/core/service/service/loader.go
+++ b/pkg/microservice/aslan/core/service/service/loader.go
@@ -32,6 +32,7 @@ import (
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/command"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/repository"
+ 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/shared/client/systemconfig"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
@@ -128,7 +129,7 @@ func preloadGerritService(detail *systemconfig.CodeHost, repoName, branchName, r
filePath := path.Join(base, loadPath.Path)
if !loadPath.IsDir {
- if !isYaml(loadPath.Path) {
+ if !commonutil.IsYaml(loadPath.Path) {
log.Error("trying to preload a non-yaml file")
return nil, e.ErrPreloadServiceTemplate.AddDesc("Non-yaml service loading is not supported")
}
@@ -145,7 +146,7 @@ func preloadGerritService(detail *systemconfig.CodeHost, repoName, branchName, r
log.Error("Failed to read directory info of path: %s, the error is: %+v", filePath, err)
return nil, e.ErrPreloadServiceTemplate.AddDesc(err.Error())
}
- if isValidServiceDir(fileInfos) {
+ if commonutil.IsValidServiceDir(fileInfos) {
svcName := loadPath.Path
if loadPath.Path == "" {
svcName = repoName
@@ -168,7 +169,7 @@ func preloadGerritService(detail *systemconfig.CodeHost, repoName, branchName, r
log.Errorf("Failed to get subdir content from gerrit with path: %s, the error is: %+v", subDirPath, err)
return nil, e.ErrPreloadServiceTemplate.AddDesc(err.Error())
}
- if isValidServiceDir(subtree) {
+ if commonutil.IsValidServiceDir(subtree) {
ret = append(ret, LoadServicePath{
ServiceName: getFileName(file.Name()),
Path: loadPath.Path,
@@ -210,7 +211,7 @@ func preloadGiteeService(detail *systemconfig.CodeHost, repoOwner, repoName, bra
filePath := path.Join(base, loadPath.Path)
if !loadPath.IsDir {
- if !isYaml(loadPath.Path) {
+ if !commonutil.IsYaml(loadPath.Path) {
log.Error("trying to preload a non-yaml file")
return nil, e.ErrPreloadServiceTemplate.AddDesc("Non-yaml service loading is not supported")
}
@@ -228,7 +229,7 @@ func preloadGiteeService(detail *systemconfig.CodeHost, repoOwner, repoName, bra
os.RemoveAll(base)
return nil, e.ErrPreloadServiceTemplate.AddDesc(err.Error())
}
- if isValidServiceDir(fileInfos) {
+ if commonutil.IsValidServiceDir(fileInfos) {
svcName := loadPath.Path
if loadPath.Path == "" {
svcName = repoName
@@ -251,7 +252,7 @@ func preloadGiteeService(detail *systemconfig.CodeHost, repoOwner, repoName, bra
log.Errorf("Failed to get subdir content from gitee with path: %s, the error is: %s", subDirPath, err)
return nil, e.ErrPreloadServiceTemplate.AddDesc(err.Error())
}
- if isValidServiceDir(subtree) {
+ if commonutil.IsValidServiceDir(subtree) {
ret = append(ret, LoadServicePath{
ServiceName: getFileName(file.Name()),
Path: loadPath.Path,
@@ -348,7 +349,7 @@ func loadGerritService(username string, ch *systemconfig.CodeHost, repoOwner, re
log.Errorf("Failed to read directory info of path: %s, the error is: %+v", filePath, err)
return e.ErrLoadServiceTemplate.AddDesc(err.Error())
}
- if isValidServiceDir(fileInfos) {
+ if commonutil.IsValidServiceDir(fileInfos) {
return loadServiceFromGerrit(fileInfos, ch.ID, username, branchName, loadPath.Path, loadPath.ServiceName, filePath, repoOwner, remoteName, repoName, args.Type, args.ProductName, loadPath.IsDir, commitInfo, force, production, log)
} else {
return e.ErrLoadServiceTemplate.AddDesc(fmt.Sprintf("%s 路径下没有yaml文件,请重新选择", loadPath.Path))
@@ -361,7 +362,7 @@ func loadGerritService(username string, ch *systemconfig.CodeHost, repoOwner, re
// log.Errorf("Failed to read subdir info from gerrit package of path: %s, the error is: %+v", subtreePath, err)
// return e.ErrLoadServiceTemplate.AddDesc(err.Error())
// }
- // if isValidServiceDir(subtreeInfo) {
+ // if commonutil.IsValidServiceDir(subtreeInfo) {
// if err := loadServiceFromGerrit(subtreeInfo, ch.ID, username, branchName, subtreeLoadPath, subtreePath, repoOwner, remoteName, repoName, args, commitInfo, force, production, log); err != nil {
// return err
// }
@@ -482,7 +483,7 @@ func loadGiteeService(username string, ch *systemconfig.CodeHost, repoOwner, rep
log.Errorf("Failed to read directory info of path: %s, the error is: %s", filePath, err)
return e.ErrLoadServiceTemplate.AddDesc(err.Error())
}
- if isValidServiceDir(fileInfos) {
+ if commonutil.IsValidServiceDir(fileInfos) {
return loadServiceFromGitee(fileInfos, ch, username, branchName, loadPath.Path, loadPath.ServiceName, filePath, repoOwner, remoteName, repoName, args.Type, args.ProductName, loadPath.IsDir, commitInfo, force, production, log)
} else {
return e.ErrLoadServiceTemplate.AddDesc(fmt.Sprintf("%s 路径下没有yaml文件,请重新选择", loadPath.Path))
@@ -496,7 +497,7 @@ func loadGiteeService(username string, ch *systemconfig.CodeHost, repoOwner, rep
// log.Errorf("Failed to read subdir info from gitee package of path: %s, the error is: %s", subtreePath, err)
// return e.ErrLoadServiceTemplate.AddDesc(err.Error())
// }
- // if isValidServiceDir(subtreeInfo) {
+ // if commonutil.IsValidServiceDir(subtreeInfo) {
// if err := loadServiceFromGitee(subtreeInfo, ch, username, branchName, subtreeLoadPath, subtreePath, repoOwner, remoteName, repoName, args, commitInfo, force, production, log); err != nil {
// return err
// }
@@ -552,7 +553,7 @@ func loadServiceFromGitee(tree []os.FileInfo, ch *systemconfig.CodeHost, usernam
func isValidGithubServiceDir(child []*github.RepositoryContent) bool {
for _, entry := range child {
- if entry.GetType() == "file" && isYaml(entry.GetName()) {
+ if entry.GetType() == "file" && commonutil.IsYaml(entry.GetName()) {
return true
}
}
@@ -561,16 +562,7 @@ func isValidGithubServiceDir(child []*github.RepositoryContent) bool {
func isValidGitlabServiceDir(child []*gitlab.TreeNode) bool {
for _, entry := range child {
- if entry.Type == "blob" && isYaml(entry.Name) {
- return true
- }
- }
- return false
-}
-
-func isValidServiceDir(child []os.FileInfo) bool {
- for _, file := range child {
- if !file.IsDir() && isYaml(file.Name()) {
+ if entry.Type == "blob" && commonutil.IsYaml(entry.Name) {
return true
}
}
@@ -580,7 +572,7 @@ func isValidServiceDir(child []os.FileInfo) bool {
func extractYamls(basePath string, tree []os.FileInfo) ([]string, error) {
var ret []string
for _, entry := range tree {
- if !entry.IsDir() && isYaml(entry.Name()) {
+ if !entry.IsDir() && commonutil.IsYaml(entry.Name()) {
tmpFilepath := fmt.Sprintf("%s/%s", basePath, entry.Name())
yamlByte, err := ioutil.ReadFile(tmpFilepath)
if err != nil {
diff --git a/pkg/microservice/aslan/core/service/service/new_loader.go b/pkg/microservice/aslan/core/service/service/new_loader.go
index b2f3d5f540..ba2c069c9b 100644
--- a/pkg/microservice/aslan/core/service/service/new_loader.go
+++ b/pkg/microservice/aslan/core/service/service/new_loader.go
@@ -19,16 +19,11 @@ package service
import (
"fmt"
"path/filepath"
- "strings"
"go.uber.org/zap"
- "github.com/koderover/zadig/v2/pkg/microservice/aslan/config"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models"
- "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/git"
- githubservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/github"
- gitlabservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/gitlab"
- "github.com/koderover/zadig/v2/pkg/setting"
+ commonutil "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util"
"github.com/koderover/zadig/v2/pkg/shared/client/systemconfig"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
"github.com/koderover/zadig/v2/pkg/tool/log"
@@ -44,7 +39,7 @@ type LoadServicePath struct {
func preloadService(ch *systemconfig.CodeHost, owner, repo, branch string, paths []PreLoadServicePath, logger *zap.SugaredLogger) ([]LoadServicePath, error) {
logger.Infof("Preloading service from %s with owner %s, repo %s, branch %s and path %s", ch.Type, owner, repo, branch, paths)
- loader, err := getLoader(ch)
+ loader, err := commonutil.GetYAMLLoader(ch)
if err != nil {
logger.Errorf("Failed to create loader client, err: %s", err)
return nil, e.ErrLoadServiceTemplate.AddDesc(err.Error())
@@ -53,7 +48,7 @@ func preloadService(ch *systemconfig.CodeHost, owner, repo, branch string, paths
resp := make([]LoadServicePath, 0)
for _, path := range paths {
if !path.IsDir {
- if !isYaml(path.Path) {
+ if !commonutil.IsYaml(path.Path) {
return nil, e.ErrPreloadServiceTemplate.AddDesc("File is not of type yaml or yml, select again")
}
@@ -69,7 +64,7 @@ func preloadService(ch *systemconfig.CodeHost, owner, repo, branch string, paths
return nil, e.ErrLoadServiceTemplate.AddDesc(err.Error())
}
- folders, files := getFoldersAndYAMLFiles(treeNodes)
+ folders, files := commonutil.GetFoldersAndYAMLFiles(treeNodes)
// if load path is a directory, we will load services in following rules:
// 1. if there is any yaml files under this directory, collect them as a service and ignore other files and directories
// 2. if not, but there is some directories under this directory, load each of them as a service
@@ -87,7 +82,7 @@ func preloadService(ch *systemconfig.CodeHost, owner, repo, branch string, paths
return nil, e.ErrLoadServiceTemplate.AddDesc(err.Error())
}
- if hasYAMLFiles(tns) {
+ if commonutil.HasYAMLFiles(tns) {
resp = append(resp, LoadServicePath{
ServiceName: getFileName(f.FullPath),
Path: f.FullPath,
@@ -118,7 +113,7 @@ type serviceInfo struct {
func loadService(username string, ch *systemconfig.CodeHost, owner, namespace, repo, branch string, args *LoadServiceReq, force, production bool, logger *zap.SugaredLogger) error {
logger.Infof("Loading service from %s with owner %s, namespace %s, repo %s, branch %s and path %v", ch.Type, owner, namespace, repo, branch, args.ServicePaths)
- loader, err := getLoader(ch)
+ loader, err := commonutil.GetYAMLLoader(ch)
if err != nil {
logger.Errorf("Failed to create loader client, err: %s", err)
return e.ErrLoadServiceTemplate.AddDesc(err.Error())
@@ -141,7 +136,7 @@ func loadService(username string, ch *systemconfig.CodeHost, owner, namespace, r
return e.ErrLoadServiceTemplate.AddDesc(err.Error())
}
- _, files := getFoldersAndYAMLFiles(treeNodes)
+ _, files := commonutil.GetFoldersAndYAMLFiles(treeNodes)
// if load path is a directory, we will load services in following rules:
// 1. if there is any yaml files under this directory, collect them as a service and ignore other files and directories
// 2. if not, but there is some directories under this directory, load each of them as a service
@@ -220,53 +215,6 @@ func loadService(username string, ch *systemconfig.CodeHost, owner, namespace, r
return nil
}
-func getFoldersAndYAMLFiles(treeNodes []*git.TreeNode) ([]*git.TreeNode, []*git.TreeNode) {
- var folders, files []*git.TreeNode
- for _, tn := range treeNodes {
- if tn.IsDir {
- folders = append(folders, tn)
- } else if isYaml(tn.Name) {
- files = append(files, tn)
- }
- }
-
- return folders, files
-}
-
-func hasYAMLFiles(treeNodes []*git.TreeNode) bool {
- for _, tn := range treeNodes {
- if !tn.IsDir && isYaml(tn.Name) {
- return true
- }
- }
-
- return false
-}
-
-type yamlLoader interface {
- GetYAMLContents(owner, repo, path, branch string, isDir, split bool) ([]string, error)
- GetLatestRepositoryCommit(owner, repo, path, branch string) (*git.RepositoryCommit, error)
- GetTree(owner, repo, path, branch string) ([]*git.TreeNode, error)
-}
-
-func getLoader(ch *systemconfig.CodeHost) (yamlLoader, error) {
- switch ch.Type {
- case setting.SourceFromGithub:
- return githubservice.NewClient(ch.AccessToken, config.ProxyHTTPSAddr(), ch.EnableProxy), nil
- case setting.SourceFromGitlab:
- return gitlabservice.NewClient(ch.ID, ch.Address, ch.AccessToken, config.ProxyHTTPSAddr(), ch.EnableProxy, ch.DisableSSL)
- default:
- // should not have happened here
- log.DPanicf("invalid source: %s", ch.Type)
- return nil, fmt.Errorf("invalid source: %s", ch.Type)
- }
-}
-
-func isYaml(filename string) bool {
- filename = strings.ToLower(filename)
- return strings.HasSuffix(filename, ".yaml") || strings.HasSuffix(filename, ".yml")
-}
-
func getFileName(fullName string) string {
name := filepath.Base(fullName)
ext := filepath.Ext(name)
diff --git a/pkg/microservice/aslan/core/service/service/openapi.go b/pkg/microservice/aslan/core/service/service/openapi.go
index 74112a69bc..c028c98357 100644
--- a/pkg/microservice/aslan/core/service/service/openapi.go
+++ b/pkg/microservice/aslan/core/service/service/openapi.go
@@ -200,8 +200,9 @@ func OpenAPIUpdateProductionServiceVariable(userName, projectName, serviceName s
func OpenAPIGetYamlService(projectKey, serviceName string, logger *zap.SugaredLogger) (*OpenAPIGetYamlServiceResp, error) {
var resp *OpenAPIGetYamlServiceResp
service, err := commonrepo.NewServiceColl().Find(&commonrepo.ServiceFindOption{
- ProductName: projectKey,
- ServiceName: serviceName,
+ ProductName: projectKey,
+ ServiceName: serviceName,
+ ExcludeStatus: setting.ProductStatusDeleting,
})
if err != nil {
msg := fmt.Errorf("failed to get service from db, projectKey: %s, serviceName: %s, error: %v", projectKey, serviceName, err)
diff --git a/pkg/microservice/aslan/core/service/service/service.go b/pkg/microservice/aslan/core/service/service/service.go
index c5bee09a06..7b3c36b234 100644
--- a/pkg/microservice/aslan/core/service/service/service.go
+++ b/pkg/microservice/aslan/core/service/service/service.go
@@ -1005,6 +1005,22 @@ func DeleteServiceTemplate(serviceName, serviceType, productName string, product
if production {
return e.ErrDeleteTemplate.AddDesc("PM service type only support testing service")
}
+
+ // 删除与该PM服务关联的部署
+ deploy, err := commonrepo.NewDeployColl().Find(&commonrepo.DeployFindOption{
+ ProjectName: productName,
+ ServiceName: serviceName,
+ })
+ if err != nil && err != mongo.ErrNoDocuments {
+ log.Errorf("DeleteServiceTemplate: failed to find deploy for pm service %s/%s, err: %v", productName, serviceName, err)
+ return e.ErrDeleteTemplate.AddDesc(err.Error())
+ }
+ if deploy != nil {
+ if err := commonrepo.NewDeployColl().Delete(deploy.ProjectName, deploy.Name); err != nil {
+ log.Errorf("DeleteServiceTemplate: failed to delete deploy %s/%s for pm service %s, err: %v", deploy.ProjectName, deploy.Name, serviceName, err)
+ return e.ErrDeleteTemplate.AddDesc(err.Error())
+ }
+ }
}
err := repository.UpdateStatus(serviceName, productName, setting.ProductStatusDeleting, production)
diff --git a/pkg/microservice/aslan/core/system/handler/registry.go b/pkg/microservice/aslan/core/system/handler/registry.go
index 57c56d59e9..2c5d650cec 100644
--- a/pkg/microservice/aslan/core/system/handler/registry.go
+++ b/pkg/microservice/aslan/core/system/handler/registry.go
@@ -31,7 +31,7 @@ import (
commonservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/system/service"
"github.com/koderover/zadig/v2/pkg/setting"
- "github.com/koderover/zadig/v2/pkg/shared/client/plutusvendor"
+ "github.com/koderover/zadig/v2/pkg/shared/client/plutusenterprise"
internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
"github.com/koderover/zadig/v2/pkg/tool/log"
@@ -84,7 +84,7 @@ func GetDefaultRegistryNamespace(c *gin.Context) {
return
}
- licenseStatus, err := plutusvendor.New().CheckZadigXLicenseStatus()
+ licenseStatus, err := plutusenterprise.New().CheckZadigXLicenseStatus()
if err != nil {
ctx.RespErr = fmt.Errorf("failed to validate zadig license status, error: %s", err)
return
@@ -92,9 +92,9 @@ func GetDefaultRegistryNamespace(c *gin.Context) {
if reg.RegType == config.RegistryProviderACREnterprise ||
reg.RegType == config.RegistryProviderTCREnterprise ||
reg.RegType == config.RegistryProviderJFrog {
- if !((licenseStatus.Type == plutusvendor.ZadigSystemTypeProfessional ||
- licenseStatus.Type == plutusvendor.ZadigSystemTypeEnterprise) &&
- licenseStatus.Status == plutusvendor.ZadigXLicenseStatusNormal) {
+ if !((licenseStatus.Type == plutusenterprise.ZadigSystemTypeProfessional ||
+ licenseStatus.Type == plutusenterprise.ZadigSystemTypeEnterprise) &&
+ licenseStatus.Status == plutusenterprise.ZadigXLicenseStatusNormal) {
ctx.RespErr = e.ErrLicenseInvalid.AddDesc("")
return
}
diff --git a/pkg/microservice/aslan/core/system/handler/s3.go b/pkg/microservice/aslan/core/system/handler/s3.go
index 086b0557c7..5c9686b422 100644
--- a/pkg/microservice/aslan/core/system/handler/s3.go
+++ b/pkg/microservice/aslan/core/system/handler/s3.go
@@ -29,7 +29,7 @@ import (
commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/s3"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/system/service"
- "github.com/koderover/zadig/v2/pkg/shared/client/plutusvendor"
+ "github.com/koderover/zadig/v2/pkg/shared/client/plutusenterprise"
internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
"github.com/koderover/zadig/v2/pkg/tool/log"
@@ -132,15 +132,15 @@ func CreateS3Storage(c *gin.Context) {
return
}
- licenseStatus, err := plutusvendor.New().CheckZadigXLicenseStatus()
+ licenseStatus, err := plutusenterprise.New().CheckZadigXLicenseStatus()
if err != nil {
ctx.RespErr = fmt.Errorf("failed to validate zadig license status, error: %s", err)
return
}
if args.Provider == config.S3StorageProviderAmazonS3 {
- if !((licenseStatus.Type == plutusvendor.ZadigSystemTypeProfessional ||
- licenseStatus.Type == plutusvendor.ZadigSystemTypeEnterprise) &&
- licenseStatus.Status == plutusvendor.ZadigXLicenseStatusNormal) {
+ if !((licenseStatus.Type == plutusenterprise.ZadigSystemTypeProfessional ||
+ licenseStatus.Type == plutusenterprise.ZadigSystemTypeEnterprise) &&
+ licenseStatus.Status == plutusenterprise.ZadigXLicenseStatusNormal) {
ctx.RespErr = e.ErrLicenseInvalid.AddDesc("")
return
}
@@ -259,15 +259,15 @@ func UpdateS3Storage(c *gin.Context) {
return
}
- licenseStatus, err := plutusvendor.New().CheckZadigXLicenseStatus()
+ licenseStatus, err := plutusenterprise.New().CheckZadigXLicenseStatus()
if err != nil {
ctx.RespErr = fmt.Errorf("failed to validate zadig license status, error: %s", err)
return
}
if args.Provider == config.S3StorageProviderAmazonS3 {
- if !((licenseStatus.Type == plutusvendor.ZadigSystemTypeProfessional ||
- licenseStatus.Type == plutusvendor.ZadigSystemTypeEnterprise) &&
- licenseStatus.Status == plutusvendor.ZadigXLicenseStatusNormal) {
+ if !((licenseStatus.Type == plutusenterprise.ZadigSystemTypeProfessional ||
+ licenseStatus.Type == plutusenterprise.ZadigSystemTypeEnterprise) &&
+ licenseStatus.Status == plutusenterprise.ZadigXLicenseStatusNormal) {
ctx.RespErr = e.ErrLicenseInvalid.AddDesc("")
return
}
diff --git a/pkg/microservice/aslan/core/system/handler/security.go b/pkg/microservice/aslan/core/system/handler/security.go
index 5739277070..4eee108397 100644
--- a/pkg/microservice/aslan/core/system/handler/security.go
+++ b/pkg/microservice/aslan/core/system/handler/security.go
@@ -24,6 +24,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/koderover/zadig/v2/pkg/types"
+ commonutil "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/system/service"
internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler"
)
@@ -72,6 +73,15 @@ func CreateOrUpdateSecuritySettings(c *gin.Context) {
ctx.RespErr = errors.New("token expiration time cannot be greater than 8640 hour")
return
}
+
+ if args.MFAEnabled {
+ err = commonutil.CheckZadigProfessionalLicense()
+ if err != nil {
+ ctx.RespErr = err
+ return
+ }
+ }
+
ctx.RespErr = service.CreateOrUpdateSecuritySettings(args, ctx.Logger)
}
diff --git a/pkg/microservice/aslan/core/system/service/private_key.go b/pkg/microservice/aslan/core/system/service/private_key.go
index 5e81904b59..be98723184 100644
--- a/pkg/microservice/aslan/core/system/service/private_key.go
+++ b/pkg/microservice/aslan/core/system/service/private_key.go
@@ -42,6 +42,10 @@ import (
func ListPrivateKeys(encryptedKey, projectName, keyword string, systemOnly bool, log *zap.SugaredLogger) ([]*commonmodels.PrivateKey, error) {
var resp []*commonmodels.PrivateKey
var err error
+ latestAgentVersion, versionErr := config.GetZadigAgentVersion()
+ if versionErr != nil {
+ log.Warnf("failed to get current zadig-agent version: %v", versionErr)
+ }
privateKeys, err := commonrepo.NewPrivateKeyColl().List(&commonrepo.PrivateKeyArgs{ProjectName: projectName, SystemOnly: systemOnly})
if err != nil {
log.Errorf("PrivateKey.List error: %s", err)
@@ -70,10 +74,24 @@ func ListPrivateKeys(encryptedKey, projectName, keyword string, systemOnly bool,
if err != nil {
return nil, err
}
+ if key.Agent != nil && key.ScheduleWorkflow {
+ key.Agent.ZadigVersion = latestAgentVersion
+ key.Agent.NeedUpdate = isAgentVersionOutdated(key.Agent.AgentVersion, latestAgentVersion)
+ }
}
return resp, nil
}
+// check agent version
+func isAgentVersionOutdated(currentVersion, latestVersion string) bool {
+ normalizedLatestVersion := strings.TrimPrefix(latestVersion, "v")
+ if normalizedLatestVersion == "" {
+ return false
+ }
+ normalizedCurrentVersion := strings.TrimPrefix(currentVersion, "v")
+ return normalizedCurrentVersion != normalizedLatestVersion
+}
+
func ListPrivateKeysInternal(log *zap.SugaredLogger) ([]*commonmodels.PrivateKey, error) {
resp, err := commonrepo.NewPrivateKeyColl().List(&commonrepo.PrivateKeyArgs{})
if err != nil {
diff --git a/pkg/microservice/aslan/core/templatestore/handler/router.go b/pkg/microservice/aslan/core/templatestore/handler/router.go
index 3108db6036..ba16630f8a 100644
--- a/pkg/microservice/aslan/core/templatestore/handler/router.go
+++ b/pkg/microservice/aslan/core/templatestore/handler/router.go
@@ -54,6 +54,9 @@ func (*Router) Inject(router *gin.RouterGroup) {
yaml := router.Group("yaml")
{
yaml.POST("", CreateYamlTemplate)
+ yaml.POST("/preload/:codehostId", PreloadYamlTemplateFromCodeHost)
+ yaml.POST("/load/:codehostId", LoadYamlTemplateFromCodeHost)
+ yaml.PUT("/load/:codehostId", SyncYamlTemplateFromCodeHost)
yaml.PUT("/:id", UpdateYamlTemplate)
yaml.PUT("/:id/variable", UpdateYamlTemplateVariable)
yaml.GET("", ListYamlTemplate)
diff --git a/pkg/microservice/aslan/core/templatestore/handler/yaml.go b/pkg/microservice/aslan/core/templatestore/handler/yaml.go
index 7951bdf939..e4f5f67497 100644
--- a/pkg/microservice/aslan/core/templatestore/handler/yaml.go
+++ b/pkg/microservice/aslan/core/templatestore/handler/yaml.go
@@ -19,6 +19,8 @@ package handler
import (
"encoding/json"
"fmt"
+ "strconv"
+ "strings"
"github.com/gin-gonic/gin"
"github.com/koderover/zadig/v2/pkg/types"
@@ -27,8 +29,19 @@ import (
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/template"
templateservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/templatestore/service"
internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler"
+ e "github.com/koderover/zadig/v2/pkg/tool/errors"
)
+type PreloadYamlTemplateFromCodeHostReq struct {
+ RepoOwner string `json:"repo_owner"`
+ RepoName string `json:"repo_name"`
+ NameSpace string `json:"namespace"`
+ RepoUUID string `json:"repo_uuid"`
+ BranchName string `json:"branch_name"`
+ RemoteName string `json:"remote_name"`
+ Paths []templateservice.PreloadYamlTemplatePath `json:"paths"`
+}
+
// @Summary Create yaml template
// @Description Create yaml template
// @Tags template
@@ -69,6 +82,189 @@ func CreateYamlTemplate(c *gin.Context) {
ctx.RespErr = templateservice.CreateYamlTemplate(req, ctx.Logger)
}
+// @Summary Preload yaml template from codehost
+// @Description Preload yaml template from codehost
+// @Tags template
+// @Accept json
+// @Produce json
+// @Param codehostId path int true "codehostId"
+// @Param repoName query string false "repoName"
+// @Param branchName query string false "branchName"
+// @Param repoOwner query string false "repoOwner"
+// @Param namespace query string false "namespace"
+// @Param remoteName query string false "remoteName"
+// @Param body body PreloadYamlTemplateFromCodeHostReq true "body"
+// @Success 200 {array} templateservice.LoadYamlTemplatePath
+// @Router /api/aslan/template/yaml/preload/{codehostId} [post]
+func PreloadYamlTemplateFromCodeHost(c *gin.Context) {
+ ctx := internalhandler.NewContext(c)
+ defer func() { internalhandler.JSONResponse(c, ctx) }()
+
+ codehostIDStr := c.Param("codehostId")
+ codehostID, err := strconv.Atoi(codehostIDStr)
+ if err != nil {
+ ctx.RespErr = e.ErrInvalidParam.AddDesc("cannot convert codehost id to int")
+ return
+ }
+
+ var req PreloadYamlTemplateFromCodeHostReq
+ if err := c.BindJSON(&req); err != nil {
+ ctx.RespErr = e.ErrInvalidParam.AddDesc("invalid PreloadYamlTemplateFromCodeHostReq json args")
+ return
+ }
+
+ if req.RepoName == "" && req.RepoUUID == "" {
+ ctx.RespErr = e.ErrInvalidParam.AddDesc("repoName and repoUUID cannot be empty at the same time")
+ return
+ }
+
+ ctx.Resp, ctx.RespErr = templateservice.PreloadYamlTemplateFromCodeHost(codehostID, req.RepoOwner, req.RepoName, req.RepoUUID, req.BranchName, req.RemoteName, req.Paths, ctx.Logger)
+}
+
+// @Summary Load yaml template from codehost
+// @Description Load yaml template from codehost
+// @Tags template
+// @Accept json
+// @Produce json
+// @Param codehostId path int true "codehostId"
+// @Param repoName query string true "repoName"
+// @Param branchName query string true "branchName"
+// @Param repoOwner query string true "repoOwner"
+// @Param namespace query string false "namespace"
+// @Param remoteName query string false "remoteName"
+// @Param body body templateservice.LoadYamlTemplateFromCodeHostReq true "body"
+// @Success 200
+// @Router /api/aslan/template/yaml/load/{codehostId} [post]
+func LoadYamlTemplateFromCodeHost(c *gin.Context) {
+ ctx, err := internalhandler.NewContextWithAuthorization(c)
+ defer func() { internalhandler.JSONResponse(c, ctx) }()
+
+ if err != nil {
+ ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err)
+ ctx.UnAuthorized = true
+ return
+ }
+
+ codehostIDStr := c.Param("codehostId")
+
+ codehostID, err := strconv.Atoi(codehostIDStr)
+ if err != nil {
+ ctx.RespErr = e.ErrInvalidParam.AddDesc("cannot convert codehost id to int")
+ return
+ }
+
+ repoName := c.Query("repoName")
+ repoUUID := c.Query("repoUUID")
+ if repoName == "" && repoUUID == "" {
+ ctx.RespErr = e.ErrInvalidParam.AddDesc("repoName and repoUUID cannot be empty at the same time")
+ return
+ }
+
+ branchName := c.Query("branchName")
+
+ args := new(templateservice.LoadYamlTemplateFromCodeHostReq)
+ if err := c.BindJSON(args); err != nil {
+ ctx.RespErr = e.ErrInvalidParam.AddDesc("invalid LoadYamlTemplateFromCodeHostReq json args")
+ return
+ }
+
+ remoteName := c.Query("remoteName")
+ repoOwner := c.Query("repoOwner")
+ namespace := c.Query("namespace")
+ if namespace == "" {
+ namespace = repoOwner
+ }
+
+ bs, _ := json.Marshal(args)
+ internalhandler.InsertOperationLog(c, ctx.UserName, "", "创建", "模板-YAML", "", "", string(bs), types.RequestBodyTypeJSON, ctx.Logger)
+
+ if !ctx.Resources.IsSystemAdmin {
+ if !ctx.Resources.SystemActions.Template.Create {
+ ctx.UnAuthorized = true
+ return
+ }
+ }
+
+ ctx.RespErr = templateservice.LoadYamlTemplateFromCodeHost(ctx.UserName, codehostID, repoOwner, namespace, repoName, repoUUID, branchName, remoteName, args, false, ctx.Logger)
+}
+
+// @Summary Sync yaml template from codehost
+// @Description Sync yaml template from codehost
+// @Tags template
+// @Accept json
+// @Produce json
+// @Param codehostId path int true "codehostId"
+// @Param repoName query string true "repoName"
+// @Param branchName query string true "branchName"
+// @Param repoOwner query string true "repoOwner"
+// @Param namespace query string false "namespace"
+// @Param remoteName query string false "remoteName"
+// @Param body body templateservice.LoadYamlTemplateFromCodeHostReq true "body"
+// @Success 200
+// @Router /api/aslan/template/yaml/load/{codehostId} [put]
+func SyncYamlTemplateFromCodeHost(c *gin.Context) {
+ ctx, err := internalhandler.NewContextWithAuthorization(c)
+ defer func() { internalhandler.JSONResponse(c, ctx) }()
+
+ if err != nil {
+ ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err)
+ ctx.UnAuthorized = true
+ return
+ }
+
+ codehostIDStr := c.Param("codehostId")
+
+ codehostID, err := strconv.Atoi(codehostIDStr)
+ if err != nil {
+ ctx.RespErr = e.ErrInvalidParam.AddDesc("cannot convert codehost id to string")
+ return
+ }
+
+ repoName := c.Query("repoName")
+ repoUUID := c.Query("repoUUID")
+ if repoName == "" && repoUUID == "" {
+ ctx.RespErr = e.ErrInvalidParam.AddDesc("repoName and repoUUID cannot be empty at the same time")
+ return
+ }
+
+ branchName := c.Query("branchName")
+
+ args := new(templateservice.LoadYamlTemplateFromCodeHostReq)
+ if err := c.BindJSON(args); err != nil {
+ ctx.RespErr = e.ErrInvalidParam.AddDesc("invalid LoadYamlTemplateFromCodeHostReq json args")
+ return
+ }
+
+ remoteName := c.Query("remoteName")
+ repoOwner := c.Query("repoOwner")
+ namespace := c.Query("namespace")
+ if namespace == "" {
+ namespace = repoOwner
+ }
+
+ if len(args.Paths) != 1 {
+ ctx.RespErr = e.ErrInvalidParam.AddDesc("paths must contain only one path")
+ return
+ }
+
+ bs, _ := json.Marshal(args)
+ templateNames := make([]string, 0, len(args.Paths))
+ for _, loadPath := range args.Paths {
+ templateNames = append(templateNames, loadPath.Name)
+ }
+ templateNameStr := strings.Join(templateNames, ",")
+ internalhandler.InsertOperationLog(c, ctx.UserName, "", "更新", "模板-YAML", templateNameStr, templateNameStr, string(bs), types.RequestBodyTypeJSON, ctx.Logger)
+
+ if !ctx.Resources.IsSystemAdmin {
+ if !ctx.Resources.SystemActions.Template.Edit {
+ ctx.UnAuthorized = true
+ return
+ }
+ }
+
+ ctx.RespErr = templateservice.LoadYamlTemplateFromCodeHost(ctx.UserName, codehostID, repoOwner, namespace, repoName, repoUUID, branchName, remoteName, args, true, ctx.Logger)
+}
+
// @Summary Update yaml template
// @Description Update yaml template
// @Tags template
diff --git a/pkg/microservice/aslan/core/templatestore/service/workflow.go b/pkg/microservice/aslan/core/templatestore/service/workflow.go
index a2be877c65..ff2fcb1abe 100644
--- a/pkg/microservice/aslan/core/templatestore/service/workflow.go
+++ b/pkg/microservice/aslan/core/templatestore/service/workflow.go
@@ -972,7 +972,8 @@ func InitWorkflowTemplateInfos() []*commonmodels.WorkflowV4Template {
Name: "test",
JobType: config.JobZadigTesting,
Spec: commonmodels.ZadigTestingJobSpec{
- TestType: " ",
+ TestType: "",
+ Source: config.SourceRuntime,
},
},
},
@@ -1107,7 +1108,10 @@ func InitWorkflowTemplateInfos() []*commonmodels.WorkflowV4Template {
{
Name: "code-scanning",
JobType: config.JobZadigScanning,
- Spec: commonmodels.ZadigScanningJobSpec{},
+ Spec: commonmodels.ZadigScanningJobSpec{
+ Source: config.SourceRuntime,
+ ScanningType: config.NormalScanningType,
+ },
},
},
},
@@ -1214,7 +1218,8 @@ func InitWorkflowTemplateInfos() []*commonmodels.WorkflowV4Template {
Name: "test",
JobType: config.JobZadigTesting,
Spec: commonmodels.ZadigTestingJobSpec{
- TestType: " ",
+ TestType: "",
+ Source: config.SourceRuntime,
},
},
},
@@ -1247,7 +1252,7 @@ func InitWorkflowTemplateInfos() []*commonmodels.WorkflowV4Template {
Name: "test",
JobType: config.JobZadigTesting,
Spec: commonmodels.ZadigTestingJobSpec{
- TestType: " ",
+ TestType: "",
},
},
},
@@ -1725,7 +1730,7 @@ func InitWorkflowTemplateInfos() []*commonmodels.WorkflowV4Template {
Name: "test",
JobType: config.JobZadigTesting,
Spec: commonmodels.ZadigTestingJobSpec{
- TestType: " ",
+ TestType: "",
},
},
},
@@ -1784,7 +1789,7 @@ func InitWorkflowTemplateInfos() []*commonmodels.WorkflowV4Template {
Name: "test",
JobType: config.JobZadigTesting,
Spec: commonmodels.ZadigTestingJobSpec{
- TestType: " ",
+ TestType: "",
},
},
},
diff --git a/pkg/microservice/aslan/core/templatestore/service/yaml.go b/pkg/microservice/aslan/core/templatestore/service/yaml.go
index d930ab7cab..4dedad2837 100644
--- a/pkg/microservice/aslan/core/templatestore/service/yaml.go
+++ b/pkg/microservice/aslan/core/templatestore/service/yaml.go
@@ -19,17 +19,29 @@ package service
import (
"errors"
"fmt"
+ "io/ioutil"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
"go.uber.org/zap"
"gopkg.in/yaml.v3"
+ "github.com/koderover/zadig/v2/pkg/microservice/aslan/config"
"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"
+ commmonservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service"
+ "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/command"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/template"
commontypes "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/types"
commonutil "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/service/service"
"github.com/koderover/zadig/v2/pkg/setting"
+ "github.com/koderover/zadig/v2/pkg/shared/client/systemconfig"
+ "github.com/koderover/zadig/v2/pkg/tool/gerrit"
+ "github.com/koderover/zadig/v2/pkg/tool/gitee"
+ "github.com/koderover/zadig/v2/pkg/util"
"github.com/koderover/zadig/v2/pkg/util/converter"
yamlutil "github.com/koderover/zadig/v2/pkg/util/yaml"
)
@@ -39,6 +51,656 @@ var DefaultSystemVariable = map[string]string{
setting.TemplateVariableService: setting.TemplateVariableServiceDescription,
}
+type LoadYamlTemplateFromCodeHostReq struct {
+ Paths []LoadYamlTemplatePath `json:"paths"`
+}
+
+type LoadYamlTemplatePath struct {
+ Name string `json:"name"`
+ Path string `json:"path"`
+ IsDir bool `json:"is_dir"`
+}
+
+type PreloadYamlTemplatePath struct {
+ Path string `json:"path"`
+ IsDir bool `json:"is_dir"`
+}
+
+type loadedYamlTemplateContent struct {
+ Path LoadYamlTemplatePath
+ Content string
+ Commit *models.Commit
+}
+
+func PreloadYamlTemplateFromCodeHost(codehostID int, repoOwner, repoName, repoUUID, branchName, remoteName string, loadPaths []PreloadYamlTemplatePath, logger *zap.SugaredLogger) ([]LoadYamlTemplatePath, error) {
+ var templates []LoadYamlTemplatePath
+
+ ch, err := systemconfig.New().GetCodeHost(codehostID)
+ if err != nil {
+ logger.Errorf("failed to get codehost %d for yaml template preload, err: %s", codehostID, err)
+ return nil, fmt.Errorf("failed to get codehost %d, err: %w", codehostID, err)
+ }
+
+ switch ch.Type {
+ case setting.SourceFromGithub, setting.SourceFromGitlab:
+ templates, err = preloadYamlTemplatesFromTreeGetter(ch, repoOwner, repoName, branchName, loadPaths, logger)
+ case setting.SourceFromGerrit:
+ templates, err = preloadYamlTemplatesFromGerrit(ch, repoOwner, repoName, branchName, remoteName, loadPaths, logger)
+ case setting.SourceFromGitee, setting.SourceFromGiteeEE:
+ templates, err = preloadYamlTemplatesFromGitee(ch, repoOwner, repoName, branchName, remoteName, loadPaths, logger)
+ default:
+ logger.Errorf("unsupported code source: %s", ch.Type)
+ return nil, fmt.Errorf("unsupported code source: %s", ch.Type)
+ }
+
+ return templates, err
+}
+
+func preloadYamlTemplatesFromTreeGetter(ch *systemconfig.CodeHost, repoOwner, repoName, branchName string, paths []PreloadYamlTemplatePath, logger *zap.SugaredLogger) ([]LoadYamlTemplatePath, error) {
+ logger.Infof("Preloading yaml template from codehost %d with namespace %s, repo %s, branch %s and path %v", ch.ID, repoOwner, repoName, branchName, paths)
+
+ loader, err := commonutil.GetYAMLLoader(ch)
+ if err != nil {
+ logger.Errorf("Failed to create loader client, err: %s", err)
+ return nil, err
+ }
+
+ resp := make([]LoadYamlTemplatePath, 0)
+ for _, loadPath := range paths {
+ if !loadPath.IsDir {
+ if !commonutil.IsYaml(loadPath.Path) {
+ return nil, fmt.Errorf("file is not of type yaml or yml, select again")
+ }
+
+ resp = append(resp, LoadYamlTemplatePath{
+ Name: getFileName(loadPath.Path),
+ Path: loadPath.Path,
+ IsDir: loadPath.IsDir,
+ })
+ } else {
+ treeNodes, err := loader.GetTree(repoOwner, repoName, loadPath.Path, branchName)
+ if err != nil {
+ logger.Errorf("failed to get tree under path %s, err: %s", loadPath.Path, err)
+ return nil, fmt.Errorf("failed to get tree under path %s, err: %s", loadPath.Path, err)
+ }
+
+ folders, files := commonutil.GetFoldersAndYAMLFiles(treeNodes)
+
+ if len(files) > 0 {
+ resp = append(resp, LoadYamlTemplatePath{
+ Name: getFileName(loadPath.Path),
+ Path: loadPath.Path,
+ IsDir: loadPath.IsDir,
+ })
+ } else if len(folders) > 0 {
+ for _, f := range folders {
+ tns, err := loader.GetTree(repoOwner, repoName, f.FullPath, branchName)
+ if err != nil {
+ logger.Errorf("Failed to get tree under path %s, err: %s", f.FullPath, err)
+ return nil, fmt.Errorf("Failed to get tree under path %s, err: %s", f.FullPath, err)
+ }
+
+ if commonutil.HasYAMLFiles(tns) {
+ resp = append(resp, LoadYamlTemplatePath{
+ Name: getFileName(f.FullPath),
+ Path: f.FullPath,
+ IsDir: f.IsDir,
+ })
+ }
+ }
+ }
+ }
+ }
+
+ if len(resp) == 0 {
+ logger.Errorf("no valid yaml is found under paths %v", paths)
+ return nil, fmt.Errorf("no valid yaml is found under paths")
+ }
+
+ return resp, nil
+}
+
+func preloadYamlTemplatesFromGerrit(ch *systemconfig.CodeHost, repoOwner, repoName, branchName, remoteName string, paths []PreloadYamlTemplatePath, logger *zap.SugaredLogger) ([]LoadYamlTemplatePath, error) {
+ resp := make([]LoadYamlTemplatePath, 0)
+
+ if remoteName == "" {
+ remoteName = "origin"
+ }
+
+ base := path.Join(config.S3StoragePath(), strings.Replace(repoName, "/", "-", -1))
+
+ if _, err := os.Stat(base); os.IsNotExist(err) {
+ err = command.RunGitCmds(ch, setting.GerritDefaultOwner, setting.GerritDefaultOwner, repoName, branchName, remoteName)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ for _, loadPath := range paths {
+ filePath := path.Join(base, loadPath.Path)
+
+ if !loadPath.IsDir {
+ if !commonutil.IsYaml(loadPath.Path) {
+ logger.Errorf("file is not of type yaml or yml, select again")
+ return nil, fmt.Errorf("file is not of type yaml or yml, select again")
+ }
+
+ pathSegment := strings.Split(loadPath.Path, "/")
+ fileName := pathSegment[len(pathSegment)-1]
+
+ resp = append(resp, LoadYamlTemplatePath{
+ Name: getFileName(fileName),
+ Path: loadPath.Path,
+ IsDir: loadPath.IsDir,
+ })
+ } else {
+ fileInfos, err := ioutil.ReadDir(filePath)
+ if err != nil {
+ logger.Errorf("failed to read yaml directory %s, err: %s", loadPath.Path, err)
+ return nil, fmt.Errorf("failed to read yaml directory %s, err: %s", loadPath.Path, err)
+ }
+
+ if commonutil.IsValidServiceDir(fileInfos) {
+ name := loadPath.Path
+ if loadPath.Path == "" {
+ name = repoName
+ }
+
+ pathList := strings.Split(name, "/")
+ folderName := pathList[len(pathList)-1]
+ resp = append(resp, LoadYamlTemplatePath{
+ Name: folderName,
+ Path: loadPath.Path,
+ IsDir: loadPath.IsDir,
+ })
+ return resp, nil
+ }
+
+ isGrandParent := false
+ for _, file := range fileInfos {
+ if file.IsDir() {
+ subDirPath := fmt.Sprintf("%s/%s", filePath, file.Name())
+ subtree, err := ioutil.ReadDir(subDirPath)
+ if err != nil {
+ logger.Errorf("failed to get subtree fromwith path %s, err: %s", subDirPath, err)
+ return nil, fmt.Errorf("failed to get subtree fromwith path %s, err: %s", subDirPath, err)
+ }
+
+ if commonutil.IsValidServiceDir(subtree) {
+ resp = append(resp, LoadYamlTemplatePath{
+ Name: getFileName(file.Name()),
+ Path: loadPath.Path,
+ IsDir: loadPath.IsDir,
+ })
+ isGrandParent = true
+ }
+ }
+ }
+
+ if !isGrandParent {
+ logger.Errorf("no valid yaml is found under directory %s", filePath)
+ return nil, fmt.Errorf("no valid yaml is found under directory %s", filePath)
+ }
+ }
+ }
+
+ return resp, nil
+}
+
+func preloadYamlTemplatesFromGitee(ch *systemconfig.CodeHost, repoOwner, repoName, branchName, remoteName string, paths []PreloadYamlTemplatePath, logger *zap.SugaredLogger) ([]LoadYamlTemplatePath, error) {
+ resp := make([]LoadYamlTemplatePath, 0)
+
+ if remoteName == "" {
+ remoteName = "origin"
+ }
+
+ base := path.Join(config.S3StoragePath(), strings.Replace(repoName, "/", "-", -1))
+
+ if exist, err := util.PathExists(base); !exist {
+ logger.Warnf("path does not exist,err:%s", err)
+ err = command.RunGitCmds(ch, repoOwner, repoOwner, repoName, branchName, remoteName)
+ if err != nil {
+ return nil, fmt.Errorf("failed to clone code, err: %s", err.Error())
+ }
+ }
+
+ for _, loadPath := range paths {
+ filePath := path.Join(base, loadPath.Path)
+
+ if !loadPath.IsDir {
+ if !commonutil.IsYaml(loadPath.Path) {
+ logger.Errorf("file is not of type yaml or yml, select again")
+ return nil, fmt.Errorf("file is not of type yaml or yml, select again")
+ }
+
+ pathSegment := strings.Split(loadPath.Path, "/")
+ fileName := pathSegment[len(pathSegment)-1]
+
+ resp = append(resp, LoadYamlTemplatePath{
+ Name: getFileName(fileName),
+ Path: loadPath.Path,
+ IsDir: loadPath.IsDir,
+ })
+ } else {
+ fileInfos, err := ioutil.ReadDir(filePath)
+ if err != nil {
+ logger.Errorf("failed to read yaml directory %s, err: %s", loadPath.Path, err)
+ os.RemoveAll(base)
+ return nil, fmt.Errorf("failed to read yaml directory %s, err: %s", loadPath.Path, err)
+ }
+
+ if commonutil.IsValidServiceDir(fileInfos) {
+ name := loadPath.Path
+ if loadPath.Path == "" {
+ name = repoName
+ }
+ pathList := strings.Split(name, "/")
+ folderName := pathList[len(pathList)-1]
+ resp = append(resp, LoadYamlTemplatePath{
+ Name: folderName,
+ Path: loadPath.Path,
+ IsDir: loadPath.IsDir,
+ })
+ return resp, nil
+ }
+
+ isGrandParent := false
+ for _, file := range fileInfos {
+ if file.IsDir() {
+ subDirPath := fmt.Sprintf("%s/%s", filePath, file.Name())
+ subtree, err := ioutil.ReadDir(subDirPath)
+ if err != nil {
+ logger.Errorf("failed to get subtree fromwith path %s, err: %s", subDirPath, err)
+ return nil, fmt.Errorf("failed to get subtree fromwith path %s, err: %s", subDirPath, err)
+ }
+ if commonutil.IsValidServiceDir(subtree) {
+ resp = append(resp, LoadYamlTemplatePath{
+ Name: getFileName(file.Name()),
+ Path: loadPath.Path,
+ IsDir: loadPath.IsDir,
+ })
+ isGrandParent = true
+ }
+ }
+ }
+ if !isGrandParent {
+ logger.Errorf("no valid yaml is found under directory %s", filePath)
+ return nil, fmt.Errorf("no valid yaml is found under directory %s", filePath)
+ }
+ }
+ }
+
+ return resp, nil
+}
+
+func getFileName(fullName string) string {
+ name := filepath.Base(fullName)
+ ext := filepath.Ext(name)
+ return name[0:(len(name) - len(ext))]
+}
+
+func LoadYamlTemplateFromCodeHost(username string, codehostID int, repoOwner, namespace, repoName, repoUUID, branchName, remoteName string, args *LoadYamlTemplateFromCodeHostReq, isSync bool, logger *zap.SugaredLogger) error {
+ ch, err := systemconfig.New().GetCodeHost(codehostID)
+ if err != nil {
+ logger.Errorf("failed to get codehost %d, err: %s", codehostID, err)
+ return fmt.Errorf("failed to get codehost %d, err: %s", codehostID, err)
+ }
+
+ if isSync {
+ yamlTemplate, err := commonrepo.NewYamlTemplateColl().GetByName(args.Paths[0].Name)
+ if err != nil {
+ logger.Errorf("failed to query yaml template, err: %s", err)
+ return fmt.Errorf("failed to query yaml template, err: %w", err)
+ }
+
+ if yamlTemplate.Name != args.Paths[0].Name {
+ return fmt.Errorf("yaml template name mismatch")
+ }
+ }
+
+ switch ch.Type {
+ case setting.SourceFromGithub, setting.SourceFromGitlab:
+ return loadYamlTemplateFromTreeGetter(ch, repoOwner, namespace, repoName, branchName, remoteName, args, isSync, logger)
+ case setting.SourceFromGerrit:
+ return loadYamlTemplateFromGerrit(ch, repoOwner, namespace, repoName, branchName, remoteName, args, isSync, logger)
+ case setting.SourceFromGitee, setting.SourceFromGiteeEE:
+ return loadYamlTemplateFromGitee(ch, repoOwner, namespace, repoName, branchName, remoteName, args, isSync, logger)
+ default:
+ logger.Errorf("unsupported code source: %s", ch.Type)
+ return fmt.Errorf("unsupported code source: %s", ch.Type)
+ }
+}
+
+func loadYamlTemplateFromTreeGetter(ch *systemconfig.CodeHost, repoOwner, namespace, repoName, branchName, remoteName string, args *LoadYamlTemplateFromCodeHostReq, isSync bool, logger *zap.SugaredLogger) error {
+ logger.Infof("Loading yaml template from codehost %d with owner %s, namespace %s, repo %s, branch %s and path %v", ch.ID, repoOwner, namespace, repoName, branchName, args.Paths)
+
+ loader, err := commonutil.GetYAMLLoader(ch)
+ if err != nil {
+ logger.Errorf("failed to get yaml loader for codehost %d, err: %s", ch.ID, err)
+ return err
+ }
+
+ contents := make([]*loadedYamlTemplateContent, 0, len(args.Paths))
+ for _, loadPath := range args.Paths {
+ if !loadPath.IsDir {
+ yamls, err := loader.GetYAMLContents(namespace, repoName, loadPath.Path, branchName, false, false)
+ if err != nil {
+ logger.Errorf("failed to get yaml content under path %s, err: %s", loadPath.Path, err)
+ return err
+ }
+ contents = append(contents, &loadedYamlTemplateContent{
+ Path: loadPath,
+ Content: util.CombineManifests(yamls),
+ })
+ } else {
+ treeNodes, err := loader.GetTree(namespace, repoName, loadPath.Path, branchName)
+ if err != nil {
+ logger.Errorf("failed to get tree under path %s, err: %s", loadPath.Path, err)
+ return err
+ }
+
+ _, files := commonutil.GetFoldersAndYAMLFiles(treeNodes)
+
+ if len(files) > 0 {
+ yamls := make([]string, 0)
+ for _, file := range files {
+ res, err := loader.GetYAMLContents(namespace, repoName, file.FullPath, branchName, false, false)
+ if err != nil {
+ logger.Errorf("failed to get yaml content under path %s, err: %s", file.FullPath, err)
+ return err
+ }
+ yamls = append(yamls, res...)
+ }
+ contents = append(contents, &loadedYamlTemplateContent{
+ Path: loadPath,
+ Content: util.CombineManifests(yamls),
+ })
+ } else {
+ logger.Errorf("no yaml file is found under directory %s", loadPath.Path)
+ return fmt.Errorf("no yaml file is found under directory %s", loadPath.Path)
+ }
+ }
+ }
+
+ for _, content := range contents {
+
+ if len(content.Content) == 0 {
+ continue
+ }
+
+ commit, err := loader.GetLatestRepositoryCommit(namespace, repoName, content.Path.Path, branchName)
+ if err != nil {
+ logger.Errorf("failed to get latest commit under path %s, err: %s", content.Path.Path, err)
+ return err
+ }
+ commitInfo := &models.Commit{SHA: commit.SHA, Message: commit.Message}
+
+ templateObj := &template.YamlTemplate{
+ Source: ch.Type,
+ CodehostID: ch.ID,
+ RepoOwner: repoOwner,
+ Namespace: namespace,
+ RepoName: repoName,
+ BranchName: branchName,
+ RemoteName: remoteName,
+ Name: content.Path.Name,
+ Content: content.Content,
+ Path: content.Path.Path,
+ LoadFromDir: content.Path.IsDir,
+ Commit: commitInfo,
+ }
+
+ if isSync {
+ origin, err := commonrepo.NewYamlTemplateColl().GetByName(templateObj.Name)
+ if err != nil {
+ return fmt.Errorf("failed to find template by name: %s, err: %w", templateObj.Name, err)
+ }
+ if err := UpdateYamlTemplate(origin.ID.Hex(), templateObj, logger); err != nil {
+ return err
+ }
+ continue
+ } else {
+ if err := CreateYamlTemplate(templateObj, logger); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+func loadYamlTemplateFromGerrit(ch *systemconfig.CodeHost, repoOwner, namespace, repoName, branchName, remoteName string, args *LoadYamlTemplateFromCodeHostReq, isSync bool, logger *zap.SugaredLogger) error {
+ if remoteName == "" {
+ remoteName = "origin"
+ }
+
+ base := path.Join(config.S3StoragePath(), strings.Replace(repoName, "/", "-", -1))
+ _ = os.RemoveAll(base)
+
+ if err := command.RunGitCmds(ch, repoOwner, repoOwner, repoName, branchName, remoteName); err != nil {
+ logger.Errorf("failed to clone gerrit repo %s branch %s, err: %s", repoName, branchName, err)
+ return err
+ }
+
+ gerritCli := gerrit.NewClient(ch.Address, ch.AccessToken, config.ProxyHTTPSAddr(), ch.EnableProxy)
+ commit, err := gerritCli.GetCommitByBranch(repoName, branchName)
+ if err != nil {
+ logger.Errorf("failed to get latest commit info from repo %s, err: %s", repoName, err)
+ return err
+ }
+ commitInfo := &models.Commit{
+ SHA: commit.Commit,
+ Message: commit.Message,
+ }
+
+ for _, loadPath := range args.Paths {
+ if !loadPath.IsDir {
+
+ content, err := os.ReadFile(path.Join(base, loadPath.Path))
+ if err != nil {
+ logger.Errorf("failed to read yaml file %s, err: %s", loadPath.Path, err)
+ return err
+ }
+ templateObj := &template.YamlTemplate{
+ Source: ch.Type,
+ CodehostID: ch.ID,
+ RepoOwner: repoOwner,
+ Namespace: namespace,
+ RepoName: repoName,
+ BranchName: branchName,
+ RemoteName: remoteName,
+ Name: loadPath.Name,
+ Content: string(content),
+ Path: loadPath.Path,
+ LoadFromDir: loadPath.IsDir,
+ Commit: commitInfo,
+ }
+
+ if isSync {
+ origin, err := commonrepo.NewYamlTemplateColl().GetByName(templateObj.Name)
+ if err != nil {
+ return fmt.Errorf("failed to find template by name: %s, err: %w", templateObj.Name, err)
+ }
+ if err := UpdateYamlTemplate(origin.ID.Hex(), templateObj, logger); err != nil {
+ return err
+ }
+ } else if err := CreateYamlTemplate(templateObj, logger); err != nil {
+ return err
+ }
+
+ } else {
+ filePath := path.Join(base, loadPath.Path)
+ fileInfos, err := ioutil.ReadDir(filePath)
+ if err != nil {
+ logger.Errorf("failed to read yaml directory %s, err: %s", loadPath.Path, err)
+ return err
+ }
+ if commonutil.IsValidServiceDir(fileInfos) {
+ yamls := make([]string, 0)
+ for _, fileInfo := range fileInfos {
+ if fileInfo.IsDir() || !commonutil.IsYaml(fileInfo.Name()) {
+ continue
+ }
+
+ content, err := os.ReadFile(path.Join(filePath, fileInfo.Name()))
+ if err != nil {
+ logger.Errorf("failed to read yaml file %s, err: %s", path.Join(loadPath.Path, fileInfo.Name()), err)
+ return err
+ }
+ yamls = append(yamls, string(content))
+ }
+
+ templateObj := &template.YamlTemplate{
+ Source: ch.Type,
+ CodehostID: ch.ID,
+ RepoOwner: repoOwner,
+ Namespace: namespace,
+ RepoName: repoName,
+ BranchName: branchName,
+ RemoteName: remoteName,
+ Name: loadPath.Name,
+ Content: util.CombineManifests(yamls),
+ Path: loadPath.Path,
+ LoadFromDir: loadPath.IsDir,
+ Commit: commitInfo,
+ }
+
+ if isSync {
+ origin, err := commonrepo.NewYamlTemplateColl().GetByName(templateObj.Name)
+ if err != nil {
+ return fmt.Errorf("failed to find template by name: %s, err: %w", templateObj.Name, err)
+ }
+ if err := UpdateYamlTemplate(origin.ID.Hex(), templateObj, logger); err != nil {
+ return err
+ }
+ } else if err := CreateYamlTemplate(templateObj, logger); err != nil {
+ return err
+ }
+ } else {
+ logger.Errorf("no valid yaml is found under directory %s", loadPath.Path)
+ return fmt.Errorf("no valid yaml is found under directory %s", loadPath.Path)
+ }
+ }
+ }
+ return nil
+}
+
+func loadYamlTemplateFromGitee(ch *systemconfig.CodeHost, repoOwner, namespace, repoName, branchName, remoteName string, args *LoadYamlTemplateFromCodeHostReq, isSync bool, logger *zap.SugaredLogger) error {
+ if remoteName == "" {
+ remoteName = "origin"
+ }
+
+ base := path.Join(config.S3StoragePath(), repoName)
+ if _, err := os.Stat(base); os.IsNotExist(err) {
+ if err := command.RunGitCmds(ch, repoOwner, repoName, repoName, branchName, remoteName); err != nil {
+ logger.Errorf("failed to clone gitee repo %s/%s branch %s, err: %s", repoOwner, repoName, branchName, err)
+ return err
+ }
+ }
+
+ giteeCli := gitee.NewClient(ch.ID, ch.Address, ch.AccessToken, config.ProxyHTTPSAddr(), ch.EnableProxy)
+ branch, err := giteeCli.GetSingleBranch(ch.Address, ch.AccessToken, repoOwner, repoName, branchName)
+ if err != nil {
+ logger.Errorf("failed to get latest commit info from repo %s, err: %s", repoName, err)
+ return err
+ }
+ commitInfo := &models.Commit{
+ SHA: branch.Commit.Sha,
+ Message: branch.Commit.Commit.Message,
+ }
+
+ for _, loadPath := range args.Paths {
+ if !loadPath.IsDir {
+ content, err := os.ReadFile(path.Join(base, loadPath.Path))
+ if err != nil {
+ logger.Errorf("failed to read yaml file %s, err: %s", loadPath.Path, err)
+ return err
+ }
+ templateObj := &template.YamlTemplate{
+ Source: ch.Type,
+ CodehostID: ch.ID,
+ RepoOwner: repoOwner,
+ Namespace: namespace,
+ RepoName: repoName,
+ BranchName: branchName,
+ RemoteName: remoteName,
+ Name: loadPath.Name,
+ Content: string(content),
+ Path: loadPath.Path,
+ LoadFromDir: loadPath.IsDir,
+ Commit: commitInfo,
+ }
+
+ if isSync {
+ origin, err := commonrepo.NewYamlTemplateColl().GetByName(templateObj.Name)
+ if err != nil {
+ return fmt.Errorf("failed to find template by name: %s, err: %w", templateObj.Name, err)
+ }
+ if err := UpdateYamlTemplate(origin.ID.Hex(), templateObj, logger); err != nil {
+ return err
+ }
+ } else if err := CreateYamlTemplate(templateObj, logger); err != nil {
+ return err
+ }
+
+ } else {
+ filePath := path.Join(base, loadPath.Path)
+ fileInfos, err := ioutil.ReadDir(filePath)
+ if err != nil {
+ logger.Errorf("failed to read yaml directory %s, err: %s", loadPath.Path, err)
+ return err
+ }
+ if commonutil.IsValidServiceDir(fileInfos) {
+ yamls := make([]string, 0)
+ for _, fileInfo := range fileInfos {
+ if fileInfo.IsDir() || !commonutil.IsYaml(fileInfo.Name()) {
+ continue
+ }
+
+ content, err := os.ReadFile(path.Join(filePath, fileInfo.Name()))
+ if err != nil {
+ logger.Errorf("failed to read yaml file %s, err: %s", path.Join(loadPath.Path, fileInfo.Name()), err)
+ return err
+ }
+ yamls = append(yamls, string(content))
+ }
+
+ templateObj := &template.YamlTemplate{
+ Source: ch.Type,
+ CodehostID: ch.ID,
+ RepoOwner: repoOwner,
+ Namespace: namespace,
+ RepoName: repoName,
+ BranchName: branchName,
+ RemoteName: remoteName,
+ Name: loadPath.Name,
+ Content: util.CombineManifests(yamls),
+ Path: loadPath.Path,
+ LoadFromDir: loadPath.IsDir,
+ Commit: commitInfo,
+ }
+
+ if isSync {
+ origin, err := commonrepo.NewYamlTemplateColl().GetByName(templateObj.Name)
+ if err != nil {
+ return fmt.Errorf("failed to find template by name: %s, err: %w", templateObj.Name, err)
+ }
+ if err := UpdateYamlTemplate(origin.ID.Hex(), templateObj, logger); err != nil {
+ return err
+ }
+ } else if err := CreateYamlTemplate(templateObj, logger); err != nil {
+ return err
+ }
+ } else {
+ logger.Errorf("no valid yaml is found under directory %s", loadPath.Path)
+ return fmt.Errorf("no valid yaml is found under directory %s", loadPath.Path)
+ }
+ }
+ }
+
+ return nil
+}
+
func CreateYamlTemplate(template *template.YamlTemplate, logger *zap.SugaredLogger) error {
extractVariableYmal, err := yamlutil.ExtractVariableYaml(template.Content)
if err != nil {
@@ -49,16 +711,33 @@ func CreateYamlTemplate(template *template.YamlTemplate, logger *zap.SugaredLogg
return fmt.Errorf("failed to convert variable yaml to service variable kv, err: %w", err)
}
- err = commonrepo.NewYamlTemplateColl().Create(&models.YamlTemplate{
+ created := &models.YamlTemplate{
Name: template.Name,
Content: template.Content,
+ Source: template.Source,
+ RepoOwner: template.RepoOwner,
+ Namespace: template.Namespace,
+ RepoName: template.RepoName,
+ Path: template.Path,
+ BranchName: template.BranchName,
+ RemoteName: template.RemoteName,
+ CodeHostID: template.CodehostID,
+ LoadFromDir: template.LoadFromDir,
+ Commit: template.Commit,
VariableYaml: extractVariableYmal,
ServiceVariableKVs: extractServiceVariableKVs,
- })
+ }
+
+ err = commonrepo.NewYamlTemplateColl().Create(created)
if err != nil {
logger.Errorf("create dockerfile template error: %s", err)
+ return err
}
- return err
+
+ if err := commmonservice.ProcessYamlTemplateWebhook(created, nil, logger); err != nil {
+ logger.Errorf("failed to process yaml template webhook for create %s, err: %s", created.Name, err)
+ }
+ return nil
}
func UpdateYamlTemplate(id string, template *template.YamlTemplate, logger *zap.SugaredLogger) error {
@@ -81,19 +760,33 @@ func UpdateYamlTemplate(id string, template *template.YamlTemplate, logger *zap.
return fmt.Errorf("failed to merge service variables, err %w", err)
}
- err = commonrepo.NewYamlTemplateColl().Update(
- id,
- &models.YamlTemplate{
- Name: template.Name,
- Content: template.Content,
- VariableYaml: template.VariableYaml,
- ServiceVariableKVs: template.ServiceVariableKVs,
- },
- )
+ updated := &models.YamlTemplate{
+ Name: template.Name,
+ Content: template.Content,
+ Source: template.Source,
+ CodeHostID: template.CodehostID,
+ RepoOwner: template.RepoOwner,
+ Namespace: template.Namespace,
+ RepoName: template.RepoName,
+ BranchName: template.BranchName,
+ RemoteName: template.RemoteName,
+ LoadFromDir: template.LoadFromDir,
+ Path: template.Path,
+ Commit: template.Commit,
+ VariableYaml: template.VariableYaml,
+ ServiceVariableKVs: template.ServiceVariableKVs,
+ }
+
+ err = commonrepo.NewYamlTemplateColl().Update(id, updated)
if err != nil {
logger.Errorf("update yaml template error: %s", err)
+ return err
}
- return err
+
+ if err := commmonservice.ProcessYamlTemplateWebhook(updated, origin, logger); err != nil {
+ logger.Errorf("failed to process yaml template webhook for update %s, err: %s", updated.Name, err)
+ }
+ return nil
}
func UpdateYamlTemplateVariable(id string, template *template.YamlTemplate, logger *zap.SugaredLogger) error {
@@ -123,8 +816,18 @@ func ListYamlTemplate(pageNum, pageSize int, logger *zap.SugaredLogger) ([]*temp
}
for _, obj := range templateList {
resp = append(resp, &template.YamlListObject{
- ID: obj.ID.Hex(),
- Name: obj.Name,
+ ID: obj.ID.Hex(),
+ Name: obj.Name,
+ Source: obj.Source,
+ CodehostID: obj.CodeHostID,
+ RepoOwner: obj.RepoOwner,
+ Namespace: obj.Namespace,
+ Repo: obj.RepoName,
+ Path: obj.Path,
+ Branch: obj.BranchName,
+ RemoteName: obj.RemoteName,
+ LoadFromDir: obj.LoadFromDir,
+ Commit: obj.Commit,
})
}
return resp, total, err
@@ -140,6 +843,16 @@ func GetYamlTemplateDetail(id string, logger *zap.SugaredLogger) (*template.Yaml
resp.ID = yamlTemplate.ID.Hex()
resp.Name = yamlTemplate.Name
resp.Content = yamlTemplate.Content
+ resp.Source = yamlTemplate.Source
+ resp.CodehostID = yamlTemplate.CodeHostID
+ resp.RepoOwner = yamlTemplate.RepoOwner
+ resp.Namespace = yamlTemplate.Namespace
+ resp.RepoName = yamlTemplate.RepoName
+ resp.Path = yamlTemplate.Path
+ resp.BranchName = yamlTemplate.BranchName
+ resp.RemoteName = yamlTemplate.RemoteName
+ resp.LoadFromDir = yamlTemplate.LoadFromDir
+ resp.Commit = yamlTemplate.Commit
resp.VariableYaml = yamlTemplate.VariableYaml
resp.ServiceVariableKVs = yamlTemplate.ServiceVariableKVs
return resp, err
@@ -163,11 +876,22 @@ func DeleteYamlTemplate(id string, logger *zap.SugaredLogger) error {
return errors.New("this template is in use")
}
+ origin, err := commonrepo.NewYamlTemplateColl().GetById(id)
+ if err != nil {
+ logger.Errorf("Failed to get yaml template of id: %s, the error is: %s", id, err)
+ return err
+ }
+
err = commonrepo.NewYamlTemplateColl().DeleteByID(id)
if err != nil {
- logger.Errorf("Failed to delete dockerfile template of id: %s, the error is: %s", id, err)
+ logger.Errorf("Failed to delete yaml template of id: %s, the error is: %s", id, err)
+ return err
}
- return err
+
+ if err := commmonservice.ProcessYamlTemplateWebhook(nil, origin, logger); err != nil {
+ logger.Errorf("failed to process yaml template webhook for delete %s, err: %s", origin.Name, err)
+ }
+ return nil
}
func SyncYamlTemplateReference(userName, id string, logger *zap.SugaredLogger) error {
diff --git a/pkg/microservice/aslan/core/vm/service/types.go b/pkg/microservice/aslan/core/vm/service/types.go
index 342a1caee4..e1963ef327 100644
--- a/pkg/microservice/aslan/core/vm/service/types.go
+++ b/pkg/microservice/aslan/core/vm/service/types.go
@@ -114,6 +114,7 @@ type HeartbeatParameters struct {
DiskSpace uint64 `json:"disk_space"`
FreeDiskSpace uint64 `json:"free_disk_space"`
VMname string `json:"vm_name"`
+ AgentVersion string `json:"agent_version"`
}
type HeartbeatRequest struct {
diff --git a/pkg/microservice/aslan/core/vm/service/vm.go b/pkg/microservice/aslan/core/vm/service/vm.go
index b010d789d4..d53aa40efc 100644
--- a/pkg/microservice/aslan/core/vm/service/vm.go
+++ b/pkg/microservice/aslan/core/vm/service/vm.go
@@ -29,7 +29,6 @@ import (
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/util/sets"
- commonconfig "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"
vmmodel "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models/vm"
@@ -39,8 +38,6 @@ import (
systemservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/system/service"
"github.com/koderover/zadig/v2/pkg/setting"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
- krkubeclient "github.com/koderover/zadig/v2/pkg/tool/kube/client"
- "github.com/koderover/zadig/v2/pkg/tool/kube/getter"
commonjob "github.com/koderover/zadig/v2/pkg/types/job"
)
@@ -122,11 +119,11 @@ func RecoveryVM(idString, user string, logger *zap.SugaredLogger) (*RecoveryAgen
func generateAgentRecoveryCmd(vm *commonmodels.PrivateKey) (*RecoveryAgentCmd, error) {
cmd := new(RecoveryAgentCmd)
- baseURL, err := getRepoURL()
+ baseURL, err := config.GetRepoURL()
if err != nil {
return nil, fmt.Errorf("failed to get agent repo url, err: %w", err)
}
- version, err := getZadigAgentVersion()
+ version, err := config.GetZadigAgentVersion()
if err != nil {
return nil, fmt.Errorf("failed to get zadig-agent version, error: %s", err)
}
@@ -230,11 +227,11 @@ func UpgradeAgent(idString, user string, logger *zap.SugaredLogger) (*UpgradeAge
func generateAgentUpgradeCmd(vm *commonmodels.PrivateKey, logger *zap.SugaredLogger) (*UpgradeAgentCmd, error) {
cmd := new(UpgradeAgentCmd)
- baseURL, err := getRepoURL()
+ baseURL, err := config.GetRepoURL()
if err != nil {
return nil, fmt.Errorf("failed to get agent repo url, err: %w", err)
}
- version, err := getZadigAgentVersion()
+ version, err := config.GetZadigAgentVersion()
if err != nil {
return nil, fmt.Errorf("failed to get zadig-agent version, error: %s", err)
}
@@ -411,6 +408,7 @@ func RegisterAgent(args *RegisterAgentRequest, logger *zap.SugaredLogger) (*Regi
return nil, fmt.Errorf("zadig server vm %s agent is nil in db", args.Token)
}
vm.Agent.AgentVersion = args.Parameters.AgentVersion
+ updateAgentVersionStatus(vm.Agent, logger)
}
err = commonrepo.NewPrivateKeyColl().Update(vm.ID.Hex(), vm)
if err != nil {
@@ -486,6 +484,10 @@ func Heartbeat(args *HeartbeatRequest, logger *zap.SugaredLogger) (*HeartbeatRes
return nil, fmt.Errorf("zadig server vm %s agent is nil in db", args.Token)
}
vm.Agent.LastHeartbeatTime = time.Now().Unix()
+ if args.Parameters != nil && args.Parameters.AgentVersion != "" {
+ vm.Agent.AgentVersion = args.Parameters.AgentVersion
+ }
+ updateAgentVersionStatus(vm.Agent, logger)
err = commonrepo.NewPrivateKeyColl().Update(vm.ID.Hex(), vm)
if err != nil {
@@ -495,8 +497,9 @@ func Heartbeat(args *HeartbeatRequest, logger *zap.SugaredLogger) (*HeartbeatRes
if vm.Agent.NeedUpdate {
resp.NeedUpdateAgentVersion = true
- resp.AgentVersion = vm.Agent.AgentVersion
+ resp.AgentVersion = vm.Agent.ZadigVersion
}
+ resp.ZadigVersion = vm.Agent.ZadigVersion
resp.ScheduleWorkflow = vm.ScheduleWorkflow
if vm.ScheduleWorkflow && vm.Agent.Workspace != "" {
@@ -509,6 +512,24 @@ func Heartbeat(args *HeartbeatRequest, logger *zap.SugaredLogger) (*HeartbeatRes
return resp, nil
}
+func updateAgentVersionStatus(agent *commonmodels.VMAgent, logger *zap.SugaredLogger) {
+ latestVersion, err := config.GetZadigAgentVersion()
+ if err != nil {
+ logger.Warnf("failed to get zadig-agent version while handling heartbeat: %v", err)
+ return
+ }
+ agent.ZadigVersion = latestVersion
+
+ normalizedLatestVersion := strings.TrimPrefix(latestVersion, "v")
+ if normalizedLatestVersion == "" {
+ agent.NeedUpdate = false
+ return
+ }
+
+ normalizedCurrentVersion := strings.TrimPrefix(agent.AgentVersion, "v")
+ agent.NeedUpdate = normalizedCurrentVersion != normalizedLatestVersion
+}
+
type VMJobGetterMap struct {
M sync.Mutex
GetterMap map[string]struct{}
@@ -676,11 +697,11 @@ func GenerateAgentAccessCmds(vm *commonmodels.PrivateKey) (*AgentAccessCmds, err
if vm.Agent != nil {
token = vm.Agent.Token
}
- baseURL, err := getRepoURL()
+ baseURL, err := config.GetRepoURL()
if err != nil {
return nil, fmt.Errorf("failed to get agent repo url, err: %w", err)
}
- version, err := getZadigAgentVersion()
+ version, err := config.GetZadigAgentVersion()
if err != nil {
return nil, fmt.Errorf("failed to get zadig-agent version, error: %s", err)
}
@@ -755,38 +776,6 @@ func GenerateAgentAccessCmds(vm *commonmodels.PrivateKey) (*AgentAccessCmds, err
return resp, nil
}
-func getZadigAgentVersion() (string, error) {
- ns := commonconfig.Namespace()
- kubeClient := krkubeclient.Client()
- configMap, found, err := getter.GetConfigMap(ns, "aslan-config", kubeClient)
- if err != nil || !found {
- return "", fmt.Errorf("failed to get aslan configmap, error: %s", err)
- }
- if found {
- version := configMap.Data["ZADIG_AGENT_VERSION"]
- if version != "" {
- return strings.TrimPrefix(version, "v"), nil
- }
- }
- return "", fmt.Errorf("zadig-agent version not found")
-}
-
-func getRepoURL() (string, error) {
- ns := commonconfig.Namespace()
- kubeClient := krkubeclient.Client()
- configMap, found, err := getter.GetConfigMap(ns, "aslan-config", kubeClient)
- if err != nil || !found {
- return "", fmt.Errorf("failed to get aslan configmap, error: %s", err)
- }
- if found {
- version := configMap.Data["ZADIG_AGENT_REPO_URL"]
- if version != "" {
- return version, nil
- }
- }
- return "", fmt.Errorf("zadig-agent repo URL not found")
-}
-
func DownloadTemporaryFile(fileID, token string, c *gin.Context, logger *zap.SugaredLogger) error {
_, err := commonrepo.NewPrivateKeyColl().Find(commonrepo.FindPrivateKeyOption{
Token: token,
diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gerrit.go b/pkg/microservice/aslan/core/workflow/service/webhook/gerrit.go
index 4343bbad68..7c60eecbf3 100644
--- a/pkg/microservice/aslan/core/workflow/service/webhook/gerrit.go
+++ b/pkg/microservice/aslan/core/workflow/service/webhook/gerrit.go
@@ -40,9 +40,11 @@ import (
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/command"
gerritservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/gerrit"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/repository"
+ "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/template"
commonutil "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util"
environmentservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/environment/service"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/service/service"
+ templateservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/templatestore/service"
"github.com/koderover/zadig/v2/pkg/setting"
"github.com/koderover/zadig/v2/pkg/shared/client/systemconfig"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
@@ -76,6 +78,10 @@ func ProcessGerritHook(payload []byte, req *http.Request, requestID string, log
if err != nil {
log.Errorf("updateServiceTemplateByGerritEvent err : %v", err)
}
+ err = updateYamlTemplateByGerritEvent(req.RequestURI, log)
+ if err != nil {
+ log.Errorf("updateYamlTemplateByGerritEvent err : %v", err)
+ }
}
var wg sync.WaitGroup
var errorList = &multierror.Error{}
@@ -200,6 +206,128 @@ func updateServiceTemplateByGerritEvent(uri string, log *zap.SugaredLogger) erro
return errs.ErrorOrNil()
}
+func updateYamlTemplateByGerritEvent(uri string, log *zap.SugaredLogger) error {
+ templates, err := commonrepo.NewYamlTemplateColl().ListBySource(setting.SourceFromGerrit)
+ if err != nil {
+ return err
+ }
+
+ errs := &multierror.Error{}
+ for _, tmpl := range templates {
+ if tmpl == nil || tmpl.Source != setting.SourceFromGerrit {
+ continue
+ }
+ if strings.Contains(uri, "?") && !strings.Contains(uri, tmpl.Name) {
+ continue
+ }
+
+ log.Infof("Started to sync yaml template %s from gerrit path %s", tmpl.Name, tmpl.Path)
+ if err := SyncYamlTemplateFromGerrit(tmpl, log); err != nil {
+ log.Errorf("failed to sync yaml template %s from gerrit, error: %v", tmpl.Name, err)
+ errs = multierror.Append(errs, err)
+ }
+ }
+
+ return errs.ErrorOrNil()
+}
+
+func SyncYamlTemplateFromGerrit(tmpl *commonmodels.YamlTemplate, log *zap.SugaredLogger) error {
+ if tmpl.Source != setting.SourceFromGerrit {
+ return fmt.Errorf("yaml template is not from gerrit")
+ }
+
+ var before string
+ if tmpl.Commit != nil {
+ before = tmpl.Commit.SHA
+ }
+
+ ch, err := systemconfig.New().GetCodeHost(tmpl.CodeHostID)
+ if err != nil {
+ return err
+ }
+
+ remoteName := tmpl.RemoteName
+ if remoteName == "" {
+ remoteName = "origin"
+ }
+
+ gerritCli := gerrit.NewClient(ch.Address, ch.AccessToken, config.ProxyHTTPSAddr(), ch.EnableProxy)
+ commit, err := gerritCli.GetCommitByBranch(tmpl.RepoName, tmpl.BranchName)
+ if err != nil {
+ return err
+ }
+
+ tmpl.Commit = &commonmodels.Commit{
+ SHA: commit.Commit,
+ Message: commit.Message,
+ }
+
+ if before == tmpl.Commit.SHA {
+ log.Infof("Before and after SHA: %s remains the same, no need to sync, source:%s", before, tmpl.Source)
+ return nil
+ }
+
+ if err := command.RunGitCmds(ch, setting.GerritDefaultOwner, setting.GerritDefaultOwner, tmpl.RepoName, tmpl.BranchName, remoteName); err != nil {
+ return err
+ }
+
+ base, err := GetGerritWorkspaceBasePath(tmpl.RepoName)
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+
+ fullPath := path.Join(base, tmpl.Path)
+ content := ""
+ if tmpl.LoadFromDir {
+ fileInfos, err := ioutil.ReadDir(fullPath)
+ if err != nil {
+ return err
+ }
+
+ files := make([]string, 0)
+ for _, fileInfo := range fileInfos {
+ if fileInfo.IsDir() || !commonutil.IsYaml(fileInfo.Name()) {
+ continue
+ }
+ file, err := os.ReadFile(path.Join(fullPath, fileInfo.Name()))
+ if err != nil {
+ return err
+ }
+ files = append(files, string(file))
+ }
+ if len(files) == 0 {
+ return fmt.Errorf("no yaml file is found under directory %s", tmpl.Path)
+ }
+ content = util.CombineManifests(files)
+ } else {
+ file, err := os.ReadFile(fullPath)
+ if err != nil {
+ return err
+ }
+ content = string(file)
+ }
+
+ if err := templateservice.UpdateYamlTemplate(tmpl.ID.Hex(), &template.YamlTemplate{
+ Name: tmpl.Name,
+ Content: content,
+ Source: tmpl.Source,
+ CodehostID: tmpl.CodeHostID,
+ RepoOwner: tmpl.RepoOwner,
+ Namespace: tmpl.Namespace,
+ RepoName: tmpl.RepoName,
+ Path: tmpl.Path,
+ BranchName: tmpl.BranchName,
+ RemoteName: tmpl.RemoteName,
+ LoadFromDir: tmpl.LoadFromDir,
+ Commit: tmpl.Commit,
+ }, log); err != nil {
+ return err
+ }
+
+ log.Infof("End of sync yaml template %s from gerrit path %s", tmpl.Name, tmpl.Path)
+ return nil
+}
+
func GetGerritWorkspaceBasePath(repoName string) (string, error) {
return gerritservice.GetGerritWorkspaceBasePath(repoName)
}
diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go
index ff7117c8c0..a649f964bb 100644
--- a/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go
+++ b/pkg/microservice/aslan/core/workflow/service/webhook/gerrit_workflowv4_task.go
@@ -173,6 +173,8 @@ func (gruem *gerritChangeMergedEventMatcherForWorkflowV4) GetHookRepo(hookRepo *
RepoOwner: hookRepo.RepoOwner,
RepoNamespace: hookRepo.GetRepoNamespace(),
Branch: hookRepo.Branch,
+ TargetBranch: hookRepo.Branch,
+ Committer: hookRepo.Committer,
Source: hookRepo.Source,
}
}
@@ -223,7 +225,9 @@ func (gpcem *gerritPatchsetCreatedEventMatcherForWorkflowV4) GetHookRepo(hookRep
RepoOwner: hookRepo.RepoOwner,
RepoNamespace: hookRepo.GetRepoNamespace(),
Branch: hookRepo.Branch,
+ TargetBranch: hookRepo.Branch,
PR: gpcem.Event.Change.Number,
+ Committer: hookRepo.Committer,
Source: hookRepo.Source,
}
}
@@ -361,6 +365,7 @@ func TriggerWorkflowV4ByGerritEvent(event *gerritTypeEvent, body []byte, uri, ba
CodehostID: item.MainRepo.CodehostID,
MergeRequestID: mergeRequestID,
CommitID: commitID,
+ RawPayload: string(body),
}
}
workflowController := controller.CreateWorkflowController(item.WorkflowArg)
diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitee.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitee.go
index c00c53f316..44d445a5a6 100644
--- a/pkg/microservice/aslan/core/workflow/service/webhook/gitee.go
+++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitee.go
@@ -32,14 +32,18 @@ import (
"github.com/otiai10/copy"
"go.uber.org/zap"
+ "github.com/koderover/zadig/v2/pkg/microservice/aslan/config"
microserviceConfig "github.com/koderover/zadig/v2/pkg/microservice/aslan/config"
commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models"
commonrepo "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb"
commonservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/command"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/repository"
+ "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/template"
+ commonutil "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util"
environmentservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/environment/service"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/service/service"
+ templateservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/templatestore/service"
"github.com/koderover/zadig/v2/pkg/setting"
"github.com/koderover/zadig/v2/pkg/shared/client/systemconfig"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
@@ -87,6 +91,9 @@ func ProcessGiteeHook(payload []byte, req *http.Request, requestID string, log *
if err := updateServiceTemplateByGiteeEvent(req.RequestURI, log); err != nil {
errorList = multierror.Append(errorList, err)
}
+ if err := updateYamlTemplateByGiteePush(event, log); err != nil {
+ errorList = multierror.Append(errorList, err)
+ }
// build webhook
//wg.Add(1)
//go func() {
@@ -109,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)
}
}()
@@ -143,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)
}
}()
@@ -169,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)
}
}()
@@ -296,6 +303,153 @@ func updateServiceTemplateByGiteeEvent(uri string, log *zap.SugaredLogger) error
return errs.ErrorOrNil()
}
+func updateYamlTemplateByGiteePush(pushEvent *gitee.PushEvent, log *zap.SugaredLogger) error {
+ changeFiles := make([]string, 0)
+ for _, commit := range pushEvent.Commits {
+ changeFiles = append(changeFiles, commit.Added...)
+ changeFiles = append(changeFiles, commit.Removed...)
+ changeFiles = append(changeFiles, commit.Modified...)
+ }
+
+ templates, _, err := commonrepo.NewYamlTemplateColl().List(0, 0)
+ if err != nil {
+ return err
+ }
+
+ errs := &multierror.Error{}
+ for _, tmpl := range templates {
+ if tmpl == nil || (tmpl.Source != setting.SourceFromGitee && tmpl.Source != setting.SourceFromGiteeEE) {
+ continue
+ }
+ namespace := tmpl.Namespace
+ if namespace == "" {
+ namespace = tmpl.RepoOwner
+ }
+ if namespace+"/"+tmpl.RepoName != pushEvent.Repository.PathWithNamespace {
+ continue
+ }
+ if strings.TrimPrefix(pushEvent.Ref, "refs/heads/") != tmpl.BranchName {
+ continue
+ }
+
+ affected := len(changeFiles) == 0
+ for _, changeFile := range changeFiles {
+ if subElem(tmpl.Path, changeFile) {
+ affected = true
+ break
+ }
+ }
+ if affected {
+ log.Infof("Started to sync yaml template %s from gitee path %s", tmpl.Name, tmpl.Path)
+ if err := SyncYamlTemplateFromGitee(tmpl, log); err != nil {
+ log.Errorf("failed to sync yaml template %s from gitee, error: %v", tmpl.Name, err)
+ errs = multierror.Append(errs, err)
+ }
+ } else {
+ log.Infof("Yaml template %s from gitee %s is not affected, no sync", tmpl.Name, tmpl.Path)
+ }
+ }
+
+ return errs.ErrorOrNil()
+}
+
+func SyncYamlTemplateFromGitee(tmpl *commonmodels.YamlTemplate, log *zap.SugaredLogger) error {
+ if tmpl.Source != setting.SourceFromGitee && tmpl.Source != setting.SourceFromGiteeEE {
+ return fmt.Errorf("yaml template is not from gitee")
+ }
+
+ var before string
+ if tmpl.Commit != nil {
+ before = tmpl.Commit.SHA
+ }
+
+ ch, err := systemconfig.New().GetCodeHost(tmpl.CodeHostID)
+ if err != nil {
+ return err
+ }
+
+ namespace := tmpl.Namespace
+ if namespace == "" {
+ namespace = tmpl.RepoOwner
+ }
+
+ giteeCli := gitee.NewClient(ch.ID, ch.Address, ch.AccessToken, config.ProxyHTTPSAddr(), ch.EnableProxy)
+ branch, err := giteeCli.GetSingleBranch(ch.Address, ch.AccessToken, tmpl.RepoOwner, tmpl.RepoName, tmpl.BranchName)
+ if err != nil {
+ return err
+ }
+
+ tmpl.Commit = &commonmodels.Commit{
+ SHA: branch.Commit.Sha,
+ Message: branch.Commit.Commit.Message,
+ }
+
+ if before == tmpl.Commit.SHA {
+ log.Infof("Before and after SHA: %s remains the same, no need to sync, source:%s", before, tmpl.Source)
+ return nil
+ }
+
+ if err := command.RunGitCmds(ch, tmpl.RepoOwner, namespace, tmpl.RepoName, tmpl.BranchName, "origin"); err != nil {
+ return err
+ }
+
+ base, err := GetWorkspaceBasePath(tmpl.RepoName)
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+
+ fullPath := path.Join(base, tmpl.Path)
+ content := ""
+ if tmpl.LoadFromDir {
+ fileInfos, err := ioutil.ReadDir(fullPath)
+ if err != nil {
+ return err
+ }
+
+ files := make([]string, 0)
+ for _, fileInfo := range fileInfos {
+ if fileInfo.IsDir() || !commonutil.IsYaml(fileInfo.Name()) {
+ continue
+ }
+ file, err := os.ReadFile(path.Join(fullPath, fileInfo.Name()))
+ if err != nil {
+ return err
+ }
+ files = append(files, string(file))
+ }
+ if len(files) == 0 {
+ return fmt.Errorf("no yaml file is found under directory %s", tmpl.Path)
+ }
+ content = util.CombineManifests(files)
+ } else {
+ file, err := os.ReadFile(fullPath)
+ if err != nil {
+ return err
+ }
+ content = string(file)
+ }
+
+ if err := templateservice.UpdateYamlTemplate(tmpl.ID.Hex(), &template.YamlTemplate{
+ Name: tmpl.Name,
+ Content: content,
+ Source: tmpl.Source,
+ CodehostID: tmpl.CodeHostID,
+ RepoOwner: tmpl.RepoOwner,
+ Namespace: tmpl.Namespace,
+ RepoName: tmpl.RepoName,
+ Path: tmpl.Path,
+ BranchName: tmpl.BranchName,
+ RemoteName: tmpl.RemoteName,
+ LoadFromDir: tmpl.LoadFromDir,
+ Commit: tmpl.Commit,
+ }, log); err != nil {
+ return err
+ }
+
+ log.Infof("End of sync yaml template %s from gitee path %s", tmpl.Name, tmpl.Path)
+ return nil
+}
+
// GetGiteeTestingServiceTemplates Get all service templates maintained in gitee
func GetGiteeTestingServiceTemplates() ([]*commonmodels.Service, error) {
opt := &commonrepo.ServiceListOption{
diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitee_workflowv4_task.go
index c5bf677f35..0aed7a1df0 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
@@ -87,6 +87,8 @@ func (gpem *giteePushEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmode
RepoNamespace: hookRepo.GetRepoNamespace(),
RepoOwner: hookRepo.RepoOwner,
Branch: hookRepo.Branch,
+ TargetBranch: hookRepo.Branch,
+ Committer: hookRepo.Committer,
Source: hookRepo.Source,
}
}
@@ -139,7 +141,9 @@ 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,
+ Committer: hookRepo.Committer,
Source: hookRepo.Source,
}
}
@@ -173,7 +177,9 @@ func (gtem *giteeTagEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmodel
RepoOwner: hookRepo.RepoOwner,
RepoNamespace: hookRepo.GetRepoNamespace(),
Branch: hookRepo.Branch,
+ TargetBranch: hookRepo.Branch,
Tag: hookRepo.Tag,
+ Committer: hookRepo.Committer,
Source: hookRepo.Source,
}
}
@@ -206,7 +212,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)
@@ -280,6 +286,7 @@ func TriggerWorkflowV4ByGiteeEvent(event interface{}, baseURI, requestID string,
MergeRequestID: mergeRequestID,
CommitID: commitID,
EventType: eventType,
+ RawPayload: rawPayload,
}
case *gitee.PushEvent:
eventType = EventTypePush
@@ -297,11 +304,13 @@ func TriggerWorkflowV4ByGiteeEvent(event interface{}, baseURI, requestID string,
IsPr: false,
CommitID: commitID,
EventType: eventType,
+ RawPayload: rawPayload,
}
case *gitee.TagPushEvent:
eventType = EventTypeTag
hookPayload = &commonmodels.HookPayload{
- EventType: eventType,
+ 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 77cc687f2a..360f452d43 100644
--- a/pkg/microservice/aslan/core/workflow/service/webhook/github.go
+++ b/pkg/microservice/aslan/core/workflow/service/webhook/github.go
@@ -32,6 +32,8 @@ import (
"github.com/koderover/zadig/v2/pkg/microservice/aslan/config"
commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models"
commonrepo "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb"
+ "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/template"
+ templateservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/templatestore/service"
"github.com/koderover/zadig/v2/pkg/setting"
"github.com/koderover/zadig/v2/pkg/shared/client/systemconfig"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
@@ -310,6 +312,11 @@ func ProcessGithubWebHook(payload []byte, req *http.Request, requestID string, l
log.Errorf("updateServiceTemplateByGithubPush failed, error:%v", err)
}
+ // sync yaml template
+ if err = updateYamlTemplateByGithubPush(et, log); err != nil {
+ log.Errorf("updateYamlTemplateByGithubPush failed, error:%v", err)
+ }
+
// sync service template helm values
if err = updateServiceTemplateHelmValuesByGithubPush(et, log); err != nil {
log.Errorf("updateServiceTemplateHelmValuesByGithubPush failed, error:%v", err)
@@ -422,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)
@@ -757,3 +764,154 @@ func SyncServiceTemplateHelmValuesFromGithub(service *commonmodels.Service, late
log.Infof("End of sync service template %s's helm values from github path %s", service.ServiceName, sourceFrom.LoadPath)
return nil
}
+
+func updateYamlTemplateByGithubPush(pushEvent *github.PushEvent, log *zap.SugaredLogger) error {
+
+ changeFiles := make([]string, 0)
+ for _, commit := range pushEvent.Commits {
+ changeFiles = append(changeFiles, commit.Added...)
+ changeFiles = append(changeFiles, commit.Removed...)
+ changeFiles = append(changeFiles, commit.Modified...)
+ }
+
+ latestCommitID := *pushEvent.After
+ latestCommitMessage := ""
+ for _, commit := range pushEvent.Commits {
+ if *commit.ID == latestCommitID {
+ latestCommitMessage = *commit.Message
+ break
+ }
+ }
+
+ templates, err := commonrepo.NewYamlTemplateColl().ListBySource(setting.SourceFromGithub)
+ if err != nil {
+ log.Errorf("Failed to get github yaml templates, error: %v", err)
+ return fmt.Errorf("failed to get github yaml templates: %w", err)
+ }
+
+ errs := &multierror.Error{}
+ for _, tmpl := range templates {
+
+ namespace := tmpl.Namespace
+ if namespace == "" {
+ namespace = tmpl.RepoOwner
+ }
+
+ // 判断 PushEvent 的 Repo 和 Branch 是否匹配该 yaml 模板
+ if namespace+"/"+tmpl.RepoName != pushEvent.GetRepo().GetFullName() {
+ continue
+ }
+ if strings.TrimPrefix(pushEvent.GetRef(), "refs/heads/") != tmpl.BranchName {
+ continue
+ }
+
+ affected := false
+ for _, changeFile := range changeFiles {
+ if subElem(tmpl.Path, changeFile) {
+ affected = true
+ break
+ }
+ }
+ if affected {
+ log.Infof("Started to sync yaml template %s from github %s", tmpl.Name, tmpl.Path)
+ if err := SyncYamlTemplateFromGithub(tmpl, latestCommitID, latestCommitMessage, log); err != nil {
+ log.Errorf("SyncYamlTemplateFromGithub failed, error: %v", err)
+ errs = multierror.Append(errs, err)
+ }
+ } else {
+ log.Infof("Yaml template %s from github %s is not affected, no sync", tmpl.Name, tmpl.Path)
+ }
+ }
+
+ return errs.ErrorOrNil()
+}
+
+func SyncYamlTemplateFromGithub(tmpl *commonmodels.YamlTemplate, latestCommitID, latestCommitMessage string, log *zap.SugaredLogger) error {
+ if tmpl.Source != setting.SourceFromGithub {
+ log.Error("YAML template is not from github")
+ return errors.New("yaml template is not from github")
+ }
+
+ var before string
+ if tmpl.Commit != nil {
+ before = tmpl.Commit.SHA
+ }
+
+ tmpl.Commit = &commonmodels.Commit{
+ SHA: latestCommitID,
+ Message: latestCommitMessage,
+ }
+
+ if before == latestCommitID {
+ log.Infof("Before and after SHA: %s remains the same, no need to sync, source:%s", before, tmpl.Source)
+ return nil
+ }
+
+ ch, err := systemconfig.New().GetCodeHost(tmpl.CodeHostID)
+ if err != nil {
+ log.Errorf("failed to get codehost %d, err: %s", tmpl.CodeHostID, err)
+ return err
+ }
+
+ namespace := tmpl.Namespace
+ if namespace == "" {
+ namespace = tmpl.RepoOwner
+ }
+
+ gc := githubtool.NewClient(&githubtool.Config{AccessToken: ch.AccessToken, Proxy: config.ProxyHTTPSAddr()})
+ fileContent, directoryContent, err := gc.GetContents(context.TODO(), namespace, tmpl.RepoName, tmpl.Path, &github.RepositoryContentGetOptions{Ref: tmpl.BranchName})
+ if err != nil {
+ return err
+ }
+
+ content := ""
+ if fileContent != nil {
+ content, err = fileContent.GetContent()
+ if err != nil {
+ return err
+ }
+ } else {
+ files := make([]string, 0)
+ for _, f := range directoryContent {
+ if f.GetType() != "file" {
+ continue
+ }
+ fileName := strings.ToLower(f.GetPath())
+ if !strings.HasSuffix(fileName, ".yaml") && !strings.HasSuffix(fileName, ".yml") {
+ continue
+ }
+
+ file, err := syncSingleFileFromGithub(namespace, tmpl.RepoName, tmpl.BranchName, f.GetPath(), ch.AccessToken)
+ if err != nil {
+ log.Errorf("syncSingleFileFromGithub failed, path: %s, err: %v", f.GetPath(), err)
+ continue
+ }
+ files = append(files, file)
+ }
+ if len(files) == 0 {
+ return fmt.Errorf("no yaml file is found under directory %s", tmpl.Path)
+ }
+ content = util.JoinYamls(files)
+ }
+
+ if err := templateservice.UpdateYamlTemplate(tmpl.ID.Hex(), &template.YamlTemplate{
+ Name: tmpl.Name,
+ Content: content,
+ Source: tmpl.Source,
+ CodehostID: tmpl.CodeHostID,
+ RepoOwner: tmpl.RepoOwner,
+ Namespace: tmpl.Namespace,
+ RepoName: tmpl.RepoName,
+ Path: tmpl.Path,
+ BranchName: tmpl.BranchName,
+ RemoteName: tmpl.RemoteName,
+ LoadFromDir: tmpl.LoadFromDir,
+ Commit: tmpl.Commit,
+ }, log); err != nil {
+ log.Errorf("update yaml template error: %s", err)
+ return err
+ }
+
+ log.Infof("End of sync yaml template %s from github path %s", tmpl.Name, tmpl.Path)
+ return nil
+}
diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go
index 699358b3a6..b33f29439c 100644
--- a/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go
+++ b/pkg/microservice/aslan/core/workflow/service/webhook/github_workflowv4_task.go
@@ -25,7 +25,6 @@ import (
"github.com/hashicorp/go-multierror"
"go.uber.org/zap"
- internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/config"
commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models"
commonrepo "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb"
@@ -33,6 +32,7 @@ import (
workflowservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/workflow/service/workflow"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/workflow/service/workflow/controller"
"github.com/koderover/zadig/v2/pkg/setting"
+ internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler"
"github.com/koderover/zadig/v2/pkg/types"
)
@@ -86,8 +86,10 @@ func (gpem *githubPushEventMatcheForWorkflowV4) GetHookRepo(hookRepo *commonmode
RepoOwner: hookRepo.RepoOwner,
RepoNamespace: hookRepo.GetRepoNamespace(),
Branch: hookRepo.Branch,
+ TargetBranch: hookRepo.Branch,
CommitID: *gpem.event.HeadCommit.ID,
CommitMessage: *gpem.event.HeadCommit.Message,
+ Committer: hookRepo.Committer,
Source: hookRepo.Source,
}
}
@@ -143,9 +145,11 @@ func (gmem *githubMergeEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmo
RepoOwner: hookRepo.RepoOwner,
RepoNamespace: hookRepo.GetRepoNamespace(),
Branch: hookRepo.Branch,
+ TargetBranch: *gmem.event.PullRequest.Base.Ref,
PR: *gmem.event.PullRequest.Number,
CommitID: *gmem.event.PullRequest.Head.SHA,
CommitMessage: *gmem.event.PullRequest.Title,
+ Committer: hookRepo.Committer,
Source: hookRepo.Source,
}
}
@@ -182,7 +186,9 @@ func (gtem *githubTagEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmode
RepoOwner: hookRepo.RepoOwner,
RepoNamespace: hookRepo.GetRepoNamespace(),
Branch: hookRepo.Branch,
+ TargetBranch: hookRepo.Branch,
Tag: hookRepo.Tag,
+ Committer: hookRepo.Committer,
Source: hookRepo.Source,
}
}
@@ -215,7 +221,7 @@ func createGithubEventMatcherForWorkflowV4(
return nil
}
-func TriggerWorkflowV4ByGithubEvent(event interface{}, baseURI, deliveryID, requestID string, log *zap.SugaredLogger) error {
+func TriggerWorkflowV4ByGithubEvent(event interface{}, rawPayload, baseURI, deliveryID, requestID string, log *zap.SugaredLogger) error {
workflows, _, err := commonrepo.NewWorkflowV4Coll().List(&commonrepo.ListWorkflowV4Option{}, 0, 0)
if err != nil {
errMsg := fmt.Sprintf("list workflow v4 error: %v", err)
@@ -284,6 +290,7 @@ func TriggerWorkflowV4ByGithubEvent(event interface{}, baseURI, deliveryID, requ
MergeRequestID: mergeRequestID,
CommitID: commitID,
EventType: eventType,
+ RawPayload: rawPayload,
}
case *github.PushEvent:
if ev.GetRef() != "" && ev.GetHeadCommit().GetID() != "" {
@@ -302,12 +309,14 @@ func TriggerWorkflowV4ByGithubEvent(event interface{}, baseURI, deliveryID, requ
DeliveryID: deliveryID,
CommitID: commitID,
EventType: eventType,
+ RawPayload: rawPayload,
}
}
case *github.CreateEvent:
eventType = EventTypeTag
hookPayload = &commonmodels.HookPayload{
- EventType: eventType,
+ 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 8a90093679..b3963515d7 100644
--- a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab.go
+++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab.go
@@ -20,6 +20,7 @@ import (
"errors"
"fmt"
"net/http"
+ "strings"
"sync"
"time"
@@ -30,6 +31,8 @@ import (
"github.com/koderover/zadig/v2/pkg/config"
commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models"
commonrepo "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb"
+ "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/template"
+ templateservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/templatestore/service"
"github.com/koderover/zadig/v2/pkg/setting"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
"github.com/koderover/zadig/v2/pkg/tool/log"
@@ -47,6 +50,7 @@ type EventPush struct {
}
func ProcessGitlabHook(payload []byte, req *http.Request, requestID string, log *zap.SugaredLogger) error {
+ start := time.Now()
token := req.Header.Get("X-Gitlab-Token")
secret := util.GetGitHookSecret()
@@ -55,10 +59,12 @@ func ProcessGitlabHook(payload []byte, req *http.Request, requestID string, log
}
eventType := gitlab.HookEventType(req)
+ parseStart := time.Now()
event, err := gitlab.ParseHook(eventType, payload)
if err != nil {
return err
}
+ log.Infof("gitlab webhook parsed event type %s in %s", eventType, time.Since(parseStart))
baseURI := config.SystemAddress()
var pushEvent *gitlab.PushEvent
@@ -68,12 +74,14 @@ func ProcessGitlabHook(payload []byte, req *http.Request, requestID string, log
switch event.(type) {
case *gitlab.PushSystemEvent:
+ parsePushSystemEventStart := time.Now()
if ev, err := gitlab.ParseWebhook(gitlab.EventTypePush, payload); err != nil {
errorList = multierror.Append(errorList, err)
} else {
event = ev
eventType = gitlab.EventTypePush
}
+ log.Infof("gitlab webhook parsed push system event in %s", time.Since(parsePushSystemEventStart))
case *gitlab.MergeEvent:
if eventType == gitlab.EventTypeSystemHook {
eventType = gitlab.EventTypeMergeRequest
@@ -83,20 +91,31 @@ func ProcessGitlabHook(payload []byte, req *http.Request, requestID string, log
switch event := event.(type) {
case *gitlab.PushEvent:
pushEvent = event
+ pushEventStart := time.Now()
changeFiles := make([]string, 0)
for _, commit := range pushEvent.Commits {
changeFiles = append(changeFiles, commit.Added...)
changeFiles = append(changeFiles, commit.Removed...)
changeFiles = append(changeFiles, commit.Modified...)
}
+ log.Infof("gitlab webhook collected %d changed files in %s", len(changeFiles), time.Since(pushEventStart))
pathWithNamespace := pushEvent.Project.PathWithNamespace
// trigger service template to re-sync from remote repo
+ serviceSyncStart := time.Now()
if err = updateServiceTemplateByPushEvent(pushEvent.Ref, changeFiles, pathWithNamespace, log); err != nil {
errorList = multierror.Append(errorList, err)
}
+ log.Infof("gitlab webhook updateServiceTemplateByPushEvent cost %s", time.Since(serviceSyncStart))
+ valuesSyncStart := time.Now()
if err = updateServiceTemplateValuesByPushEvent(pushEvent.Ref, changeFiles, pathWithNamespace, log); err != nil {
errorList = multierror.Append(errorList, err)
}
+ log.Infof("gitlab webhook updateServiceTemplateValuesByPushEvent cost %s", time.Since(valuesSyncStart))
+ yamlSyncStart := time.Now()
+ if err = updateYamlTemplateByGitlabPush(pushEvent.Ref, changeFiles, pathWithNamespace, log); err != nil {
+ errorList = multierror.Append(errorList, err)
+ }
+ log.Infof("gitlab webhook updateYamlTemplateByGitlabPush cost %s", time.Since(yamlSyncStart))
case *gitlab.MergeEvent:
mergeEvent = event
case *gitlab.TagEvent:
@@ -123,25 +142,31 @@ func ProcessGitlabHook(payload []byte, req *http.Request, requestID string, log
wg.Add(1)
go func() {
defer wg.Done()
+ triggerTestStart := time.Now()
if err = TriggerTestByGitlabEvent(pushEvent, baseURI, requestID, log); err != nil {
errorList = multierror.Append(errorList, err)
}
+ log.Infof("gitlab webhook TriggerTestByGitlabEvent push cost %s", time.Since(triggerTestStart))
}()
wg.Add(1)
go func() {
defer wg.Done()
+ triggerScanningStart := time.Now()
if err = TriggerScanningByGitlabEvent(pushEvent, baseURI, requestID, log); err != nil {
errorList = multierror.Append(errorList, err)
}
+ log.Infof("gitlab webhook TriggerScanningByGitlabEvent push cost %s", time.Since(triggerScanningStart))
}()
wg.Add(1)
go func() {
defer wg.Done()
- if err = TriggerWorkflowV4ByGitlabEvent(pushEvent, baseURI, requestID, log); err != nil {
+ triggerWorkflowV4Start := time.Now()
+ 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))
}()
}
@@ -150,25 +175,31 @@ func ProcessGitlabHook(payload []byte, req *http.Request, requestID string, log
wg.Add(1)
go func() {
defer wg.Done()
+ triggerTestStart := time.Now()
if err = TriggerTestByGitlabEvent(mergeEvent, baseURI, requestID, log); err != nil {
errorList = multierror.Append(errorList, err)
}
+ log.Infof("gitlab webhook TriggerTestByGitlabEvent merge cost %s", time.Since(triggerTestStart))
}()
wg.Add(1)
go func() {
defer wg.Done()
+ triggerScanningStart := time.Now()
if err = TriggerScanningByGitlabEvent(mergeEvent, baseURI, requestID, log); err != nil {
errorList = multierror.Append(errorList, err)
}
+ log.Infof("gitlab webhook TriggerScanningByGitlabEvent merge cost %s", time.Since(triggerScanningStart))
}()
wg.Add(1)
go func() {
defer wg.Done()
- if err = TriggerWorkflowV4ByGitlabEvent(mergeEvent, baseURI, requestID, log); err != nil {
+ triggerWorkflowV4Start := time.Now()
+ 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))
}()
}
@@ -177,29 +208,38 @@ func ProcessGitlabHook(payload []byte, req *http.Request, requestID string, log
wg.Add(1)
go func() {
defer wg.Done()
+ triggerTestStart := time.Now()
if err = TriggerTestByGitlabEvent(tagEvent, baseURI, requestID, log); err != nil {
errorList = multierror.Append(errorList, err)
}
+ log.Infof("gitlab webhook TriggerTestByGitlabEvent tag cost %s", time.Since(triggerTestStart))
}()
wg.Add(1)
go func() {
defer wg.Done()
+ triggerScanningStart := time.Now()
if err = TriggerScanningByGitlabEvent(tagEvent, baseURI, requestID, log); err != nil {
errorList = multierror.Append(errorList, err)
}
+ log.Infof("gitlab webhook TriggerScanningByGitlabEvent tag cost %s", time.Since(triggerScanningStart))
}()
wg.Add(1)
go func() {
defer wg.Done()
- if err = TriggerWorkflowV4ByGitlabEvent(tagEvent, baseURI, requestID, log); err != nil {
+ triggerWorkflowV4Start := time.Now()
+ 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))
}()
}
+ waitStart := time.Now()
wg.Wait()
+ log.Infof("gitlab webhook wait async tasks cost %s", time.Since(waitStart))
+ log.Infof("gitlab webhook total cost %s", time.Since(start))
return errorList.ErrorOrNil()
}
@@ -327,6 +367,131 @@ func updateServiceTemplateByPushEvent(ref string, diffs []string, pathWithNamesp
return errs.ErrorOrNil()
}
+func updateYamlTemplateByGitlabPush(ref string, diffs []string, pathWithNamespace string, log *zap.SugaredLogger) error {
+ start := time.Now()
+ templates, err := commonrepo.NewYamlTemplateColl().ListBySource(setting.SourceFromGitlab)
+ if err != nil {
+ return err
+ }
+
+ errs := &multierror.Error{}
+ for _, tmpl := range templates {
+ if tmpl == nil || tmpl.Source != setting.SourceFromGitlab {
+ continue
+ }
+ namespace := tmpl.Namespace
+ if namespace == "" {
+ namespace = tmpl.RepoOwner
+ }
+ if namespace+"/"+tmpl.RepoName != pathWithNamespace {
+ continue
+ }
+ if strings.TrimPrefix(ref, "refs/heads/") != tmpl.BranchName {
+ continue
+ }
+
+ affected := len(diffs) == 0
+ for _, diff := range diffs {
+ if subElem(tmpl.Path, diff) {
+ affected = true
+ break
+ }
+ }
+ if affected {
+ log.Infof("Started to sync yaml template %s from gitlab path %s", tmpl.Name, tmpl.Path)
+ if err := SyncYamlTemplateFromGitlab(tmpl, log); err != nil {
+ log.Errorf("failed to sync yaml template %s from gitlab, error: %v", tmpl.Name, err)
+ errs = multierror.Append(errs, err)
+ }
+ } else {
+ log.Infof("Yaml template %s from gitlab %s is not affected, no sync", tmpl.Name, tmpl.Path)
+ }
+ }
+
+ log.Infof("gitlab webhook updateYamlTemplateByGitlabPush scanned %d templates in %s", len(templates), time.Since(start))
+ return errs.ErrorOrNil()
+}
+
+func SyncYamlTemplateFromGitlab(tmpl *commonmodels.YamlTemplate, log *zap.SugaredLogger) error {
+ start := time.Now()
+ if tmpl.Source != setting.SourceFromGitlab {
+ return fmt.Errorf("yaml template is not from gitlab")
+ }
+
+ var before string
+ if tmpl.Commit != nil {
+ before = tmpl.Commit.SHA
+ }
+
+ client, err := getGitlabClientByCodehostId(tmpl.CodeHostID)
+ if err != nil {
+ return err
+ }
+
+ namespace := tmpl.Namespace
+ if namespace == "" {
+ namespace = tmpl.RepoOwner
+ }
+
+ latestCommitStart := time.Now()
+ commit, err := GitlabGetLatestCommit(client, namespace, tmpl.RepoName, tmpl.BranchName, tmpl.Path)
+ if err != nil {
+ return err
+ }
+ log.Infof("gitlab webhook sync yaml template %s get latest commit cost %s", tmpl.Name, time.Since(latestCommitStart))
+
+ tmpl.Commit = &commonmodels.Commit{
+ SHA: commit.ID,
+ Message: commit.Message,
+ }
+
+ if before == tmpl.Commit.SHA {
+ log.Infof("Before and after SHA: %s remains the same, no need to sync", before)
+ return nil
+ }
+
+ pathType := "blob"
+ if tmpl.LoadFromDir {
+ pathType = "tree"
+ }
+ rawFilesStart := time.Now()
+ files, err := GitlabGetRawFiles(client, namespace, tmpl.RepoName, tmpl.BranchName, tmpl.Path, pathType)
+ if err != nil {
+ return err
+ }
+ log.Infof("gitlab webhook sync yaml template %s get raw files cost %s", tmpl.Name, time.Since(rawFilesStart))
+ if len(files) == 0 {
+ return fmt.Errorf("no yaml file is found under directory %s", tmpl.Path)
+ }
+
+ content := util.JoinYamls(files)
+ if pathType == "blob" {
+ content = files[0]
+ }
+
+ updateTemplateStart := time.Now()
+ if err := templateservice.UpdateYamlTemplate(tmpl.ID.Hex(), &template.YamlTemplate{
+ Name: tmpl.Name,
+ Content: content,
+ Source: tmpl.Source,
+ CodehostID: tmpl.CodeHostID,
+ RepoOwner: tmpl.RepoOwner,
+ Namespace: tmpl.Namespace,
+ RepoName: tmpl.RepoName,
+ Path: tmpl.Path,
+ BranchName: tmpl.BranchName,
+ RemoteName: tmpl.RemoteName,
+ LoadFromDir: tmpl.LoadFromDir,
+ Commit: tmpl.Commit,
+ }, log); err != nil {
+ return err
+ }
+ log.Infof("gitlab webhook sync yaml template %s update template cost %s", tmpl.Name, time.Since(updateTemplateStart))
+
+ log.Infof("End of sync yaml template %s from gitlab path %s, total cost %s", tmpl.Name, tmpl.Path, time.Since(start))
+ return nil
+}
+
func GetGitlabTestingServiceTemplates() ([]*commonmodels.Service, error) {
opt := &commonrepo.ServiceListOption{
Source: setting.SourceFromGitlab,
diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go
index 1c896aa053..520d728773 100644
--- a/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go
+++ b/pkg/microservice/aslan/core/workflow/service/webhook/gitlab_workflowv4_task.go
@@ -108,7 +108,9 @@ 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,
+ Committer: hookRepo.Committer,
Source: hookRepo.Source,
}
}
@@ -231,6 +233,8 @@ func (gpem *gitlabPushEventMatcherForWorkflowV4) GetHookRepo(hookRepo *commonmod
RepoOwner: hookRepo.RepoOwner,
RepoNamespace: hookRepo.GetRepoNamespace(),
Branch: hookRepo.Branch,
+ TargetBranch: hookRepo.Branch,
+ Committer: hookRepo.Committer,
Source: hookRepo.Source,
}
}
@@ -268,12 +272,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)
@@ -370,6 +376,7 @@ func TriggerWorkflowV4ByGitlabEvent(event interface{}, baseURI, requestID string
CommitID: commitID,
CodehostID: eventRepo.CodehostID,
EventType: eventType,
+ RawPayload: rawPayload,
}
case *gitlab.PushEvent:
eventType = EventTypePush
@@ -387,11 +394,13 @@ func TriggerWorkflowV4ByGitlabEvent(event interface{}, baseURI, requestID string
CommitID: commitID,
CodehostID: eventRepo.CodehostID,
EventType: eventType,
+ RawPayload: rawPayload,
}
case *gitlab.TagEvent:
eventType = EventTypeTag
hookPayload = &commonmodels.HookPayload{
- EventType: eventType,
+ EventType: eventType,
+ RawPayload: rawPayload,
}
}
if autoCancelOpt.Type != "" {
diff --git a/pkg/microservice/aslan/core/workflow/service/webhook/utils.go b/pkg/microservice/aslan/core/workflow/service/webhook/utils.go
index 0a9f7554bf..617261c309 100644
--- a/pkg/microservice/aslan/core/workflow/service/webhook/utils.go
+++ b/pkg/microservice/aslan/core/workflow/service/webhook/utils.go
@@ -20,9 +20,7 @@ import (
"context"
"errors"
"fmt"
- "os"
"path"
- "path/filepath"
"strings"
"sync"
@@ -672,17 +670,28 @@ func checkRepoNamespaceMatch(hookRepo *commonmodels.MainHookRepo, pathWithNamesp
return (hookRepo.GetRepoNamespace() + "/" + hookRepo.RepoName) == pathWithNamespace
}
+func normalizeRepoRelativePath(p string) string {
+ p = strings.TrimSpace(strings.ReplaceAll(p, "\\", "/"))
+ if p == "" {
+ return ""
+ }
+ cleaned := path.Clean("/" + strings.TrimLeft(p, "/"))
+ if cleaned == "/" || cleaned == "." {
+ return ""
+ }
+ return strings.TrimPrefix(cleaned, "/")
+}
+
// check if sub path is a part of parent path
// eg: parent: k1/k2 sub: k1/k2/k3 return true
// parent k1/k2-2 sub: k1/k2/k3 return false
func subElem(parent, sub string) bool {
- up := ".." + string(os.PathSeparator)
- rel, err := filepath.Rel(parent, sub)
- if err != nil {
- log.Errorf("failed to check path is relative, parent: %s, sub: %s", parent, sub)
- return false
+ parent = normalizeRepoRelativePath(parent)
+ sub = normalizeRepoRelativePath(sub)
+ if parent == "" {
+ return true
}
- if !strings.HasPrefix(rel, up) && rel != ".." {
+ if sub == parent || strings.HasPrefix(sub, parent+"/") {
return true
}
return false
diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_approval.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_approval.go
index c4fb94dd49..eabe4cd3f5 100644
--- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_approval.go
+++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_approval.go
@@ -25,9 +25,7 @@ import (
"github.com/koderover/zadig/v2/pkg/microservice/aslan/config"
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/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/tool/lark"
"github.com/koderover/zadig/v2/pkg/types"
@@ -289,13 +287,22 @@ func (j ApprovalJobController) ToTask(taskID int64) ([]*commonmodels.JobTask, er
return nil, fmt.Errorf("num of approval-node %d approver is 0", i)
}
} else if node.ApproveNodeType == lark.ApproveNodeTypeUserGroup {
- if node.Type != lark.ApproveTypeStart && node.Type != lark.ApproveTypeEnd {
- if len(node.ApproveGroups) == 0 {
- return nil, fmt.Errorf("num of approval-node %d approver is 0", i)
+
+ if node.Type == lark.ApproveTypeStart || node.Type == lark.ApproveTypeEnd {
+ users, err := util.ConvertLarkUserGroupToUser(j.jobSpec.LarkApproval.ID, node.CcGroups)
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert lark user group to user: %s", err)
}
+ node.CcUsers = users
+ jobSpec.LarkApproval.ApprovalNodes[i] = node
+ continue
}
- users, err := convertLarkUserGroupToUser(j.jobSpec.LarkApproval.ID, node.ApproveGroups)
+ if len(node.ApproveGroups) == 0 {
+ return nil, fmt.Errorf("num of approval-node %d approver is 0", i)
+ }
+
+ users, err := util.ConvertLarkUserGroupToUser(j.jobSpec.LarkApproval.ID, node.ApproveGroups)
if err != nil {
return nil, fmt.Errorf("failed to convert lark user group to user: %s", err)
}
@@ -308,12 +315,6 @@ func (j ApprovalJobController) ToTask(taskID int64) ([]*commonmodels.JobTask, er
}
node.ApproveUsers = approveUsers
- users, err = convertLarkUserGroupToUser(j.jobSpec.LarkApproval.ID, node.CcGroups)
- if err != nil {
- return nil, fmt.Errorf("failed to convert lark user group to user: %s", err)
- }
- node.CcUsers = users
-
jobSpec.LarkApproval.ApprovalNodes[i] = node
}
if !lo.Contains([]string{"AND", "OR"}, string(node.Type)) {
@@ -337,46 +338,6 @@ func (j ApprovalJobController) ToTask(taskID int64) ([]*commonmodels.JobTask, er
return resp, nil
}
-func convertLarkUserGroupToUser(larkApprovalID string, groups []*commonmodels.LarkApprovalGroup) ([]*lark.UserInfo, error) {
- userSet := sets.NewString()
- users := make([]*lark.UserInfo, 0)
- for _, group := range groups {
- userGroup, err := larkservice.GetLarkUserGroup(larkApprovalID, group.GroupID)
- if err != nil {
- return nil, fmt.Errorf("failed to get lark user group: %s", err)
- }
-
- if userGroup.MemberUserCount > 0 {
- userInfos, err := larkservice.GetLarkUserGroupMembersInfo(larkApprovalID, group.GroupID, "user", setting.LarkUserOpenID, "")
- if err != nil {
- return nil, fmt.Errorf("failed to get lark department user infos: %s", err)
- }
-
- for _, user := range userInfos {
- if !userSet.Has(user.ID) {
- users = append(users, user)
- userSet.Insert(user.ID)
- }
- }
- }
-
- if userGroup.MemberDepartmentCount > 0 {
- userInfos, err := larkservice.GetLarkUserGroupMembersInfo(larkApprovalID, group.GroupID, "department", setting.LarkDepartmentID, "")
- if err != nil {
- return nil, fmt.Errorf("failed to get lark department user infos: %s", err)
- }
-
- for _, user := range userInfos {
- if !userSet.Has(user.ID) {
- users = append(users, user)
- userSet.Insert(user.ID)
- }
- }
- }
- }
- return users, nil
-}
-
func (j ApprovalJobController) SetRepo(repo *types.Repository) error {
return nil
}
diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_dms.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_dms.go
index 2cc1221c23..a225ed0681 100644
--- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_dms.go
+++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_dms.go
@@ -80,6 +80,11 @@ func (j DMSJobController) Validate(isExecution bool) error {
if j.jobSpec.ID != currJobSpec.ID {
return fmt.Errorf("given apollo job spec does not match current apollo job")
}
+ if mode := strings.ToLower(j.jobSpec.ExecuteMode); mode != "" &&
+ mode != string(config.DMSJobExecuteModeParallel) &&
+ mode != string(config.DMSJobExecuteModeSerial) {
+ return fmt.Errorf("invalid dms execute mode: %s", j.jobSpec.ExecuteMode)
+ }
if isExecution {
}
@@ -109,6 +114,7 @@ func (j DMSJobController) Update(useUserInput bool, ticket *commonmodels.Approva
j.jobSpec.ID = currJobSpec.ID
j.jobSpec.RemarkTemplate = currJobSpec.RemarkTemplate
+ j.jobSpec.ExecuteMode = normalizeDMSExecuteMode(currJobSpec.ExecuteMode)
return nil
}
@@ -138,7 +144,8 @@ func (j DMSJobController) ToTask(taskID int64) ([]*commonmodels.JobTask, error)
},
JobType: string(config.JobDMS),
Spec: &commonmodels.JobTaskDMSSpec{
- ID: j.jobSpec.ID,
+ ID: j.jobSpec.ID,
+ ExecuteMode: normalizeDMSExecuteMode(j.jobSpec.ExecuteMode),
Orders: func() (list []*commonmodels.DMSTaskOrder) {
for _, order := range j.jobSpec.Orders {
list = append(list, &commonmodels.DMSTaskOrder{
@@ -188,3 +195,12 @@ func (j DMSJobController) RenderDynamicVariableOptions(key string, option *Rende
func (j DMSJobController) IsServiceTypeJob() bool {
return false
}
+
+func normalizeDMSExecuteMode(mode string) string {
+ switch strings.ToLower(mode) {
+ case string(config.DMSJobExecuteModeSerial):
+ return string(config.DMSJobExecuteModeSerial)
+ default:
+ return string(config.DMSJobExecuteModeParallel)
+ }
+}
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..10b208235d 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
@@ -86,30 +86,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 +230,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/job_plugin.go b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_plugin.go
index 5efc574970..119bd493bf 100644
--- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_plugin.go
+++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/job/job_plugin.go
@@ -167,6 +167,17 @@ func (j PluginJobController) GetVariableList(jobName string, getAggregatedVariab
Type: "string",
IsCredential: false,
})
+
+ for _, output := range j.jobSpec.Plugin.Outputs {
+ if getServiceSpecificVariables {
+ resp = append(resp, &commonmodels.KeyVal{
+ Key: strings.Join([]string{"job", j.name, "output", output.Name}, "."),
+ Value: "",
+ Type: "string",
+ IsCredential: false,
+ })
+ }
+ }
}
return resp, nil
}
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 8d0060c44f..4106b19ae6 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
@@ -458,64 +458,9 @@ func generateKeyValsFromWorkflowParam(params []*commonmodels.Param) []*commonmod
return resp
}
-func repoNameToRepoIndex(repoName string) string {
- words := map[rune]string{
- '0': "A", '1': "B", '2': "C", '3': "D", '4': "E",
- '5': "F", '6': "G", '7': "H", '8': "I", '9': "J",
- }
- result := ""
- for i, digit := range repoName {
- if word, ok := words[digit]; ok {
- result += word
- } else {
- result += repoName[i:]
- break
- }
- }
-
- result = strings.Replace(result, "-", "_", -1)
- result = strings.Replace(result, ".", "_", -1)
-
- return result
-}
-
func getReposVariables(repos []*types.Repository) []*commonmodels.KeyVal {
- ret := make([]*commonmodels.KeyVal, 0)
- for index, repo := range repos {
- repoNameIndex := fmt.Sprintf("REPONAME_%d", index)
- ret = append(ret, &commonmodels.KeyVal{Key: repoNameIndex, Value: repo.RepoName, IsCredential: false})
-
- repoIndex := fmt.Sprintf("REPO_%d", index)
- repoName := repoNameToRepoIndex(repo.RepoName)
- ret = append(ret, &commonmodels.KeyVal{Key: repoIndex, Value: repoName, IsCredential: false})
-
- if len(repo.Branch) > 0 {
- ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_BRANCH", repoName), Value: repo.Branch, IsCredential: false})
- }
-
- if len(repo.Tag) > 0 {
- ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_TAG", repoName), Value: repo.Tag, IsCredential: false})
- }
-
- if repo.PR > 0 {
- ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PR", repoName), Value: strconv.Itoa(repo.PR), IsCredential: false})
- }
-
- ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PRE_MERGE_BRANCHES", repoName), Value: repo.GetPreMergeBranches(), IsCredential: false})
-
- ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_ORG", repoName), Value: repo.RepoOwner, IsCredential: false})
-
- if len(repo.PRs) > 0 {
- prStrs := []string{}
- for _, pr := range repo.PRs {
- prStrs = append(prStrs, strconv.Itoa(pr))
- }
- ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_PR", repoName), Value: strings.Join(prStrs, ","), IsCredential: false})
- }
-
- if len(repo.CommitID) > 0 {
- ret = append(ret, &commonmodels.KeyVal{Key: fmt.Sprintf("%s_COMMIT_ID", repoName), Value: repo.CommitID, IsCredential: false})
- }
+ ret := commonutil.RepoVariableKVs(repos)
+ for _, repo := range repos {
ret = append(ret, getEnvFromCommitMsg(repo.CommitMessage)...)
}
return ret
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 97c96bf7eb..c450fbf5e5 100644
--- a/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go
+++ b/pkg/microservice/aslan/core/workflow/service/workflow/controller/workflow.go
@@ -19,12 +19,10 @@ package controller
import (
"encoding/json"
"fmt"
- "net/url"
"regexp"
"strings"
"time"
- configbase "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"
commonrepo "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb"
@@ -33,7 +31,7 @@ import (
commonutil "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util"
jobctrl "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/workflow/service/workflow/controller/job"
"github.com/koderover/zadig/v2/pkg/setting"
- "github.com/koderover/zadig/v2/pkg/shared/client/plutusvendor"
+ "github.com/koderover/zadig/v2/pkg/shared/client/plutusenterprise"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
"github.com/koderover/zadig/v2/pkg/tool/log"
"github.com/koderover/zadig/v2/pkg/types"
@@ -208,19 +206,16 @@ func (w *Workflow) ToJobTasks(taskID int64, creator, account, uid string) ([]*co
}
}
+ pairs := make([]string, 0, len(globalKeyMap)*2)
+ for k, v := range globalKeyMap {
+ escaped, _ := json.Marshal(v)
+ pairs = append(pairs, "{{."+k+"}}", strings.Trim(string(escaped), `"`))
+ }
+ replacer := strings.NewReplacer(pairs...)
+
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)
- }
+ taskString := replacer.Replace(string(taskBytes))
err := json.Unmarshal([]byte(taskString), &task)
if err != nil {
@@ -355,40 +350,18 @@ func (w *Workflow) RenderWorkflowDefaultParams(taskID int64, creator, account, u
}
func (w *Workflow) getWorkflowDefaultParams(taskID int64, creator, account, uid string) ([]*commonmodels.Param, error) {
- resp := []*commonmodels.Param{}
projectInfo, err := templaterepo.NewProductColl().Find(w.Project)
if err != nil {
return nil, fmt.Errorf("failed to find project info for project %s, error: %s", w.Project, err)
}
- resp = append(resp, &commonmodels.Param{Name: "project", Value: w.Project, ParamsType: "string", IsCredential: false})
- resp = append(resp, &commonmodels.Param{Name: "project.id", Value: w.Project, ParamsType: "string", IsCredential: false})
- resp = append(resp, &commonmodels.Param{Name: "project.name", Value: projectInfo.ProjectName, ParamsType: "string", IsCredential: false})
- resp = append(resp, &commonmodels.Param{Name: "workflow.id", Value: w.Name, ParamsType: "string", IsCredential: false})
- resp = append(resp, &commonmodels.Param{Name: "workflow.name", Value: w.DisplayName, ParamsType: "string", IsCredential: false})
- resp = append(resp, &commonmodels.Param{Name: "workflow.task.id", Value: fmt.Sprintf("%d", taskID), ParamsType: "string", IsCredential: false})
- resp = append(resp, &commonmodels.Param{Name: "workflow.task.creator", Value: creator, ParamsType: "string", IsCredential: false})
- resp = append(resp, &commonmodels.Param{Name: "workflow.task.creator.id", Value: account, ParamsType: "string", IsCredential: false})
- resp = append(resp, &commonmodels.Param{Name: "workflow.task.creator.userId", Value: uid, ParamsType: "string", IsCredential: false})
- resp = append(resp, &commonmodels.Param{Name: "workflow.task.timestamp", Value: fmt.Sprintf("%d", time.Now().Unix()), ParamsType: "string", IsCredential: false})
- resp = append(resp, &commonmodels.Param{Name: "workflow.task.datetime", Value: time.Now().Format(time.DateTime), ParamsType: "string", IsCredential: false})
- detailURL := fmt.Sprintf("%s/v1/projects/detail/%s/pipelines/custom/%s/%d?display_name=%s",
- configbase.SystemAddress(),
- w.Project,
- w.Name,
- taskID,
- url.QueryEscape(w.DisplayName),
- )
- resp = append(resp, &commonmodels.Param{Name: "workflow.task.url", Value: detailURL, ParamsType: "string", IsCredential: false})
-
- for _, param := range w.Params {
- paramsKey := strings.Join([]string{"workflow", "params", param.Name}, ".")
- newParam := &commonmodels.Param{Name: paramsKey, Value: param.Value, ParamsType: "string", IsCredential: false}
- if param.ParamsType == string(commonmodels.MultiSelectType) {
- newParam.Value = strings.Join(param.ChoiceValue, ",")
- } else if param.ParamsType == string(commonmodels.FileType) {
- continue
- }
- resp = append(resp, newParam)
+ resp := make([]*commonmodels.Param, 0)
+ for _, kv := range commonutil.BuildWorkflowSystemVariableKVs(w.WorkflowV4, w.Project, projectInfo.ProjectName, taskID, creator, account, uid, time.Now()) {
+ resp = append(resp, &commonmodels.Param{
+ Name: kv.Key,
+ Value: kv.Value,
+ ParamsType: "string",
+ IsCredential: kv.IsCredential,
+ })
}
return resp, nil
}
@@ -413,7 +386,7 @@ func (w *Workflow) Validate(isExecution bool) error {
return e.ErrLintWorkflow.AddErr(err)
}
- licenseStatus, err := plutusvendor.New().CheckZadigXLicenseStatus()
+ licenseStatus, err := plutusenterprise.New().CheckZadigXLicenseStatus()
if err != nil {
return fmt.Errorf("failed to validate zadig license status, error: %s", err)
}
@@ -450,7 +423,11 @@ func (w *Workflow) Validate(isExecution bool) error {
return e.ErrLicenseInvalid.AddDesc("基础版不支持工作流手动执行")
}
}
-
+ if stage.ManualExec != nil && stage.ManualExec.LarkPersonNotificationConfig != nil {
+ if stage.ManualExec.LarkPersonNotificationConfig.AppID == "" {
+ return e.ErrLintWorkflow.AddDesc(fmt.Sprintf("manual execution notification app id cannot be empty for stage %s", stage.Name))
+ }
+ }
if _, ok := stageNameMap[stage.Name]; !ok {
stageNameMap[stage.Name] = true
} else {
diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/types.go b/pkg/microservice/aslan/core/workflow/service/workflow/types.go
index 19694482ac..dfb8206e5a 100644
--- a/pkg/microservice/aslan/core/workflow/service/workflow/types.go
+++ b/pkg/microservice/aslan/core/workflow/service/workflow/types.go
@@ -140,6 +140,8 @@ type CreateCustomTaskNotifyInput struct {
ID int `json:"id"`
// 通知类型,支持:feishu 飞书群组通知(自定义机器人)、feishu_app 飞书群组通知(自建应用)、feishu_person 飞书成员通知、dingding 钉钉,wechat 企业微信、msteams Teams、mail 邮件
Type setting.NotifyWebHookType `json:"type"`
+ // 运行时是否启用该通知;为空时保持原有配置
+ Enabled *bool `json:"enabled,omitempty"`
// 飞书群组通知(自定义机器人)配置
LarkHookNotificationConfig *CreateCustomTaskLarkHookNotificationConfig `json:"lark_hook_notification_config"`
// 飞书群通知(自建应用)配置
@@ -156,42 +158,59 @@ type CreateCustomTaskNotifyInput struct {
MailNotificationConfig *CreateCustomTaskMailNotificationConfig `json:"mail_notification_config"`
}
+type CreateCustomTaskDynamicRecipient struct {
+ Value string `json:"value"`
+ IdentityType string `json:"identity_type"`
+}
+
type CreateCustomTaskLarkUserInfo struct {
ID string `json:"id"`
// 支持 open_id、user_id
IDType string `json:"id_type"`
+ // 标记该成员是否为工作流执行人
+ IsExecutor bool `json:"is_executor,omitempty"`
+ // 标记该成员是否为当前阶段执行人
+ IsStageExecutor bool `json:"is_stage_executor,omitempty"`
}
type CreateCustomTaskLarkGroupNotificationConfig struct {
- ChatID string `json:"chat_id"`
- AtUsers []CreateCustomTaskLarkUserInfo `json:"at_users"`
+ ChatID string `json:"chat_id"`
+ AtUsers []CreateCustomTaskLarkUserInfo `json:"at_users"`
+ DynamicRecipients []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"`
}
type CreateCustomTaskLarkPersonNotificationConfig struct {
- Users []CreateCustomTaskLarkUserInfo `json:"users"`
+ Users []CreateCustomTaskLarkUserInfo `json:"users"`
+ DynamicRecipients []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"`
}
type CreateCustomTaskLarkHookNotificationConfig struct {
- AtUsers []string `json:"at_users"`
- IsAtAll bool `json:"is_at_all"`
+ AtUsers []string `json:"at_users"`
+ DynamicRecipients []CreateCustomTaskDynamicRecipient `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 []CreateCustomTaskDynamicRecipient `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 []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"`
+ IsAtAll bool `json:"is_at_all"`
}
type CreateCustomTaskMSTeamsNotificationConfig struct {
- AtEmails []string `json:"at_emails"`
+ AtEmails []string `json:"at_emails"`
+ DynamicRecipients []CreateCustomTaskDynamicRecipient `json:"dynamic_recipients"`
}
type CreateCustomTaskMailNotificationConfig struct {
- UserIDs []string `json:"user_ids"`
+ UserIDs []string `json:"user_ids"`
+ Users []*commonmodels.User `json:"users"`
+ DynamicRecipients []CreateCustomTaskDynamicRecipient `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 682d7342cd..94f5c925d8 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
@@ -664,6 +664,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)
@@ -690,9 +691,27 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea
notifyInputsMap[notifyInput.ID] = notifyInput
}
+ toDynamicRecipients := func(inputs []CreateCustomTaskDynamicRecipient) []*commonmodels.DynamicRecipient {
+ resp := make([]*commonmodels.DynamicRecipient, 0, len(inputs))
+ for _, input := range inputs {
+ if input.Value == "" {
+ continue
+ }
+ resp = append(resp, &commonmodels.DynamicRecipient{
+ Value: input.Value,
+ IdentityType: input.IdentityType,
+ })
+ }
+ return resp
+ }
+
for i, notifyCtl := range notifyCtls {
notifyInput, ok := notifyInputsMap[i]
if ok && notifyCtl.WebHookType == notifyInput.Type {
+ if notifyInput.Enabled != nil {
+ notifyCtl.Enabled = *notifyInput.Enabled
+ }
+
switch notifyCtl.WebHookType {
case setting.NotifyWebHookTypeFeishu:
if notifyCtl.LarkHookNotificationConfig == nil {
@@ -701,9 +720,10 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea
}
config := &commonmodels.LarkHookNotificationConfig{
- HookAddress: notifyCtl.LarkHookNotificationConfig.HookAddress,
- AtUsers: notifyInput.LarkHookNotificationConfig.AtUsers,
- IsAtAll: notifyInput.LarkHookNotificationConfig.IsAtAll,
+ HookAddress: notifyCtl.LarkHookNotificationConfig.HookAddress,
+ AtUsers: notifyInput.LarkHookNotificationConfig.AtUsers,
+ DynamicRecipients: toDynamicRecipients(notifyInput.LarkHookNotificationConfig.DynamicRecipients),
+ IsAtAll: notifyInput.LarkHookNotificationConfig.IsAtAll,
}
notifyCtl.LarkHookNotificationConfig = config
@@ -714,14 +734,17 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea
}
config := &commonmodels.LarkPersonNotificationConfig{
- AppID: notifyCtl.LarkPersonNotificationConfig.AppID,
+ AppID: notifyCtl.LarkPersonNotificationConfig.AppID,
+ DynamicRecipients: toDynamicRecipients(notifyInput.LarkPersonNotificationConfig.DynamicRecipients),
}
targetUsers := make([]*larktool.UserInfo, 0)
for _, user := range notifyInput.LarkPersonNotificationConfig.Users {
targetUsers = append(targetUsers, &larktool.UserInfo{
- ID: user.ID,
- IDType: user.IDType,
+ ID: user.ID,
+ IDType: user.IDType,
+ IsExecutor: user.IsExecutor,
+ IsStageExecutor: user.IsStageExecutor,
})
}
config.TargetUsers = targetUsers
@@ -734,7 +757,8 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea
}
config := &commonmodels.LarkGroupNotificationConfig{
- AppID: notifyCtl.LarkGroupNotificationConfig.AppID,
+ AppID: notifyCtl.LarkGroupNotificationConfig.AppID,
+ DynamicRecipients: toDynamicRecipients(notifyInput.LarkGroupNotificationConfig.DynamicRecipients),
Chat: &commonmodels.LarkChat{
ChatID: notifyInput.LarkGroupNotificationConfig.ChatID,
},
@@ -757,9 +781,10 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea
}
config := &commonmodels.WechatNotificationConfig{
- HookAddress: notifyCtl.WechatNotificationConfig.HookAddress,
- AtUsers: notifyInput.WechatNotificationConfig.AtUsers,
- IsAtAll: notifyInput.WechatNotificationConfig.IsAtAll,
+ HookAddress: notifyCtl.WechatNotificationConfig.HookAddress,
+ AtUsers: notifyInput.WechatNotificationConfig.AtUsers,
+ DynamicRecipients: toDynamicRecipients(notifyInput.WechatNotificationConfig.DynamicRecipients),
+ IsAtAll: notifyInput.WechatNotificationConfig.IsAtAll,
}
notifyCtl.WechatNotificationConfig = config
@@ -770,9 +795,10 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea
}
config := &commonmodels.DingDingNotificationConfig{
- HookAddress: notifyCtl.DingDingNotificationConfig.HookAddress,
- AtMobiles: notifyInput.DingDingNotificationConfig.AtMobiles,
- IsAtAll: notifyInput.DingDingNotificationConfig.IsAtAll,
+ HookAddress: notifyCtl.DingDingNotificationConfig.HookAddress,
+ AtMobiles: notifyInput.DingDingNotificationConfig.AtMobiles,
+ DynamicRecipients: toDynamicRecipients(notifyInput.DingDingNotificationConfig.DynamicRecipients),
+ IsAtAll: notifyInput.DingDingNotificationConfig.IsAtAll,
}
notifyCtl.DingDingNotificationConfig = config
@@ -783,8 +809,9 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea
}
config := &commonmodels.MSTeamsNotificationConfig{
- HookAddress: notifyCtl.MSTeamsNotificationConfig.HookAddress,
- AtEmails: notifyInput.MSTeamsNotificationConfig.AtEmails,
+ HookAddress: notifyCtl.MSTeamsNotificationConfig.HookAddress,
+ AtEmails: notifyInput.MSTeamsNotificationConfig.AtEmails,
+ DynamicRecipients: toDynamicRecipients(notifyInput.MSTeamsNotificationConfig.DynamicRecipients),
}
notifyCtl.MSTeamsNotificationConfig = config
@@ -795,13 +822,30 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea
}
config := &commonmodels.MailNotificationConfig{
- TargetUsers: make([]*commonmodels.User, 0),
+ TargetUsers: make([]*commonmodels.User, 0),
+ DynamicRecipients: toDynamicRecipients(notifyInput.MailNotificationConfig.DynamicRecipients),
}
- for _, userID := range notifyInput.MailNotificationConfig.UserIDs {
- config.TargetUsers = append(config.TargetUsers, &commonmodels.User{
- UserID: userID,
- })
+ if len(notifyInput.MailNotificationConfig.Users) > 0 {
+ for _, user := range notifyInput.MailNotificationConfig.Users {
+ if user == nil {
+ continue
+ }
+ config.TargetUsers = append(config.TargetUsers, &commonmodels.User{
+ Type: user.Type,
+ UserID: user.UserID,
+ UserName: user.UserName,
+ GroupID: user.GroupID,
+ GroupName: user.GroupName,
+ })
+ }
+ } else {
+ for _, userID := range notifyInput.MailNotificationConfig.UserIDs {
+ config.TargetUsers = append(config.TargetUsers, &commonmodels.User{
+ Type: setting.UserTypeUser,
+ UserID: userID,
+ })
+ }
}
notifyCtl.MailNotificationConfig = config
@@ -813,6 +857,34 @@ func updateNotifyCtls(notifyCtls []*commonmodels.NotifyCtl, notifyInputs []*Crea
return notifyCtls
}
+func buildWorkflowTaskRuntimeContext(task *commonmodels.WorkflowTask) map[string]string {
+ if task == nil || task.WorkflowArgs == nil {
+ return nil
+ }
+
+ keyMap := commonutil.KeyValsToMap(commonutil.BuildWorkflowRuntimeVariableKVs(
+ task.WorkflowArgs,
+ task.ProjectName,
+ task.ProjectDisplayName,
+ task.TaskID,
+ task.TaskCreator,
+ task.TaskCreatorAccount,
+ task.TaskCreatorID,
+ time.Unix(task.StartTime, 0),
+ ))
+
+ resp := make(map[string]string, len(keyMap))
+ for key, value := range keyMap {
+ // Payload variables are resolved at task creation time and stored in RawPayload;
+ // they don't need to be persisted in GlobalContext (which would duplicate them in MongoDB).
+ 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) {
originWorkflow, err := commonrepo.NewWorkflowV4Coll().Find(workflowName)
if err != nil {
@@ -890,7 +962,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 {
@@ -942,6 +1023,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 {
@@ -1018,7 +1100,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 {
@@ -1091,6 +1182,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 {
@@ -3152,8 +3244,6 @@ func ListWorkflowFilterInfo(project, workflow, typeName string, jobName string,
return []*ListWorkflowFilterInfoResponse{}, fmt.Errorf("paramerter is empty")
}
- envMap := make(map[string]*commonmodels.Product)
-
switch typeName {
case "creator":
creators, err := commonrepo.NewworkflowTaskv4Coll().ListCreator(project, workflow)
@@ -3171,37 +3261,30 @@ func ListWorkflowFilterInfo(project, workflow, typeName string, jobName string,
}
return resp, nil
case "envName":
- workflow, err := commonrepo.NewWorkflowV4Coll().Find(workflow)
+ productList, err := commonrepo.NewProductColl().List(&commonrepo.ProductListOptions{
+ Name: project,
+ })
if err != nil {
- logger.Errorf("failed to find workflow %s: %v", workflow, err)
- return []*ListWorkflowFilterInfoResponse{}, fmt.Errorf("failed to find workflow %s: %v", workflow, err)
+ logger.Errorf("failed to list envs from product for project %s: %v", project, err)
+ return []*ListWorkflowFilterInfoResponse{}, fmt.Errorf("failed to list envs from product for project %s: %v", project, err)
}
- resp := make([]*ListWorkflowFilterInfoResponse, 0)
- for _, stage := range workflow.Stages {
- for _, job := range stage.Jobs {
- if job.Name == jobName && job.JobType == config.JobZadigDeploy {
- deploy := &commonmodels.ZadigDeployJobSpec{}
- if err := commonmodels.IToi(job.Spec, deploy); err != nil {
- return nil, err
- }
-
- env, _ := CheckFixedMarkReturnNoFixedEnv(deploy.Env)
- if envMap[env] == nil {
- envInfo, err := commonutil.GetEnvInfo(project, env, envMap)
- if err != nil {
- return nil, err
- }
-
- resp = append(resp, &ListWorkflowFilterInfoResponse{
- Key: envInfo.EnvName,
- Name: envInfo.Alias,
- })
- }
- return resp, nil
- }
+ resp := make([]*ListWorkflowFilterInfoResponse, 0, len(productList))
+ for _, envInfo := range productList {
+ if envInfo == nil {
+ continue
}
+ name := envInfo.EnvName
+ if envInfo.Alias != "" {
+ name = envInfo.Alias
+ }
+
+ resp = append(resp, &ListWorkflowFilterInfoResponse{
+ Key: envInfo.EnvName,
+ Name: name,
+ })
}
+
return resp, nil
case "serviceName":
services := make([]string, 0)
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 f6c7754643..eec39e6d20 100644
--- a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go
+++ b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_v4.go
@@ -58,6 +58,7 @@ import (
commonservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/collaboration"
helmservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/helm"
+ runtimeWorkflowController "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/workflowcontroller"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/kube"
larkservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/lark"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/service/s3"
@@ -305,6 +306,20 @@ func FindWorkflowV4Raw(name string, logger *zap.SugaredLogger) (*commonmodels.Wo
}
func DeleteWorkflowV4(name string, logger *zap.SugaredLogger) error {
+ // Cancel any queued or running tasks before deleting the workflow.
+ taskQueue, err := commonrepo.NewWorkflowQueueColl().List(&commonrepo.ListWorfklowQueueOption{
+ WorkflowName: name,
+ })
+ if err != nil {
+ logger.Errorf("Failed to list queued tasks for workflow %s, the error is: %v", name, err)
+ return e.ErrDeleteWorkflow.AddErr(err)
+ }
+ for _, task := range taskQueue {
+ if err := runtimeWorkflowController.CancelWorkflowTask("system", task.WorkflowName, task.TaskID, logger); err != nil {
+ logger.Warnf("Failed to cancel task %d for workflow %s before deletion, the error is: %v", task.TaskID, name, err)
+ }
+ }
+
workflow, err := commonrepo.NewWorkflowV4Coll().Find(name)
if err != nil {
logger.Errorf("Failed to delete WorkflowV4: %s, the error is: %v", name, err)
diff --git a/pkg/microservice/aslan/core/workflow/testing/service/scanning.go b/pkg/microservice/aslan/core/workflow/testing/service/scanning.go
index 8229f29465..4475ce3ceb 100644
--- a/pkg/microservice/aslan/core/workflow/testing/service/scanning.go
+++ b/pkg/microservice/aslan/core/workflow/testing/service/scanning.go
@@ -19,7 +19,6 @@ package service
import (
"context"
"fmt"
- "strings"
"go.mongodb.org/mongo-driver/mongo"
"go.uber.org/zap"
@@ -46,6 +45,9 @@ func CreateScanningModule(username string, args *Scanning, log *zap.SugaredLogge
if len(args.Name) == 0 {
return e.ErrCreateScanningModule.AddDesc("empty Name")
}
+ if err := commonutil.ValidateGeneratedWorkflowJobName(args.Name, commonutil.GenerateScanningModuleJobName); err != nil {
+ return e.ErrCreateScanningModule.AddDesc(err.Error())
+ }
err := util.CheckDefineResourceParam(args.AdvancedSetting.ResReq, args.AdvancedSetting.ResReqSpec)
if err != nil {
@@ -74,6 +76,9 @@ func UpdateScanningModule(id, username string, args *Scanning, log *zap.SugaredL
if len(args.Name) == 0 {
return e.ErrUpdateScanningModule.AddDesc("empty Name")
}
+ if err := commonutil.ValidateGeneratedWorkflowJobName(args.Name, commonutil.GenerateScanningModuleJobName); err != nil {
+ return e.ErrUpdateScanningModule.AddDesc(err.Error())
+ }
scanning, err := commonrepo.NewScanningColl().GetByID(id)
if err != nil {
@@ -537,12 +542,8 @@ func generateCustomWorkflowFromScanningModule(scanInfo *commonmodels.Scanning, a
}
job := make([]*commonmodels.Job, 0)
- name := scanInfo.Name
- if len(name) >= 32 {
- name = strings.TrimSuffix(scanInfo.Name[:31], "-")
- }
job = append(job, &commonmodels.Job{
- Name: name,
+ Name: commonutil.GenerateScanningModuleJobName(scanInfo.Name),
JobType: config.JobZadigScanning,
Skipped: false,
Spec: &commonmodels.ZadigScanningJobSpec{
diff --git a/pkg/microservice/aslan/core/workflow/testing/service/test_task.go b/pkg/microservice/aslan/core/workflow/testing/service/test_task.go
index 77a4468d53..c2e84b1b27 100644
--- a/pkg/microservice/aslan/core/workflow/testing/service/test_task.go
+++ b/pkg/microservice/aslan/core/workflow/testing/service/test_task.go
@@ -19,7 +19,6 @@ package service
import (
"fmt"
"strconv"
- "strings"
"github.com/koderover/zadig/v2/pkg/types"
"go.uber.org/zap"
@@ -341,7 +340,7 @@ func generateCustomWorkflowFromTestingModule(testInfo *commonmodels.Testing, arg
job := make([]*commonmodels.Job, 0)
job = append(job, &commonmodels.Job{
- Name: strings.ToLower(testInfo.Name),
+ Name: util.GenerateTestingModuleJobName(testInfo.Name),
JobType: config.JobZadigTesting,
Skipped: false,
Spec: &commonmodels.ZadigTestingJobSpec{
diff --git a/pkg/microservice/aslan/core/workflow/testing/service/testing.go b/pkg/microservice/aslan/core/workflow/testing/service/testing.go
index c30504f946..3b93a25614 100644
--- a/pkg/microservice/aslan/core/workflow/testing/service/testing.go
+++ b/pkg/microservice/aslan/core/workflow/testing/service/testing.go
@@ -52,6 +52,9 @@ func CreateTesting(username string, testing *commonmodels.Testing, log *zap.Suga
if len(testing.Name) == 0 {
return e.ErrCreateTestModule.AddDesc("empty Name")
}
+ if err := commonutil.ValidateGeneratedWorkflowJobName(testing.Name, commonutil.GenerateTestingModuleJobName); err != nil {
+ return e.ErrCreateTestModule.AddDesc(err.Error())
+ }
if err := commonutil.CheckDefineResourceParam(testing.PreTest.ResReq, testing.PreTest.ResReqSpec); err != nil {
return e.ErrCreateTestModule.AddDesc(err.Error())
}
@@ -115,6 +118,9 @@ func UpdateTesting(username string, testing *commonmodels.Testing, log *zap.Suga
if len(testing.Name) == 0 {
return e.ErrUpdateTestModule.AddDesc("empty Name")
}
+ if err := commonutil.ValidateGeneratedWorkflowJobName(testing.Name, commonutil.GenerateTestingModuleJobName); err != nil {
+ return e.ErrUpdateTestModule.AddDesc(err.Error())
+ }
if err := commonutil.CheckDefineResourceParam(testing.PreTest.ResReq, testing.PreTest.ResReqSpec); err != nil {
return e.ErrUpdateTestModule.AddDesc(err.Error())
}
diff --git a/pkg/microservice/user/core/handler/permission/role.go b/pkg/microservice/user/core/handler/permission/role.go
index 8b664310e5..e494db59c4 100644
--- a/pkg/microservice/user/core/handler/permission/role.go
+++ b/pkg/microservice/user/core/handler/permission/role.go
@@ -28,20 +28,20 @@ import (
userhandler "github.com/koderover/zadig/v2/pkg/microservice/user/core/handler/user"
"github.com/koderover/zadig/v2/pkg/microservice/user/core/service/permission"
"github.com/koderover/zadig/v2/pkg/setting"
- "github.com/koderover/zadig/v2/pkg/shared/client/plutusvendor"
+ "github.com/koderover/zadig/v2/pkg/shared/client/plutusenterprise"
internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
"github.com/koderover/zadig/v2/pkg/tool/log"
)
func checkLicense(actions []string) error {
- licenseStatus, err := plutusvendor.New().CheckZadigXLicenseStatus()
+ licenseStatus, err := plutusenterprise.New().CheckZadigXLicenseStatus()
if err != nil {
return fmt.Errorf("failed to validate zadig license status, error: %s", err)
}
- if !((licenseStatus.Type == plutusvendor.ZadigSystemTypeProfessional ||
- licenseStatus.Type == plutusvendor.ZadigSystemTypeEnterprise) &&
- licenseStatus.Status == plutusvendor.ZadigXLicenseStatusNormal) {
+ if !((licenseStatus.Type == plutusenterprise.ZadigSystemTypeProfessional ||
+ licenseStatus.Type == plutusenterprise.ZadigSystemTypeEnterprise) &&
+ licenseStatus.Status == plutusenterprise.ZadigXLicenseStatusNormal) {
actionSet := sets.NewString(actions...)
if actionSet.Has(permission.VerbCreateReleasePlan) || actionSet.Has(permission.VerbDeleteReleasePlan) ||
actionSet.Has(permission.VerbEditReleasePlanMetadata) || actionSet.Has(permission.VerbEditReleasePlanApproval) ||
@@ -240,14 +240,14 @@ func UpdateRoleImpl(c *gin.Context, ctx *internalhandler.Context) {
}
}
- //licenseStatus, err := plutusvendor.New().CheckZadigXLicenseStatus()
+ //licenseStatus, err := plutusenterprise.New().CheckZadigXLicenseStatus()
//if err != nil {
// ctx.RespErr = fmt.Errorf("failed to validate zadig license status, error: %s", err)
// return
//}
- //if !((licenseStatus.Type == plutusvendor.ZadigSystemTypeProfessional ||
- // licenseStatus.Type == plutusvendor.ZadigSystemTypeEnterprise) &&
- // licenseStatus.Status == plutusvendor.ZadigXLicenseStatusNormal) {
+ //if !((licenseStatus.Type == plutusenterprise.ZadigSystemTypeProfessional ||
+ // licenseStatus.Type == plutusenterprise.ZadigSystemTypeEnterprise) &&
+ // licenseStatus.Status == plutusenterprise.ZadigXLicenseStatusNormal) {
// actionSet := sets.NewString(args.Actions...)
// if actionSet.Has(permission.VerbCreateReleasePlan) || actionSet.Has(permission.VerbDeleteReleasePlan) ||
// actionSet.Has(permission.VerbEditReleasePlan) || actionSet.Has(permission.VerbGetReleasePlan) ||
diff --git a/pkg/microservice/user/core/handler/user/mfa.go b/pkg/microservice/user/core/handler/user/mfa.go
index 8283e0cda1..7b5894aa75 100644
--- a/pkg/microservice/user/core/handler/user/mfa.go
+++ b/pkg/microservice/user/core/handler/user/mfa.go
@@ -21,6 +21,7 @@ import (
"github.com/gin-gonic/gin"
+ commonutil "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util"
loginsvc "github.com/koderover/zadig/v2/pkg/microservice/user/core/service/login"
internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
@@ -76,6 +77,12 @@ func ResetUserMFAByAdmin(c *gin.Context) {
return
}
+ err = commonutil.CheckZadigProfessionalLicense()
+ if err != nil {
+ ctx.RespErr = err
+ return
+ }
+
ctx.RespErr = loginsvc.ResetUserMFA(c.Param("uid"), ctx.Logger)
}
@@ -136,6 +143,12 @@ func EnableUserMFA(c *gin.Context) {
return
}
+ err = commonutil.CheckZadigProfessionalLicense()
+ if err != nil {
+ ctx.RespErr = err
+ return
+ }
+
args := &loginsvc.MFAEnrollArgs{}
if err := c.ShouldBindJSON(args); err != nil {
ctx.RespErr = err
@@ -177,7 +190,7 @@ func DisableUserMFA(c *gin.Context) {
ctx.RespErr = err
return
}
- ctx.RespErr = loginsvc.DisableUserMFA(uid, args, ctx.Logger)
+ ctx.Resp, ctx.RespErr = loginsvc.DisableUserMFA(uid, args, ctx.Logger)
}
// @Summary 重新生成恢复码
diff --git a/pkg/microservice/user/core/handler/user/user.go b/pkg/microservice/user/core/handler/user/user.go
index a077d1e16a..8d9b2b86a4 100644
--- a/pkg/microservice/user/core/handler/user/user.go
+++ b/pkg/microservice/user/core/handler/user/user.go
@@ -341,10 +341,10 @@ func OpenAPIListUsersBrief(c *gin.Context) {
for _, userInfo := range resp.Users {
briefUserList = append(briefUserList, &types.UserBriefInfo{
LastLoginTime: userInfo.LastLoginTime,
- UID: userInfo.Uid,
- Account: userInfo.Account,
- IdentityType: userInfo.IdentityType,
- Name: userInfo.Name,
+ UID: userInfo.Uid,
+ Account: userInfo.Account,
+ IdentityType: userInfo.IdentityType,
+ Name: userInfo.Name,
})
}
@@ -401,10 +401,10 @@ func ListUsersBrief(c *gin.Context) {
for _, userInfo := range resp.Users {
briefUserList = append(briefUserList, &types.UserBriefInfo{
LastLoginTime: userInfo.LastLoginTime,
- UID: userInfo.Uid,
- Account: userInfo.Account,
- IdentityType: userInfo.IdentityType,
- Name: userInfo.Name,
+ UID: userInfo.Uid,
+ Account: userInfo.Account,
+ IdentityType: userInfo.IdentityType,
+ Name: userInfo.Name,
})
}
diff --git a/pkg/microservice/user/core/init/action_initialization.sql b/pkg/microservice/user/core/init/action_initialization.sql
index c345dd361c..f78e7a1c38 100644
--- a/pkg/microservice/user/core/init/action_initialization.sql
+++ b/pkg/microservice/user/core/init/action_initialization.sql
@@ -11,6 +11,7 @@ VALUES
("查看", "get_environment", "Environment", 1),
("创建", "create_environment", "Environment", 1),
("配置", "config_environment", "Environment", 1),
+ ("调整副本", "scale_environment", "Environment", 1),
("管理服务实例", "manage_environment", "Environment", 1),
("重启", "restart_environment", "Environment", 1),
("回滚", "rollback_environment", "Environment", 1),
@@ -20,6 +21,7 @@ VALUES
("查看", "get_production_environment", "ProductionEnvironment", 1),
("创建", "create_production_environment", "ProductionEnvironment", 1),
("配置", "config_production_environment", "ProductionEnvironment", 1),
+ ("调整副本", "scale_production_environment", "ProductionEnvironment", 1),
("管理服务实例", "edit_production_environment", "ProductionEnvironment", 1),
("重启", "restart_production_environment", "ProductionEnvironment", 1),
("回滚", "rollback_production_environment", "ProductionEnvironment", 1),
@@ -71,6 +73,9 @@ VALUES
("删除", "delete_release_plan", "ReleasePlan", 2),
("配置", "edit_config_release_plan", "ReleasePlan", 2),
("查看", "get_business_directory", "BusinessDirectory", 2),
+ ("新建", "create_business_directory", "BusinessDirectory", 2),
+ ("编辑", "edit_business_directory", "BusinessDirectory", 2),
+ ("删除", "delete_business_directory", "BusinessDirectory", 2),
("查看", "get_cluster_management", "ClusterManagement", 2),
("新建", "create_cluster_management", "ClusterManagement", 2),
("编辑", "edit_cluster_management", "ClusterManagement", 2),
diff --git a/pkg/microservice/user/core/init/dm_action_initialization.sql b/pkg/microservice/user/core/init/dm_action_initialization.sql
index da5b9e6488..6c8bb04950 100644
--- a/pkg/microservice/user/core/init/dm_action_initialization.sql
+++ b/pkg/microservice/user/core/init/dm_action_initialization.sql
@@ -11,6 +11,7 @@ VALUES
('查看', 'get_environment', 'Environment', 1),
('创建', 'create_environment', 'Environment', 1),
('配置', 'config_environment', 'Environment', 1),
+ ('调整副本', 'scale_environment', 'Environment', 1),
('管理服务实例', 'manage_environment', 'Environment', 1),
('重启', 'restart_environment', 'Environment', 1),
('回滚', 'rollback_environment', 'Environment', 1),
@@ -20,6 +21,7 @@ VALUES
('查看', 'get_production_environment', 'ProductionEnvironment', 1),
('创建', 'create_production_environment', 'ProductionEnvironment', 1),
('配置', 'config_production_environment', 'ProductionEnvironment', 1),
+ ('调整副本', 'scale_production_environment', 'ProductionEnvironment', 1),
('管理服务实例', 'edit_production_environment', 'ProductionEnvironment', 1),
('重启', 'restart_production_environment', 'ProductionEnvironment', 1),
('回滚', 'rollback_production_environment', 'ProductionEnvironment', 1),
@@ -68,6 +70,9 @@ VALUES
('编辑', 'edit_release_plan', 'ReleasePlan', 2),
('删除', 'delete_release_plan', 'ReleasePlan', 2),
('查看', 'get_business_directory', 'BusinessDirectory', 2),
+ ('新建', 'create_business_directory', 'BusinessDirectory', 2),
+ ('编辑', 'edit_business_directory', 'BusinessDirectory', 2),
+ ('删除', 'delete_business_directory', 'BusinessDirectory', 2),
('查看', 'get_cluster_management', 'ClusterManagement', 2),
('新建', 'create_cluster_management', 'ClusterManagement', 2),
('编辑', 'edit_cluster_management', 'ClusterManagement', 2),
diff --git a/pkg/microservice/user/core/init/dm_mysql.sql b/pkg/microservice/user/core/init/dm_mysql.sql
index 9108c58d3a..a06dee9bf2 100644
--- a/pkg/microservice/user/core/init/dm_mysql.sql
+++ b/pkg/microservice/user/core/init/dm_mysql.sql
@@ -78,6 +78,7 @@ CREATE TABLE IF NOT EXISTS role (
name varchar(32) NOT NULL COMMENT '角色名称',
description varchar(64) NOT NULL COMMENT '描述',
type int NOT NULL COMMENT '资源范围,1-系统自带, 2-用户自定义',
+ global_read_only tinyint NOT NULL DEFAULT '0' COMMENT '全局只读开关,开启后可对所有项目扩散只读权限',
namespace varchar(32) NOT NULL COMMENT '所属项目,*为全局角色标记',
PRIMARY KEY (id)
) ;
diff --git a/pkg/microservice/user/core/init/mysql.sql b/pkg/microservice/user/core/init/mysql.sql
index 6d53b40b82..68d491b991 100644
--- a/pkg/microservice/user/core/init/mysql.sql
+++ b/pkg/microservice/user/core/init/mysql.sql
@@ -74,7 +74,9 @@ CREATE TABLE IF NOT EXISTS `role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(32) NOT NULL COMMENT '角色名称',
`description` varchar(64) NOT NULL COMMENT '描述',
+
`type` int(11) NOT NULL COMMENT '资源范围,1-系统自带, 2-用户自定义',
+ `global_read_only` tinyint(1) NOT NULL DEFAULT '0' COMMENT '全局只读开关,开启后可对所有项目扩散只读权限',
`namespace` varchar(32) NOT NULL COMMENT '所属项目,*为全局角色标记',
PRIMARY KEY (`id`),
UNIQUE KEY `namespaced_role` (`namespace`, `name`)
diff --git a/pkg/microservice/user/core/repository/models/role.go b/pkg/microservice/user/core/repository/models/role.go
index 5709c3258c..614bbb02b5 100644
--- a/pkg/microservice/user/core/repository/models/role.go
+++ b/pkg/microservice/user/core/repository/models/role.go
@@ -36,11 +36,12 @@ func (Role) TableName() string {
// NewRole is the schema for role in mysql database, after version 1.7
type NewRole struct {
- ID uint `gorm:"primarykey" json:"id"`
- Name string `gorm:"column:name" json:"name"`
- Description string `gorm:"column:description" json:"description"`
- Type int64 `gorm:"column:type" json:"type"`
- Namespace string `gorm:"column:namespace" json:"namespace"`
+ ID uint `gorm:"primarykey" json:"id"`
+ Name string `gorm:"column:name" json:"name"`
+ Description string `gorm:"column:description" json:"description"`
+ Type int64 `gorm:"column:type" json:"type"`
+ Namespace string `gorm:"column:namespace" json:"namespace"`
+ GlobalReadOnly bool `gorm:"column:global_read_only" json:"global_read_only"`
RoleActionBindings []RoleActionBinding `gorm:"foreignKey:RoleID;constraint:OnDelete:CASCADE;" json:"-"`
RoleUserBindings []NewRoleBinding `gorm:"foreignKey:RoleID;constraint:OnDelete:CASCADE;" json:"-"`
diff --git a/pkg/microservice/user/core/repository/mongodb/role.go b/pkg/microservice/user/core/repository/mongodb/role.go
deleted file mode 100644
index 8c9ee9ac47..0000000000
--- a/pkg/microservice/user/core/repository/mongodb/role.go
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
-Copyright 2023 The KodeRover Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package mongodb
-
-import (
- "context"
- "errors"
- "fmt"
-
- "go.mongodb.org/mongo-driver/bson"
- "go.mongodb.org/mongo-driver/mongo"
- "go.mongodb.org/mongo-driver/mongo/options"
-
- "github.com/koderover/zadig/v2/pkg/config"
- "github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/models"
- mongotool "github.com/koderover/zadig/v2/pkg/tool/mongo"
-)
-
-type RoleColl struct {
- *mongo.Collection
-
- coll string
-}
-
-func NewRoleColl() *RoleColl {
- name := models.Role{}.TableName()
- return &RoleColl{
- Collection: mongotool.Database(config.PolicyDatabase()).Collection(name),
- coll: name,
- }
-}
-
-func (c *RoleColl) GetCollectionName() string {
- return c.coll
-}
-
-func (c *RoleColl) Get(ns, name string) (*models.Role, bool, error) {
- res := &models.Role{}
-
- query := bson.M{"namespace": ns, "name": name}
- err := c.FindOne(context.TODO(), query).Decode(res)
- if err != nil {
- if err == mongo.ErrNoDocuments {
- return nil, false, nil
- }
-
- return nil, false, err
- }
-
- return res, true, nil
-}
-
-func (c *RoleColl) List() ([]*models.Role, error) {
- var res []*models.Role
-
- ctx := context.Background()
-
- cursor, err := c.Collection.Find(ctx, bson.M{})
- if err != nil {
- return nil, err
- }
-
- err = cursor.All(ctx, &res)
- if err != nil {
- return nil, err
- }
- return res, nil
-}
-
-func (c *RoleColl) ListBy(projectName string) ([]*models.Role, error) {
- var res []*models.Role
-
- ctx := context.Background()
- query := bson.M{"namespace": projectName}
-
- cursor, err := c.Collection.Find(ctx, query)
- if err != nil {
- return nil, err
- }
-
- err = cursor.All(ctx, &res)
- if err != nil {
- return nil, err
- }
- return res, nil
-}
-
-func (c *RoleColl) ListBySpaceAndName(projectName string, name string) ([]*models.Role, error) {
- var res []*models.Role
-
- ctx := context.Background()
- query := bson.M{"namespace": projectName, "name": name}
-
- cursor, err := c.Collection.Find(ctx, query)
- if err != nil {
- return nil, err
- }
-
- err = cursor.All(ctx, &res)
- if err != nil {
- return nil, err
- }
- return res, nil
-}
-
-func (c *RoleColl) ListRoleByVerb(projectName, verb string) ([]*models.Role, error) {
- var res []*models.Role
-
- ctx := context.Background()
- query := bson.M{
- "namespace": projectName,
- "rules.verb": verb,
- }
-
- cursor, err := c.Collection.Find(ctx, query)
- if err != nil {
- return nil, err
- }
-
- err = cursor.All(ctx, &res)
- if err != nil {
- return nil, err
- }
- return res, nil
-}
-
-func (c *RoleColl) Create(obj *models.Role) error {
- if obj == nil {
- return fmt.Errorf("nil object")
- }
-
- _, err := c.InsertOne(context.TODO(), obj)
-
- return err
-}
-
-func (c *RoleColl) Delete(name string, projectName string) error {
- query := bson.M{"name": name, "namespace": projectName}
- _, err := c.DeleteOne(context.TODO(), query)
- return err
-}
-
-func (c *RoleColl) DeleteMany(names []string, projectName string) error {
- query := bson.M{"namespace": projectName}
- if len(names) > 0 {
- query["name"] = bson.M{"$in": names}
- }
- _, err := c.Collection.DeleteMany(context.TODO(), query)
- return err
-}
-
-func (c *RoleColl) UpdateRole(obj *models.Role) error {
- // avoid panic issue
- if obj == nil {
- return errors.New("nil Role")
- }
-
- query := bson.M{"name": obj.Name, "namespace": obj.Namespace}
- change := bson.M{"$set": bson.M{
- "rules": obj.Rules,
- }}
- _, err := c.UpdateOne(context.TODO(), query, change)
- return err
-}
-
-func (c *RoleColl) UpdateOrCreate(obj *models.Role) error {
- if obj == nil {
- return fmt.Errorf("nil object")
- }
-
- query := bson.M{"name": obj.Name, "namespace": obj.Namespace}
- opts := options.Replace().SetUpsert(true)
- _, err := c.ReplaceOne(context.TODO(), query, obj, opts)
-
- return err
-}
diff --git a/pkg/microservice/user/core/repository/mongodb/role_binding.go b/pkg/microservice/user/core/repository/mongodb/role_binding.go
deleted file mode 100644
index d14e5d3d3e..0000000000
--- a/pkg/microservice/user/core/repository/mongodb/role_binding.go
+++ /dev/null
@@ -1,315 +0,0 @@
-/*
-Copyright 2023 The KodeRover Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package mongodb
-
-import (
- "context"
- "fmt"
-
- "go.mongodb.org/mongo-driver/bson"
- "go.mongodb.org/mongo-driver/mongo"
- "go.mongodb.org/mongo-driver/mongo/options"
-
- "github.com/koderover/zadig/v2/pkg/config"
- "github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/models"
- "github.com/koderover/zadig/v2/pkg/tool/log"
- mongotool "github.com/koderover/zadig/v2/pkg/tool/mongo"
-)
-
-type ListOptions struct {
- RoleName, RoleNamespace string
-}
-
-type RoleBindingColl struct {
- *mongo.Collection
-
- coll string
-}
-
-func NewRoleBindingColl() *RoleBindingColl {
- name := models.RoleBinding{}.TableName()
- return &RoleBindingColl{
- Collection: mongotool.Database(config.PolicyDatabase()).Collection(name),
- coll: name,
- }
-}
-
-func (c *RoleBindingColl) GetCollectionName() string {
- return c.coll
-}
-
-func (c *RoleBindingColl) List(opts ...*ListOptions) ([]*models.RoleBinding, error) {
- var res []*models.RoleBinding
-
- ctx := context.Background()
- query := bson.M{}
- if len(opts) > 0 {
- opt := opts[0]
- if opt.RoleName != "" {
- query["role_ref.name"] = opt.RoleName
- query["role_ref.namespace"] = opt.RoleNamespace
- }
- }
-
- cursor, err := c.Collection.Find(ctx, query)
- if err != nil {
- return nil, err
- }
-
- err = cursor.All(ctx, &res)
- if err != nil {
- return nil, err
- }
-
- return res, nil
-}
-
-func (c *RoleBindingColl) ListBy(projectName, uid string) ([]*models.RoleBinding, error) {
- var res []*models.RoleBinding
-
- ctx := context.Background()
- query := bson.M{"namespace": projectName}
- if uid != "" {
- query["subjects.uid"] = uid
- query["subjects.kind"] = models.UserKind
- }
-
- cursor, err := c.Collection.Find(ctx, query)
- if err != nil {
- return nil, err
- }
-
- err = cursor.All(ctx, &res)
- if err != nil {
- return nil, err
- }
-
- return res, nil
-}
-
-func (c *RoleBindingColl) ListAllUserRB(projectName string) ([]*models.RoleBinding, error) {
- var res []*models.RoleBinding
-
- ctx := context.Background()
- query := bson.M{}
- if projectName != "" {
- query["namespace"] = projectName
- }
- query["subjects.uid"] = "*"
- query["subjects.kind"] = models.UserKind
-
- cursor, err := c.Collection.Find(ctx, query)
- if err != nil {
- return nil, err
- }
-
- err = cursor.All(ctx, &res)
- if err != nil {
- return nil, err
- }
-
- return res, nil
-}
-
-func (c *RoleBindingColl) ListRoleBindingsByUIDs(uids []string) ([]*models.RoleBinding, error) {
- var res []*models.RoleBinding
-
- ctx := context.Background()
- query := bson.M{}
- if len(uids) > 0 {
- query["subjects.uid"] = bson.M{"$in": uids}
- }
-
- cursor, err := c.Collection.Find(ctx, query)
- if err != nil {
- return nil, err
- }
-
- err = cursor.All(ctx, &res)
- if err != nil {
- return nil, err
- }
-
- return res, nil
-}
-
-func (c *RoleBindingColl) ListSystemRoleBindingsByUIDs(uids []string) ([]*models.RoleBinding, error) {
- var res []*models.RoleBinding
-
- ctx := context.Background()
- query := bson.M{"namespace": "*"}
- if len(uids) > 0 {
- query["subjects.uid"] = bson.M{"$in": uids}
- }
-
- cursor, err := c.Collection.Find(ctx, query)
- if err != nil {
- return nil, err
- }
-
- err = cursor.All(ctx, &res)
- if err != nil {
- return nil, err
- }
-
- return res, nil
-}
-
-func (c *RoleBindingColl) Delete(name string, projectName string) error {
- query := bson.M{"name": name, "namespace": projectName}
- _, err := c.DeleteOne(context.TODO(), query)
- return err
-}
-
-func (c *RoleBindingColl) DeleteMany(names []string, projectName string, userID string) error {
- query := bson.M{}
- if projectName != "" {
- query["namespace"] = projectName
- }
- if len(names) > 0 {
- query["name"] = bson.M{"$in": names}
- }
-
- if userID != "" {
- query["subjects.uid"] = userID
- }
- _, err := c.Collection.DeleteMany(context.TODO(), query)
-
- return err
-}
-
-func (c *RoleBindingColl) DeleteByRole(roleName string, projectName string) error {
- query := bson.M{"role_ref.name": roleName, "role_ref.namespace": projectName}
- // if projectName == "", delete all rolebindings in all namespaces
- if projectName != "" {
- query["namespace"] = projectName
- }
- _, err := c.Collection.DeleteMany(context.TODO(), query)
-
- return err
-}
-
-func (c *RoleBindingColl) DeleteByRoles(roleNames []string, projectName string) error {
- if projectName == "" {
- return fmt.Errorf("projectName is empty")
- }
- if len(roleNames) == 0 {
- return nil
- }
-
- query := bson.M{"role_ref.name": bson.M{"$in": roleNames}, "role_ref.namespace": projectName, "namespace": projectName}
- _, err := c.Collection.DeleteMany(context.TODO(), query)
-
- return err
-}
-
-func (c *RoleBindingColl) Create(obj *models.RoleBinding) error {
- if obj == nil {
- return fmt.Errorf("nil object")
- }
-
- _, err := c.InsertOne(context.TODO(), obj)
-
- return err
-}
-
-func (c *RoleBindingColl) BulkCreate(objs []*models.RoleBinding) error {
- if len(objs) == 0 {
- return nil
- }
-
- var ois []interface{}
- for _, obj := range objs {
- ois = append(ois, obj)
- }
-
- res, err := c.InsertMany(context.TODO(), ois)
- if mongo.IsDuplicateKeyError(err) {
- log.Warnf("Duplicate key found, inserted IDs is %v", res.InsertedIDs)
- return nil
- }
-
- return err
-}
-
-func (c *RoleBindingColl) UpdateOrCreate(obj *models.RoleBinding) error {
- if obj == nil {
- return fmt.Errorf("nil object")
- }
-
- query := bson.M{"name": obj.Name, "namespace": obj.Namespace}
- opts := options.Replace().SetUpsert(true)
- _, err := c.ReplaceOne(context.TODO(), query, obj, opts)
-
- return err
-}
-
-type RoleBinding struct {
- Uid string `json:"uid"`
- Namespace string `json:"namespace"`
-}
-
-type ListRoleBindingsOpt struct {
- RoleBindings []RoleBinding
-}
-
-func (c *RoleBindingColl) ListByRoleBindingOpt(opt ListRoleBindingsOpt) ([]*models.RoleBinding, error) {
- var res []*models.RoleBinding
-
- if len(opt.RoleBindings) == 0 {
- return nil, nil
- }
- condition := bson.A{}
- for _, meta := range opt.RoleBindings {
- condition = append(condition, bson.M{
- "namespace": meta.Namespace,
- "subjects.uid": meta.Uid,
- })
- }
- filter := bson.D{{"$or", condition}}
- cursor, err := c.Collection.Find(context.TODO(), filter)
- if err == mongo.ErrNoDocuments {
- return nil, nil
- }
- if err != nil {
- return nil, err
- }
- if err := cursor.All(context.TODO(), &res); err != nil {
- return nil, err
- }
- return res, nil
-}
-
-func (c *RoleBindingColl) ListUserRoleBinding(uid string) ([]*models.RoleBinding, error) {
- var res []*models.RoleBinding
-
- query := bson.M{
- "subjects.uid": uid,
- }
- cursor, err := c.Collection.Find(context.Background(), query)
- if err == mongo.ErrNoDocuments {
- // it is possible for a user to have no role bindings
- return nil, nil
- }
- if err != nil {
- return nil, err
- }
- if err := cursor.All(context.Background(), &res); err != nil {
- return nil, err
- }
- return res, nil
-}
diff --git a/pkg/microservice/user/core/repository/orm/user.go b/pkg/microservice/user/core/repository/orm/user.go
index b765bdcee4..57f96e06e0 100644
--- a/pkg/microservice/user/core/repository/orm/user.go
+++ b/pkg/microservice/user/core/repository/orm/user.go
@@ -101,7 +101,7 @@ func ListUsersByLoginTime(page int, perPage int, name string, order setting.List
err error
)
- err = db.Select("user.uid, user.name, user.account, user.identity_type, IFNULL(user_login.last_login_time, 0) as last_login_time").
+ err = db.Select("user.uid, user.name, user.account, user.identity_type, user.api_token_enabled, IFNULL(user_login.last_login_time, 0) as last_login_time").
Where("user.name LIKE ?", "%"+name+"%").
Joins("LEFT JOIN user_login on user_login.uid = user.uid").
Order("IFNULL(user_login.last_login_time, 0) " + string(order)).
@@ -117,6 +117,50 @@ func ListUsersByLoginTime(page int, perPage int, name string, order setting.List
return users, nil
}
+// listUIDsByRoles returns distinct user uids that have any of the given role names.
+func listUIDsByRoles(roles []string, db *gorm.DB) ([]string, error) {
+ var uids []string
+ err := db.Table("role_binding").
+ Distinct("role_binding.uid").
+ Joins("INNER JOIN role ON role.id = role_binding.role_id").
+ Where("role.name IN ?", roles).
+ Pluck("role_binding.uid", &uids).Error
+
+ if err != nil && err != gorm.ErrRecordNotFound {
+ return nil, err
+ }
+ return uids, nil
+}
+
+// ListUsersByNameAndRoleWithLoginTime gets a list of users filtered by name and roles,
+// ordered by last_login_time with pagination. It is implemented in two simple steps:
+// 1. Find the uids of users that have any of the given roles (role_binding + role).
+// 2. Query user + user_login for those uids, filter by name, order by last_login_time and paginate.
+func ListUsersByNameAndRoleWithLoginTime(page int, perPage int, name string, roles []string, order setting.ListUserOrder, db *gorm.DB) ([]models.UserWithLoginTime, error) {
+ uids, err := listUIDsByRoles(roles, db)
+ if err != nil {
+ return nil, err
+ }
+ if len(uids) == 0 {
+ return []models.UserWithLoginTime{}, nil
+ }
+
+ var users []models.UserWithLoginTime
+ err = db.Table("user").
+ Select("user.uid, user.name, user.account, user.identity_type, user.api_token_enabled, IFNULL(user_login.last_login_time, 0) AS last_login_time").
+ Joins("LEFT JOIN user_login ON user_login.uid = user.uid").
+ Where("user.uid IN ? AND user.name LIKE ?", uids, "%"+name+"%").
+ Order("last_login_time " + string(order)).
+ Offset((page - 1) * perPage).
+ Limit(perPage).
+ Find(&users).Error
+
+ if err != nil && err != gorm.ErrRecordNotFound {
+ return nil, err
+ }
+ return users, nil
+}
+
// ListUsersByNameAndRole gets a list of users based on paging constraints, the name of the user, and the roles of the user
func ListUsersByNameAndRole(page int, perPage int, name string, roles []string, db *gorm.DB) ([]models.User, error) {
var (
diff --git a/pkg/microservice/user/core/service.go b/pkg/microservice/user/core/service.go
index 219ecbb32b..d8fa1e27af 100644
--- a/pkg/microservice/user/core/service.go
+++ b/pkg/microservice/user/core/service.go
@@ -25,21 +25,16 @@ import (
"time"
_ "github.com/go-sql-driver/mysql"
- "github.com/google/uuid"
- "go.mongodb.org/mongo-driver/mongo"
configbase "github.com/koderover/zadig/v2/pkg/config"
"github.com/koderover/zadig/v2/pkg/microservice/user/config"
"github.com/koderover/zadig/v2/pkg/microservice/user/core/repository"
"github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/models"
- "github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/mongodb"
- "github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/orm"
permissionservice "github.com/koderover/zadig/v2/pkg/microservice/user/core/service/permission"
"github.com/koderover/zadig/v2/pkg/setting"
gormtool "github.com/koderover/zadig/v2/pkg/tool/gorm"
"github.com/koderover/zadig/v2/pkg/tool/log"
mongotool "github.com/koderover/zadig/v2/pkg/tool/mongo"
- "github.com/koderover/zadig/v2/pkg/types"
)
func Start(_ context.Context) {
@@ -99,7 +94,6 @@ func initDatabase() {
}
initializeSystemActions()
- syncUserRoleBinding()
}
func Stop(_ context.Context) {
@@ -216,279 +210,3 @@ func initializeSystemActions() {
}
fmt.Println("system actions initialized...")
}
-
-// syncUserRoleBinding sync all the roles and role binding into mysql after 1.7
-// NOTE:
-// this action will only be performed once regardless of the version, the execution condition is there are no roles in mysql table
-// since this could be a lengthy procedure, the helm installation process need to be modified.
-func syncUserRoleBinding() {
- log.Infof("start sync user role binding")
- // check if the mysql Role exists
- var roleCount int64
- err := repository.DB.Table("role").Count(&roleCount).Error
- if err != nil {
- // if we failed to count the mysql role table, panic and restart.
- log.Panicf("Failed to count roles in the mysql role table to do the data initialization, error: %s", err)
- }
-
- if roleCount > 0 {
- return
- }
-
- tx := repository.DB.Begin()
-
- // if there are no role presented in the roles table, it means that the move all the roles and corresponding role binding into mysql
- allRoles, err := mongodb.NewRoleColl().List()
- log.Infof("find all roles count: %v, err: %+v", len(allRoles), err)
- if err != nil && err != mongo.ErrNoDocuments {
- tx.Rollback()
- log.Panicf("failed to list all roles from previous system, error: %s", err)
- }
-
- if len(allRoles) == 0 {
- // if no roles is in the previous mongodb, it is a fresh installation. We create the default role, which is just system admin, and finish
- adminRole := &models.NewRole{
- Name: "admin",
- Description: "拥有系统中任何操作的权限",
- Type: int64(setting.RoleTypeSystem),
- Namespace: "*",
- }
-
- err := orm.CreateRole(adminRole, tx)
- if err != nil {
- tx.Rollback()
- log.Panicf("failed to initialize admin role for system, tearing down user service...")
- }
- }
-
- roleIDMap := make(map[string]uint)
- actionIDMap := make(map[string]uint)
-
- // initialize user group, for ONCE
- gid, _ := uuid.NewUUID()
- err = orm.CreateUserGroup(&models.UserGroup{
- GroupID: gid.String(),
- GroupName: types.AllUserGroupName,
- Description: "系统中的所有用户",
- Type: int64(setting.RoleTypeSystem),
- }, tx)
-
- if err != nil {
- tx.Rollback()
- log.Panicf("failed to initialize user group data, error: %s", err)
- }
-
- // create the role below and corresponding action binding for each project:
- // 1. project-admin
- // 2. read-only
- // 3. read-project-only
- projectList, err := mongodb.NewProjectColl().List()
- if err != nil && err != mongo.ErrNoDocuments {
- tx.Rollback()
- log.Panicf("Failed to get project list to create project default role, error: %s", err)
- }
-
- log.Infof("projectList count: %v, err: %+v", len(projectList), err)
-
- for _, project := range projectList {
- projectAdminRole := &models.NewRole{
- Name: "project-admin",
- Description: "拥有指定项目中任何操作的权限",
- Type: int64(setting.RoleTypeSystem),
- Namespace: project.ProductName,
- }
- readOnlyRole := &models.NewRole{
- Name: "read-only",
- Description: "拥有指定项目中所有资源的读权限",
- Type: int64(setting.RoleTypeSystem),
- Namespace: project.ProductName,
- }
- readProjectOnlyRole := &models.NewRole{
- Name: "read-project-only",
- Description: "拥有指定项目本身的读权限,无权限查看和操作项目内资源",
- Type: int64(setting.RoleTypeSystem),
- Namespace: project.ProductName,
- }
- err = orm.BulkCreateRole([]*models.NewRole{projectAdminRole, readOnlyRole, readProjectOnlyRole}, tx)
- if err != nil {
- tx.Rollback()
- log.Panicf("failed to create system default role for project: %s, error: %s", project.ProductName, err)
- }
- roleIDMap[fmt.Sprintf("%s+%s", projectAdminRole.Name, projectAdminRole.Namespace)] = projectAdminRole.ID
- roleIDMap[fmt.Sprintf("%s+%s", readOnlyRole.Name, readOnlyRole.Namespace)] = readOnlyRole.ID
- roleIDMap[fmt.Sprintf("%s+%s", readProjectOnlyRole.Name, readProjectOnlyRole.Namespace)] = readProjectOnlyRole.ID
-
- actionIDList := make([]uint, 0)
- for _, verb := range readOnlyAction {
- if _, ok := actionIDMap[verb]; !ok {
- action, err := orm.GetActionByVerb(verb, repository.DB)
- if err != nil {
- tx.Rollback()
- log.Panicf("unexpected database error getting action, err: %s", err)
- }
- // if we found one, save it into the cache
- actionIDMap[verb] = action.ID
- }
-
- // after the cache was done, getting the action id and add it to the list
- actionIDList = append(actionIDList, actionIDMap[verb])
- }
-
- // after all the action counted for, bulk create some role-action bindings
- err = orm.BulkCreateRoleActionBindings(readOnlyRole.ID, actionIDList, tx)
- if err != nil {
- tx.Rollback()
- log.Panicf("failed to create action binding for role %s in namespace %s, error: %s", readOnlyRole.Name, readOnlyRole.Namespace, err)
- }
- }
-
-RoleLoop:
- for _, role := range allRoles {
- // create corresponding mysql role
- mysqlRole := &models.NewRole{
- Name: role.Name,
- Description: role.Desc,
- Namespace: role.Namespace,
- }
-
- if role.Type == setting.ResourceTypeSystem {
- mysqlRole.Type = int64(setting.RoleTypeSystem)
- } else {
- mysqlRole.Type = int64(setting.RoleTypeCustom)
- }
-
- // special case for "project-admin", "contributor", "read-only" and "read-project-only"
- // this will be dealt for each project
- if role.Namespace == "" {
- continue RoleLoop
- } else {
- err = orm.CreateRole(mysqlRole, tx)
- if err != nil {
- tx.Rollback()
- log.Panicf("failed to create role: %s for namespace %s, error: %s", role.Namespace, role.Namespace, err)
- }
- }
-
- // save the role information into the map (mainly for id)
- identity := fmt.Sprintf("%s+%s", mysqlRole.Name, mysqlRole.Namespace)
- roleIDMap[identity] = mysqlRole.ID
-
- // after the role and role binding is created, create its corresponding action binding
- actionIDList := make([]uint, 0)
- for _, resourceAction := range role.Rules {
- VerbLoop:
- for _, verb := range resourceAction.Verbs {
- // admins and project-admins, which only have a verb "*", does not need role
- if verb == "*" {
- continue RoleLoop
- }
-
- // special case for double get_test in quality center and projected testing
- if verb == "get_test" && role.Namespace == "*" {
- verb = "get_test_detail"
- }
-
- if verb == "run_test" && role.Namespace == "*" {
- continue
- }
-
- if _, ok := actionIDMap[verb]; !ok {
- action, err := orm.GetActionByVerb(verb, repository.DB)
- if err != nil {
- tx.Rollback()
- log.Panicf("unexpected database error getting action, err: %s", err)
- }
- // if we found one, save it into the cache
- if action.ID != 0 {
- actionIDMap[verb] = action.ID
- } else {
- log.Errorf("failed to find action: %s", verb)
- // otherwise do nothing
- continue VerbLoop
- }
- }
-
- // after the cache was done, getting the action id and add it to the list
- actionIDList = append(actionIDList, actionIDMap[verb])
- }
- }
- // after all the action counted for, bulk create some role-action bindings
- err = orm.BulkCreateRoleActionBindings(mysqlRole.ID, actionIDList, tx)
- if err != nil {
- tx.Rollback()
- log.Panicf("failed to create action binding for role %s in namespace %s, error: %s", mysqlRole.Name, mysqlRole.Namespace, err)
- }
- }
-
- log.Infof("start handling rolebindings")
- // after syncing all the roles into the database, sync the user-role binding into the mysql table and we are done
- rbList, err := mongodb.NewRoleBindingColl().List()
- if err != nil && err != mongo.ErrNoDocuments {
- tx.Rollback()
- log.Panicf("failed to find role bindings to sync, error: %s", err)
- }
-
- userRBmap := make(map[string][]uint)
- // this is only used to do pre-1.7.0 dara migration, which means there is only one group: all-users
- // which is presented
- groupBindingList := make([]uint, 0)
-
- for _, rb := range rbList {
- // dangerous, but ok for the system
- uid := rb.Subjects[0].UID
- if uid == "*" {
- roleKey := fmt.Sprintf("%s+%s", rb.RoleRef.Name, rb.Namespace)
- if roleID, ok := roleIDMap[roleKey]; ok {
- groupBindingList = append(groupBindingList, roleID)
- } else {
- // if the role is not found, there is a possibility that the role has been deleted, we just print error logs.
- log.Errorf("role: %s in namespace: %s not found, skip creating role binding between groupID: %s and role: %s...", rb.RoleRef.Name, rb.Namespace, gid.String(), rb.RoleRef.Name)
- continue
- }
- }
-
- // the role_ref.namespace is not really reliable, so we will just use namespace, special case list:
- // 1. admin: role_ref.name = admin, role_ref.namespace = *, namespace = *
- // 2. project_admin: role_ref.name = project-admin, role_ref.namespace = "", namespace = project_key
- // 3. read_only: role_ref.name = read-only, role_ref.namespace = "", namespace = project_key
- // 4. read_project_only: role_ref.name = read-project-only, role_ref.namespace = "", namespace = project_key
- roleKey := fmt.Sprintf("%s+%s", rb.RoleRef.Name, rb.Namespace)
- if roleID, ok := roleIDMap[roleKey]; ok {
- userRBmap[uid] = append(userRBmap[uid], roleID)
- } else {
- // if the role is not found, there is a possibility that the role has been deleted, we just print error logs.
- log.Errorf("role: %s in namespace: %s not found, skip creating role binding between uid: %s and role: %s...", rb.RoleRef.Name, rb.Namespace, uid, rb.RoleRef.Name)
- continue
- }
- }
-
- for uid, roleIDList := range userRBmap {
- userInfo, err := orm.GetUserByUid(uid, tx)
- if err != nil {
- tx.Rollback()
- log.Panicf("failed to find user of uid: %s, error: %s", uid, err)
- }
-
- // if no user found, the data is corrupted: there is a role binding without a user, we ignore it
- // someone fucked up and the userinfo might be nil
- if userInfo == nil || len(userInfo.UID) == 0 {
- log.Warnf("No user with id: %s is found, skip creating a binding for it...")
- continue
- }
-
- err = orm.BulkCreateRoleBindingForUser(uid, roleIDList, tx)
- if err != nil {
- tx.Rollback()
- log.Panicf("failed to batch create role bindings for user: %s, error is: %s", uid, err)
- }
- }
-
- err = orm.BulkCreateGroupRoleBindings(gid.String(), groupBindingList, tx)
- if err != nil {
- tx.Rollback()
- log.Panicf("failed to bulk create roles for user group: %s, error is: %s", gid.String(), err)
- }
-
- tx.Commit()
- log.Info("User role and role binding synchronization done successfully!")
-}
diff --git a/pkg/microservice/user/core/service/login/local.go b/pkg/microservice/user/core/service/login/local.go
index a37c1e06c1..b232aef503 100644
--- a/pkg/microservice/user/core/service/login/local.go
+++ b/pkg/microservice/user/core/service/login/local.go
@@ -32,7 +32,7 @@ import (
"github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/orm"
"github.com/koderover/zadig/v2/pkg/microservice/user/core/service/common"
"github.com/koderover/zadig/v2/pkg/setting"
- "github.com/koderover/zadig/v2/pkg/shared/client/plutusvendor"
+ "github.com/koderover/zadig/v2/pkg/shared/client/plutusenterprise"
zadigCache "github.com/koderover/zadig/v2/pkg/tool/cache"
)
@@ -68,13 +68,13 @@ type CheckSignatureRes struct {
}
func CheckSignature(lastLoginTime int64, logger *zap.SugaredLogger) error {
- vendorClient := plutusvendor.New()
- err := vendorClient.Health()
+ enterpriseClient := plutusenterprise.New()
+ err := enterpriseClient.Health()
if err != nil {
return err
}
- status, checkErr := vendorClient.CheckZadigXLicenseStatus()
+ status, checkErr := enterpriseClient.CheckZadigXLicenseStatus()
if checkErr != nil {
return checkErr
}
@@ -84,7 +84,7 @@ func CheckSignature(lastLoginTime int64, logger *zap.SugaredLogger) error {
return err
}
- res, checkErr := vendorClient.CheckSignature(userNum)
+ res, checkErr := enterpriseClient.CheckSignature(userNum)
if checkErr != nil {
return checkErr
}
diff --git a/pkg/microservice/user/core/service/login/mfa.go b/pkg/microservice/user/core/service/login/mfa.go
index 13089141fb..7185b82e24 100644
--- a/pkg/microservice/user/core/service/login/mfa.go
+++ b/pkg/microservice/user/core/service/login/mfa.go
@@ -292,7 +292,7 @@ func SetupMFA(args *MFASetupArgs, logger *zap.SugaredLogger) (*MFASetupResp, err
secret = key.Secret()
challenge.PendingSecret, err = zadigcrypto.AesEncrypt(secret)
if err != nil {
- return nil, fmt.Errorf("failed to encrypt mfa secret")
+ return nil, fmt.Errorf("failed to encrypt mfa secret, err: %s", err)
}
challenge.LastRefreshUnixTs = time.Now().Unix()
@@ -482,52 +482,56 @@ func EnableUserMFA(uid string, args *MFAEnrollArgs, logger *zap.SugaredLogger) (
return EnrollMFA(args, logger)
}
-func DisableUserMFA(uid string, args *MFADisableArgs, logger *zap.SugaredLogger) error {
+type MFADisableResponse struct {
+ Token string `json:"token"`
+}
+
+func DisableUserMFA(uid string, args *MFADisableArgs, logger *zap.SugaredLogger) (*MFADisableResponse, error) {
if uid == "" {
- return fmt.Errorf("uid is empty")
+ return nil, fmt.Errorf("uid is empty")
}
if args == nil {
- return fmt.Errorf("disable mfa args are required")
+ return nil, fmt.Errorf("disable mfa args are required")
}
if args.OTPCode == "" && args.RecoveryCode == "" {
- return fmt.Errorf("otp code or recovery code is required")
+ return nil, fmt.Errorf("otp code or recovery code is required")
}
settings, err := common.GetSystemSecuritySettings(logger)
if err != nil {
- return err
+ return nil, err
}
if settings.MFAEnabled {
- return fmt.Errorf("mfa is enforced by administrator")
+ return nil, fmt.Errorf("mfa is enforced by administrator")
}
userMFA, err := orm.GetUserMFA(uid, repository.DB)
if err != nil {
- return err
+ return nil, err
}
if userMFA == nil || !userMFA.Enabled {
- return fmt.Errorf("mfa not enabled")
+ return nil, fmt.Errorf("mfa not enabled")
}
valid := false
if args.OTPCode != "" {
secret, err := zadigcrypto.AesDecrypt(userMFA.SecretCipher)
if err != nil {
- return fmt.Errorf("failed to decode mfa secret")
+ return nil, fmt.Errorf("failed to decode mfa secret")
}
valid = validateTOTPCode(secret, args.OTPCode)
} else {
valid, err = consumeRecoveryCode(userMFA, args.RecoveryCode, logger)
if err != nil {
- return err
+ return nil, err
}
}
if !valid {
- return fmt.Errorf("invalid mfa verification code")
+ return nil, fmt.Errorf("invalid mfa verification code")
}
if err := orm.DeleteUserMFA(uid, repository.DB); err != nil {
- return err
+ return nil, err
}
if cacheErr := setUserMFAEnabledCache(uid, false); cacheErr != nil && logger != nil {
logger.Warnf("failed to sync user mfa cache during disable, uid: %s, err: %v", uid, cacheErr)
@@ -537,7 +541,13 @@ func DisableUserMFA(uid string, args *MFADisableArgs, logger *zap.SugaredLogger)
logger.Warnf("failed to clear user token cache during mfa disable, uid: %s, err: %v", uid, err)
}
}
- return nil
+
+ user, err := issueLoginTokenByUID(uid, false, logger)
+ if err != nil {
+ return nil, err
+ }
+
+ return &MFADisableResponse{Token: user.Token}, nil
}
func RegenerateRecoveryCodes(uid string, args *MFARecoveryCodesArgs, logger *zap.SugaredLogger) (*MFARecoveryCodesResp, error) {
diff --git a/pkg/microservice/user/core/service/permission/authn.go b/pkg/microservice/user/core/service/permission/authn.go
index bdef3989a0..9dbf41143c 100644
--- a/pkg/microservice/user/core/service/permission/authn.go
+++ b/pkg/microservice/user/core/service/permission/authn.go
@@ -83,11 +83,11 @@ func IsPublicURL(reqPath, method string) bool {
return true
}
- if realPath == "/api/plutus/license" && (method == http.MethodPost || method == http.MethodGet) {
+ if realPath == "/api/plutus-enterprise/license" && (method == http.MethodPost || method == http.MethodGet) {
return true
}
- if realPath == "/api/plutus/organization" && method == http.MethodGet {
+ if realPath == "/api/plutus-enterprise/organization" && method == http.MethodGet {
return true
}
diff --git a/pkg/microservice/user/core/service/permission/authz.go b/pkg/microservice/user/core/service/permission/authz.go
index c24fb8142b..e78128da4b 100644
--- a/pkg/microservice/user/core/service/permission/authz.go
+++ b/pkg/microservice/user/core/service/permission/authz.go
@@ -19,6 +19,7 @@ package permission
import (
"database/sql"
"fmt"
+ "slices"
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/util/sets"
@@ -32,6 +33,7 @@ import (
"github.com/koderover/zadig/v2/pkg/types"
)
+// GetUserAuthInfo get user auth info
func GetUserAuthInfo(uid string, logger *zap.SugaredLogger) (*AuthorizedResources, error) {
// system calls
if uid == "" {
@@ -67,6 +69,7 @@ func GetUserAuthInfo(uid string, logger *zap.SugaredLogger) (*AuthorizedResource
systemActions := generateDefaultSystemActions()
// we generate a map of namespaced(project) permission
projectActionMap := make(map[string]*ProjectActions)
+ globalReadVerbSet := sets.New[string]()
roles, err := ListRoleByUID(uid)
if err != nil {
@@ -80,6 +83,14 @@ func GetUserAuthInfo(uid string, logger *zap.SugaredLogger) (*AuthorizedResource
projectActionMap[role.Namespace] = generateDefaultProjectActions()
}
}
+ if role.Namespace == GeneralNamespace && role.GlobalReadOnly {
+ for _, verb := range readOnlyAction {
+ globalReadVerbSet.Insert(verb)
+ }
+ for _, verb := range globalReadOnlySystemAction {
+ modifySystemAction(systemActions, verb)
+ }
+ }
// project admin does not have any bindings, it is special
if role.Name == ProjectAdminRole {
@@ -97,7 +108,11 @@ func GetUserAuthInfo(uid string, logger *zap.SugaredLogger) (*AuthorizedResource
for _, action := range actions {
switch role.Namespace {
case GeneralNamespace:
+ // inject system actions for global read-only role
modifySystemAction(systemActions, action)
+ if role.GlobalReadOnly && isReadOnlyActionVerb(action) {
+ globalReadVerbSet.Insert(action)
+ }
default:
modifyUserProjectAuth(projectActionMap[role.Namespace], action)
}
@@ -124,6 +139,17 @@ func GetUserAuthInfo(uid string, logger *zap.SugaredLogger) (*AuthorizedResource
projectActionMap[role.Namespace] = generateDefaultProjectActions()
}
}
+ // global read-only role has special permission
+ if role.Namespace == GeneralNamespace && role.GlobalReadOnly {
+ for _, verb := range readOnlyAction {
+ globalReadVerbSet.Insert(verb)
+ }
+
+ // 开启 SystemAction read权限 for global read-only role
+ for _, verb := range globalReadOnlySystemAction {
+ modifySystemAction(systemActions, verb)
+ }
+ }
if role.Name == ProjectAdminRole {
projectActionMap[role.Namespace].IsProjectAdmin = true
@@ -138,15 +164,27 @@ func GetUserAuthInfo(uid string, logger *zap.SugaredLogger) (*AuthorizedResource
}
for _, action := range actions {
+ if role.Namespace == GeneralNamespace && role.GlobalReadOnly && !isGlobalReadOnlyRoleActionVerb(action) {
+ continue
+ }
switch role.Namespace {
case GeneralNamespace:
+ // inject system actions for global read-only role
modifySystemAction(systemActions, action)
+ if role.GlobalReadOnly && isReadOnlyActionVerb(action) {
+ globalReadVerbSet.Insert(action)
+ }
default:
modifyUserProjectAuth(projectActionMap[role.Namespace], action)
}
}
}
+ //grant global read permission to all projects.
+ if err := grantGlobalReadAuthToAllProjects(projectActionMap, globalReadVerbSet.UnsortedList()); err != nil {
+ return nil, err
+ }
+
projectInfo := make(map[string]ProjectActions)
for proj, actions := range projectActionMap {
projectInfo[proj] = *actions
@@ -221,10 +259,14 @@ func CheckPermissionGivenByCollaborationMode(uid, projectKey, resource, action s
return
}
+// ListAuthorizedProject list authorized projects for a user
+// if user is system admin, return all projects
+// if user is not system admin, return projects that user is in
func ListAuthorizedProject(uid string, logger *zap.SugaredLogger) ([]string, error) {
tx := repository.DB.Begin(&sql.TxOptions{ReadOnly: true})
- respSet := sets.NewString()
+ respSet := sets.New[string]()
+ projectCache := &allProjectCache{}
isSystemAdmin, err := checkUserIsSystemAdmin(uid, tx)
if err != nil {
@@ -234,17 +276,13 @@ func ListAuthorizedProject(uid string, logger *zap.SugaredLogger) ([]string, err
}
if isSystemAdmin {
- projectList, err := mongodb.NewProjectColl().List()
- if err != nil {
+ if err := projectCache.insertAllProjects(respSet); err != nil {
tx.Rollback()
logger.Errorf("failed to list project for project admin to return authorized projects, error: %s", err)
return nil, fmt.Errorf("failed to list project for project admin to return authorized projects, error: %s", err)
}
- for _, project := range projectList {
- respSet.Insert(project.ProductName)
- }
tx.Commit()
- return respSet.List(), nil
+ return respSet.UnsortedList(), nil
}
groupIDList := make([]string, 0)
@@ -277,9 +315,17 @@ func ListAuthorizedProject(uid string, logger *zap.SugaredLogger) ([]string, err
}
for _, role := range roles {
- if role.Namespace != GeneralNamespace {
- respSet.Insert(role.Namespace)
+ if role.Namespace == GeneralNamespace {
+ if role.GlobalReadOnly {
+ if err := projectCache.insertAllProjects(respSet); err != nil {
+ tx.Rollback()
+ logger.Errorf("failed to list all projects for global read role %s, error: %s", role.Name, err)
+ return nil, err
+ }
+ }
+ continue
}
+ respSet.Insert(role.Namespace)
}
groupRoles, err := orm.ListRoleByGroupIDs(groupIDList, tx)
@@ -290,9 +336,17 @@ func ListAuthorizedProject(uid string, logger *zap.SugaredLogger) ([]string, err
}
for _, role := range groupRoles {
- if role.Namespace != GeneralNamespace {
- respSet.Insert(role.Namespace)
+ if role.Namespace == GeneralNamespace {
+ if role.GlobalReadOnly {
+ if err := projectCache.insertAllProjects(respSet); err != nil {
+ tx.Rollback()
+ logger.Errorf("failed to list all projects for global read role %s, error: %s", role.Name, err)
+ return nil, err
+ }
+ }
+ continue
}
+ respSet.Insert(role.Namespace)
}
// TODO: add user group support for collaboration mode
@@ -302,7 +356,7 @@ func ListAuthorizedProject(uid string, logger *zap.SugaredLogger) ([]string, err
// given by the role.
tx.Commit()
logger.Warnf("failed to find user collaboration mode, error: %s", err)
- return respSet.List(), nil
+ return respSet.UnsortedList(), nil
}
// if user have collaboration mode, they must have access to this project.
@@ -311,11 +365,12 @@ func ListAuthorizedProject(uid string, logger *zap.SugaredLogger) ([]string, err
}
tx.Commit()
- return respSet.List(), nil
+ return respSet.UnsortedList(), nil
}
func ListAuthorizedProjectByVerb(uid, resource, verb string, logger *zap.SugaredLogger) ([]string, error) {
- respSet := sets.NewString()
+ respSet := sets.New[string]()
+ projectCache := &allProjectCache{}
tx := repository.DB.Begin(&sql.TxOptions{ReadOnly: true})
@@ -327,17 +382,13 @@ func ListAuthorizedProjectByVerb(uid, resource, verb string, logger *zap.Sugared
}
if isSystemAdmin {
- projectList, err := mongodb.NewProjectColl().List()
- if err != nil {
+ if err := projectCache.insertAllProjects(respSet); err != nil {
tx.Rollback()
logger.Errorf("failed to list project for project admin to return authorized projects, error: %s", err)
return nil, fmt.Errorf("failed to list project for project admin to return authorized projects, error: %s", err)
}
- for _, project := range projectList {
- respSet.Insert(project.ProductName)
- }
tx.Commit()
- return respSet.List(), nil
+ return respSet.UnsortedList(), nil
}
groupIDList := make([]string, 0)
@@ -375,6 +426,28 @@ func ListAuthorizedProjectByVerb(uid, resource, verb string, logger *zap.Sugared
}
}
+ // if user has global read only role, we must return all projects.
+ if isReadOnlyActionVerb(verb) {
+ systemRoles, err := orm.ListRoleByUID(uid, tx)
+ if err != nil {
+ tx.Rollback()
+ logger.Errorf("failed to list roles for uid: %s, error: %s", uid, err)
+ return nil, fmt.Errorf("failed to list roles for uid: %s, error: %s", uid, err)
+ }
+
+ for _, role := range systemRoles {
+ if role.Namespace == GeneralNamespace && role.GlobalReadOnly {
+ if err := projectCache.insertAllProjects(respSet); err != nil {
+ tx.Rollback()
+ logger.Errorf("failed to list all projects for global read role %s, error: %s", role.Name, err)
+ return nil, err
+ }
+ break
+ }
+ }
+ }
+
+ // if user has project admin role, we must return all projects.
adminRoles, err := orm.ListProjectAdminRoleByUID(uid, tx)
if err != nil {
tx.Rollback()
@@ -395,12 +468,33 @@ func ListAuthorizedProjectByVerb(uid, resource, verb string, logger *zap.Sugared
return nil, fmt.Errorf("failed to list roles for groupid: %+v, error: %s", groupIDList, err)
}
+ // if user has global read only role, we must return all projects.
for _, role := range groupRoles {
if role.Namespace != GeneralNamespace {
respSet.Insert(role.Namespace)
}
}
+ //
+ if isReadOnlyActionVerb(verb) {
+ systemRoles, err := orm.ListRoleByGroupIDs(groupIDList, tx)
+ if err != nil {
+ tx.Rollback()
+ logger.Errorf("failed to list roles for groupid: %+v, error: %s", groupIDList, err)
+ return nil, fmt.Errorf("failed to list roles for groupid: %+v, error: %s", groupIDList, err)
+ }
+ for _, role := range systemRoles {
+ if role.Namespace == GeneralNamespace && role.GlobalReadOnly {
+ if err := projectCache.insertAllProjects(respSet); err != nil {
+ tx.Rollback()
+ logger.Errorf("failed to list all projects for global read role %s, error: %s", role.Name, err)
+ return nil, err
+ }
+ break
+ }
+ }
+ }
+
groupAdminRoles, err := orm.ListProjectAdminRoleByGroupIDs(groupIDList, tx)
if err != nil {
tx.Rollback()
@@ -419,7 +513,7 @@ func ListAuthorizedProjectByVerb(uid, resource, verb string, logger *zap.Sugared
}
tx.Commit()
- return respSet.List(), nil
+ return respSet.UnsortedList(), nil
}
// ListAuthorizedWorkflow lists all workflows authorized by collaboration mode
@@ -576,6 +670,71 @@ func generateAdminRoleResource() *AuthorizedResources {
}
}
+// isReadOnlyActionVerb check if the action is a read-only action.
+func isReadOnlyActionVerb(action string) bool {
+ return slices.Contains(readOnlyAction, action)
+}
+
+// isGlobalReadOnlySystemActionVerb check if the action is a global read-only system action.
+func isGlobalReadOnlySystemActionVerb(action string) bool {
+ return slices.Contains(globalReadOnlySystemAction, action)
+}
+
+// project action 和 system action 的交集
+func isGlobalReadOnlyRoleActionVerb(action string) bool {
+ return isReadOnlyActionVerb(action) || isGlobalReadOnlySystemActionVerb(action)
+}
+
+// grantGlobalReadAuthToAllProjects grant global read permission to all projects.
+func grantGlobalReadAuthToAllProjects(projectActionMap map[string]*ProjectActions, verbs []string) error {
+ if len(verbs) == 0 {
+ return nil
+ }
+ projectList, err := mongodb.NewProjectColl().List()
+ if err != nil {
+ return fmt.Errorf("failed to list projects for global read permission, error: %s", err)
+ }
+
+ // get project list
+ for _, project := range projectList {
+ if _, ok := projectActionMap[project.ProductName]; !ok {
+ projectActionMap[project.ProductName] = generateDefaultProjectActions()
+ }
+ // 对用户所有的project action开启
+ for _, verb := range verbs {
+ modifyUserProjectAuth(projectActionMap[project.ProductName], verb)
+ }
+ }
+ return nil
+}
+
+// allProjectCache caches all project names (lazy-loaded)
+type allProjectCache struct {
+ loaded bool // whether data has been loaded from DB
+ projectNames []string // cached project names
+}
+
+// insertAllProjects loads all projects once and inserts them into respSet
+func (c *allProjectCache) insertAllProjects(respSet sets.Set[string]) error {
+ // load from DB only on first call
+ if !c.loaded {
+ projectList, err := mongodb.NewProjectColl().List()
+ if err != nil {
+ return err
+ }
+ for _, project := range projectList {
+ c.projectNames = append(c.projectNames, project.ProductName)
+ }
+ c.loaded = true
+ }
+
+ // reuse cache
+ for _, projectName := range c.projectNames {
+ respSet.Insert(projectName)
+ }
+ return nil
+}
+
// generateDefaultProjectActions generate an ProjectActions without any authorization info.
func generateDefaultProjectActions() *ProjectActions {
return &ProjectActions{
@@ -591,6 +750,7 @@ func generateDefaultProjectActions() *ProjectActions {
View: false,
Create: false,
EditConfig: false,
+ Scale: false,
ManagePods: false,
Restart: false,
Rollback: false,
@@ -601,6 +761,7 @@ func generateDefaultProjectActions() *ProjectActions {
View: false,
Create: false,
EditConfig: false,
+ Scale: false,
ManagePods: false,
Restart: false,
Rollback: false,
@@ -806,6 +967,8 @@ func modifyUserProjectAuth(userAuthInfo *ProjectActions, verb string) {
userAuthInfo.Env.Create = true
case VerbConfigEnvironment:
userAuthInfo.Env.EditConfig = true
+ case VerbScaleEnvironment:
+ userAuthInfo.Env.Scale = true
case VerbManageEnvironment:
userAuthInfo.Env.ManagePods = true
case VerbRestartEnvironment:
@@ -824,6 +987,8 @@ func modifyUserProjectAuth(userAuthInfo *ProjectActions, verb string) {
userAuthInfo.ProductionEnv.Create = true
case VerbConfigProductionEnv:
userAuthInfo.ProductionEnv.EditConfig = true
+ case VerbScaleProductionEnv:
+ userAuthInfo.ProductionEnv.Scale = true
case VerbEditProductionEnv:
userAuthInfo.ProductionEnv.ManagePods = true
case VerbRestartProductionEnv:
@@ -907,6 +1072,12 @@ func modifySystemAction(systemActions *SystemActions, verb string) {
systemActions.ReleasePlan.EditConfig = true
case VerbGetBusinessDirectory:
systemActions.BusinessDirectory.View = true
+ case VerbCreateBusinessDirectory:
+ systemActions.BusinessDirectory.Create = true
+ case VerbEditBusinessDirectory:
+ systemActions.BusinessDirectory.Edit = true
+ case VerbDeleteBusinessDirectory:
+ systemActions.BusinessDirectory.Delete = true
case VerbGetClusterManagement:
systemActions.ClusterManagement.View = true
case VerbCreateClusterManagement:
diff --git a/pkg/microservice/user/core/service/permission/internal.go b/pkg/microservice/user/core/service/permission/internal.go
index 9fe721164c..6d0c20d8cf 100644
--- a/pkg/microservice/user/core/service/permission/internal.go
+++ b/pkg/microservice/user/core/service/permission/internal.go
@@ -44,6 +44,22 @@ var readOnlyAction = []string{
VerbGetSprint,
}
+// globalReadOnlySystemAction defines system-level read-only actions granted by global-read-only role.
+// It intentionally excludes system settings and enterprise management actions.
+var globalReadOnlySystemAction = []string{
+ VerbGetTemplate,
+ VerbViewTestCenter,
+ VerbViewReleaseCenter,
+ VerbDeliveryCenterGetVersions,
+ VerbDeliveryCenterGetArtifact,
+ VerbGetDataCenterOverview,
+ VerbGetDataCenterInsight,
+ VerbGetBusinessDirectory,
+ VerbGetReleasePlan,
+ VerbGetRegistryManagement,
+ VerbGetS3StorageManagement,
+}
+
func InitializeProjectAuthorization(namespace string, isPublic bool, admins []string, log *zap.SugaredLogger) error {
tx := repository.DB.Begin()
// First, create default roles
diff --git a/pkg/microservice/user/core/service/permission/permission.go b/pkg/microservice/user/core/service/permission/permission.go
index 00f03f12b5..d82dd2d2fc 100644
--- a/pkg/microservice/user/core/service/permission/permission.go
+++ b/pkg/microservice/user/core/service/permission/permission.go
@@ -87,6 +87,11 @@ func GetUserPermissionByProject(uid, projectName string, log *zap.SugaredLogger)
}
for _, role := range roles {
+ if role.Namespace == GeneralNamespace && role.GlobalReadOnly {
+ for _, action := range readOnlyAction {
+ projectVerbSet.Insert(action)
+ }
+ }
if role.Namespace != projectName {
continue
}
@@ -128,6 +133,11 @@ func GetUserPermissionByProject(uid, projectName string, log *zap.SugaredLogger)
}
for _, role := range groupRoleMap {
+ if role.Namespace == GeneralNamespace && role.GlobalReadOnly {
+ for _, action := range readOnlyAction {
+ projectVerbSet.Insert(action)
+ }
+ }
if role.Namespace != projectName {
continue
}
@@ -266,7 +276,13 @@ func GetUserRules(uid string, log *zap.SugaredLogger) (*GetUserRulesResp, error)
switch role.Namespace {
case GeneralNamespace:
+ if role.GlobalReadOnly {
+ systemVerbs = append(systemVerbs, globalReadOnlySystemAction...)
+ }
for _, action := range actions {
+ if role.GlobalReadOnly && !isGlobalReadOnlyRoleActionVerb(action) {
+ continue
+ }
systemVerbs = append(systemVerbs, action)
}
}
@@ -311,7 +327,13 @@ func GetUserRules(uid string, log *zap.SugaredLogger) (*GetUserRulesResp, error)
switch role.Namespace {
case GeneralNamespace:
+ if role.GlobalReadOnly {
+ systemVerbs = append(systemVerbs, globalReadOnlySystemAction...)
+ }
for _, action := range actions {
+ if role.GlobalReadOnly && !isGlobalReadOnlyRoleActionVerb(action) {
+ continue
+ }
systemVerbs = append(systemVerbs, action)
}
}
diff --git a/pkg/microservice/user/core/service/permission/role.go b/pkg/microservice/user/core/service/permission/role.go
index 0232277f96..b1a619efe6 100644
--- a/pkg/microservice/user/core/service/permission/role.go
+++ b/pkg/microservice/user/core/service/permission/role.go
@@ -41,11 +41,11 @@ const (
RoleActionKeyFormat = "role_action_%d"
UIDRoleKeyFormat = "uid_role_%s"
- UIDRoleDataFormat = "%d++%s++%s"
+ UIDRoleDataFormat = "%d++%s++%s++%t"
UIDRoleLock = "lock_uid_role_%s"
GIDRoleKeyFormat = "gid_role_%s"
- GIDRoleDataFormat = "%d++%s++%s"
+ GIDRoleDataFormat = "%d++%s++%s++%t"
GIDRoleLock = "lock_gid_role_%s"
)
@@ -55,11 +55,12 @@ const (
var ActionMap = make(map[string]uint)
type CreateRoleReq struct {
- Name string `json:"name"`
- Actions []string `json:"actions"`
- Namespace string `json:"namespace"`
- Desc string `json:"desc,omitempty"`
- Type string `json:"type,omitempty"`
+ Name string `json:"name"`
+ Actions []string `json:"actions"`
+ Namespace string `json:"namespace"`
+ Desc string `json:"desc,omitempty"`
+ Type string `json:"type,omitempty"`
+ GlobalReadOnly bool `json:"global_read_only,omitempty"`
}
// ListRoleByUID lists all roles by uid with cache.
@@ -78,7 +79,7 @@ func ListRoleByUID(uid string) ([]*types.Role, error) {
// if we got the data from cache, simply return it\
for _, roleInfo := range resp {
roleInfos := strings.Split(roleInfo, "++")
- if len(roleInfos) != 3 {
+ if len(roleInfos) != 3 && len(roleInfos) != 4 {
// if the data is corrupted, stop using it.
useCache = false
break
@@ -92,9 +93,10 @@ func ListRoleByUID(uid string) ([]*types.Role, error) {
}
response = append(response, &types.Role{
- ID: uint(roleID),
- Namespace: roleInfos[1],
- Name: roleInfos[2],
+ ID: uint(roleID),
+ Namespace: roleInfos[1],
+ Name: roleInfos[2],
+ GlobalReadOnly: len(roleInfos) == 4 && roleInfos[3] == "true",
})
}
} else {
@@ -119,11 +121,12 @@ func ListRoleByUID(uid string) ([]*types.Role, error) {
for _, role := range roles {
response = append(response, &types.Role{
- ID: role.ID,
- Namespace: role.Namespace,
- Name: role.Name,
+ ID: role.ID,
+ Namespace: role.Namespace,
+ Name: role.Name,
+ GlobalReadOnly: role.GlobalReadOnly,
})
- cacheData = append(cacheData, fmt.Sprintf(UIDRoleDataFormat, role.ID, role.Namespace, role.Name))
+ cacheData = append(cacheData, fmt.Sprintf(UIDRoleDataFormat, role.ID, role.Namespace, role.Name, role.GlobalReadOnly))
}
err = roleCache.Delete(uidRoleKey)
@@ -156,7 +159,7 @@ func ListRoleByGID(gid string) ([]*types.Role, error) {
// if we got the data from cache, simply return it\
for _, roleInfo := range resp {
roleInfos := strings.Split(roleInfo, "++")
- if len(roleInfos) != 3 {
+ if len(roleInfos) != 3 && len(roleInfos) != 4 {
// if the data is corrupted, stop using it.
useCache = false
break
@@ -170,9 +173,10 @@ func ListRoleByGID(gid string) ([]*types.Role, error) {
}
response = append(response, &types.Role{
- ID: uint(roleID),
- Namespace: roleInfos[1],
- Name: roleInfos[2],
+ ID: uint(roleID),
+ Namespace: roleInfos[1],
+ Name: roleInfos[2],
+ GlobalReadOnly: len(roleInfos) == 4 && roleInfos[3] == "true",
})
}
} else {
@@ -197,11 +201,12 @@ func ListRoleByGID(gid string) ([]*types.Role, error) {
for _, role := range roles {
response = append(response, &types.Role{
- ID: role.ID,
- Namespace: role.Namespace,
- Name: role.Name,
+ ID: role.ID,
+ Namespace: role.Namespace,
+ Name: role.Name,
+ GlobalReadOnly: role.GlobalReadOnly,
})
- cacheData = append(cacheData, fmt.Sprintf(GIDRoleDataFormat, role.ID, role.Namespace, role.Name))
+ cacheData = append(cacheData, fmt.Sprintf(GIDRoleDataFormat, role.ID, role.Namespace, role.Name, role.GlobalReadOnly))
}
err = roleCache.Delete(gidRoleKey)
@@ -266,12 +271,14 @@ func ListActionByRole(roleID uint) ([]string, error) {
}
func CreateRole(ns string, req *CreateRoleReq, log *zap.SugaredLogger) error {
+
tx := repository.DB.Begin()
role := &models.NewRole{
- Name: req.Name,
- Description: req.Desc,
- Namespace: ns,
+ Name: req.Name,
+ Description: req.Desc,
+ Namespace: ns,
+ GlobalReadOnly: req.GlobalReadOnly,
}
if req.Type == string(setting.ResourceTypeSystem) {
@@ -340,6 +347,7 @@ func CreateRole(ns string, req *CreateRoleReq, log *zap.SugaredLogger) error {
// UpdateRole updates the role and its action binding.
func UpdateRole(ns string, req *CreateRoleReq, log *zap.SugaredLogger) error {
+
tx := repository.DB.Begin()
// Doing a tricky thing here: removing the whole role-action binding, then re-adding them.
diff --git a/pkg/microservice/user/core/service/permission/role_template.go b/pkg/microservice/user/core/service/permission/role_template.go
index e7488dc4f8..62575d5177 100644
--- a/pkg/microservice/user/core/service/permission/role_template.go
+++ b/pkg/microservice/user/core/service/permission/role_template.go
@@ -19,6 +19,7 @@ package permission
import (
"errors"
"fmt"
+
"github.com/koderover/zadig/v2/pkg/config"
"github.com/koderover/zadig/v2/pkg/microservice/user/core/repository"
"github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/models"
@@ -52,6 +53,7 @@ func ListRoleTemplates(log *zap.SugaredLogger) ([]*types.RoleTemplate, error) {
}
func CreateRoleTemplate(req *CreateRoleReq, log *zap.SugaredLogger) error {
+
tx := repository.DB.Begin()
roleTemplate := &models.RoleTemplate{
@@ -95,6 +97,7 @@ func CreateRoleTemplate(req *CreateRoleReq, log *zap.SugaredLogger) error {
}
func UpdateRoleTemplate(req *CreateRoleReq, log *zap.SugaredLogger) error {
+
tx := repository.DB.Begin()
roleTemplateInfo, err := orm.GetRoleTemplate(req.Name, repository.DB)
diff --git a/pkg/microservice/user/core/service/permission/types.go b/pkg/microservice/user/core/service/permission/types.go
index 94bdc3714f..ce4c331355 100644
--- a/pkg/microservice/user/core/service/permission/types.go
+++ b/pkg/microservice/user/core/service/permission/types.go
@@ -63,6 +63,7 @@ const (
VerbGetEnvironment = "get_environment"
VerbCreateEnvironment = "create_environment"
VerbConfigEnvironment = "config_environment"
+ VerbScaleEnvironment = "scale_environment"
VerbManageEnvironment = "manage_environment"
VerbRestartEnvironment = "restart_environment"
VerbRollbackEnvironment = "rollback_environment"
@@ -73,6 +74,7 @@ const (
VerbGetProductionEnv = "get_production_environment"
VerbCreateProductionEnv = "create_production_environment"
VerbConfigProductionEnv = "config_production_environment"
+ VerbScaleProductionEnv = "scale_production_environment"
VerbEditProductionEnv = "edit_production_environment"
VerbRestartProductionEnv = "restart_production_environment"
VerbRollbackProductionEnv = "rollback_production_environment"
@@ -152,7 +154,10 @@ const (
VerbEditHelmRepoManagement = "edit_helmrepo_management"
VerbDeleteHelmRepoManagement = "delete_helmrepo_management"
// business directory
- VerbGetBusinessDirectory = "get_business_directory"
+ VerbGetBusinessDirectory = "get_business_directory"
+ VerbCreateBusinessDirectory = "create_business_directory"
+ VerbEditBusinessDirectory = "edit_business_directory"
+ VerbDeleteBusinessDirectory = "delete_business_directory"
// dbinstance management
VerbGetDBInstanceManagement = "get_dbinstance_management"
VerbCreateDBInstanceManagement = "create_dbinstance_management"
@@ -219,6 +224,8 @@ type EnvActions struct {
Create bool
// 配置
EditConfig bool
+ // 调整副本
+ Scale bool
// 管理服务实例
ManagePods bool
Restart bool
@@ -234,6 +241,8 @@ type ProductionEnvActions struct {
Create bool
// 配置
EditConfig bool
+ // 调整副本
+ Scale bool
// 管理服务实例
ManagePods bool
Restart bool
@@ -345,7 +354,10 @@ type ReleasePlanActions struct {
}
type BusinessDirectoryActions struct {
- View bool
+ View bool
+ Create bool
+ Edit bool
+ Delete bool
}
type ClusterManagementActions struct {
diff --git a/pkg/microservice/user/core/service/permission/user.go b/pkg/microservice/user/core/service/permission/user.go
index c4aaa84c76..ed7b50e29e 100644
--- a/pkg/microservice/user/core/service/permission/user.go
+++ b/pkg/microservice/user/core/service/permission/user.go
@@ -22,6 +22,7 @@ import (
"fmt"
"net/url"
"regexp"
+ "strings"
"time"
"github.com/dexidp/dex/connector/ldap"
@@ -42,7 +43,7 @@ import (
"github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/orm"
"github.com/koderover/zadig/v2/pkg/microservice/user/core/service/login"
"github.com/koderover/zadig/v2/pkg/setting"
- "github.com/koderover/zadig/v2/pkg/shared/client/plutusvendor"
+ "github.com/koderover/zadig/v2/pkg/shared/client/plutusenterprise"
"github.com/koderover/zadig/v2/pkg/shared/client/systemconfig"
zadigCache "github.com/koderover/zadig/v2/pkg/tool/cache"
e "github.com/koderover/zadig/v2/pkg/tool/errors"
@@ -319,7 +320,14 @@ func GenerateAPIToken(uid string, logger *zap.SugaredLogger) (string, error) {
return "", e.ErrForbidden
}
- token, err := generatePermanentAPIToken(user)
+ userMFA, err := orm.GetUserMFA(uid, repository.DB)
+ if err != nil {
+ logger.Errorf("GenerateAPIToken GetUserMFA:%s error, error msg:%s", uid, err.Error())
+ return "", err
+ }
+ mfaEnabled := userMFA != nil && userMFA.Enabled
+
+ token, err := generatePermanentAPIToken(user, mfaEnabled)
if err != nil {
logger.Errorf("GenerateAPIToken create token for user:%s error, error msg:%s", user.Account, err.Error())
return "", err
@@ -368,12 +376,13 @@ func userCanUseAPIToken(user *models.User) (bool, error) {
return isSystemAdmin || user.APITokenEnabled, nil
}
-func generatePermanentAPIToken(user *models.User) (string, error) {
+func generatePermanentAPIToken(user *models.User, mfaVerified bool) (string, error) {
return login.CreateToken(&login.Claims{
Name: user.Name,
UID: user.UID,
Email: user.Email,
PreferredUsername: user.Account,
+ MFAVerified: mfaVerified,
StandardClaims: jwt.StandardClaims{
Audience: setting.ProductName,
// 24*365*100=876000
@@ -462,6 +471,14 @@ func SearchUserByAccount(args *QueryArgs, logger *zap.SugaredLogger) (*types.Use
}
func SearchUsers(args *QueryArgs, logger *zap.SugaredLogger) (*types.UsersResp, error) {
+ // normalize args.Order to avoid SQL injection since it is concatenated into the ORDER BY clause.
+ switch strings.ToUpper(string(args.Order)) {
+ case string(setting.ListUserOrderAsc):
+ args.Order = setting.ListUserOrderAsc
+ default:
+ args.Order = setting.ListUserOrderDesc
+ }
+
var count int64
var err error
if len(args.Roles) == 0 {
@@ -498,12 +515,20 @@ func SearchUsers(args *QueryArgs, logger *zap.SugaredLogger) (*types.UsersResp,
return nil, err
}
} else {
- us, err = orm.ListUsersByNameAndRole(args.Page, args.PerPage, args.Name, args.Roles, repository.DB)
- if err != nil {
- logger.Errorf("SeachUsers SeachUsers By name:%s error, error msg:%s", args.Name, err.Error())
- return nil, err
+ if args.OrderBy == setting.ListUserOrderByLoginTime {
+ users, err = orm.ListUsersByNameAndRoleWithLoginTime(args.Page, args.PerPage, args.Name, args.Roles, args.Order, repository.DB)
+ if err != nil {
+ logger.Errorf("SeachUsers SeachUsers By name:%s error, error msg:%s", args.Name, err.Error())
+ return nil, err
+ }
+ } else {
+ us, err = orm.ListUsersByNameAndRole(args.Page, args.PerPage, args.Name, args.Roles, repository.DB)
+ if err != nil {
+ logger.Errorf("SeachUsers SeachUsers By name:%s error, error msg:%s", args.Name, err.Error())
+ return nil, err
+ }
+ users = models.UsersToUserWithLoginTimes(us)
}
- users = models.UsersToUserWithLoginTimes(us)
}
var uids []string
@@ -1113,7 +1138,7 @@ func GetUserCount(logger *zap.SugaredLogger) (*types.UserStatistics, error) {
return nil, err
}
- vendorClient := plutusvendor.New()
+ vendorClient := plutusenterprise.New()
err = vendorClient.Health()
if err != nil {
return nil, err
diff --git a/pkg/microservice/user/server/grpc/server.go b/pkg/microservice/user/server/grpc/server.go
index dbbc8eea1a..f34c59f802 100644
--- a/pkg/microservice/user/server/grpc/server.go
+++ b/pkg/microservice/user/server/grpc/server.go
@@ -22,6 +22,7 @@ import (
"fmt"
"net/http"
"net/url"
+ "regexp"
"strings"
"time"
@@ -47,6 +48,8 @@ const (
mfaEnrollmentRequiredReason = "mfa_enrollment_required"
mfaVerificationRequiredReason = "mfa_verification_required"
mfaReasonHeaderKey = "x-zadig-auth-reason"
+
+ mfaEnrollmentBypassURLRegExp = `^/api/v1/users/[^/]+/mfa/[A-Za-z0-9_-]+$`
)
func (s *AuthServer) Check(ctx context.Context, request *ext_authz_v3.CheckRequest) (*ext_authz_v3.CheckResponse, error) {
@@ -249,6 +252,11 @@ func isMFAGateBypassURL(requestPath, method string) bool {
return true
}
+ match, _ := regexp.MatchString(mfaEnrollmentBypassURLRegExp, path)
+ if match && (method == http.MethodPost || method == http.MethodGet) {
+ return true
+ }
+
return false
}
diff --git a/pkg/middleware/gin/license.go b/pkg/middleware/gin/license.go
index f02da6bfbc..ee0d755681 100644
--- a/pkg/middleware/gin/license.go
+++ b/pkg/middleware/gin/license.go
@@ -66,7 +66,7 @@ func ProcessLicense() gin.HandlerFunc {
// return
//}
//// for the rest of the apis we need to check if the license works
- //client := plutusvendor.New()
+ //client := plutusenterprise.New()
//resp, err := client.CheckZadigXLicenseStatus()
//if err != nil {
// // if there are some unknown errors we return a
diff --git a/pkg/setting/consts.go b/pkg/setting/consts.go
index ed1db0407d..745e564bfa 100644
--- a/pkg/setting/consts.go
+++ b/pkg/setting/consts.go
@@ -58,6 +58,8 @@ const (
ENVLarkPluginID = "LARK_PLUGIN_ID"
ENVLarkPluginSecret = "LARK_PLUGIN_SECRET"
ENVLarkPluginAccessTokenType = "LARK_PLUGIN_ACCESS_TOKEN_TYPE"
+ ZadigAgentVersion = "ZADIG_AGENT_VERSION"
+ ZadigAgentRepoURL = "ZADIG_AGENT_REPO_URL"
ENVBuildBaseImage = "BUILD_BASE_IMAGE"
@@ -944,9 +946,10 @@ const (
)
const (
- UserTypeUser string = "user"
- UserTypeGroup string = "group"
- UserTypeTaskCreator string = "task_creator"
+ UserTypeUser string = "user"
+ UserTypeGroup string = "group"
+ UserTypeTaskCreator string = "task_creator"
+ UserTypeStageExecutor string = "stage_executor"
)
type ContainerType string
@@ -1014,8 +1017,9 @@ const (
type ReleasePlanCallBackResultType string
const (
- ReleasePlanCallBackResultTypeSuccess ReleasePlanCallBackResultType = "success"
- ReleasePlanCallBackResultTypeFailed ReleasePlanCallBackResultType = "failed"
+ ReleasePlanCallBackResultTypeSuccess ReleasePlanCallBackResultType = "success"
+ ReleasePlanCallBackResultTypeExecuting ReleasePlanCallBackResultType = "executing"
+ ReleasePlanCallBackResultTypeFailed ReleasePlanCallBackResultType = "failed"
)
type ListWorkflowV4InGlobalSortBy string
diff --git a/pkg/setting/types.go b/pkg/setting/types.go
index 75e4cc86da..8ee3b329e1 100644
--- a/pkg/setting/types.go
+++ b/pkg/setting/types.go
@@ -139,6 +139,7 @@ const (
Vendor
User
TimeNlp
+ Enterprise
)
type ServiceInfo struct {
@@ -203,4 +204,8 @@ var Services = map[int]*ServiceInfo{
Name: "time-nlp",
Port: 8000,
},
+ Enterprise: {
+ Name: "plutus-enterprise",
+ Port: 28000,
+ },
}
diff --git a/pkg/shared/client/plutusenterprise/client.go b/pkg/shared/client/plutusenterprise/client.go
new file mode 100644
index 0000000000..f8086b435f
--- /dev/null
+++ b/pkg/shared/client/plutusenterprise/client.go
@@ -0,0 +1,41 @@
+/*
+Copyright 2021 The KodeRover Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package plutusenterprise
+
+import (
+ "github.com/koderover/zadig/v2/pkg/config"
+ "github.com/koderover/zadig/v2/pkg/tool/httpclient"
+)
+
+type Client struct {
+ *httpclient.Client
+
+ host string
+}
+
+func New() *Client {
+ host := config.EnterpriseServiceAddress()
+
+ c := httpclient.New(
+ httpclient.SetHostURL(host + "/api/plutus-enterprise"),
+ )
+
+ return &Client{
+ Client: c,
+ host: host,
+ }
+}
diff --git a/pkg/shared/client/plutusenterprise/plutusenterprise.go b/pkg/shared/client/plutusenterprise/plutusenterprise.go
new file mode 100644
index 0000000000..11589f72b6
--- /dev/null
+++ b/pkg/shared/client/plutusenterprise/plutusenterprise.go
@@ -0,0 +1,97 @@
+/*
+Copyright 2021 The KodeRover Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package plutusenterprise
+
+import (
+ "fmt"
+
+ "github.com/koderover/zadig/v2/pkg/tool/httpclient"
+)
+
+type CheckSignatrueResp struct {
+ Code int64 `json:"code"`
+}
+
+func (c *Client) CheckSignature(userNum int64) (*CheckSignatrueResp, error) {
+ url := fmt.Sprintf("/signature/check?user_num=%d", userNum)
+ res := &CheckSignatrueResp{}
+ _, err := c.Post(url, httpclient.SetResult(res))
+ return res, err
+}
+
+const (
+ ZadigSystemTypeBasic = "basic"
+ ZadigSystemTypeProfessional = "professional"
+ ZadigSystemTypeEnterprise = "enterprise"
+ ZadigXLicenseStatusNormal = "normal"
+
+ ZadigLicenseFeatureAI = "ai"
+ ZadigLicenseFeatureSae = "sae"
+ ZadigLicenseFeatureDelivery = "delivery"
+)
+
+type ZadigXLicenseStatus struct {
+ Type string `json:"type"`
+ Status string `json:"status"`
+ SystemID string `json:"system_id"`
+ UserLimit int64 `json:"user_limit"`
+ UserCount int64 `json:"user_count"`
+ License string `json:"license"`
+ ExpireAt int64 `json:"expire_at"`
+ AvailableVersion string `json:"available_version"`
+ CurrentVersion string `json:"current_version"`
+ Features []string `json:"features"`
+ ImprovementPlan bool `json:"improvement_plan"`
+ CreatedAt int64 `json:"created_time"`
+ UpdatedAt int64 `json:"updated_time"`
+}
+
+func (c *Client) CheckZadigXLicenseStatus() (*ZadigXLicenseStatus, error) {
+ url := "/license"
+ res := &ZadigXLicenseStatus{}
+ _, err := c.Get(url, httpclient.SetResult(res))
+ return res, err
+}
+
+func (c *Client) Health() error {
+ url := "/health"
+ _, err := c.Get(url)
+ return err
+}
+
+type UpgradeCheckResponse struct {
+ AllowUpgrade bool `json:"allow_upgrade"`
+ IsUpgrade bool `json:"is_upgrade"`
+ FromVersion string `json:"from_version"`
+ ToVersion string `json:"to_version"`
+ MaintenanceExpireAt int64 `json:"maintenance_expire_at"`
+}
+
+type checkUpgradePermissionReq struct {
+ FromVersion string `json:"from_version"`
+ ToVersion string `json:"to_version"`
+}
+
+func (c *Client) CheckUpgrade(fromVersion, toVersion string) (*UpgradeCheckResponse, error) {
+ url := "/license/upgrade/check"
+ res := &UpgradeCheckResponse{}
+ _, err := c.Post(url, httpclient.SetBody(&checkUpgradePermissionReq{
+ FromVersion: fromVersion,
+ ToVersion: toVersion,
+ }), httpclient.SetResult(res))
+ return res, err
+}
diff --git a/pkg/shared/client/user/user_auth.go b/pkg/shared/client/user/user_auth.go
index 7c11926f94..1ad62755ba 100644
--- a/pkg/shared/client/user/user_auth.go
+++ b/pkg/shared/client/user/user_auth.go
@@ -68,6 +68,8 @@ type EnvActions struct {
Create bool
// 配置
EditConfig bool
+ // 调整副本
+ Scale bool
// 管理服务实例
ManagePods bool
Restart bool
@@ -83,6 +85,8 @@ type ProductionEnvActions struct {
Create bool
// 配置
EditConfig bool
+ // 调整副本
+ Scale bool
// 管理服务实例
ManagePods bool
Restart bool
@@ -178,6 +182,10 @@ type ReleasePlanActions struct {
type BusinessDirectoryActions struct {
View bool
+ // Edit business directory metadata/configuration.
+ Edit bool
+ Create bool
+ Delete bool
}
type ClusterManagementActions struct {
diff --git a/pkg/shared/kube/wrapper/ingress.go b/pkg/shared/kube/wrapper/ingress.go
index 2b03e9bd89..e1357d619f 100644
--- a/pkg/shared/kube/wrapper/ingress.go
+++ b/pkg/shared/kube/wrapper/ingress.go
@@ -60,10 +60,14 @@ func GetIngressHostInfo(ing *v1.Ingress) []resource.HostInfo {
for _, path := range rule.HTTP.Paths {
backend := resource.Backend{
- Path: path.Path,
- PathType: string(*path.PathType),
- ServiceName: path.Backend.Service.Name,
- ServicePort: fmt.Sprintf("%d", path.Backend.Service.Port.Number),
+ Path: path.Path,
+ }
+ if path.PathType != nil {
+ backend.PathType = string(*path.PathType)
+ }
+ if path.Backend.Service != nil {
+ backend.ServiceName = path.Backend.Service.Name
+ backend.ServicePort = fmt.Sprintf("%d", path.Backend.Service.Port.Number)
}
info.Backends = append(info.Backends, backend)
}
@@ -86,6 +90,8 @@ func (ing *ingress) HostInfo() []resource.HostInfo {
continue
}
+ // TODO: currently we are only dealing with the paths which are directed to services, if we need to support
+ // resource field in ingress, change the logic below and make sure the upper layer is changed correctly.
for _, path := range rule.HTTP.Paths {
pathType := string(extensionsv1beta1.PathTypeImplementationSpecific)
if path.PathType != nil {
diff --git a/pkg/tool/errors/http_errors.go b/pkg/tool/errors/http_errors.go
index 2e22af893a..91b36f1545 100644
--- a/pkg/tool/errors/http_errors.go
+++ b/pkg/tool/errors/http_errors.go
@@ -932,7 +932,8 @@ var (
//-----------------------------------------------------------------------------------------------
// License APIs Range: 7050 - 7059
//-----------------------------------------------------------------------------------------------
- ErrLicenseInvalid = NewHTTPError(7050, "用户许可证不可用,请检查许可证后重试")
+ ErrLicenseInvalid = NewHTTPError(7050, "用户许可证不可用,请检查许可证后重试")
+ ErrUpgradeNotAllowed = NewHTTPError(6695, "当前许可证维保期已过,不允许升级")
//-----------------------------------------------------------------------------------------------
// Istio Grayscale APIs Range: 7060 - 7069
diff --git a/pkg/tool/helmclient/helmclient.go b/pkg/tool/helmclient/helmclient.go
index c5144e2d8b..93caafd203 100644
--- a/pkg/tool/helmclient/helmclient.go
+++ b/pkg/tool/helmclient/helmclient.go
@@ -1015,6 +1015,13 @@ func (hClient *HelmClient) Clone() (*HelmClient, error) {
return ret, nil
}
+func (hClient *HelmClient) GetKubeClient() (client.Client, error) {
+ if hClient.kubeClient == nil {
+ return nil, fmt.Errorf("kubeClient is not initialized")
+ }
+ return hClient.kubeClient, nil
+}
+
// mergeInstallOptions merges values of the provided chart to helm install options used by the client.
func mergeInstallOptions(chartSpec *hc.ChartSpec, installOptions *action.Install) {
installOptions.CreateNamespace = chartSpec.CreateNamespace
diff --git a/pkg/tool/kube/containerlog/log.go b/pkg/tool/kube/containerlog/log.go
index 3018d42a55..a11e2d2ecc 100644
--- a/pkg/tool/kube/containerlog/log.go
+++ b/pkg/tool/kube/containerlog/log.go
@@ -47,6 +47,7 @@ func GetContainerLogs(namespace, podName, containerName string, follow bool, tai
if len(pod.Status.ContainerStatuses) == 0 {
return fmt.Errorf("length of container statuses is 0 for pod %s in ns %s", podName, namespace)
}
+
if pod.Status.ContainerStatuses[0].State.Terminated == nil {
return fmt.Errorf("failed to get pod status' terminated message")
}
diff --git a/pkg/tool/lark/model.go b/pkg/tool/lark/model.go
index 793dfc7447..3ac14c59e2 100644
--- a/pkg/tool/lark/model.go
+++ b/pkg/tool/lark/model.go
@@ -23,6 +23,8 @@ type UserInfo struct {
Avatar string `json:"avatar,omitempty" yaml:"avatar,omitempty" bson:"avatar,omitempty"`
// IsExecutor marks if the user is the executor of the workflow
IsExecutor bool `json:"is_executor" yaml:"is_executor" bson:"is_executor"`
+ // IsStageExecutor marks if the user represents the current stage executors
+ IsStageExecutor bool `json:"is_stage_executor,omitempty" yaml:"is_stage_executor,omitempty" bson:"is_stage_executor,omitempty"`
}
type DepartmentInfo struct {
@@ -86,7 +88,7 @@ type ApprovalInstanceData struct {
Timeline []*InstanceTimeline `json:"timeline,omitempty" yaml:"timeline,omitempty" bson:"timeline,omitempty"` // 审批动态
// 以下是 Zadig 添加的用于展示的字段
- UserName *string `json:"user_name,omitempty" yaml:"user_name,omitempty" bson:"user_name,omitempty"` // 发起人姓名
+ UserName *string `json:"user_name,omitempty" yaml:"user_name,omitempty" bson:"user_name,omitempty"` // 发起人姓名
UserAvatar *string `json:"user_avatar,omitempty" yaml:"user_avatar,omitempty" bson:"user_avatar,omitempty"` // 发起人头像
}
@@ -112,7 +114,7 @@ type InstanceTask struct {
EndTime *string `json:"end_time,omitempty" yaml:"end_time,omitempty" bson:"end_time,omitempty"` // task 完成时间, 未完成为 0
// 以下是 Zadig 添加的用于展示的字段
- UserName *string `json:"user_name,omitempty" yaml:"user_name,omitempty" bson:"user_name,omitempty"` // 审批人姓名
+ UserName *string `json:"user_name,omitempty" yaml:"user_name,omitempty" bson:"user_name,omitempty"` // 审批人姓名
UserAvatar *string `json:"user_avatar,omitempty" yaml:"user_avatar,omitempty" bson:"user_avatar,omitempty"` // 审批人头像
}
@@ -166,7 +168,7 @@ type InstanceTimeline struct {
Files []*File `json:"files,omitempty" yaml:"files,omitempty" bson:"files,omitempty"` // 审批附件
// 以下是 Zadig 添加的用于展示的字段
- UserName *string `json:"user_name,omitempty" yaml:"user_name,omitempty" bson:"user_name,omitempty"` // 动态产生用户姓名
+ UserName *string `json:"user_name,omitempty" yaml:"user_name,omitempty" bson:"user_name,omitempty"` // 动态产生用户姓名
UserAvatar *string `json:"user_avatar,omitempty" yaml:"user_avatar,omitempty" bson:"user_avatar,omitempty"` // 动态产生用户头像
}
@@ -178,6 +180,6 @@ type InstanceCcUser struct {
OpenId *string `json:"open_id,omitempty" yaml:"open_id,omitempty" bson:"open_id,omitempty"` // 抄送人 open id
// 以下是 Zadig 添加的用于展示的字段
- UserName *string `json:"user_name,omitempty" yaml:"user_name,omitempty" bson:"user_name,omitempty"` // 抄送人姓名
+ UserName *string `json:"user_name,omitempty" yaml:"user_name,omitempty" bson:"user_name,omitempty"` // 抄送人姓名
UserAvatar *string `json:"user_avatar,omitempty" yaml:"user_avatar,omitempty" bson:"user_avatar,omitempty"` // 抄送人头像
-}
\ No newline at end of file
+}
diff --git a/pkg/types/authz.go b/pkg/types/authz.go
index 650f138459..a1aa9e17f1 100644
--- a/pkg/types/authz.go
+++ b/pkg/types/authz.go
@@ -36,6 +36,7 @@ const (
// env actions for collaboration
EnvActionView = "get_environment"
EnvActionEditConfig = "config_environment"
+ EnvActionScale = "scale_environment"
EnvActionManagePod = "manage_environment"
EnvActionRestart = "restart_environment"
EnvActionRollback = "rollback_environment"
@@ -44,6 +45,7 @@ const (
// production env actions
ProductionEnvActionView = "get_production_environment"
ProductionEnvActionEditConfig = "config_production_environment"
+ ProductionEnvActionScale = "scale_production_environment"
ProductionEnvActionManagePod = "edit_production_environment"
ProductionEnvActionRestart = "restart_production_environment"
ProductionEnvActionRollback = "rollback_production_environment"
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"`
diff --git a/pkg/types/role.go b/pkg/types/role.go
index 29d9ae3050..90e3c2e185 100644
--- a/pkg/types/role.go
+++ b/pkg/types/role.go
@@ -17,19 +17,21 @@ limitations under the License.
package types
type Role struct {
- ID uint `json:"id"`
- Name string `json:"name"`
- Namespace string `json:"namespace"`
- Description string `json:"desc"`
- Type string `json:"type"`
+ ID uint `json:"id"`
+ Name string `json:"name"`
+ Namespace string `json:"namespace"`
+ Description string `json:"desc"`
+ Type string `json:"type"`
+ GlobalReadOnly bool `json:"global_read_only,omitempty"`
}
type DetailedRole struct {
- ID uint `json:"id"`
- Name string `json:"name"`
- Namespace string `json:"namespace"`
- Description string `json:"desc"`
- Type string `json:"type"`
+ ID uint `json:"id"`
+ Name string `json:"name"`
+ Namespace string `json:"namespace"`
+ Description string `json:"desc"`
+ Type string `json:"type"`
+ GlobalReadOnly bool `json:"global_read_only,omitempty"`
// ResourceActions represents a set of verbs with its corresponding resource.
// the json response of this field `rules` is used for compatibility.
ResourceActions []*ResourceAction `json:"rules"`