package service import ( "context" "encoding/json" "fmt" "net/url" "reflect" "strconv" "strings" "time" "go-common/app/admin/ep/saga/conf" "go-common/app/admin/ep/saga/model" "go-common/library/ecode" "go-common/library/log" "github.com/BurntSushi/toml" ) const ( _configFlag = "\r\n" _sagaConfigFlag = "[[property.repos]]" ) const ( _svenConfigAppName = "app_name" _svenConfigEnv = "env" _svenConfigZone = "zone" _svenConfigTreeID = "tree_id" _svenConfigToken = "token" _svenConfigBuild = "build" _svenConfigUser = "user" _svenConfigData = "data" _svenConfigNames = "names" _svenConfigMark = "mark" _svenConfigConfigIDs = "config_ids" _svenConfigForce = "force" _svenConfigIncrement = "increment" ) const ( _formatStr = ` %s=%s` _formatStrQuo = ` %s="%s"` _formatValue = ` %s=%v` _formatInt = ` %s=%d` ) const ( _defaultBranch = "master" _defaultLockTimeout = 600 ) const ( _repoURL = "URL" _repoGroup = "Group" _repoName = "Name" _repoLanguage = "Language" _repoLockTimeout = "LockTimeout" _repoAuthBranches = "AuthBranches" _repoTargetBranches = "TargetBranches" ) var ( sagaConfigCnName = []string{ "仓库地址", "仓库组名", "仓库名称", "仓库别名", "开发语言", "权限分支", "目标分支", "MR锁定超时时间(s)", "最少review人数", "是否关联pipeline", "自动合并", "权限限制", "准入标签", "超级权限用户", } sagaConfigMark = []string{ "仓库地址", "仓库组名", "仓库名称", "仓库别名", "仓库使用语言", "saga的权限管控将以此分支配置的CONTRIBUTORS.md为准,即使CONTRIBUTORS.md在其他分支上更改了,也都会以此分支的鉴权信息为准。", "配置的分支可以触发saga行为,如配置 targetBranches为master、release分支,则MR的目标分支为master或release时,都能触发saga行为。支持通配", "每个仓库在每个时间点只能允许一个MR在合并。MR合并时会取得一个锁并在其合并结束后将其释放;如果获取的锁MR在lockTimeout时间内都未能结束,则会认为超时并将锁自动释放,这样其他MR才能有机会获取到独享锁及合并的机会。", "最终合并前除了需要owner点赞外,还需通过权限文件配置的Reviewer中minReviewer数量的人点赞后可合并。", "配置后saga将会检查pipeline执行结果,并会在最后merge前再次retry pipeline。不配置的话saga不会对pipeline执行结果进行判断。", "此配置以 relatePipeline 为基础,打开后saga在MR最终合并前将不再retry pipeline,并且 +mr 时如果pipeline还在运行中,待pipeline运行通过后MR将会自动合并。", "打开后owner的权限将只限定在当前目录,即如果子目录配置了owner等信息,根目录的owner等将不能再管控子目录。", "如果配置了标签,saga只合入打了此label的MR。", "如果有配置,原来的鉴权文件CONTRIBUTORS.md将会失效,需要合并的MR都必须通过super users的review。super users本身也拥有+mr直接合并的权利。", } ) // SagaUserList ... func (s *Service) SagaUserList(c context.Context) (resp []string, err error) { resp = conf.Conf.Property.Sven.SagaConfigsParam.UserList return } // QueryAllConfigFile ... func (s *Service) QueryAllConfigFile(c context.Context, sessionID string, isSaga bool) (resp *model.ConfigData, err error) { var ( url = conf.Conf.Property.Sven.Configs + "?app_name=%s&tree_id=%s&env=%s&zone=%s&build_id=%s" sagaConfig = conf.Conf.Property.Sven.SagaConfigsParam runnerConfig = conf.Conf.Property.Sven.ConfigsParam ) if isSaga { url = fmt.Sprintf(url, sagaConfig.AppName, strconv.Itoa(sagaConfig.TreeID), sagaConfig.Env, sagaConfig.Zone, strconv.Itoa(sagaConfig.BuildId)) } else { url = fmt.Sprintf(url, runnerConfig.AppName, strconv.Itoa(runnerConfig.TreeID), runnerConfig.Env, runnerConfig.Zone, strconv.Itoa(runnerConfig.BuildId)) } return s.dao.QueryAllConfigFile(c, sessionID, url) } // QueryConfigFileContent ... func (s *Service) QueryConfigFileContent(c context.Context, sessionID string) (content string, err error) { var ( url = conf.Conf.Property.Sven.ConfigValue fileName = conf.Conf.Property.Sven.SagaConfigsParam.FileName configs *model.ConfigData ) if configs, err = s.QueryAllConfigFile(c, sessionID, true); err != nil { return } for _, confValue := range configs.BuildFiles { if confValue.Name == fileName { log.Info("QueryConfigFileContent get config name: %s", fileName) id := strconv.Itoa(confValue.ID) url = fmt.Sprintf(url+"?config_id=%s", id) if content, err = s.dao.QueryConfigFileContent(c, sessionID, url); err != nil { return } } } return } // QueryProjectSagaConfig ... func (s *Service) QueryProjectSagaConfig(c context.Context, sessionID string, projectID int) (sagaConfig *model.RepoConfig, err error) { var ( projectInfo *model.ProjectInfo content string ) if content, err = s.QueryConfigFileContent(c, sessionID); err != nil { return } index := strings.Index(content, _sagaConfigFlag) if index < 0 { return } content = content[index:] log.Info("QueryProjectSagaConfig content: %s", content) Conf := &model.Config{} if _, err = toml.Decode(content, &Conf); err != nil { log.Error("QueryProjectSagaConfig Decode err(%+v)", err) return } if projectInfo, err = s.dao.ProjectInfoByID(projectID); err != nil { log.Error("QueryProjectSagaConfig ProjectInfoByID err(%+v)", err) return } projectUrl := strings.Replace(projectInfo.Repo, "git-test", "git", 1) for _, r := range Conf.Property.Repos { if r.URL == projectUrl { sagaConfig = r return } } return } // UpdateConfig ... func (s *Service) UpdateConfig(c context.Context, sessionID, user, configFileName, configContent, mark string, isSaga bool) (resp *model.CommonResp, err error) { var ( reqUrl = conf.Conf.Property.Sven.ConfigUpdate params = url.Values{} ) if isSaga { params.Set(_svenConfigAppName, conf.Conf.Property.Sven.SagaConfigsParam.AppName) params.Set(_svenConfigEnv, conf.Conf.Property.Sven.SagaConfigsParam.Env) params.Set(_svenConfigZone, conf.Conf.Property.Sven.SagaConfigsParam.Zone) params.Set(_svenConfigTreeID, strconv.Itoa(conf.Conf.Property.Sven.SagaConfigsParam.TreeID)) params.Set(_svenConfigToken, conf.Conf.Property.Sven.SagaConfigsParam.Token) } else { params.Set(_svenConfigAppName, conf.Conf.Property.Sven.ConfigsParam.AppName) params.Set(_svenConfigEnv, conf.Conf.Property.Sven.ConfigsParam.Env) params.Set(_svenConfigZone, conf.Conf.Property.Sven.ConfigsParam.Zone) params.Set(_svenConfigTreeID, strconv.Itoa(conf.Conf.Property.Sven.ConfigsParam.TreeID)) params.Set(_svenConfigToken, conf.Conf.Property.Sven.ConfigsParam.Token) } data := `[{"name":"%s","comment":"%s","mark":"%s"}]` data = fmt.Sprintf(data, configFileName, configContent, mark) params.Set(_svenConfigData, data) params.Set(_svenConfigUser, user) log.Info("UpdateConfig params:%v", params) if resp, err = s.dao.RequestConfig(c, sessionID, reqUrl, params); err != nil { return } if resp.Code == ecode.OK.Code() && resp.Message == "0" { log.Info("RequestConfig success") resp = nil } return } // PublicConfig ... func (s *Service) PublicConfig(c context.Context, sessionID, user, configFileName, mark string, isSaga bool) (resp *model.CommonResp, err error) { var ( reqUrl = conf.Conf.Property.Sven.TagUpdate params = url.Values{} ) if isSaga { params.Set(_svenConfigAppName, conf.Conf.Property.Sven.SagaConfigsParam.AppName) params.Set(_svenConfigEnv, conf.Conf.Property.Sven.SagaConfigsParam.Env) params.Set(_svenConfigZone, conf.Conf.Property.Sven.SagaConfigsParam.Zone) params.Set(_svenConfigTreeID, strconv.Itoa(conf.Conf.Property.Sven.SagaConfigsParam.TreeID)) params.Set(_svenConfigToken, conf.Conf.Property.Sven.SagaConfigsParam.Token) params.Set(_svenConfigBuild, conf.Conf.Property.Sven.SagaConfigsParam.Build) } else { params.Set(_svenConfigAppName, conf.Conf.Property.Sven.ConfigsParam.AppName) params.Set(_svenConfigEnv, conf.Conf.Property.Sven.ConfigsParam.Env) params.Set(_svenConfigZone, conf.Conf.Property.Sven.ConfigsParam.Zone) params.Set(_svenConfigTreeID, strconv.Itoa(conf.Conf.Property.Sven.ConfigsParam.TreeID)) params.Set(_svenConfigToken, conf.Conf.Property.Sven.ConfigsParam.Token) params.Set(_svenConfigBuild, conf.Conf.Property.Sven.ConfigsParam.Build) } params.Set(_svenConfigForce, strconv.Itoa(conf.Conf.Property.Sven.SagaConfigsParam.Force)) params.Set(_svenConfigIncrement, strconv.Itoa(conf.Conf.Property.Sven.SagaConfigsParam.Increment)) params.Set(_svenConfigMark, mark) params.Set(_svenConfigUser, user) params.Set(_svenConfigNames, configFileName) params.Set(_svenConfigConfigIDs, "") if resp, err = s.dao.RequestConfig(c, sessionID, reqUrl, params); err != nil { return } if resp.Code == ecode.OK.Code() && resp.Message == "0" { log.Info("RequestConfig success") resp = nil } return } // ParseRequestConfig ... func (s *Service) ParseRequestConfig(projectInfo *model.ProjectInfo, configs []model.ConfigSagaItem) (requestConfig *model.RepoConfig, requestConfigStr string, err error) { var ( configStr string content []byte ) configStr = doBasicDefault(projectInfo) for _, config := range configs { rv := reflect.ValueOf(config.Value) if !rv.IsValid() { configStr, _ = doDefault(config.Name, configStr) continue } if rv.Kind() == reflect.Slice { log.Info("%s is slice", config.Name) if rv.IsNil() { configStr, _ = doDefault(config.Name, configStr) continue } if content, err = json.Marshal(config.Value); err != nil { log.Error("ParseRequestConfig err(%+v)", err) return } configTr := fmt.Sprintf(_formatStr, config.Name, string(content)) configStr = configStr + configTr + _configFlag } else { configTr := fmt.Sprintf(_formatValue, config.Name, config.Value) configStr = configStr + configTr + _configFlag } } log.Info("ParseRequestConfig: %s", configStr) requestConfigStr = configStr requestConfig = &model.RepoConfig{} if _, err = toml.Decode(configStr, &requestConfig); err != nil { log.Error("ParseRequestConfig toml decode err(%+v)", err) return } return } // doBasicDefault ... func doBasicDefault(projectInfo *model.ProjectInfo) (configStr string) { var configTr string configStr = _configFlag configTr = fmt.Sprintf(_formatStrQuo, _repoURL, projectInfo.Repo) configStr = configStr + configTr + _configFlag configTr = fmt.Sprintf(_formatStrQuo, _repoGroup, projectInfo.SpaceName) configStr = configStr + configTr + _configFlag configTr = fmt.Sprintf(_formatStrQuo, _repoName, projectInfo.Name) configStr = configStr + configTr + _configFlag return configStr } // doDefault ... func doDefault(name, config string) (configStr string, err error) { var ( content []byte defaultBr = []string{_defaultBranch} ) configStr = config if strings.ToLower(name) == strings.ToLower(_repoLockTimeout) { configTr := fmt.Sprintf(_formatInt, name, _defaultLockTimeout) configStr = configStr + configTr + _configFlag } if strings.ToLower(name) == strings.ToLower(_repoAuthBranches) || strings.ToLower(name) == strings.ToLower(_repoTargetBranches) { if content, err = json.Marshal(defaultBr); err != nil { log.Error("Marshal err(%+v)", err) return } configTr := fmt.Sprintf(_formatStr, name, string(content)) configStr = configStr + configTr + _configFlag } return } // ParseSvenConfig ... func (s *Service) ParseSvenConfig(c context.Context, sessionID, projectUrl string) (fileContent, svenConfig string, err error) { var ( content string projectConfigs []string ) if fileContent, err = s.QueryConfigFileContent(c, sessionID); err != nil { return } log.Info("ParseSvenConfig fileContent : %s", fileContent) index := strings.Index(fileContent, _sagaConfigFlag) if index < 0 { log.Warn("ParseSvenConfig not found any config flag: %s", projectUrl) return } content = fileContent[index:] projectConfigs = strings.Split(content, _sagaConfigFlag) for i := 0; i < len(projectConfigs); i++ { if strings.Contains(projectConfigs[i], projectUrl) { svenConfig = projectConfigs[i] return } } return } // ReplaceConfig ... func (s *Service) ReplaceConfig(c context.Context, username, sessionID string, projectInfo *model.ProjectInfo, req *model.ConfigList) (newConfig string, err error) { var ( requestConfig *model.RepoConfig requestConfigStr string fileContent string svenConfig string ) if requestConfig, requestConfigStr, err = s.ParseRequestConfig(projectInfo, req.Configs); err != nil { return } if fileContent, svenConfig, err = s.ParseSvenConfig(c, sessionID, projectInfo.Repo); err != nil { return } if len(svenConfig) <= 0 { return } index := strings.Index(svenConfig, "#") if index > 0 { annotate := svenConfig[index:] requestConfigStr = requestConfigStr + _configFlag + " " + annotate } log.Info("ReplaceConfig requestConfig: %v", requestConfig) log.Info("ReplaceConfig requestConfigStr: %s", requestConfigStr) log.Info("ReplaceConfig svenConfig: %s", svenConfig) newConfig = strings.Replace(fileContent, svenConfig, requestConfigStr, 1) log.Info("ReplaceConfig newConfig: %s", newConfig) return } // ReleaseSagaConfig ... func (s *Service) ReleaseSagaConfig(c context.Context, username, sessionID string, req *model.ConfigList) (resp *model.CommonResp, err error) { var ( configFileName = conf.Conf.Property.Sven.SagaConfigsParam.FileName sagaConfig *model.RepoConfig projectInfo *model.ProjectInfo projectID = req.ProjectID newConfigContent string ) if sagaConfig, err = s.QueryProjectSagaConfig(c, sessionID, req.ProjectID); err != nil || sagaConfig == nil { log.Error("ReleaseSagaConfig err(%+v)", err) return } if projectInfo, err = s.dao.ProjectInfoByID(projectID); err != nil { log.Error("ProjectInfoByID err(%+v)", err) return } projectInfo.Repo = strings.Replace(projectInfo.Repo, "git-test", "git", 1) log.Info("ReleaseSagaConfig query project: %s, sagaConfig: %v", projectInfo.Name, sagaConfig) if sagaConfig.Name == projectInfo.Name { if newConfigContent, err = s.ReplaceConfig(c, username, sessionID, projectInfo, req); err != nil { return } year, month, day := time.Now().Date() monthInt := int(month) hour := time.Now().Hour() updateMark := fmt.Sprintf("%s-%d-%d-%d-%d | from saga-admin", username, year, monthInt, day, hour) newConfigContent = strconv.Quote(newConfigContent)[1 : len(strconv.Quote(newConfigContent))-1] log.Info("ReleaseSagaConfig newConfig: %s", newConfigContent) if _, err = s.UpdateConfig(c, sessionID, username, configFileName, newConfigContent, updateMark, true); err != nil { log.Error("UpdateConfig err(%+v)", err) return } if _, err = s.PublicConfig(c, sessionID, username, configFileName, updateMark, true); err != nil { log.Error("PublicConfig err(%+v)", err) return } } return } // OptionSaga ... func (s *Service) OptionSaga(c context.Context, projectID, sessionID string) (resp []*model.OptionSagaItem, err error) { var sagaConfig *model.RepoConfig projectIDInt, _ := strconv.Atoi(projectID) if sagaConfig, err = s.QueryProjectSagaConfig(c, sessionID, projectIDInt); err != nil || sagaConfig == nil { log.Error("QueryProjectSagaConfig err(%+v)", err) return } t := reflect.TypeOf(sagaConfig) if t.Kind() != reflect.Ptr { log.Info("OptionSaga the object is not a Ptr, but it is : %v", t.Kind()) return } t = reflect.TypeOf(sagaConfig).Elem() if t.Kind() != reflect.Struct { log.Info("OptionSaga the object is not a struct, but it is : %v", t.Kind()) return } v := reflect.ValueOf(sagaConfig).Elem() for i := 0; i < t.NumField(); i++ { f := t.Field(i) if f.Name == _repoURL || f.Name == _repoGroup || f.Name == _repoName || f.Name == _repoLanguage { continue } val := v.Field(i).Interface() log.Info("OptionSaga === %s: %v = %v", f.Name, f.Type, val) sagaItem := &model.OptionSagaItem{} sagaItem.Name = f.Name sagaItem.Value = val sagaItem.CNName = sagaConfigCnName[i] sagaItem.Remark = sagaConfigMark[i] configTr := fmt.Sprintf(`%v`, f.Type) sagaItem.Type = configTr resp = append(resp, sagaItem) } return }