github.com/jenkins-x/test-infra@v0.0.7/prow/config/config.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package config knows how to read and parse config.yaml. 18 // It also implements an agent to read the secrets. 19 package config 20 21 import ( 22 "bytes" 23 "errors" 24 "fmt" 25 "io/ioutil" 26 "net/url" 27 "os" 28 "path/filepath" 29 "regexp" 30 "strings" 31 "text/template" 32 "time" 33 34 "github.com/sirupsen/logrus" 35 "gopkg.in/robfig/cron.v2" 36 "k8s.io/api/core/v1" 37 "k8s.io/apimachinery/pkg/labels" 38 "k8s.io/apimachinery/pkg/util/sets" 39 "k8s.io/apimachinery/pkg/util/validation" 40 "sigs.k8s.io/yaml" 41 42 prowjobv1 "k8s.io/test-infra/prow/apis/prowjobs/v1" 43 "k8s.io/test-infra/prow/config/org" 44 "k8s.io/test-infra/prow/github" 45 "k8s.io/test-infra/prow/kube" 46 "k8s.io/test-infra/prow/pod-utils/decorate" 47 "k8s.io/test-infra/prow/pod-utils/downwardapi" 48 ) 49 50 // Config is a read-only snapshot of the config. 51 type Config struct { 52 JobConfig 53 ProwConfig 54 } 55 56 // JobConfig is config for all prow jobs 57 type JobConfig struct { 58 // Presets apply to all job types. 59 Presets []Preset `json:"presets,omitempty"` 60 // Full repo name (such as "kubernetes/kubernetes") -> list of jobs. 61 Presubmits map[string][]Presubmit `json:"presubmits,omitempty"` 62 Postsubmits map[string][]Postsubmit `json:"postsubmits,omitempty"` 63 64 // Periodics are not associated with any repo. 65 Periodics []Periodic `json:"periodics,omitempty"` 66 } 67 68 // ProwConfig is config for all prow controllers 69 type ProwConfig struct { 70 Tide Tide `json:"tide,omitempty"` 71 Plank Plank `json:"plank,omitempty"` 72 Sinker Sinker `json:"sinker,omitempty"` 73 Deck Deck `json:"deck,omitempty"` 74 BranchProtection BranchProtection `json:"branch-protection,omitempty"` 75 Orgs map[string]org.Config `json:"orgs,omitempty"` 76 Gerrit Gerrit `json:"gerrit,omitempty"` 77 Build Build `json:"build,omitempty"` 78 79 // TODO: Move this out of the main config. 80 JenkinsOperators []JenkinsOperator `json:"jenkins_operators,omitempty"` 81 82 // ProwJobNamespace is the namespace in the cluster that prow 83 // components will use for looking up ProwJobs. The namespace 84 // needs to exist and will not be created by prow. 85 // Defaults to "default". 86 ProwJobNamespace string `json:"prowjob_namespace,omitempty"` 87 // PodNamespace is the namespace in the cluster that prow 88 // components will use for looking up Pods owned by ProwJobs. 89 // The namespace needs to exist and will not be created by prow. 90 // Defaults to "default". 91 PodNamespace string `json:"pod_namespace,omitempty"` 92 93 // LogLevel enables dynamically updating the log level of the 94 // standard logger that is used by all prow components. 95 // 96 // Valid values: 97 // 98 // "debug", "info", "warn", "warning", "error", "fatal", "panic" 99 // 100 // Defaults to "info". 101 LogLevel string `json:"log_level,omitempty"` 102 103 // PushGateway is a prometheus push gateway. 104 PushGateway PushGateway `json:"push_gateway,omitempty"` 105 106 // OwnersDirBlacklist is used to configure which directories to ignore when 107 // searching for OWNERS{,_ALIAS} files in a repo. 108 OwnersDirBlacklist OwnersDirBlacklist `json:"owners_dir_blacklist,omitempty"` 109 110 // Pub/Sub Subscriptions that we want to listen to 111 PubSubSubscriptions PubsubSubscriptions `json:"pubsub_subscriptions,omitempty"` 112 } 113 114 // OwnersDirBlacklist is used to configure which directories to ignore when 115 // searching for OWNERS{,_ALIAS} files in a repo. 116 type OwnersDirBlacklist struct { 117 // Repos configures a directory blacklist per repo (or org) 118 Repos map[string][]string `json:"repos"` 119 // Default configures a default blacklist for repos (or orgs) not 120 // specifically configured 121 Default []string `json:"default"` 122 } 123 124 // PushGateway is a prometheus push gateway. 125 type PushGateway struct { 126 // Endpoint is the location of the prometheus pushgateway 127 // where prow will push metrics to. 128 Endpoint string `json:"endpoint,omitempty"` 129 // IntervalString compiles into Interval at load time. 130 IntervalString string `json:"interval,omitempty"` 131 // Interval specifies how often prow will push metrics 132 // to the pushgateway. Defaults to 1m. 133 Interval time.Duration `json:"-"` 134 } 135 136 // Controller holds configuration applicable to all agent-specific 137 // prow controllers. 138 type Controller struct { 139 // JobURLTemplateString compiles into JobURLTemplate at load time. 140 JobURLTemplateString string `json:"job_url_template,omitempty"` 141 // JobURLTemplate is compiled at load time from JobURLTemplateString. It 142 // will be passed a kube.ProwJob and is used to set the URL for the 143 // "Details" link on GitHub as well as the link from deck. 144 JobURLTemplate *template.Template `json:"-"` 145 146 // ReportTemplateString compiles into ReportTemplate at load time. 147 ReportTemplateString string `json:"report_template,omitempty"` 148 // ReportTemplate is compiled at load time from ReportTemplateString. It 149 // will be passed a kube.ProwJob and can provide an optional blurb below 150 // the test failures comment. 151 ReportTemplate *template.Template `json:"-"` 152 153 // MaxConcurrency is the maximum number of tests running concurrently that 154 // will be allowed by the controller. 0 implies no limit. 155 MaxConcurrency int `json:"max_concurrency,omitempty"` 156 157 // MaxGoroutines is the maximum number of goroutines spawned inside the 158 // controller to handle tests. Defaults to 20. Needs to be a positive 159 // number. 160 MaxGoroutines int `json:"max_goroutines,omitempty"` 161 162 // AllowCancellations enables aborting presubmit jobs for commits that 163 // have been superseded by newer commits in Github pull requests. 164 AllowCancellations bool `json:"allow_cancellations,omitempty"` 165 } 166 167 // Plank is config for the plank controller. 168 type Plank struct { 169 Controller `json:",inline"` 170 // PodPendingTimeoutString compiles into PodPendingTimeout at load time. 171 PodPendingTimeoutString string `json:"pod_pending_timeout,omitempty"` 172 // PodPendingTimeout is after how long the controller will perform a garbage 173 // collection on pending pods. Defaults to one day. 174 PodPendingTimeout time.Duration `json:"-"` 175 // DefaultDecorationConfig are defaults for shared fields for ProwJobs 176 // that request to have their PodSpecs decorated 177 DefaultDecorationConfig *kube.DecorationConfig `json:"default_decoration_config,omitempty"` 178 // JobURLPrefix is the host and path prefix under 179 // which job details will be viewable 180 JobURLPrefix string `json:"job_url_prefix,omitempty"` 181 } 182 183 // Gerrit is config for the gerrit controller. 184 type Gerrit struct { 185 // TickInterval is how often we do a sync with binded gerrit instance 186 TickIntervalString string `json:"tick_interval,omitempty"` 187 TickInterval time.Duration `json:"-"` 188 // RateLimit defines how many changes to query per gerrit API call 189 // default is 5 190 RateLimit int `json:"ratelimit,omitempty"` 191 } 192 193 // JenkinsOperator is config for the jenkins-operator controller. 194 type JenkinsOperator struct { 195 Controller `json:",inline"` 196 // LabelSelectorString compiles into LabelSelector at load time. 197 // If set, this option needs to match --label-selector used by 198 // the desired jenkins-operator. This option is considered 199 // invalid when provided with a single jenkins-operator config. 200 // 201 // For label selector syntax, see below: 202 // https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors 203 LabelSelectorString string `json:"label_selector,omitempty"` 204 // LabelSelector is used so different jenkins-operator replicas 205 // can use their own configuration. 206 LabelSelector labels.Selector `json:"-"` 207 } 208 209 // Sinker is config for the sinker controller. 210 type Sinker struct { 211 // ResyncPeriodString compiles into ResyncPeriod at load time. 212 ResyncPeriodString string `json:"resync_period,omitempty"` 213 // ResyncPeriod is how often the controller will perform a garbage 214 // collection. Defaults to one hour. 215 ResyncPeriod time.Duration `json:"-"` 216 // MaxProwJobAgeString compiles into MaxProwJobAge at load time. 217 MaxProwJobAgeString string `json:"max_prowjob_age,omitempty"` 218 // MaxProwJobAge is how old a ProwJob can be before it is garbage-collected. 219 // Defaults to one week. 220 MaxProwJobAge time.Duration `json:"-"` 221 // MaxPodAgeString compiles into MaxPodAge at load time. 222 MaxPodAgeString string `json:"max_pod_age,omitempty"` 223 // MaxPodAge is how old a Pod can be before it is garbage-collected. 224 // Defaults to one day. 225 MaxPodAge time.Duration `json:"-"` 226 } 227 228 // Spyglass holds config for Spyglass 229 type Spyglass struct { 230 // Viewers is a map of Regexp strings to viewer names that defines which sets 231 // of artifacts need to be consumed by which viewers. The keys are compiled 232 // and stored in RegexCache at load time. 233 Viewers map[string][]string `json:"viewers,omitempty"` 234 // RegexCache is a map of viewer regexp strings to their compiled equivalents. 235 RegexCache map[string]*regexp.Regexp `json:"-"` 236 // SizeLimit is the max size artifact in bytes that Spyglass will attempt to 237 // read in entirety. This will only affect viewers attempting to use 238 // artifact.ReadAll(). To exclude outlier artifacts, set this limit to 239 // expected file size + variance. To include all artifacts with high 240 // probability, use 2*maximum observed artifact size. 241 SizeLimit int64 `json:"size_limit,omitempty"` 242 } 243 244 // Deck holds config for deck. 245 type Deck struct { 246 // Spyglass specifies which viewers will be used for which artifacts when viewing a job in Deck 247 Spyglass Spyglass `json:"spyglass,omitempty"` 248 // TideUpdatePeriodString compiles into TideUpdatePeriod at load time. 249 TideUpdatePeriodString string `json:"tide_update_period,omitempty"` 250 // TideUpdatePeriod specifies how often Deck will fetch status from Tide. Defaults to 10s. 251 TideUpdatePeriod time.Duration `json:"-"` 252 // HiddenRepos is a list of orgs and/or repos that should not be displayed by Deck. 253 HiddenRepos []string `json:"hidden_repos,omitempty"` 254 // ExternalAgentLogs ensures external agents can expose 255 // their logs in prow. 256 ExternalAgentLogs []ExternalAgentLog `json:"external_agent_logs,omitempty"` 257 // Branding of the frontend 258 Branding *Branding `json:"branding,omitempty"` 259 } 260 261 // ExternalAgentLog ensures an external agent like Jenkins can expose 262 // its logs in prow. 263 type ExternalAgentLog struct { 264 // Agent is an external prow agent that supports exposing 265 // logs via deck. 266 Agent string `json:"agent,omitempty"` 267 // SelectorString compiles into Selector at load time. 268 SelectorString string `json:"selector,omitempty"` 269 // Selector can be used in prow deployments where the workload has 270 // been sharded between controllers of the same agent. For more info 271 // see https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors 272 Selector labels.Selector `json:"-"` 273 // URLTemplateString compiles into URLTemplate at load time. 274 URLTemplateString string `json:"url_template,omitempty"` 275 // URLTemplate is compiled at load time from URLTemplateString. It 276 // will be passed a kube.ProwJob and the generated URL should provide 277 // logs for the ProwJob. 278 URLTemplate *template.Template `json:"-"` 279 } 280 281 // Branding holds branding configuration for deck. 282 type Branding struct { 283 // Logo is the location of the logo that will be loaded in deck. 284 Logo string `json:"logo,omitempty"` 285 // Favicon is the location of the favicon that will be loaded in deck. 286 Favicon string `json:"favicon,omitempty"` 287 // BackgroundColor is the color of the background. 288 BackgroundColor string `json:"background_color,omitempty"` 289 // HeaderColor is the color of the header. 290 HeaderColor string `json:"header_color,omitempty"` 291 } 292 293 // Build is config for the knative build controller. 294 type Build struct { 295 // AllowCancellations enables aborting presubmit jobs for commits that 296 // have been superseded by newer commits in Github pull requests. 297 AllowCancellations bool `json:"allow_cancellations,omitempty"` 298 } 299 300 // PubSubSubscriptions maps GCP projects to a list of Topics. 301 type PubsubSubscriptions map[string][]string 302 303 // Load loads and parses the config at path. 304 func Load(prowConfig, jobConfig string) (c *Config, err error) { 305 // we never want config loading to take down the prow components 306 defer func() { 307 if r := recover(); r != nil { 308 c, err = nil, fmt.Errorf("panic loading config: %v", r) 309 } 310 }() 311 c, err = loadConfig(prowConfig, jobConfig) 312 if err != nil { 313 return nil, err 314 } 315 if err := c.finalizeJobConfig(); err != nil { 316 return nil, err 317 } 318 if err := c.validateComponentConfig(); err != nil { 319 return nil, err 320 } 321 if err := c.validateJobConfig(); err != nil { 322 return nil, err 323 } 324 return c, nil 325 } 326 327 // loadConfig loads one or multiple config files and returns a config object. 328 func loadConfig(prowConfig, jobConfig string) (*Config, error) { 329 stat, err := os.Stat(prowConfig) 330 if err != nil { 331 return nil, err 332 } 333 334 if stat.IsDir() { 335 return nil, fmt.Errorf("prowConfig cannot be a dir - %s", prowConfig) 336 } 337 338 var nc Config 339 if err := yamlToConfig(prowConfig, &nc); err != nil { 340 return nil, err 341 } 342 if err := parseProwConfig(&nc); err != nil { 343 return nil, err 344 } 345 346 // TODO(krzyzacy): temporary allow empty jobconfig 347 // also temporary allow job config in prow config 348 if jobConfig == "" { 349 return &nc, nil 350 } 351 352 stat, err = os.Stat(jobConfig) 353 if err != nil { 354 return nil, err 355 } 356 357 if !stat.IsDir() { 358 // still support a single file 359 var jc JobConfig 360 if err := yamlToConfig(jobConfig, &jc); err != nil { 361 return nil, err 362 } 363 if err := nc.mergeJobConfig(jc); err != nil { 364 return nil, err 365 } 366 return &nc, nil 367 } 368 369 // we need to ensure all config files have unique basenames, 370 // since updateconfig plugin will use basename as a key in the configmap 371 uniqueBasenames := sets.String{} 372 373 err = filepath.Walk(jobConfig, func(path string, info os.FileInfo, err error) error { 374 if err != nil { 375 logrus.WithError(err).Errorf("walking path %q.", path) 376 // bad file should not stop us from parsing the directory 377 return nil 378 } 379 380 if strings.HasPrefix(info.Name(), "..") { 381 // kubernetes volumes also include files we 382 // should not look be looking into for keys 383 if info.IsDir() { 384 return filepath.SkipDir 385 } 386 return nil 387 } 388 389 if filepath.Ext(path) != ".yaml" && filepath.Ext(path) != ".yml" { 390 return nil 391 } 392 393 if info.IsDir() { 394 return nil 395 } 396 397 base := filepath.Base(path) 398 if uniqueBasenames.Has(base) { 399 return fmt.Errorf("duplicated basename is not allowed: %s", base) 400 } 401 uniqueBasenames.Insert(base) 402 403 var subConfig JobConfig 404 if err := yamlToConfig(path, &subConfig); err != nil { 405 return err 406 } 407 return nc.mergeJobConfig(subConfig) 408 }) 409 410 if err != nil { 411 return nil, err 412 } 413 414 return &nc, nil 415 } 416 417 // LoadSecrets loads multiple paths of secrets and add them in a map. 418 func LoadSecrets(paths []string) (map[string][]byte, error) { 419 secretsMap := make(map[string][]byte, len(paths)) 420 421 for _, path := range paths { 422 secretValue, err := LoadSingleSecret(path) 423 if err != nil { 424 return nil, err 425 } 426 secretsMap[path] = secretValue 427 } 428 return secretsMap, nil 429 } 430 431 // LoadSingleSecret reads and returns the value of a single file. 432 func LoadSingleSecret(path string) ([]byte, error) { 433 b, err := ioutil.ReadFile(path) 434 if err != nil { 435 return nil, fmt.Errorf("error reading %s: %v", path, err) 436 } 437 return bytes.TrimSpace(b), nil 438 } 439 440 // yamlToConfig converts a yaml file into a Config object 441 func yamlToConfig(path string, nc interface{}) error { 442 b, err := ioutil.ReadFile(path) 443 if err != nil { 444 return fmt.Errorf("error reading %s: %v", path, err) 445 } 446 if err := yaml.Unmarshal(b, nc); err != nil { 447 return fmt.Errorf("error unmarshaling %s: %v", path, err) 448 } 449 var jc *JobConfig 450 switch v := nc.(type) { 451 case *JobConfig: 452 jc = v 453 case *Config: 454 jc = &v.JobConfig 455 } 456 for rep := range jc.Presubmits { 457 var fix func(*Presubmit) 458 fix = func(job *Presubmit) { 459 job.SourcePath = path 460 for i := range job.RunAfterSuccess { 461 fix(&job.RunAfterSuccess[i]) 462 } 463 } 464 for i := range jc.Presubmits[rep] { 465 fix(&jc.Presubmits[rep][i]) 466 } 467 } 468 for rep := range jc.Postsubmits { 469 var fix func(*Postsubmit) 470 fix = func(job *Postsubmit) { 471 job.SourcePath = path 472 for i := range job.RunAfterSuccess { 473 fix(&job.RunAfterSuccess[i]) 474 } 475 } 476 for i := range jc.Postsubmits[rep] { 477 fix(&jc.Postsubmits[rep][i]) 478 } 479 } 480 481 var fix func(*Periodic) 482 fix = func(job *Periodic) { 483 job.SourcePath = path 484 for i := range job.RunAfterSuccess { 485 fix(&job.RunAfterSuccess[i]) 486 } 487 } 488 for i := range jc.Periodics { 489 fix(&jc.Periodics[i]) 490 } 491 return nil 492 } 493 494 // mergeConfig merges two JobConfig together 495 // It will try to merge: 496 // - Presubmits 497 // - Postsubmits 498 // - Periodics 499 // - PodPresets 500 func (c *Config) mergeJobConfig(jc JobConfig) error { 501 // Merge everything 502 // *** Presets *** 503 c.Presets = append(c.Presets, jc.Presets...) 504 505 // validate no duplicated presets 506 validLabels := map[string]string{} 507 for _, preset := range c.Presets { 508 for label, val := range preset.Labels { 509 if _, ok := validLabels[label]; ok { 510 return fmt.Errorf("duplicated preset label : %s", label) 511 } 512 validLabels[label] = val 513 } 514 } 515 516 // *** Periodics *** 517 c.Periodics = append(c.Periodics, jc.Periodics...) 518 519 // *** Presubmits *** 520 if c.Presubmits == nil { 521 c.Presubmits = make(map[string][]Presubmit) 522 } 523 for repo, jobs := range jc.Presubmits { 524 c.Presubmits[repo] = append(c.Presubmits[repo], jobs...) 525 } 526 527 // *** Postsubmits *** 528 if c.Postsubmits == nil { 529 c.Postsubmits = make(map[string][]Postsubmit) 530 } 531 for repo, jobs := range jc.Postsubmits { 532 c.Postsubmits[repo] = append(c.Postsubmits[repo], jobs...) 533 } 534 535 return nil 536 } 537 538 func setPresubmitDecorationDefaults(c *Config, ps *Presubmit) { 539 if ps.Decorate { 540 ps.DecorationConfig = ps.DecorationConfig.ApplyDefault(c.Plank.DefaultDecorationConfig) 541 } 542 543 for i := range ps.RunAfterSuccess { 544 setPresubmitDecorationDefaults(c, &ps.RunAfterSuccess[i]) 545 } 546 } 547 548 func setPostsubmitDecorationDefaults(c *Config, ps *Postsubmit) { 549 if ps.Decorate { 550 ps.DecorationConfig = ps.DecorationConfig.ApplyDefault(c.Plank.DefaultDecorationConfig) 551 } 552 553 for i := range ps.RunAfterSuccess { 554 setPostsubmitDecorationDefaults(c, &ps.RunAfterSuccess[i]) 555 } 556 } 557 558 func setPeriodicDecorationDefaults(c *Config, ps *Periodic) { 559 if ps.Decorate { 560 ps.DecorationConfig = ps.DecorationConfig.ApplyDefault(c.Plank.DefaultDecorationConfig) 561 } 562 563 for i := range ps.RunAfterSuccess { 564 setPeriodicDecorationDefaults(c, &ps.RunAfterSuccess[i]) 565 } 566 } 567 568 // finalizeJobConfig mutates and fixes entries for jobspecs 569 func (c *Config) finalizeJobConfig() error { 570 if c.decorationRequested() { 571 if c.Plank.DefaultDecorationConfig == nil { 572 return errors.New("no default decoration config provided for plank") 573 } 574 if c.Plank.DefaultDecorationConfig.UtilityImages == nil { 575 return errors.New("no default decoration image pull specs provided for plank") 576 } 577 if c.Plank.DefaultDecorationConfig.GCSConfiguration == nil { 578 return errors.New("no default GCS decoration config provided for plank") 579 } 580 if c.Plank.DefaultDecorationConfig.GCSCredentialsSecret == "" { 581 return errors.New("no default GCS credentials secret provided for plank") 582 } 583 584 for _, vs := range c.Presubmits { 585 for i := range vs { 586 setPresubmitDecorationDefaults(c, &vs[i]) 587 } 588 } 589 590 for _, js := range c.Postsubmits { 591 for i := range js { 592 setPostsubmitDecorationDefaults(c, &js[i]) 593 } 594 } 595 596 for i := range c.Periodics { 597 setPeriodicDecorationDefaults(c, &c.Periodics[i]) 598 } 599 } 600 601 // Ensure that regexes are valid and set defaults. 602 for _, vs := range c.Presubmits { 603 c.defaultPresubmitFields(vs) 604 if err := SetPresubmitRegexes(vs); err != nil { 605 return fmt.Errorf("could not set regex: %v", err) 606 } 607 } 608 for _, js := range c.Postsubmits { 609 c.defaultPostsubmitFields(js) 610 if err := SetPostsubmitRegexes(js); err != nil { 611 return fmt.Errorf("could not set regex: %v", err) 612 } 613 } 614 615 c.defaultPeriodicFields(c.Periodics) 616 617 for _, v := range c.AllPresubmits(nil) { 618 if err := resolvePresets(v.Name, v.Labels, v.Spec, c.Presets); err != nil { 619 return err 620 } 621 } 622 623 for _, v := range c.AllPostsubmits(nil) { 624 if err := resolvePresets(v.Name, v.Labels, v.Spec, c.Presets); err != nil { 625 return err 626 } 627 } 628 629 for _, v := range c.AllPeriodics() { 630 if err := resolvePresets(v.Name, v.Labels, v.Spec, c.Presets); err != nil { 631 return err 632 } 633 } 634 635 return nil 636 } 637 638 // validateComponentConfig validates the infrastructure component configuration 639 func (c *Config) validateComponentConfig() error { 640 if _, err := url.Parse(c.Plank.JobURLPrefix); c.Plank.JobURLPrefix != "" && err != nil { 641 return fmt.Errorf("plank declares an invalid job URL prefix %q: %v", c.Plank.JobURLPrefix, err) 642 } 643 return nil 644 } 645 646 var jobNameRegex = regexp.MustCompile(`^[A-Za-z0-9-._]+$`) 647 648 func validateJobBase(v JobBase, jobType kube.ProwJobType, podNamespace string) error { 649 if !jobNameRegex.MatchString(v.Name) { 650 return fmt.Errorf("name: must match regex %q", jobNameRegex.String()) 651 } 652 // Ensure max_concurrency is non-negative. 653 if v.MaxConcurrency < 0 { 654 return fmt.Errorf("max_concurrency: %d must be a non-negative number", v.MaxConcurrency) 655 } 656 if err := validateAgent(v, podNamespace); err != nil { 657 return err 658 } 659 if err := validatePodSpec(jobType, v.Spec); err != nil { 660 return err 661 } 662 if err := validateLabels(v.Labels); err != nil { 663 return err 664 } 665 if v.Spec == nil || len(v.Spec.Containers) == 0 { 666 return nil // knative-build and jenkins jobs have no spec 667 } 668 return validateDecoration(v.Spec.Containers[0], v.DecorationConfig) 669 } 670 671 // validateJobConfig validates if all the jobspecs/presets are valid 672 // if you are mutating the jobs, please add it to finalizeJobConfig above 673 func (c *Config) validateJobConfig() error { 674 type orgRepoJobName struct { 675 orgRepo, jobName string 676 } 677 678 // Validate presubmits. 679 // Checking that no duplicate job in prow config exists on the same org / repo / branch. 680 validPresubmits := map[orgRepoJobName][]Presubmit{} 681 for repo, jobs := range c.Presubmits { 682 for _, job := range listPresubmits(jobs) { 683 repoJobName := orgRepoJobName{repo, job.Name} 684 for _, existingJob := range validPresubmits[repoJobName] { 685 if existingJob.Brancher.Intersects(job.Brancher) { 686 return fmt.Errorf("duplicated presubmit job: %s", job.Name) 687 } 688 } 689 validPresubmits[repoJobName] = append(validPresubmits[repoJobName], job) 690 } 691 } 692 693 for _, v := range c.AllPresubmits(nil) { 694 if err := validateJobBase(v.JobBase, prowjobv1.PresubmitJob, c.PodNamespace); err != nil { 695 return fmt.Errorf("invalid presubmit job %s: %v", v.Name, err) 696 } 697 if err := validateTriggering(v); err != nil { 698 return err 699 } 700 } 701 702 // Validate postsubmits. 703 // Checking that no duplicate job in prow config exists on the same org / repo / branch. 704 validPostsubmits := map[orgRepoJobName][]Postsubmit{} 705 for repo, jobs := range c.Postsubmits { 706 for _, job := range listPostsubmits(jobs) { 707 repoJobName := orgRepoJobName{repo, job.Name} 708 for _, existingJob := range validPostsubmits[repoJobName] { 709 if existingJob.Brancher.Intersects(job.Brancher) { 710 return fmt.Errorf("duplicated postsubmit job: %s", job.Name) 711 } 712 } 713 validPostsubmits[repoJobName] = append(validPostsubmits[repoJobName], job) 714 } 715 } 716 717 for _, j := range c.AllPostsubmits(nil) { 718 if err := validateJobBase(j.JobBase, prowjobv1.PostsubmitJob, c.PodNamespace); err != nil { 719 return fmt.Errorf("invalid postsubmit job %s: %v", j.Name, err) 720 } 721 } 722 723 // validate no duplicated periodics 724 validPeriodics := sets.NewString() 725 // Ensure that the periodic durations are valid and specs exist. 726 for _, p := range c.AllPeriodics() { 727 if validPeriodics.Has(p.Name) { 728 return fmt.Errorf("duplicated periodic job : %s", p.Name) 729 } 730 validPeriodics.Insert(p.Name) 731 if err := validateJobBase(p.JobBase, prowjobv1.PeriodicJob, c.PodNamespace); err != nil { 732 return fmt.Errorf("invalid periodic job %s: %v", p.Name, err) 733 } 734 } 735 // Set the interval on the periodic jobs. It doesn't make sense to do this 736 // for child jobs. 737 for j, p := range c.Periodics { 738 if p.Cron != "" && p.Interval != "" { 739 return fmt.Errorf("cron and interval cannot be both set in periodic %s", p.Name) 740 } else if p.Cron == "" && p.Interval == "" { 741 return fmt.Errorf("cron and interval cannot be both empty in periodic %s", p.Name) 742 } else if p.Cron != "" { 743 if _, err := cron.Parse(p.Cron); err != nil { 744 return fmt.Errorf("invalid cron string %s in periodic %s: %v", p.Cron, p.Name, err) 745 } 746 } else { 747 d, err := time.ParseDuration(c.Periodics[j].Interval) 748 if err != nil { 749 return fmt.Errorf("cannot parse duration for %s: %v", c.Periodics[j].Name, err) 750 } 751 c.Periodics[j].interval = d 752 } 753 } 754 755 return nil 756 } 757 758 func parseProwConfig(c *Config) error { 759 if err := ValidateController(&c.Plank.Controller); err != nil { 760 return fmt.Errorf("validating plank config: %v", err) 761 } 762 763 if c.Plank.PodPendingTimeoutString == "" { 764 c.Plank.PodPendingTimeout = 24 * time.Hour 765 } else { 766 podPendingTimeout, err := time.ParseDuration(c.Plank.PodPendingTimeoutString) 767 if err != nil { 768 return fmt.Errorf("cannot parse duration for plank.pod_pending_timeout: %v", err) 769 } 770 c.Plank.PodPendingTimeout = podPendingTimeout 771 } 772 773 if c.Gerrit.TickIntervalString == "" { 774 c.Gerrit.TickInterval = time.Minute 775 } else { 776 tickInterval, err := time.ParseDuration(c.Gerrit.TickIntervalString) 777 if err != nil { 778 return fmt.Errorf("cannot parse duration for c.gerrit.tick_interval: %v", err) 779 } 780 c.Gerrit.TickInterval = tickInterval 781 } 782 783 if c.Gerrit.RateLimit == 0 { 784 c.Gerrit.RateLimit = 5 785 } 786 787 for i := range c.JenkinsOperators { 788 if err := ValidateController(&c.JenkinsOperators[i].Controller); err != nil { 789 return fmt.Errorf("validating jenkins_operators config: %v", err) 790 } 791 sel, err := labels.Parse(c.JenkinsOperators[i].LabelSelectorString) 792 if err != nil { 793 return fmt.Errorf("invalid jenkins_operators.label_selector option: %v", err) 794 } 795 c.JenkinsOperators[i].LabelSelector = sel 796 // TODO: Invalidate overlapping selectors more 797 if len(c.JenkinsOperators) > 1 && c.JenkinsOperators[i].LabelSelectorString == "" { 798 return errors.New("selector overlap: cannot use an empty label_selector with multiple selectors") 799 } 800 if len(c.JenkinsOperators) == 1 && c.JenkinsOperators[0].LabelSelectorString != "" { 801 return errors.New("label_selector is invalid when used for a single jenkins-operator") 802 } 803 } 804 805 for i, agentToTmpl := range c.Deck.ExternalAgentLogs { 806 urlTemplate, err := template.New(agentToTmpl.Agent).Parse(agentToTmpl.URLTemplateString) 807 if err != nil { 808 return fmt.Errorf("parsing template for agent %q: %v", agentToTmpl.Agent, err) 809 } 810 c.Deck.ExternalAgentLogs[i].URLTemplate = urlTemplate 811 // we need to validate selectors used by deck since these are not 812 // sent to the api server. 813 s, err := labels.Parse(c.Deck.ExternalAgentLogs[i].SelectorString) 814 if err != nil { 815 return fmt.Errorf("error parsing selector %q: %v", c.Deck.ExternalAgentLogs[i].SelectorString, err) 816 } 817 c.Deck.ExternalAgentLogs[i].Selector = s 818 } 819 820 if c.Deck.TideUpdatePeriodString == "" { 821 c.Deck.TideUpdatePeriod = time.Second * 10 822 } else { 823 period, err := time.ParseDuration(c.Deck.TideUpdatePeriodString) 824 if err != nil { 825 return fmt.Errorf("cannot parse duration for deck.tide_update_period: %v", err) 826 } 827 c.Deck.TideUpdatePeriod = period 828 } 829 830 if c.Deck.Spyglass.SizeLimit == 0 { 831 c.Deck.Spyglass.SizeLimit = 100e6 832 } else if c.Deck.Spyglass.SizeLimit <= 0 { 833 return fmt.Errorf("invalid value for deck.spyglass.size_limit, must be >=0") 834 } 835 836 c.Deck.Spyglass.RegexCache = make(map[string]*regexp.Regexp) 837 for k := range c.Deck.Spyglass.Viewers { 838 r, err := regexp.Compile(k) 839 if err != nil { 840 return fmt.Errorf("cannot compile regexp %s, err: %v", k, err) 841 } 842 c.Deck.Spyglass.RegexCache[k] = r 843 } 844 845 // Map old viewer names to the new ones for backwards compatibility. 846 // TODO(Katharine, #10274): remove this, eventually. 847 oldViewers := map[string]string{ 848 "build-log-viewer": "buildlog", 849 "metadata-viewer": "metadata", 850 "junit-viewer": "junit", 851 } 852 853 for re, viewers := range c.Deck.Spyglass.Viewers { 854 for i, v := range viewers { 855 if rename, ok := oldViewers[v]; ok { 856 c.Deck.Spyglass.Viewers[re][i] = rename 857 } 858 } 859 } 860 861 if c.PushGateway.IntervalString == "" { 862 c.PushGateway.Interval = time.Minute 863 } else { 864 interval, err := time.ParseDuration(c.PushGateway.IntervalString) 865 if err != nil { 866 return fmt.Errorf("cannot parse duration for push_gateway.interval: %v", err) 867 } 868 c.PushGateway.Interval = interval 869 } 870 871 if c.Sinker.ResyncPeriodString == "" { 872 c.Sinker.ResyncPeriod = time.Hour 873 } else { 874 resyncPeriod, err := time.ParseDuration(c.Sinker.ResyncPeriodString) 875 if err != nil { 876 return fmt.Errorf("cannot parse duration for sinker.resync_period: %v", err) 877 } 878 c.Sinker.ResyncPeriod = resyncPeriod 879 } 880 881 if c.Sinker.MaxProwJobAgeString == "" { 882 c.Sinker.MaxProwJobAge = 7 * 24 * time.Hour 883 } else { 884 maxProwJobAge, err := time.ParseDuration(c.Sinker.MaxProwJobAgeString) 885 if err != nil { 886 return fmt.Errorf("cannot parse duration for max_prowjob_age: %v", err) 887 } 888 c.Sinker.MaxProwJobAge = maxProwJobAge 889 } 890 891 if c.Sinker.MaxPodAgeString == "" { 892 c.Sinker.MaxPodAge = 24 * time.Hour 893 } else { 894 maxPodAge, err := time.ParseDuration(c.Sinker.MaxPodAgeString) 895 if err != nil { 896 return fmt.Errorf("cannot parse duration for max_pod_age: %v", err) 897 } 898 c.Sinker.MaxPodAge = maxPodAge 899 } 900 901 if c.Tide.SyncPeriodString == "" { 902 c.Tide.SyncPeriod = time.Minute 903 } else { 904 period, err := time.ParseDuration(c.Tide.SyncPeriodString) 905 if err != nil { 906 return fmt.Errorf("cannot parse duration for tide.sync_period: %v", err) 907 } 908 c.Tide.SyncPeriod = period 909 } 910 if c.Tide.StatusUpdatePeriodString == "" { 911 c.Tide.StatusUpdatePeriod = c.Tide.SyncPeriod 912 } else { 913 period, err := time.ParseDuration(c.Tide.StatusUpdatePeriodString) 914 if err != nil { 915 return fmt.Errorf("cannot parse duration for tide.status_update_period: %v", err) 916 } 917 c.Tide.StatusUpdatePeriod = period 918 } 919 920 if c.Tide.MaxGoroutines == 0 { 921 c.Tide.MaxGoroutines = 20 922 } 923 if c.Tide.MaxGoroutines <= 0 { 924 return fmt.Errorf("tide has invalid max_goroutines (%d), it needs to be a positive number", c.Tide.MaxGoroutines) 925 } 926 927 for name, method := range c.Tide.MergeType { 928 if method != github.MergeMerge && 929 method != github.MergeRebase && 930 method != github.MergeSquash { 931 return fmt.Errorf("merge type %q for %s is not a valid type", method, name) 932 } 933 } 934 935 for i, tq := range c.Tide.Queries { 936 if err := tq.Validate(); err != nil { 937 return fmt.Errorf("tide query (index %d) is invalid: %v", i, err) 938 } 939 } 940 941 if c.ProwJobNamespace == "" { 942 c.ProwJobNamespace = "default" 943 } 944 if c.PodNamespace == "" { 945 c.PodNamespace = "default" 946 } 947 948 if c.LogLevel == "" { 949 c.LogLevel = "info" 950 } 951 lvl, err := logrus.ParseLevel(c.LogLevel) 952 if err != nil { 953 return err 954 } 955 logrus.SetLevel(lvl) 956 957 return nil 958 } 959 960 func (c *JobConfig) decorationRequested() bool { 961 for _, vs := range c.Presubmits { 962 for i := range vs { 963 if vs[i].Decorate { 964 return true 965 } 966 } 967 } 968 969 for _, js := range c.Postsubmits { 970 for i := range js { 971 if js[i].Decorate { 972 return true 973 } 974 } 975 } 976 977 for i := range c.Periodics { 978 if c.Periodics[i].Decorate { 979 return true 980 } 981 } 982 983 return false 984 } 985 986 func validateLabels(labels map[string]string) error { 987 for label, value := range labels { 988 for _, prowLabel := range decorate.Labels() { 989 if label == prowLabel { 990 return fmt.Errorf("label %s is reserved for decoration", label) 991 } 992 } 993 if errs := validation.IsQualifiedName(label); len(errs) != 0 { 994 return fmt.Errorf("invalid label %s: %v", label, errs) 995 } 996 if errs := validation.IsValidLabelValue(labels[label]); len(errs) != 0 { 997 return fmt.Errorf("label %s has invalid value %s: %v", label, value, errs) 998 } 999 } 1000 return nil 1001 } 1002 1003 func validateAgent(v JobBase, podNamespace string) error { 1004 k := string(prowjobv1.KubernetesAgent) 1005 b := string(prowjobv1.KnativeBuildAgent) 1006 j := string(prowjobv1.JenkinsAgent) 1007 agents := sets.NewString(k, b, j) 1008 agent := v.Agent 1009 switch { 1010 case !agents.Has(agent): 1011 return fmt.Errorf("agent must be one of %s (found %q)", strings.Join(agents.List(), ", "), agent) 1012 case v.Spec != nil && agent != k: 1013 return fmt.Errorf("job specs require agent: %s (found %q)", k, agent) 1014 case agent == k && v.Spec == nil: 1015 return errors.New("kubernetes jobs require a spec") 1016 case v.BuildSpec != nil && agent != b: 1017 return fmt.Errorf("job build_specs require agent: %s (found %q)", b, agent) 1018 case agent == b && v.BuildSpec == nil: 1019 return errors.New("knative-build jobs require a build_spec") 1020 case v.DecorationConfig != nil && agent != k && agent != b: 1021 // TODO(fejta): only source decoration supported... 1022 return fmt.Errorf("decoration requires agent: %s or %s (found %q)", k, b, agent) 1023 case v.ErrorOnEviction && agent != k: 1024 return fmt.Errorf("error_on_eviction only applies to agent: %s (found %q)", k, agent) 1025 case v.Namespace == nil || *v.Namespace == "": 1026 return fmt.Errorf("failed to default namespace") 1027 case *v.Namespace != podNamespace && agent != b: 1028 // TODO(fejta): update plank to allow this (depends on client change) 1029 return fmt.Errorf("namespace customization requires agent: %s (found %q)", b, agent) 1030 } 1031 return nil 1032 } 1033 1034 func validateDecoration(container v1.Container, config *kube.DecorationConfig) error { 1035 if config == nil { 1036 return nil 1037 } 1038 1039 if err := config.Validate(); err != nil { 1040 return fmt.Errorf("invalid decoration config: %v", err) 1041 } 1042 var args []string 1043 args = append(append(args, container.Command...), container.Args...) 1044 if len(args) == 0 || args[0] == "" { 1045 return errors.New("decorated job containers must specify command and/or args") 1046 } 1047 return nil 1048 } 1049 1050 func resolvePresets(name string, labels map[string]string, spec *v1.PodSpec, presets []Preset) error { 1051 for _, preset := range presets { 1052 if err := mergePreset(preset, labels, spec); err != nil { 1053 return fmt.Errorf("job %s failed to merge presets: %v", name, err) 1054 } 1055 } 1056 1057 return nil 1058 } 1059 1060 func validatePodSpec(jobType kube.ProwJobType, spec *v1.PodSpec) error { 1061 if spec == nil { 1062 return nil 1063 } 1064 1065 if len(spec.InitContainers) != 0 { 1066 return errors.New("pod spec may not use init containers") 1067 } 1068 1069 if n := len(spec.Containers); n != 1 { 1070 return fmt.Errorf("pod spec must specify exactly 1 container, found: %d", n) 1071 } 1072 1073 for _, env := range spec.Containers[0].Env { 1074 for _, prowEnv := range downwardapi.EnvForType(jobType) { 1075 if env.Name == prowEnv { 1076 // TODO(fejta): consider allowing this 1077 return fmt.Errorf("env %s is reserved", env.Name) 1078 } 1079 } 1080 } 1081 1082 for _, mount := range spec.Containers[0].VolumeMounts { 1083 for _, prowMount := range decorate.VolumeMounts() { 1084 if mount.Name == prowMount { 1085 return fmt.Errorf("volumeMount name %s is reserved for decoration", prowMount) 1086 } 1087 } 1088 for _, prowMountPath := range decorate.VolumeMountPaths() { 1089 if strings.HasPrefix(mount.MountPath, prowMountPath) || strings.HasPrefix(prowMountPath, mount.MountPath) { 1090 return fmt.Errorf("mount %s at %s conflicts with decoration mount at %s", mount.Name, mount.MountPath, prowMountPath) 1091 } 1092 } 1093 } 1094 1095 for _, volume := range spec.Volumes { 1096 for _, prowVolume := range decorate.VolumeMounts() { 1097 if volume.Name == prowVolume { 1098 return fmt.Errorf("volume %s is a reserved for decoration", volume.Name) 1099 } 1100 } 1101 } 1102 1103 return nil 1104 } 1105 1106 func validateTriggering(job Presubmit) error { 1107 if job.AlwaysRun && job.RunIfChanged != "" { 1108 return fmt.Errorf("job %s is set to always run but also declares run_if_changed targets, which are mutually exclusive", job.Name) 1109 } 1110 1111 if !job.SkipReport && job.Context == "" { 1112 return fmt.Errorf("job %s is set to report but has no context configured", job.Name) 1113 } 1114 1115 return nil 1116 } 1117 1118 // ValidateController validates the provided controller config. 1119 func ValidateController(c *Controller) error { 1120 urlTmpl, err := template.New("JobURL").Parse(c.JobURLTemplateString) 1121 if err != nil { 1122 return fmt.Errorf("parsing template: %v", err) 1123 } 1124 c.JobURLTemplate = urlTmpl 1125 1126 reportTmpl, err := template.New("Report").Parse(c.ReportTemplateString) 1127 if err != nil { 1128 return fmt.Errorf("parsing template: %v", err) 1129 } 1130 c.ReportTemplate = reportTmpl 1131 if c.MaxConcurrency < 0 { 1132 return fmt.Errorf("controller has invalid max_concurrency (%d), it needs to be a non-negative number", c.MaxConcurrency) 1133 } 1134 if c.MaxGoroutines == 0 { 1135 c.MaxGoroutines = 20 1136 } 1137 if c.MaxGoroutines <= 0 { 1138 return fmt.Errorf("controller has invalid max_goroutines (%d), it needs to be a positive number", c.MaxGoroutines) 1139 } 1140 return nil 1141 } 1142 1143 // DefaultTriggerFor returns the default regexp string used to match comments 1144 // that should trigger the job with this name. 1145 func DefaultTriggerFor(name string) string { 1146 return fmt.Sprintf(`(?m)^/test( | .* )%s,?($|\s.*)`, name) 1147 } 1148 1149 // DefaultRerunCommandFor returns the default rerun command for the job with 1150 // this name. 1151 func DefaultRerunCommandFor(name string) string { 1152 return fmt.Sprintf("/test %s", name) 1153 } 1154 1155 // defaultJobBase configures common parameters, currently Agent and Namespace. 1156 func (c *ProwConfig) defaultJobBase(base *JobBase) { 1157 if base.Agent == "" { // Use kubernetes by default 1158 base.Agent = string(kube.KubernetesAgent) 1159 } 1160 if base.Namespace == nil || *base.Namespace == "" { 1161 s := c.PodNamespace 1162 base.Namespace = &s 1163 } 1164 if base.Cluster == "" { 1165 base.Cluster = kube.DefaultClusterAlias 1166 } 1167 } 1168 1169 func (c *ProwConfig) defaultPresubmitFields(js []Presubmit) { 1170 for i := range js { 1171 c.defaultJobBase(&js[i].JobBase) 1172 if js[i].Context == "" { 1173 js[i].Context = js[i].Name 1174 } 1175 // Default the values of Trigger and RerunCommand if both fields are 1176 // specified. Otherwise let validation fail as both or neither should have 1177 // been specified. 1178 if js[i].Trigger == "" && js[i].RerunCommand == "" { 1179 js[i].Trigger = DefaultTriggerFor(js[i].Name) 1180 js[i].RerunCommand = DefaultRerunCommandFor(js[i].Name) 1181 } 1182 c.defaultPresubmitFields(js[i].RunAfterSuccess) 1183 } 1184 } 1185 1186 func (c *ProwConfig) defaultPostsubmitFields(js []Postsubmit) { 1187 for i := range js { 1188 c.defaultJobBase(&js[i].JobBase) 1189 c.defaultPostsubmitFields(js[i].RunAfterSuccess) 1190 } 1191 } 1192 1193 func (c *ProwConfig) defaultPeriodicFields(js []Periodic) { 1194 for i := range js { 1195 c.defaultJobBase(&js[i].JobBase) 1196 c.defaultPeriodicFields(js[i].RunAfterSuccess) 1197 } 1198 } 1199 1200 // SetPresubmitRegexes compiles and validates all the regular expressions for 1201 // the provided presubmits. 1202 func SetPresubmitRegexes(js []Presubmit) error { 1203 for i, j := range js { 1204 if re, err := regexp.Compile(j.Trigger); err == nil { 1205 js[i].re = re 1206 } else { 1207 return fmt.Errorf("could not compile trigger regex for %s: %v", j.Name, err) 1208 } 1209 if !js[i].re.MatchString(j.RerunCommand) { 1210 return fmt.Errorf("for job %s, rerun command \"%s\" does not match trigger \"%s\"", j.Name, j.RerunCommand, j.Trigger) 1211 } 1212 b, err := setBrancherRegexes(j.Brancher) 1213 if err != nil { 1214 return fmt.Errorf("could not set branch regexes for %s: %v", j.Name, err) 1215 } 1216 js[i].Brancher = b 1217 1218 c, err := setChangeRegexes(j.RegexpChangeMatcher) 1219 if err != nil { 1220 return fmt.Errorf("could not set change regexes for %s: %v", j.Name, err) 1221 } 1222 js[i].RegexpChangeMatcher = c 1223 1224 if err := SetPresubmitRegexes(j.RunAfterSuccess); err != nil { 1225 return err 1226 } 1227 } 1228 return nil 1229 } 1230 1231 // setBrancherRegexes compiles and validates all the regular expressions for 1232 // the provided branch specifiers. 1233 func setBrancherRegexes(br Brancher) (Brancher, error) { 1234 if len(br.Branches) > 0 { 1235 if re, err := regexp.Compile(strings.Join(br.Branches, `|`)); err == nil { 1236 br.re = re 1237 } else { 1238 return br, fmt.Errorf("could not compile positive branch regex: %v", err) 1239 } 1240 } 1241 if len(br.SkipBranches) > 0 { 1242 if re, err := regexp.Compile(strings.Join(br.SkipBranches, `|`)); err == nil { 1243 br.reSkip = re 1244 } else { 1245 return br, fmt.Errorf("could not compile negative branch regex: %v", err) 1246 } 1247 } 1248 return br, nil 1249 } 1250 1251 func setChangeRegexes(cm RegexpChangeMatcher) (RegexpChangeMatcher, error) { 1252 if cm.RunIfChanged != "" { 1253 re, err := regexp.Compile(cm.RunIfChanged) 1254 if err != nil { 1255 return cm, fmt.Errorf("could not compile run_if_changed regex: %v", err) 1256 } 1257 cm.reChanges = re 1258 } 1259 return cm, nil 1260 } 1261 1262 // SetPostsubmitRegexes compiles and validates all the regular expressions for 1263 // the provided postsubmits. 1264 func SetPostsubmitRegexes(ps []Postsubmit) error { 1265 for i, j := range ps { 1266 b, err := setBrancherRegexes(j.Brancher) 1267 if err != nil { 1268 return fmt.Errorf("could not set branch regexes for %s: %v", j.Name, err) 1269 } 1270 ps[i].Brancher = b 1271 c, err := setChangeRegexes(j.RegexpChangeMatcher) 1272 if err != nil { 1273 return fmt.Errorf("could not set change regexes for %s: %v", j.Name, err) 1274 } 1275 ps[i].RegexpChangeMatcher = c 1276 if err := SetPostsubmitRegexes(j.RunAfterSuccess); err != nil { 1277 return err 1278 } 1279 } 1280 return nil 1281 }