github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/plugins/override/override.go (about) 1 /* 2 Copyright 2018 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 override supports the /override context command. 18 package override 19 20 import ( 21 "fmt" 22 "regexp" 23 "strings" 24 25 "github.com/sirupsen/logrus" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/util/sets" 28 29 "k8s.io/test-infra/prow/config" 30 "k8s.io/test-infra/prow/github" 31 "k8s.io/test-infra/prow/kube" 32 "k8s.io/test-infra/prow/pjutil" 33 "k8s.io/test-infra/prow/pluginhelp" 34 "k8s.io/test-infra/prow/plugins" 35 ) 36 37 const pluginName = "override" 38 39 var ( 40 overrideRe = regexp.MustCompile(`(?mi)^/override (.+?)\s*$`) 41 ) 42 43 type githubClient interface { 44 CreateComment(owner, repo string, number int, comment string) error 45 CreateStatus(org, repo, ref string, s github.Status) error 46 GetPullRequest(org, repo string, number int) (*github.PullRequest, error) 47 GetRef(org, repo, ref string) (string, error) 48 HasPermission(org, repo, user string, role ...string) (bool, error) 49 ListStatuses(org, repo, ref string) ([]github.Status, error) 50 } 51 52 type kubeClient interface { 53 CreateProwJob(kube.ProwJob) (kube.ProwJob, error) 54 } 55 56 type overrideClient interface { 57 githubClient 58 kubeClient 59 presubmitForContext(org, repo, context string) *config.Presubmit 60 } 61 62 type client struct { 63 gc githubClient 64 jc config.JobConfig 65 kc kubeClient 66 } 67 68 func (c client) CreateComment(owner, repo string, number int, comment string) error { 69 return c.gc.CreateComment(owner, repo, number, comment) 70 } 71 func (c client) CreateStatus(org, repo, ref string, s github.Status) error { 72 return c.gc.CreateStatus(org, repo, ref, s) 73 } 74 75 func (c client) GetRef(org, repo, ref string) (string, error) { 76 return c.gc.GetRef(org, repo, ref) 77 } 78 79 func (c client) GetPullRequest(org, repo string, number int) (*github.PullRequest, error) { 80 return c.gc.GetPullRequest(org, repo, number) 81 } 82 func (c client) ListStatuses(org, repo, ref string) ([]github.Status, error) { 83 return c.gc.ListStatuses(org, repo, ref) 84 } 85 func (c client) HasPermission(org, repo, user string, role ...string) (bool, error) { 86 return c.gc.HasPermission(org, repo, user, role...) 87 } 88 89 func (c client) CreateProwJob(pj kube.ProwJob) (kube.ProwJob, error) { 90 return c.kc.CreateProwJob(pj) 91 } 92 93 func (c client) presubmitForContext(org, repo, context string) *config.Presubmit { 94 for _, p := range c.jc.AllPresubmits([]string{org + "/" + repo}) { 95 if p.Context == context { 96 return &p 97 } 98 } 99 return nil 100 } 101 102 func init() { 103 plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment, helpProvider) 104 } 105 106 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) { 107 pluginHelp := &pluginhelp.PluginHelp{ 108 Description: "The override plugin allows repo admins to force a github status context to pass", 109 } 110 pluginHelp.AddCommand(pluginhelp.Command{ 111 Usage: "/override [context]", 112 Description: "Forces a github status context to green (one per line).", 113 Featured: false, 114 WhoCanUse: "Repo administrators", 115 Examples: []string{"/override pull-repo-whatever", "/override ci/circleci", "/override deleted-job"}, 116 }) 117 return pluginHelp, nil 118 } 119 120 func handleGenericComment(pc plugins.Agent, e github.GenericCommentEvent) error { 121 c := client{ 122 gc: pc.GitHubClient, 123 jc: pc.Config.JobConfig, 124 kc: pc.KubeClient, 125 } 126 return handle(c, pc.Logger, &e) 127 } 128 129 func authorized(gc githubClient, log *logrus.Entry, org, repo, user string) bool { 130 ok, err := gc.HasPermission(org, repo, user, github.RoleAdmin) 131 if err != nil { 132 log.WithError(err).Warnf("cannot determine whether %s is an admin of %s/%s", user, org, repo) 133 return false 134 } 135 return ok 136 } 137 138 func description(user string) string { 139 return fmt.Sprintf("Overridden by %s", user) 140 } 141 142 func handle(oc overrideClient, log *logrus.Entry, e *github.GenericCommentEvent) error { 143 144 if !e.IsPR || e.IssueState != "open" || e.Action != github.GenericCommentActionCreated { 145 return nil 146 } 147 148 mat := overrideRe.FindAllStringSubmatch(e.Body, -1) 149 if len(mat) == 0 { 150 return nil 151 } 152 153 overrides := sets.String{} 154 155 for _, m := range mat { 156 overrides.Insert(m[1]) 157 } 158 159 org := e.Repo.Owner.Login 160 repo := e.Repo.Name 161 number := e.Number 162 user := e.User.Login 163 164 if !authorized(oc, log, org, repo, user) { 165 resp := fmt.Sprintf("%s unauthorized: /override is restricted to repo administrators", user) 166 log.Warn(resp) 167 return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp)) 168 } 169 170 pr, err := oc.GetPullRequest(org, repo, number) 171 if err != nil { 172 resp := fmt.Sprintf("Cannot get PR #%d in %s/%s", number, org, repo) 173 log.WithError(err).Warn(resp) 174 return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp)) 175 } 176 177 sha := pr.Head.SHA 178 statuses, err := oc.ListStatuses(org, repo, sha) 179 if err != nil { 180 resp := fmt.Sprintf("Cannot get commit statuses for PR #%d in %s/%s", number, org, repo) 181 log.WithError(err).Warn(resp) 182 return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp)) 183 } 184 185 done := sets.String{} 186 187 defer func() { 188 if len(done) == 0 { 189 return 190 } 191 msg := fmt.Sprintf("Overrode contexts on behalf of %s: %s", user, strings.Join(done.List(), ", ")) 192 log.Info(msg) 193 oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, msg)) 194 }() 195 196 for _, status := range statuses { 197 if status.State == github.StatusSuccess || !overrides.Has(status.Context) { 198 continue 199 } 200 // First create the overridden prow result if necessary 201 if pre := oc.presubmitForContext(org, repo, status.Context); pre != nil { 202 baseSHA, err := oc.GetRef(org, repo, "heads/"+pr.Base.Ref) 203 if err != nil { 204 resp := fmt.Sprintf("Cannot get base ref of PR") 205 log.WithError(err).Warn(resp) 206 return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp)) 207 } 208 209 pj := pjutil.NewPresubmit(*pr, baseSHA, *pre, e.GUID) 210 now := metav1.Now() 211 pj.Status = kube.ProwJobStatus{ 212 StartTime: now, 213 CompletionTime: &now, 214 State: kube.SuccessState, 215 Description: description(user), 216 URL: e.HTMLURL, 217 } 218 log.WithFields(pjutil.ProwJobFields(&pj)).Info("Creating a new prowjob.") 219 if _, err := oc.CreateProwJob(pj); err != nil { 220 resp := fmt.Sprintf("Failed to create override job for %s", status.Context) 221 log.WithError(err).Warn(resp) 222 return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp)) 223 } 224 } 225 status.State = github.StatusSuccess 226 status.Description = description(user) 227 if err := oc.CreateStatus(org, repo, sha, status); err != nil { 228 resp := fmt.Sprintf("Cannot update PR status for context %s", status.Context) 229 log.WithError(err).Warn(resp) 230 return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp)) 231 } 232 done.Insert(status.Context) 233 } 234 return nil 235 }