sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/config.go (about) 1 /* 2 Copyright 2018 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 "encoding/json" 21 "errors" 22 "fmt" 23 "path" 24 "reflect" 25 "regexp" 26 "sort" 27 "strings" 28 "time" 29 30 "sigs.k8s.io/yaml" 31 32 "github.com/sirupsen/logrus" 33 34 "github.com/google/go-cmp/cmp" 35 utilerrors "k8s.io/apimachinery/pkg/util/errors" 36 "k8s.io/apimachinery/pkg/util/sets" 37 38 "sigs.k8s.io/prow/pkg/bugzilla" 39 "sigs.k8s.io/prow/pkg/config" 40 "sigs.k8s.io/prow/pkg/kube" 41 "sigs.k8s.io/prow/pkg/labels" 42 "sigs.k8s.io/prow/pkg/logrusutil" 43 "sigs.k8s.io/prow/pkg/plugins/ownersconfig" 44 ) 45 46 const ( 47 defaultBlunderbussReviewerCount = 2 48 ) 49 50 // Configuration is the top-level serialization target for plugin Configuration. 51 type Configuration struct { 52 // Plugins is a map of organizations (eg "o") or repositories 53 // (eg "o/r") to lists of enabled plugin names. 54 // If it is defined on both organization and repository levels, the list of enabled 55 // plugin names for the repository is the merging list of the two levels. 56 // You can find a comprehensive list of the default available plugins here 57 // https://github.com/kubernetes-sigs/prow/tree/main/pkg/plugins 58 // note that you're also able to add external plugins. 59 Plugins Plugins `json:"plugins,omitempty"` 60 61 // ExternalPlugins is a map of repositories (eg "k/k") to lists of 62 // external plugins. 63 ExternalPlugins map[string][]ExternalPlugin `json:"external_plugins,omitempty"` 64 65 // Owners contains configuration related to handling OWNERS files. 66 Owners Owners `json:"owners,omitempty"` 67 68 // Built-in plugins specific configuration. 69 Approve []Approve `json:"approve,omitempty"` 70 Blockades []Blockade `json:"blockades,omitempty"` 71 Blunderbuss Blunderbuss `json:"blunderbuss,omitempty"` 72 Bugzilla Bugzilla `json:"bugzilla,omitempty"` 73 BranchCleaner BranchCleaner `json:"branch_cleaner,omitempty"` 74 Cat Cat `json:"cat,omitempty"` 75 CherryPickApproved []CherryPickApproved `json:"cherry_pick_approved,omitempty"` 76 CherryPickUnapproved CherryPickUnapproved `json:"cherry_pick_unapproved,omitempty"` 77 ConfigUpdater ConfigUpdater `json:"config_updater,omitempty"` 78 Dco map[string]*Dco `json:"dco,omitempty"` 79 Golint Golint `json:"golint,omitempty"` 80 Goose Goose `json:"goose,omitempty"` 81 Heart Heart `json:"heart,omitempty"` 82 Label Label `json:"label,omitempty"` 83 Lgtm []Lgtm `json:"lgtm,omitempty"` 84 Jira *Jira `json:"jira,omitempty"` 85 MilestoneApplier map[string]BranchToMilestone `json:"milestone_applier,omitempty"` 86 RepoMilestone map[string]Milestone `json:"repo_milestone,omitempty"` 87 Project ProjectConfig `json:"project_config,omitempty"` 88 ProjectManager ProjectManager `json:"project_manager,omitempty"` 89 RequireMatchingLabel []RequireMatchingLabel `json:"require_matching_label,omitempty"` 90 Retitle Retitle `json:"retitle,omitempty"` 91 Slack Slack `json:"slack,omitempty"` 92 SigMention SigMention `json:"sigmention,omitempty"` 93 Size Size `json:"size,omitempty"` 94 Triggers []Trigger `json:"triggers,omitempty"` 95 Welcome []Welcome `json:"welcome,omitempty"` 96 Override Override `json:"override,omitempty"` 97 Help Help `json:"help,omitempty"` 98 } 99 100 type Help struct { 101 // HelpGuidelinesURL is the URL of the help page, which provides guidance on how and when to use the help wanted and good first issue labels. 102 // The default value is "https://git.k8s.io/community/contributors/guide/help-wanted.md". 103 HelpGuidelinesURL string `json:"help_guidelines_url,omitempty"` 104 // Guidelines summary is the message displayed when an issue is labeled with help-wanted and/or good-first-issue reflecting 105 // a summary of the guidelines that an issue should follow to qualify as help-wanted or good-first-issue. The main purpose 106 // of a summary is to try and increase visibility of these guidelines to the author of the issue alongisde providing the 107 // HelpGuidelinesURL which will provide a more detailed version of the guidelines. 108 // 109 // HelpGuidelinesSummary is the summary of the guide lines for a help-wanted issue. 110 HelpGuidelinesSummary string `json:"help_guidelines_summary,omitempty"` 111 } 112 113 func (h *Help) setDefaults() { 114 if h.HelpGuidelinesURL == "" { 115 h.HelpGuidelinesURL = "https://git.k8s.io/community/contributors/guide/help-wanted.md" 116 } 117 } 118 119 // Golint holds configuration for the golint plugin 120 type Golint struct { 121 // MinimumConfidence is the smallest permissible confidence 122 // in (0,1] over which problems will be printed. Defaults to 123 // 0.8, as does the `go lint` tool. 124 MinimumConfidence *float64 `json:"minimum_confidence,omitempty"` 125 } 126 127 // Plugins maps orgOrRepo to plugins 128 type Plugins map[string]OrgPlugins 129 130 type OrgPlugins struct { 131 ExcludedRepos []string `json:"excluded_repos,omitempty"` 132 Plugins []string `json:"plugins,omitempty"` 133 } 134 135 // ExternalPlugin holds configuration for registering an external 136 // plugin in prow. 137 type ExternalPlugin struct { 138 // Name of the plugin. 139 Name string `json:"name"` 140 // Endpoint is the location of the external plugin. Defaults to 141 // the name of the plugin, ie. "http://{{name}}". 142 Endpoint string `json:"endpoint,omitempty"` 143 // Events are the events that need to be demuxed by the hook 144 // server to the external plugin. If no events are specified, 145 // everything is sent. 146 Events []string `json:"events,omitempty"` 147 } 148 149 // Blunderbuss defines configuration for the blunderbuss plugin. 150 type Blunderbuss struct { 151 // ReviewerCount is the minimum number of reviewers to request 152 // reviews from. Defaults to requesting reviews from 2 reviewers 153 ReviewerCount *int `json:"request_count,omitempty"` 154 // MaxReviewerCount is the maximum number of reviewers to request 155 // reviews from. Defaults to 0 meaning no limit. 156 MaxReviewerCount int `json:"max_request_count,omitempty"` 157 // ExcludeApprovers controls whether approvers are considered to be 158 // reviewers. By default, approvers are considered as reviewers if 159 // insufficient reviewers are available. If ExcludeApprovers is true, 160 // approvers will never be considered as reviewers. 161 ExcludeApprovers bool `json:"exclude_approvers,omitempty"` 162 // UseStatusAvailability controls whether blunderbuss will consider GitHub's 163 // status availability when requesting reviews for users. This will use at one 164 // additional token per successful reviewer (and potentially more depending on 165 // how many busy reviewers it had to pass over). 166 UseStatusAvailability bool `json:"use_status_availability,omitempty"` 167 // IgnoreDrafts instructs the plugin to ignore assigning reviewers 168 // to the PR that is in Draft state. Default it's false. 169 IgnoreDrafts bool `json:"ignore_drafts,omitempty"` 170 // IgnoreAuthors skips requesting reviewers for specified users. 171 // This is useful when a bot user or admin opens a PR that will be 172 // merged regardless of approvals. 173 IgnoreAuthors []string `json:"ignore_authors,omitempty"` 174 } 175 176 // Owners contains configuration related to handling OWNERS files. 177 type Owners struct { 178 // MDYAMLRepos is a list of org and org/repo strings specifying the repos that support YAML 179 // OWNERS config headers at the top of markdown (*.md) files. These headers function just like 180 // the config in an OWNERS file, but only apply to the file itself instead of the entire 181 // directory and all sub-directories. 182 // The yaml header must be at the start of the file and be bracketed with "---" like so: 183 /* 184 --- 185 approvers: 186 - mikedanese 187 - thockin 188 189 --- 190 */ 191 MDYAMLRepos []string `json:"mdyamlrepos,omitempty"` 192 193 // SkipCollaborators disables collaborator cross-checks and forces both 194 // the approve and lgtm plugins to use solely OWNERS files for access 195 // control in the provided repos. 196 SkipCollaborators []string `json:"skip_collaborators,omitempty"` 197 198 // LabelsDenyList holds a list of labels that should not be present in any 199 // OWNERS file, preventing their automatic addition by the owners-label plugin. 200 // This check is performed by the verify-owners plugin. 201 LabelsDenyList []string `json:"labels_denylist,omitempty"` 202 203 // Filenames allows configuring repos to use a separate set of filenames for 204 // any plugin that interacts with these files. Keys are in "org" or "org/repo" format. 205 Filenames map[string]ownersconfig.Filenames `json:"filenames,omitempty"` 206 } 207 208 // OwnersFilenames determines which filenames to use for OWNERS and OWNERS_ALIASES for a repo. 209 func (c *Configuration) OwnersFilenames(org, repo string) ownersconfig.Filenames { 210 full := fmt.Sprintf("%s/%s", org, repo) 211 212 if config, configured := c.Owners.Filenames[full]; configured { 213 return config 214 } 215 216 if config, configured := c.Owners.Filenames[org]; configured { 217 return config 218 } 219 220 return ownersconfig.Filenames{ 221 Owners: ownersconfig.DefaultOwnersFile, 222 OwnersAliases: ownersconfig.DefaultOwnersAliasesFile, 223 } 224 } 225 226 // MDYAMLEnabled returns a boolean denoting if the passed repo supports YAML OWNERS config headers 227 // at the top of markdown (*.md) files. These function like OWNERS files but only apply to the file 228 // itself. 229 func (c *Configuration) MDYAMLEnabled(org, repo string) bool { 230 full := fmt.Sprintf("%s/%s", org, repo) 231 for _, elem := range c.Owners.MDYAMLRepos { 232 if elem == org || elem == full { 233 return true 234 } 235 } 236 return false 237 } 238 239 // SkipCollaborators returns a boolean denoting if collaborator cross-checks are enabled for 240 // the passed repo. If it's true, approve and lgtm plugins rely solely on OWNERS files. 241 func (c *Configuration) SkipCollaborators(org, repo string) bool { 242 full := fmt.Sprintf("%s/%s", org, repo) 243 for _, elem := range c.Owners.SkipCollaborators { 244 if elem == org || elem == full { 245 return true 246 } 247 } 248 return false 249 } 250 251 // Retitle specifies configuration for the retitle plugin. 252 type Retitle struct { 253 // AllowClosedIssues allows retitling closed/merged issues and PRs. 254 AllowClosedIssues bool `json:"allow_closed_issues,omitempty"` 255 } 256 257 // SigMention specifies configuration for the sigmention plugin. 258 type SigMention struct { 259 // Regexp parses comments and should return matches to team mentions. 260 // These mentions enable labeling issues or PRs with sig/team labels. 261 // Furthermore, teams with the following suffixes will be mapped to 262 // kind/* labels: 263 // 264 // * @org/team-bugs --maps to--> kind/bug 265 // * @org/team-feature-requests --maps to--> kind/feature 266 // * @org/team-api-reviews --maps to--> kind/api-change 267 // * @org/team-proposals --maps to--> kind/design 268 // 269 // Note that you need to make sure your regexp covers the above 270 // mentions if you want to use the extra labeling. Defaults to: 271 // (?m)@kubernetes/sig-([\w-]*)-(misc|test-failures|bugs|feature-requests|proposals|pr-reviews|api-reviews) 272 // 273 // Compiles into Re during config load. 274 Regexp string `json:"regexp,omitempty"` 275 Re *regexp.Regexp `json:"-"` 276 } 277 278 // Size specifies configuration for the size plugin, defining lower bounds (in # lines changed) for each size label. 279 // XS is assumed to be zero. 280 type Size struct { 281 S int `json:"s"` 282 M int `json:"m"` 283 L int `json:"l"` 284 Xl int `json:"xl"` 285 Xxl int `json:"xxl"` 286 } 287 288 // Blockade specifies a configuration for a single blockade. 289 // 290 // The configuration for the blockade plugin is defined as a list of these structures. 291 type Blockade struct { 292 // Repos are either of the form org/repos or just org. 293 Repos []string `json:"repos,omitempty"` 294 // BranchRegexp is the regular expression for branches that the blockade applies to. 295 // If BranchRegexp is not specified, the blockade applies to all branches by default. 296 // Compiles into BranchRe during config load. 297 BranchRegexp *string `json:"branchregexp,omitempty"` 298 BranchRe *regexp.Regexp `json:"-"` 299 // BlockRegexps are regular expressions matching the file paths to block. 300 BlockRegexps []string `json:"blockregexps,omitempty"` 301 // ExceptionRegexps are regular expressions matching the file paths that are exceptions to the BlockRegexps. 302 ExceptionRegexps []string `json:"exceptionregexps,omitempty"` 303 // Explanation is a string that will be included in the comment left when blocking a PR. This should 304 // be an explanation of why the paths specified are blockaded. 305 Explanation string `json:"explanation,omitempty"` 306 } 307 308 // Approve specifies a configuration for a single approve. 309 // 310 // The configuration for the approve plugin is defined as a list of these structures. 311 type Approve struct { 312 // Repos is either of the form org/repos or just org. 313 Repos []string `json:"repos,omitempty"` 314 // IssueRequired indicates if an associated issue is required for approval in 315 // the specified repos. 316 IssueRequired bool `json:"issue_required,omitempty"` 317 // RequireSelfApproval disables automatic approval from PR authors with approval rights. 318 // Otherwise the plugin assumes the author of the PR with approval rights approves the changes in the PR. 319 RequireSelfApproval *bool `json:"require_self_approval,omitempty"` 320 // LgtmActsAsApprove indicates that the lgtm command should be used to 321 // indicate approval 322 LgtmActsAsApprove bool `json:"lgtm_acts_as_approve,omitempty"` 323 // IgnoreReviewState causes the approve plugin to ignore the GitHub review state. Otherwise: 324 // * an APPROVE github review is equivalent to leaving an "/approve" message. 325 // * A REQUEST_CHANGES github review is equivalent to leaving an /approve cancel" message. 326 IgnoreReviewState *bool `json:"ignore_review_state,omitempty"` 327 // CommandHelpLink is the link to the help page which shows the available commands for each repo. 328 // The default value is "https://go.k8s.io/bot-commands". The command help page is served by Deck 329 // and available under https://<deck-url>/command-help, e.g. "https://prow.k8s.io/command-help" 330 CommandHelpLink string `json:"commandHelpLink"` 331 // PrProcessLink is the link to the help page which explains the code review process. 332 // The default value is "https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process". 333 PrProcessLink string `json:"pr_process_link,omitempty"` 334 } 335 336 var ( 337 warnDependentBugTargetRelease time.Time 338 ) 339 340 func (a Approve) HasSelfApproval() bool { 341 if a.RequireSelfApproval != nil { 342 return !*a.RequireSelfApproval 343 } 344 return true 345 } 346 347 func (a Approve) ConsiderReviewState() bool { 348 if a.IgnoreReviewState != nil { 349 return !*a.IgnoreReviewState 350 } 351 return true 352 } 353 354 func (a Approve) getRepos() []string { 355 return a.Repos 356 } 357 358 // Lgtm specifies a configuration for a single lgtm. 359 // The configuration for the lgtm plugin is defined as a list of these structures. 360 type Lgtm struct { 361 // Repos is either of the form org/repos or just org. 362 Repos []string `json:"repos,omitempty"` 363 // ReviewActsAsLgtm indicates that a GitHub review of "approve" or "request changes" 364 // acts as adding or removing the lgtm label 365 ReviewActsAsLgtm bool `json:"review_acts_as_lgtm,omitempty"` 366 // StoreTreeHash indicates if tree_hash should be stored inside a comment to detect 367 // squashed commits before removing lgtm labels 368 StoreTreeHash bool `json:"store_tree_hash,omitempty"` 369 // WARNING: This disables the security mechanism that prevents a malicious member (or 370 // compromised GitHub account) from merging arbitrary code. Use with caution. 371 // 372 // StickyLgtmTeam specifies the GitHub team whose members are trusted with sticky LGTM, 373 // which eliminates the need to re-lgtm minor fixes/updates. 374 StickyLgtmTeam string `json:"trusted_team_for_sticky_lgtm,omitempty"` 375 } 376 377 // Jira holds the config for the jira plugin. 378 type Jira struct { 379 // DisabledJiraProjects are projects for which we will never try to create a link, 380 // for example including `enterprise` here would disable linking for all issues 381 // that start with `enterprise-` like `enterprise-4.` Matching is case-insenitive. 382 DisabledJiraProjects []string `json:"disabled_jira_projects,omitempty"` 383 } 384 385 // Cat contains the configuration for the cat plugin. 386 type Cat struct { 387 // Path to file containing an api key for thecatapi.com 388 KeyPath string `json:"key_path,omitempty"` 389 } 390 391 // Goose contains the configuration for the goose plugin. 392 type Goose struct { 393 // Path to file containing an api key for unsplash.com 394 KeyPath string `json:"key_path,omitempty"` 395 } 396 397 // Label contains the configuration for the label plugin. 398 type Label struct { 399 // AdditionalLabels is a set of additional labels enabled for use 400 // on top of the existing "kind/*", "priority/*", and "area/*" labels. 401 AdditionalLabels []string `json:"additional_labels,omitempty"` 402 403 // RestrictedLabels allows to configure labels that can only be modified 404 // by users that belong to at least one of the configured teams. The key 405 // defines to which repos this applies and can be `*` for global, an org 406 // or a repo in org/repo notation. 407 RestrictedLabels map[string][]RestrictedLabel `json:"restricted_labels,omitempty"` 408 } 409 410 func (l Label) RestrictedLabelsFor(org, repo string) map[string]RestrictedLabel { 411 result := map[string]RestrictedLabel{} 412 for _, orgRepoKey := range []string{"*", org, org + "/" + repo} { 413 for _, restrictedLabel := range l.RestrictedLabels[orgRepoKey] { 414 result[strings.ToLower(restrictedLabel.Label)] = restrictedLabel 415 } 416 } 417 418 return result 419 } 420 421 func (l Label) IsRestrictedLabelInAdditionalLables(restricted string) bool { 422 for _, additional := range l.AdditionalLabels { 423 if restricted == additional { 424 return true 425 } 426 } 427 return false 428 } 429 430 type RestrictedLabel struct { 431 Label string `json:"label"` 432 AllowedTeams []string `json:"allowed_teams,omitempty"` 433 AllowedUsers []string `json:"allowed_users,omitempty"` 434 AssignOn []AssignOnLabel `json:"assign_on,omitempty"` 435 } 436 437 // AssignOnLabel specifies the label that would trigger the RestrictedLabel.AllowedUsers' 438 // to be assigned on the PR. 439 type AssignOnLabel struct { 440 Label string `json:"label"` 441 } 442 443 // Trigger specifies a configuration for a single trigger. 444 // 445 // The configuration for the trigger plugin is defined as a list of these structures. 446 type Trigger struct { 447 // Repos is either of the form org/repos or just org. 448 Repos []string `json:"repos,omitempty"` 449 // TrustedApps is the explicit list of GitHub apps whose PRs will be automatically 450 // considered as trusted. The list should contain usernames of each GitHub App without [bot] suffix. 451 // By default, trigger will ignore this list. 452 TrustedApps []string `json:"trusted_apps,omitempty"` 453 // TrustedOrg is the org whose members' PRs will be automatically built for 454 // PRs to the above repos. The default is the PR's org. 455 // 456 // Deprecated: TrustedOrg functionality is deprecated and will be removed in 457 // January 2020. 458 TrustedOrg string `json:"trusted_org,omitempty"` 459 // JoinOrgURL is a link that redirects users to a location where they 460 // should be able to read more about joining the organization in order 461 // to become trusted members. Defaults to the GitHub link of TrustedOrg. 462 JoinOrgURL string `json:"join_org_url,omitempty"` 463 // OnlyOrgMembers requires PRs and/or /ok-to-test comments to come from org members. 464 // By default, trigger also include repo collaborators. 465 OnlyOrgMembers bool `json:"only_org_members,omitempty"` 466 // IgnoreOkToTest makes trigger ignore /ok-to-test comments. 467 // This is a security mitigation to only allow testing from trusted users. 468 IgnoreOkToTest bool `json:"ignore_ok_to_test,omitempty"` 469 // TriggerGitHubWorkflows enables workflows run by github to be triggered by prow. 470 TriggerGitHubWorkflows bool `json:"trigger_github_workflows,omitempty"` 471 } 472 473 // Heart contains the configuration for the heart plugin. 474 type Heart struct { 475 // Adorees is a list of GitHub logins for members 476 // for whom we will add emojis to comments 477 Adorees []string `json:"adorees,omitempty"` 478 // CommentRegexp is the regular expression for comments 479 // made by adorees that the plugin adds emojis to. 480 // If not specified, the plugin will not add emojis to 481 // any comments. 482 // Compiles into CommentRe during config load. 483 CommentRegexp string `json:"commentregexp,omitempty"` 484 CommentRe *regexp.Regexp `json:"-"` 485 } 486 487 // Milestone contains the configuration options for the milestone and 488 // milestonestatus plugins. 489 type Milestone struct { 490 // ID of the github team for the milestone maintainers (used for setting status labels) 491 // You can curl the following endpoint in order to determine the github ID of your team 492 // responsible for maintaining the milestones: 493 // curl -H "Authorization: token <token>" https://api.github.com/orgs/<org-name>/teams 494 // Deprecated: use MaintainersTeam instead 495 MaintainersID int `json:"maintainers_id,omitempty"` 496 MaintainersTeam string `json:"maintainers_team,omitempty"` 497 MaintainersFriendlyName string `json:"maintainers_friendly_name,omitempty"` 498 } 499 500 // BranchToMilestone is a map of the branch name to the configured milestone for that branch. 501 // This is used by the milestoneapplier plugin. 502 type BranchToMilestone map[string]string 503 504 // Slack contains the configuration for the slack plugin. 505 type Slack struct { 506 MentionChannels []string `json:"mentionchannels,omitempty"` 507 MergeWarnings []MergeWarning `json:"mergewarnings,omitempty"` 508 } 509 510 // ConfigMapSpec contains configuration options for the configMap being updated 511 // by the config-updater plugin. 512 type ConfigMapSpec struct { 513 // Name of ConfigMap 514 Name string `json:"name"` 515 // PartitionedNames is a slice of names of ConfigMaps that the keys should be balanced across. 516 // This is useful when no explicit key is given and file names/paths are used as keys instead. 517 // This is used to work around the 1MB ConfigMap size limit by spreading the keys across multiple 518 // separate ConfigMaps. 519 // PartitionedNames is mutually exclusive with the "Name" field. 520 PartitionedNames []string `json:"partitioned_names,omitempty"` 521 // Key is the key in the ConfigMap to update with the file contents. 522 // If no explicit key is given, the basename of the file will be used unless 523 // use_full_path_as_key: true is set, in which case the full filepath relative 524 // to the repository root will be used, replacing slashes with dashes. 525 Key string `json:"key,omitempty"` 526 // GZIP toggles whether the key's data should be GZIP'd before being stored 527 // If set to false and the global GZIP option is enabled, this file will 528 // will not be GZIP'd. 529 GZIP *bool `json:"gzip,omitempty"` 530 // Clusters is a map from cluster to namespaces 531 // which specifies the targets the configMap needs to be deployed, i.e., each namespace in map[cluster] 532 Clusters map[string][]string `json:"clusters,omitempty"` 533 // ClusterGroup is a list of named cluster_groups to target. Mutually exclusive with clusters. 534 ClusterGroups []string `json:"cluster_groups,omitempty"` 535 // UseFullPathAsKey controls if the full path of the original file relative to the 536 // repository root should be used as the configmap key. Slashes will be replaced by 537 // dashes. Using this avoids the need for unique file names in the original repo. 538 UseFullPathAsKey bool `json:"use_full_path_as_key,omitempty"` 539 } 540 541 // A ClusterGroup is a list of clusters with namespaces 542 type ClusterGroup struct { 543 Clusters []string `json:"clusters,omitempty"` 544 Namespaces []string `json:"namespaces,omitempty"` 545 } 546 547 // ConfigUpdater contains the configuration for the config-updater plugin. 548 type ConfigUpdater struct { 549 // ClusterGroups is a map of ClusterGroups that can be used as a target 550 // in the map config. 551 ClusterGroups map[string]ClusterGroup `json:"cluster_groups,omitempty"` 552 // A map of filename => ConfigMapSpec. 553 // Whenever a commit changes filename, prow will update the corresponding configmap. 554 // map[string]ConfigMapSpec{ "/my/path.yaml": {Name: "foo", Namespace: "otherNamespace" }} 555 // will result in replacing the foo configmap whenever path.yaml changes 556 Maps map[string]ConfigMapSpec `json:"maps,omitempty"` 557 // If GZIP is true then files will be gzipped before insertion into 558 // their corresponding configmap 559 GZIP bool `json:"gzip"` 560 } 561 562 type configUpdatedWithoutUnmarshaler ConfigUpdater 563 564 func (cu *ConfigUpdater) UnmarshalJSON(d []byte) error { 565 var target configUpdatedWithoutUnmarshaler 566 if err := json.Unmarshal(d, &target); err != nil { 567 return err 568 } 569 *cu = ConfigUpdater(target) 570 return nil 571 } 572 573 func (cu *ConfigUpdater) resolve() error { 574 if err := validateConfigUpdater(cu); err != nil { 575 return err 576 } 577 var errs []error 578 for k, v := range cu.Maps { 579 if len(v.Clusters) > 0 { 580 continue 581 } 582 583 clusters := map[string][]string{} 584 for _, clusterGroupName := range v.ClusterGroups { 585 clusterGroup := cu.ClusterGroups[clusterGroupName] 586 for _, cluster := range clusterGroup.Clusters { 587 clusters[cluster] = append(clusters[cluster], clusterGroup.Namespaces...) 588 } 589 } 590 591 cu.Maps[k] = ConfigMapSpec{ 592 Name: v.Name, 593 PartitionedNames: v.PartitionedNames, 594 Key: v.Key, 595 GZIP: v.GZIP, 596 Clusters: clusters, 597 } 598 } 599 600 cu.ClusterGroups = nil 601 602 return utilerrors.NewAggregate(errs) 603 } 604 605 // ProjectConfig contains the configuration options for the project plugin 606 type ProjectConfig struct { 607 // Org level configs for github projects; key is org name 608 Orgs map[string]ProjectOrgConfig `json:"project_org_configs,omitempty"` 609 } 610 611 // ProjectOrgConfig holds the github project config for an entire org. 612 // This can be overridden by ProjectRepoConfig. 613 type ProjectOrgConfig struct { 614 // ID of the github project maintainer team for a give project or org 615 MaintainerTeamID int `json:"org_maintainers_team_id,omitempty"` 616 // A map of project name to default column; an issue/PR will be added 617 // to the default column if column name is not provided in the command 618 ProjectColumnMap map[string]string `json:"org_default_column_map,omitempty"` 619 // Repo level configs for github projects; key is repo name 620 Repos map[string]ProjectRepoConfig `json:"project_repo_configs,omitempty"` 621 } 622 623 // ProjectRepoConfig holds the github project config for a github project. 624 type ProjectRepoConfig struct { 625 // ID of the github project maintainer team for a give project or org 626 MaintainerTeamID int `json:"repo_maintainers_team_id,omitempty"` 627 // A map of project name to default column; an issue/PR will be added 628 // to the default column if column name is not provided in the command 629 ProjectColumnMap map[string]string `json:"repo_default_column_map,omitempty"` 630 } 631 632 // ProjectManager represents the config for the ProjectManager plugin, holding top 633 // level config options, configuration is a hierarchial structure with top level element 634 // being org or org/repo with the list of projects as its children 635 type ProjectManager struct { 636 OrgRepos map[string]ManagedOrgRepo `json:"orgsRepos,omitempty"` 637 } 638 639 // ManagedOrgRepo is used by the ProjectManager plugin to represent an Organisation 640 // or Repository with a list of Projects 641 type ManagedOrgRepo struct { 642 Projects map[string]ManagedProject `json:"projects,omitempty"` 643 } 644 645 // ManagedProject is used by the ProjectManager plugin to represent a Project 646 // with a list of Columns 647 type ManagedProject struct { 648 Columns []ManagedColumn `json:"columns,omitempty"` 649 } 650 651 // ManagedColumn is used by the ProjectQueries plugin to represent a project column 652 // and the conditions to add a PR to that column 653 type ManagedColumn struct { 654 // Either of ID or Name should be specified 655 ID *int `json:"id,omitempty"` 656 Name string `json:"name,omitempty"` 657 // State must be open, closed or all 658 State string `json:"state,omitempty"` 659 // all the labels here should match to the incoming event to be bale to add the card to the project 660 Labels []string `json:"labels,omitempty"` 661 // Configuration is effective is the issue events repo/Owner/Login matched the org 662 Org string `json:"org,omitempty"` 663 } 664 665 // MergeWarning is a config for the slackevents plugin's manual merge warnings. 666 // If a PR is pushed to any of the repos listed in the config then send messages 667 // to the all the slack channels listed if pusher is NOT in the allowlist. 668 type MergeWarning struct { 669 // Repos is either of the form org/repos or just org. 670 Repos []string `json:"repos,omitempty"` 671 // List of channels on which a event is published. 672 Channels []string `json:"channels,omitempty"` 673 // A slack event is published if the user is not part of the ExemptUsers. 674 ExemptUsers []string `json:"exempt_users,omitempty"` 675 // A slack event is published if the user is not on the exempt branches. 676 ExemptBranches map[string][]string `json:"exempt_branches,omitempty"` 677 } 678 679 // Welcome is config for the welcome plugin. 680 type Welcome struct { 681 // Repos is either of the form org/repos or just org. 682 Repos []string `json:"repos,omitempty"` 683 // MessageTemplate is the welcome message template to post on new-contributor PRs 684 // For the info struct see prow/plugins/welcome/welcome.go's PRInfo 685 MessageTemplate string `json:"message_template,omitempty"` 686 // Post welcome message in all cases, even if PR author is not an existing 687 // contributor or part of the organization 688 AlwaysPost bool `json:"always_post,omitempty"` 689 } 690 691 func (w Welcome) getRepos() []string { 692 return w.Repos 693 } 694 695 // Dco is config for the DCO (https://developercertificate.org/) checker plugin. 696 type Dco struct { 697 // SkipDCOCheckForMembers is used to skip DCO check for trusted org members 698 SkipDCOCheckForMembers bool `json:"skip_dco_check_for_members,omitempty"` 699 // TrustedApps defines list of apps which commits will not be checked for DCO singoff. 700 // The list should contain usernames of each GitHub App without [bot] suffix. 701 // By default, this option is ignored. 702 TrustedApps []string `json:"trusted_apps,omitempty"` 703 // TrustedOrg is the org whose members' commits will not be checked for DCO signoff 704 // if the skip DCO option is enabled. The default is the PR's org. 705 TrustedOrg string `json:"trusted_org,omitempty"` 706 // SkipDCOCheckForCollaborators is used to skip DCO check for trusted org members 707 SkipDCOCheckForCollaborators bool `json:"skip_dco_check_for_collaborators,omitempty"` 708 // ContributingRepo is used to point users to a different repo containing CONTRIBUTING.md 709 ContributingRepo string `json:"contributing_repo,omitempty"` 710 // ContributingBranch allows setting a custom branch where to find CONTRIBUTING.md 711 ContributingBranch string `json:"contributing_branch,omitempty"` 712 // ContributingPath is used to override the default path to CONTRIBUTING.md 713 ContributingPath string `json:"contributing_path,omitempty"` 714 } 715 716 // CherryPickApproved is the config for the cherrypick-approved plugin. 717 type CherryPickApproved struct { 718 // Org is the GitHub organization that this config applies to. 719 Org string `json:"org,omitempty"` 720 // Repo is the GitHub repository within Org that this config applies to. 721 Repo string `json:"repo,omitempty"` 722 // BranchRegexp is the regular expression for branch names such that 723 // the plugin treats only PRs against these branch names as cherrypick PRs. 724 // Compiles into BranchRe during config load. 725 BranchRegexp string `json:"branchregexp,omitempty"` 726 BranchRe *regexp.Regexp `json:"-"` 727 // Approvers is the list of GitHub logins allowed to approve a cherry-pick. 728 Approvers []string `json:"approvers,omitempty"` 729 } 730 731 // CherryPickUnapproved is the config for the cherrypick-unapproved plugin. 732 type CherryPickUnapproved struct { 733 // BranchRegexp is the regular expression for branch names such that 734 // the plugin treats only PRs against these branch names as cherrypick PRs. 735 // Compiles into BranchRe during config load. 736 BranchRegexp string `json:"branchregexp,omitempty"` 737 BranchRe *regexp.Regexp `json:"-"` 738 // Comment is the comment added by the plugin while adding the 739 // `do-not-merge/cherry-pick-not-approved` label. 740 Comment string `json:"comment,omitempty"` 741 } 742 743 // RequireMatchingLabel is the config for the require-matching-label plugin. 744 type RequireMatchingLabel struct { 745 // Org is the GitHub organization that this config applies to. 746 Org string `json:"org,omitempty"` 747 // Repo is the GitHub repository within Org that this config applies to. 748 // This fields may be omitted to apply this config across all repos in Org. 749 Repo string `json:"repo,omitempty"` 750 // Branch is the branch ref of PRs that this config applies to. 751 // This field is only valid if `prs: true` and may be omitted to apply this 752 // config across all branches in the repo or org. 753 Branch string `json:"branch,omitempty"` 754 // PRs is a bool indicating if this config applies to PRs. 755 PRs bool `json:"prs,omitempty"` 756 // Issues is a bool indicating if this config applies to issues. 757 Issues bool `json:"issues,omitempty"` 758 759 // Regexp is the string specifying the regular expression used to look for 760 // matching labels. 761 Regexp string `json:"regexp,omitempty"` 762 // Re is the compiled version of Regexp. It should not be specified in config. 763 Re *regexp.Regexp `json:"-"` 764 765 // MissingLabel is the label to apply if an issue does not have any label 766 // matching the Regexp. 767 MissingLabel string `json:"missing_label,omitempty"` 768 // MissingComment is the comment to post when we add the MissingLabel to an 769 // issue. This is typically used to explain why MissingLabel was added and 770 // how to move forward. 771 // This field is optional. If unspecified, no comment is created when labeling. 772 MissingComment string `json:"missing_comment,omitempty"` 773 774 // GracePeriod is the amount of time to wait before processing newly opened 775 // or reopened issues and PRs. This delay allows other automation to apply 776 // labels before we look for matching labels. 777 // Defaults to '5s'. 778 GracePeriod string `json:"grace_period,omitempty"` 779 GracePeriodDuration time.Duration `json:"-"` 780 } 781 782 // validate checks the following properties: 783 // - Org, Regexp, MissingLabel, and GracePeriod must be non-empty. 784 // - Repo does not contain a '/' (should use Org+Repo). 785 // - At least one of PRs or Issues must be true. 786 // - Branch only specified if 'prs: true' 787 // - MissingLabel must not match Regexp. 788 func (r RequireMatchingLabel) validate() error { 789 if r.Org == "" { 790 return errors.New("must specify 'org'") 791 } 792 if strings.Contains(r.Repo, "/") { 793 return errors.New("'repo' may not contain '/'; specify the organization with 'org'") 794 } 795 if r.Regexp == "" { 796 return errors.New("must specify 'regexp'") 797 } 798 if r.MissingLabel == "" { 799 return errors.New("must specify 'missing_label'") 800 } 801 if r.GracePeriod == "" { 802 return errors.New("must specify 'grace_period'") 803 } 804 if !r.PRs && !r.Issues { 805 return errors.New("must specify 'prs: true' and/or 'issues: true'") 806 } 807 if !r.PRs && r.Branch != "" { 808 return errors.New("branch cannot be specified without `prs: true'") 809 } 810 if r.Re.MatchString(r.MissingLabel) { 811 return errors.New("'regexp' must not match 'missing_label'") 812 } 813 return nil 814 } 815 816 // Describe generates a human readable description of the behavior that this 817 // configuration specifies. 818 func (r RequireMatchingLabel) Describe() string { 819 str := &strings.Builder{} 820 fmt.Fprintf(str, "Applies the '%s' label ", r.MissingLabel) 821 if r.MissingComment == "" { 822 fmt.Fprint(str, "to ") 823 } else { 824 fmt.Fprint(str, "and comments on ") 825 } 826 827 if r.Issues { 828 fmt.Fprint(str, "Issues ") 829 if r.PRs { 830 fmt.Fprint(str, "and ") 831 } 832 } 833 if r.PRs { 834 if r.Branch != "" { 835 fmt.Fprintf(str, "'%s' branch ", r.Branch) 836 } 837 fmt.Fprint(str, "PRs ") 838 } 839 840 if r.Repo == "" { 841 fmt.Fprintf(str, "in the '%s' GitHub org ", r.Org) 842 } else { 843 fmt.Fprintf(str, "in the '%s/%s' GitHub repo ", r.Org, r.Repo) 844 } 845 fmt.Fprintf(str, "that have no labels matching the regular expression '%s'.", r.Regexp) 846 return str.String() 847 } 848 849 // ApproveFor finds the Approve for a repo, if one exists. 850 // Approval configuration can be listed for a repository 851 // or an organization. 852 func (c *Configuration) ApproveFor(org, repo string) *Approve { 853 fullName := fmt.Sprintf("%s/%s", org, repo) 854 855 a := func() *Approve { 856 // First search for repo config 857 for _, approve := range c.Approve { 858 if !sets.New[string](approve.Repos...).Has(fullName) { 859 continue 860 } 861 return &approve 862 } 863 864 // If you don't find anything, loop again looking for an org config 865 for _, approve := range c.Approve { 866 if !sets.New[string](approve.Repos...).Has(org) { 867 continue 868 } 869 return &approve 870 } 871 872 // Return an empty config, and use plugin defaults 873 return &Approve{} 874 }() 875 if a.CommandHelpLink == "" { 876 a.CommandHelpLink = "https://go.k8s.io/bot-commands" 877 } 878 if a.PrProcessLink == "" { 879 a.PrProcessLink = "https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process" 880 } 881 return a 882 } 883 884 // LgtmFor finds the Lgtm for a repo, if one exists 885 // a trigger can be listed for the repo itself or for the 886 // owning organization 887 func (c *Configuration) LgtmFor(org, repo string) *Lgtm { 888 fullName := fmt.Sprintf("%s/%s", org, repo) 889 for _, lgtm := range c.Lgtm { 890 if !sets.New[string](lgtm.Repos...).Has(fullName) { 891 continue 892 } 893 return &lgtm 894 } 895 // If you don't find anything, loop again looking for an org config 896 for _, lgtm := range c.Lgtm { 897 if !sets.New[string](lgtm.Repos...).Has(org) { 898 continue 899 } 900 return &lgtm 901 } 902 return &Lgtm{} 903 } 904 905 // TriggerFor finds the Trigger for a repo, if one exists 906 // a trigger can be listed for the repo itself or for the 907 // owning organization 908 func (c *Configuration) TriggerFor(org, repo string) Trigger { 909 fullName := fmt.Sprintf("%s/%s", org, repo) 910 // Prioritize repo level triggers over org level triggers. 911 for _, trigger := range c.Triggers { 912 if !sets.NewString(trigger.Repos...).Has(fullName) { 913 continue 914 } 915 return trigger 916 } 917 // If you don't find anything, loop again looking for an org config 918 for _, trigger := range c.Triggers { 919 if !sets.NewString(trigger.Repos...).Has(org) { 920 continue 921 } 922 return trigger 923 } 924 925 var tr Trigger 926 tr.SetDefaults() 927 return tr 928 } 929 930 func (t *Trigger) SetDefaults() { 931 if t.TrustedOrg != "" && t.JoinOrgURL == "" { 932 t.JoinOrgURL = fmt.Sprintf("https://github.com/orgs/%s/people", t.TrustedOrg) 933 } 934 } 935 936 // DcoFor finds the Dco for a repo, if one exists 937 // a Dco can be listed for the repo itself or for the 938 // owning organization 939 func (c *Configuration) DcoFor(org, repo string) *Dco { 940 if c.Dco[fmt.Sprintf("%s/%s", org, repo)] != nil { 941 return c.Dco[fmt.Sprintf("%s/%s", org, repo)] 942 } 943 if c.Dco[org] != nil { 944 return c.Dco[org] 945 } 946 if c.Dco["*"] != nil { 947 return c.Dco["*"] 948 } 949 return &Dco{} 950 } 951 952 func OldToNewPlugins(oldPlugins map[string][]string) Plugins { 953 newPlugins := make(Plugins) 954 for repo, plugins := range oldPlugins { 955 newPlugins[repo] = OrgPlugins{ 956 Plugins: plugins, 957 } 958 } 959 return newPlugins 960 } 961 962 type pluginsWithoutUnmarshaler Plugins 963 964 var warnTriggerDeprecatedConfig time.Time 965 966 func (p *Plugins) UnmarshalJSON(d []byte) error { 967 var oldPlugins map[string][]string 968 if err := yaml.Unmarshal(d, &oldPlugins); err == nil { 969 logrusutil.ThrottledWarnf(&warnTriggerDeprecatedConfig, time.Hour, "plugins declaration uses a deprecated config style, see https://github.com/kubernetes/test-infra/issues/20631#issuecomment-787693609 for a migration guide") 970 *p = OldToNewPlugins(oldPlugins) 971 return nil 972 } 973 var target pluginsWithoutUnmarshaler 974 err := yaml.Unmarshal(d, &target) 975 *p = Plugins(target) 976 return err 977 } 978 979 // EnabledReposForPlugin returns the orgs and repos that have enabled the passed plugin. 980 func (c *Configuration) EnabledReposForPlugin(plugin string) (orgs, repos []string, orgExceptions map[string]sets.Set[string]) { 981 orgExceptions = make(map[string]sets.Set[string]) 982 for repo, plugins := range c.Plugins { 983 found := false 984 for _, candidate := range plugins.Plugins { 985 if candidate == plugin { 986 found = true 987 break 988 } 989 } 990 if found { 991 if strings.Contains(repo, "/") { 992 repos = append(repos, repo) 993 } else { 994 orgs = append(orgs, repo) 995 orgExceptions[repo] = sets.New[string]() 996 for _, excludedRepo := range plugins.ExcludedRepos { 997 orgExceptions[repo].Insert(fmt.Sprintf("%s/%s", repo, excludedRepo)) 998 } 999 } 1000 } 1001 } 1002 // <plugin> plugin might be declared in both org and org/repo 1003 // in that case, remove repo from org's orgExceptions despite the excluded_repo in org 1004 for _, repo := range repos { 1005 orgExceptions[strings.Split(repo, "/")[0]].Delete(repo) 1006 } 1007 return 1008 } 1009 1010 // EnabledReposForExternalPlugin returns the orgs and repos that have enabled the passed 1011 // external plugin. 1012 func (c *Configuration) EnabledReposForExternalPlugin(plugin string) (orgs, repos []string) { 1013 for repo, plugins := range c.ExternalPlugins { 1014 found := false 1015 for _, candidate := range plugins { 1016 if candidate.Name == plugin { 1017 found = true 1018 break 1019 } 1020 } 1021 if found { 1022 if strings.Contains(repo, "/") { 1023 repos = append(repos, repo) 1024 } else { 1025 orgs = append(orgs, repo) 1026 } 1027 } 1028 } 1029 return 1030 } 1031 1032 // SetDefaults sets default options for config updating 1033 func (cu *ConfigUpdater) SetDefaults() { 1034 if len(cu.Maps) == 0 { 1035 cu.Maps = map[string]ConfigMapSpec{ 1036 "config/prow/config.yaml": { 1037 Name: "config", 1038 }, 1039 "config/prow/plugins.yaml": { 1040 Name: "plugins", 1041 }, 1042 } 1043 } 1044 1045 for name, spec := range cu.Maps { 1046 if len(spec.Clusters) == 0 && len(spec.ClusterGroups) == 0 { 1047 spec.Clusters = map[string][]string{kube.DefaultClusterAlias: {""}} 1048 } 1049 cu.Maps[name] = spec 1050 } 1051 } 1052 1053 func (c *Configuration) setDefaults() { 1054 c.Help.setDefaults() 1055 1056 c.ConfigUpdater.SetDefaults() 1057 1058 for repo, plugins := range c.ExternalPlugins { 1059 for i, p := range plugins { 1060 if p.Endpoint != "" { 1061 continue 1062 } 1063 c.ExternalPlugins[repo][i].Endpoint = fmt.Sprintf("http://%s", p.Name) 1064 } 1065 } 1066 if c.Blunderbuss.ReviewerCount == nil { 1067 c.Blunderbuss.ReviewerCount = new(int) 1068 *c.Blunderbuss.ReviewerCount = defaultBlunderbussReviewerCount 1069 } 1070 for i := range c.Triggers { 1071 c.Triggers[i].SetDefaults() 1072 } 1073 if c.SigMention.Regexp == "" { 1074 c.SigMention.Regexp = `(?m)@kubernetes/sig-([\w-]*)-(misc|test-failures|bugs|feature-requests|proposals|pr-reviews|api-reviews)` 1075 } 1076 if c.Owners.LabelsDenyList == nil { 1077 c.Owners.LabelsDenyList = []string{labels.Approved, labels.LGTM} 1078 } 1079 for _, milestone := range c.RepoMilestone { 1080 if milestone.MaintainersFriendlyName == "" { 1081 milestone.MaintainersFriendlyName = "SIG Chairs/TLs" 1082 } 1083 } 1084 if c.CherryPickUnapproved.BranchRegexp == "" { 1085 c.CherryPickUnapproved.BranchRegexp = `^release-.*$` 1086 } 1087 if c.CherryPickUnapproved.Comment == "" { 1088 c.CherryPickUnapproved.Comment = `This PR is not for the master branch but does not have the ` + "`cherry-pick-approved`" + ` label. Adding the ` + "`do-not-merge/cherry-pick-not-approved`" + ` label.` 1089 } 1090 1091 for i := range c.CherryPickApproved { 1092 if c.CherryPickApproved[i].BranchRegexp == "" { 1093 c.CherryPickApproved[i].BranchRegexp = `^release-.*$` 1094 } 1095 } 1096 1097 for i, rml := range c.RequireMatchingLabel { 1098 if rml.GracePeriod == "" { 1099 c.RequireMatchingLabel[i].GracePeriod = "5s" 1100 } 1101 } 1102 } 1103 1104 // validatePluginsDupes will return an error if there are duplicated plugins. 1105 // It is sometimes a sign of misconfiguration and is always useless for a 1106 // plugin to be specified at both the org and repo levels. 1107 func validatePluginsDupes(plugins Plugins) error { 1108 var errors []error 1109 for repo, repoConfig := range plugins { 1110 if strings.Contains(repo, "/") { 1111 org := strings.Split(repo, "/")[0] 1112 if dupes := findDuplicatedPluginConfig(repoConfig.Plugins, plugins[org].Plugins); len(dupes) > 0 { 1113 errors = append(errors, fmt.Errorf("plugins %v are duplicated for %s and %s", dupes, repo, org)) 1114 } 1115 } 1116 } 1117 return utilerrors.NewAggregate(errors) 1118 } 1119 1120 // ValidatePluginsUnknown will return an error if there are any unrecognized 1121 // plugins configured. 1122 func (c *Configuration) ValidatePluginsUnknown() error { 1123 var errors []error 1124 for _, configuration := range c.Plugins { 1125 for _, plugin := range configuration.Plugins { 1126 if _, ok := pluginHelp[plugin]; !ok { 1127 errors = append(errors, fmt.Errorf("unknown plugin: %s", plugin)) 1128 } 1129 } 1130 } 1131 return utilerrors.NewAggregate(errors) 1132 } 1133 1134 func validateSizes(size Size) error { 1135 if size.S > size.M || size.M > size.L || size.L > size.Xl || size.Xl > size.Xxl { 1136 return errors.New("invalid size plugin configuration - one of the smaller sizes is bigger than a larger one") 1137 } 1138 1139 return nil 1140 } 1141 1142 func findDuplicatedPluginConfig(repoConfig, orgConfig []string) []string { 1143 var dupes []string 1144 for _, repoPlugin := range repoConfig { 1145 for _, orgPlugin := range orgConfig { 1146 if repoPlugin == orgPlugin { 1147 dupes = append(dupes, repoPlugin) 1148 } 1149 } 1150 } 1151 1152 return dupes 1153 } 1154 1155 func validateExternalPlugins(pluginMap map[string][]ExternalPlugin) error { 1156 var errors []string 1157 1158 for repo, plugins := range pluginMap { 1159 if !strings.Contains(repo, "/") { 1160 continue 1161 } 1162 org := strings.Split(repo, "/")[0] 1163 1164 var orgConfig []string 1165 for _, p := range pluginMap[org] { 1166 orgConfig = append(orgConfig, p.Name) 1167 } 1168 1169 var repoConfig []string 1170 for _, p := range plugins { 1171 repoConfig = append(repoConfig, p.Name) 1172 } 1173 1174 if dupes := findDuplicatedPluginConfig(repoConfig, orgConfig); len(dupes) > 0 { 1175 errors = append(errors, fmt.Sprintf("external plugins %v are duplicated for %s and %s", dupes, repo, org)) 1176 } 1177 } 1178 1179 if len(errors) > 0 { 1180 return fmt.Errorf("invalid plugin configuration:\n\t%v", strings.Join(errors, "\n\t")) 1181 } 1182 return nil 1183 } 1184 1185 func validateBlunderbuss(b *Blunderbuss) error { 1186 if b.ReviewerCount != nil && *b.ReviewerCount < 1 { 1187 return fmt.Errorf("invalid request_count: %v (needs to be positive)", *b.ReviewerCount) 1188 } 1189 return nil 1190 } 1191 1192 // ConfigMapID is a name/namespace/cluster combination that identifies a config map 1193 type ConfigMapID struct { 1194 Name, Namespace, Cluster string 1195 } 1196 1197 func validateConfigUpdater(updater *ConfigUpdater) error { 1198 updater.SetDefaults() 1199 configMapKeys := map[ConfigMapID]sets.Set[string]{} 1200 for file, config := range updater.Maps { 1201 // Check that Name and PartitionedNames are mutually exclusive 1202 if config.Name != "" && len(config.PartitionedNames) > 0 { 1203 return errors.New("'name' and 'partitioned_names' are mutually exclusive in the config_updater plugin configuration") 1204 } 1205 name := config.Name 1206 if name == "" { 1207 name = strings.Join(config.PartitionedNames, ",") 1208 } 1209 // Check that PartitionedNames doesn't use too many partitions. 1210 if len(config.PartitionedNames) > 256 { 1211 return fmt.Errorf("the PartitionedNames field in config_updater plugin config currently supports a maximum of 256 partitions, but you have %d defined", len(config.PartitionedNames)) 1212 } 1213 // Check that keys are not associated with multiple files. 1214 for cluster, namespaces := range config.Clusters { 1215 for _, namespace := range namespaces { 1216 cmID := ConfigMapID{ 1217 Name: name, 1218 Namespace: namespace, 1219 Cluster: cluster, 1220 } 1221 1222 key := config.Key 1223 if key == "" { 1224 key = path.Base(file) 1225 } 1226 1227 if _, ok := configMapKeys[cmID]; ok { 1228 if configMapKeys[cmID].Has(key) { 1229 return fmt.Errorf("key %s in configmap %s updated with more than one file", key, name) 1230 } 1231 configMapKeys[cmID].Insert(key) 1232 } else { 1233 configMapKeys[cmID] = sets.New[string](key) 1234 } 1235 } 1236 } 1237 } 1238 var errs []error 1239 for k, v := range updater.Maps { 1240 if len(v.Clusters) > 0 && len(v.ClusterGroups) > 0 { 1241 errs = append(errs, fmt.Errorf("item maps.%s contains both clusters and cluster_groups", k)) 1242 continue 1243 } 1244 1245 if len(v.Clusters) > 0 { 1246 continue 1247 } 1248 1249 for idx, clusterGroupName := range v.ClusterGroups { 1250 _, hasClusterGroup := updater.ClusterGroups[clusterGroupName] 1251 if !hasClusterGroup { 1252 errs = append(errs, fmt.Errorf("item maps.%s.cluster_groups.%d references inexistent cluster group named %s", k, idx, clusterGroupName)) 1253 continue 1254 } 1255 } 1256 } 1257 return utilerrors.NewAggregate(errs) 1258 } 1259 1260 func validateRequireMatchingLabel(rs []RequireMatchingLabel) error { 1261 for i, r := range rs { 1262 if err := r.validate(); err != nil { 1263 return fmt.Errorf("error validating require_matching_label config #%d: %w", i, err) 1264 } 1265 } 1266 return nil 1267 } 1268 1269 func validateProjectManager(pm ProjectManager) error { 1270 1271 projectConfig := pm 1272 // No ProjectManager configuration provided, we have nothing to validate 1273 if len(projectConfig.OrgRepos) == 0 { 1274 return nil 1275 } 1276 1277 for orgRepoName, managedOrgRepo := range pm.OrgRepos { 1278 if len(managedOrgRepo.Projects) == 0 { 1279 return fmt.Errorf("Org/repo: %s, has no projects configured", orgRepoName) 1280 } 1281 for projectName, managedProject := range managedOrgRepo.Projects { 1282 var labelSets []sets.Set[string] 1283 if len(managedProject.Columns) == 0 { 1284 return fmt.Errorf("Org/repo: %s, project %s, has no columns configured", orgRepoName, projectName) 1285 } 1286 for _, managedColumn := range managedProject.Columns { 1287 if managedColumn.ID == nil && (len(managedColumn.Name) == 0) { 1288 return fmt.Errorf("Org/repo: %s, project %s, column %v, has no name/id configured", orgRepoName, projectName, managedColumn) 1289 } 1290 if len(managedColumn.Labels) == 0 { 1291 return fmt.Errorf("Org/repo: %s, project %s, column %s, has no labels configured", orgRepoName, projectName, managedColumn.Name) 1292 } 1293 if len(managedColumn.Org) == 0 { 1294 return fmt.Errorf("Org/repo: %s, project %s, column %s, has no org configured", orgRepoName, projectName, managedColumn.Name) 1295 } 1296 sSet := sets.New[string](managedColumn.Labels...) 1297 for _, labels := range labelSets { 1298 if sSet.Equal(labels) { 1299 return fmt.Errorf("Org/repo: %s, project %s, column %s has same labels configured as another column", orgRepoName, projectName, managedColumn.Name) 1300 } 1301 } 1302 labelSets = append(labelSets, sSet) 1303 } 1304 } 1305 } 1306 return nil 1307 } 1308 1309 var warnTriggerTrustedOrg time.Time 1310 1311 func validateTrigger(triggers []Trigger) error { 1312 for _, trigger := range triggers { 1313 if trigger.TrustedOrg != "" { 1314 logrusutil.ThrottledWarnf(&warnTriggerTrustedOrg, 5*time.Minute, "trusted_org functionality is deprecated. Please ensure your configuration is updated before the end of December 2019.") 1315 } 1316 } 1317 return nil 1318 } 1319 1320 var warnRepoMilestone time.Time 1321 1322 func validateRepoMilestone(milestones map[string]Milestone) { 1323 for _, milestone := range milestones { 1324 if milestone.MaintainersID != 0 { 1325 logrusutil.ThrottledWarnf(&warnRepoMilestone, time.Hour, "deprecated field: maintainers_id is configured for repo_milestone, maintainers_team should be used instead") 1326 } 1327 } 1328 } 1329 1330 func compileRegexpsAndDurations(pc *Configuration) error { 1331 cRe, err := regexp.Compile(pc.SigMention.Regexp) 1332 if err != nil { 1333 return err 1334 } 1335 pc.SigMention.Re = cRe 1336 1337 unapprovedBranchRe, err := regexp.Compile(pc.CherryPickUnapproved.BranchRegexp) 1338 if err != nil { 1339 return err 1340 } 1341 pc.CherryPickUnapproved.BranchRe = unapprovedBranchRe 1342 1343 for i := range pc.CherryPickApproved { 1344 approvedBranchRe, err := regexp.Compile(pc.CherryPickApproved[i].BranchRegexp) 1345 if err != nil { 1346 return err 1347 } 1348 pc.CherryPickApproved[i].BranchRe = approvedBranchRe 1349 } 1350 1351 for i := range pc.Blockades { 1352 if pc.Blockades[i].BranchRegexp == nil { 1353 continue 1354 } 1355 branchRe, err := regexp.Compile(*pc.Blockades[i].BranchRegexp) 1356 if err != nil { 1357 return fmt.Errorf("failed to compile blockade branchregexp: %q, error: %w", *pc.Blockades[i].BranchRegexp, err) 1358 } 1359 pc.Blockades[i].BranchRe = branchRe 1360 } 1361 1362 commentRe, err := regexp.Compile(pc.Heart.CommentRegexp) 1363 if err != nil { 1364 return err 1365 } 1366 pc.Heart.CommentRe = commentRe 1367 1368 rs := pc.RequireMatchingLabel 1369 for i := range rs { 1370 re, err := regexp.Compile(rs[i].Regexp) 1371 if err != nil { 1372 return fmt.Errorf("failed to compile label regexp: %q, error: %w", rs[i].Regexp, err) 1373 } 1374 rs[i].Re = re 1375 1376 var dur time.Duration 1377 dur, err = time.ParseDuration(rs[i].GracePeriod) 1378 if err != nil { 1379 return fmt.Errorf("failed to compile grace period duration: %q, error: %w", rs[i].GracePeriod, err) 1380 } 1381 rs[i].GracePeriodDuration = dur 1382 } 1383 return nil 1384 } 1385 1386 func (c *Configuration) Validate() error { 1387 if len(c.Plugins) == 0 { 1388 logrus.Warn("no plugins specified-- check syntax?") 1389 } 1390 1391 // Defaulting should run before validation. 1392 c.setDefaults() 1393 // Regexp compilation should run after defaulting, but before validation. 1394 if err := compileRegexpsAndDurations(c); err != nil { 1395 return err 1396 } 1397 1398 if err := validatePluginsDupes(c.Plugins); err != nil { 1399 return err 1400 } 1401 if err := validateExternalPlugins(c.ExternalPlugins); err != nil { 1402 return err 1403 } 1404 if err := validateBlunderbuss(&c.Blunderbuss); err != nil { 1405 return err 1406 } 1407 if err := validateConfigUpdater(&c.ConfigUpdater); err != nil { 1408 return err 1409 } 1410 if err := validateSizes(c.Size); err != nil { 1411 return err 1412 } 1413 if err := validateRequireMatchingLabel(c.RequireMatchingLabel); err != nil { 1414 return err 1415 } 1416 if err := validateProjectManager(c.ProjectManager); err != nil { 1417 return err 1418 } 1419 if err := validateTrigger(c.Triggers); err != nil { 1420 return err 1421 } 1422 if err := validateRepoDupes(c.Approve); err != nil { 1423 return err 1424 } 1425 if err := validateRepoDupes(c.Welcome); err != nil { 1426 return err 1427 } 1428 validateRepoMilestone(c.RepoMilestone) 1429 1430 return nil 1431 } 1432 1433 type ListableRepos interface { 1434 getRepos() []string 1435 } 1436 1437 func validateRepoDupes[C ListableRepos](configs []C) error { 1438 var errs []error 1439 orgs := map[string]bool{} 1440 repos := map[string]bool{} 1441 for _, config := range configs { 1442 for _, entry := range config.getRepos() { 1443 if strings.Contains(entry, "/") { 1444 if repos[entry] { 1445 errs = append(errs, fmt.Errorf("The repo %q is duplicated in the 'welcome' plugin configuration.", entry)) 1446 } 1447 repos[entry] = true 1448 } else { 1449 if orgs[entry] { 1450 errs = append(errs, fmt.Errorf("The org %q is duplicated in the 'welcome' plugin configuration.", entry)) 1451 } 1452 orgs[entry] = true 1453 } 1454 } 1455 } 1456 return utilerrors.NewAggregate(errs) 1457 } 1458 1459 func (pluginConfig *ProjectConfig) GetMaintainerTeam(org string, repo string) int { 1460 for orgName, orgConfig := range pluginConfig.Orgs { 1461 if org == orgName { 1462 // look for repo level configs first because repo level config overrides org level configs 1463 for repoName, repoConfig := range orgConfig.Repos { 1464 if repo == repoName { 1465 return repoConfig.MaintainerTeamID 1466 } 1467 } 1468 return orgConfig.MaintainerTeamID 1469 } 1470 } 1471 return -1 1472 } 1473 1474 func (pluginConfig *ProjectConfig) GetColumnMap(org string, repo string) map[string]string { 1475 for orgName, orgConfig := range pluginConfig.Orgs { 1476 if org == orgName { 1477 for repoName, repoConfig := range orgConfig.Repos { 1478 if repo == repoName { 1479 return repoConfig.ProjectColumnMap 1480 } 1481 } 1482 return orgConfig.ProjectColumnMap 1483 } 1484 } 1485 return nil 1486 } 1487 1488 func (pluginConfig *ProjectConfig) GetOrgColumnMap(org string) map[string]string { 1489 for orgName, orgConfig := range pluginConfig.Orgs { 1490 if org == orgName { 1491 return orgConfig.ProjectColumnMap 1492 } 1493 } 1494 return nil 1495 } 1496 1497 // Bugzilla holds options for checking Bugzilla bugs in a defaulting hierarchy. 1498 type Bugzilla struct { 1499 // Default settings mapped by branch in any repo in any org. 1500 // The `*` wildcard will apply to all branches. 1501 Default map[string]BugzillaBranchOptions `json:"default,omitempty"` 1502 // Options for specific orgs. The `*` wildcard will apply to all orgs. 1503 Orgs map[string]BugzillaOrgOptions `json:"orgs,omitempty"` 1504 } 1505 1506 // BugzillaOrgOptions holds options for checking Bugzilla bugs for an org. 1507 type BugzillaOrgOptions struct { 1508 // Default settings mapped by branch in any repo in this org. 1509 // The `*` wildcard will apply to all branches. 1510 Default map[string]BugzillaBranchOptions `json:"default,omitempty"` 1511 // Options for specific repos. The `*` wildcard will apply to all repos. 1512 Repos map[string]BugzillaRepoOptions `json:"repos,omitempty"` 1513 } 1514 1515 // BugzillaRepoOptions holds options for checking Bugzilla bugs for a repo. 1516 type BugzillaRepoOptions struct { 1517 // Options for specific branches in this repo. 1518 // The `*` wildcard will apply to all branches. 1519 Branches map[string]BugzillaBranchOptions `json:"branches,omitempty"` 1520 } 1521 1522 // BugzillaBugState describes bug states in the Bugzilla plugin config, used 1523 // for example to specify states that bugs are supposed to be in or to which 1524 // they should be made after some action. 1525 type BugzillaBugState struct { 1526 Status string `json:"status,omitempty"` 1527 Resolution string `json:"resolution,omitempty"` 1528 } 1529 1530 // String converts a Bugzilla state into human-readable description 1531 func (s *BugzillaBugState) String() string { 1532 return bugzilla.PrettyStatus(s.Status, s.Resolution) 1533 } 1534 1535 // AsBugUpdate returns a BugUpdate struct for updating a given to bug to the 1536 // desired state. The returned struct will have only those fields set where the 1537 // state differs from the parameter bug. If the bug state matches the desired 1538 // state, returns nil. If the parameter bug is empty or a nil pointer, the 1539 // returned BugUpdate will have all fields set that are set in the state. 1540 func (s *BugzillaBugState) AsBugUpdate(bug *bugzilla.Bug) *bugzilla.BugUpdate { 1541 if s == nil { 1542 return nil 1543 } 1544 1545 var ret *bugzilla.BugUpdate 1546 var update bugzilla.BugUpdate 1547 1548 if s.Status != "" && (bug == nil || s.Status != bug.Status) { 1549 ret = &update 1550 update.Status = s.Status 1551 } 1552 if s.Resolution != "" && (bug == nil || s.Resolution != bug.Resolution) { 1553 ret = &update 1554 update.Resolution = s.Resolution 1555 } 1556 1557 return ret 1558 } 1559 1560 // Matches returns whether a given bug matches the state 1561 func (s *BugzillaBugState) Matches(bug *bugzilla.Bug) bool { 1562 if s == nil || bug == nil { 1563 return false 1564 } 1565 if s.Status != "" && s.Status != bug.Status { 1566 return false 1567 } 1568 1569 if s.Resolution != "" && s.Resolution != bug.Resolution { 1570 return false 1571 } 1572 return true 1573 } 1574 1575 // BugzillaBranchOptions describes how to check if a Bugzilla bug is valid or not. 1576 // 1577 // Note on `Status` vs `State` fields: `State` fields implement a superset of 1578 // functionality provided by the `Status` fields and are meant to eventually 1579 // supersede `Status` fields. Implementations using these structures should 1580 // *only* use `Status` fields or only `States` fields, never both. The 1581 // implementation mirrors `Status` fields into the matching `State` fields in 1582 // the `ResolveBugzillaOptions` method to handle existing config, and is also 1583 // able to sufficiently resolve the presence of both types of fields. 1584 type BugzillaBranchOptions struct { 1585 // ExcludeDefaults excludes defaults from more generic Bugzilla configurations. 1586 ExcludeDefaults *bool `json:"exclude_defaults,omitempty"` 1587 1588 // EnableBackporting enables functionality to create new backport bugs for 1589 // cherrypick PRs created by the cherrypick plugin that reference bugzilla bugs. 1590 EnableBackporting *bool `json:"enable_backporting,omitempty"` 1591 1592 // ValidateByDefault determines whether a validation check is run for all pull 1593 // requests by default 1594 ValidateByDefault *bool `json:"validate_by_default,omitempty"` 1595 1596 // IsOpen determines whether a bug needs to be open to be valid 1597 IsOpen *bool `json:"is_open,omitempty"` 1598 // TargetRelease determines which release a bug needs to target to be valid 1599 TargetRelease *string `json:"target_release,omitempty"` 1600 // Statuses determine which statuses a bug may have to be valid 1601 Statuses *[]string `json:"statuses,omitempty"` 1602 // ValidStates determine states in which the bug may be to be valid 1603 ValidStates *[]BugzillaBugState `json:"valid_states,omitempty"` 1604 1605 // DependentBugStatuses determine which statuses a bug's dependent bugs may have 1606 // to deem the child bug valid. These are merged into DependentBugStates when 1607 // resolving branch options. 1608 DependentBugStatuses *[]string `json:"dependent_bug_statuses,omitempty"` 1609 // DependentBugStates determine states in which a bug's dependents bugs may be 1610 // to deem the child bug valid. If set, all blockers must have a valid state. 1611 DependentBugStates *[]BugzillaBugState `json:"dependent_bug_states,omitempty"` 1612 // DependentBugTargetReleases determines the set of valid target 1613 // releases for dependent bugs. If set, all blockers must have a 1614 // valid target release. 1615 DependentBugTargetReleases *[]string `json:"dependent_bug_target_releases,omitempty"` 1616 // DeprecatedDependentBugTargetRelease determines which release a 1617 // bug's dependent bugs need to target to be valid. If set, all 1618 // blockers must have a valid target releasee. 1619 // 1620 // Deprecated: Use DependentBugTargetReleases instead. If set, 1621 // DependentBugTargetRelease will be appended to 1622 // DeprecatedDependentBugTargetReleases. 1623 DeprecatedDependentBugTargetRelease *string `json:"dependent_bug_target_release,omitempty"` 1624 1625 // StatusAfterValidation is the status which the bug will be moved to after being 1626 // deemed valid and linked to a PR. Will implicitly be considered a part of `statuses` 1627 // if others are set. 1628 StatusAfterValidation *string `json:"status_after_validation,omitempty"` 1629 // StateAfterValidation is the state to which the bug will be moved after being 1630 // deemed valid and linked to a PR. Will implicitly be considered a part of `ValidStates` 1631 // if others are set. 1632 StateAfterValidation *BugzillaBugState `json:"state_after_validation,omitempty"` 1633 // AddExternalLink determines whether the pull request will be added to the Bugzilla 1634 // bug using the ExternalBug tracker API after being validated 1635 AddExternalLink *bool `json:"add_external_link,omitempty"` 1636 // StatusAfterMerge is the status which the bug will be moved to after all pull requests 1637 // in the external bug tracker have been merged. 1638 StatusAfterMerge *string `json:"status_after_merge,omitempty"` 1639 // StateAfterMerge is the state to which the bug will be moved after all pull requests 1640 // in the external bug tracker have been merged. 1641 StateAfterMerge *BugzillaBugState `json:"state_after_merge,omitempty"` 1642 // StateAfterClose is the state to which the bug will be moved if all pull requests 1643 // in the external bug tracker have been closed. 1644 StateAfterClose *BugzillaBugState `json:"state_after_close,omitempty"` 1645 1646 // AllowedGroups is a list of bugzilla bug group names that the bugzilla plugin can 1647 // link to in PRs. If a bug is part of a group that is not in this list, the bugzilla 1648 // plugin will not link the bug to the PR. 1649 AllowedGroups []string `json:"allowed_groups,omitempty"` 1650 } 1651 1652 type BugzillaBugStateSet map[BugzillaBugState]interface{} 1653 1654 func NewBugzillaBugStateSet(states []BugzillaBugState) BugzillaBugStateSet { 1655 set := make(BugzillaBugStateSet, len(states)) 1656 for _, state := range states { 1657 set[state] = nil 1658 } 1659 1660 return set 1661 } 1662 1663 func (s BugzillaBugStateSet) Has(state BugzillaBugState) bool { 1664 _, ok := s[state] 1665 return ok 1666 } 1667 1668 func (s BugzillaBugStateSet) Insert(states ...BugzillaBugState) BugzillaBugStateSet { 1669 for _, state := range states { 1670 s[state] = nil 1671 } 1672 return s 1673 } 1674 1675 func statesMatch(first, second []BugzillaBugState) bool { 1676 if len(first) != len(second) { 1677 return false 1678 } 1679 1680 firstSet := NewBugzillaBugStateSet(first) 1681 secondSet := NewBugzillaBugStateSet(second) 1682 1683 for state := range firstSet { 1684 if !secondSet.Has(state) { 1685 return false 1686 } 1687 } 1688 1689 return true 1690 } 1691 1692 func (o BugzillaBranchOptions) matches(other BugzillaBranchOptions) bool { 1693 validateByDefaultMatch := o.ValidateByDefault == nil && other.ValidateByDefault == nil || 1694 (o.ValidateByDefault != nil && other.ValidateByDefault != nil && *o.ValidateByDefault == *other.ValidateByDefault) 1695 isOpenMatch := o.IsOpen == nil && other.IsOpen == nil || 1696 (o.IsOpen != nil && other.IsOpen != nil && *o.IsOpen == *other.IsOpen) 1697 targetReleaseMatch := o.TargetRelease == nil && other.TargetRelease == nil || 1698 (o.TargetRelease != nil && other.TargetRelease != nil && *o.TargetRelease == *other.TargetRelease) 1699 bugStatesMatch := o.ValidStates == nil && other.ValidStates == nil || 1700 (o.ValidStates != nil && other.ValidStates != nil && statesMatch(*o.ValidStates, *other.ValidStates)) 1701 dependentBugStatesMatch := o.DependentBugStates == nil && other.DependentBugStates == nil || 1702 (o.DependentBugStates != nil && other.DependentBugStates != nil && statesMatch(*o.DependentBugStates, *other.DependentBugStates)) 1703 statesAfterValidationMatch := o.StateAfterValidation == nil && other.StateAfterValidation == nil || 1704 (o.StateAfterValidation != nil && other.StateAfterValidation != nil && *o.StateAfterValidation == *other.StateAfterValidation) 1705 addExternalLinkMatch := o.AddExternalLink == nil && other.AddExternalLink == nil || 1706 (o.AddExternalLink != nil && other.AddExternalLink != nil && *o.AddExternalLink == *other.AddExternalLink) 1707 statesAfterMergeMatch := o.StateAfterMerge == nil && other.StateAfterMerge == nil || 1708 (o.StateAfterMerge != nil && other.StateAfterMerge != nil && *o.StateAfterMerge == *other.StateAfterMerge) 1709 return validateByDefaultMatch && isOpenMatch && targetReleaseMatch && bugStatesMatch && dependentBugStatesMatch && statesAfterValidationMatch && addExternalLinkMatch && statesAfterMergeMatch 1710 } 1711 1712 const BugzillaOptionsWildcard = `*` 1713 1714 // OptionsForItem resolves a set of options for an item, honoring 1715 // the `*` wildcard and doing defaulting if it is present with the 1716 // item itself. 1717 func OptionsForItem(item string, config map[string]BugzillaBranchOptions) BugzillaBranchOptions { 1718 return ResolveBugzillaOptions(config[BugzillaOptionsWildcard], config[item]) 1719 } 1720 1721 func mergeStatusesIntoStates(states *[]BugzillaBugState, statuses *[]string) *[]BugzillaBugState { 1722 var newStates []BugzillaBugState 1723 stateSet := BugzillaBugStateSet{} 1724 1725 if states != nil { 1726 stateSet = stateSet.Insert(*states...) 1727 } 1728 if statuses != nil { 1729 for _, status := range *statuses { 1730 stateSet = stateSet.Insert(BugzillaBugState{Status: status}) 1731 } 1732 } 1733 1734 for state := range stateSet { 1735 newStates = append(newStates, state) 1736 } 1737 1738 if len(newStates) > 0 { 1739 sort.Slice(newStates, func(i, j int) bool { 1740 return newStates[i].Status < newStates[j].Status || (newStates[i].Status == newStates[j].Status && newStates[i].Resolution < newStates[j].Resolution) 1741 }) 1742 return &newStates 1743 } 1744 return nil 1745 } 1746 1747 // ResolveBugzillaOptions implements defaulting for a parent/child configuration, 1748 // preferring child fields where set. This method also reflects all "Status" 1749 // fields into matching `State` fields. 1750 func ResolveBugzillaOptions(parent, child BugzillaBranchOptions) BugzillaBranchOptions { 1751 output := BugzillaBranchOptions{} 1752 1753 if child.ExcludeDefaults == nil || !*child.ExcludeDefaults { 1754 // populate with the parent 1755 if parent.ExcludeDefaults != nil { 1756 output.ExcludeDefaults = parent.ExcludeDefaults 1757 } 1758 if parent.ValidateByDefault != nil { 1759 output.ValidateByDefault = parent.ValidateByDefault 1760 } 1761 if parent.IsOpen != nil { 1762 output.IsOpen = parent.IsOpen 1763 } 1764 if parent.TargetRelease != nil { 1765 output.TargetRelease = parent.TargetRelease 1766 } 1767 if parent.ValidStates != nil { 1768 output.ValidStates = parent.ValidStates 1769 } 1770 if parent.Statuses != nil { 1771 output.Statuses = parent.Statuses 1772 output.ValidStates = mergeStatusesIntoStates(output.ValidStates, parent.Statuses) 1773 } 1774 if parent.DependentBugStates != nil { 1775 output.DependentBugStates = parent.DependentBugStates 1776 } 1777 if parent.DependentBugStatuses != nil { 1778 output.DependentBugStatuses = parent.DependentBugStatuses 1779 output.DependentBugStates = mergeStatusesIntoStates(output.DependentBugStates, parent.DependentBugStatuses) 1780 } 1781 if parent.DependentBugTargetReleases != nil { 1782 output.DependentBugTargetReleases = parent.DependentBugTargetReleases 1783 } 1784 if parent.DeprecatedDependentBugTargetRelease != nil { 1785 logrusutil.ThrottledWarnf(&warnDependentBugTargetRelease, 5*time.Minute, "Please update plugins.yaml to use dependent_bug_target_releases instead of the deprecated dependent_bug_target_release") 1786 if parent.DependentBugTargetReleases == nil { 1787 output.DependentBugTargetReleases = &[]string{*parent.DeprecatedDependentBugTargetRelease} 1788 } else if !sets.New[string](*parent.DependentBugTargetReleases...).Has(*parent.DeprecatedDependentBugTargetRelease) { 1789 dependentBugTargetReleases := append(*output.DependentBugTargetReleases, *parent.DeprecatedDependentBugTargetRelease) 1790 output.DependentBugTargetReleases = &dependentBugTargetReleases 1791 } 1792 } 1793 if parent.StatusAfterValidation != nil { 1794 output.StatusAfterValidation = parent.StatusAfterValidation 1795 output.StateAfterValidation = &BugzillaBugState{Status: *output.StatusAfterValidation} 1796 } 1797 if parent.StateAfterValidation != nil { 1798 output.StateAfterValidation = parent.StateAfterValidation 1799 } 1800 if parent.AddExternalLink != nil { 1801 output.AddExternalLink = parent.AddExternalLink 1802 } 1803 if parent.StatusAfterMerge != nil { 1804 output.StatusAfterMerge = parent.StatusAfterMerge 1805 output.StateAfterMerge = &BugzillaBugState{Status: *output.StatusAfterMerge} 1806 } 1807 if parent.StateAfterMerge != nil { 1808 output.StateAfterMerge = parent.StateAfterMerge 1809 } 1810 if parent.StateAfterClose != nil { 1811 output.StateAfterClose = parent.StateAfterClose 1812 } 1813 if parent.AllowedGroups != nil { 1814 output.AllowedGroups = sets.List(sets.New[string](output.AllowedGroups...).Insert(parent.AllowedGroups...)) 1815 } 1816 } 1817 1818 // override with the child 1819 if child.ExcludeDefaults != nil { 1820 output.ExcludeDefaults = child.ExcludeDefaults 1821 } 1822 if child.ValidateByDefault != nil { 1823 output.ValidateByDefault = child.ValidateByDefault 1824 } 1825 if child.IsOpen != nil { 1826 output.IsOpen = child.IsOpen 1827 } 1828 if child.TargetRelease != nil { 1829 output.TargetRelease = child.TargetRelease 1830 } 1831 1832 if child.ValidStates != nil { 1833 output.ValidStates = child.ValidStates 1834 } 1835 if child.Statuses != nil { 1836 output.Statuses = child.Statuses 1837 if child.ValidStates == nil { 1838 output.ValidStates = nil 1839 } 1840 output.ValidStates = mergeStatusesIntoStates(output.ValidStates, child.Statuses) 1841 } 1842 1843 if child.DependentBugStates != nil { 1844 output.DependentBugStates = child.DependentBugStates 1845 } 1846 if child.DependentBugStatuses != nil { 1847 output.DependentBugStatuses = child.DependentBugStatuses 1848 if child.DependentBugStates == nil { 1849 output.DependentBugStates = nil 1850 } 1851 output.DependentBugStates = mergeStatusesIntoStates(output.DependentBugStates, child.DependentBugStatuses) 1852 } 1853 if child.DependentBugTargetReleases != nil { 1854 output.DependentBugTargetReleases = child.DependentBugTargetReleases 1855 } 1856 if child.DeprecatedDependentBugTargetRelease != nil { 1857 logrusutil.ThrottledWarnf(&warnDependentBugTargetRelease, 5*time.Minute, "Please update plugins.yaml to use dependent_bug_target_releases instead of the deprecated dependent_bug_target_release") 1858 if child.DependentBugTargetReleases == nil { 1859 output.DependentBugTargetReleases = &[]string{*child.DeprecatedDependentBugTargetRelease} 1860 } else if !sets.New[string](*child.DependentBugTargetReleases...).Has(*child.DeprecatedDependentBugTargetRelease) { 1861 dependentBugTargetReleases := append(*output.DependentBugTargetReleases, *child.DeprecatedDependentBugTargetRelease) 1862 output.DependentBugTargetReleases = &dependentBugTargetReleases 1863 } 1864 } 1865 if child.StatusAfterValidation != nil { 1866 output.StatusAfterValidation = child.StatusAfterValidation 1867 if child.StateAfterValidation == nil { 1868 output.StateAfterValidation = &BugzillaBugState{Status: *child.StatusAfterValidation} 1869 } 1870 } 1871 if child.StateAfterValidation != nil { 1872 output.StateAfterValidation = child.StateAfterValidation 1873 } 1874 if child.AddExternalLink != nil { 1875 output.AddExternalLink = child.AddExternalLink 1876 } 1877 if child.StatusAfterMerge != nil { 1878 output.StatusAfterMerge = child.StatusAfterMerge 1879 if child.StateAfterMerge == nil { 1880 output.StateAfterMerge = &BugzillaBugState{Status: *child.StatusAfterMerge} 1881 } 1882 } 1883 if child.StateAfterMerge != nil { 1884 output.StateAfterMerge = child.StateAfterMerge 1885 } 1886 if child.StateAfterClose != nil { 1887 output.StateAfterClose = child.StateAfterClose 1888 } 1889 if child.AllowedGroups != nil { 1890 output.AllowedGroups = sets.List(sets.New[string](output.AllowedGroups...).Insert(child.AllowedGroups...)) 1891 } 1892 1893 // Status fields should not be used anywhere now when they were mirrored to states 1894 output.Statuses = nil 1895 output.DependentBugStatuses = nil 1896 output.StatusAfterMerge = nil 1897 output.StatusAfterValidation = nil 1898 1899 return output 1900 } 1901 1902 // OptionsForBranch determines the criteria for a valid Bugzilla bug on a branch of a repo 1903 // by defaulting in a cascading way, in the following order (later entries override earlier 1904 // ones), always searching for the wildcard as well as the branch name: global, then org, 1905 // repo, and finally branch-specific configuration. 1906 func (b *Bugzilla) OptionsForBranch(org, repo, branch string) BugzillaBranchOptions { 1907 options := OptionsForItem(branch, b.Default) 1908 orgOptions, exists := b.Orgs[org] 1909 if !exists { 1910 return options 1911 } 1912 options = ResolveBugzillaOptions(options, OptionsForItem(branch, orgOptions.Default)) 1913 1914 repoOptions, exists := orgOptions.Repos[repo] 1915 if !exists { 1916 return options 1917 } 1918 options = ResolveBugzillaOptions(options, OptionsForItem(branch, repoOptions.Branches)) 1919 1920 return options 1921 } 1922 1923 // OptionsForRepo determines the criteria for a valid Bugzilla bug on branches of a repo 1924 // by defaulting in a cascading way, in the following order (later entries override earlier 1925 // ones), always searching for the wildcard as well as the branch name: global, then org, 1926 // repo, and finally branch-specific configuration. 1927 func (b *Bugzilla) OptionsForRepo(org, repo string) map[string]BugzillaBranchOptions { 1928 options := map[string]BugzillaBranchOptions{} 1929 for branch := range b.Default { 1930 options[branch] = b.OptionsForBranch(org, repo, branch) 1931 } 1932 1933 orgOptions, exists := b.Orgs[org] 1934 if exists { 1935 for branch := range orgOptions.Default { 1936 options[branch] = b.OptionsForBranch(org, repo, branch) 1937 } 1938 } 1939 1940 repoOptions, exists := orgOptions.Repos[repo] 1941 if exists { 1942 for branch := range repoOptions.Branches { 1943 options[branch] = b.OptionsForBranch(org, repo, branch) 1944 } 1945 } 1946 1947 // if there are nested defaults there is no reason to call out branches 1948 // from higher levels of config 1949 var toDelete []string 1950 for branch, branchOptions := range options { 1951 if branchOptions.matches(options[BugzillaOptionsWildcard]) && branch != BugzillaOptionsWildcard { 1952 toDelete = append(toDelete, branch) 1953 } 1954 } 1955 for _, branch := range toDelete { 1956 delete(options, branch) 1957 } 1958 1959 return options 1960 } 1961 1962 // BranchCleaner contains the configuration for the branchcleaner plugin. 1963 type BranchCleaner struct { 1964 // PreservedBranches is a map of org/repo branches 1965 // format: 1966 // ``` 1967 // preserved_branches: 1968 // <org>: ["master", "release"] 1969 // <org/repo>: ["master", "release"] 1970 // ``` 1971 // branches in this allow map would be exempt from branch gc 1972 // even if the branches are already merged into the target branch 1973 PreservedBranches map[string][]string `json:"preserved_branches,omitempty"` 1974 } 1975 1976 // IsPreservedBranch check if the branch is in the preserved branch list or not. 1977 func (b *BranchCleaner) IsPreservedBranch(org, repo, branch string) bool { 1978 fullRepoName := fmt.Sprintf("%s/%s", org, repo) 1979 for _, pb := range b.PreservedBranches[fullRepoName] { 1980 if branch == pb { 1981 return true 1982 } 1983 1984 if match, _ := regexp.MatchString(pb, branch); match { 1985 return true 1986 } 1987 } 1988 for _, pb := range b.PreservedBranches[org] { 1989 if branch == pb { 1990 return true 1991 } 1992 1993 if match, _ := regexp.MatchString(pb, branch); match { 1994 return true 1995 } 1996 } 1997 // no repo or org match. 1998 return false 1999 } 2000 2001 // Override holds options for the override plugin 2002 type Override struct { 2003 AllowTopLevelOwners bool `json:"allow_top_level_owners,omitempty"` 2004 // AllowedGitHubTeams is a map of orgs and/or repositories (eg "org" or "org/repo") to list of GitHub team slugs, 2005 // members of which are allowed to override contexts 2006 AllowedGitHubTeams map[string][]string `json:"allowed_github_teams,omitempty"` 2007 } 2008 2009 func (c *Configuration) mergeFrom(other *Configuration) error { 2010 var errs []error 2011 2012 diff := cmp.Diff(other, &Configuration{Approve: other.Approve, Bugzilla: other.Bugzilla, 2013 ExternalPlugins: other.ExternalPlugins, Label: Label{RestrictedLabels: other.Label.RestrictedLabels}, 2014 Lgtm: other.Lgtm, Plugins: other.Plugins, Triggers: other.Triggers, Welcome: other.Welcome}, 2015 config.DefaultDiffOpts...) 2016 2017 if diff != "" { 2018 errs = append(errs, fmt.Errorf("supplemental plugin configuration has config that doesn't support merging: %s", diff)) 2019 } 2020 2021 if c.Plugins == nil { 2022 c.Plugins = Plugins{} 2023 } 2024 if err := c.Plugins.mergeFrom(&other.Plugins); err != nil { 2025 errs = append(errs, fmt.Errorf("failed to merge .plugins from supplemental config: %w", err)) 2026 } 2027 2028 if err := c.Bugzilla.mergeFrom(&other.Bugzilla); err != nil { 2029 errs = append(errs, fmt.Errorf("failed to merge .bugzilla from supplemental config: %w", err)) 2030 } 2031 2032 c.Approve = append(c.Approve, other.Approve...) 2033 c.Lgtm = append(c.Lgtm, other.Lgtm...) 2034 c.Triggers = append(c.Triggers, other.Triggers...) 2035 c.Welcome = append(c.Welcome, other.Welcome...) 2036 2037 if err := c.mergeExternalPluginsFrom(other.ExternalPlugins); err != nil { 2038 errs = append(errs, fmt.Errorf("failed to merge .external-plugins from supplemental config: %w", err)) 2039 } 2040 2041 if err := c.Label.mergeFrom(&other.Label); err != nil { 2042 errs = append(errs, fmt.Errorf("failed to merge .label from supplemental config: %w", err)) 2043 } 2044 2045 return utilerrors.NewAggregate(errs) 2046 } 2047 2048 func (c *Configuration) mergeExternalPluginsFrom(other map[string][]ExternalPlugin) error { 2049 if c.ExternalPlugins == nil && other != nil { 2050 c.ExternalPlugins = make(map[string][]ExternalPlugin) 2051 } 2052 2053 var errs []error 2054 for orgOrRepo, config := range other { 2055 if _, ok := c.ExternalPlugins[orgOrRepo]; ok { 2056 errs = append(errs, fmt.Errorf("found duplicate config for external-plugins.%s", orgOrRepo)) 2057 continue 2058 } 2059 c.ExternalPlugins[orgOrRepo] = config 2060 } 2061 2062 return utilerrors.NewAggregate(errs) 2063 } 2064 2065 func (p *Plugins) mergeFrom(other *Plugins) error { 2066 if other == nil { 2067 return nil 2068 } 2069 if len(*p) == 0 { 2070 *p = *other 2071 return nil 2072 } 2073 2074 var errs []error 2075 for orgOrRepo, config := range *other { 2076 if _, ok := (*p)[orgOrRepo]; ok { 2077 errs = append(errs, fmt.Errorf("found duplicate config for plugins.%s", orgOrRepo)) 2078 continue 2079 } 2080 (*p)[orgOrRepo] = config 2081 } 2082 2083 return utilerrors.NewAggregate(errs) 2084 } 2085 2086 func (p *Bugzilla) mergeFrom(other *Bugzilla) error { 2087 if other == nil { 2088 return nil 2089 } 2090 2091 var errs []error 2092 if other.Default != nil { 2093 if p.Default != nil { 2094 errs = append(errs, errors.New("configuration of global default defined in multiple places")) 2095 } else { 2096 p.Default = other.Default 2097 } 2098 } 2099 if len(other.Orgs) != 0 && p.Orgs == nil { 2100 p.Orgs = make(map[string]BugzillaOrgOptions) 2101 } 2102 for org, orgConfig := range other.Orgs { 2103 if _, ok := p.Orgs[org]; !ok { 2104 p.Orgs[org] = BugzillaOrgOptions{} 2105 } 2106 if orgConfig.Default != nil { 2107 if p.Orgs[org].Default != nil { 2108 errs = append(errs, fmt.Errorf("found duplicate organization config for bugzilla.%s", org)) 2109 continue 2110 } 2111 newConfig := p.Orgs[org] 2112 newConfig.Default = orgConfig.Default 2113 p.Orgs[org] = newConfig 2114 } 2115 if len(orgConfig.Repos) != 0 && p.Orgs[org].Repos == nil { 2116 newConfig := p.Orgs[org] 2117 newConfig.Repos = make(map[string]BugzillaRepoOptions) 2118 p.Orgs[org] = newConfig 2119 } 2120 for repo, repoConfig := range orgConfig.Repos { 2121 if _, ok := p.Orgs[org].Repos[repo]; ok { 2122 errs = append(errs, fmt.Errorf("found duplicate repository config for bugzilla.%s/%s", org, repo)) 2123 continue 2124 } 2125 p.Orgs[org].Repos[repo] = repoConfig 2126 } 2127 } 2128 return utilerrors.NewAggregate(errs) 2129 } 2130 2131 func (l *Label) mergeFrom(other *Label) error { 2132 if other == nil { 2133 return nil 2134 } 2135 l.AdditionalLabels = append(l.AdditionalLabels, other.AdditionalLabels...) 2136 2137 var errs []error 2138 for key, labelConfigs := range other.RestrictedLabels { 2139 for _, labelConfig := range labelConfigs { 2140 if conflictingIdx := getLabelConfigFromRestrictedLabelsSlice(l.RestrictedLabels[key], labelConfig.Label); conflictingIdx != -1 { 2141 errs = append(errs, fmt.Errorf("there are multiple label.restricted_labels configs for label %s", labelConfig.Label)) 2142 } 2143 } 2144 if l.RestrictedLabels == nil { 2145 l.RestrictedLabels = map[string][]RestrictedLabel{} 2146 } 2147 l.RestrictedLabels[key] = append(l.RestrictedLabels[key], labelConfigs...) 2148 } 2149 2150 return utilerrors.NewAggregate(errs) 2151 } 2152 2153 func getLabelConfigFromRestrictedLabelsSlice(s []RestrictedLabel, label string) int { 2154 for idx, item := range s { 2155 if item.Label == label { 2156 return idx 2157 } 2158 } 2159 2160 return -1 2161 } 2162 2163 func (c *Configuration) HasConfigFor() (global bool, orgs sets.Set[string], repos sets.Set[string]) { 2164 equals := reflect.DeepEqual(c, 2165 &Configuration{Approve: c.Approve, Bugzilla: c.Bugzilla, ExternalPlugins: c.ExternalPlugins, 2166 Label: Label{RestrictedLabels: c.Label.RestrictedLabels}, Lgtm: c.Lgtm, Plugins: c.Plugins, 2167 Triggers: c.Triggers, Welcome: c.Welcome}) 2168 2169 if !equals || c.Bugzilla.Default != nil { 2170 global = true 2171 } 2172 orgs = sets.Set[string]{} 2173 repos = sets.Set[string]{} 2174 for orgOrRepo := range c.Plugins { 2175 if strings.Contains(orgOrRepo, "/") { 2176 repos.Insert(orgOrRepo) 2177 } else { 2178 orgs.Insert(orgOrRepo) 2179 } 2180 } 2181 2182 for org, orgConfig := range c.Bugzilla.Orgs { 2183 if orgConfig.Default != nil { 2184 orgs.Insert(org) 2185 } 2186 for repo := range orgConfig.Repos { 2187 repos.Insert(org + "/" + repo) 2188 } 2189 } 2190 2191 for _, approveConfig := range c.Approve { 2192 for _, orgOrRepo := range approveConfig.Repos { 2193 if strings.Contains(orgOrRepo, "/") { 2194 repos.Insert(orgOrRepo) 2195 } else { 2196 orgs.Insert(orgOrRepo) 2197 } 2198 } 2199 } 2200 2201 if len(c.Label.AdditionalLabels) > 0 { 2202 global = true 2203 } 2204 for key := range c.Label.RestrictedLabels { 2205 if key == "*" { 2206 global = true 2207 } else if strings.Contains(key, "/") { 2208 repos.Insert(key) 2209 } else { 2210 orgs.Insert(key) 2211 } 2212 } 2213 2214 for _, lgtm := range c.Lgtm { 2215 for _, orgOrRepo := range lgtm.Repos { 2216 if strings.Contains(orgOrRepo, "/") { 2217 repos.Insert(orgOrRepo) 2218 } else { 2219 orgs.Insert(orgOrRepo) 2220 } 2221 } 2222 } 2223 2224 for _, trigger := range c.Triggers { 2225 for _, orgOrRepo := range trigger.Repos { 2226 if strings.Contains(orgOrRepo, "/") { 2227 repos.Insert(orgOrRepo) 2228 } else { 2229 orgs.Insert(orgOrRepo) 2230 } 2231 } 2232 } 2233 2234 for _, welcome := range c.Welcome { 2235 for _, orgOrRepo := range welcome.Repos { 2236 if strings.Contains(orgOrRepo, "/") { 2237 repos.Insert(orgOrRepo) 2238 } else { 2239 orgs.Insert(orgOrRepo) 2240 } 2241 } 2242 } 2243 2244 for orgOrRepo := range c.ExternalPlugins { 2245 if strings.Contains(orgOrRepo, "/") { 2246 repos.Insert(orgOrRepo) 2247 } else { 2248 orgs.Insert(orgOrRepo) 2249 } 2250 } 2251 2252 return global, orgs, repos 2253 }