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  }