sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/githubeventserver/githubeventserver.go (about) 1 /* 2 Copyright 2020 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 githubeventserver 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "fmt" 24 "io" 25 "net/http" 26 "strconv" 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/pluginhelp" 35 pluginhelp_externalplugins "sigs.k8s.io/prow/pkg/pluginhelp/externalplugins" 36 pluginhelp_hook "sigs.k8s.io/prow/pkg/pluginhelp/hook" 37 "sigs.k8s.io/prow/pkg/plugins" 38 ) 39 40 const ( 41 eventTypeField = "event-type" 42 43 statusEvent = "status" 44 pushEvent = "push" 45 pullRequestReviewCommentEvent = "pull_request_review_comment" 46 pullRequestReviewEvent = "pull_request_review" 47 pullRequestEvent = "pull_request" 48 issueCommentEvent = "issue_comment" 49 issuesEvent = "issues" 50 workflowRunEvent = "workflow_run" 51 registryPackageEvent = "registry_package" 52 ) 53 54 // GitHubEventServer hold all the information needed for the 55 // github event server implementation. 56 type GitHubEventServer struct { 57 wg *sync.WaitGroup 58 59 serveMuxHandler *serveMuxHandler 60 httpServeMux *http.ServeMux 61 62 httpServer *http.Server 63 } 64 65 // New creates a new GitHubEventServer from the given arguments. 66 // It also assigns the serveMuxHandler in the http.ServeMux. 67 func New(o Options, hmacTokenGenerator func() []byte, logger *logrus.Entry) *GitHubEventServer { 68 var wg sync.WaitGroup 69 70 githubEventServer := &GitHubEventServer{ 71 wg: &wg, 72 serveMuxHandler: &serveMuxHandler{ 73 hmacTokenGenerator: hmacTokenGenerator, 74 log: logger, 75 metrics: o.Metrics, 76 wg: &wg, 77 }, 78 } 79 80 httpServeMux := http.NewServeMux() 81 httpServeMux.Handle(o.endpoint, githubEventServer.serveMuxHandler) 82 83 githubEventServer.httpServeMux = httpServeMux 84 githubEventServer.httpServer = &http.Server{Addr: ":" + strconv.Itoa(o.port), Handler: httpServeMux} 85 86 return githubEventServer 87 } 88 89 // ListenAndServe runs the http server 90 func (g *GitHubEventServer) ListenAndServe() error { 91 return g.httpServer.ListenAndServe() 92 } 93 94 // Shutdown shutdowns the http server 95 func (g *GitHubEventServer) Shutdown(ctx context.Context) error { 96 return g.httpServer.Shutdown(ctx) 97 } 98 99 // ReviewCommentEventHandler is a type of function that handles GitHub's review comment events 100 type ReviewCommentEventHandler func(*logrus.Entry, github.ReviewCommentEvent) 101 102 // ReviewEventHandler is a type of function that handles GitHub's review events. 103 type ReviewEventHandler func(*logrus.Entry, github.ReviewEvent) 104 105 // PushEventHandler is a type of function that handles GitHub's push events. 106 type PushEventHandler func(*logrus.Entry, github.PushEvent) 107 108 // IssueCommentEventHandler is a type of function that handles GitHub's issue comment events. 109 type IssueCommentEventHandler func(*logrus.Entry, github.IssueCommentEvent) 110 111 // PullRequestHandler is a type of function that handles GitHub's pull request events. 112 type PullRequestHandler func(*logrus.Entry, github.PullRequestEvent) 113 114 // IssueEventHandler is a type of function that handles GitHub's issue events. 115 type IssueEventHandler func(*logrus.Entry, github.IssueEvent) 116 117 // StatusEventHandler is a type of function that handles GitHub's status events. 118 type StatusEventHandler func(*logrus.Entry, github.StatusEvent) 119 120 // WorkflowRunEventHandler is a type of function that handles GitHub's workflow run events. 121 type WorkflowRunEventHandler func(*logrus.Entry, github.WorkflowRunEvent) 122 123 // RegistryPackageEventHandler is a type of function that handles GitHub's registry package events. 124 type RegistryPackageEventHandler func(*logrus.Entry, github.RegistryPackageEvent) 125 126 // RegisterReviewCommentEventHandler registers an ReviewCommentEventHandler function in GitHubEventServerOptions 127 func (g *GitHubEventServer) RegisterReviewCommentEventHandler(fn ReviewCommentEventHandler) { 128 g.serveMuxHandler.reviewCommentEventHandlers = append(g.serveMuxHandler.reviewCommentEventHandlers, fn) 129 } 130 131 // RegisterReviewEventHandler registers an ReviewEventHandler function in GitHubEventServerOptions 132 func (g *GitHubEventServer) RegisterReviewEventHandler(fn ReviewEventHandler) { 133 g.serveMuxHandler.reviewEventHandlers = append(g.serveMuxHandler.reviewEventHandlers, fn) 134 } 135 136 // RegisterPushEventHandler registers an PushEventHandler function in GitHubEventServerOptions 137 func (g *GitHubEventServer) RegisterPushEventHandler(fn PushEventHandler) { 138 g.serveMuxHandler.pushEventHandlers = append(g.serveMuxHandler.pushEventHandlers, fn) 139 } 140 141 // RegisterHandleIssueCommentEvent registers an IssueCommentEventHandler function in GitHubEventServerOptions 142 func (g *GitHubEventServer) RegisterHandleIssueCommentEvent(fn IssueCommentEventHandler) { 143 g.serveMuxHandler.issueCommentEventHandlers = append(g.serveMuxHandler.issueCommentEventHandlers, fn) 144 } 145 146 // RegisterHandlePullRequestEvent registers an PullRequestHandler function in GitHubEventServerOptions 147 func (g *GitHubEventServer) RegisterHandlePullRequestEvent(fn PullRequestHandler) { 148 g.serveMuxHandler.pullRequestHandlers = append(g.serveMuxHandler.pullRequestHandlers, fn) 149 } 150 151 // RegisterIssueEventHandler registers an IssueEventHandler function in GitHubEventServerOptions 152 func (g *GitHubEventServer) RegisterIssueEventHandler(fn IssueEventHandler) { 153 g.serveMuxHandler.issueEventHandlers = append(g.serveMuxHandler.issueEventHandlers, fn) 154 } 155 156 // RegisterStatusEventHandler registers an StatusEventHandler function in GitHubEventServerOptions 157 func (g *GitHubEventServer) RegisterStatusEventHandler(fn StatusEventHandler) { 158 g.serveMuxHandler.statusEventHandlers = append(g.serveMuxHandler.statusEventHandlers, fn) 159 } 160 161 // RegisterWorkflowRunEventHandler registers an WorkflowRunEventHandler function in GitHubEventServerOptions 162 func (g *GitHubEventServer) RegisterWorkflowRunEventHandler(fn WorkflowRunEventHandler) { 163 g.serveMuxHandler.workflowRunEventHandler = append(g.serveMuxHandler.workflowRunEventHandler, fn) 164 } 165 166 // RegisterWorkflowRunEventHandler registers a RegistryPackageEventHandler function in GitHubEventServerOptions 167 func (g *GitHubEventServer) RegisterRegistryPackageEventHandler(fn RegistryPackageEventHandler) { 168 g.serveMuxHandler.registryPackageEventHandlers = append(g.serveMuxHandler.registryPackageEventHandlers, fn) 169 } 170 171 // RegisterExternalPlugins registers the external plugins in GitHubEventServerOptions 172 func (g *GitHubEventServer) RegisterExternalPlugins(p map[string][]plugins.ExternalPlugin) { 173 g.serveMuxHandler.externalPlugins = p 174 } 175 176 // RegisterHelpProvider registers a help provider function in GitHubEventServerOptions http.ServeMux 177 func (g *GitHubEventServer) RegisterHelpProvider(helpProvider func([]config.OrgRepo) (*pluginhelp.PluginHelp, error), log *logrus.Entry) { 178 pluginhelp_externalplugins.ServeExternalPluginHelp(g.httpServeMux, log, helpProvider) 179 } 180 181 // RegisterPluginHelpAgentHandle registers a help agent in with the given endpoint in the GitHubEventServerOptions http.ServeMux 182 func (g *GitHubEventServer) RegisterPluginHelpAgentHandle(endpoint string, helpAgent *pluginhelp_hook.HelpAgent) { 183 g.httpServeMux.Handle(endpoint, helpAgent) 184 } 185 186 // RegisterCustomFuncHandle registers a custom func(w http.ResponseWriter, r *http.Request) 187 // with the given endpoint in the GitHubEventServerOptions http.ServeMux 188 func (g *GitHubEventServer) RegisterCustomFuncHandle(endpoint string, fn func(w http.ResponseWriter, r *http.Request)) { 189 g.httpServeMux.HandleFunc(endpoint, fn) 190 } 191 192 // GracefulShutdown handles all requests sent before receiving the shutdown signal. 193 func (g *GitHubEventServer) GracefulShutdown() { 194 logrus.Info("Waiting for the remaining requests") 195 g.wg.Wait() 196 } 197 198 // serveMuxHandler is a http serveMux handler that implements the ServeHTTP method. 199 // see https://godoc.org/net/http#ServeMux 200 type serveMuxHandler struct { 201 log *logrus.Entry 202 wg *sync.WaitGroup 203 204 reviewCommentEventHandlers []ReviewCommentEventHandler 205 reviewEventHandlers []ReviewEventHandler 206 pullRequestHandlers []PullRequestHandler 207 pushEventHandlers []PushEventHandler 208 issueCommentEventHandlers []IssueCommentEventHandler 209 issueEventHandlers []IssueEventHandler 210 statusEventHandlers []StatusEventHandler 211 workflowRunEventHandler []WorkflowRunEventHandler 212 registryPackageEventHandlers []RegistryPackageEventHandler 213 214 externalPlugins map[string][]plugins.ExternalPlugin 215 216 hmacTokenGenerator func() []byte 217 metrics *Metrics 218 219 c http.Client 220 } 221 222 func (s *serveMuxHandler) handleEvent(eventType, eventGUID string, payload []byte, h http.Header) error { 223 var org string 224 var repo string 225 226 l := logrus.WithFields(logrus.Fields{eventTypeField: eventType, github.EventGUID: eventGUID}) 227 228 // We don't want to fail the webhook due to a metrics error. 229 if counter, err := s.metrics.WebhookCounter.GetMetricWithLabelValues(eventType); err != nil { 230 l.WithError(err).Warn("Failed to get metric for eventType " + eventType) 231 } else { 232 counter.Inc() 233 } 234 235 switch eventType { 236 case issuesEvent: 237 var i github.IssueEvent 238 if err := json.Unmarshal(payload, &i); err != nil { 239 return err 240 } 241 i.GUID = eventGUID 242 org = i.Repo.Owner.Login 243 repo = i.Repo.Name 244 245 for _, issueEventHandler := range s.issueEventHandlers { 246 fn := issueEventHandler 247 s.wg.Add(1) 248 go func() { 249 defer s.wg.Done() 250 fn(l.WithFields(logrus.Fields{ 251 github.OrgLogField: i.Repo.Owner.Login, 252 github.RepoLogField: i.Repo.Name, 253 github.PrLogField: i.Issue.Number, 254 "author": i.Issue.User.Login, 255 "url": i.Issue.HTMLURL, 256 }), i) 257 }() 258 } 259 260 case issueCommentEvent: 261 var ic github.IssueCommentEvent 262 if err := json.Unmarshal(payload, &ic); err != nil { 263 return err 264 } 265 ic.GUID = eventGUID 266 org = ic.Repo.Owner.Login 267 repo = ic.Repo.Name 268 269 for _, issueCommentEventHandler := range s.issueCommentEventHandlers { 270 fn := issueCommentEventHandler 271 s.wg.Add(1) 272 go func() { 273 defer s.wg.Done() 274 fn(l.WithFields(logrus.Fields{ 275 github.OrgLogField: ic.Repo.Owner.Login, 276 github.RepoLogField: ic.Repo.Name, 277 github.PrLogField: ic.Issue.Number, 278 "author": ic.Comment.User.Login, 279 "url": ic.Comment.HTMLURL, 280 }), ic) 281 }() 282 } 283 284 case pullRequestEvent: 285 var pr github.PullRequestEvent 286 if err := json.Unmarshal(payload, &pr); err != nil { 287 return err 288 } 289 pr.GUID = eventGUID 290 org = pr.Repo.Owner.Login 291 repo = pr.Repo.Name 292 293 for _, pullRequestHandler := range s.pullRequestHandlers { 294 fn := pullRequestHandler 295 s.wg.Add(1) 296 go func() { 297 defer s.wg.Done() 298 fn(l.WithFields(logrus.Fields{ 299 github.OrgLogField: pr.Repo.Owner.Login, 300 github.RepoLogField: pr.Repo.Name, 301 github.PrLogField: pr.Number, 302 "author": pr.PullRequest.User.Login, 303 "url": pr.PullRequest.HTMLURL, 304 }), pr) 305 }() 306 } 307 308 case pullRequestReviewEvent: 309 var re github.ReviewEvent 310 if err := json.Unmarshal(payload, &re); err != nil { 311 return err 312 } 313 re.GUID = eventGUID 314 org = re.Repo.Owner.Login 315 repo = re.Repo.Name 316 317 for _, reviewEventHandler := range s.reviewEventHandlers { 318 fn := reviewEventHandler 319 s.wg.Add(1) 320 go func() { 321 defer s.wg.Done() 322 fn(l.WithFields(logrus.Fields{ 323 github.OrgLogField: re.Repo.Owner.Login, 324 github.RepoLogField: re.Repo.Name, 325 github.PrLogField: re.PullRequest.Number, 326 "review": re.Review.ID, 327 "reviewer": re.Review.User.Login, 328 "url": re.Review.HTMLURL, 329 }), re) 330 }() 331 } 332 333 case pullRequestReviewCommentEvent: 334 var rce github.ReviewCommentEvent 335 if err := json.Unmarshal(payload, &rce); err != nil { 336 return err 337 } 338 rce.GUID = eventGUID 339 org = rce.Repo.Owner.Login 340 repo = rce.Repo.Name 341 342 for _, reviewCommentEventHandler := range s.reviewCommentEventHandlers { 343 fn := reviewCommentEventHandler 344 s.wg.Add(1) 345 go func() { 346 defer s.wg.Done() 347 fn(l.WithFields(logrus.Fields{ 348 github.OrgLogField: rce.Repo.Owner.Login, 349 github.RepoLogField: rce.Repo.Name, 350 github.PrLogField: rce.PullRequest.Number, 351 "review": rce.Comment.ReviewID, 352 "commenter": rce.Comment.User.Login, 353 "url": rce.Comment.HTMLURL, 354 }), rce) 355 }() 356 } 357 358 case pushEvent: 359 var pe github.PushEvent 360 if err := json.Unmarshal(payload, &pe); err != nil { 361 return err 362 } 363 pe.GUID = eventGUID 364 org = pe.Repo.Owner.Login 365 repo = pe.Repo.Name 366 367 for _, pushEventHandler := range s.pushEventHandlers { 368 fn := pushEventHandler 369 s.wg.Add(1) 370 go func() { 371 defer s.wg.Done() 372 fn(l.WithFields(logrus.Fields{ 373 github.OrgLogField: pe.Repo.Owner.Name, 374 github.RepoLogField: pe.Repo.Name, 375 "ref": pe.Ref, 376 "head": pe.After, 377 }), pe) 378 }() 379 } 380 381 case statusEvent: 382 var se github.StatusEvent 383 if err := json.Unmarshal(payload, &se); err != nil { 384 return err 385 } 386 se.GUID = eventGUID 387 org = se.Repo.Owner.Login 388 repo = se.Repo.Name 389 390 for _, statusEventHandler := range s.statusEventHandlers { 391 fn := statusEventHandler 392 s.wg.Add(1) 393 go func() { 394 defer s.wg.Done() 395 fn(l.WithFields(logrus.Fields{ 396 github.OrgLogField: se.Repo.Owner.Login, 397 github.RepoLogField: se.Repo.Name, 398 "context": se.Context, 399 "sha": se.SHA, 400 "state": se.State, 401 "id": se.ID, 402 }), se) 403 }() 404 } 405 406 case workflowRunEvent: 407 var wre github.WorkflowRunEvent 408 if err := json.Unmarshal(payload, &wre); err != nil { 409 return err 410 } 411 wre.GUID = eventGUID 412 org = wre.Repo.Owner.Login 413 repo = wre.Repo.Name 414 415 for _, workflowRunEventHandler := range s.workflowRunEventHandler { 416 fn := workflowRunEventHandler 417 s.wg.Add(1) 418 go func() { 419 defer s.wg.Done() 420 fn(l.WithFields(logrus.Fields{ 421 github.OrgLogField: wre.Repo.Owner.Login, 422 github.RepoLogField: wre.Repo.Name, 423 "workflow_id": wre.WorkflowRun.WorkflowID, 424 }), wre) 425 }() 426 } 427 428 case registryPackageEvent: 429 var rpe github.RegistryPackageEvent 430 if err := json.Unmarshal(payload, &rpe); err != nil { 431 return err 432 } 433 rpe.GUID = eventGUID 434 org = rpe.Repo.Owner.Login 435 repo = rpe.Repo.Name 436 437 for _, registryPackageEventHandler := range s.registryPackageEventHandlers { 438 fn := registryPackageEventHandler 439 s.wg.Add(1) 440 go func() { 441 defer s.wg.Done() 442 fn(l.WithFields(logrus.Fields{ 443 github.OrgLogField: rpe.Repo.Owner.Login, 444 github.RepoLogField: rpe.Repo.Name, 445 "registry_package_id": rpe.RegistryPackage.ID, 446 }), rpe) 447 }() 448 } 449 450 default: 451 l.Debug("Ignoring unhandled event type.") 452 } 453 454 // Redirect event to external plugins if necessary 455 s.demuxExternal(l, s.getExternalPluginsForEvent(org, repo, eventType), payload, h, s.wg) 456 457 return nil 458 } 459 460 // ServeHTTP validates an incoming webhook and puts it into the event channel. 461 func (s *serveMuxHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 462 eventType, eventGUID, payload, ok, resp := github.ValidateWebhook(w, r, s.hmacTokenGenerator) 463 if counter, err := s.metrics.ResponseCounter.GetMetricWithLabelValues(strconv.Itoa(resp)); err != nil { 464 logrus.WithFields(logrus.Fields{ 465 "status-code": resp, 466 }).WithError(err).Error("Failed to get metric for reporting webhook status code") 467 } else { 468 counter.Inc() 469 } 470 471 if !ok { 472 return 473 } 474 fmt.Fprint(w, "Event received. Have a nice day.") 475 476 if err := s.handleEvent(eventType, eventGUID, payload, r.Header); err != nil { 477 logrus.WithError(err).Error("Error parsing event.") 478 } 479 } 480 481 // demuxExternal dispatches the provided payload to the external plugins. 482 func (s *serveMuxHandler) demuxExternal(l *logrus.Entry, externalPlugins []plugins.ExternalPlugin, payload []byte, h http.Header, wg *sync.WaitGroup) { 483 h.Set("User-Agent", "ProwHook") 484 for _, p := range externalPlugins { 485 wg.Add(1) 486 go func(p plugins.ExternalPlugin) { 487 defer wg.Done() 488 if err := s.dispatch(p.Endpoint, payload, h); err != nil { 489 l.WithError(err).WithField("external-plugin", p.Name).Error("Error dispatching event to external plugin.") 490 } else { 491 l.WithField("external-plugin", p.Name).Info("Dispatched event to external plugin") 492 } 493 }(p) 494 } 495 } 496 497 // dispatch creates a new request using the provided payload and headers 498 // and dispatches the request to the provided endpoint. 499 func (s *serveMuxHandler) dispatch(endpoint string, payload []byte, h http.Header) error { 500 req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(payload)) 501 if err != nil { 502 return err 503 } 504 req.Header = h 505 resp, err := s.do(req) 506 if err != nil { 507 return err 508 } 509 defer resp.Body.Close() 510 rb, err := io.ReadAll(resp.Body) 511 if err != nil { 512 return err 513 } 514 if resp.StatusCode < 200 || resp.StatusCode > 299 { 515 return fmt.Errorf("response has status %q and body %q", resp.Status, string(rb)) 516 } 517 return nil 518 } 519 520 func (s *serveMuxHandler) do(req *http.Request) (*http.Response, error) { 521 var resp *http.Response 522 var err error 523 backoff := 100 * time.Millisecond 524 maxRetries := 5 525 526 for retries := 0; retries < maxRetries; retries++ { 527 resp, err = s.c.Do(req) 528 if err == nil { 529 break 530 } 531 time.Sleep(backoff) 532 backoff *= 2 533 } 534 return resp, err 535 } 536 537 func (s *serveMuxHandler) getExternalPluginsForEvent(org, repo, event string) []plugins.ExternalPlugin { 538 pluginsByEvent := make(map[string][]plugins.ExternalPlugin) 539 540 var external []plugins.ExternalPlugin 541 542 fullRepo := fmt.Sprintf("%s/%s", org, repo) 543 external = append(external, s.externalPlugins[org]...) 544 external = append(external, s.externalPlugins[fullRepo]...) 545 546 for _, ep := range external { 547 for _, event := range ep.Events { 548 pluginsByEvent[event] = append(pluginsByEvent[event], ep) 549 } 550 } 551 552 if externalPlugins, ok := pluginsByEvent[event]; ok { 553 return externalPlugins 554 } 555 return []plugins.ExternalPlugin{} 556 }