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  }