github.com/abayer/test-infra@v0.0.5/prow/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  	"context"
    21  	"errors"
    22  	"flag"
    23  	"net/http"
    24  	"net/url"
    25  	"os"
    26  	"os/signal"
    27  	"strconv"
    28  	"syscall"
    29  	"time"
    30  
    31  	"github.com/prometheus/client_golang/prometheus/promhttp"
    32  	"github.com/sirupsen/logrus"
    33  
    34  	"k8s.io/test-infra/prow/config"
    35  	"k8s.io/test-infra/prow/flagutil"
    36  	"k8s.io/test-infra/prow/git"
    37  	"k8s.io/test-infra/prow/github"
    38  	"k8s.io/test-infra/prow/hook"
    39  	"k8s.io/test-infra/prow/kube"
    40  	"k8s.io/test-infra/prow/logrusutil"
    41  	"k8s.io/test-infra/prow/metrics"
    42  	pluginhelp "k8s.io/test-infra/prow/pluginhelp/hook"
    43  	"k8s.io/test-infra/prow/plugins"
    44  	"k8s.io/test-infra/prow/repoowners"
    45  	"k8s.io/test-infra/prow/slack"
    46  )
    47  
    48  type options struct {
    49  	port int
    50  
    51  	configPath    string
    52  	jobConfigPath string
    53  	cluster       string
    54  	pluginConfig  string
    55  
    56  	dryRun      bool
    57  	gracePeriod time.Duration
    58  	deckURL     string
    59  
    60  	githubEndpoint  flagutil.Strings
    61  	githubTokenFile string
    62  
    63  	webhookSecretFile string
    64  	slackTokenFile    string
    65  }
    66  
    67  func (o *options) Validate() error {
    68  	if o.dryRun && o.deckURL == "" {
    69  		return errors.New("a dry-run was requested but required flag --deck-url was unset")
    70  	}
    71  	return nil
    72  }
    73  
    74  func gatherOptions() options {
    75  	o := options{
    76  		githubEndpoint: flagutil.NewStrings("https://api.github.com"),
    77  	}
    78  	flag.IntVar(&o.port, "port", 8888, "Port to listen on.")
    79  
    80  	flag.StringVar(&o.configPath, "config-path", "/etc/config/config.yaml", "Path to config.yaml.")
    81  	flag.StringVar(&o.jobConfigPath, "job-config-path", "", "Path to prow job configs.")
    82  	flag.StringVar(&o.cluster, "cluster", "", "Path to kube.Cluster YAML file. If empty, uses the local cluster.")
    83  	flag.StringVar(&o.pluginConfig, "plugin-config", "/etc/plugins/plugins.yaml", "Path to plugin config file.")
    84  
    85  	flag.BoolVar(&o.dryRun, "dry-run", true, "Dry run for testing. Uses API tokens but does not mutate.")
    86  	flag.DurationVar(&o.gracePeriod, "grace-period", 180*time.Second, "On shutdown, try to handle remaining events for the specified duration. ")
    87  	flag.StringVar(&o.deckURL, "deck-url", "", "Deck URL for read-only access to the cluster.")
    88  
    89  	flag.Var(&o.githubEndpoint, "github-endpoint", "GitHub's API endpoint.")
    90  	flag.StringVar(&o.githubTokenFile, "github-token-file", "/etc/github/oauth", "Path to the file containing the GitHub OAuth secret.")
    91  
    92  	flag.StringVar(&o.webhookSecretFile, "hmac-secret-file", "/etc/webhook/hmac", "Path to the file containing the GitHub HMAC secret.")
    93  	flag.StringVar(&o.slackTokenFile, "slack-token-file", "", "Path to the file containing the Slack token to use.")
    94  	flag.Parse()
    95  	return o
    96  }
    97  
    98  func main() {
    99  	o := gatherOptions()
   100  	if err := o.Validate(); err != nil {
   101  		logrus.Fatalf("Invalid options: %v", err)
   102  	}
   103  	logrus.SetFormatter(logrusutil.NewDefaultFieldsFormatter(nil, logrus.Fields{"component": "hook"}))
   104  
   105  	configAgent := &config.Agent{}
   106  	if err := configAgent.Start(o.configPath, o.jobConfigPath); err != nil {
   107  		logrus.WithError(err).Fatal("Error starting config agent.")
   108  	}
   109  
   110  	var err error
   111  	for _, ep := range o.githubEndpoint.Strings() {
   112  		_, err = url.ParseRequestURI(ep)
   113  		if err != nil {
   114  			logrus.WithError(err).Fatalf("Invalid --endpoint URL %q.", ep)
   115  		}
   116  	}
   117  
   118  	var tokens []string
   119  
   120  	// Append the path of hmac and github secrets.
   121  	tokens = append(tokens, o.githubTokenFile)
   122  	tokens = append(tokens, o.webhookSecretFile)
   123  
   124  	// This is necessary since slack token is optional.
   125  	if o.slackTokenFile != "" {
   126  		tokens = append(tokens, o.slackTokenFile)
   127  	}
   128  
   129  	secretAgent := &config.SecretAgent{}
   130  	if err := secretAgent.Start(tokens); err != nil {
   131  		logrus.WithError(err).Fatal("Error starting secrets agent.")
   132  	}
   133  
   134  	teamToken := string(secretAgent.GetSecret(o.slackTokenFile))
   135  
   136  	var githubClient *github.Client
   137  	var kubeClient *kube.Client
   138  	if o.dryRun {
   139  		githubClient = github.NewDryRunClient(
   140  			secretAgent.GetTokenGenerator(o.githubTokenFile), o.githubEndpoint.Strings()...)
   141  		kubeClient = kube.NewFakeClient(o.deckURL)
   142  	} else {
   143  		githubClient = github.NewClient(
   144  			secretAgent.GetTokenGenerator(o.githubTokenFile), o.githubEndpoint.Strings()...)
   145  		if o.cluster == "" {
   146  			kubeClient, err = kube.NewClientInCluster(configAgent.Config().ProwJobNamespace)
   147  			if err != nil {
   148  				logrus.WithError(err).Fatal("Error getting kube client.")
   149  			}
   150  		} else {
   151  			kubeClient, err = kube.NewClientFromFile(o.cluster, configAgent.Config().ProwJobNamespace)
   152  			if err != nil {
   153  				logrus.WithError(err).Fatal("Error getting kube client.")
   154  			}
   155  		}
   156  	}
   157  
   158  	var slackClient *slack.Client
   159  	if !o.dryRun && teamToken != "" {
   160  		logrus.Info("Using real slack client.")
   161  		slackClient = slack.NewClient(teamToken)
   162  	}
   163  	if slackClient == nil {
   164  		logrus.Info("Using fake slack client.")
   165  		slackClient = slack.NewFakeClient()
   166  	}
   167  
   168  	gitClient, err := git.NewClient()
   169  	if err != nil {
   170  		logrus.WithError(err).Fatal("Error getting git client.")
   171  	}
   172  	defer gitClient.Clean()
   173  	// Get the bot's name in order to set credentials for the git client.
   174  	botName, err := githubClient.BotName()
   175  	if err != nil {
   176  		logrus.WithError(err).Fatal("Error getting bot name.")
   177  	}
   178  	gitClient.SetCredentials(botName, secretAgent.GetTokenGenerator(o.githubTokenFile))
   179  
   180  	pluginAgent := &plugins.PluginAgent{}
   181  
   182  	ownersClient := repoowners.NewClient(
   183  		gitClient, githubClient,
   184  		configAgent, pluginAgent.MDYAMLEnabled,
   185  		pluginAgent.SkipCollaborators,
   186  	)
   187  
   188  	pluginAgent.PluginClient = plugins.PluginClient{
   189  		GitHubClient: githubClient,
   190  		KubeClient:   kubeClient,
   191  		GitClient:    gitClient,
   192  		SlackClient:  slackClient,
   193  		OwnersClient: ownersClient,
   194  		Logger:       logrus.WithField("agent", "plugin"),
   195  	}
   196  	if err := pluginAgent.Start(o.pluginConfig); err != nil {
   197  		logrus.WithError(err).Fatal("Error starting plugins.")
   198  	}
   199  
   200  	promMetrics := hook.NewMetrics()
   201  
   202  	// Push metrics to the configured prometheus pushgateway endpoint.
   203  	pushGateway := configAgent.Config().PushGateway
   204  	if pushGateway.Endpoint != "" {
   205  		go metrics.PushMetrics("hook", pushGateway.Endpoint, pushGateway.Interval)
   206  	}
   207  
   208  	server := &hook.Server{
   209  		ConfigAgent:    configAgent,
   210  		Plugins:        pluginAgent,
   211  		Metrics:        promMetrics,
   212  		TokenGenerator: secretAgent.GetTokenGenerator(o.webhookSecretFile),
   213  	}
   214  	defer server.GracefulShutdown()
   215  
   216  	// Return 200 on / for health checks.
   217  	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {})
   218  	http.Handle("/metrics", promhttp.Handler())
   219  	// For /hook, handle a webhook normally.
   220  	http.Handle("/hook", server)
   221  	// Serve plugin help information from /plugin-help.
   222  	http.Handle("/plugin-help", pluginhelp.NewHelpAgent(pluginAgent, githubClient))
   223  
   224  	httpServer := &http.Server{Addr: ":" + strconv.Itoa(o.port)}
   225  
   226  	// Shutdown gracefully on SIGTERM or SIGINT
   227  	sig := make(chan os.Signal, 1)
   228  	signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
   229  	go func() {
   230  		<-sig
   231  		logrus.Info("Hook is shutting down...")
   232  		ctx, cancel := context.WithTimeout(context.Background(), o.gracePeriod)
   233  		defer cancel()
   234  		httpServer.Shutdown(ctx)
   235  	}()
   236  
   237  	logrus.WithError(httpServer.ListenAndServe()).Warn("Server exited.")
   238  }