From 51982fdddd9ebd162ad8ffa6c44a60f6108dab16 Mon Sep 17 00:00:00 2001 From: "xu.zhu" Date: Mon, 27 Nov 2023 20:41:25 +0800 Subject: [PATCH] feat: support for checks of rollback and restart Signed-off-by: xu.zhu --- core/cmd/cmd.go | 2 +- core/controller/cluster/controller.go | 3 + .../controller/cluster/controller_basic_v2.go | 37 ++-- .../cluster/controller_operation.go | 86 +------- core/controller/member/controller_test.go | 2 +- core/controller/pipelinerun/controller.go | 200 +++++++++++++++++- .../cluster/gitrepo/gitrepo_cluster_mock.go | 28 +++ pkg/cluster/gitrepo/gitrepo_cluster.go | 79 +++++++ pkg/cluster/service/service.go | 81 ++++++- pkg/cluster/service/service_test.go | 4 +- 10 files changed, 408 insertions(+), 114 deletions(-) diff --git a/core/cmd/cmd.go b/core/cmd/cmd.go index ad291255d..d6c3f9d8f 100644 --- a/core/cmd/cmd.go +++ b/core/cmd/cmd.go @@ -434,7 +434,7 @@ func Init(ctx context.Context, flags *Flags, coreConfig *config.Config) { groupSvc := groupservice.NewService(manager) eventSvc := eventservice.New(manager) applicationSvc := applicationservice.NewService(groupSvc, manager) - clusterSvc := clusterservice.NewService(applicationSvc, manager) + clusterSvc := clusterservice.NewService(applicationSvc, clusterGitRepo, manager) userSvc := userservice.NewService(manager) tokenSvc := tokenservice.NewService(manager, coreConfig.TokenConfig) diff --git a/core/controller/cluster/controller.go b/core/controller/cluster/controller.go index 6de7c35c9..a3a8bd52b 100644 --- a/core/controller/cluster/controller.go +++ b/core/controller/cluster/controller.go @@ -17,6 +17,7 @@ package cluster import ( "context" + clusterservice "github.com/horizoncd/horizon/pkg/cluster/service" templatemanager "github.com/horizoncd/horizon/pkg/template/manager" "k8s.io/apimachinery/pkg/runtime/schema" @@ -164,6 +165,7 @@ type controller struct { tokenConfig token.Config templateUpgradeMapper template.UpgradeMapper collectionManager collectionmanager.Manager + clusterSvc clusterservice.Service } var _ Controller = (*controller)(nil) @@ -206,5 +208,6 @@ func NewController(config *config.Config, param *param.Param) Controller { tokenConfig: config.TokenConfig, templateUpgradeMapper: config.TemplateUpgradeMapper, collectionManager: param.CollectionMgr, + clusterSvc: param.ClusterSvc, } } diff --git a/core/controller/cluster/controller_basic_v2.go b/core/controller/cluster/controller_basic_v2.go index 2563845f1..1f84a447a 100644 --- a/core/controller/cluster/controller_basic_v2.go +++ b/core/controller/cluster/controller_basic_v2.go @@ -657,7 +657,7 @@ func (c *controller) CreatePipelineRun(ctx context.Context, clusterID uint, return nil, err } - // 找一下是否需要check,如果不需要则直接设为ready + // if checks is empty, set status to ready checks, err := c.prSvc.GetCheckByResource(ctx, clusterID, common.ResourceCluster) if err != nil { return nil, err @@ -683,8 +683,6 @@ func (c *controller) CreatePipelineRun(ctx context.Context, clusterID uint, func (c *controller) createPipelineRun(ctx context.Context, clusterID uint, r *CreatePipelineRunRequest) (*prmodels.Pipelinerun, error) { defer wlog.Start(ctx, "cluster controller: create pipeline run").StopPrint() - var action string - var err error cluster, err := c.clusterMgr.GetByID(ctx, clusterID) if err != nil { @@ -694,8 +692,16 @@ func (c *controller) createPipelineRun(ctx context.Context, clusterID uint, return nil, herrors.ErrBuildDeployNotSupported } - var gitURL, gitRef, gitRefType, imageURL, codeCommitID = cluster.GitURL, - cluster.GitRef, cluster.GitRefType, cluster.Image, cluster.GitRef + var ( + title = r.Title + action string + gitURL = cluster.GitURL + gitRefType = cluster.GitRefType + gitRef = cluster.GitRef + codeCommitID string + imageURL = cluster.Image + rollbackFrom *uint + ) application, err := c.applicationMgr.GetByID(ctx, cluster.ApplicationID) if err != nil { @@ -768,6 +774,7 @@ func (c *controller) createPipelineRun(ctx context.Context, clusterID uint, } case prmodels.ActionRollback: + title = prmodels.ActionRollback action = prmodels.ActionRollback // get pipelinerun to rollback, and do some validation @@ -787,14 +794,17 @@ func (c *controller) createPipelineRun(ctx context.Context, clusterID uint, "the pipelinerun with id: %v is not belongs to cluster: %v", r.PipelinerunID, clusterID) } - // Deprecated: for internal usage - err = c.checkAndSyncGitOpsBranch(ctx, application.Name, cluster.Name, pipelinerun.ConfigCommit) - if err != nil { - return nil, err - } + gitURL = pipelinerun.GitURL + gitRefType = pipelinerun.GitRefType + gitRef = pipelinerun.GitRef + codeCommitID = pipelinerun.GitCommit + imageURL = pipelinerun.ImageURL + rollbackFrom = &pipelinerun.ID + configCommitSHA = configCommit.Master - gitURL, gitRefType, gitRef, codeCommitID, imageURL = - cluster.GitURL, cluster.GitRefType, cluster.GitRef, pipelinerun.GitCommit, pipelinerun.ImageURL + case prmodels.ActionRestart: + title = prmodels.ActionRestart + action = prmodels.ActionRestart configCommitSHA = configCommit.Master default: @@ -805,7 +815,7 @@ func (c *controller) createPipelineRun(ctx context.Context, clusterID uint, ClusterID: clusterID, Action: action, Status: string(prmodels.StatusPending), - Title: r.Title, + Title: title, Description: r.Description, GitURL: gitURL, GitRefType: gitRefType, @@ -814,5 +824,6 @@ func (c *controller) createPipelineRun(ctx context.Context, clusterID uint, ImageURL: imageURL, LastConfigCommit: lastConfigCommitSHA, ConfigCommit: configCommitSHA, + RollbackFrom: rollbackFrom, }, nil } diff --git a/core/controller/cluster/controller_operation.go b/core/controller/cluster/controller_operation.go index c817d2b8e..7d288a226 100644 --- a/core/controller/cluster/controller_operation.go +++ b/core/controller/cluster/controller_operation.go @@ -280,7 +280,7 @@ func getDeployImage(imageURL, deployTag string) (string, error) { func (c *controller) Rollback(ctx context.Context, clusterID uint, r *RollbackRequest) (_ *PipelinerunIDResponse, err error) { - const op = "cluster controller: rollback " + const op = "cluster controller: rollback" defer wlog.Start(ctx, op).StopPrint() // 1. get pipelinerun to rollback, and do some validation @@ -335,8 +335,8 @@ func (c *controller) Rollback(ctx context.Context, return nil, err } - // Deprecated: for internal usage - err = c.checkAndSyncGitOpsBranch(ctx, application.Name, cluster.Name, pipelinerun.ConfigCommit) + // for internal usage + err = c.clusterGitRepo.CheckAndSyncGitOpsBranch(ctx, application.Name, cluster.Name, pipelinerun.ConfigCommit) if err != nil { return nil, err } @@ -368,7 +368,7 @@ func (c *controller) Rollback(ctx context.Context, // 6. update template and tags in db // TODO(zhuxu): remove strong dependencies on db updates, just print an err log when updates fail - cluster, err = c.updateTemplateAndTagsFromFile(ctx, application, cluster) + cluster, err = c.clusterSvc.SyncDBWithGitRepo(ctx, application, cluster) if err != nil { return nil, err } @@ -671,7 +671,7 @@ func (c *controller) Upgrade(ctx context.Context, clusterID uint) error { } // 3. sync gitops branch if restarts occur - err = c.syncGitOpsBranch(ctx, application.Name, cluster.Name) + err = c.clusterGitRepo.SyncGitOpsBranch(ctx, application.Name, cluster.Name) if err != nil { return err } @@ -765,79 +765,3 @@ func (c *controller) updateTemplateAndTagsFromFile(ctx context.Context, } return cluster, nil } - -func (c *controller) checkAndSyncGitOpsBranch(ctx context.Context, application, - cluster string, commit string) error { - changed, err := c.manifestVersionChanged(ctx, application, cluster, commit) - if err != nil { - return err - } - if changed { - err = c.syncGitOpsBranch(ctx, application, cluster) - if err != nil { - return err - } - } - return nil -} - -// Deprecated: for internal usage -// manifestVersionChanged determines whether manifest version is changed -func (c *controller) manifestVersionChanged(ctx context.Context, application, - cluster string, commit string) (bool, error) { - currentManifest, err1 := c.clusterGitRepo.GetManifest(ctx, application, cluster, nil) - if err1 != nil { - if _, ok := perror.Cause(err1).(*herrors.HorizonErrNotFound); !ok { - log.Errorf(ctx, "get cluster manifest error, err = %s", err1.Error()) - return false, err1 - } - } - targetManifest, err2 := c.clusterGitRepo.GetManifest(ctx, application, cluster, &commit) - if err2 != nil { - if _, ok := perror.Cause(err2).(*herrors.HorizonErrNotFound); !ok { - log.Errorf(ctx, "get cluster manifest error, err = %s", err2.Error()) - return false, err2 - } - } - if err1 != nil && err2 != nil { - // manifest does not exist in both revisions - return false, nil - } - if err1 != nil || err2 != nil { - // One exists and the other does not exist in two revisions - return true, nil - } - return currentManifest.Version != targetManifest.Version, nil -} - -// Deprecated: for internal usage -// syncGitOpsBranch syncs gitOps branch with default branch to avoid merge conflicts. -// Restart updates time in restart.yaml in default branch. When other actions update -// template prefix in gitOps branch, there are merge conflicts in restart.yaml because -// usual context lines of 'git diff' are three. Ref: https://git-scm.com/docs/git-diff -// For example: -// -// <<<<<<< HEAD -// javaapp: -// restartTime: "2025-02-19 10:24:52" -// ======= -// rollout: -// restartTime: "2025-02-14 12:12:07" -// >>>>>>> gitops -func (c *controller) syncGitOpsBranch(ctx context.Context, application, cluster string) error { - gitOpsBranch := gitrepo.GitOpsBranch - defaultBranch := c.clusterGitRepo.DefaultBranch() - diff, err := c.clusterGitRepo.CompareConfig(ctx, application, cluster, - &gitOpsBranch, &defaultBranch) - if err != nil { - return err - } - if diff != "" { - _, err = c.clusterGitRepo.MergeBranch(ctx, application, - cluster, defaultBranch, gitOpsBranch, nil) - if err != nil { - return err - } - } - return nil -} diff --git a/core/controller/member/controller_test.go b/core/controller/member/controller_test.go index a4aa68561..98455ddff 100644 --- a/core/controller/member/controller_test.go +++ b/core/controller/member/controller_test.go @@ -95,7 +95,7 @@ func createContext(t *testing.T) { groupSvc = groupservice.NewService(manager) applicationSvc = applicationservice.NewService(groupSvc, manager) - clusterSvc = clusterservice.NewService(applicationSvc, manager) + clusterSvc = clusterservice.NewService(applicationSvc, nil, manager) eventSvc = eventservice.New(manager) } diff --git a/core/controller/pipelinerun/controller.go b/core/controller/pipelinerun/controller.go index 257da76d4..1ec63dfd1 100644 --- a/core/controller/pipelinerun/controller.go +++ b/core/controller/pipelinerun/controller.go @@ -26,10 +26,15 @@ import ( herrors "github.com/horizoncd/horizon/core/errors" "github.com/horizoncd/horizon/lib/q" appmanager "github.com/horizoncd/horizon/pkg/application/manager" + appmodels "github.com/horizoncd/horizon/pkg/application/models" + "github.com/horizoncd/horizon/pkg/authentication/user" + "github.com/horizoncd/horizon/pkg/cd" "github.com/horizoncd/horizon/pkg/cluster/code" codemodels "github.com/horizoncd/horizon/pkg/cluster/code" "github.com/horizoncd/horizon/pkg/cluster/gitrepo" clustermanager "github.com/horizoncd/horizon/pkg/cluster/manager" + clustermodels "github.com/horizoncd/horizon/pkg/cluster/models" + clusterservice "github.com/horizoncd/horizon/pkg/cluster/service" "github.com/horizoncd/horizon/pkg/cluster/tekton" "github.com/horizoncd/horizon/pkg/cluster/tekton/collector" "github.com/horizoncd/horizon/pkg/cluster/tekton/factory" @@ -96,6 +101,8 @@ type controller struct { clusterGitRepo gitrepo.ClusterGitRepo userMgr usermanager.Manager eventSvc eventservice.Service + cd cd.CD + clusterSvc clusterservice.Service } var _ Controller = (*controller)(nil) @@ -117,6 +124,8 @@ func NewController(config *config.Config, param *param.Param) Controller { userMgr: param.UserMgr, templateReleaseMgr: param.TemplateReleaseMgr, eventSvc: param.EventSvc, + cd: param.CD, + clusterSvc: param.ClusterSvc, } } @@ -394,26 +403,38 @@ func (c *controller) execute(ctx context.Context, pr *prmodels.Pipelinerun) erro return err } - // 1. get cluster + // 0. get resources cluster, err := c.clusterMgr.GetByID(ctx, pr.ClusterID) if err != nil { return err } - - // 2. get application application, err := c.appMgr.GetByID(ctx, cluster.ApplicationID) if err != nil { return err } - // 3. generate a JWT token for tekton callback - token, err := c.tokenSvc.CreateJWTToken(strconv.Itoa(int(currentUser.GetID())), + switch pr.Action { + case prmodels.ActionBuildDeploy, prmodels.ActionDeploy: + return c.executeDeploy(ctx, application, cluster, pr, currentUser) + case prmodels.ActionRestart: + return c.executeRestart(ctx, application, cluster, pr) + case prmodels.ActionRollback: + return c.executeRollback(ctx, application, cluster, pr) + default: + return perror.Wrapf(herrors.ErrParamInvalid, "unsupported action %v", pr.Action) + } +} + +func (c *controller) executeDeploy(ctx context.Context, application *appmodels.Application, + cluster *clustermodels.Cluster, pr *prmodels.Pipelinerun, currentUser user.User) error { + // 1. generate a JWT token for tekton callback + callbackToken, err := c.tokenSvc.CreateJWTToken(strconv.Itoa(int(currentUser.GetID())), c.tokenConfig.CallbackTokenExpireIn, tokensvc.WithPipelinerunID(pr.ID)) if err != nil { return err } - // 4. create pipelinerun in k8s + // 2. create pipelinerun in k8s tektonClient, err := c.tektonFty.GetTekton(cluster.EnvironmentName) if err != nil { return err @@ -465,7 +486,7 @@ func (c *controller) execute(ctx context.Context, pr *prmodels.Pipelinerun) erro Region: cluster.RegionName, RegionID: regionEntity.ID, Template: cluster.Template, - Token: token, + Token: callbackToken, }) if err != nil { return err @@ -485,7 +506,172 @@ func (c *controller) execute(ctx context.Context, pr *prmodels.Pipelinerun) erro if err != nil { return err } + return nil +} + +func (c *controller) executeRestart(ctx context.Context, application *appmodels.Application, + cluster *clustermodels.Cluster, pr *prmodels.Pipelinerun) error { + // 1. update pr status to running + if err := c.prMgr.PipelineRun.UpdateColumns(ctx, pr.ID, map[string]interface{}{ + "status": prmodels.StatusRunning, + "started_at": time.Now(), + }); err != nil { + return perror.Wrapf(err, "failed to update pr status, pr = %d, status = %s", + pr.ID, prmodels.StatusRunning) + } + // 2. update restartTime in git repo, then update pr status to merged + lastConfigCommit, err := c.clusterGitRepo.GetConfigCommit(ctx, application.Name, cluster.Name) + if err != nil { + return perror.Wrapf(err, "failed to get last config commit, cluster = %s", cluster.Name) + } + commit, err := c.clusterGitRepo.UpdateRestartTime(ctx, application.Name, cluster.Name, cluster.Template) + if err != nil { + return perror.Wrapf(err, "failed to update cluster restart time, cluster = %s", cluster.Name) + } + if err := c.prMgr.PipelineRun.UpdateColumns(ctx, pr.ID, map[string]interface{}{ + "status": prmodels.StatusMerged, + "last_config_commit": lastConfigCommit.Master, + "config_commit": commit, + }); err != nil { + return perror.Wrapf(err, "failed to update pr columns, pr = %d, status = %s, config_commit = %s", + pr.ID, prmodels.StatusMerged, commit) + } + // 3. deploy cluster in cd system + if err := c.cd.DeployCluster(ctx, &cd.DeployClusterParams{ + Environment: cluster.EnvironmentName, + Cluster: cluster.Name, + Revision: commit, + }); err != nil { + return perror.Wrapf(err, "failed to deploy cluster in CD, cluster = %s, revision = %s", + cluster.Name, commit) + } + // 4. update pr status to ok + if err := c.prMgr.PipelineRun.UpdateColumns(ctx, pr.ID, map[string]interface{}{ + "status": prmodels.StatusOK, + "finished_at": time.Now(), + }); err != nil { + return perror.Wrapf(err, "failed to update pr status, pr = %d, status = %s", + pr.ID, prmodels.StatusOK) + } + // 5. create event + c.eventSvc.CreateEventIgnoreError(ctx, common.ResourceCluster, cluster.ID, + eventmodels.ClusterRestarted, nil) + return nil +} + +func (c *controller) executeRollback(ctx context.Context, application *appmodels.Application, + cluster *clustermodels.Cluster, pr *prmodels.Pipelinerun) error { + // 1. update pr status to running + if err := c.prMgr.PipelineRun.UpdateColumns(ctx, pr.ID, map[string]interface{}{ + "status": prmodels.StatusRunning, + "started_at": time.Now(), + }); err != nil { + return perror.Wrapf(err, "failed to update pr status, pr = %d, status = %s", + pr.ID, prmodels.StatusRunning) + } + // 2. get pipelinerun to rollback + if pr.RollbackFrom == nil { + return perror.Wrapf(herrors.ErrParamInvalid, "pipelinerun to rollback is empty") + } + prToRollback, err := c.prMgr.PipelineRun.GetByID(ctx, *pr.RollbackFrom) + if err != nil { + return perror.Wrapf(err, "failed to get pipelinerun to rollback, pr = %d", *pr.RollbackFrom) + } + + // for internal usage + if err = c.clusterGitRepo.CheckAndSyncGitOpsBranch(ctx, application.Name, + cluster.Name, prToRollback.ConfigCommit); err != nil { + return perror.Wrapf(err, "failed to check and sync gitops branch, cluster = %s", cluster.Name) + } + + // 3. rollback cluster config in git repo and update status + lastConfigCommit, err := c.clusterGitRepo.GetConfigCommit(ctx, application.Name, cluster.Name) + if err != nil { + return perror.Wrapf(err, "failed to get last config commit, cluster = %s", cluster.Name) + } + if _, err := c.clusterGitRepo.Rollback(ctx, application.Name, cluster.Name, + prToRollback.ConfigCommit); err != nil { + return perror.Wrapf(err, "failed to rollback cluster config, cluster = %s, commit = %s", + cluster.Name, prToRollback.ConfigCommit) + } + if err := c.prMgr.PipelineRun.UpdateColumns(ctx, pr.ID, map[string]interface{}{ + "status": prmodels.StatusCommitted, + "last_config_commit": lastConfigCommit.Master, + }); err != nil { + return perror.Wrapf(err, "failed to update pr columns, pr = %d, status = %s", + pr.ID, prmodels.StatusCommitted) + } + + // 4. merge branch & update config commit and status + masterRevision, err := c.clusterGitRepo.MergeBranch(ctx, application.Name, cluster.Name, + gitrepo.GitOpsBranch, c.clusterGitRepo.DefaultBranch(), &pr.ID) + if err != nil { + return perror.Wrapf(err, "failed to merge branch, cluster = %s", cluster.Name) + } + if err := c.prMgr.PipelineRun.UpdateColumns(ctx, pr.ID, map[string]interface{}{ + "status": prmodels.StatusMerged, + "config_commit": masterRevision, + }); err != nil { + return perror.Wrapf(err, "failed to update pr columns, pr = %d, status = %s, config_commit = %s", + pr.ID, prmodels.StatusMerged, masterRevision) + } + + // 5. update template and tags in db + cluster, err = c.clusterSvc.SyncDBWithGitRepo(ctx, application, cluster) + if err != nil { + return perror.Wrapf(err, "failed to sync db with git repo, cluster = %s", cluster.Name) + } + + // 6. create cluster in cd system + regionEntity, err := c.regionMgr.GetRegionEntity(ctx, cluster.RegionName) + if err != nil { + return perror.Wrapf(err, "failed to get region entity, region = %s", cluster.RegionName) + } + envValue, err := c.clusterGitRepo.GetEnvValue(ctx, application.Name, cluster.Name, cluster.Template) + if err != nil { + return perror.Wrapf(err, "failed to get env value, cluster = %s", cluster.Name) + } + repoInfo := c.clusterGitRepo.GetRepoInfo(ctx, application.Name, cluster.Name) + if err := c.cd.CreateCluster(ctx, &cd.CreateClusterParams{ + Environment: cluster.EnvironmentName, + Cluster: cluster.Name, + GitRepoURL: repoInfo.GitRepoURL, + ValueFiles: repoInfo.ValueFiles, + RegionEntity: regionEntity, + Namespace: envValue.Namespace, + }); err != nil { + return perror.Wrapf(err, "failed to create cluster in CD, cluster = %s", cluster.Name) + } + + // 7. reset cluster status + if cluster.Status == common.ClusterStatusFreed { + cluster.Status = common.ClusterStatusEmpty + cluster, err = c.clusterMgr.UpdateByID(ctx, cluster.ID, cluster) + if err != nil { + return perror.Wrapf(err, "failed to update cluster status, cluster = %s", cluster.Name) + } + } + + // 8. deploy cluster in cd and update status + if err := c.cd.DeployCluster(ctx, &cd.DeployClusterParams{ + Environment: cluster.EnvironmentName, + Cluster: cluster.Name, + Revision: masterRevision, + }); err != nil { + return perror.Wrapf(err, "failed to deploy cluster in CD, cluster = %s, revision = %s", + cluster.Name, masterRevision) + } + if err := c.prMgr.PipelineRun.UpdateColumns(ctx, pr.ID, map[string]interface{}{ + "status": prmodels.StatusOK, + "finished_at": time.Now(), + }); err != nil { + return perror.Wrapf(err, "failed to update pr status, pr = %d, status = %s", + pr.ID, prmodels.StatusOK) + } + // 9. record event + c.eventSvc.CreateEventIgnoreError(ctx, common.ResourceCluster, cluster.ID, + eventmodels.ClusterRollbacked, nil) return nil } diff --git a/mock/pkg/cluster/gitrepo/gitrepo_cluster_mock.go b/mock/pkg/cluster/gitrepo/gitrepo_cluster_mock.go index 6631bde7d..65d946f51 100644 --- a/mock/pkg/cluster/gitrepo/gitrepo_cluster_mock.go +++ b/mock/pkg/cluster/gitrepo/gitrepo_cluster_mock.go @@ -37,6 +37,20 @@ func (m *MockClusterGitRepo) EXPECT() *MockClusterGitRepoMockRecorder { return m.recorder } +// CheckAndSyncGitOpsBranch mocks base method. +func (m *MockClusterGitRepo) CheckAndSyncGitOpsBranch(ctx context.Context, application, cluster, commit string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckAndSyncGitOpsBranch", ctx, application, cluster, commit) + ret0, _ := ret[0].(error) + return ret0 +} + +// CheckAndSyncGitOpsBranch indicates an expected call of CheckAndSyncGitOpsBranch. +func (mr *MockClusterGitRepoMockRecorder) CheckAndSyncGitOpsBranch(ctx, application, cluster, commit interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckAndSyncGitOpsBranch", reflect.TypeOf((*MockClusterGitRepo)(nil).CheckAndSyncGitOpsBranch), ctx, application, cluster, commit) +} + // CompareConfig mocks base method. func (m *MockClusterGitRepo) CompareConfig(ctx context.Context, application, cluster string, from, to *string) (string, error) { m.ctrl.T.Helper() @@ -272,6 +286,20 @@ func (mr *MockClusterGitRepoMockRecorder) Rollback(ctx, application, cluster, co return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rollback", reflect.TypeOf((*MockClusterGitRepo)(nil).Rollback), ctx, application, cluster, commit) } +// SyncGitOpsBranch mocks base method. +func (m *MockClusterGitRepo) SyncGitOpsBranch(ctx context.Context, application, cluster string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SyncGitOpsBranch", ctx, application, cluster) + ret0, _ := ret[0].(error) + return ret0 +} + +// SyncGitOpsBranch indicates an expected call of SyncGitOpsBranch. +func (mr *MockClusterGitRepoMockRecorder) SyncGitOpsBranch(ctx, application, cluster interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncGitOpsBranch", reflect.TypeOf((*MockClusterGitRepo)(nil).SyncGitOpsBranch), ctx, application, cluster) +} + // UpdateCluster mocks base method. func (m *MockClusterGitRepo) UpdateCluster(ctx context.Context, params *gitrepo.UpdateClusterParams) error { m.ctrl.T.Helper() diff --git a/pkg/cluster/gitrepo/gitrepo_cluster.go b/pkg/cluster/gitrepo/gitrepo_cluster.go index d5bc86254..66b1df024 100644 --- a/pkg/cluster/gitrepo/gitrepo_cluster.go +++ b/pkg/cluster/gitrepo/gitrepo_cluster.go @@ -154,6 +154,11 @@ type ClusterGitRepo interface { // GetManifest returns manifest with specific revision, defaults to gitops branch GetManifest(ctx context.Context, application, cluster string, commit *string) (*pkgcommon.Manifest, error) + // CheckAndSyncGitOpsBranch checks and sync if gitops branch is not up-to-date with master branch + // for internal usage + CheckAndSyncGitOpsBranch(ctx context.Context, application, cluster, commit string) error + // SyncGitOpsBranch syncs gitops branch to up-to-date with master branch + SyncGitOpsBranch(ctx context.Context, application, cluster string) error } type clusterGitopsRepo struct { gitlabLib gitlablib.Interface @@ -1079,6 +1084,20 @@ func (g *clusterGitopsRepo) GetEnvValue(ctx context.Context, return envMap[templateName][common.GitopsEnvValueNamespace], nil } +func (g *clusterGitopsRepo) CheckAndSyncGitOpsBranch(ctx context.Context, application, cluster, commit string) error { + changed, err := g.manifestVersionChanged(ctx, application, cluster, commit) + if err != nil { + return err + } + if changed { + err = g.SyncGitOpsBranch(ctx, application, cluster) + if err != nil { + return err + } + } + return nil +} + func (g *clusterGitopsRepo) Rollback(ctx context.Context, application, cluster, commit string) (_ string, err error) { const op = "cluster git repo: rollback" defer wlog.Start(ctx, op).StopPrint() @@ -1759,6 +1778,66 @@ func (g *clusterGitopsRepo) readFile(ctx context.Context, application, cluster, return g.gitlabLib.GetFile(ctx, pid, GitOpsBranch, fileName) } +// for internal usage +// manifestVersionChanged determines whether manifest version is changed +func (g *clusterGitopsRepo) manifestVersionChanged(ctx context.Context, application, + cluster string, commit string) (bool, error) { + currentManifest, err1 := g.GetManifest(ctx, application, cluster, nil) + if err1 != nil { + if _, ok := perror.Cause(err1).(*herrors.HorizonErrNotFound); !ok { + log.Errorf(ctx, "get cluster manifest error, err = %s", err1.Error()) + return false, err1 + } + } + targetManifest, err2 := g.GetManifest(ctx, application, cluster, &commit) + if err2 != nil { + if _, ok := perror.Cause(err2).(*herrors.HorizonErrNotFound); !ok { + log.Errorf(ctx, "get cluster manifest error, err = %s", err2.Error()) + return false, err2 + } + } + if err1 != nil && err2 != nil { + // manifest does not exist in both revisions + return false, nil + } + if err1 != nil || err2 != nil { + // One exists and the other does not exist in two revisions + return true, nil + } + return currentManifest.Version != targetManifest.Version, nil +} + +// SyncGitOpsBranch for internal usage, syncs gitOps branch with default branch to avoid merge conflicts. +// Restart updates time in restart.yaml in default branch. When other actions update +// template prefix in gitOps branch, there are merge conflicts in restart.yaml because +// usual context lines of 'git diff' are three. Ref: https://git-scm.com/docs/git-diff +// For example: +// +// <<<<<<< HEAD +// javaapp: +// restartTime: "2025-02-19 10:24:52" +// ======= +// rollout: +// restartTime: "2025-02-14 12:12:07" +// >>>>>>> gitops +func (g *clusterGitopsRepo) SyncGitOpsBranch(ctx context.Context, application, cluster string) error { + gitOpsBranch := GitOpsBranch + defaultBranch := g.DefaultBranch() + diff, err := g.CompareConfig(ctx, application, cluster, + &gitOpsBranch, &defaultBranch) + if err != nil { + return err + } + if diff != "" { + _, err = g.MergeBranch(ctx, application, + cluster, defaultBranch, gitOpsBranch, nil) + if err != nil { + return err + } + } + return nil +} + func renameTemplateName(name string) string { templateName := []byte(name) for i := range templateName { diff --git a/pkg/cluster/service/service.go b/pkg/cluster/service/service.go index 2e7778afc..1620df6a7 100644 --- a/pkg/cluster/service/service.go +++ b/pkg/cluster/service/service.go @@ -18,28 +18,54 @@ import ( "context" "fmt" - applicationservice "github.com/horizoncd/horizon/pkg/application/service" + "github.com/horizoncd/horizon/core/common" + appmodels "github.com/horizoncd/horizon/pkg/application/models" + appservice "github.com/horizoncd/horizon/pkg/application/service" + "github.com/horizoncd/horizon/pkg/cluster/gitrepo" clustermanager "github.com/horizoncd/horizon/pkg/cluster/manager" + clustermodels "github.com/horizoncd/horizon/pkg/cluster/models" "github.com/horizoncd/horizon/pkg/param/managerparam" + tagmanager "github.com/horizoncd/horizon/pkg/tag/manager" + tmodels "github.com/horizoncd/horizon/pkg/tag/models" + trmanager "github.com/horizoncd/horizon/pkg/templaterelease/manager" ) type Service interface { // GetByID get detail of an application by id GetByID(ctx context.Context, id uint) (*ClusterDetail, error) + // SyncDBWithGitRepo syncs template and tags in db when git repo files are updated + SyncDBWithGitRepo(ctx context.Context, application *appmodels.Application, + cluster *clustermodels.Cluster) (*clustermodels.Cluster, error) } type service struct { - applicationService applicationservice.Service - clusterManager clustermanager.Manager + appSvc appservice.Service + clusterMgr clustermanager.Manager + trMgr trmanager.Manager + tagMgr tagmanager.Manager + clusterGitRepo gitrepo.ClusterGitRepo +} + +var _ Service = (*service)(nil) + +func NewService(applicationSvc appservice.Service, clusterGitRep gitrepo.ClusterGitRepo, + manager *managerparam.Manager) Service { + return &service{ + appSvc: applicationSvc, + clusterMgr: manager.ClusterMgr, + trMgr: manager.TemplateReleaseMgr, + tagMgr: manager.TagMgr, + clusterGitRepo: clusterGitRep, + } } func (s service) GetByID(ctx context.Context, id uint) (*ClusterDetail, error) { - cluster, err := s.clusterManager.GetByID(ctx, id) + cluster, err := s.clusterMgr.GetByID(ctx, id) if err != nil { return nil, err } - application, err := s.applicationService.GetByID(ctx, cluster.ApplicationID) + application, err := s.appSvc.GetByID(ctx, cluster.ApplicationID) if err != nil { return nil, err } @@ -52,9 +78,46 @@ func (s service) GetByID(ctx context.Context, id uint) (*ClusterDetail, error) { return clusterDetail, nil } -func NewService(applicationSvc applicationservice.Service, manager *managerparam.Manager) Service { - return &service{ - applicationService: applicationSvc, - clusterManager: manager.ClusterMgr, +func (s service) SyncDBWithGitRepo(ctx context.Context, application *appmodels.Application, + cluster *clustermodels.Cluster) (*clustermodels.Cluster, error) { + templateFromFile, err := s.clusterGitRepo.GetClusterTemplate(ctx, application.Name, cluster.Name) + if err != nil { + return nil, err + } + cluster.Template = templateFromFile.Name + cluster.TemplateRelease = templateFromFile.Release + cluster, err = s.clusterMgr.UpdateByID(ctx, cluster.ID, cluster) + if err != nil { + return nil, err + } + + files, err := s.clusterGitRepo.GetClusterValueFiles(ctx, application.Name, cluster.Name) + if err != nil { + return nil, err + } + + for _, file := range files { + if file.FileName == common.GitopsFileTags { + release, err := s.trMgr.GetByTemplateNameAndRelease(ctx, cluster.Template, cluster.TemplateRelease) + if err != nil { + return nil, err + } + midMap := file.Content[release.ChartName].(map[string]interface{}) + tagsMap := midMap[common.GitopsKeyTags].(map[string]interface{}) + tags := make([]*tmodels.TagBasic, 0, len(tagsMap)) + for k, v := range tagsMap { + value, ok := v.(string) + if !ok { + continue + } + tags = append(tags, &tmodels.TagBasic{ + Key: k, + Value: value, + }) + } + return cluster, s.tagMgr.UpsertByResourceTypeID(ctx, + common.ResourceCluster, cluster.ID, tags) + } } + return cluster, nil } diff --git a/pkg/cluster/service/service_test.go b/pkg/cluster/service/service_test.go index 65c2c9db5..c1d228440 100644 --- a/pkg/cluster/service/service_test.go +++ b/pkg/cluster/service/service_test.go @@ -69,8 +69,8 @@ func TestServiceGetByID(t *testing.T) { t.Run("GetByID", func(t *testing.T) { s := service{ - applicationService: applicationservice.NewService(groupservice.NewService(manager), manager), - clusterManager: manager.ClusterMgr, + appSvc: applicationservice.NewService(groupservice.NewService(manager), manager), + clusterMgr: manager.ClusterMgr, } result, err := s.GetByID(ctx, application.ID) assert.Nil(t, err)