github.com/yrj2011/jx-test-infra@v0.0.0-20190529031832-7a2065ee98eb/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 "os" 27 "path/filepath" 28 "regexp" 29 "strings" 30 "text/template" 31 "time" 32 33 "github.com/ghodss/yaml" 34 "github.com/sirupsen/logrus" 35 cron "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 40 "k8s.io/test-infra/prow/config/org" 41 "k8s.io/test-infra/prow/github" 42 "k8s.io/test-infra/prow/kube" 43 "k8s.io/test-infra/prow/pod-utils/decorate" 44 "k8s.io/test-infra/prow/pod-utils/downwardapi" 45 ) 46 47 // Config is a read-only snapshot of the config. 48 type Config struct { 49 JobConfig 50 ProwConfig 51 } 52 53 // JobConfig is config for all prow jobs 54 type JobConfig struct { 55 // Presets apply to all job types. 56 Presets []Preset `json:"presets,omitempty"` 57 // Full repo name (such as "kubernetes/kubernetes") -> list of jobs. 58 Presubmits map[string][]Presubmit `json:"presubmits,omitempty"` 59 Postsubmits map[string][]Postsubmit `json:"postsubmits,omitempty"` 60 61 // Periodics are not associated with any repo. 62 Periodics []Periodic `json:"periodics,omitempty"` 63 } 64 65 // ProwConfig is config for all prow controllers 66 type ProwConfig struct { 67 Tide Tide `json:"tide,omitempty"` 68 Plank Plank `json:"plank,omitempty"` 69 Sinker Sinker `json:"sinker,omitempty"` 70 Deck Deck `json:"deck,omitempty"` 71 BranchProtection BranchProtection `json:"branch-protection,omitempty"` 72 Orgs map[string]org.Config `json:"orgs,omitempty"` 73 Gerrit Gerrit `json:"gerrit,omitempty"` 74 75 // TODO: Move this out of the main config. 76 JenkinsOperators []JenkinsOperator `json:"jenkins_operators,omitempty"` 77 78 // ProwJobNamespace is the namespace in the cluster that prow 79 // components will use for looking up ProwJobs. The namespace 80 // needs to exist and will not be created by prow. 81 // Defaults to "default". 82 ProwJobNamespace string `json:"prowjob_namespace,omitempty"` 83 // PodNamespace is the namespace in the cluster that prow 84 // components will use for looking up Pods owned by ProwJobs. 85 // The namespace needs to exist and will not be created by prow. 86 // Defaults to "default". 87 PodNamespace string `json:"pod_namespace,omitempty"` 88 89 // LogLevel enables dynamically updating the log level of the 90 // standard logger that is used by all prow components. 91 // 92 // Valid values: 93 // 94 // "debug", "info", "warn", "warning", "error", "fatal", "panic" 95 // 96 // Defaults to "info". 97 LogLevel string `json:"log_level,omitempty"` 98 99 // PushGateway is a prometheus push gateway. 100 PushGateway PushGateway `json:"push_gateway,omitempty"` 101 102 // OwnersDirBlacklist is used to configure which directories to ignore when 103 // searching for OWNERS{,_ALIAS} files in a repo. 104 OwnersDirBlacklist OwnersDirBlacklist `json:"owners_dir_blacklist,omitempty"` 105 } 106 107 // OwnersDirBlacklist is used to configure which directories to ignore when 108 // searching for OWNERS{,_ALIAS} files in a repo. 109 type OwnersDirBlacklist struct { 110 // Repos configures a directory blacklist per repo (or org) 111 Repos map[string][]string `json:"repos"` 112 // Default configures a default blacklist for repos (or orgs) not 113 // specifically configured 114 Default []string `json:"default"` 115 } 116 117 // PushGateway is a prometheus push gateway. 118 type PushGateway struct { 119 // Endpoint is the location of the prometheus pushgateway 120 // where prow will push metrics to. 121 Endpoint string `json:"endpoint,omitempty"` 122 // IntervalString compiles into Interval at load time. 123 IntervalString string `json:"interval,omitempty"` 124 // Interval specifies how often prow will push metrics 125 // to the pushgateway. Defaults to 1m. 126 Interval time.Duration `json:"-"` 127 } 128 129 // Controller holds configuration applicable to all agent-specific 130 // prow controllers. 131 type Controller struct { 132 // JobURLTemplateString compiles into JobURLTemplate at load time. 133 JobURLTemplateString string `json:"job_url_template,omitempty"` 134 // JobURLTemplate is compiled at load time from JobURLTemplateString. It 135 // will be passed a kube.ProwJob and is used to set the URL for the 136 // "Details" link on GitHub as well as the link from deck. 137 JobURLTemplate *template.Template `json:"-"` 138 139 // ReportTemplateString compiles into ReportTemplate at load time. 140 ReportTemplateString string `json:"report_template,omitempty"` 141 // ReportTemplate is compiled at load time from ReportTemplateString. It 142 // will be passed a kube.ProwJob and can provide an optional blurb below 143 // the test failures comment. 144 ReportTemplate *template.Template `json:"-"` 145 146 // MaxConcurrency is the maximum number of tests running concurrently that 147 // will be allowed by the controller. 0 implies no limit. 148 MaxConcurrency int `json:"max_concurrency,omitempty"` 149 150 // MaxGoroutines is the maximum number of goroutines spawned inside the 151 // controller to handle tests. Defaults to 20. Needs to be a positive 152 // number. 153 MaxGoroutines int `json:"max_goroutines,omitempty"` 154 155 // AllowCancellations enables aborting presubmit jobs for commits that 156 // have been superseded by newer commits in Github pull requests. 157 AllowCancellations bool `json:"allow_cancellations,omitempty"` 158 } 159 160 // Plank is config for the plank controller. 161 type Plank struct { 162 Controller `json:",inline"` 163 // PodPendingTimeoutString compiles into PodPendingTimeout at load time. 164 PodPendingTimeoutString string `json:"pod_pending_timeout,omitempty"` 165 // PodPendingTimeout is after how long the controller will perform a garbage 166 // collection on pending pods. Defaults to one day. 167 PodPendingTimeout time.Duration `json:"-"` 168 // DefaultDecorationConfig are defaults for shared fields for ProwJobs 169 // that request to have their PodSpecs decorated 170 DefaultDecorationConfig *kube.DecorationConfig `json:"default_decoration_config,omitempty"` 171 } 172 173 // Gerrit is config for the gerrit controller. 174 type Gerrit struct { 175 // TickInterval is how often we do a sync with binded gerrit instance 176 TickIntervalString string `json:"tick_interval,omitempty"` 177 TickInterval time.Duration `json:"-"` 178 // RateLimit defines how many changes to query per gerrit API call 179 // default is 5 180 RateLimit int `json:"ratelimit,omitempty"` 181 } 182 183 // JenkinsOperator is config for the jenkins-operator controller. 184 type JenkinsOperator struct { 185 Controller `json:",inline"` 186 // LabelSelectorString compiles into LabelSelector at load time. 187 // If set, this option needs to match --label-selector used by 188 // the desired jenkins-operator. This option is considered 189 // invalid when provided with a single jenkins-operator config. 190 // 191 // For label selector syntax, see below: 192 // https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors 193 LabelSelectorString string `json:"label_selector,omitempty"` 194 // LabelSelector is used so different jenkins-operator replicas 195 // can use their own configuration. 196 LabelSelector labels.Selector `json:"-"` 197 } 198 199 // Sinker is config for the sinker controller. 200 type Sinker struct { 201 // ResyncPeriodString compiles into ResyncPeriod at load time. 202 ResyncPeriodString string `json:"resync_period,omitempty"` 203 // ResyncPeriod is how often the controller will perform a garbage 204 // collection. Defaults to one hour. 205 ResyncPeriod time.Duration `json:"-"` 206 // MaxProwJobAgeString compiles into MaxProwJobAge at load time. 207 MaxProwJobAgeString string `json:"max_prowjob_age,omitempty"` 208 // MaxProwJobAge is how old a ProwJob can be before it is garbage-collected. 209 // Defaults to one week. 210 MaxProwJobAge time.Duration `json:"-"` 211 // MaxPodAgeString compiles into MaxPodAge at load time. 212 MaxPodAgeString string `json:"max_pod_age,omitempty"` 213 // MaxPodAge is how old a Pod can be before it is garbage-collected. 214 // Defaults to one day. 215 MaxPodAge time.Duration `json:"-"` 216 } 217 218 // Deck holds config for deck. 219 type Deck struct { 220 // TideUpdatePeriodString compiles into TideUpdatePeriod at load time. 221 TideUpdatePeriodString string `json:"tide_update_period,omitempty"` 222 // TideUpdatePeriod specifies how often Deck will fetch status from Tide. Defaults to 10s. 223 TideUpdatePeriod time.Duration `json:"-"` 224 // HiddenRepos is a list of orgs and/or repos that should not be displayed by Deck. 225 HiddenRepos []string `json:"hidden_repos,omitempty"` 226 // ExternalAgentLogs ensures external agents can expose 227 // their logs in prow. 228 ExternalAgentLogs []ExternalAgentLog `json:"external_agent_logs,omitempty"` 229 // Branding of the frontend 230 Branding *Branding `json:"branding,omitempty"` 231 } 232 233 // ExternalAgentLog ensures an external agent like Jenkins can expose 234 // its logs in prow. 235 type ExternalAgentLog struct { 236 // Agent is an external prow agent that supports exposing 237 // logs via deck. 238 Agent string `json:"agent,omitempty"` 239 // SelectorString compiles into Selector at load time. 240 SelectorString string `json:"selector,omitempty"` 241 // Selector can be used in prow deployments where the workload has 242 // been sharded between controllers of the same agent. For more info 243 // see https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors 244 Selector labels.Selector `json:"-"` 245 // URLTemplateString compiles into URLTemplate at load time. 246 URLTemplateString string `json:"url_template,omitempty"` 247 // URLTemplate is compiled at load time from URLTemplateString. It 248 // will be passed a kube.ProwJob and the generated URL should provide 249 // logs for the ProwJob. 250 URLTemplate *template.Template `json:"-"` 251 } 252 253 // Branding holds branding configuration for deck. 254 type Branding struct { 255 // Logo is the location of the logo that will be loaded in deck. 256 Logo string `json:"logo,omitempty"` 257 // Favicon is the location of the favicon that will be loaded in deck. 258 Favicon string `json:"favicon,omitempty"` 259 // BackgroundColor is the color of the background. 260 BackgroundColor string `json:"background_color,omitempty"` 261 // HeaderColor is the color of the header. 262 HeaderColor string `json:"header_color,omitempty"` 263 } 264 265 // Load loads and parses the config at path. 266 func Load(prowConfig, jobConfig string) (c *Config, err error) { 267 // we never want config loading to take down the prow components 268 defer func() { 269 if r := recover(); r != nil { 270 c, err = nil, fmt.Errorf("panic loading config: %v", r) 271 } 272 }() 273 c, err = loadConfig(prowConfig, jobConfig) 274 if err != nil { 275 return nil, err 276 } 277 if err := c.finalizeJobConfig(); err != nil { 278 return nil, err 279 } 280 if err := c.validateJobConfig(); err != nil { 281 return nil, err 282 } 283 return c, nil 284 } 285 286 // loadConfig loads one or multiple config files and returns a config object. 287 func loadConfig(prowConfig, jobConfig string) (*Config, error) { 288 stat, err := os.Stat(prowConfig) 289 if err != nil { 290 return nil, err 291 } 292 293 if stat.IsDir() { 294 return nil, fmt.Errorf("prowConfig cannot be a dir - %s", prowConfig) 295 } 296 297 var nc Config 298 if err := yamlToConfig(prowConfig, &nc); err != nil { 299 return nil, err 300 } 301 if err := parseProwConfig(&nc); err != nil { 302 return nil, err 303 } 304 305 // TODO(krzyzacy): temporary allow empty jobconfig 306 // also temporary allow job config in prow config 307 if jobConfig == "" { 308 return &nc, nil 309 } 310 311 stat, err = os.Stat(jobConfig) 312 if err != nil { 313 return nil, err 314 } 315 316 if !stat.IsDir() { 317 // still support a single file 318 var jc JobConfig 319 if err := yamlToConfig(jobConfig, &jc); err != nil { 320 return nil, err 321 } 322 if err := nc.mergeJobConfig(jc); err != nil { 323 return nil, err 324 } 325 return &nc, nil 326 } 327 328 // we need to ensure all config files have unique basenames, 329 // since updateconfig plugin will use basename as a key in the configmap 330 uniqueBasenames := sets.String{} 331 332 err = filepath.Walk(jobConfig, func(path string, info os.FileInfo, err error) error { 333 if err != nil { 334 logrus.WithError(err).Errorf("walking path %q.", path) 335 // bad file should not stop us from parsing the directory 336 return nil 337 } 338 339 if strings.HasPrefix(info.Name(), "..") { 340 // kubernetes volumes also include files we 341 // should not look be looking into for keys 342 if info.IsDir() { 343 return filepath.SkipDir 344 } 345 return nil 346 } 347 348 if filepath.Ext(path) != ".yaml" { 349 return nil 350 } 351 352 if info.IsDir() { 353 return nil 354 } 355 356 base := filepath.Base(path) 357 if uniqueBasenames.Has(base) { 358 return fmt.Errorf("duplicated basename is not allowed: %s", base) 359 } 360 uniqueBasenames.Insert(base) 361 362 var subConfig JobConfig 363 if err := yamlToConfig(path, &subConfig); err != nil { 364 return err 365 } 366 return nc.mergeJobConfig(subConfig) 367 }) 368 369 if err != nil { 370 return nil, err 371 } 372 373 return &nc, nil 374 } 375 376 // LoadSecrets loads multiple paths of secrets and add them in a map. 377 func LoadSecrets(paths []string) (map[string][]byte, error) { 378 secretsMap := make(map[string][]byte, len(paths)) 379 380 for _, path := range paths { 381 secretValue, err := LoadSingleSecret(path) 382 if err != nil { 383 return nil, err 384 } 385 secretsMap[path] = secretValue 386 } 387 return secretsMap, nil 388 } 389 390 // LoadSingleSecret reads and returns the value of a single file. 391 func LoadSingleSecret(path string) ([]byte, error) { 392 b, err := ioutil.ReadFile(path) 393 if err != nil { 394 return nil, fmt.Errorf("error reading %s: %v", path, err) 395 } 396 return bytes.TrimSpace(b), nil 397 } 398 399 // yamlToConfig converts a yaml file into a Config object 400 func yamlToConfig(path string, nc interface{}) error { 401 b, err := ioutil.ReadFile(path) 402 if err != nil { 403 return fmt.Errorf("error reading %s: %v", path, err) 404 } 405 if err := yaml.Unmarshal(b, nc); err != nil { 406 return fmt.Errorf("error unmarshaling %s: %v", path, err) 407 } 408 return nil 409 } 410 411 // mergeConfig merges two JobConfig together 412 // It will try to merge: 413 // - Presubmits 414 // - Postsubmits 415 // - Periodics 416 // - PodPresets 417 func (c *Config) mergeJobConfig(jc JobConfig) error { 418 // Merge everything 419 // *** Presets *** 420 c.Presets = append(c.Presets, jc.Presets...) 421 422 // validate no duplicated presets 423 validLabels := map[string]string{} 424 for _, preset := range c.Presets { 425 for label, val := range preset.Labels { 426 if _, ok := validLabels[label]; ok { 427 return fmt.Errorf("duplicated preset label : %s", label) 428 } 429 validLabels[label] = val 430 } 431 } 432 433 // *** Periodics *** 434 c.Periodics = append(c.Periodics, jc.Periodics...) 435 436 // *** Presubmits *** 437 if c.Presubmits == nil { 438 c.Presubmits = make(map[string][]Presubmit) 439 } 440 for repo, jobs := range jc.Presubmits { 441 c.Presubmits[repo] = append(c.Presubmits[repo], jobs...) 442 } 443 444 // *** Postsubmits *** 445 if c.Postsubmits == nil { 446 c.Postsubmits = make(map[string][]Postsubmit) 447 } 448 for repo, jobs := range jc.Postsubmits { 449 c.Postsubmits[repo] = append(c.Postsubmits[repo], jobs...) 450 } 451 452 return nil 453 } 454 455 func setPresubmitDecorationDefaults(c *Config, ps *Presubmit) { 456 if ps.Decorate { 457 ps.DecorationConfig = setDecorationDefaults(ps.DecorationConfig, c.Plank.DefaultDecorationConfig) 458 } 459 460 for i := range ps.RunAfterSuccess { 461 setPresubmitDecorationDefaults(c, &ps.RunAfterSuccess[i]) 462 } 463 } 464 465 func setPostsubmitDecorationDefaults(c *Config, ps *Postsubmit) { 466 if ps.Decorate { 467 ps.DecorationConfig = setDecorationDefaults(ps.DecorationConfig, c.Plank.DefaultDecorationConfig) 468 } 469 470 for i := range ps.RunAfterSuccess { 471 setPostsubmitDecorationDefaults(c, &ps.RunAfterSuccess[i]) 472 } 473 } 474 475 func setPeriodicDecorationDefaults(c *Config, ps *Periodic) { 476 if ps.Decorate { 477 ps.DecorationConfig = setDecorationDefaults(ps.DecorationConfig, c.Plank.DefaultDecorationConfig) 478 } 479 480 for i := range ps.RunAfterSuccess { 481 setPeriodicDecorationDefaults(c, &ps.RunAfterSuccess[i]) 482 } 483 } 484 485 // finalizeJobConfig mutates and fixes entries for jobspecs 486 func (c *Config) finalizeJobConfig() error { 487 if c.decorationRequested() { 488 if c.Plank.DefaultDecorationConfig == nil { 489 return errors.New("no default decoration config provided for plank") 490 } 491 if c.Plank.DefaultDecorationConfig.UtilityImages == nil { 492 return errors.New("no default decoration image pull specs provided for plank") 493 } 494 if c.Plank.DefaultDecorationConfig.GCSConfiguration == nil { 495 return errors.New("no default GCS decoration config provided for plank") 496 } 497 if c.Plank.DefaultDecorationConfig.GCSCredentialsSecret == "" { 498 return errors.New("no default GCS credentials secret provided for plank") 499 } 500 501 for _, vs := range c.Presubmits { 502 for i := range vs { 503 setPresubmitDecorationDefaults(c, &vs[i]) 504 } 505 } 506 507 for _, js := range c.Postsubmits { 508 for i := range js { 509 setPostsubmitDecorationDefaults(c, &js[i]) 510 } 511 } 512 513 for i := range c.Periodics { 514 setPeriodicDecorationDefaults(c, &c.Periodics[i]) 515 } 516 } 517 518 // Ensure that regexes are valid. 519 for _, vs := range c.Presubmits { 520 if err := SetPresubmitRegexes(vs); err != nil { 521 return fmt.Errorf("could not set regex: %v", err) 522 } 523 } 524 for _, js := range c.Postsubmits { 525 if err := SetPostsubmitRegexes(js); err != nil { 526 return fmt.Errorf("could not set regex: %v", err) 527 } 528 } 529 530 for _, v := range c.AllPresubmits(nil) { 531 if err := resolvePresets(v.Name, v.Labels, v.Spec, c.Presets); err != nil { 532 return err 533 } 534 } 535 536 for _, v := range c.AllPostsubmits(nil) { 537 if err := resolvePresets(v.Name, v.Labels, v.Spec, c.Presets); err != nil { 538 return err 539 } 540 } 541 542 for _, v := range c.AllPeriodics() { 543 if err := resolvePresets(v.Name, v.Labels, v.Spec, c.Presets); err != nil { 544 return err 545 } 546 } 547 548 return nil 549 } 550 551 // validateJobConfig validates if all the jobspecs/presets are valid 552 // if you are mutating the jobs, please add it to finalizeJobConfig above 553 func (c *Config) validateJobConfig() error { 554 type orgRepoJobName struct { 555 orgRepo, jobName string 556 } 557 558 // Validate presubmits. 559 // Checking that no duplicate job in prow config exists on the same org / repo / branch. 560 validPresubmits := map[orgRepoJobName][]Presubmit{} 561 for repo, jobs := range c.Presubmits { 562 for _, job := range listPresubmits(jobs) { 563 repoJobName := orgRepoJobName{repo, job.Name} 564 for _, existingJob := range validPresubmits[repoJobName] { 565 if existingJob.Brancher.Intersects(job.Brancher) { 566 return fmt.Errorf("duplicated presubmit job: %s", job.Name) 567 } 568 } 569 validPresubmits[repoJobName] = append(validPresubmits[repoJobName], job) 570 } 571 } 572 573 for _, v := range c.AllPresubmits(nil) { 574 if err := validateAgent(v.Name, v.Agent, v.Spec, v.DecorationConfig); err != nil { 575 return err 576 } 577 // Ensure max_concurrency is non-negative. 578 if v.MaxConcurrency < 0 { 579 return fmt.Errorf("job %s jas invalid max_concurrency (%d), it needs to be a non-negative number", v.Name, v.MaxConcurrency) 580 } 581 if err := validatePodSpec(v.Name, kube.PresubmitJob, v.Spec); err != nil { 582 return err 583 } 584 if err := validateLabels(v.Name, v.Labels); err != nil { 585 return err 586 } 587 if err := validateTriggering(v); err != nil { 588 return err 589 } 590 } 591 592 // Validate postsubmits. 593 // Checking that no duplicate job in prow config exists on the same org / repo / branch. 594 validPostsubmits := map[orgRepoJobName][]Postsubmit{} 595 for repo, jobs := range c.Postsubmits { 596 for _, job := range listPostsubmits(jobs) { 597 repoJobName := orgRepoJobName{repo, job.Name} 598 for _, existingJob := range validPostsubmits[repoJobName] { 599 if existingJob.Brancher.Intersects(job.Brancher) { 600 return fmt.Errorf("duplicated postsubmit job: %s", job.Name) 601 } 602 } 603 validPostsubmits[repoJobName] = append(validPostsubmits[repoJobName], job) 604 } 605 } 606 607 for _, j := range c.AllPostsubmits(nil) { 608 if err := validateAgent(j.Name, j.Agent, j.Spec, j.DecorationConfig); err != nil { 609 return err 610 } 611 // Ensure max_concurrency is non-negative. 612 if j.MaxConcurrency < 0 { 613 return fmt.Errorf("job %s jas invalid max_concurrency (%d), it needs to be a non-negative number", j.Name, j.MaxConcurrency) 614 } 615 if err := validatePodSpec(j.Name, kube.PostsubmitJob, j.Spec); err != nil { 616 return err 617 } 618 if err := validateLabels(j.Name, j.Labels); err != nil { 619 return err 620 } 621 } 622 623 // validate no duplicated periodics 624 validPeriodics := sets.NewString() 625 // Ensure that the periodic durations are valid and specs exist. 626 for _, p := range c.AllPeriodics() { 627 if validPeriodics.Has(p.Name) { 628 return fmt.Errorf("duplicated periodic job : %s", p.Name) 629 } 630 validPeriodics.Insert(p.Name) 631 if err := validateAgent(p.Name, p.Agent, p.Spec, p.DecorationConfig); err != nil { 632 return err 633 } 634 if err := validatePodSpec(p.Name, kube.PeriodicJob, p.Spec); err != nil { 635 return err 636 } 637 if err := validateLabels(p.Name, p.Labels); err != nil { 638 return err 639 } 640 } 641 // Set the interval on the periodic jobs. It doesn't make sense to do this 642 // for child jobs. 643 for j, p := range c.Periodics { 644 if p.Cron != "" && p.Interval != "" { 645 return fmt.Errorf("cron and interval cannot be both set in periodic %s", p.Name) 646 } else if p.Cron == "" && p.Interval == "" { 647 return fmt.Errorf("cron and interval cannot be both empty in periodic %s", p.Name) 648 } else if p.Cron != "" { 649 if _, err := cron.Parse(p.Cron); err != nil { 650 return fmt.Errorf("invalid cron string %s in periodic %s: %v", p.Cron, p.Name, err) 651 } 652 } else { 653 d, err := time.ParseDuration(c.Periodics[j].Interval) 654 if err != nil { 655 return fmt.Errorf("cannot parse duration for %s: %v", c.Periodics[j].Name, err) 656 } 657 c.Periodics[j].interval = d 658 } 659 } 660 661 return nil 662 } 663 664 func parseProwConfig(c *Config) error { 665 if err := ValidateController(&c.Plank.Controller); err != nil { 666 return fmt.Errorf("validating plank config: %v", err) 667 } 668 669 if c.Plank.PodPendingTimeoutString == "" { 670 c.Plank.PodPendingTimeout = 24 * time.Hour 671 } else { 672 podPendingTimeout, err := time.ParseDuration(c.Plank.PodPendingTimeoutString) 673 if err != nil { 674 return fmt.Errorf("cannot parse duration for plank.pod_pending_timeout: %v", err) 675 } 676 c.Plank.PodPendingTimeout = podPendingTimeout 677 } 678 679 if c.Gerrit.TickIntervalString == "" { 680 c.Gerrit.TickInterval = time.Minute 681 } else { 682 tickInterval, err := time.ParseDuration(c.Gerrit.TickIntervalString) 683 if err != nil { 684 return fmt.Errorf("cannot parse duration for c.gerrit.tick_interval: %v", err) 685 } 686 c.Gerrit.TickInterval = tickInterval 687 } 688 689 if c.Gerrit.RateLimit == 0 { 690 c.Gerrit.RateLimit = 5 691 } 692 693 for i := range c.JenkinsOperators { 694 if err := ValidateController(&c.JenkinsOperators[i].Controller); err != nil { 695 return fmt.Errorf("validating jenkins_operators config: %v", err) 696 } 697 sel, err := labels.Parse(c.JenkinsOperators[i].LabelSelectorString) 698 if err != nil { 699 return fmt.Errorf("invalid jenkins_operators.label_selector option: %v", err) 700 } 701 c.JenkinsOperators[i].LabelSelector = sel 702 // TODO: Invalidate overlapping selectors more 703 if len(c.JenkinsOperators) > 1 && c.JenkinsOperators[i].LabelSelectorString == "" { 704 return errors.New("selector overlap: cannot use an empty label_selector with multiple selectors") 705 } 706 if len(c.JenkinsOperators) == 1 && c.JenkinsOperators[0].LabelSelectorString != "" { 707 return errors.New("label_selector is invalid when used for a single jenkins-operator") 708 } 709 } 710 711 for i, agentToTmpl := range c.Deck.ExternalAgentLogs { 712 urlTemplate, err := template.New(agentToTmpl.Agent).Parse(agentToTmpl.URLTemplateString) 713 if err != nil { 714 return fmt.Errorf("parsing template for agent %q: %v", agentToTmpl.Agent, err) 715 } 716 c.Deck.ExternalAgentLogs[i].URLTemplate = urlTemplate 717 // we need to validate selectors used by deck since these are not 718 // sent to the api server. 719 s, err := labels.Parse(c.Deck.ExternalAgentLogs[i].SelectorString) 720 if err != nil { 721 return fmt.Errorf("error parsing selector %q: %v", c.Deck.ExternalAgentLogs[i].SelectorString, err) 722 } 723 c.Deck.ExternalAgentLogs[i].Selector = s 724 } 725 726 if c.Deck.TideUpdatePeriodString == "" { 727 c.Deck.TideUpdatePeriod = time.Second * 10 728 } else { 729 period, err := time.ParseDuration(c.Deck.TideUpdatePeriodString) 730 if err != nil { 731 return fmt.Errorf("cannot parse duration for deck.tide_update_period: %v", err) 732 } 733 c.Deck.TideUpdatePeriod = period 734 } 735 736 if c.PushGateway.IntervalString == "" { 737 c.PushGateway.Interval = time.Minute 738 } else { 739 interval, err := time.ParseDuration(c.PushGateway.IntervalString) 740 if err != nil { 741 return fmt.Errorf("cannot parse duration for push_gateway.interval: %v", err) 742 } 743 c.PushGateway.Interval = interval 744 } 745 746 if c.Sinker.ResyncPeriodString == "" { 747 c.Sinker.ResyncPeriod = time.Hour 748 } else { 749 resyncPeriod, err := time.ParseDuration(c.Sinker.ResyncPeriodString) 750 if err != nil { 751 return fmt.Errorf("cannot parse duration for sinker.resync_period: %v", err) 752 } 753 c.Sinker.ResyncPeriod = resyncPeriod 754 } 755 756 if c.Sinker.MaxProwJobAgeString == "" { 757 c.Sinker.MaxProwJobAge = 7 * 24 * time.Hour 758 } else { 759 maxProwJobAge, err := time.ParseDuration(c.Sinker.MaxProwJobAgeString) 760 if err != nil { 761 return fmt.Errorf("cannot parse duration for max_prowjob_age: %v", err) 762 } 763 c.Sinker.MaxProwJobAge = maxProwJobAge 764 } 765 766 if c.Sinker.MaxPodAgeString == "" { 767 c.Sinker.MaxPodAge = 24 * time.Hour 768 } else { 769 maxPodAge, err := time.ParseDuration(c.Sinker.MaxPodAgeString) 770 if err != nil { 771 return fmt.Errorf("cannot parse duration for max_pod_age: %v", err) 772 } 773 c.Sinker.MaxPodAge = maxPodAge 774 } 775 776 if c.Tide.SyncPeriodString == "" { 777 c.Tide.SyncPeriod = time.Minute 778 } else { 779 period, err := time.ParseDuration(c.Tide.SyncPeriodString) 780 if err != nil { 781 return fmt.Errorf("cannot parse duration for tide.sync_period: %v", err) 782 } 783 c.Tide.SyncPeriod = period 784 } 785 if c.Tide.StatusUpdatePeriodString == "" { 786 c.Tide.StatusUpdatePeriod = c.Tide.SyncPeriod 787 } else { 788 period, err := time.ParseDuration(c.Tide.StatusUpdatePeriodString) 789 if err != nil { 790 return fmt.Errorf("cannot parse duration for tide.status_update_period: %v", err) 791 } 792 c.Tide.StatusUpdatePeriod = period 793 } 794 795 if c.Tide.MaxGoroutines == 0 { 796 c.Tide.MaxGoroutines = 20 797 } 798 if c.Tide.MaxGoroutines <= 0 { 799 return fmt.Errorf("tide has invalid max_goroutines (%d), it needs to be a positive number", c.Tide.MaxGoroutines) 800 } 801 802 for name, method := range c.Tide.MergeType { 803 if method != github.MergeMerge && 804 method != github.MergeRebase && 805 method != github.MergeSquash { 806 return fmt.Errorf("merge type %q for %s is not a valid type", method, name) 807 } 808 } 809 810 for i, tq := range c.Tide.Queries { 811 if err := tq.Validate(); err != nil { 812 return fmt.Errorf("tide query (index %d) is invalid: %v", i, err) 813 } 814 } 815 816 if c.ProwJobNamespace == "" { 817 c.ProwJobNamespace = "default" 818 } 819 if c.PodNamespace == "" { 820 c.PodNamespace = "default" 821 } 822 823 if c.LogLevel == "" { 824 c.LogLevel = "info" 825 } 826 lvl, err := logrus.ParseLevel(c.LogLevel) 827 if err != nil { 828 return err 829 } 830 logrus.SetLevel(lvl) 831 832 return nil 833 } 834 835 func (c *JobConfig) decorationRequested() bool { 836 for _, vs := range c.Presubmits { 837 for i := range vs { 838 if vs[i].Decorate { 839 return true 840 } 841 } 842 } 843 844 for _, js := range c.Postsubmits { 845 for i := range js { 846 if js[i].Decorate { 847 return true 848 } 849 } 850 } 851 852 for i := range c.Periodics { 853 if c.Periodics[i].Decorate { 854 return true 855 } 856 } 857 858 return false 859 } 860 861 func setDecorationDefaults(provided, defaults *kube.DecorationConfig) *kube.DecorationConfig { 862 merged := &kube.DecorationConfig{} 863 if provided != nil { 864 merged = provided 865 } 866 867 if merged.Timeout == 0 { 868 merged.Timeout = defaults.Timeout 869 } 870 if merged.GracePeriod == 0 { 871 merged.GracePeriod = defaults.GracePeriod 872 } 873 if merged.UtilityImages == nil { 874 merged.UtilityImages = defaults.UtilityImages 875 } 876 if merged.GCSConfiguration == nil { 877 merged.GCSConfiguration = defaults.GCSConfiguration 878 } 879 if merged.GCSCredentialsSecret == "" { 880 merged.GCSCredentialsSecret = defaults.GCSCredentialsSecret 881 } 882 if len(merged.SSHKeySecrets) == 0 { 883 merged.SSHKeySecrets = defaults.SSHKeySecrets 884 } 885 886 return merged 887 } 888 889 func validateLabels(name string, labels map[string]string) error { 890 for label := range labels { 891 for _, prowLabel := range decorate.Labels() { 892 if label == prowLabel { 893 return fmt.Errorf("job %s attempted to set Prow-controlled label %s to %s", name, label, labels[label]) 894 } 895 } 896 } 897 return nil 898 } 899 900 func validateAgent(name, agent string, spec *v1.PodSpec, config *kube.DecorationConfig) error { 901 // Ensure that k8s jobs have a pod spec. 902 if agent == string(kube.KubernetesAgent) && spec == nil { 903 return fmt.Errorf("job %s has no spec", name) 904 } 905 // Only k8s jobs can be decorated 906 if agent != string(kube.KubernetesAgent) && config != nil { 907 return fmt.Errorf("job %s configured PodSpec decoration but is not a Kubernetes job", name) 908 } 909 // Jobs asking for decoration should provide config 910 if agent == string(kube.KubernetesAgent) && config != nil { 911 if config.UtilityImages == nil { 912 return fmt.Errorf("job %s does not configure pod utility images but asks for decoration", name) 913 } 914 if config.GCSConfiguration == nil || config.GCSCredentialsSecret == "" { 915 return fmt.Errorf("job %s does not configure GCS uploads but asks for decoration", name) 916 } 917 } 918 // Ensure agent is a known value. 919 if agent != string(kube.KubernetesAgent) && agent != string(kube.JenkinsAgent) && agent != string(kube.BuildAgent) { 920 return fmt.Errorf("job %s has invalid agent (%s), it needs to be one of the following: %s %s %s", 921 name, agent, kube.KubernetesAgent, kube.JenkinsAgent, kube.BuildAgent) 922 } 923 return nil 924 } 925 926 func resolvePresets(name string, labels map[string]string, spec *v1.PodSpec, presets []Preset) error { 927 for _, preset := range presets { 928 if err := mergePreset(preset, labels, spec); err != nil { 929 return fmt.Errorf("job %s failed to merge presets: %v", name, err) 930 } 931 } 932 933 return nil 934 } 935 936 func validatePodSpec(name string, jobType kube.ProwJobType, spec *v1.PodSpec) error { 937 if spec == nil { 938 return nil 939 } 940 941 if len(spec.InitContainers) != 0 { 942 return fmt.Errorf("job %s specified init containers, which is not allowed", name) 943 } 944 945 if len(spec.Containers) != 1 { 946 return fmt.Errorf("job %s specified %d containers when only one is allowed", name, len(spec.Containers)) 947 } 948 949 for _, env := range spec.Containers[0].Env { 950 for _, prowEnv := range downwardapi.EnvForType(jobType) { 951 if env.Name == prowEnv { 952 return fmt.Errorf("job %s attempted to set Prow-controlled environment variable %s to %s on test container", name, env.Name, env.Value) 953 } 954 } 955 } 956 957 for _, mount := range spec.Containers[0].VolumeMounts { 958 for _, prowMount := range decorate.VolumeMounts() { 959 if mount.Name == prowMount { 960 return fmt.Errorf("job %s attempted to mount a Prow-controlled volume mount %s on test container", name, mount.Name) 961 } 962 } 963 for _, prowMountPath := range decorate.VolumeMountPaths() { 964 if strings.HasPrefix(mount.MountPath, prowMountPath) || strings.HasPrefix(prowMountPath, mount.MountPath) { 965 return fmt.Errorf("job %s mounts %s at %s, which would conflict with a Prow-controlled mount at %s", name, mount.Name, mount.MountPath, prowMountPath) 966 } 967 } 968 } 969 970 for _, volume := range spec.Volumes { 971 for _, prowVolume := range decorate.VolumeMounts() { 972 if volume.Name == prowVolume { 973 return fmt.Errorf("job %s attempted to add a Prow-controlled volume %s", name, volume.Name) 974 } 975 } 976 } 977 978 return nil 979 } 980 981 func validateTriggering(job Presubmit) error { 982 if job.AlwaysRun && job.RunIfChanged != "" { 983 return fmt.Errorf("job %s is set to always run but also declares run_if_changed targets, which are mutually exclusive", job.Name) 984 } 985 986 if !job.SkipReport && job.Context == "" { 987 return fmt.Errorf("job %s is set to report but has no context configured", job.Name) 988 } 989 990 return nil 991 } 992 993 // ValidateController validates the provided controller config. 994 func ValidateController(c *Controller) error { 995 urlTmpl, err := template.New("JobURL").Parse(c.JobURLTemplateString) 996 if err != nil { 997 return fmt.Errorf("parsing template: %v", err) 998 } 999 c.JobURLTemplate = urlTmpl 1000 1001 reportTmpl, err := template.New("Report").Parse(c.ReportTemplateString) 1002 if err != nil { 1003 return fmt.Errorf("parsing template: %v", err) 1004 } 1005 c.ReportTemplate = reportTmpl 1006 if c.MaxConcurrency < 0 { 1007 return fmt.Errorf("controller has invalid max_concurrency (%d), it needs to be a non-negative number", c.MaxConcurrency) 1008 } 1009 if c.MaxGoroutines == 0 { 1010 c.MaxGoroutines = 20 1011 } 1012 if c.MaxGoroutines <= 0 { 1013 return fmt.Errorf("controller has invalid max_goroutines (%d), it needs to be a positive number", c.MaxGoroutines) 1014 } 1015 return nil 1016 } 1017 1018 // SetPresubmitRegexes compiles and validates all the regular expressions for 1019 // the provided presubmits. 1020 func SetPresubmitRegexes(js []Presubmit) error { 1021 for i, j := range js { 1022 if re, err := regexp.Compile(j.Trigger); err == nil { 1023 js[i].re = re 1024 } else { 1025 return fmt.Errorf("could not compile trigger regex for %s: %v", j.Name, err) 1026 } 1027 if !js[i].re.MatchString(j.RerunCommand) { 1028 return fmt.Errorf("for job %s, rerun command \"%s\" does not match trigger \"%s\"", j.Name, j.RerunCommand, j.Trigger) 1029 } 1030 if err := SetPresubmitRegexes(j.RunAfterSuccess); err != nil { 1031 return err 1032 } 1033 if j.RunIfChanged != "" { 1034 re, err := regexp.Compile(j.RunIfChanged) 1035 if err != nil { 1036 return fmt.Errorf("could not compile changes regex for %s: %v", j.Name, err) 1037 } 1038 js[i].reChanges = re 1039 } 1040 b, err := setBrancherRegexes(j.Brancher) 1041 if err != nil { 1042 return fmt.Errorf("could not set branch regexes for %s: %v", j.Name, err) 1043 } 1044 js[i].Brancher = b 1045 } 1046 return nil 1047 } 1048 1049 // setBrancherRegexes compiles and validates all the regular expressions for 1050 // the provided branch specifiers. 1051 func setBrancherRegexes(br Brancher) (Brancher, error) { 1052 if len(br.Branches) > 0 { 1053 if re, err := regexp.Compile(strings.Join(br.Branches, `|`)); err == nil { 1054 br.re = re 1055 } else { 1056 return br, fmt.Errorf("could not compile positive branch regex: %v", err) 1057 } 1058 } 1059 if len(br.SkipBranches) > 0 { 1060 if re, err := regexp.Compile(strings.Join(br.SkipBranches, `|`)); err == nil { 1061 br.reSkip = re 1062 } else { 1063 return br, fmt.Errorf("could not compile negative branch regex: %v", err) 1064 } 1065 } 1066 return br, nil 1067 } 1068 1069 // SetPostsubmitRegexes compiles and validates all the regular expressions for 1070 // the provided postsubmits. 1071 func SetPostsubmitRegexes(ps []Postsubmit) error { 1072 for i, j := range ps { 1073 b, err := setBrancherRegexes(j.Brancher) 1074 if err != nil { 1075 return fmt.Errorf("could not set branch regexes for %s: %v", j.Name, err) 1076 } 1077 ps[i].Brancher = b 1078 if err := SetPostsubmitRegexes(j.RunAfterSuccess); err != nil { 1079 return err 1080 } 1081 } 1082 return nil 1083 }