sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/plugins.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 plugins 18 19 import ( 20 "context" 21 _ "embed" 22 "errors" 23 "fmt" 24 "io/fs" 25 "os" 26 "path/filepath" 27 "strings" 28 "sync" 29 "time" 30 31 "k8s.io/apimachinery/pkg/util/sets" 32 33 "sigs.k8s.io/prow/pkg/genyaml" 34 35 "github.com/prometheus/client_golang/prometheus" 36 "github.com/sirupsen/logrus" 37 utilerrors "k8s.io/apimachinery/pkg/util/errors" 38 "k8s.io/client-go/kubernetes" 39 corev1 "k8s.io/client-go/kubernetes/typed/core/v1" 40 "sigs.k8s.io/yaml" 41 42 "sigs.k8s.io/prow/pkg/bugzilla" 43 prowv1 "sigs.k8s.io/prow/pkg/client/clientset/versioned/typed/prowjobs/v1" 44 "sigs.k8s.io/prow/pkg/commentpruner" 45 "sigs.k8s.io/prow/pkg/config" 46 "sigs.k8s.io/prow/pkg/git/v2" 47 "sigs.k8s.io/prow/pkg/github" 48 "sigs.k8s.io/prow/pkg/jira" 49 "sigs.k8s.io/prow/pkg/pluginhelp" 50 "sigs.k8s.io/prow/pkg/repoowners" 51 "sigs.k8s.io/prow/pkg/slack" 52 "sigs.k8s.io/prow/pkg/version" 53 ) 54 55 var ( 56 pluginHelp = map[string]HelpProvider{} 57 genericCommentHandlers = map[string]GenericCommentHandler{} 58 issueHandlers = map[string]IssueHandler{} 59 issueCommentHandlers = map[string]IssueCommentHandler{} 60 pullRequestHandlers = map[string]PullRequestHandler{} 61 pushEventHandlers = map[string]PushEventHandler{} 62 reviewEventHandlers = map[string]ReviewEventHandler{} 63 reviewCommentEventHandlers = map[string]ReviewCommentEventHandler{} 64 statusEventHandlers = map[string]StatusEventHandler{} 65 // CommentMap is used by many plugins for printing help messages defined in 66 // config.go. 67 CommentMap, _ = genyaml.NewCommentMap(nil) 68 69 //go:embed config.go 70 embededConfigGoFileContent []byte 71 ) 72 73 func init() { 74 // This requires the source code to be present and to be in the right relative 75 // location to the working directory. Don't even bother to try outside of the 76 // hook binary, otherwise all components that load the plugin config initially 77 // show an error which is confusing. 78 if version.Name != "hook" { 79 return 80 } 81 82 if cm, err := genyaml.NewCommentMap(map[string][]byte{"prow/plugins/config.go": embededConfigGoFileContent}); err == nil { 83 CommentMap = cm 84 } else { 85 logrus.WithError(err).Error("Failed to initialize commentMap") 86 } 87 } 88 89 // HelpProvider defines the function type that construct a pluginhelp.PluginHelp for enabled 90 // plugins. It takes into account the plugins configuration and enabled repositories. 91 type HelpProvider func(config *Configuration, enabledRepos []config.OrgRepo) (*pluginhelp.PluginHelp, error) 92 93 // HelpProviders returns the map of registered plugins with their associated HelpProvider. 94 func HelpProviders() map[string]HelpProvider { 95 return pluginHelp 96 } 97 98 // IssueHandler defines the function contract for a github.IssueEvent handler. 99 type IssueHandler func(Agent, github.IssueEvent) error 100 101 // RegisterIssueHandler registers a plugin's github.IssueEvent handler. 102 func RegisterIssueHandler(name string, fn IssueHandler, help HelpProvider) { 103 pluginHelp[name] = help 104 issueHandlers[name] = fn 105 } 106 107 // IssueCommentHandler defines the function contract for a github.IssueCommentEvent handler. 108 type IssueCommentHandler func(Agent, github.IssueCommentEvent) error 109 110 // RegisterIssueCommentHandler registers a plugin's github.IssueCommentEvent handler. 111 func RegisterIssueCommentHandler(name string, fn IssueCommentHandler, help HelpProvider) { 112 pluginHelp[name] = help 113 issueCommentHandlers[name] = fn 114 } 115 116 // PullRequestHandler defines the function contract for a github.PullRequestEvent handler. 117 type PullRequestHandler func(Agent, github.PullRequestEvent) error 118 119 // RegisterPullRequestHandler registers a plugin's github.PullRequestEvent handler. 120 func RegisterPullRequestHandler(name string, fn PullRequestHandler, help HelpProvider) { 121 pluginHelp[name] = help 122 pullRequestHandlers[name] = fn 123 } 124 125 // StatusEventHandler defines the function contract for a github.StatusEvent handler. 126 type StatusEventHandler func(Agent, github.StatusEvent) error 127 128 // RegisterStatusEventHandler registers a plugin's github.StatusEvent handler. 129 func RegisterStatusEventHandler(name string, fn StatusEventHandler, help HelpProvider) { 130 pluginHelp[name] = help 131 statusEventHandlers[name] = fn 132 } 133 134 // PushEventHandler defines the function contract for a github.PushEvent handler. 135 type PushEventHandler func(Agent, github.PushEvent) error 136 137 // RegisterPushEventHandler registers a plugin's github.PushEvent handler. 138 func RegisterPushEventHandler(name string, fn PushEventHandler, help HelpProvider) { 139 pluginHelp[name] = help 140 pushEventHandlers[name] = fn 141 } 142 143 // ReviewEventHandler defines the function contract for a github.ReviewEvent handler. 144 type ReviewEventHandler func(Agent, github.ReviewEvent) error 145 146 // RegisterReviewEventHandler registers a plugin's github.ReviewEvent handler. 147 func RegisterReviewEventHandler(name string, fn ReviewEventHandler, help HelpProvider) { 148 pluginHelp[name] = help 149 reviewEventHandlers[name] = fn 150 } 151 152 // ReviewCommentEventHandler defines the function contract for a github.ReviewCommentEvent handler. 153 type ReviewCommentEventHandler func(Agent, github.ReviewCommentEvent) error 154 155 // RegisterReviewCommentEventHandler registers a plugin's github.ReviewCommentEvent handler. 156 func RegisterReviewCommentEventHandler(name string, fn ReviewCommentEventHandler, help HelpProvider) { 157 pluginHelp[name] = help 158 reviewCommentEventHandlers[name] = fn 159 } 160 161 // GenericCommentHandler defines the function contract for a github.GenericCommentEvent handler. 162 type GenericCommentHandler func(Agent, github.GenericCommentEvent) error 163 164 // RegisterGenericCommentHandler registers a plugin's github.GenericCommentEvent handler. 165 func RegisterGenericCommentHandler(name string, fn GenericCommentHandler, help HelpProvider) { 166 pluginHelp[name] = help 167 genericCommentHandlers[name] = fn 168 } 169 170 type PluginGitHubClient interface { 171 github.Client 172 Query(ctx context.Context, q interface{}, vars map[string]interface{}) error 173 } 174 175 // Agent may be used concurrently, so each entry must be thread-safe. 176 type Agent struct { 177 GitHubClient PluginGitHubClient 178 ProwJobClient prowv1.ProwJobInterface 179 KubernetesClient kubernetes.Interface 180 BuildClusterCoreV1Clients map[string]corev1.CoreV1Interface 181 GitClient git.ClientFactory 182 SlackClient *slack.Client 183 BugzillaClient bugzilla.Client 184 JiraClient jira.Client 185 186 OwnersClient repoowners.Interface 187 188 // Metrics exposes metrics that can be updated by plugins 189 Metrics *Metrics 190 191 // Config provides information about the jobs 192 // that we know how to run for repos. 193 Config *config.Config 194 // PluginConfig provides plugin-specific options 195 PluginConfig *Configuration 196 197 Logger *logrus.Entry 198 199 // may be nil if not initialized 200 commentPruner *commentpruner.EventClient 201 } 202 203 // NewAgent bootstraps a new config.Agent struct from the passed dependencies. 204 func NewAgent(configAgent *config.Agent, pluginConfigAgent *ConfigAgent, clientAgent *ClientAgent, githubOrg string, metrics *Metrics, logger *logrus.Entry, plugin string) Agent { 205 logger = logger.WithField("plugin", plugin) 206 prowConfig := configAgent.Config() 207 pluginConfig := pluginConfigAgent.Config() 208 gitHubClient := &githubV4OrgAddingWrapper{org: githubOrg, Client: clientAgent.GitHubClient.WithFields(logger.Data).ForPlugin(plugin)} 209 jiraClient := clientAgent.JiraClient 210 if jiraClient != nil { 211 jiraClient = clientAgent.JiraClient.WithFields(logger.Data).ForPlugin(plugin) 212 } 213 return Agent{ 214 GitHubClient: gitHubClient, 215 KubernetesClient: clientAgent.KubernetesClient, 216 BuildClusterCoreV1Clients: clientAgent.BuildClusterCoreV1Clients, 217 ProwJobClient: clientAgent.ProwJobClient, 218 GitClient: clientAgent.GitClient, 219 SlackClient: clientAgent.SlackClient, 220 OwnersClient: clientAgent.OwnersClient.WithFields(logger.Data).WithGitHubClient(gitHubClient).ForPlugin(plugin), 221 BugzillaClient: clientAgent.BugzillaClient.WithFields(logger.Data).ForPlugin(plugin), 222 JiraClient: jiraClient, 223 Metrics: metrics, 224 Config: prowConfig, 225 PluginConfig: pluginConfig, 226 Logger: logger, 227 } 228 } 229 230 // InitializeCommentPruner attaches a commentpruner.EventClient to the agent to handle 231 // pruning comments. 232 func (a *Agent) InitializeCommentPruner(org, repo string, pr int) { 233 a.commentPruner = commentpruner.NewEventClient( 234 a.GitHubClient, a.Logger.WithField("client", "commentpruner"), 235 org, repo, pr, 236 ) 237 } 238 239 // TookAction indicates whether any client with implemented Used() function was used 240 func (a *Agent) TookAction() bool { 241 jiraClientTookAction := false 242 if a.JiraClient != nil { 243 jiraClientTookAction = a.JiraClient.Used() 244 } 245 return a.GitHubClient.Used() || a.OwnersClient.Used() || a.BugzillaClient.Used() || jiraClientTookAction 246 } 247 248 // CommentPruner will return the commentpruner.EventClient attached to the agent or an error 249 // if one is not attached. 250 func (a *Agent) CommentPruner() (*commentpruner.EventClient, error) { 251 if a.commentPruner == nil { 252 return nil, errors.New("comment pruner client never initialized") 253 } 254 return a.commentPruner, nil 255 } 256 257 // ClientAgent contains the various clients that are attached to the Agent. 258 type ClientAgent struct { 259 GitHubClient github.Client 260 ProwJobClient prowv1.ProwJobInterface 261 KubernetesClient kubernetes.Interface 262 BuildClusterCoreV1Clients map[string]corev1.CoreV1Interface 263 GitClient git.ClientFactory 264 SlackClient *slack.Client 265 OwnersClient repoowners.Interface 266 BugzillaClient bugzilla.Client 267 JiraClient jira.Client 268 } 269 270 // ConfigAgent contains the agent mutex and the Agent configuration. 271 type ConfigAgent struct { 272 mut sync.Mutex 273 configuration *Configuration 274 } 275 276 func NewFakeConfigAgent() ConfigAgent { 277 return ConfigAgent{configuration: &Configuration{}} 278 } 279 280 // Load attempts to load config from the path. It returns an error if either 281 // the file can't be read or the configuration is invalid. 282 // If checkUnknownPlugins is true, unrecognized plugin names will make config 283 // loading fail. 284 // If skipResolveConfigUpdater is true, the ConfigUpdater of the config will not be resolved. 285 func (pa *ConfigAgent) Load(path string, supplementalPluginConfigDirs []string, supplementalPluginConfigFileSuffix string, checkUnknownPlugins, skipResolveConfigUpdater bool) error { 286 b, err := os.ReadFile(path) 287 if err != nil { 288 return err 289 } 290 np := &Configuration{} 291 if err := yaml.Unmarshal(b, np); err != nil { 292 return err 293 } 294 295 var errs []error 296 for _, supplementalPluginConfigDir := range supplementalPluginConfigDirs { 297 if supplementalPluginConfigFileSuffix == "" { 298 break 299 } 300 if err := filepath.Walk(supplementalPluginConfigDir, func(path string, info fs.FileInfo, err error) error { 301 if err != nil { 302 return err 303 } 304 305 // Kubernetes configmap mounts create symlinks for the configmap keys that point to files prefixed with '..'. 306 // This allows it to do atomic changes by changing the symlink to a new target when the configmap content changes. 307 // This means that we should ignore the '..'-prefixed files, otherwise we might end up reading a half-written file and will 308 // get duplicate data. 309 if strings.HasPrefix(info.Name(), "..") { 310 if info.IsDir() { 311 return filepath.SkipDir 312 } 313 return nil 314 } 315 316 if info.IsDir() || !strings.HasSuffix(path, supplementalPluginConfigFileSuffix) { 317 return nil 318 } 319 320 data, err := os.ReadFile(path) 321 if err != nil { 322 errs = append(errs, fmt.Errorf("failed to read %s: %w", path, err)) 323 return nil 324 } 325 326 cfg := &Configuration{} 327 if err := yaml.Unmarshal(data, cfg); err != nil { 328 errs = append(errs, fmt.Errorf("failed to unmarshal %s: %w", path, err)) 329 return nil 330 } 331 332 if err := np.mergeFrom(cfg); err != nil { 333 errs = append(errs, fmt.Errorf("failed to merge config from %s into main config: %w", path, err)) 334 } 335 336 return nil 337 338 }); err != nil { 339 errs = append(errs, fmt.Errorf("failed to walk %s: %w", supplementalPluginConfigDir, err)) 340 } 341 } 342 if err := utilerrors.NewAggregate(errs); err != nil { 343 return err 344 } 345 346 if err := np.Validate(); err != nil { 347 return err 348 } 349 if checkUnknownPlugins { 350 if err := np.ValidatePluginsUnknown(); err != nil { 351 return err 352 } 353 } 354 if !skipResolveConfigUpdater { 355 if err := np.ConfigUpdater.resolve(); err != nil { 356 return err 357 } 358 } 359 360 pa.Set(np) 361 return nil 362 } 363 364 // Config returns the agent current Configuration. 365 func (pa *ConfigAgent) Config() *Configuration { 366 pa.mut.Lock() 367 defer pa.mut.Unlock() 368 return pa.configuration 369 } 370 371 // Set attempts to set the plugins that are enabled on repos. Plugins are listed 372 // as a map from repositories to the list of plugins that are enabled on them. 373 // Specifying simply an org name will also work, and will enable the plugin on 374 // all repos in the org. 375 func (pa *ConfigAgent) Set(pc *Configuration) { 376 pa.mut.Lock() 377 defer pa.mut.Unlock() 378 pa.configuration = pc 379 } 380 381 // Start starts polling path for plugin config. If the first attempt fails, 382 // then start returns the error. Future errors will halt updates but not stop. 383 // If checkUnknownPlugins is true, unrecognized plugin names will make config 384 // loading fail. 385 func (pa *ConfigAgent) Start(path string, supplementalPluginConfigDirs []string, supplementalPluginConfigFileSuffix string, checkUnknownPlugins, skipResolveConfigUpdater bool) error { 386 if err := pa.Load(path, supplementalPluginConfigDirs, supplementalPluginConfigFileSuffix, checkUnknownPlugins, skipResolveConfigUpdater); err != nil { 387 return err 388 } 389 ticker := time.NewTicker(time.Minute) 390 go func() { 391 for range ticker.C { 392 if err := pa.Load(path, supplementalPluginConfigDirs, supplementalPluginConfigFileSuffix, checkUnknownPlugins, skipResolveConfigUpdater); err != nil { 393 logrus.WithField("path", path).WithError(err).Error("Error loading plugin config.") 394 } 395 } 396 }() 397 return nil 398 } 399 400 // GenericCommentHandlers returns a map of plugin names to handlers for the repo. 401 func (pa *ConfigAgent) GenericCommentHandlers(owner, repo string) map[string]GenericCommentHandler { 402 pa.mut.Lock() 403 defer pa.mut.Unlock() 404 405 hs := map[string]GenericCommentHandler{} 406 for _, p := range pa.getPlugins(owner, repo) { 407 if h, ok := genericCommentHandlers[p]; ok { 408 hs[p] = h 409 } 410 } 411 return hs 412 } 413 414 // IssueHandlers returns a map of plugin names to handlers for the repo. 415 func (pa *ConfigAgent) IssueHandlers(owner, repo string) map[string]IssueHandler { 416 pa.mut.Lock() 417 defer pa.mut.Unlock() 418 419 hs := map[string]IssueHandler{} 420 for _, p := range pa.getPlugins(owner, repo) { 421 if h, ok := issueHandlers[p]; ok { 422 hs[p] = h 423 } 424 } 425 return hs 426 } 427 428 // IssueCommentHandlers returns a map of plugin names to handlers for the repo. 429 func (pa *ConfigAgent) IssueCommentHandlers(owner, repo string) map[string]IssueCommentHandler { 430 pa.mut.Lock() 431 defer pa.mut.Unlock() 432 433 hs := map[string]IssueCommentHandler{} 434 for _, p := range pa.getPlugins(owner, repo) { 435 if h, ok := issueCommentHandlers[p]; ok { 436 hs[p] = h 437 } 438 } 439 440 return hs 441 } 442 443 // PullRequestHandlers returns a map of plugin names to handlers for the repo. 444 func (pa *ConfigAgent) PullRequestHandlers(owner, repo string) map[string]PullRequestHandler { 445 pa.mut.Lock() 446 defer pa.mut.Unlock() 447 448 hs := map[string]PullRequestHandler{} 449 for _, p := range pa.getPlugins(owner, repo) { 450 if h, ok := pullRequestHandlers[p]; ok { 451 hs[p] = h 452 } 453 } 454 455 return hs 456 } 457 458 // ReviewEventHandlers returns a map of plugin names to handlers for the repo. 459 func (pa *ConfigAgent) ReviewEventHandlers(owner, repo string) map[string]ReviewEventHandler { 460 pa.mut.Lock() 461 defer pa.mut.Unlock() 462 463 hs := map[string]ReviewEventHandler{} 464 for _, p := range pa.getPlugins(owner, repo) { 465 if h, ok := reviewEventHandlers[p]; ok { 466 hs[p] = h 467 } 468 } 469 470 return hs 471 } 472 473 // ReviewCommentEventHandlers returns a map of plugin names to handlers for the repo. 474 func (pa *ConfigAgent) ReviewCommentEventHandlers(owner, repo string) map[string]ReviewCommentEventHandler { 475 pa.mut.Lock() 476 defer pa.mut.Unlock() 477 478 hs := map[string]ReviewCommentEventHandler{} 479 for _, p := range pa.getPlugins(owner, repo) { 480 if h, ok := reviewCommentEventHandlers[p]; ok { 481 hs[p] = h 482 } 483 } 484 485 return hs 486 } 487 488 // StatusEventHandlers returns a map of plugin names to handlers for the repo. 489 func (pa *ConfigAgent) StatusEventHandlers(owner, repo string) map[string]StatusEventHandler { 490 pa.mut.Lock() 491 defer pa.mut.Unlock() 492 493 hs := map[string]StatusEventHandler{} 494 for _, p := range pa.getPlugins(owner, repo) { 495 if h, ok := statusEventHandlers[p]; ok { 496 hs[p] = h 497 } 498 } 499 500 return hs 501 } 502 503 // PushEventHandlers returns a map of plugin names to handlers for the repo. 504 func (pa *ConfigAgent) PushEventHandlers(owner, repo string) map[string]PushEventHandler { 505 pa.mut.Lock() 506 defer pa.mut.Unlock() 507 508 hs := map[string]PushEventHandler{} 509 for _, p := range pa.getPlugins(owner, repo) { 510 if h, ok := pushEventHandlers[p]; ok { 511 hs[p] = h 512 } 513 } 514 515 return hs 516 } 517 518 // getPlugins returns a list of plugins that are enabled on a given (org, repository). 519 func (pa *ConfigAgent) getPlugins(owner, repo string) []string { 520 var plugins []string 521 522 fullName := fmt.Sprintf("%s/%s", owner, repo) 523 if !sets.NewString(pa.configuration.Plugins[owner].ExcludedRepos...).Has(repo) { 524 plugins = append(plugins, pa.configuration.Plugins[owner].Plugins...) 525 } 526 plugins = append(plugins, pa.configuration.Plugins[fullName].Plugins...) 527 528 return plugins 529 } 530 531 // EventsForPlugin returns the registered events for the passed plugin. 532 func EventsForPlugin(name string) []string { 533 var events []string 534 if _, ok := issueHandlers[name]; ok { 535 events = append(events, "issue") 536 } 537 if _, ok := issueCommentHandlers[name]; ok { 538 events = append(events, "issue_comment") 539 } 540 if _, ok := pullRequestHandlers[name]; ok { 541 events = append(events, "pull_request") 542 } 543 if _, ok := pushEventHandlers[name]; ok { 544 events = append(events, "push") 545 } 546 if _, ok := reviewEventHandlers[name]; ok { 547 events = append(events, "pull_request_review") 548 } 549 if _, ok := reviewCommentEventHandlers[name]; ok { 550 events = append(events, "pull_request_review_comment") 551 } 552 if _, ok := statusEventHandlers[name]; ok { 553 events = append(events, "status") 554 } 555 if _, ok := genericCommentHandlers[name]; ok { 556 events = append(events, "GenericCommentEvent (any event for user text)") 557 } 558 return events 559 } 560 561 var configMapSizeGauges = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 562 Name: "prow_configmap_size_bytes", 563 Help: "Size of data fields in ConfigMaps updated automatically by Prow in bytes.", 564 }, []string{"name", "namespace"}) 565 566 func init() { 567 prometheus.MustRegister(configMapSizeGauges) 568 } 569 570 // Metrics is a set of metrics that are gathered by plugins. 571 // It is up the consumers of these metrics to ensure that they 572 // update the values in a thread-safe manner. 573 type Metrics struct { 574 ConfigMapGauges *prometheus.GaugeVec 575 } 576 577 // NewMetrics returns a reference to the metrics plugins manage 578 func NewMetrics() *Metrics { 579 return &Metrics{ 580 ConfigMapGauges: configMapSizeGauges, 581 } 582 } 583 584 type githubV4OrgAddingWrapper struct { 585 org string 586 github.Client 587 } 588 589 func (c *githubV4OrgAddingWrapper) Query(ctx context.Context, q interface{}, args map[string]interface{}) error { 590 return c.QueryWithGitHubAppsSupport(ctx, q, args, c.org) 591 }