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  }