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 }