github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-cluster/pkg/app/config.go (about)

     1  // Copyright 2025 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 app
     5  
     6  import (
     7  	"fmt"
     8  	"net/mail"
     9  	"os"
    10  	"strings"
    11  	"sync"
    12  
    13  	"gopkg.in/yaml.v3"
    14  )
    15  
    16  type AppConfig struct {
    17  	// The name that will be shown on the Web UI.
    18  	Name string `yaml:"name"`
    19  	// Public URL of the web dashboard (without / at the end).
    20  	URL string `yaml:"URL"`
    21  	// How many workflows are scheduled in parallel.
    22  	ParallelWorkflows int `yaml:"parallelWorkflows"`
    23  	// What Lore archives are to be polled for new patch series.
    24  	LoreArchives []string `yaml:"loreArchives"`
    25  	// Parameters used for sending/generating emails.
    26  	EmailReporting *EmailConfig `yaml:"emailReporting"`
    27  }
    28  
    29  const (
    30  	SenderSMTP    = "smtp"
    31  	SenderDashapi = "dashapi"
    32  )
    33  
    34  type EmailConfig struct {
    35  	// The public name of the system.
    36  	Name string `yaml:"name"`
    37  	// Link to the public documentation.
    38  	DocsLink string `yaml:"docs"`
    39  	// Contact email.
    40  	SupportEmail string `yaml:"supportEmail"`
    41  	// The address will be suggested for the Tested-by tag.
    42  	CreditEmail string `yaml:"creditEmail"`
    43  	// The means to send the emails ("smtp", "dashapi").
    44  	Sender string `yaml:"sender"`
    45  	// Will be used if Sender is "smtp".
    46  	SMTP *SMTPConfig `yaml:"smtpConfig"`
    47  	// Will be used if Sender is "dashapi".
    48  	Dashapi *DashapiConfig `yaml:"dashapiConfig"`
    49  	// Moderation requests will be sent there.
    50  	ModerationList string `yaml:"moderationList"`
    51  	// The list email-reporter listens on.
    52  	ArchiveList string `yaml:"archiveList"`
    53  	// The lists/emails to be Cc'd for actual reports (not moderation).
    54  	ReportCC []string `yaml:"reportCc"`
    55  	// Lore git archive to poll for incoming messages.
    56  	LoreArchiveURL string `yaml:"loreArchiveURL"`
    57  	// The prefix which will be added to all reports' titles.
    58  	SubjectPrefix string `yaml:"subjectPrefix"`
    59  }
    60  
    61  type SMTPConfig struct {
    62  	// The email from which to send the reports.
    63  	From string `yaml:"from"`
    64  }
    65  
    66  type DashapiConfig struct {
    67  	// The URI at which the dashboard is accessible.
    68  	Addr string `yaml:"addr"`
    69  	// Client name to be used for authorization.
    70  	// OAuth will be used instead of a key.
    71  	Client string `yaml:"client"`
    72  	// The email from which to send the reports.
    73  	From string `yaml:"from"`
    74  	// The emails will be sent from "name+" + contextPrefix + ID + "@domain".
    75  	ContextPrefix string `yaml:"contextPrefix"`
    76  }
    77  
    78  // The project configuration is expected to be mounted at /config/config.yaml.
    79  
    80  func Config() (*AppConfig, error) {
    81  	configLoadedOnce.Do(loadConfig)
    82  	return config, configErr
    83  }
    84  
    85  const configPath = `/config/config.yaml`
    86  
    87  var configLoadedOnce sync.Once
    88  var configErr error
    89  var config *AppConfig
    90  
    91  func loadConfig() {
    92  	data, err := os.ReadFile(configPath)
    93  	if err != nil {
    94  		configErr = fmt.Errorf("failed to read %q: %w", configPath, err)
    95  		return
    96  	}
    97  	obj := AppConfig{
    98  		Name:              "Syzbot CI",
    99  		ParallelWorkflows: 1,
   100  	}
   101  	err = yaml.Unmarshal(data, &obj)
   102  	if err != nil {
   103  		configErr = fmt.Errorf("failed to parse: %w", err)
   104  		return
   105  	}
   106  	err = obj.Validate()
   107  	if err != nil {
   108  		configErr = err
   109  		return
   110  	}
   111  	config = &obj
   112  }
   113  
   114  func (c AppConfig) Validate() error {
   115  	if c.ParallelWorkflows < 0 {
   116  		return fmt.Errorf("parallelWorkflows must be non-negative")
   117  	}
   118  	if err := ensureURL("url", c.URL); err != nil {
   119  		return err
   120  	}
   121  	if c.EmailReporting != nil {
   122  		if err := c.EmailReporting.Validate(); err != nil {
   123  			return fmt.Errorf("emailReporting: %w", err)
   124  		}
   125  	}
   126  	return nil
   127  }
   128  
   129  func (c EmailConfig) Validate() error {
   130  	for _, err := range []error{
   131  		ensureNonEmpty("name", c.Name),
   132  		ensureEmail("supportEmail", c.SupportEmail),
   133  		ensureEmail("moderationList", c.ModerationList),
   134  		ensureEmail("archiveList", c.ArchiveList),
   135  	} {
   136  		if err != nil {
   137  			return err
   138  		}
   139  	}
   140  	if c.SMTP != nil {
   141  		if err := c.SMTP.Validate(); err != nil {
   142  			return err
   143  		}
   144  	}
   145  	if c.Dashapi != nil {
   146  		if err := c.Dashapi.Validate(); err != nil {
   147  			return err
   148  		}
   149  	}
   150  	switch c.Sender {
   151  	case SenderSMTP:
   152  		if c.SMTP == nil {
   153  			return fmt.Errorf("sender is %q, but smtpConfig is empty", SenderSMTP)
   154  		}
   155  	case SenderDashapi:
   156  		if c.Dashapi == nil {
   157  			return fmt.Errorf("sender is %q, but dashapiConfig is empty", SenderDashapi)
   158  		}
   159  	default:
   160  		return fmt.Errorf("invalid sender value, must be %q or %q", SenderSMTP, SenderDashapi)
   161  	}
   162  	return nil
   163  }
   164  
   165  func (c SMTPConfig) Validate() error {
   166  	return ensureEmail("from", c.From)
   167  }
   168  
   169  func (c DashapiConfig) Validate() error {
   170  	for _, err := range []error{
   171  		ensureNonEmpty("addr", c.Addr),
   172  		ensureNonEmpty("client", c.Client),
   173  		ensureEmail("from", c.From),
   174  	} {
   175  		if err != nil {
   176  			return err
   177  		}
   178  	}
   179  	return nil
   180  }
   181  
   182  func ensureNonEmpty(name, val string) error {
   183  	if val == "" {
   184  		return fmt.Errorf("%v must not be empty", name)
   185  	}
   186  	return nil
   187  }
   188  
   189  func ensureURL(name, val string) error {
   190  	if err := ensureNonEmpty(name, val); err != nil {
   191  		return err
   192  	}
   193  	if strings.HasSuffix(val, "/") {
   194  		return fmt.Errorf("%v should not contain / at the end", name)
   195  	}
   196  	return nil
   197  }
   198  
   199  func ensureEmail(name, val string) error {
   200  	if err := ensureNonEmpty(name, val); err != nil {
   201  		return err
   202  	}
   203  	_, err := mail.ParseAddress(val)
   204  	if err != nil {
   205  		return fmt.Errorf("%v contains invalid email address", name)
   206  	}
   207  	return nil
   208  }