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