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 }