github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/config.go (about) 1 // Copyright 2017 syzkaller project authors. All rights reserved. 2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4 package main 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/json" 10 "fmt" 11 "net/mail" 12 "regexp" 13 "time" 14 15 "github.com/google/go-cmp/cmp" 16 "github.com/google/syzkaller/dashboard/dashapi" 17 "github.com/google/syzkaller/pkg/auth" 18 "github.com/google/syzkaller/pkg/email" 19 "github.com/google/syzkaller/pkg/subsystem" 20 "github.com/google/syzkaller/pkg/vcs" 21 ) 22 23 // There are multiple configurable aspects of the app (namespaces, reporting, API clients, etc). 24 // The exact config is stored in a global config variable and is read-only. 25 // Also see config_stub.go. 26 type GlobalConfig struct { 27 // Min access levels specified hierarchically throughout the config. 28 AccessLevel AccessLevel 29 // Email suffix of authorized users (e.g. "@foobar.com"). 30 AuthDomain string 31 // Google Analytics Tracking ID. 32 AnalyticsTrackingID string 33 // URL prefix of source coverage reports. 34 // Dashboard will append manager_name.html to that prefix. 35 // syz-ci can upload these reports to GCS. 36 CoverPath string 37 // Global API clients that work across namespaces (e.g. external reporting). 38 // The keys are client identities (names), the values are their passwords. 39 Clients map[string]string 40 // List of emails blocked from issuing test requests. 41 EmailBlocklist []string 42 // Bug obsoleting settings. See ObsoletingConfig for details. 43 Obsoleting ObsoletingConfig 44 // Namespace that is shown by default (no namespace selected yet). 45 DefaultNamespace string 46 // Per-namespace config. 47 // Namespaces are a mechanism to separate groups of different kernels. 48 // E.g. Debian 4.4 kernels and Ubuntu 4.9 kernels. 49 // Each namespace has own reporting config, own API clients 50 // and bugs are not merged across namespaces. 51 Namespaces map[string]*Config 52 // app's own email address which will appear in FROM field of mails sent by the app. 53 OwnEmailAddress string 54 // List of email addresses which are considered app's own email addresses. 55 // All emails sent from one of these email addresses shall be ignored by the app on reception. 56 ExtraOwnEmailAddresses []string 57 // Main part of the URL at which the app is reachable. 58 // This URL is used e.g. to construct HTML links contained in the emails sent by the app. 59 AppURL string 60 // The email address to display on all web pages. 61 ContactEmail string 62 // Emails received via the addresses below will be attributed to the corresponding 63 // kind of Discussion. 64 DiscussionEmails []DiscussionEmailConfig 65 // Incoming request throttling. 66 Throttle ThrottleConfig 67 } 68 69 // Per-namespace config. 70 type Config struct { 71 // See GlobalConfig.AccessLevel. 72 AccessLevel AccessLevel 73 // If set, this namespace is not actively tested, no notifications are sent, etc. 74 // It's kept mostly read-only for historical reference. 75 Decommissioned bool 76 // Name used in UI. 77 DisplayTitle string 78 // Unique string that allows to show "similar bugs" across different namespaces. 79 // Similar bugs are shown only across namespaces with the same value of SimilarityDomain. 80 SimilarityDomain string 81 // Per-namespace clients that act only on a particular namespace. 82 // The keys are client identities (names), the values are their passwords. 83 Clients map[string]string 84 // A random string used for hashing, can be anything, but once fixed it can't 85 // be changed as it becomes a part of persistent bug identifiers. 86 Key string 87 // Mail bugs without reports (e.g. "no output"). 88 MailWithoutReport bool 89 // How long should we wait before reporting a bug. 90 ReportingDelay time.Duration 91 // How long should we wait for a C repro before reporting a bug. 92 WaitForRepro time.Duration 93 // If set, successful fix bisections will auto-close the bug. 94 FixBisectionAutoClose bool 95 // If set, dashboard will periodically request repros and revoke no longer working ones. 96 RetestRepros bool 97 // If set, dashboard will periodically verify the presence of the missing backports in the 98 // tested kernel trees. 99 RetestMissingBackports bool 100 // If set, dashboard will create patch testing jobs to determine bug origin trees. 101 FindBugOriginTrees bool 102 // Managers contains some special additional info about syz-manager instances. 103 Managers map[string]ConfigManager 104 // Reporting config. 105 Reporting []Reporting 106 // TransformCrash hook is called when a manager uploads a crash. 107 // The hook can transform the crash or discard the crash by returning false. 108 TransformCrash func(build *Build, crash *dashapi.Crash) bool `json:"-"` 109 // NeedRepro hook can be used to prevent reproduction of some bugs. 110 NeedRepro func(bug *Bug) bool `json:"-"` 111 // List of kernel repositories for this namespace. 112 // The first repo considered the "main" repo (e.g. fixing commit info is shown against this repo). 113 // Other repos are secondary repos, they may be tested or not. 114 // If not tested they are used to poll for fixing commits. 115 Repos []KernelRepo 116 // If not nil, bugs in this namespace will be exported to the specified Kcidb. 117 Kcidb *KcidbConfig 118 // Subsystems config. 119 Subsystems SubsystemsConfig 120 // Instead of Last acitivity, display Discussions on the main page. 121 DisplayDiscussions bool 122 // Cache what we display on the web dashboard. 123 CacheUIPages bool 124 } 125 126 // DiscussionEmailConfig defines the correspondence between an email and a DiscussionSource. 127 type DiscussionEmailConfig struct { 128 // The address at which syzbot has received the message. 129 ReceiveAddress string 130 // The associated DiscussionSource. 131 Source dashapi.DiscussionSource 132 } 133 134 // SubsystemsConfig describes where to take the list of subsystems and how to infer them. 135 type SubsystemsConfig struct { 136 // If Service is set, dashboard will use it to infer and recalculate subsystems. 137 Service *subsystem.Service 138 // If all existing subsystem labels must be recalculated, increase this integer. 139 Revision int 140 // Periodic per-subsystem reminders about open bugs. 141 Reminder *BugListReportingConfig 142 // Maps old subsystem names to new ones. 143 Redirect map[string]string 144 } 145 146 // BugListReportingConfig describes how aggregated reminders about open bugs should be processed. 147 type BugListReportingConfig struct { 148 // Reports are sent every PeriodDays days (30 by default). 149 PeriodDays int 150 // Reports will include details about BugsInReport bugs (10 by default). 151 BugsInReport int 152 // Bugs that were first discovered less than MinBugAge ago, will not be included. 153 // The default value is 1 weeks. 154 MinBugAge time.Duration 155 // Don't include a bug in the report if there has been a human reply to one of the 156 // discussions involving the bug during the last UserReplyFrist units of time. 157 // The default value is 2 weeks. 158 UserReplyFrist time.Duration 159 // Reports will only be sent if there are at least MinBugsCount bugs to notify about. 160 // The default value is 2. 161 MinBugsCount int 162 // SourceReporting is the name of the reporting stage from which bugs should be taken. 163 SourceReporting string 164 // If ModerationConfig is set, bug lists will be first sent there for human confirmation. 165 // For now, only EmailConfig is supported. 166 ModerationConfig ReportingType 167 // Config specifies how exactly such notifications should be delivered. 168 // For now, only EmailConfig is supported. 169 Config ReportingType 170 } 171 172 // ObsoletingConfig describes how bugs should be obsoleted. 173 // First, for each bug we conservatively estimate period since the last crash 174 // when we consider it stopped happenning. This estimation is based on the first/last time 175 // and number and rate of crashes. Then this period is capped by MinPeriod/MaxPeriod. 176 // Then if the period has elapsed since the last crash, we obsolete the bug. 177 // NonFinalMinPeriod/NonFinalMaxPeriod (if specified) are used to cap bugs in non-final reportings. 178 // Additionally ConfigManager.ObsoletingMin/MaxPeriod override the cap settings 179 // for bugs that happen only on that manager. 180 // If no periods are specified, no bugs are obsoleted. 181 type ObsoletingConfig struct { 182 MinPeriod time.Duration 183 MaxPeriod time.Duration 184 NonFinalMinPeriod time.Duration 185 NonFinalMaxPeriod time.Duration 186 // Reproducers are retested every ReproRetestPeriod. 187 // If the period is zero, not retesting is performed. 188 ReproRetestPeriod time.Duration 189 // Reproducer retesting begins after there have been no crashes during 190 // the ReproRetestStart period. 191 // By default, it's 14 days. 192 ReproRetestStart time.Duration 193 } 194 195 // ConfigManager describes a single syz-manager instance. 196 // Dashboard does not generally need to know about all of them, 197 // but in some special cases it needs to know some additional information. 198 type ConfigManager struct { 199 Decommissioned bool // The instance is no longer active. 200 DelegatedTo string // If Decommissioned, test requests should go to this instance instead. 201 // Normally instances can test patches on any tree. 202 // However, some (e.g. non-upstreamed KMSAN) can test only on a fixed tree. 203 // RestrictedTestingRepo contains the repo for such instances 204 // and RestrictedTestingReason contains a human readable reason for the restriction. 205 RestrictedTestingRepo string 206 RestrictedTestingReason string 207 // If a bug happens only on this manager, this overrides global obsoleting settings. 208 // See ObsoletingConfig for details. 209 ObsoletingMinPeriod time.Duration 210 ObsoletingMaxPeriod time.Duration 211 // Determines if fix bisection should be disabled on this manager. 212 FixBisectionDisabled bool 213 // CC for all bugs that happened only on this manager. 214 CC CCConfig 215 // Other parameters being equal, Priority helps to order bug's crashes. 216 // Priority is an integer in the range [-3;3]. 217 Priority int 218 } 219 220 const ( 221 MinManagerPriority = -3 222 MaxManagerPriority = 3 223 ) 224 225 // One reporting stage. 226 type Reporting struct { 227 // See GlobalConfig.AccessLevel. 228 AccessLevel AccessLevel 229 // A unique name (the app does not care about exact contents). 230 Name string 231 // Name used in UI. 232 DisplayTitle string 233 // Filter can be used to conditionally skip this reporting or hold off reporting. 234 Filter ReportingFilter `json:"-"` 235 // How many new bugs report per day. 236 DailyLimit int 237 // Upstream reports into next reporting after this period. 238 Embargo time.Duration 239 // Type of reporting and its configuration. 240 // The app has one built-in type, EmailConfig, which reports bugs by email. 241 // And ExternalConfig which can be used to attach any external reporting system (e.g. Bugzilla). 242 Config ReportingType 243 // List of labels to notify about (keys are strings of form "label:value"). 244 // The value is the string that will be included in the notification message. 245 // Notifications will only be sent for automatically assigned labels. 246 Labels map[string]string 247 // Set for all but last reporting stages. 248 moderation bool 249 } 250 251 type ReportingType interface { 252 // Type returns a unique string that identifies this reporting type (e.g. "email"). 253 Type() string 254 // Validate validates the current object, this is called only during init. 255 Validate() error 256 } 257 258 type KernelRepo struct { 259 URL string 260 Branch string 261 // Alias is a short, readable name of a kernel repository. 262 Alias string 263 // ReportingPriority says if we need to prefer to report crashes in this 264 // repo over crashes in repos with lower value. Must be in [0-9] range. 265 ReportingPriority int 266 // CC for all bugs reported on this repo. 267 CC CCConfig 268 // This repository should not be polled for commits, e.g. because it's no longer active. 269 NoPoll bool 270 // LabelIntroduced is assigned to a bug if it was supposedly introduced 271 // in this particular tree (i.e. no other tree from CommitInflow has it). 272 LabelIntroduced string 273 // LabelReached is assiged to a bug if it's the latest tree so far to which 274 // the bug has spread (i.e. no other tree to which commits flow from this one 275 // has this bug). 276 LabelReached string 277 // CommitInflow are the descriptions of commit sources of this tree. 278 CommitInflow []KernelRepoLink 279 // Enable the missing backport tracking feature for this tree. 280 DetectMissingBackports bool 281 // Append this string to the config file before running reproducers on this tree. 282 AppendConfig string 283 } 284 285 type KernelRepoLink struct { 286 // Alias of the repository from which commits flow into the current one. 287 Alias string 288 // Whether commits from the other repository merged or cherry-picked. 289 Merge bool 290 // Whether syzbot should try to fix bisect the bug in the Alias tree. 291 BisectFixes bool 292 } 293 294 type CCConfig struct { 295 // Additional CC list to add to bugs unconditionally. 296 Always []string 297 // Additional CC list to add to bugs if we are mailing maintainers. 298 Maintainers []string 299 // Additional CC list to add to build/boot bugs if we are mailing maintainers. 300 BuildMaintainers []string 301 } 302 303 type KcidbConfig struct { 304 // Origin is how this system identified in Kcidb, e.g. "syzbot_foobar". 305 Origin string 306 // Project is Kcidb GCE project name, e.g. "kernelci-production". 307 Project string 308 // Topic is pubsub topic to publish messages to, e.g. "playground_kernelci_new". 309 Topic string 310 // Credentials is Google application credentials file contents to use for authorization. 311 Credentials []byte 312 } 313 314 // ThrottleConfig determines how many requests a single client can make in a period of time. 315 type ThrottleConfig struct { 316 // The time period to be considered. 317 Window time.Duration 318 // No more than Limit requests are allowed within the time window. 319 Limit int 320 } 321 322 func (t ThrottleConfig) Empty() bool { 323 return t.Window == 0 || t.Limit == 0 324 } 325 326 var ( 327 namespaceNameRe = regexp.MustCompile("^[a-zA-Z0-9-_.]{4,32}$") 328 clientNameRe = regexp.MustCompile("^[a-zA-Z0-9-_.]{4,100}$") 329 clientKeyRe = regexp.MustCompile("^([a-zA-Z0-9]{16,128})|(" + regexp.QuoteMeta(auth.OauthMagic) + ".*)$") 330 ) 331 332 type ( 333 FilterResult int 334 ReportingFilter func(bug *Bug) FilterResult 335 ) 336 337 const ( 338 FilterReport FilterResult = iota // Report bug in this reporting (default). 339 FilterSkip // Skip this reporting and proceed to the next one. 340 FilterHold // Hold off with reporting this bug. 341 ) 342 343 func ConstFilter(result FilterResult) ReportingFilter { 344 return func(bug *Bug) FilterResult { 345 return result 346 } 347 } 348 349 func (cfg *Config) ReportingByName(name string) *Reporting { 350 for i := range cfg.Reporting { 351 reporting := &cfg.Reporting[i] 352 if reporting.Name == name { 353 return reporting 354 } 355 } 356 return nil 357 } 358 359 // configDontUse holds the configuration object that is installed either by tests 360 // or from mainConfig in main function (a separate file should install mainConfig 361 // in an init function). 362 // Please access it via the getConfig(context.Context) method. 363 var ( 364 configDontUse *GlobalConfig 365 mainConfig *GlobalConfig 366 ) 367 368 // To ensure config integrity during tests, we marshal config after it's installed 369 // and optionally verify it during execution. 370 var ( 371 ensureConfigImmutability = false 372 marshaledConfig = "" 373 ) 374 375 func installConfig(cfg *GlobalConfig) { 376 checkConfig(cfg) 377 if configDontUse != nil { 378 panic("another config is already installed") 379 } 380 configDontUse = cfg 381 if ensureConfigImmutability { 382 marshaledConfig = cfg.marshalJSON() 383 } 384 initEmailReporting() 385 initHTTPHandlers() 386 initAPIHandlers() 387 initKcidb() 388 } 389 390 var contextConfigKey = "Updated config (to be used during tests). Use only in tests!" 391 392 func contextWithConfig(c context.Context, cfg *GlobalConfig) context.Context { 393 return context.WithValue(c, &contextConfigKey, cfg) 394 } 395 396 func getConfig(c context.Context) *GlobalConfig { 397 // Check point. 398 validateGlobalConfig() 399 400 if val, ok := c.Value(&contextConfigKey).(*GlobalConfig); ok { 401 return val 402 } 403 return configDontUse // The base config was not overwriten. 404 } 405 406 func validateGlobalConfig() { 407 if ensureConfigImmutability { 408 currentConfig := configDontUse.marshalJSON() 409 if diff := cmp.Diff(currentConfig, marshaledConfig); diff != "" { 410 panic("global config changed during execution: " + diff) 411 } 412 } 413 } 414 415 func getNsConfig(c context.Context, ns string) *Config { 416 return getConfig(c).Namespaces[ns] 417 } 418 419 func checkConfig(cfg *GlobalConfig) { 420 if cfg == nil { 421 panic("installing nil config") 422 } 423 if len(cfg.Namespaces) == 0 { 424 panic("no namespaces found") 425 } 426 for i := range cfg.EmailBlocklist { 427 cfg.EmailBlocklist[i] = email.CanonicalEmail(cfg.EmailBlocklist[i]) 428 } 429 if cfg.Throttle.Limit < 0 { 430 panic("throttle limit cannot be negative") 431 } 432 if (cfg.Throttle.Limit != 0) != (cfg.Throttle.Window != 0) { 433 panic("throttling window and limit must be both set") 434 } 435 namespaces := make(map[string]bool) 436 clientNames := make(map[string]bool) 437 checkClients(clientNames, cfg.Clients) 438 checkConfigAccessLevel(&cfg.AccessLevel, AccessPublic, "global") 439 checkObsoleting(&cfg.Obsoleting) 440 if cfg.Namespaces[cfg.DefaultNamespace] == nil { 441 panic(fmt.Sprintf("default namespace %q is not found", cfg.DefaultNamespace)) 442 } 443 for ns, cfg := range cfg.Namespaces { 444 checkNamespace(ns, cfg, namespaces, clientNames) 445 } 446 checkDiscussionEmails(cfg.DiscussionEmails) 447 } 448 449 func checkDiscussionEmails(list []DiscussionEmailConfig) { 450 dup := map[string]struct{}{} 451 for _, item := range list { 452 email := item.ReceiveAddress 453 if _, ok := dup[email]; ok { 454 panic(fmt.Sprintf("duplicate %s in DiscussionEmails", email)) 455 } 456 dup[email] = struct{}{} 457 } 458 } 459 460 func checkObsoleting(o *ObsoletingConfig) { 461 if (o.MinPeriod == 0) != (o.MaxPeriod == 0) { 462 panic("obsoleting: both or none of Min/MaxPeriod must be specified") 463 } 464 if o.MinPeriod > o.MaxPeriod { 465 panic(fmt.Sprintf("obsoleting: Min > MaxPeriod (%v > %v)", o.MinPeriod, o.MaxPeriod)) 466 } 467 if o.MinPeriod != 0 && o.MinPeriod < 24*time.Hour { 468 panic(fmt.Sprintf("obsoleting: too low MinPeriod: %v, want at least %v", o.MinPeriod, 24*time.Hour)) 469 } 470 if (o.NonFinalMinPeriod == 0) != (o.NonFinalMaxPeriod == 0) { 471 panic("obsoleting: both or none of NonFinalMin/MaxPeriod must be specified") 472 } 473 if o.NonFinalMinPeriod > o.NonFinalMaxPeriod { 474 panic(fmt.Sprintf("obsoleting: NonFinalMin > MaxPeriod (%v > %v)", o.NonFinalMinPeriod, o.NonFinalMaxPeriod)) 475 } 476 if o.NonFinalMinPeriod != 0 && o.NonFinalMinPeriod < 24*time.Hour { 477 panic(fmt.Sprintf("obsoleting: too low MinPeriod: %v, want at least %v", o.NonFinalMinPeriod, 24*time.Hour)) 478 } 479 if o.MinPeriod == 0 && o.NonFinalMinPeriod != 0 { 480 panic("obsoleting: NonFinalMinPeriod without MinPeriod") 481 } 482 if o.ReproRetestStart == 0 { 483 o.ReproRetestStart = time.Hour * 24 * 14 484 } 485 } 486 487 func checkNamespace(ns string, cfg *Config, namespaces, clientNames map[string]bool) { 488 if !namespaceNameRe.MatchString(ns) { 489 panic(fmt.Sprintf("bad namespace name: %q", ns)) 490 } 491 if namespaces[ns] { 492 panic(fmt.Sprintf("duplicate namespace %q", ns)) 493 } 494 namespaces[ns] = true 495 if cfg.DisplayTitle == "" { 496 cfg.DisplayTitle = ns 497 } 498 if cfg.SimilarityDomain == "" { 499 cfg.SimilarityDomain = ns 500 } 501 checkClients(clientNames, cfg.Clients) 502 for name, mgr := range cfg.Managers { 503 checkManager(ns, name, mgr) 504 } 505 if !clientKeyRe.MatchString(cfg.Key) { 506 panic(fmt.Sprintf("bad namespace %q key: %q", ns, cfg.Key)) 507 } 508 if len(cfg.Reporting) == 0 { 509 panic(fmt.Sprintf("no reporting in namespace %q", ns)) 510 } 511 if cfg.TransformCrash == nil { 512 cfg.TransformCrash = func(build *Build, crash *dashapi.Crash) bool { 513 return true 514 } 515 } 516 if cfg.NeedRepro == nil { 517 cfg.NeedRepro = func(bug *Bug) bool { 518 return true 519 } 520 } 521 if cfg.Kcidb != nil { 522 checkKcidb(ns, cfg.Kcidb) 523 } 524 checkKernelRepos(ns, cfg, cfg.Repos) 525 checkNamespaceReporting(ns, cfg) 526 checkSubsystems(ns, cfg) 527 } 528 529 func checkSubsystems(ns string, cfg *Config) { 530 if cfg.Subsystems.Reminder == nil { 531 // Nothing to validate. 532 return 533 } 534 if cfg.Subsystems.Service == nil { 535 panic(fmt.Sprintf("%v: Subsystems.Reminder is set while Subsystems.Service is nil", ns)) 536 } 537 reminder := cfg.Subsystems.Reminder 538 if reminder.SourceReporting == "" { 539 panic(fmt.Sprintf("%v: Reminder.SourceReporting must be set", ns)) 540 } 541 if reminder.Config == nil { 542 panic(fmt.Sprintf("%v: Reminder.Config must be set", ns)) 543 } 544 reporting := cfg.ReportingByName(reminder.SourceReporting) 545 if reporting == nil { 546 panic(fmt.Sprintf("%v: Reminder.SourceReporting %v points to a non-existent reporting", 547 ns, reminder.SourceReporting)) 548 } 549 if reporting.AccessLevel != AccessPublic { 550 panic(fmt.Sprintf("%v: Reminder.SourceReporting must point to a public reporting", ns)) 551 } 552 if reminder.PeriodDays == 0 { 553 reminder.PeriodDays = 30 554 } else if reminder.PeriodDays < 0 { 555 panic(fmt.Sprintf("%v: Reminder.PeriodDays must be > 0", ns)) 556 } 557 if reminder.BugsInReport == 0 { 558 reminder.BugsInReport = 10 559 } else if reminder.BugsInReport < 0 { 560 panic(fmt.Sprintf("%v: Reminder.BugsInReport must be > 0", ns)) 561 } 562 if reminder.MinBugAge == 0 { 563 reminder.MinBugAge = 24 * time.Hour * 7 564 } 565 if reminder.UserReplyFrist == 0 { 566 reminder.UserReplyFrist = 24 * time.Hour * 7 * 2 567 } 568 if reminder.MinBugsCount == 0 { 569 reminder.MinBugsCount = 2 570 } else if reminder.MinBugsCount < 0 { 571 panic(fmt.Sprintf("%v: Reminder.MinBugsCount must be > 0", ns)) 572 } 573 } 574 575 func checkKernelRepos(ns string, config *Config, repos []KernelRepo) { 576 if len(repos) == 0 { 577 panic(fmt.Sprintf("no repos in namespace %q", ns)) 578 } 579 introduced, reached := map[string]bool{}, map[string]bool{} 580 aliasMap := map[string]bool{} 581 canBeLabels := false 582 for _, repo := range repos { 583 if !vcs.CheckRepoAddress(repo.URL) { 584 panic(fmt.Sprintf("%v: bad repo URL %q", ns, repo.URL)) 585 } 586 if !vcs.CheckBranch(repo.Branch) { 587 panic(fmt.Sprintf("%v: bad repo branch %q", ns, repo.Branch)) 588 } 589 if repo.Alias == "" { 590 panic(fmt.Sprintf("%v: empty repo alias for %q", ns, repo.Alias)) 591 } 592 if aliasMap[repo.Alias] { 593 panic(fmt.Sprintf("%v: duplicate alias for %q", ns, repo.Alias)) 594 } 595 aliasMap[repo.Alias] = true 596 if prio := repo.ReportingPriority; prio < 0 || prio > 9 { 597 panic(fmt.Sprintf("%v: bad kernel repo reporting priority %v for %q", ns, prio, repo.Alias)) 598 } 599 checkCC(&repo.CC) 600 if repo.LabelIntroduced != "" { 601 introduced[repo.LabelIntroduced] = true 602 if reached[repo.LabelIntroduced] { 603 panic(fmt.Sprintf("%v: label %s is used for both introduced and reached", ns, repo.LabelIntroduced)) 604 } 605 } 606 if repo.LabelReached != "" { 607 reached[repo.LabelReached] = true 608 if introduced[repo.LabelReached] { 609 panic(fmt.Sprintf("%v: label %s is used for both introduced and reached", ns, repo.LabelReached)) 610 } 611 } 612 canBeLabels = canBeLabels || repo.DetectMissingBackports 613 } 614 if len(introduced)+len(reached) > 0 { 615 canBeLabels = true 616 } 617 if canBeLabels && !config.FindBugOriginTrees { 618 panic(fmt.Sprintf("%v: repo labels are set, but FindBugOriginTrees is disabled", ns)) 619 } 620 if !canBeLabels && config.FindBugOriginTrees { 621 panic(fmt.Sprintf("%v: FindBugOriginTrees is enabled, but all repo labels are disabled", ns)) 622 } 623 // And finally test links. 624 _, err := makeRepoGraph(repos) 625 if err != nil { 626 panic(fmt.Sprintf("%v: %s", ns, err)) 627 } 628 } 629 630 func checkCC(cc *CCConfig) { 631 emails := append(append(append([]string{}, cc.Always...), cc.Maintainers...), cc.BuildMaintainers...) 632 for _, email := range emails { 633 if _, err := mail.ParseAddress(email); err != nil { 634 panic(fmt.Sprintf("bad email address %q: %v", email, err)) 635 } 636 } 637 } 638 639 func checkNamespaceReporting(ns string, cfg *Config) { 640 checkConfigAccessLevel(&cfg.AccessLevel, cfg.AccessLevel, fmt.Sprintf("namespace %q", ns)) 641 parentAccessLevel := cfg.AccessLevel 642 reportingNames := make(map[string]bool) 643 // Go backwards because access levels get stricter backwards. 644 for ri := len(cfg.Reporting) - 1; ri >= 0; ri-- { 645 reporting := &cfg.Reporting[ri] 646 if reporting.Name == "" { 647 panic(fmt.Sprintf("empty reporting name in namespace %q", ns)) 648 } 649 if reportingNames[reporting.Name] { 650 panic(fmt.Sprintf("duplicate reporting name %q", reporting.Name)) 651 } 652 if reporting.DisplayTitle == "" { 653 reporting.DisplayTitle = reporting.Name 654 } 655 reporting.moderation = ri < len(cfg.Reporting)-1 656 if !reporting.moderation && reporting.Embargo != 0 { 657 panic(fmt.Sprintf("embargo in the last reporting %v", reporting.Name)) 658 } 659 checkConfigAccessLevel(&reporting.AccessLevel, parentAccessLevel, 660 fmt.Sprintf("reporting %q/%q", ns, reporting.Name)) 661 parentAccessLevel = reporting.AccessLevel 662 if reporting.DailyLimit < 0 || reporting.DailyLimit > 1000 { 663 panic(fmt.Sprintf("reporting %v: bad daily limit %v", reporting.Name, reporting.DailyLimit)) 664 } 665 if reporting.Filter == nil { 666 reporting.Filter = ConstFilter(FilterReport) 667 } 668 reportingNames[reporting.Name] = true 669 if reporting.Config.Type() == "" { 670 panic(fmt.Sprintf("empty reporting type for %q", reporting.Name)) 671 } 672 if err := reporting.Config.Validate(); err != nil { 673 panic(err) 674 } 675 if _, err := json.Marshal(reporting.Config); err != nil { 676 panic(fmt.Sprintf("failed to json marshal %q config: %v", 677 reporting.Name, err)) 678 } 679 } 680 } 681 682 func checkManager(ns, name string, mgr ConfigManager) { 683 if mgr.Decommissioned && mgr.DelegatedTo == "" { 684 panic(fmt.Sprintf("decommissioned manager %v/%v does not have delegate", ns, name)) 685 } 686 if !mgr.Decommissioned && mgr.DelegatedTo != "" { 687 panic(fmt.Sprintf("non-decommissioned manager %v/%v has delegate", ns, name)) 688 } 689 if mgr.RestrictedTestingRepo != "" && mgr.RestrictedTestingReason == "" { 690 panic(fmt.Sprintf("restricted manager %v/%v does not have restriction reason", ns, name)) 691 } 692 if mgr.RestrictedTestingRepo == "" && mgr.RestrictedTestingReason != "" { 693 panic(fmt.Sprintf("unrestricted manager %v/%v has restriction reason", ns, name)) 694 } 695 if (mgr.ObsoletingMinPeriod == 0) != (mgr.ObsoletingMaxPeriod == 0) { 696 panic(fmt.Sprintf("manager %v/%v obsoleting: both or none of Min/MaxPeriod must be specified", ns, name)) 697 } 698 if mgr.ObsoletingMinPeriod > mgr.ObsoletingMaxPeriod { 699 panic(fmt.Sprintf("manager %v/%v obsoleting: Min > MaxPeriod", ns, name)) 700 } 701 if mgr.ObsoletingMinPeriod != 0 && mgr.ObsoletingMinPeriod < 24*time.Hour { 702 panic(fmt.Sprintf("manager %v/%v obsoleting: too low MinPeriod", ns, name)) 703 } 704 if mgr.Priority < MinManagerPriority && mgr.Priority > MaxManagerPriority { 705 panic(fmt.Sprintf("manager %v/%v priority is not in the [%d;%d] range", 706 ns, name, MinManagerPriority, MaxManagerPriority)) 707 } 708 checkCC(&mgr.CC) 709 } 710 711 func checkKcidb(ns string, kcidb *KcidbConfig) { 712 if !regexp.MustCompile("^[a-z0-9_]+$").MatchString(kcidb.Origin) { 713 panic(fmt.Sprintf("%v: bad Kcidb origin %q", ns, kcidb.Origin)) 714 } 715 if kcidb.Project == "" { 716 panic(fmt.Sprintf("%v: empty Kcidb project", ns)) 717 } 718 if kcidb.Topic == "" { 719 panic(fmt.Sprintf("%v: empty Kcidb topic", ns)) 720 } 721 if !bytes.Contains(kcidb.Credentials, []byte("private_key")) { 722 panic(fmt.Sprintf("%v: empty Kcidb credentials", ns)) 723 } 724 } 725 726 func checkConfigAccessLevel(current *AccessLevel, parent AccessLevel, what string) { 727 verifyAccessLevel(parent) 728 if *current == 0 { 729 *current = parent 730 } 731 verifyAccessLevel(*current) 732 if *current < parent { 733 panic(fmt.Sprintf("bad %v access level %v", what, *current)) 734 } 735 } 736 737 func checkClients(clientNames map[string]bool, clients map[string]string) { 738 for name, key := range clients { 739 if !clientNameRe.MatchString(name) { 740 panic(fmt.Sprintf("bad client name: %v", name)) 741 } 742 if !clientKeyRe.MatchString(key) { 743 panic(fmt.Sprintf("bad client key: %v", key)) 744 } 745 if clientNames[name] { 746 panic(fmt.Sprintf("duplicate client name: %v", name)) 747 } 748 clientNames[name] = true 749 } 750 } 751 752 func (cfg *Config) lastActiveReporting() int { 753 last := len(cfg.Reporting) - 1 754 for last > 0 && cfg.Reporting[last].DailyLimit == 0 { 755 last-- 756 } 757 return last 758 } 759 760 func (gCfg *GlobalConfig) marshalJSON() string { 761 ret, err := json.MarshalIndent(gCfg, "", " ") 762 if err != nil { 763 panic(err) 764 } 765 return string(ret) 766 }