github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/cmd/checkconfig/main.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  // checkconfig loads configuration for Prow to validate it
    18  package main
    19  
    20  import (
    21  	"errors"
    22  	"flag"
    23  	"fmt"
    24  	"strings"
    25  
    26  	"github.com/sirupsen/logrus"
    27  	"k8s.io/apimachinery/pkg/util/sets"
    28  	"k8s.io/apimachinery/pkg/util/validation"
    29  	"k8s.io/test-infra/prow/apis/prowjobs/v1"
    30  	"k8s.io/test-infra/prow/errorutil"
    31  	needsrebase "k8s.io/test-infra/prow/external-plugins/needs-rebase/plugin"
    32  	"k8s.io/test-infra/prow/flagutil"
    33  	"k8s.io/test-infra/prow/labels"
    34  	"k8s.io/test-infra/prow/plugins/approve"
    35  	"k8s.io/test-infra/prow/plugins/blockade"
    36  	"k8s.io/test-infra/prow/plugins/blunderbuss"
    37  	"k8s.io/test-infra/prow/plugins/cherrypickunapproved"
    38  	"k8s.io/test-infra/prow/plugins/hold"
    39  	"k8s.io/test-infra/prow/plugins/owners-label"
    40  	"k8s.io/test-infra/prow/plugins/releasenote"
    41  	"k8s.io/test-infra/prow/plugins/verify-owners"
    42  	"k8s.io/test-infra/prow/plugins/wip"
    43  
    44  	"k8s.io/test-infra/prow/config"
    45  	_ "k8s.io/test-infra/prow/hook"
    46  	"k8s.io/test-infra/prow/logrusutil"
    47  	"k8s.io/test-infra/prow/plugins"
    48  	"k8s.io/test-infra/prow/plugins/lgtm"
    49  )
    50  
    51  type options struct {
    52  	configPath    string
    53  	jobConfigPath string
    54  	pluginConfig  string
    55  
    56  	warnings flagutil.Strings
    57  	strict   bool
    58  }
    59  
    60  func reportWarning(strict bool, errs errorutil.Aggregate) {
    61  	for _, item := range errs.Strings() {
    62  		logrus.Warn(item)
    63  	}
    64  	if strict {
    65  		logrus.Fatal("Strict is set and there were warnings")
    66  	}
    67  }
    68  
    69  func (o *options) warningEnabled(warning string) bool {
    70  	for _, registeredWarning := range o.warnings.Strings() {
    71  		if warning == registeredWarning {
    72  			return true
    73  		}
    74  	}
    75  	return false
    76  }
    77  
    78  const (
    79  	mismatchedTideWarning   = "mismatched-tide"
    80  	nonDecoratedJobsWarning = "non-decorated-jobs"
    81  	jobNameLengthWarning    = "long-job-names"
    82  	needsOkToTestWarning    = "needs-ok-to-test"
    83  	validateOwnersWarning   = "validate-owners"
    84  )
    85  
    86  var allWarnings = []string{
    87  	mismatchedTideWarning,
    88  	nonDecoratedJobsWarning,
    89  	jobNameLengthWarning,
    90  	needsOkToTestWarning,
    91  	validateOwnersWarning,
    92  }
    93  
    94  func (o *options) Validate() error {
    95  	if o.configPath == "" {
    96  		return errors.New("required flag --config-path was unset")
    97  	}
    98  	if o.pluginConfig == "" {
    99  		return errors.New("required flag --plugin-config was unset")
   100  	}
   101  	for _, warning := range o.warnings.Strings() {
   102  		found := false
   103  		for _, registeredWarning := range allWarnings {
   104  			if warning == registeredWarning {
   105  				found = true
   106  				break
   107  			}
   108  		}
   109  		if !found {
   110  			return fmt.Errorf("no such warning %q, valid warnings: %v", warning, allWarnings)
   111  		}
   112  	}
   113  	return nil
   114  }
   115  
   116  func gatherOptions() options {
   117  	o := options{}
   118  	flag.StringVar(&o.configPath, "config-path", "", "Path to config.yaml.")
   119  	flag.StringVar(&o.jobConfigPath, "job-config-path", "", "Path to prow job configs.")
   120  	flag.StringVar(&o.pluginConfig, "plugin-config", "", "Path to plugin config file.")
   121  	flag.Var(&o.warnings, "warnings", "Comma-delimited list of warnings to validate.")
   122  	flag.BoolVar(&o.strict, "strict", false, "If set, consider all warnings as errors.")
   123  	flag.Parse()
   124  	return o
   125  }
   126  
   127  func main() {
   128  	o := gatherOptions()
   129  	if err := o.Validate(); err != nil {
   130  		logrus.Fatalf("Invalid options: %v", err)
   131  	}
   132  
   133  	// use all warnings by default
   134  	if len(o.warnings.Strings()) == 0 {
   135  		o.warnings = flagutil.NewStrings(allWarnings...)
   136  	}
   137  
   138  	logrus.SetFormatter(
   139  		logrusutil.NewDefaultFieldsFormatter(&logrus.TextFormatter{}, logrus.Fields{"component": "checkconfig"}),
   140  	)
   141  
   142  	configAgent := config.Agent{}
   143  	if err := configAgent.Start(o.configPath, o.jobConfigPath); err != nil {
   144  		logrus.WithError(err).Fatal("Error loading Prow config.")
   145  	}
   146  	cfg := configAgent.Config()
   147  
   148  	pluginAgent := plugins.ConfigAgent{}
   149  	if err := pluginAgent.Load(o.pluginConfig); err != nil {
   150  		logrus.WithError(err).Fatal("Error loading Prow plugin config.")
   151  	}
   152  	pcfg := pluginAgent.Config()
   153  
   154  	// the following checks are useful in finding user errors but their
   155  	// presence won't lead to strictly incorrect behavior, so we can
   156  	// detect them here but don't necessarily want to stop config re-load
   157  	// in all components on their failure.
   158  	var errs []error
   159  	if o.warningEnabled(mismatchedTideWarning) {
   160  		if err := validateTideRequirements(cfg, pcfg); err != nil {
   161  			errs = append(errs, err)
   162  		}
   163  	}
   164  	if o.warningEnabled(nonDecoratedJobsWarning) {
   165  		if err := validateDecoratedJobs(cfg); err != nil {
   166  			errs = append(errs, err)
   167  		}
   168  	}
   169  	if o.warningEnabled(jobNameLengthWarning) {
   170  		if err := validateJobRequirements(cfg.JobConfig); err != nil {
   171  			errs = append(errs, err)
   172  		}
   173  	}
   174  	if o.warningEnabled(needsOkToTestWarning) {
   175  		if err := validateNeedsOkToTestLabel(cfg); err != nil {
   176  			errs = append(errs, err)
   177  		}
   178  	}
   179  	if o.warningEnabled(validateOwnersWarning) {
   180  		if err := verifyOwnersPlugin(pcfg); err != nil {
   181  			errs = append(errs, err)
   182  		}
   183  	}
   184  	if len(errs) > 0 {
   185  		reportWarning(o.strict, errorutil.NewAggregate(errs...))
   186  	}
   187  }
   188  
   189  func validateJobRequirements(c config.JobConfig) error {
   190  	var validationErrs []error
   191  	for repo, jobs := range c.Presubmits {
   192  		for _, job := range jobs {
   193  			validationErrs = append(validationErrs, validatePresubmitJob(repo, job))
   194  		}
   195  	}
   196  	for repo, jobs := range c.Postsubmits {
   197  		for _, job := range jobs {
   198  			validationErrs = append(validationErrs, validatePostsubmitJob(repo, job))
   199  		}
   200  	}
   201  	for _, job := range c.Periodics {
   202  		validationErrs = append(validationErrs, validatePeriodicJob(job))
   203  	}
   204  
   205  	return errorutil.NewAggregate(validationErrs...)
   206  }
   207  
   208  func validatePresubmitJob(repo string, job config.Presubmit) error {
   209  	var validationErrs []error
   210  	// Prow labels k8s resources with job names. Labels are capped at 63 chars.
   211  	if job.Agent == string(v1.KubernetesAgent) && len(job.Name) > validation.LabelValueMaxLength {
   212  		validationErrs = append(validationErrs, fmt.Errorf("name of Presubmit job %q (for repo %q) too long (should be at most 63 characters)", job.Name, repo))
   213  	}
   214  	return errorutil.NewAggregate(validationErrs...)
   215  }
   216  
   217  func validatePostsubmitJob(repo string, job config.Postsubmit) error {
   218  	var validationErrs []error
   219  	// Prow labels k8s resources with job names. Labels are capped at 63 chars.
   220  	if job.Agent == string(v1.KubernetesAgent) && len(job.Name) > validation.LabelValueMaxLength {
   221  		validationErrs = append(validationErrs, fmt.Errorf("name of Postsubmit job %q (for repo %q) too long (should be at most 63 characters)", job.Name, repo))
   222  	}
   223  	return errorutil.NewAggregate(validationErrs...)
   224  }
   225  
   226  func validatePeriodicJob(job config.Periodic) error {
   227  	var validationErrs []error
   228  	// Prow labels k8s resources with job names. Labels are capped at 63 chars.
   229  	if job.Agent == string(v1.KubernetesAgent) && len(job.Name) > validation.LabelValueMaxLength {
   230  		validationErrs = append(validationErrs, fmt.Errorf("name of Periodic job %q too long (should be at most 63 characters)", job.Name))
   231  	}
   232  	return errorutil.NewAggregate(validationErrs...)
   233  }
   234  
   235  func validateTideRequirements(cfg *config.Config, pcfg *plugins.Configuration) error {
   236  	type matcher struct {
   237  		// matches determines if the tide query appropriately honors the
   238  		// label in question -- whether by requiring it or forbidding it
   239  		matches func(label string, query config.TideQuery) bool
   240  		// verb is used in forming error messages
   241  		verb string
   242  	}
   243  	requires := matcher{
   244  		matches: func(label string, query config.TideQuery) bool {
   245  			return sets.NewString(query.Labels...).Has(label)
   246  		},
   247  		verb: "require",
   248  	}
   249  	forbids := matcher{
   250  		matches: func(label string, query config.TideQuery) bool {
   251  			return sets.NewString(query.MissingLabels...).Has(label)
   252  		},
   253  		verb: "forbid",
   254  	}
   255  
   256  	// configs list relationships between tide config
   257  	// and plugin enablement that we want to validate
   258  	configs := []struct {
   259  		// plugin and label identify the relationship we are validating
   260  		plugin, label string
   261  		// external indicates plugin is external or not
   262  		external bool
   263  		// matcher determines if the tide query appropriately honors the
   264  		// label in question -- whether by requiring it or forbidding it
   265  		matcher matcher
   266  		// config holds the orgs and repos for which tide does honor the
   267  		// label; this container is populated conditionally from queries
   268  		// using the matcher
   269  		config *orgRepoConfig
   270  	}{
   271  		{plugin: lgtm.PluginName, label: labels.LGTM, matcher: requires},
   272  		{plugin: approve.PluginName, label: labels.Approved, matcher: requires},
   273  		{plugin: hold.PluginName, label: labels.Hold, matcher: forbids},
   274  		{plugin: wip.PluginName, label: labels.WorkInProgress, matcher: forbids},
   275  		{plugin: verifyowners.PluginName, label: labels.InvalidOwners, matcher: forbids},
   276  		{plugin: releasenote.PluginName, label: releasenote.ReleaseNoteLabelNeeded, matcher: forbids},
   277  		{plugin: cherrypickunapproved.PluginName, label: labels.CpUnapproved, matcher: forbids},
   278  		{plugin: blockade.PluginName, label: labels.BlockedPaths, matcher: forbids},
   279  		{plugin: needsrebase.PluginName, label: labels.NeedsRebase, external: true, matcher: forbids},
   280  	}
   281  
   282  	for i := range configs {
   283  		// For each plugin determine the subset of tide queries that match and then
   284  		// the orgs and repos that the subset matches.
   285  		var matchingQueries config.TideQueries
   286  		for _, query := range cfg.Tide.Queries {
   287  			if configs[i].matcher.matches(configs[i].label, query) {
   288  				matchingQueries = append(matchingQueries, query)
   289  			}
   290  		}
   291  		configs[i].config = newOrgRepoConfig(matchingQueries.OrgExceptionsAndRepos())
   292  	}
   293  
   294  	overallTideConfig := newOrgRepoConfig(cfg.Tide.Queries.OrgExceptionsAndRepos())
   295  
   296  	// Now actually execute the checks we just configured.
   297  	var validationErrs []error
   298  	for _, pluginConfig := range configs {
   299  		err := ensureValidConfiguration(
   300  			pluginConfig.plugin,
   301  			pluginConfig.label,
   302  			pluginConfig.matcher.verb,
   303  			pluginConfig.config,
   304  			overallTideConfig,
   305  			enabledOrgReposForPlugin(pcfg, pluginConfig.plugin, pluginConfig.external),
   306  		)
   307  		validationErrs = append(validationErrs, err)
   308  	}
   309  
   310  	return errorutil.NewAggregate(validationErrs...)
   311  }
   312  
   313  func newOrgRepoConfig(orgExceptions map[string]sets.String, repos sets.String) *orgRepoConfig {
   314  	return &orgRepoConfig{
   315  		orgExceptions: orgExceptions,
   316  		repos:         repos,
   317  	}
   318  }
   319  
   320  // orgRepoConfig describes a set of repositories with an explicit
   321  // whitelist and a mapping of blacklists for owning orgs
   322  type orgRepoConfig struct {
   323  	// orgExceptions holds explicit blacklists of repos for owning orgs
   324  	orgExceptions map[string]sets.String
   325  	// repos is a whitelist of repos
   326  	repos sets.String
   327  }
   328  
   329  func (c *orgRepoConfig) items() []string {
   330  	items := make([]string, 0, len(c.orgExceptions)+len(c.repos))
   331  	for org, excepts := range c.orgExceptions {
   332  		item := fmt.Sprintf("org: %s", org)
   333  		if excepts.Len() > 0 {
   334  			item = fmt.Sprintf("%s without repo(s) %s", item, strings.Join(excepts.List(), ", "))
   335  			for _, repo := range excepts.List() {
   336  				item = fmt.Sprintf("%s '%s'", item, repo)
   337  			}
   338  		}
   339  		items = append(items, item)
   340  	}
   341  	for _, repo := range c.repos.List() {
   342  		items = append(items, fmt.Sprintf("repo: %s", repo))
   343  	}
   344  	return items
   345  }
   346  
   347  // difference returns a new orgRepoConfig that represents the set difference of
   348  // the repos specified by the receiver and the parameter orgRepoConfigs.
   349  func (c *orgRepoConfig) difference(c2 *orgRepoConfig) *orgRepoConfig {
   350  	res := &orgRepoConfig{
   351  		orgExceptions: make(map[string]sets.String),
   352  		repos:         sets.NewString().Union(c.repos),
   353  	}
   354  	for org, excepts1 := range c.orgExceptions {
   355  		if excepts2, ok := c2.orgExceptions[org]; ok {
   356  			res.repos.Insert(excepts2.Difference(excepts1).UnsortedList()...)
   357  		} else {
   358  			excepts := sets.NewString().Union(excepts1)
   359  			// Add any applicable repos in repos2 to excepts
   360  			for _, repo := range c2.repos.UnsortedList() {
   361  				if parts := strings.SplitN(repo, "/", 2); len(parts) == 2 && parts[0] == org {
   362  					excepts.Insert(repo)
   363  				}
   364  			}
   365  			res.orgExceptions[org] = excepts
   366  		}
   367  	}
   368  
   369  	res.repos = res.repos.Difference(c2.repos)
   370  
   371  	for _, repo := range res.repos.UnsortedList() {
   372  		if parts := strings.SplitN(repo, "/", 2); len(parts) == 2 {
   373  			if excepts2, ok := c2.orgExceptions[parts[0]]; ok && !excepts2.Has(repo) {
   374  				res.repos.Delete(repo)
   375  			}
   376  		}
   377  	}
   378  	return res
   379  }
   380  
   381  // intersection returns a new orgRepoConfig that represents the set intersection
   382  // of the repos specified by the receiver and the parameter orgRepoConfigs.
   383  func (c *orgRepoConfig) intersection(c2 *orgRepoConfig) *orgRepoConfig {
   384  	res := &orgRepoConfig{
   385  		orgExceptions: make(map[string]sets.String),
   386  		repos:         sets.NewString(),
   387  	}
   388  	for org, excepts1 := range c.orgExceptions {
   389  		// Include common orgs, but union exceptions.
   390  		if excepts2, ok := c2.orgExceptions[org]; ok {
   391  			res.orgExceptions[org] = excepts1.Union(excepts2)
   392  		} else {
   393  			// Include right side repos that match left side org.
   394  			for _, repo := range c2.repos.UnsortedList() {
   395  				if parts := strings.SplitN(repo, "/", 2); len(parts) == 2 && parts[0] == org && !excepts1.Has(repo) {
   396  					res.repos.Insert(repo)
   397  				}
   398  			}
   399  		}
   400  	}
   401  	for _, repo := range c.repos.UnsortedList() {
   402  		if c2.repos.Has(repo) {
   403  			res.repos.Insert(repo)
   404  		} else if parts := strings.SplitN(repo, "/", 2); len(parts) == 2 {
   405  			// Include left side repos that match right side org.
   406  			if excepts2, ok := c2.orgExceptions[parts[0]]; ok && !excepts2.Has(repo) {
   407  				res.repos.Insert(repo)
   408  			}
   409  		}
   410  	}
   411  	return res
   412  }
   413  
   414  // union returns a new orgRepoConfig that represents the set union of the
   415  // repos specified by the receiver and the parameter orgRepoConfigs
   416  func (c *orgRepoConfig) union(c2 *orgRepoConfig) *orgRepoConfig {
   417  	res := &orgRepoConfig{
   418  		orgExceptions: make(map[string]sets.String),
   419  		repos:         sets.NewString(),
   420  	}
   421  
   422  	for org, excepts1 := range c.orgExceptions {
   423  		// keep only items in both blacklists that are not in the
   424  		// explicit repo whitelists for the other configuration;
   425  		// we know from how the orgRepoConfigs are constructed that
   426  		// a org blacklist won't intersect it's own repo whitelist
   427  		pruned := excepts1.Difference(c2.repos)
   428  		if excepts2, ok := c2.orgExceptions[org]; ok {
   429  			res.orgExceptions[org] = pruned.Intersection(excepts2.Difference(c.repos))
   430  		} else {
   431  			res.orgExceptions[org] = pruned
   432  		}
   433  	}
   434  
   435  	for org, excepts2 := range c2.orgExceptions {
   436  		// update any blacklists not previously updated
   437  		if _, exists := res.orgExceptions[org]; !exists {
   438  			res.orgExceptions[org] = excepts2.Difference(c.repos)
   439  		}
   440  	}
   441  
   442  	// we need to prune out repos in the whitelists which are
   443  	// covered by an org already; we know from above that no
   444  	// org blacklist in the result will contain a repo whitelist
   445  	for _, repo := range c.repos.Union(c2.repos).UnsortedList() {
   446  		parts := strings.SplitN(repo, "/", 2)
   447  		if len(parts) != 2 {
   448  			logrus.Warnf("org/repo %q is formatted incorrectly", repo)
   449  			continue
   450  		}
   451  		if _, exists := res.orgExceptions[parts[0]]; !exists {
   452  			res.repos.Insert(repo)
   453  		}
   454  	}
   455  	return res
   456  }
   457  
   458  func enabledOrgReposForPlugin(c *plugins.Configuration, plugin string, external bool) *orgRepoConfig {
   459  	var (
   460  		orgs  []string
   461  		repos []string
   462  	)
   463  	if external {
   464  		orgs, repos = c.EnabledReposForExternalPlugin(plugin)
   465  	} else {
   466  		orgs, repos = c.EnabledReposForPlugin(plugin)
   467  	}
   468  	orgMap := make(map[string]sets.String, len(orgs))
   469  	for _, org := range orgs {
   470  		orgMap[org] = nil
   471  	}
   472  	return newOrgRepoConfig(orgMap, sets.NewString(repos...))
   473  }
   474  
   475  // ensureValidConfiguration enforces rules about tide and plugin config.
   476  // In this context, a subset is the set of repos or orgs for which a specific
   477  // plugin is either enabled (for plugins) or required for merge (for tide). The
   478  // tide superset is every org or repo that has any configuration at all in tide.
   479  // Specifically:
   480  //   - every item in the tide subset must also be in the plugins subset
   481  //   - every item in the plugins subset that is in the tide superset must also be in the tide subset
   482  // For example:
   483  //   - if org/repo is configured in tide to require lgtm, it must have the lgtm plugin enabled
   484  //   - if org/repo is configured in tide, the tide configuration must require the same set of
   485  //     plugins as are configured. If the repository has LGTM and approve enabled, the tide query
   486  //     must require both labels
   487  func ensureValidConfiguration(plugin, label, verb string, tideSubSet, tideSuperSet, pluginsSubSet *orgRepoConfig) error {
   488  	notEnabled := tideSubSet.difference(pluginsSubSet).items()
   489  	notRequired := pluginsSubSet.intersection(tideSuperSet).difference(tideSubSet).items()
   490  
   491  	var configErrors []error
   492  	if len(notEnabled) > 0 {
   493  		configErrors = append(configErrors, fmt.Errorf("the following orgs or repos %s the %s label for merging but do not enable the %s plugin: %v", verb, label, plugin, notEnabled))
   494  	}
   495  	if len(notRequired) > 0 {
   496  		configErrors = append(configErrors, fmt.Errorf("the following orgs or repos enable the %s plugin but do not %s the %s label for merging: %v", plugin, verb, label, notRequired))
   497  	}
   498  
   499  	return errorutil.NewAggregate(configErrors...)
   500  }
   501  
   502  func validateDecoratedJobs(cfg *config.Config) error {
   503  	var nonDecoratedJobs []string
   504  	for _, presubmit := range cfg.AllPresubmits([]string{}) {
   505  		if presubmit.Agent == string(v1.KubernetesAgent) && !presubmit.Decorate {
   506  			nonDecoratedJobs = append(nonDecoratedJobs, presubmit.Name)
   507  		}
   508  	}
   509  
   510  	for _, postsubmit := range cfg.AllPostsubmits([]string{}) {
   511  		if postsubmit.Agent == string(v1.KubernetesAgent) && !postsubmit.Decorate {
   512  			nonDecoratedJobs = append(nonDecoratedJobs, postsubmit.Name)
   513  		}
   514  	}
   515  
   516  	for _, periodic := range cfg.AllPeriodics() {
   517  		if periodic.Agent == string(v1.KubernetesAgent) && !periodic.Decorate {
   518  			nonDecoratedJobs = append(nonDecoratedJobs, periodic.Name)
   519  		}
   520  	}
   521  
   522  	if len(nonDecoratedJobs) > 0 {
   523  		return fmt.Errorf("the following jobs use the kubernetes provider but do not use the pod utilities: %v", nonDecoratedJobs)
   524  	}
   525  	return nil
   526  }
   527  
   528  func validateNeedsOkToTestLabel(cfg *config.Config) error {
   529  	var queryErrors []error
   530  	for i, query := range cfg.Tide.Queries {
   531  		for _, label := range query.Labels {
   532  			if label == lgtm.LGTMLabel {
   533  				for _, label := range query.MissingLabels {
   534  					if label == labels.NeedsOkToTest {
   535  						queryErrors = append(queryErrors, fmt.Errorf(
   536  							"the tide query at position %d"+
   537  								"forbids the %q label and requires the %q label, "+
   538  								"which is not recommended; "+
   539  								"see https://github.com/kubernetes/test-infra/blob/master/prow/cmd/tide/maintainers.md#best-practices "+
   540  								"for more information",
   541  							i, labels.NeedsOkToTest, lgtm.LGTMLabel),
   542  						)
   543  					}
   544  				}
   545  			}
   546  		}
   547  	}
   548  	return errorutil.NewAggregate(queryErrors...)
   549  }
   550  
   551  func verifyOwnersPlugin(cfg *plugins.Configuration) error {
   552  	// we do not know the set of repos that use OWNERS, but we
   553  	// can get a reasonable proxy for this by looking at where
   554  	// the `approve', `blunderbuss' and `owners-label' plugins
   555  	// are enabled
   556  	approveConfig := enabledOrgReposForPlugin(cfg, approve.PluginName, false)
   557  	blunderbussConfig := enabledOrgReposForPlugin(cfg, blunderbuss.PluginName, false)
   558  	ownersLabelConfig := enabledOrgReposForPlugin(cfg, ownerslabel.PluginName, false)
   559  	ownersConfig := approveConfig.union(blunderbussConfig).union(ownersLabelConfig)
   560  	validateOwnersConfig := enabledOrgReposForPlugin(cfg, verifyowners.PluginName, false)
   561  
   562  	invalid := ownersConfig.difference(validateOwnersConfig).items()
   563  	if len(invalid) > 0 {
   564  		return fmt.Errorf("the following orgs or repos "+
   565  			"enable at least one plugin that uses OWNERS files (%s) "+
   566  			"but do not enable the %s plugin to ensure validity of OWNERS files: %v",
   567  			strings.Join([]string{approve.PluginName, blunderbuss.PluginName, ownerslabel.PluginName}, ", "),
   568  			verifyowners.PluginName, invalid,
   569  		)
   570  	}
   571  	return nil
   572  }