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 }