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  }