github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/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  }