diff --git a/workflow-pr/main.go b/workflow-pr/main.go index e78214d..f7de7b1 100644 --- a/workflow-pr/main.go +++ b/workflow-pr/main.go @@ -65,8 +65,6 @@ func updateOrCreatePRBranch(req *common.PullRequestWebhookEvent, git *common.Git } var DebugMode bool -var checkOnStart bool -var checkInterval time.Duration func main() { if err := common.RequireGiteaSecretToken(); err != nil { @@ -80,12 +78,10 @@ func main() { giteaHost := flag.String("gitea", "src.opensuse.org", "Gitea instance") rabbitUrl := flag.String("url", "amqps://rabbit.opensuse.org", "URL for RabbitMQ instance") flag.BoolVar(&DebugMode, "debug", false, "Extra debugging information") - flag.BoolVar(&checkOnStart, "check-on-start", false, "Check all repositories for consistency on start, without delays") + checkOnStart := flag.Bool("check-on-start", false, "Check all repositories for consistency on start, without delays") checkIntervalHours := flag.Float64("check-interval", 5, "Check interval (+-random delay) for repositories for consitency, in hours") flag.Parse() - checkInterval = time.Duration(*checkIntervalHours) * time.Hour - if len(*workflowConfig) == 0 { log.Fatalln("No configuratio file specified. Aborting") } @@ -115,23 +111,20 @@ func main() { } } + gitea := common.AllocateGiteaTransport(*giteaHost) - - checker := &DefaultStateChecker { - gitea: common.AllocateGiteaTransport(*giteaHost), - git: &common.GitHandlerImpl{}, - } req.Synced = &PullRequestSynced{ - gitea: checker.gitea, + gitea: gitea, } req.Opened = &PullRequestOpened{ - gitea: checker.gitea, + gitea: gitea, } req.Closed = &PullRequestClosed{ - gitea: checker.gitea, + gitea: gitea, } - go checker.consistencyCheckProcess(req) + checker := CreateDefaultStateChecker(*checkOnStart, req, gitea, time.Duration(*checkIntervalHours)*time.Hour) + go checker.ConsistencyCheckProcess() var defs common.ListenDefinitions diff --git a/workflow-pr/repo_check.go b/workflow-pr/repo_check.go index 510899c..d5f6f86 100644 --- a/workflow-pr/repo_check.go +++ b/workflow-pr/repo_check.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "log" "math/rand" @@ -11,20 +12,48 @@ import ( "src.opensuse.org/autogits/common" ) +//go:generate mockgen -source=repo_check.go -destination=mock/repo_check.go -typed + type StateChecker interface { + VerifyProjectState(orgName string, configs []*common.AutogitConfig, idx int) error + CheckRepos() error + ConsistencyCheckProcess() error } type DefaultStateChecker struct { - processor *RequestProcessor + exitCheckLoop bool + checkOnStart bool + checkInterval time.Duration - gitea common.Gitea - git common.GitHandlerGenerator + gitea common.Gitea + git common.GitHandlerGenerator + processor *RequestProcessor + i StateChecker } -func (s *DefaultStateChecker) verifyProjectState(processor *RequestProcessor, git *common.GitHandler, orgName string, config *common.AutogitConfig, configs []*common.AutogitConfig) error { +func CreateDefaultStateChecker(checkOnStart bool, processor *RequestProcessor, gitea common.Gitea, interval time.Duration) *DefaultStateChecker { + var s = &DefaultStateChecker{ + git: &common.GitHandlerImpl{}, + gitea: gitea, + checkInterval: interval, + checkOnStart: checkOnStart, + processor: processor, + } + s.i = s + return s +} + +func (s *DefaultStateChecker) VerifyProjectState(orgName string, configs []*common.AutogitConfig, idx int) error { org := common.Organization{ Username: orgName, } + + git, err := s.git.CreateGitHandler(GitAuthor, GitEmail, AppName) + if err != nil { + return fmt.Errorf("Cannot create git handler: %w", err) + } + + config := configs[idx] repo, err := s.gitea.CreateRepositoryIfNotExist(git, org, config.GitProjectName) if err != nil { return fmt.Errorf("Error fetching or creating '%s/%s' -- aborting verifyProjectState(). Err: %w", orgName, config.GitProjectName, err) @@ -120,43 +149,45 @@ nextSubmodule: return nil } -func (s *DefaultStateChecker) checkRepos(processor *RequestProcessor) { - for org, configs := range processor.configuredRepos { - for _, config := range configs { - if checkInterval > 0 { - sleepInterval := checkInterval - checkInterval/2 + time.Duration(rand.Int63n(int64(checkInterval))) +func (s *DefaultStateChecker) CheckRepos() error { + errorList := make([]error, 0, 10) + + for org, configs := range s.processor.configuredRepos { + for configIdx, config := range configs { + if s.checkInterval > 0 { + sleepInterval := (s.checkInterval - s.checkInterval/2) + time.Duration(rand.Int63n(int64(s.checkInterval))) log.Println(" - sleep interval", sleepInterval, "until next check") time.Sleep(sleepInterval) } log.Printf(" ++ starting verification, org: `%s` config: `%s`\n", org, config.GitProjectName) - git, err := s.git.CreateGitHandler(GitAuthor, GitEmail, AppName) - if err != nil { - log.Println("Faield to allocate GitHandler:", err) - return - } - if !DebugMode { - defer git.Close() - } - if err := s.verifyProjectState(processor, git, org, config, configs); err != nil { + if err := s.i.VerifyProjectState(org, configs, configIdx); err != nil { log.Printf(" *** verification failed, org: `%s`, err: %#v\n", org, err) + errorList = append(errorList, err) } log.Printf(" ++ verification complete, org: `%s` config: `%s`\n", org, config.GitProjectName) } } + + return errors.Join(errorList...) } -func (s *DefaultStateChecker) consistencyCheckProcess(processor *RequestProcessor) { - if checkOnStart { - savedCheckInterval := checkInterval - checkInterval = 0 +func (s *DefaultStateChecker) ConsistencyCheckProcess() error { + if s.checkOnStart { + savedCheckInterval := s.checkInterval + s.checkInterval = 0 log.Println("== Startup consistency check begin...") - s.checkRepos(processor) + s.i.CheckRepos() log.Println("== Startup consistency check done...") - checkInterval = savedCheckInterval + s.checkInterval = savedCheckInterval } for { - s.checkRepos(processor) + if s.exitCheckLoop { + break + } + s.i.CheckRepos() } + + return nil } diff --git a/workflow-pr/repo_check_test.go b/workflow-pr/repo_check_test.go new file mode 100644 index 0000000..e2ccd31 --- /dev/null +++ b/workflow-pr/repo_check_test.go @@ -0,0 +1,137 @@ +package main + +import ( + "bytes" + "errors" + "log" + "testing" + + "go.uber.org/mock/gomock" + "src.opensuse.org/autogits/common" + mock_common "src.opensuse.org/autogits/common/mock" + mock_main "src.opensuse.org/workflow-pr/mock" +) + +func TestRepoCheck(t *testing.T) { + var logBuf bytes.Buffer + oldOut := log.Writer() + log.SetOutput(&logBuf) + defer log.SetOutput(oldOut) + + t.Run("Consistency Check On Start", func(t *testing.T) { + c := CreateDefaultStateChecker(true, nil, nil, 100) + ctl := gomock.NewController(t) + state := mock_main.NewMockStateChecker(ctl) + c.i = state + state.EXPECT().CheckRepos().Do(func() error { + // only checkOnStart has checkInterval = 0 + if c.checkInterval != 0 { + t.Fail() + } + + c.exitCheckLoop = true + return nil + }) + + c.ConsistencyCheckProcess() + if c.checkInterval != 100 { + t.Fail() + } + }) + + t.Run("No consistency Check On Start", func(t *testing.T) { + c := CreateDefaultStateChecker(true, nil, nil, 100) + ctl := gomock.NewController(t) + state := mock_main.NewMockStateChecker(ctl) + c.i = state + + nCalls := 10 + state.EXPECT().CheckRepos().Do(func() error { + // only checkOnStart has checkInterval = 0 + if c.checkInterval != 100 { + t.Fail() + } + + nCalls-- + if nCalls == 0 { + c.exitCheckLoop = true + } + return nil + }).Times(nCalls) + c.checkOnStart = false + + c.ConsistencyCheckProcess() + }) + + t.Run("CheckRepos() calls CheckProjectState() for each project", func(t *testing.T) { + ctl := gomock.NewController(t) + state := mock_main.NewMockStateChecker(ctl) + gitea := mock_common.NewMockGitea(ctl) + + config1 := &common.AutogitConfig{ + GitProjectName: "git_repo1", + Organization: "repo1_org", + } + config2 := &common.AutogitConfig{ + GitProjectName: "git_repo2", + Organization: "repo2_org", + } + config3 := &common.AutogitConfig{ + GitProjectName: "git_repo3", + Organization: "repo3_org", + } + + configs := &RequestProcessor{ + configuredRepos: map[string][]*common.AutogitConfig{ + "repo1_org": []*common.AutogitConfig{config1}, + "repo2_org": []*common.AutogitConfig{config2}, + "repo3_org": []*common.AutogitConfig{config3}, + }, + } + r := configs.configuredRepos + + c := CreateDefaultStateChecker(true, configs, gitea, 100) + c.i = state + + state.EXPECT().VerifyProjectState("repo1_org", r["repo1_org"], 0) + state.EXPECT().VerifyProjectState("repo2_org", r["repo2_org"], 0) + state.EXPECT().VerifyProjectState("repo3_org", r["repo3_org"], 0) + + if err := c.CheckRepos(); err != nil { + t.Error(err) + } + }) + + t.Run("CheckRepos errors", func(t *testing.T) { + ctl := gomock.NewController(t) + state := mock_main.NewMockStateChecker(ctl) + gitea := mock_common.NewMockGitea(ctl) + git := mock_common.NewMockGitHandlerGenerator(ctl) + + config1 := &common.AutogitConfig{ + GitProjectName: "git_repo1", + Organization: "repo1_org", + } + + configs := &RequestProcessor{ + configuredRepos: map[string][]*common.AutogitConfig{ + "repo1_org": []*common.AutogitConfig{config1}, + }, + } + //r := configs.configuredRepos + + c := CreateDefaultStateChecker(true, configs, gitea, 100) + c.i = state + c.git = git + + err := errors.New("test error") + state.EXPECT().VerifyProjectState("repo1_org", gomock.Any(), 0).Return(err) + + r := c.CheckRepos() + + if !errors.Is(r, err) { + t.Error(err) + } + }) + +}