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 }