github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/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  	"strconv"
    26  	"time"
    27  
    28  	"github.com/sirupsen/logrus"
    29  	"sigs.k8s.io/prow/cmd/external-plugins/needs-rebase/plugin"
    30  	"sigs.k8s.io/prow/pkg/config/secret"
    31  	"sigs.k8s.io/prow/pkg/flagutil"
    32  	prowflagutil "sigs.k8s.io/prow/pkg/flagutil"
    33  	pluginsflagutil "sigs.k8s.io/prow/pkg/flagutil/plugins"
    34  	"sigs.k8s.io/prow/pkg/github"
    35  	"sigs.k8s.io/prow/pkg/interrupts"
    36  	"sigs.k8s.io/prow/pkg/labels"
    37  	"sigs.k8s.io/prow/pkg/logrusutil"
    38  	"sigs.k8s.io/prow/pkg/pjutil"
    39  	"sigs.k8s.io/prow/pkg/pluginhelp/externalplugins"
    40  )
    41  
    42  type options struct {
    43  	port int
    44  
    45  	pluginsConfig          pluginsflagutil.PluginOptions
    46  	dryRun                 bool
    47  	github                 prowflagutil.GitHubOptions
    48  	instrumentationOptions prowflagutil.InstrumentationOptions
    49  	logLevel               string
    50  
    51  	updatePeriod time.Duration
    52  
    53  	webhookSecretFile string
    54  
    55  	cacheValidTime int
    56  }
    57  
    58  const defaultHourlyTokens = 360
    59  
    60  func (o *options) Validate() error {
    61  	for idx, group := range []flagutil.OptionGroup{&o.github} {
    62  		if err := group.Validate(o.dryRun); err != nil {
    63  			return fmt.Errorf("%d: %w", idx, err)
    64  		}
    65  	}
    66  
    67  	return nil
    68  }
    69  
    70  func gatherOptions() options {
    71  	o := options{}
    72  	fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
    73  	fs.IntVar(&o.port, "port", 8888, "Port to listen on.")
    74  	fs.BoolVar(&o.dryRun, "dry-run", true, "Dry run for testing. Uses API tokens but does not mutate.")
    75  	fs.DurationVar(&o.updatePeriod, "update-period", time.Hour*24, "Period duration for periodic scans of all PRs.")
    76  	fs.StringVar(&o.webhookSecretFile, "hmac-secret-file", "/etc/webhook/hmac", "Path to the file containing the GitHub HMAC secret.")
    77  	fs.StringVar(&o.logLevel, "log-level", "debug", fmt.Sprintf("Log level is one of %v.", logrus.AllLevels))
    78  	fs.IntVar(&o.cacheValidTime, "cache-valid-time", 0, "Do not re-check PR mergeability for comment events within this time (seconds)")
    79  
    80  	o.github.AddCustomizedFlags(fs, prowflagutil.ThrottlerDefaults(defaultHourlyTokens, defaultHourlyTokens))
    81  
    82  	o.pluginsConfig.PluginConfigPathDefault = "/etc/plugins/plugins.yaml"
    83  	for _, group := range []flagutil.OptionGroup{&o.instrumentationOptions, &o.pluginsConfig} {
    84  		group.AddFlags(fs)
    85  	}
    86  	fs.Parse(os.Args[1:])
    87  	return o
    88  }
    89  
    90  func main() {
    91  	logrusutil.ComponentInit()
    92  	o := gatherOptions()
    93  	if err := o.Validate(); err != nil {
    94  		logrus.Fatalf("Invalid options: %v", err)
    95  	}
    96  
    97  	logLevel, err := logrus.ParseLevel(o.logLevel)
    98  	if err != nil {
    99  		logrus.WithError(err).Fatal("Failed to parse loglevel")
   100  	}
   101  	logrus.SetLevel(logLevel)
   102  	log := logrus.StandardLogger().WithField("plugin", labels.NeedsRebase)
   103  
   104  	if err := secret.Add(o.webhookSecretFile); err != nil {
   105  		logrus.WithError(err).Fatal("Error starting secrets agent.")
   106  	}
   107  
   108  	pa, err := o.pluginsConfig.PluginAgent()
   109  	if err != nil {
   110  		log.WithError(err).Fatal("Error loading plugin config")
   111  	}
   112  
   113  	githubClient, err := o.github.GitHubClient(o.dryRun)
   114  	if err != nil {
   115  		logrus.WithError(err).Fatal("Error getting GitHub client.")
   116  	}
   117  
   118  	issueCache := plugin.NewCache(o.cacheValidTime)
   119  
   120  	server := &Server{
   121  		tokenGenerator: secret.GetTokenGenerator(o.webhookSecretFile),
   122  		ghc:            githubClient,
   123  		log:            log,
   124  		issueCache:     issueCache,
   125  	}
   126  
   127  	defer interrupts.WaitForGracefulShutdown()
   128  
   129  	interrupts.TickLiteral(func() {
   130  		start := time.Now()
   131  		if err := plugin.HandleAll(log, githubClient, pa.Config(), o.github.AppID != "", issueCache); err != nil {
   132  			log.WithError(err).Error("Error during periodic update of all PRs.")
   133  		}
   134  		log.WithField("duration", fmt.Sprintf("%v", time.Since(start))).Info("Periodic update complete.")
   135  	}, o.updatePeriod)
   136  
   137  	health := pjutil.NewHealthOnPort(o.instrumentationOptions.HealthPort)
   138  	health.ServeReady()
   139  
   140  	mux := http.NewServeMux()
   141  	mux.Handle("/", server)
   142  	externalplugins.ServeExternalPluginHelp(mux, log, plugin.HelpProvider)
   143  	httpServer := &http.Server{Addr: ":" + strconv.Itoa(o.port), Handler: mux}
   144  	interrupts.ListenAndServe(httpServer, 5*time.Second)
   145  }
   146  
   147  // Server implements http.Handler. It validates incoming GitHub webhooks and
   148  // then dispatches them to the appropriate plugins.
   149  type Server struct {
   150  	tokenGenerator func() []byte
   151  	ghc            github.Client
   152  	log            *logrus.Entry
   153  	issueCache     *plugin.Cache
   154  }
   155  
   156  // ServeHTTP validates an incoming webhook and puts it into the event channel.
   157  func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   158  	// TODO: Move webhook handling logic out of hook binary so that we don't have to import all
   159  	// plugins just to validate the webhook.
   160  	eventType, eventGUID, payload, ok, _ := github.ValidateWebhook(w, r, s.tokenGenerator)
   161  	if !ok {
   162  		return
   163  	}
   164  	fmt.Fprint(w, "Event received. Have a nice day.")
   165  
   166  	if err := s.handleEvent(eventType, eventGUID, payload); err != nil {
   167  		logrus.WithError(err).Error("Error parsing event.")
   168  	}
   169  }
   170  
   171  func (s *Server) handleEvent(eventType, eventGUID string, payload []byte) error {
   172  	l := s.log.WithFields(
   173  		logrus.Fields{
   174  			"event-type":     eventType,
   175  			github.EventGUID: eventGUID,
   176  		},
   177  	)
   178  	switch eventType {
   179  	case "pull_request":
   180  		var pre github.PullRequestEvent
   181  		if err := json.Unmarshal(payload, &pre); err != nil {
   182  			return err
   183  		}
   184  		go func() {
   185  			if err := plugin.HandlePullRequestEvent(l, s.ghc, &pre); err != nil {
   186  				l.WithField("event-type", eventType).WithError(err).Info("Error handling event.")
   187  			}
   188  		}()
   189  	case "issue_comment":
   190  		var ice github.IssueCommentEvent
   191  		if err := json.Unmarshal(payload, &ice); err != nil {
   192  			return err
   193  		}
   194  		go func() {
   195  			if err := plugin.HandleIssueCommentEvent(l, s.ghc, &ice, s.issueCache); err != nil {
   196  				l.WithField("event-type", eventType).WithError(err).Info("Error handling event.")
   197  			}
   198  		}()
   199  	default:
   200  		s.log.Debugf("received an event of type %q but didn't ask for it", eventType)
   201  	}
   202  	return nil
   203  }