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  }