github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/hook/main.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 main
    18  
    19  import (
    20  	"flag"
    21  	"net/http"
    22  	"os"
    23  	"strconv"
    24  	"time"
    25  
    26  	"github.com/sirupsen/logrus"
    27  	"k8s.io/apimachinery/pkg/util/sets"
    28  	"sigs.k8s.io/prow/pkg/pjutil/pprof"
    29  
    30  	"sigs.k8s.io/prow/pkg/bugzilla"
    31  	"sigs.k8s.io/prow/pkg/config"
    32  	"sigs.k8s.io/prow/pkg/config/secret"
    33  	"sigs.k8s.io/prow/pkg/flagutil"
    34  	prowflagutil "sigs.k8s.io/prow/pkg/flagutil"
    35  	configflagutil "sigs.k8s.io/prow/pkg/flagutil/config"
    36  	pluginsflagutil "sigs.k8s.io/prow/pkg/flagutil/plugins"
    37  	"sigs.k8s.io/prow/pkg/githubeventserver"
    38  	"sigs.k8s.io/prow/pkg/hook"
    39  	"sigs.k8s.io/prow/pkg/interrupts"
    40  	jiraclient "sigs.k8s.io/prow/pkg/jira"
    41  	"sigs.k8s.io/prow/pkg/logrusutil"
    42  	"sigs.k8s.io/prow/pkg/metrics"
    43  	"sigs.k8s.io/prow/pkg/pjutil"
    44  	pluginhelp "sigs.k8s.io/prow/pkg/pluginhelp/hook"
    45  	"sigs.k8s.io/prow/pkg/plugins"
    46  	bzplugin "sigs.k8s.io/prow/pkg/plugins/bugzilla"
    47  	"sigs.k8s.io/prow/pkg/plugins/jira"
    48  	"sigs.k8s.io/prow/pkg/plugins/ownersconfig"
    49  	"sigs.k8s.io/prow/pkg/repoowners"
    50  	"sigs.k8s.io/prow/pkg/slack"
    51  
    52  	_ "sigs.k8s.io/prow/pkg/version"
    53  )
    54  
    55  const (
    56  	defaultWebhookPath = "/hook"
    57  )
    58  
    59  type options struct {
    60  	webhookPath string
    61  	port        int
    62  
    63  	config        configflagutil.ConfigOptions
    64  	pluginsConfig pluginsflagutil.PluginOptions
    65  
    66  	dryRun                 bool
    67  	gracePeriod            time.Duration
    68  	kubernetes             prowflagutil.KubernetesOptions
    69  	github                 prowflagutil.GitHubOptions
    70  	githubEnablement       prowflagutil.GitHubEnablementOptions
    71  	bugzilla               prowflagutil.BugzillaOptions
    72  	instrumentationOptions prowflagutil.InstrumentationOptions
    73  	jira                   prowflagutil.JiraOptions
    74  
    75  	webhookSecretFile string
    76  	slackTokenFile    string
    77  }
    78  
    79  func (o *options) Validate() error {
    80  	for _, group := range []flagutil.OptionGroup{&o.kubernetes, &o.github, &o.bugzilla, &o.jira, &o.githubEnablement, &o.config, &o.pluginsConfig} {
    81  		if err := group.Validate(o.dryRun); err != nil {
    82  			return err
    83  		}
    84  	}
    85  
    86  	return nil
    87  }
    88  
    89  func gatherOptions(fs *flag.FlagSet, args ...string) options {
    90  	var o options
    91  	fs.StringVar(&o.webhookPath, "webhook-path", defaultWebhookPath, "The path of webhook events, default is '/hook'.")
    92  	fs.IntVar(&o.port, "port", 8888, "Port to listen on.")
    93  
    94  	fs.BoolVar(&o.dryRun, "dry-run", true, "Dry run for testing. Uses API tokens but does not mutate.")
    95  	fs.DurationVar(&o.gracePeriod, "grace-period", 180*time.Second, "On shutdown, try to handle remaining events for the specified duration. ")
    96  	o.pluginsConfig.PluginConfigPathDefault = "/etc/plugins/plugins.yaml"
    97  	for _, group := range []flagutil.OptionGroup{&o.kubernetes, &o.github, &o.bugzilla, &o.instrumentationOptions, &o.jira, &o.githubEnablement, &o.config, &o.pluginsConfig} {
    98  		group.AddFlags(fs)
    99  	}
   100  
   101  	fs.StringVar(&o.webhookSecretFile, "hmac-secret-file", "/etc/webhook/hmac", "Path to the file containing the GitHub HMAC secret.")
   102  	fs.StringVar(&o.slackTokenFile, "slack-token-file", "", "Path to the file containing the Slack token to use.")
   103  	fs.Parse(args)
   104  	return o
   105  }
   106  
   107  func main() {
   108  	logrusutil.ComponentInit()
   109  
   110  	o := gatherOptions(flag.NewFlagSet(os.Args[0], flag.ExitOnError), os.Args[1:]...)
   111  	if err := o.Validate(); err != nil {
   112  		logrus.WithError(err).Fatal("Invalid options")
   113  	}
   114  
   115  	configAgent, err := o.config.ConfigAgent()
   116  	if err != nil {
   117  		logrus.WithError(err).Fatal("Error starting config agent.")
   118  	}
   119  	o.kubernetes.SetDisabledClusters(sets.New[string](configAgent.Config().DisabledClusters...))
   120  
   121  	var tokens []string
   122  
   123  	// Append the path of hmac and github secrets.
   124  	if o.github.TokenPath != "" {
   125  		tokens = append(tokens, o.github.TokenPath)
   126  	}
   127  	if o.github.AppPrivateKeyPath != "" {
   128  		tokens = append(tokens, o.github.AppPrivateKeyPath)
   129  	}
   130  	tokens = append(tokens, o.webhookSecretFile)
   131  
   132  	// This is necessary since slack token is optional.
   133  	if o.slackTokenFile != "" {
   134  		tokens = append(tokens, o.slackTokenFile)
   135  	}
   136  
   137  	if o.bugzilla.ApiKeyPath != "" {
   138  		tokens = append(tokens, o.bugzilla.ApiKeyPath)
   139  	}
   140  
   141  	if err := secret.Add(tokens...); err != nil {
   142  		logrus.WithError(err).Fatal("Error starting secrets agent.")
   143  	}
   144  
   145  	pluginAgent, err := o.pluginsConfig.PluginAgent()
   146  	if err != nil {
   147  		logrus.WithError(err).Fatal("Error starting plugins.")
   148  	}
   149  
   150  	githubClient, err := o.github.GitHubClient(o.dryRun)
   151  	if err != nil {
   152  		logrus.WithError(err).Fatal("Error getting GitHub client.")
   153  	}
   154  	gitClient, err := o.github.GitClientFactory("", &o.config.InRepoConfigCacheDirBase, o.dryRun, false)
   155  	if err != nil {
   156  		logrus.WithError(err).Fatal("Error getting Git client.")
   157  	}
   158  
   159  	var bugzillaClient bugzilla.Client
   160  	if orgs, repos, _ := pluginAgent.Config().EnabledReposForPlugin(bzplugin.PluginName); orgs != nil || repos != nil {
   161  		client, err := o.bugzilla.BugzillaClient()
   162  		if err != nil {
   163  			logrus.WithError(err).Fatal("Error getting Bugzilla client.")
   164  		}
   165  		bugzillaClient = client
   166  	} else {
   167  		// we want something non-nil here with good no-op behavior,
   168  		// so the test fake is a cheap way to do that
   169  		bugzillaClient = &bugzilla.Fake{}
   170  	}
   171  
   172  	var jiraClient jiraclient.Client
   173  	if orgs, repos, _ := pluginAgent.Config().EnabledReposForPlugin(jira.PluginName); orgs != nil || repos != nil {
   174  		client, err := o.jira.Client()
   175  		if err != nil {
   176  			logrus.WithError(err).Fatal("Failed to construct Jira Client")
   177  		}
   178  		jiraClient = client
   179  	}
   180  
   181  	infrastructureClient, err := o.kubernetes.InfrastructureClusterClient(o.dryRun)
   182  	if err != nil {
   183  		logrus.WithError(err).Fatal("Error getting Kubernetes client for infrastructure cluster.")
   184  	}
   185  
   186  	buildClusterCoreV1Clients, err := o.kubernetes.BuildClusterCoreV1Clients(o.dryRun)
   187  	if err != nil {
   188  		logrus.WithError(err).Fatal("Error getting Kubernetes clients for build cluster.")
   189  	}
   190  
   191  	prowJobClient, err := o.kubernetes.ProwJobClient(configAgent.Config().ProwJobNamespace, o.dryRun)
   192  	if err != nil {
   193  		logrus.WithError(err).Fatal("Error getting ProwJob client for infrastructure cluster.")
   194  	}
   195  
   196  	var slackClient *slack.Client
   197  	if !o.dryRun && string(secret.GetSecret(o.slackTokenFile)) != "" {
   198  		logrus.Info("Using real slack client.")
   199  		slackClient = slack.NewClient(secret.GetTokenGenerator(o.slackTokenFile))
   200  	}
   201  	if slackClient == nil {
   202  		logrus.Info("Using fake slack client.")
   203  		slackClient = slack.NewFakeClient()
   204  	}
   205  
   206  	mdYAMLEnabled := func(org, repo string) bool {
   207  		return pluginAgent.Config().MDYAMLEnabled(org, repo)
   208  	}
   209  	skipCollaborators := func(org, repo string) bool {
   210  		return pluginAgent.Config().SkipCollaborators(org, repo)
   211  	}
   212  	ownersDirDenylist := func() *config.OwnersDirDenylist {
   213  		// OwnersDirDenylist struct contains some defaults that's required by all
   214  		// repos, so this function cannot return nil
   215  		res := &config.OwnersDirDenylist{}
   216  		if l := configAgent.Config().OwnersDirDenylist; l != nil {
   217  			res = l
   218  		}
   219  		return res
   220  	}
   221  	resolver := func(org, repo string) ownersconfig.Filenames {
   222  		return pluginAgent.Config().OwnersFilenames(org, repo)
   223  	}
   224  	ownersClient := repoowners.NewClient(gitClient, githubClient, mdYAMLEnabled, skipCollaborators, ownersDirDenylist, resolver)
   225  
   226  	clientAgent := &plugins.ClientAgent{
   227  		GitHubClient:              githubClient,
   228  		ProwJobClient:             prowJobClient,
   229  		KubernetesClient:          infrastructureClient,
   230  		BuildClusterCoreV1Clients: buildClusterCoreV1Clients,
   231  		GitClient:                 gitClient,
   232  		SlackClient:               slackClient,
   233  		OwnersClient:              ownersClient,
   234  		BugzillaClient:            bugzillaClient,
   235  		JiraClient:                jiraClient,
   236  	}
   237  
   238  	promMetrics := githubeventserver.NewMetrics()
   239  
   240  	defer interrupts.WaitForGracefulShutdown()
   241  
   242  	// Expose prometheus metrics
   243  	metrics.ExposeMetrics("hook", configAgent.Config().PushGateway, o.instrumentationOptions.MetricsPort)
   244  	pprof.Instrument(o.instrumentationOptions)
   245  
   246  	server := &hook.Server{
   247  		ClientAgent:    clientAgent,
   248  		ConfigAgent:    configAgent,
   249  		Plugins:        pluginAgent,
   250  		Metrics:        promMetrics,
   251  		RepoEnabled:    o.githubEnablement.EnablementChecker(),
   252  		TokenGenerator: secret.GetTokenGenerator(o.webhookSecretFile),
   253  	}
   254  	interrupts.OnInterrupt(func() {
   255  		server.GracefulShutdown()
   256  		if err := gitClient.Clean(); err != nil {
   257  			logrus.WithError(err).Error("Could not clean up git client cache.")
   258  		}
   259  	})
   260  
   261  	health := pjutil.NewHealthOnPort(o.instrumentationOptions.HealthPort)
   262  
   263  	hookMux := http.NewServeMux()
   264  	// TODO remove this health endpoint when the migration to health endpoint is done
   265  	// Return 200 on / for health checks.
   266  	hookMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {})
   267  
   268  	// For /hook, handle a webhook normally.
   269  	hookMux.Handle(o.webhookPath, server)
   270  	// Serve plugin help information from /plugin-help.
   271  	hookMux.Handle("/plugin-help", pluginhelp.NewHelpAgent(pluginAgent, githubClient))
   272  
   273  	httpServer := &http.Server{Addr: ":" + strconv.Itoa(o.port), Handler: hookMux}
   274  
   275  	health.ServeReady()
   276  
   277  	interrupts.ListenAndServe(httpServer, o.gracePeriod)
   278  }