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"`