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