github.com/abayer/test-infra@v0.0.5/prow/plugins/trigger/trigger.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package trigger 18 19 import ( 20 "fmt" 21 "strings" 22 23 "github.com/sirupsen/logrus" 24 25 "k8s.io/apimachinery/pkg/util/sets" 26 27 "k8s.io/test-infra/prow/config" 28 "k8s.io/test-infra/prow/github" 29 "k8s.io/test-infra/prow/kube" 30 "k8s.io/test-infra/prow/pjutil" 31 "k8s.io/test-infra/prow/pluginhelp" 32 "k8s.io/test-infra/prow/plugins" 33 ) 34 35 const ( 36 pluginName = "trigger" 37 lgtmLabel = "lgtm" 38 ) 39 40 func init() { 41 plugins.RegisterIssueCommentHandler(pluginName, handleIssueComment, helpProvider) 42 plugins.RegisterPullRequestHandler(pluginName, handlePullRequest, helpProvider) 43 plugins.RegisterPushEventHandler(pluginName, handlePush, helpProvider) 44 } 45 46 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) { 47 configInfo := map[string]string{} 48 for _, orgRepo := range enabledRepos { 49 parts := strings.Split(orgRepo, "/") 50 if len(parts) != 2 { 51 return nil, fmt.Errorf("invalid repo in enabledRepos: %q", orgRepo) 52 } 53 org, repoName := parts[0], parts[1] 54 if trigger := config.TriggerFor(org, repoName); trigger != nil && trigger.TrustedOrg != "" { 55 org = trigger.TrustedOrg 56 } 57 configInfo[orgRepo] = fmt.Sprintf("The trusted Github organization for this repository is %q.", org) 58 } 59 pluginHelp := &pluginhelp.PluginHelp{ 60 Description: `The trigger plugin starts tests in reaction to commands and pull request events. It is responsible for ensuring that test jobs are only run on trusted PRs. A PR is considered trusted if the author is a member of the 'trusted organization' for the repository or if such a member has left an '/ok-to-test' command on the PR. 61 <br>Trigger starts jobs automatically when a new trusted PR is created or when an untrusted PR becomes trusted, but it can also be used to start jobs manually via the '/test' command. 62 <br>The '/retest' command can be used to rerun jobs that have reported failure.`, 63 Config: configInfo, 64 } 65 pluginHelp.AddCommand(pluginhelp.Command{ 66 Usage: "/ok-to-test", 67 Description: "Marks a PR as 'trusted' and starts tests.", 68 Featured: false, 69 WhoCanUse: "Members of the trusted organization for the repo.", 70 Examples: []string{"/ok-to-test"}, 71 }) 72 pluginHelp.AddCommand(pluginhelp.Command{ 73 Usage: "/test (<job name>|all)", 74 Description: "Manually starts a/all test job(s).", 75 Featured: true, 76 WhoCanUse: "Anyone can trigger this command on a trusted PR.", 77 Examples: []string{"/test all", "/test pull-bazel-test"}, 78 }) 79 pluginHelp.AddCommand(pluginhelp.Command{ 80 Usage: "/retest", 81 Description: "Rerun test jobs that have failed.", 82 Featured: true, 83 WhoCanUse: "Anyone can trigger this command on a trusted PR.", 84 Examples: []string{"/retest"}, 85 }) 86 return pluginHelp, nil 87 } 88 89 type githubClient interface { 90 AddLabel(org, repo string, number int, label string) error 91 BotName() (string, error) 92 IsCollaborator(org, repo, user string) (bool, error) 93 IsMember(org, user string) (bool, error) 94 GetPullRequest(org, repo string, number int) (*github.PullRequest, error) 95 GetRef(org, repo, ref string) (string, error) 96 CreateComment(owner, repo string, number int, comment string) error 97 ListIssueComments(owner, repo string, issue int) ([]github.IssueComment, error) 98 CreateStatus(owner, repo, ref string, status github.Status) error 99 GetCombinedStatus(org, repo, ref string) (*github.CombinedStatus, error) 100 GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error) 101 RemoveLabel(org, repo string, number int, label string) error 102 DeleteStaleComments(org, repo string, number int, comments []github.IssueComment, isStale func(github.IssueComment) bool) error 103 } 104 105 type kubeClient interface { 106 CreateProwJob(kube.ProwJob) (kube.ProwJob, error) 107 } 108 109 type client struct { 110 GitHubClient githubClient 111 KubeClient kubeClient 112 Config *config.Config 113 Logger *logrus.Entry 114 } 115 116 func getClient(pc plugins.PluginClient) client { 117 return client{ 118 GitHubClient: pc.GitHubClient, 119 Config: pc.Config, 120 KubeClient: pc.KubeClient, 121 Logger: pc.Logger, 122 } 123 } 124 125 func handlePullRequest(pc plugins.PluginClient, pr github.PullRequestEvent) error { 126 org, repo, _ := orgRepoAuthor(pr.PullRequest) 127 return handlePR(getClient(pc), pc.PluginConfig.TriggerFor(org, repo), pr) 128 } 129 130 func handleIssueComment(pc plugins.PluginClient, ic github.IssueCommentEvent) error { 131 return handleIC(getClient(pc), pc.PluginConfig.TriggerFor(ic.Repo.Owner.Login, ic.Repo.Name), ic) 132 } 133 134 func handlePush(pc plugins.PluginClient, pe github.PushEvent) error { 135 return handlePE(getClient(pc), pe) 136 } 137 138 // trustedUser returns true if user is trusted in repo. 139 // 140 // Trusted users are either repo collaborators, org members or trusted org members. 141 // Whether repo collaborators and/or a second org is trusted is configured by trigger. 142 func trustedUser(ghc githubClient, trigger *plugins.Trigger, user, org, repo string) (bool, error) { 143 // First check if user is a collaborator, assuming this is allowed 144 allowCollaborators := trigger == nil || !trigger.OnlyOrgMembers 145 if allowCollaborators { 146 if ok, err := ghc.IsCollaborator(org, repo, user); err != nil { 147 return false, fmt.Errorf("error in IsCollaborator: %v", err) 148 } else if ok { 149 return true, nil 150 } 151 } 152 153 // TODO(fejta): consider dropping support for org checks in the future. 154 155 // Next see if the user is an org member 156 if member, err := ghc.IsMember(org, user); err != nil { 157 return false, fmt.Errorf("error in IsMember(%s): %v", org, err) 158 } else if member { 159 return true, nil 160 } 161 162 // Determine if there is a second org to check 163 if trigger == nil || trigger.TrustedOrg == "" || trigger.TrustedOrg == org { 164 return false, nil // No trusted org and/or it is the same 165 } 166 167 // Check the second trusted org. 168 member, err := ghc.IsMember(trigger.TrustedOrg, user) 169 if err != nil { 170 return false, fmt.Errorf("error in IsMember(%s): %v", trigger.TrustedOrg, err) 171 } 172 return member, nil 173 } 174 175 func fileChangesGetter(ghc githubClient, org, repo string, num int) func() ([]string, error) { 176 var changedFiles []string 177 return func() ([]string, error) { 178 // Fetch the changed files from github at most once. 179 if changedFiles == nil { 180 changes, err := ghc.GetPullRequestChanges(org, repo, num) 181 if err != nil { 182 return nil, fmt.Errorf("error getting pull request changes: %v", err) 183 } 184 changedFiles = []string{} 185 for _, change := range changes { 186 changedFiles = append(changedFiles, change.Filename) 187 } 188 } 189 return changedFiles, nil 190 } 191 } 192 193 func allContexts(parent config.Presubmit) []string { 194 contexts := []string{parent.Context} 195 for _, child := range parent.RunAfterSuccess { 196 contexts = append(contexts, allContexts(child)...) 197 } 198 return contexts 199 } 200 201 func runOrSkipRequested(c client, pr *github.PullRequest, requestedJobs []config.Presubmit, forceRunContexts map[string]bool, body, eventGUID string) error { 202 org := pr.Base.Repo.Owner.Login 203 repo := pr.Base.Repo.Name 204 number := pr.Number 205 206 baseSHA, err := c.GitHubClient.GetRef(org, repo, "heads/"+pr.Base.Ref) 207 if err != nil { 208 return err 209 } 210 211 // Use a closure to lazily retrieve the file changes only if they are needed. 212 // We only have to fetch the changes if there is at least one RunIfChanged 213 // job that is not being force run (due to a `/retest` after a failure or 214 // because it is explicitly triggered with `/test foo`). 215 getChanges := fileChangesGetter(c.GitHubClient, org, repo, number) 216 // shouldRun indicates if a job should actually run. 217 shouldRun := func(j config.Presubmit) (bool, error) { 218 if !j.RunsAgainstBranch(pr.Base.Ref) { 219 return false, nil 220 } 221 if j.RunIfChanged == "" || forceRunContexts[j.Context] || j.TriggerMatches(body) { 222 return true, nil 223 } 224 changes, err := getChanges() 225 if err != nil { 226 return false, err 227 } 228 return j.RunsAgainstChanges(changes), nil 229 } 230 231 // For each job determine if any sharded version of the job runs. 232 // This in turn determines which jobs to run and which contexts to mark as "Skipped". 233 // 234 // Note: Job sharding is achieved with presubmit configurations that overlap on 235 // name, but run under disjoint circumstances. For example, a job 'foo' can be 236 // sharded to have different pod specs for different branches by 237 // creating 2 presubmit configurations with the name foo, but different pod 238 // specs, and specifying different branches for each job. 239 var toRunJobs []config.Presubmit 240 toRun := sets.NewString() 241 toSkip := sets.NewString() 242 for _, job := range requestedJobs { 243 runs, err := shouldRun(job) 244 if err != nil { 245 return err 246 } 247 if runs { 248 toRunJobs = append(toRunJobs, job) 249 toRun.Insert(job.Context) 250 } else if !job.SkipReport { 251 // we need to post context statuses for all jobs; if a job is slated to 252 // run after the success of a parent job that is skipped, it must be 253 // skipped as well 254 toSkip.Insert(allContexts(job)...) 255 } 256 } 257 // 'Skip' any context that is requested, but doesn't have any job shards that 258 // will run. 259 for _, context := range toSkip.Difference(toRun).List() { 260 if err := c.GitHubClient.CreateStatus(org, repo, pr.Head.SHA, github.Status{ 261 State: github.StatusSuccess, 262 Context: context, 263 Description: "Skipped", 264 }); err != nil { 265 return err 266 } 267 } 268 269 var errors []error 270 for _, job := range toRunJobs { 271 c.Logger.Infof("Starting %s build.", job.Name) 272 pj := pjutil.NewPresubmit(*pr, baseSHA, job, eventGUID) 273 c.Logger.WithFields(pjutil.ProwJobFields(&pj)).Info("Creating a new prowjob.") 274 if _, err := c.KubeClient.CreateProwJob(pj); err != nil { 275 errors = append(errors, err) 276 } 277 } 278 if len(errors) > 0 { 279 return fmt.Errorf("errors starting jobs: %v", errors) 280 } 281 return nil 282 }