github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/external-plugins/needs-rebase/main.go (about)

     1  /*
     2  Copyright 2017 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 main
    18  
    19  import (
    20  	"encoding/json"
    21  	"flag"
    22  	"fmt"
    23  	"net/http"
    24  	"os"
    25  	"os/signal"
    26  	"strconv"
    27  	"syscall"
    28  	"time"
    29  
    30  	"github.com/sirupsen/logrus"
    31  
    32  	"k8s.io/test-infra/pkg/flagutil"
    33  	"k8s.io/test-infra/prow/config/secret"
    34  	"k8s.io/test-infra/prow/external-plugins/needs-rebase/plugin"
    35  	prowflagutil "k8s.io/test-infra/prow/flagutil"
    36  	"k8s.io/test-infra/prow/github"
    37  	"k8s.io/test-infra/prow/labels"
    38  
    39  	// TODO: Remove the need for this import; it's currently required to allow the plugin config loader to function correctly (it expects plugins to be initialised)
    40  	// See https://github.com/kubernetes/test-infra/pull/8933#issuecomment-411511180
    41  	_ "k8s.io/test-infra/prow/hook"
    42  	"k8s.io/test-infra/prow/pluginhelp/externalplugins"
    43  	"k8s.io/test-infra/prow/plugins"
    44  )
    45  
    46  type options struct {
    47  	port int
    48  
    49  	pluginConfig string
    50  	dryRun       bool
    51  	github       prowflagutil.GitHubOptions
    52  
    53  	updatePeriod time.Duration
    54  
    55  	webhookSecretFile string
    56  }
    57  
    58  func (o *options) Validate() error {
    59  	for _, group := range []flagutil.OptionGroup{&o.github} {
    60  		if err := group.Validate(o.dryRun); err != nil {
    61  			return err
    62  		}
    63  	}
    64  
    65  	return nil
    66  }
    67  
    68  func gatherOptions() options {
    69  	o := options{}
    70  	fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
    71  	fs.IntVar(&o.port, "port", 8888, "Port to listen on.")
    72  	fs.StringVar(&o.pluginConfig, "plugin-config", "/etc/plugins/plugins.yaml", "Path to plugin config file.")
    73  	fs.BoolVar(&o.dryRun, "dry-run", true, "Dry run for testing. Uses API tokens but does not mutate.")
    74  	fs.DurationVar(&o.updatePeriod, "update-period", time.Hour*24, "Period duration for periodic scans of all PRs.")
    75  	fs.StringVar(&o.webhookSecretFile, "hmac-secret-file", "/etc/webhook/hmac", "Path to the file containing the GitHub HMAC secret.")
    76  
    77  	for _, group := range []flagutil.OptionGroup{&o.github} {
    78  		group.AddFlags(fs)
    79  	}
    80  	fs.Parse(os.Args[1:])
    81  	return o
    82  }
    83  
    84  func main() {
    85  	o := gatherOptions()
    86  	if err := o.Validate(); err != nil {
    87  		logrus.Fatalf("Invalid options: %v", err)
    88  	}
    89  
    90  	logrus.SetFormatter(&logrus.JSONFormatter{})
    91  	// TODO: Use global option from the prow config.
    92  	logrus.SetLevel(logrus.InfoLevel)
    93  	log := logrus.StandardLogger().WithField("plugin", labels.NeedsRebase)
    94  
    95  	// Ignore SIGTERM so that we don't drop hooks when the pod is removed.
    96  	// We'll get SIGTERM first and then SIGKILL after our graceful termination
    97  	// deadline.
    98  	signal.Ignore(syscall.SIGTERM)
    99  
   100  	secretAgent := &secret.Agent{}
   101  	if err := secretAgent.Start([]string{o.github.TokenPath, o.webhookSecretFile}); err != nil {
   102  		logrus.WithError(err).Fatal("Error starting secrets agent.")
   103  	}
   104  
   105  	pa := &plugins.ConfigAgent{}
   106  	if err := pa.Start(o.pluginConfig); err != nil {
   107  		log.WithError(err).Fatalf("Error loading plugin config from %q.", o.pluginConfig)
   108  	}
   109  
   110  	githubClient, err := o.github.GitHubClient(secretAgent, o.dryRun)
   111  	if err != nil {
   112  		logrus.WithError(err).Fatal("Error getting GitHub client.")
   113  	}
   114  	githubClient.Throttle(360, 360)
   115  
   116  	server := &Server{
   117  		tokenGenerator: secretAgent.GetTokenGenerator(o.webhookSecretFile),
   118  		ghc:            githubClient,
   119  		log:            log,
   120  	}
   121  
   122  	go periodicUpdate(log, pa, githubClient, o.updatePeriod)
   123  
   124  	http.Handle("/", server)
   125  	externalplugins.ServeExternalPluginHelp(http.DefaultServeMux, log, plugin.HelpProvider)
   126  	logrus.Fatal(http.ListenAndServe(":"+strconv.Itoa(o.port), nil))
   127  }
   128  
   129  // Server implements http.Handler. It validates incoming GitHub webhooks and
   130  // then dispatches them to the appropriate plugins.
   131  type Server struct {
   132  	tokenGenerator func() []byte
   133  	ghc            *github.Client
   134  	log            *logrus.Entry
   135  }
   136  
   137  // ServeHTTP validates an incoming webhook and puts it into the event channel.
   138  func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   139  	// TODO: Move webhook handling logic out of hook binary so that we don't have to import all
   140  	// plugins just to validate the webhook.
   141  	eventType, eventGUID, payload, ok, _ := github.ValidateWebhook(w, r, s.tokenGenerator())
   142  	if !ok {
   143  		return
   144  	}
   145  	fmt.Fprint(w, "Event received. Have a nice day.")
   146  
   147  	if err := s.handleEvent(eventType, eventGUID, payload); err != nil {
   148  		logrus.WithError(err).Error("Error parsing event.")
   149  	}
   150  }
   151  
   152  func (s *Server) handleEvent(eventType, eventGUID string, payload []byte) error {
   153  	l := s.log.WithFields(
   154  		logrus.Fields{
   155  			"event-type":     eventType,
   156  			github.EventGUID: eventGUID,
   157  		},
   158  	)
   159  	switch eventType {
   160  	case "pull_request":
   161  		var pre github.PullRequestEvent
   162  		if err := json.Unmarshal(payload, &pre); err != nil {
   163  			return err
   164  		}
   165  		go func() {
   166  			if err := plugin.HandleEvent(l, s.ghc, &pre); err != nil {
   167  				l.Info("Error handling event.")
   168  			}
   169  		}()
   170  	default:
   171  		s.log.Debugf("received an event of type %q but didn't ask for it", eventType)
   172  	}
   173  	return nil
   174  }
   175  
   176  func periodicUpdate(log *logrus.Entry, pa *plugins.ConfigAgent, ghc *github.Client, period time.Duration) {
   177  	update := func() {
   178  		start := time.Now()
   179  		if err := plugin.HandleAll(log, ghc, pa.Config()); err != nil {
   180  			log.WithError(err).Error("Error during periodic update of all PRs.")
   181  		}
   182  		log.WithField("duration", fmt.Sprintf("%v", time.Since(start))).Info("Periodic update complete.")
   183  	}
   184  
   185  	update()
   186  	for range time.Tick(period) {
   187  		update()
   188  	}
   189  }