github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/hook/server.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 hook 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "io" 24 "net/http" 25 "strconv" 26 "strings" 27 "sync" 28 "time" 29 30 "github.com/sirupsen/logrus" 31 32 "sigs.k8s.io/prow/pkg/config" 33 "sigs.k8s.io/prow/pkg/github" 34 "sigs.k8s.io/prow/pkg/githubeventserver" 35 _ "sigs.k8s.io/prow/pkg/hook/plugin-imports" 36 "sigs.k8s.io/prow/pkg/plugins" 37 ) 38 39 // Server implements http.Handler. It validates incoming GitHub webhooks and 40 // then dispatches them to the appropriate plugins. 41 type Server struct { 42 ClientAgent *plugins.ClientAgent 43 Plugins *plugins.ConfigAgent 44 ConfigAgent *config.Agent 45 TokenGenerator func() []byte 46 Metrics *githubeventserver.Metrics 47 RepoEnabled func(org, repo string) bool 48 49 // c is an http client used for dispatching events 50 // to external plugin services. 51 c http.Client 52 // Tracks running handlers for graceful shutdown 53 wg sync.WaitGroup 54 } 55 56 // ServeHTTP validates an incoming webhook and puts it into the event channel. 57 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 58 eventType, eventGUID, payload, ok, resp := github.ValidateWebhook(w, r, s.TokenGenerator) 59 if counter, err := s.Metrics.ResponseCounter.GetMetricWithLabelValues(strconv.Itoa(resp)); err != nil { 60 logrus.WithFields(logrus.Fields{ 61 "status-code": resp, 62 }).WithError(err).Error("Failed to get metric for reporting webhook status code") 63 } else { 64 counter.Inc() 65 } 66 67 if !ok { 68 return 69 } 70 fmt.Fprint(w, "Event received. Have a nice day.") 71 72 if err := s.demuxEvent(eventType, eventGUID, payload, r.Header); err != nil { 73 logrus.WithError(err).Error("Error parsing event.") 74 } 75 } 76 77 func (s *Server) demuxEvent(eventType, eventGUID string, payload []byte, h http.Header) error { 78 l := logrus.WithFields( 79 logrus.Fields{ 80 eventTypeField: eventType, 81 github.EventGUID: eventGUID, 82 }, 83 ) 84 // We don't want to fail the webhook due to a metrics error. 85 if counter, err := s.Metrics.WebhookCounter.GetMetricWithLabelValues(eventType); err != nil { 86 l.WithError(err).Warn("Failed to get metric for eventType " + eventType) 87 } else { 88 counter.Inc() 89 } 90 var srcRepo string 91 switch eventType { 92 case "issues": 93 var i github.IssueEvent 94 if err := json.Unmarshal(payload, &i); err != nil { 95 return err 96 } 97 i.GUID = eventGUID 98 srcRepo = i.Repo.FullName 99 if s.RepoEnabled(i.Repo.Owner.Login, i.Repo.Name) { 100 s.wg.Add(1) 101 go s.handleIssueEvent(l, i) 102 } 103 case "issue_comment": 104 var ic github.IssueCommentEvent 105 if err := json.Unmarshal(payload, &ic); err != nil { 106 return err 107 } 108 ic.GUID = eventGUID 109 srcRepo = ic.Repo.FullName 110 if s.RepoEnabled(ic.Repo.Owner.Login, ic.Repo.Name) { 111 s.wg.Add(1) 112 go s.handleIssueCommentEvent(l, ic) 113 } 114 case "pull_request": 115 var pr github.PullRequestEvent 116 if err := json.Unmarshal(payload, &pr); err != nil { 117 return err 118 } 119 pr.GUID = eventGUID 120 srcRepo = pr.Repo.FullName 121 if s.RepoEnabled(pr.Repo.Owner.Login, pr.Repo.Name) { 122 s.wg.Add(1) 123 go s.handlePullRequestEvent(l, pr) 124 } 125 case "pull_request_review": 126 var re github.ReviewEvent 127 if err := json.Unmarshal(payload, &re); err != nil { 128 return err 129 } 130 re.GUID = eventGUID 131 srcRepo = re.Repo.FullName 132 if s.RepoEnabled(re.Repo.Owner.Login, re.Repo.Name) { 133 s.wg.Add(1) 134 go s.handleReviewEvent(l, re) 135 } 136 case "pull_request_review_comment": 137 var rce github.ReviewCommentEvent 138 if err := json.Unmarshal(payload, &rce); err != nil { 139 return err 140 } 141 rce.GUID = eventGUID 142 srcRepo = rce.Repo.FullName 143 if s.RepoEnabled(rce.Repo.Owner.Login, rce.Repo.Name) { 144 s.wg.Add(1) 145 go s.handleReviewCommentEvent(l, rce) 146 } 147 case "push": 148 var pe github.PushEvent 149 if err := json.Unmarshal(payload, &pe); err != nil { 150 return err 151 } 152 pe.GUID = eventGUID 153 srcRepo = pe.Repo.FullName 154 if s.RepoEnabled(pe.Repo.Owner.Login, pe.Repo.Name) { 155 s.wg.Add(1) 156 go s.handlePushEvent(l, pe) 157 } 158 case "status": 159 var se github.StatusEvent 160 if err := json.Unmarshal(payload, &se); err != nil { 161 return err 162 } 163 se.GUID = eventGUID 164 srcRepo = se.Repo.FullName 165 if s.RepoEnabled(se.Repo.Owner.Login, se.Repo.Name) { 166 s.wg.Add(1) 167 go s.handleStatusEvent(l, se) 168 } 169 default: 170 var ge github.GenericEvent 171 if err := json.Unmarshal(payload, &ge); err != nil { 172 return err 173 } 174 srcRepo = ge.Repo.FullName 175 l.Debug("Ignoring unhandled event type. (Might still be handled by external plugins.)") 176 } 177 // Demux events only to external plugins that require this event. 178 if external := s.needDemux(eventType, srcRepo); len(external) > 0 { 179 s.wg.Add(1) 180 go s.demuxExternal(l, external, payload, h) 181 } 182 return nil 183 } 184 185 // needDemux returns whether there are any external plugins that need to 186 // get the present event. 187 func (s *Server) needDemux(eventType, orgRepo string) []plugins.ExternalPlugin { 188 var matching []plugins.ExternalPlugin 189 split := strings.Split(orgRepo, "/") 190 srcOrg := split[0] 191 var srcRepo string 192 if len(split) > 1 { 193 srcRepo = split[1] 194 } 195 if !s.RepoEnabled(srcOrg, srcRepo) { 196 return nil 197 } 198 199 for repo, plugins := range s.Plugins.Config().ExternalPlugins { 200 // Make sure the repositories match 201 if repo != orgRepo && repo != srcOrg { 202 continue 203 } 204 205 // Make sure the events match 206 for _, p := range plugins { 207 if len(p.Events) == 0 { 208 matching = append(matching, p) 209 } else { 210 for _, et := range p.Events { 211 if et != eventType { 212 continue 213 } 214 matching = append(matching, p) 215 break 216 } 217 } 218 } 219 } 220 return matching 221 } 222 223 // demuxExternal dispatches the provided payload to the external plugins. 224 func (s *Server) demuxExternal(l *logrus.Entry, externalPlugins []plugins.ExternalPlugin, payload []byte, h http.Header) { 225 defer s.wg.Done() 226 h.Set("User-Agent", "ProwHook") 227 for _, p := range externalPlugins { 228 s.wg.Add(1) 229 go func(p plugins.ExternalPlugin) { 230 defer s.wg.Done() 231 if err := s.dispatch(p.Endpoint, payload, h); err != nil { 232 l.WithError(err).WithField("external-plugin", p.Name).Error("Error dispatching event to external plugin.") 233 } else { 234 l.WithField("external-plugin", p.Name).Info("Dispatched event to external plugin") 235 } 236 }(p) 237 } 238 } 239 240 // dispatch creates a new request using the provided payload and headers 241 // and dispatches the request to the provided endpoint. 242 func (s *Server) dispatch(endpoint string, payload []byte, h http.Header) error { 243 req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(payload)) 244 if err != nil { 245 return err 246 } 247 req.Header = h 248 resp, err := s.do(req) 249 if err != nil { 250 return err 251 } 252 defer resp.Body.Close() 253 rb, err := io.ReadAll(resp.Body) 254 if err != nil { 255 return err 256 } 257 if resp.StatusCode < 200 || resp.StatusCode > 299 { 258 return fmt.Errorf("response has status %q and body %q", resp.Status, string(rb)) 259 } 260 return nil 261 } 262 263 // GracefulShutdown implements a graceful shutdown protocol. It handles all requests sent before 264 // receiving the shutdown signal. 265 func (s *Server) GracefulShutdown() { 266 s.wg.Wait() // Handle remaining requests 267 } 268 269 func (s *Server) do(req *http.Request) (*http.Response, error) { 270 var resp *http.Response 271 var err error 272 backoff := 100 * time.Millisecond 273 maxRetries := 5 274 275 for retries := 0; retries < maxRetries; retries++ { 276 resp, err = s.c.Do(req) 277 if err == nil { 278 break 279 } 280 time.Sleep(backoff) 281 backoff *= 2 282 } 283 return resp, err 284 }