github.com/abayer/test-infra@v0.0.5/prow/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/ioutil" 24 "net/http" 25 "strings" 26 "sync" 27 "time" 28 29 "github.com/sirupsen/logrus" 30 31 "k8s.io/test-infra/prow/config" 32 "k8s.io/test-infra/prow/github" 33 "k8s.io/test-infra/prow/plugins" 34 ) 35 36 // Server implements http.Handler. It validates incoming GitHub webhooks and 37 // then dispatches them to the appropriate plugins. 38 type Server struct { 39 Plugins *plugins.PluginAgent 40 ConfigAgent *config.Agent 41 TokenGenerator func() []byte 42 Metrics *Metrics 43 44 // c is an http client used for dispatching events 45 // to external plugin services. 46 c http.Client 47 // Tracks running handlers for graceful shutdown 48 wg sync.WaitGroup 49 } 50 51 // ServeHTTP validates an incoming webhook and puts it into the event channel. 52 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 53 eventType, eventGUID, payload, ok := ValidateWebhook(w, r, s.TokenGenerator()) 54 if !ok { 55 return 56 } 57 fmt.Fprint(w, "Event received. Have a nice day.") 58 59 if err := s.demuxEvent(eventType, eventGUID, payload, r.Header); err != nil { 60 logrus.WithError(err).Error("Error parsing event.") 61 } 62 } 63 64 // ValidateWebhook ensures that the provided request conforms to the 65 // format of a Github webhook and the payload can be validated with 66 // the provided hmac secret. It returns the event type, the event guid, 67 // the payload of the request, and whether the webhook is valid or not. 68 func ValidateWebhook(w http.ResponseWriter, r *http.Request, hmacSecret []byte) (string, string, []byte, bool) { 69 defer r.Body.Close() 70 71 // Our health check uses GET, so just kick back a 200. 72 if r.Method == http.MethodGet { 73 return "", "", nil, false 74 } 75 76 // Header checks: It must be a POST with an event type and a signature. 77 if r.Method != http.MethodPost { 78 resp := "405 Method not allowed" 79 logrus.Debug(resp) 80 http.Error(w, resp, http.StatusMethodNotAllowed) 81 return "", "", nil, false 82 } 83 eventType := r.Header.Get("X-GitHub-Event") 84 if eventType == "" { 85 resp := "400 Bad Request: Missing X-GitHub-Event Header" 86 logrus.Debug(resp) 87 http.Error(w, resp, http.StatusBadRequest) 88 return "", "", nil, false 89 } 90 eventGUID := r.Header.Get("X-GitHub-Delivery") 91 if eventGUID == "" { 92 resp := "400 Bad Request: Missing X-GitHub-Delivery Header" 93 logrus.Debug(resp) 94 http.Error(w, resp, http.StatusBadRequest) 95 return "", "", nil, false 96 } 97 sig := r.Header.Get("X-Hub-Signature") 98 if sig == "" { 99 resp := "403 Forbidden: Missing X-Hub-Signature" 100 logrus.Debug(resp) 101 http.Error(w, resp, http.StatusForbidden) 102 return "", "", nil, false 103 } 104 contentType := r.Header.Get("content-type") 105 if contentType != "application/json" { 106 resp := "400 Bad Request: Hook only accepts content-type: application/json - please reconfigure this hook on GitHub" 107 logrus.Debug(resp) 108 http.Error(w, resp, http.StatusBadRequest) 109 return "", "", nil, false 110 } 111 112 payload, err := ioutil.ReadAll(r.Body) 113 if err != nil { 114 resp := "500 Internal Server Error: Failed to read request body" 115 logrus.Debug(resp) 116 http.Error(w, resp, http.StatusInternalServerError) 117 return "", "", nil, false 118 } 119 120 // Validate the payload with our HMAC secret. 121 if !github.ValidatePayload(payload, sig, hmacSecret) { 122 resp := "403 Forbidden: Invalid X-Hub-Signature" 123 logrus.Debug(resp) 124 http.Error(w, resp, http.StatusForbidden) 125 return "", "", nil, false 126 } 127 128 return eventType, eventGUID, payload, true 129 } 130 131 func (s *Server) demuxEvent(eventType, eventGUID string, payload []byte, h http.Header) error { 132 l := logrus.WithFields( 133 logrus.Fields{ 134 "event-type": eventType, 135 github.EventGUID: eventGUID, 136 }, 137 ) 138 // We don't want to fail the webhook due to a metrics error. 139 if counter, err := s.Metrics.WebhookCounter.GetMetricWithLabelValues(eventType); err != nil { 140 l.WithError(err).Warn("Failed to get metric for eventType " + eventType) 141 } else { 142 counter.Inc() 143 } 144 var srcRepo string 145 switch eventType { 146 case "issues": 147 var i github.IssueEvent 148 if err := json.Unmarshal(payload, &i); err != nil { 149 return err 150 } 151 i.GUID = eventGUID 152 srcRepo = i.Repo.FullName 153 s.wg.Add(1) 154 go s.handleIssueEvent(l, i) 155 case "issue_comment": 156 var ic github.IssueCommentEvent 157 if err := json.Unmarshal(payload, &ic); err != nil { 158 return err 159 } 160 ic.GUID = eventGUID 161 srcRepo = ic.Repo.FullName 162 s.wg.Add(1) 163 go s.handleIssueCommentEvent(l, ic) 164 case "pull_request": 165 var pr github.PullRequestEvent 166 if err := json.Unmarshal(payload, &pr); err != nil { 167 return err 168 } 169 pr.GUID = eventGUID 170 srcRepo = pr.Repo.FullName 171 s.wg.Add(1) 172 go s.handlePullRequestEvent(l, pr) 173 case "pull_request_review": 174 var re github.ReviewEvent 175 if err := json.Unmarshal(payload, &re); err != nil { 176 return err 177 } 178 re.GUID = eventGUID 179 srcRepo = re.Repo.FullName 180 s.wg.Add(1) 181 go s.handleReviewEvent(l, re) 182 case "pull_request_review_comment": 183 var rce github.ReviewCommentEvent 184 if err := json.Unmarshal(payload, &rce); err != nil { 185 return err 186 } 187 rce.GUID = eventGUID 188 srcRepo = rce.Repo.FullName 189 s.wg.Add(1) 190 go s.handleReviewCommentEvent(l, rce) 191 case "push": 192 var pe github.PushEvent 193 if err := json.Unmarshal(payload, &pe); err != nil { 194 return err 195 } 196 pe.GUID = eventGUID 197 srcRepo = pe.Repo.FullName 198 s.wg.Add(1) 199 go s.handlePushEvent(l, pe) 200 case "status": 201 var se github.StatusEvent 202 if err := json.Unmarshal(payload, &se); err != nil { 203 return err 204 } 205 se.GUID = eventGUID 206 srcRepo = se.Repo.FullName 207 s.wg.Add(1) 208 go s.handleStatusEvent(l, se) 209 } 210 // Demux events only to external plugins that require this event. 211 if external := s.needDemux(eventType, srcRepo); len(external) > 0 { 212 go s.demuxExternal(l, external, payload, h) 213 } 214 return nil 215 } 216 217 // needDemux returns whether there are any external plugins that need to 218 // get the present event. 219 func (s *Server) needDemux(eventType, srcRepo string) []plugins.ExternalPlugin { 220 var matching []plugins.ExternalPlugin 221 srcOrg := strings.Split(srcRepo, "/")[0] 222 223 for repo, plugins := range s.Plugins.Config().ExternalPlugins { 224 // Make sure the repositories match 225 var matchesRepo bool 226 if repo == srcRepo { 227 matchesRepo = true 228 } 229 // If repo is an org, we need to compare orgs. 230 if !matchesRepo && !strings.Contains(repo, "/") && repo == srcOrg { 231 matchesRepo = true 232 } 233 // No need to continue if the repos don't match. 234 if !matchesRepo { 235 continue 236 } 237 238 // Make sure the events match 239 for _, p := range plugins { 240 if len(p.Events) == 0 { 241 matching = append(matching, p) 242 } else { 243 for _, et := range p.Events { 244 if et != eventType { 245 continue 246 } 247 matching = append(matching, p) 248 break 249 } 250 } 251 } 252 } 253 return matching 254 } 255 256 // demuxExternal dispatches the provided payload to the external plugins. 257 func (s *Server) demuxExternal(l *logrus.Entry, externalPlugins []plugins.ExternalPlugin, payload []byte, h http.Header) { 258 h.Set("User-Agent", "ProwHook") 259 for _, p := range externalPlugins { 260 s.wg.Add(1) 261 go func(p plugins.ExternalPlugin) { 262 defer s.wg.Done() 263 if err := s.dispatch(p.Endpoint, payload, h); err != nil { 264 l.WithError(err).WithField("external-plugin", p.Name).Error("Error dispatching event to external plugin.") 265 } else { 266 l.WithField("external-plugin", p.Name).Info("Dispatched event to external plugin") 267 } 268 }(p) 269 } 270 } 271 272 // dispatch creates a new request using the provided payload and headers 273 // and dispatches the request to the provided endpoint. 274 func (s *Server) dispatch(endpoint string, payload []byte, h http.Header) error { 275 req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(payload)) 276 if err != nil { 277 return err 278 } 279 req.Header = h 280 resp, err := s.do(req) 281 if err != nil { 282 return err 283 } 284 defer resp.Body.Close() 285 rb, err := ioutil.ReadAll(resp.Body) 286 if err != nil { 287 return err 288 } 289 if resp.StatusCode < 200 || resp.StatusCode > 299 { 290 return fmt.Errorf("response has status %q and body %q", resp.Status, string(rb)) 291 } 292 return nil 293 } 294 295 // Implements a graceful shutdown protool. Handles all requests sent before receiving shutdown signal. 296 func (s *Server) GracefulShutdown() { 297 s.wg.Wait() // Handle remaining requests 298 return 299 } 300 301 func (s *Server) do(req *http.Request) (*http.Response, error) { 302 var resp *http.Response 303 var err error 304 backoff := 100 * time.Millisecond 305 maxRetries := 5 306 307 for retries := 0; retries < maxRetries; retries++ { 308 resp, err = s.c.Do(req) 309 if err == nil { 310 break 311 } 312 time.Sleep(backoff) 313 backoff *= 2 314 } 315 return resp, err 316 }