mirror of
https://github.com/iLoveElysia/openbilibili.git
synced 2026-03-14 05:46:26 -05:00
1288 lines
40 KiB
Go
1288 lines
40 KiB
Go
/*
|
|
Copyright 2017 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
// Package config knows how to read and parse config.yaml.
|
|
// It also implements an agent to read the secrets.
|
|
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/ghodss/yaml"
|
|
"github.com/sirupsen/logrus"
|
|
"gopkg.in/robfig/cron.v2"
|
|
"k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
"k8s.io/apimachinery/pkg/util/validation"
|
|
|
|
prowjobv1 "k8s.io/test-infra/prow/apis/prowjobs/v1"
|
|
"k8s.io/test-infra/prow/config/org"
|
|
"k8s.io/test-infra/prow/gitserver"
|
|
"k8s.io/test-infra/prow/kube"
|
|
"k8s.io/test-infra/prow/pod-utils/decorate"
|
|
"k8s.io/test-infra/prow/pod-utils/downwardapi"
|
|
)
|
|
|
|
// Config is a read-only snapshot of the config.
|
|
type Config struct {
|
|
JobConfig
|
|
ProwConfig
|
|
CommonConfig
|
|
}
|
|
|
|
type CommonConfig struct {
|
|
Hdfs HdfsConfig `json:"hdfs,omitempty"`
|
|
}
|
|
|
|
// HdfsConfig is config for gowfs file system
|
|
type HdfsConfig struct {
|
|
Addr string `json:"addr,omitempty"`
|
|
User string `json:"user,omitempty"`
|
|
TimePeriodString string `json:"time_period,omitempty"`
|
|
Timeout time.Duration `json:"-"`
|
|
DisableKeepAlive bool `json:"disable_keep_alive,omitempty"`
|
|
BaseURL string `json:"base_url,omitempty"`
|
|
}
|
|
|
|
// JobConfig is config for all prow jobs
|
|
type JobConfig struct {
|
|
// Presets apply to all job types.
|
|
Presets []Preset `json:"presets,omitempty"`
|
|
// Full repo name (such as "kubernetes/kubernetes") -> list of jobs.
|
|
Presubmits map[string][]Presubmit `json:"presubmits,omitempty"`
|
|
Postsubmits map[string][]Postsubmit `json:"postsubmits,omitempty"`
|
|
|
|
// Periodics are not associated with any repo.
|
|
Periodics []Periodic `json:"periodics,omitempty"`
|
|
}
|
|
|
|
// ProwConfig is config for all prow controllers
|
|
type ProwConfig struct {
|
|
Tide Tide `json:"tide,omitempty"`
|
|
Plank Plank `json:"plank,omitempty"`
|
|
Sinker Sinker `json:"sinker,omitempty"`
|
|
Deck Deck `json:"deck,omitempty"`
|
|
BranchProtection BranchProtection `json:"branch-protection,omitempty"`
|
|
Orgs map[string]org.Config `json:"orgs,omitempty"`
|
|
Gerrit Gerrit `json:"gerrit,omitempty"`
|
|
BuildStatus BuildStatus `json:"build-status, omitempty"`
|
|
|
|
// TODO: Move this out of the main config.
|
|
JenkinsOperators []JenkinsOperator `json:"jenkins_operators,omitempty"`
|
|
|
|
// ProwJobNamespace is the namespace in the cluster that prow
|
|
// components will use for looking up ProwJobs. The namespace
|
|
// needs to exist and will not be created by prow.
|
|
// Defaults to "default".
|
|
ProwJobNamespace string `json:"prowjob_namespace,omitempty"`
|
|
// PodNamespace is the namespace in the cluster that prow
|
|
// components will use for looking up Pods owned by ProwJobs.
|
|
// The namespace needs to exist and will not be created by prow.
|
|
// Defaults to "default".
|
|
PodNamespace string `json:"pod_namespace,omitempty"`
|
|
|
|
// LogLevel enables dynamically updating the log level of the
|
|
// standard logger that is used by all prow components.
|
|
//
|
|
// Valid values:
|
|
//
|
|
// "debug", "info", "warn", "warning", "error", "fatal", "panic"
|
|
//
|
|
// Defaults to "info".
|
|
LogLevel string `json:"log_level,omitempty"`
|
|
|
|
// PushGateway is a prometheus push gateway.
|
|
PushGateway PushGateway `json:"push_gateway,omitempty"`
|
|
|
|
// OwnersDirBlacklist is used to configure which directories to ignore when
|
|
// searching for OWNERS{,_ALIAS} files in a repo.
|
|
OwnersDirBlacklist OwnersDirBlacklist `json:"owners_dir_blacklist,omitempty"`
|
|
}
|
|
|
|
// OwnersDirBlacklist is used to configure which directories to ignore when
|
|
// searching for OWNERS{,_ALIAS} files in a repo.
|
|
type OwnersDirBlacklist struct {
|
|
// Repos configures a directory blacklist per repo (or org)
|
|
Repos map[string][]string `json:"repos"`
|
|
// Default configures a default blacklist for repos (or orgs) not
|
|
// specifically configured
|
|
Default []string `json:"default"`
|
|
}
|
|
|
|
// PushGateway is a prometheus push gateway.
|
|
type PushGateway struct {
|
|
// Endpoint is the location of the prometheus pushgateway
|
|
// where prow will push metrics to.
|
|
Endpoint string `json:"endpoint,omitempty"`
|
|
// IntervalString compiles into Interval at load time.
|
|
IntervalString string `json:"interval,omitempty"`
|
|
// Interval specifies how often prow will push metrics
|
|
// to the pushgateway. Defaults to 1m.
|
|
Interval time.Duration `json:"-"`
|
|
}
|
|
|
|
// Controller holds configuration applicable to all agent-specific
|
|
// prow controllers.
|
|
type Controller struct {
|
|
// JobURLTemplateString compiles into JobURLTemplate at load time.
|
|
JobURLTemplateString string `json:"job_url_template,omitempty"`
|
|
// JobURLTemplate is compiled at load time from JobURLTemplateString. It
|
|
// will be passed a kube.ProwJob and is used to set the URL for the
|
|
// "Details" link on GitHub as well as the link from deck.
|
|
JobURLTemplate *template.Template `json:"-"`
|
|
|
|
// ReportTemplateString compiles into ReportTemplate at load time.
|
|
ReportTemplateString string `json:"report_template,omitempty"`
|
|
// ReportTemplate is compiled at load time from ReportTemplateString. It
|
|
// will be passed a kube.ProwJob and can provide an optional blurb below
|
|
// the test failures comment.
|
|
ReportTemplate *template.Template `json:"-"`
|
|
|
|
// MaxConcurrency is the maximum number of tests running concurrently that
|
|
// will be allowed by the controller. 0 implies no limit.
|
|
MaxConcurrency int `json:"max_concurrency,omitempty"`
|
|
|
|
// MaxGoroutines is the maximum number of goroutines spawned inside the
|
|
// controller to handle tests. Defaults to 20. Needs to be a positive
|
|
// number.
|
|
MaxGoroutines int `json:"max_goroutines,omitempty"`
|
|
|
|
// AllowCancellations enables aborting presubmit jobs for commits that
|
|
// have been superseded by newer commits in Github pull requests.
|
|
AllowCancellations bool `json:"allow_cancellations,omitempty"`
|
|
}
|
|
|
|
// Plank is config for the plank controller.
|
|
type Plank struct {
|
|
Controller `json:",inline"`
|
|
// PodPendingTimeoutString compiles into PodPendingTimeout at load time.
|
|
PodPendingTimeoutString string `json:"pod_pending_timeout,omitempty"`
|
|
// PodPendingTimeout is after how long the controller will perform a garbage
|
|
// collection on pending pods. Defaults to one day.
|
|
PodPendingTimeout time.Duration `json:"-"`
|
|
// DefaultDecorationConfig are defaults for shared fields for ProwJobs
|
|
// that request to have their PodSpecs decorated
|
|
DefaultDecorationConfig *kube.DecorationConfig `json:"default_decoration_config,omitempty"`
|
|
// JobURLPrefix is the host and path prefix under
|
|
// which job details will be viewable
|
|
JobURLPrefix string `json:"job_url_prefix,omitempty"`
|
|
}
|
|
|
|
// Gerrit is config for the gerrit controller.
|
|
type Gerrit struct {
|
|
// TickInterval is how often we do a sync with binded gerrit instance
|
|
TickIntervalString string `json:"tick_interval,omitempty"`
|
|
TickInterval time.Duration `json:"-"`
|
|
// RateLimit defines how many changes to query per gerrit API call
|
|
// default is 5
|
|
RateLimit int `json:"ratelimit,omitempty"`
|
|
}
|
|
|
|
// JenkinsOperator is config for the jenkins-operator controller.
|
|
type JenkinsOperator struct {
|
|
Controller `json:",inline"`
|
|
// LabelSelectorString compiles into LabelSelector at load time.
|
|
// If set, this option needs to match --label-selector used by
|
|
// the desired jenkins-operator. This option is considered
|
|
// invalid when provided with a single jenkins-operator config.
|
|
//
|
|
// For label selector syntax, see below:
|
|
// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors
|
|
LabelSelectorString string `json:"label_selector,omitempty"`
|
|
// LabelSelector is used so different jenkins-operator replicas
|
|
// can use their own configuration.
|
|
LabelSelector labels.Selector `json:"-"`
|
|
}
|
|
|
|
// Sinker is config for the sinker controller.
|
|
type Sinker struct {
|
|
// ResyncPeriodString compiles into ResyncPeriod at load time.
|
|
ResyncPeriodString string `json:"resync_period,omitempty"`
|
|
// ResyncPeriod is how often the controller will perform a garbage
|
|
// collection. Defaults to one hour.
|
|
ResyncPeriod time.Duration `json:"-"`
|
|
// MaxProwJobAgeString compiles into MaxProwJobAge at load time.
|
|
MaxProwJobAgeString string `json:"max_prowjob_age,omitempty"`
|
|
// MaxProwJobAge is how old a ProwJob can be before it is garbage-collected.
|
|
// Defaults to one week.
|
|
MaxProwJobAge time.Duration `json:"-"`
|
|
// MaxPodAgeString compiles into MaxPodAge at load time.
|
|
MaxPodAgeString string `json:"max_pod_age,omitempty"`
|
|
// MaxPodAge is how old a Pod can be before it is garbage-collected.
|
|
// Defaults to one day.
|
|
MaxPodAge time.Duration `json:"-"`
|
|
}
|
|
|
|
// Spyglass holds config for Spyglass
|
|
type Spyglass struct {
|
|
// Viewers is a map of Regexp strings to viewer names that defines which sets
|
|
// of artifacts need to be consumed by which viewers. The keys are compiled
|
|
// and stored in RegexCache at load time.
|
|
Viewers map[string][]string `json:"viewers,omitempty"`
|
|
// RegexCache is a map of viewer regexp strings to their compiled equivalents.
|
|
RegexCache map[string]*regexp.Regexp `json:"-"`
|
|
// SizeLimit is the max size artifact in bytes that Spyglass will attempt to
|
|
// read in entirety. This will only affect viewers attempting to use
|
|
// artifact.ReadAll(). To exclude outlier artifacts, set this limit to
|
|
// expected file size + variance. To include all artifacts with high
|
|
// probability, use 2*maximum observed artifact size.
|
|
SizeLimit int64 `json:"size_limit,omitempty"`
|
|
}
|
|
|
|
// Deck holds config for deck.
|
|
type Deck struct {
|
|
// Spyglass specifies which viewers will be used for which artifacts when viewing a job in Deck
|
|
Spyglass Spyglass `json:"spyglass,omitempty"`
|
|
// TideUpdatePeriodString compiles into TideUpdatePeriod at load time.
|
|
TideUpdatePeriodString string `json:"tide_update_period,omitempty"`
|
|
// TideUpdatePeriod specifies how often Deck will fetch status from Tide. Defaults to 10s.
|
|
TideUpdatePeriod time.Duration `json:"-"`
|
|
// HiddenRepos is a list of orgs and/or repos that should not be displayed by Deck.
|
|
HiddenRepos []string `json:"hidden_repos,omitempty"`
|
|
// ExternalAgentLogs ensures external agents can expose
|
|
// their logs in prow.
|
|
ExternalAgentLogs []ExternalAgentLog `json:"external_agent_logs,omitempty"`
|
|
// Branding of the frontend
|
|
Branding *Branding `json:"branding,omitempty"`
|
|
// Host of localhost
|
|
Host string `json:"host,omitempty"`
|
|
}
|
|
|
|
// ExternalAgentLog ensures an external agent like Jenkins can expose
|
|
// its logs in prow.
|
|
type ExternalAgentLog struct {
|
|
// Agent is an external prow agent that supports exposing
|
|
// logs via deck.
|
|
Agent string `json:"agent,omitempty"`
|
|
// SelectorString compiles into Selector at load time.
|
|
SelectorString string `json:"selector,omitempty"`
|
|
// Selector can be used in prow deployments where the workload has
|
|
// been sharded between controllers of the same agent. For more info
|
|
// see https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors
|
|
Selector labels.Selector `json:"-"`
|
|
// URLTemplateString compiles into URLTemplate at load time.
|
|
URLTemplateString string `json:"url_template,omitempty"`
|
|
// URLTemplate is compiled at load time from URLTemplateString. It
|
|
// will be passed a kube.ProwJob and the generated URL should provide
|
|
// logs for the ProwJob.
|
|
URLTemplate *template.Template `json:"-"`
|
|
}
|
|
|
|
// Branding holds branding configuration for deck.
|
|
type Branding struct {
|
|
// Logo is the location of the logo that will be loaded in deck.
|
|
Logo string `json:"logo,omitempty"`
|
|
// Favicon is the location of the favicon that will be loaded in deck.
|
|
Favicon string `json:"favicon,omitempty"`
|
|
// BackgroundColor is the color of the background.
|
|
BackgroundColor string `json:"background_color,omitempty"`
|
|
// HeaderColor is the color of the header.
|
|
HeaderColor string `json:"header_color,omitempty"`
|
|
}
|
|
|
|
// Load loads and parses the config at path.
|
|
func Load(prowConfig, jobConfig string) (c *Config, err error) {
|
|
// we never want config loading to take down the prow components
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
c, err = nil, fmt.Errorf("panic loading config: %v", r)
|
|
}
|
|
}()
|
|
c, err = loadConfig(prowConfig, jobConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := c.finalizeJobConfig(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := c.validateComponentConfig(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := c.validateJobConfig(); err != nil {
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// loadConfig loads one or multiple config files and returns a config object.
|
|
func loadConfig(prowConfig, jobConfig string) (*Config, error) {
|
|
stat, err := os.Stat(prowConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if stat.IsDir() {
|
|
return nil, fmt.Errorf("prowConfig cannot be a dir - %s", prowConfig)
|
|
}
|
|
|
|
var nc Config
|
|
if err := yamlToConfig(prowConfig, &nc); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := parseProwConfig(&nc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO(krzyzacy): temporary allow empty jobconfig
|
|
// also temporary allow job config in prow config
|
|
if jobConfig == "" {
|
|
return &nc, nil
|
|
}
|
|
|
|
stat, err = os.Stat(jobConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !stat.IsDir() {
|
|
// still support a single file
|
|
var jc JobConfig
|
|
if err := yamlToConfig(jobConfig, &jc); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := nc.mergeJobConfig(jc); err != nil {
|
|
return nil, err
|
|
}
|
|
return &nc, nil
|
|
}
|
|
|
|
// we need to ensure all config files have unique basenames,
|
|
// since updateconfig plugin will use basename as a key in the configmap
|
|
uniqueBasenames := sets.String{}
|
|
|
|
err = filepath.Walk(jobConfig, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
logrus.WithError(err).Errorf("walking path %q.", path)
|
|
// bad file should not stop us from parsing the directory
|
|
return nil
|
|
}
|
|
|
|
if strings.HasPrefix(info.Name(), "..") {
|
|
// kubernetes volumes also include files we
|
|
// should not look be looking into for keys
|
|
if info.IsDir() {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if filepath.Ext(path) != ".yaml" && filepath.Ext(path) != ".yml" {
|
|
return nil
|
|
}
|
|
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
base := filepath.Base(path)
|
|
if uniqueBasenames.Has(base) {
|
|
return fmt.Errorf("duplicated basename is not allowed: %s", base)
|
|
}
|
|
uniqueBasenames.Insert(base)
|
|
|
|
var subConfig JobConfig
|
|
if err := yamlToConfig(path, &subConfig); err != nil {
|
|
return err
|
|
}
|
|
return nc.mergeJobConfig(subConfig)
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &nc, nil
|
|
}
|
|
|
|
// LoadSecrets loads multiple paths of secrets and add them in a map.
|
|
func LoadSecrets(paths []string) (map[string][]byte, error) {
|
|
secretsMap := make(map[string][]byte, len(paths))
|
|
|
|
for _, path := range paths {
|
|
secretValue, err := LoadSingleSecret(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
secretsMap[path] = secretValue
|
|
}
|
|
return secretsMap, nil
|
|
}
|
|
|
|
// LoadSingleSecret reads and returns the value of a single file.
|
|
func LoadSingleSecret(path string) ([]byte, error) {
|
|
b, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading %s: %v", path, err)
|
|
}
|
|
return bytes.TrimSpace(b), nil
|
|
}
|
|
|
|
func LoadCommonConfig(path string, nc interface{}) error {
|
|
var (
|
|
b []byte
|
|
err error
|
|
)
|
|
if b, err = ioutil.ReadFile(path); err != nil {
|
|
return fmt.Errorf("LoadCommonConfig error reading %s: %v", path, err)
|
|
}
|
|
if err = yaml.Unmarshal(b, nc); err != nil {
|
|
return fmt.Errorf("LoadCommonConfig error unmarshaling %s: %v", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// yamlToConfig converts a yaml file into a Config object
|
|
func yamlToConfig(path string, nc interface{}) error {
|
|
b, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading %s: %v", path, err)
|
|
}
|
|
if err := yaml.Unmarshal(b, nc); err != nil {
|
|
return fmt.Errorf("error unmarshaling %s: %v", path, err)
|
|
}
|
|
var jc *JobConfig
|
|
switch v := nc.(type) {
|
|
case *JobConfig:
|
|
jc = v
|
|
case *Config:
|
|
jc = &v.JobConfig
|
|
}
|
|
for rep := range jc.Presubmits {
|
|
var fix func(*Presubmit)
|
|
fix = func(job *Presubmit) {
|
|
job.SourcePath = path
|
|
for i := range job.RunAfterSuccess {
|
|
fix(&job.RunAfterSuccess[i])
|
|
}
|
|
}
|
|
for i := range jc.Presubmits[rep] {
|
|
fix(&jc.Presubmits[rep][i])
|
|
}
|
|
}
|
|
for rep := range jc.Postsubmits {
|
|
var fix func(*Postsubmit)
|
|
fix = func(job *Postsubmit) {
|
|
job.SourcePath = path
|
|
for i := range job.RunAfterSuccess {
|
|
fix(&job.RunAfterSuccess[i])
|
|
}
|
|
}
|
|
for i := range jc.Postsubmits[rep] {
|
|
fix(&jc.Postsubmits[rep][i])
|
|
}
|
|
}
|
|
|
|
var fix func(*Periodic)
|
|
fix = func(job *Periodic) {
|
|
job.SourcePath = path
|
|
for i := range job.RunAfterSuccess {
|
|
fix(&job.RunAfterSuccess[i])
|
|
}
|
|
}
|
|
for i := range jc.Periodics {
|
|
fix(&jc.Periodics[i])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// mergeConfig merges two JobConfig together
|
|
// It will try to merge:
|
|
// - Presubmits
|
|
// - Postsubmits
|
|
// - Periodics
|
|
// - PodPresets
|
|
func (c *Config) mergeJobConfig(jc JobConfig) error {
|
|
// Merge everything
|
|
// *** Presets ***
|
|
c.Presets = append(c.Presets, jc.Presets...)
|
|
|
|
// validate no duplicated presets
|
|
validLabels := map[string]string{}
|
|
for _, preset := range c.Presets {
|
|
for label, val := range preset.Labels {
|
|
if _, ok := validLabels[label]; ok {
|
|
return fmt.Errorf("duplicated preset label : %s", label)
|
|
}
|
|
validLabels[label] = val
|
|
}
|
|
}
|
|
|
|
// *** Periodics ***
|
|
c.Periodics = append(c.Periodics, jc.Periodics...)
|
|
|
|
// *** Presubmits ***
|
|
if c.Presubmits == nil {
|
|
c.Presubmits = make(map[string][]Presubmit)
|
|
}
|
|
for repo, jobs := range jc.Presubmits {
|
|
c.Presubmits[repo] = append(c.Presubmits[repo], jobs...)
|
|
}
|
|
|
|
// *** Postsubmits ***
|
|
if c.Postsubmits == nil {
|
|
c.Postsubmits = make(map[string][]Postsubmit)
|
|
}
|
|
for repo, jobs := range jc.Postsubmits {
|
|
c.Postsubmits[repo] = append(c.Postsubmits[repo], jobs...)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func setPresubmitDecorationDefaults(c *Config, ps *Presubmit) {
|
|
if ps.Decorate {
|
|
ps.DecorationConfig = ps.DecorationConfig.ApplyDefault(c.Plank.DefaultDecorationConfig)
|
|
}
|
|
|
|
for i := range ps.RunAfterSuccess {
|
|
setPresubmitDecorationDefaults(c, &ps.RunAfterSuccess[i])
|
|
}
|
|
}
|
|
|
|
func setPostsubmitDecorationDefaults(c *Config, ps *Postsubmit) {
|
|
if ps.Decorate {
|
|
ps.DecorationConfig = ps.DecorationConfig.ApplyDefault(c.Plank.DefaultDecorationConfig)
|
|
}
|
|
|
|
for i := range ps.RunAfterSuccess {
|
|
setPostsubmitDecorationDefaults(c, &ps.RunAfterSuccess[i])
|
|
}
|
|
}
|
|
|
|
func setPeriodicDecorationDefaults(c *Config, ps *Periodic) {
|
|
if ps.Decorate {
|
|
ps.DecorationConfig = ps.DecorationConfig.ApplyDefault(c.Plank.DefaultDecorationConfig)
|
|
}
|
|
|
|
for i := range ps.RunAfterSuccess {
|
|
setPeriodicDecorationDefaults(c, &ps.RunAfterSuccess[i])
|
|
}
|
|
}
|
|
|
|
// finalizeJobConfig mutates and fixes entries for jobspecs
|
|
func (c *Config) finalizeJobConfig() error {
|
|
if c.decorationRequested() {
|
|
if c.Plank.DefaultDecorationConfig == nil {
|
|
return errors.New("no default decoration config provided for plank")
|
|
}
|
|
if c.Plank.DefaultDecorationConfig.UtilityImages == nil {
|
|
return errors.New("no default decoration image pull specs provided for plank")
|
|
}
|
|
if c.Plank.DefaultDecorationConfig.GCSConfiguration == nil {
|
|
return errors.New("no default GCS decoration config provided for plank")
|
|
}
|
|
if c.Plank.DefaultDecorationConfig.GCSCredentialsSecret == "" {
|
|
return errors.New("no default GCS credentials secret provided for plank")
|
|
}
|
|
|
|
for _, vs := range c.Presubmits {
|
|
for i := range vs {
|
|
setPresubmitDecorationDefaults(c, &vs[i])
|
|
}
|
|
}
|
|
|
|
for _, js := range c.Postsubmits {
|
|
for i := range js {
|
|
setPostsubmitDecorationDefaults(c, &js[i])
|
|
}
|
|
}
|
|
|
|
for i := range c.Periodics {
|
|
setPeriodicDecorationDefaults(c, &c.Periodics[i])
|
|
}
|
|
}
|
|
|
|
// Ensure that regexes are valid and set defaults.
|
|
for _, vs := range c.Presubmits {
|
|
c.defaultPresubmitFields(vs)
|
|
if err := SetPresubmitRegexes(vs); err != nil {
|
|
return fmt.Errorf("could not set regex: %v", err)
|
|
}
|
|
}
|
|
for _, js := range c.Postsubmits {
|
|
c.defaultPostsubmitFields(js)
|
|
if err := SetPostsubmitRegexes(js); err != nil {
|
|
return fmt.Errorf("could not set regex: %v", err)
|
|
}
|
|
}
|
|
|
|
c.defaultPeriodicFields(c.Periodics)
|
|
|
|
for _, v := range c.AllPresubmits(nil) {
|
|
if err := resolvePresets(v.Name, v.Labels, v.Spec, c.Presets); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, v := range c.AllPostsubmits(nil) {
|
|
if err := resolvePresets(v.Name, v.Labels, v.Spec, c.Presets); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, v := range c.AllPeriodics() {
|
|
if err := resolvePresets(v.Name, v.Labels, v.Spec, c.Presets); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateComponentConfig validates the infrastructure component configuration
|
|
func (c *Config) validateComponentConfig() error {
|
|
if _, err := url.Parse(c.Plank.JobURLPrefix); c.Plank.JobURLPrefix != "" && err != nil {
|
|
return fmt.Errorf("plank declares an invalid job URL prefix %q: %v", c.Plank.JobURLPrefix, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var jobNameRegex = regexp.MustCompile(`^[A-Za-z0-9-._]+$`)
|
|
|
|
func validateJobBase(v JobBase, jobType kube.ProwJobType, podNamespace string) error {
|
|
if !jobNameRegex.MatchString(v.Name) {
|
|
return fmt.Errorf("name: must match regex %q", jobNameRegex.String())
|
|
}
|
|
// Ensure max_concurrency is non-negative.
|
|
if v.MaxConcurrency < 0 {
|
|
return fmt.Errorf("max_concurrency: %d must be a non-negative number", v.MaxConcurrency)
|
|
}
|
|
if err := validateAgent(v, podNamespace); err != nil {
|
|
return err
|
|
}
|
|
if err := validatePodSpec(jobType, v.Spec); err != nil {
|
|
return err
|
|
}
|
|
if err := validateLabels(v.Labels); err != nil {
|
|
return err
|
|
}
|
|
if v.Spec == nil || len(v.Spec.Containers) == 0 {
|
|
return nil // knative-build and jenkins jobs have no spec
|
|
}
|
|
return validateDecoration(v.Spec.Containers[0], v.DecorationConfig)
|
|
}
|
|
|
|
// validateJobConfig validates if all the jobspecs/presets are valid
|
|
// if you are mutating the jobs, please add it to finalizeJobConfig above
|
|
func (c *Config) validateJobConfig() error {
|
|
type orgRepoJobName struct {
|
|
orgRepo, jobName string
|
|
}
|
|
|
|
// Validate presubmits.
|
|
// Checking that no duplicate job in prow config exists on the same org / repo / branch.
|
|
validPresubmits := map[orgRepoJobName][]Presubmit{}
|
|
for repo, jobs := range c.Presubmits {
|
|
for _, job := range listPresubmits(jobs) {
|
|
repoJobName := orgRepoJobName{repo, job.Name}
|
|
for _, existingJob := range validPresubmits[repoJobName] {
|
|
if existingJob.Brancher.Intersects(job.Brancher) {
|
|
return fmt.Errorf("duplicated presubmit job: %s", job.Name)
|
|
}
|
|
}
|
|
validPresubmits[repoJobName] = append(validPresubmits[repoJobName], job)
|
|
}
|
|
}
|
|
|
|
for _, v := range c.AllPresubmits(nil) {
|
|
if err := validateJobBase(v.JobBase, prowjobv1.PresubmitJob, c.PodNamespace); err != nil {
|
|
return fmt.Errorf("invalid presubmit job %s: %v", v.Name, err)
|
|
}
|
|
if err := validateTriggering(v); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Validate postsubmits.
|
|
// Checking that no duplicate job in prow config exists on the same org / repo / branch.
|
|
validPostsubmits := map[orgRepoJobName][]Postsubmit{}
|
|
for repo, jobs := range c.Postsubmits {
|
|
for _, job := range listPostsubmits(jobs) {
|
|
repoJobName := orgRepoJobName{repo, job.Name}
|
|
for _, existingJob := range validPostsubmits[repoJobName] {
|
|
if existingJob.Brancher.Intersects(job.Brancher) {
|
|
return fmt.Errorf("duplicated postsubmit job: %s", job.Name)
|
|
}
|
|
}
|
|
validPostsubmits[repoJobName] = append(validPostsubmits[repoJobName], job)
|
|
}
|
|
}
|
|
|
|
for _, j := range c.AllPostsubmits(nil) {
|
|
if err := validateJobBase(j.JobBase, prowjobv1.PostsubmitJob, c.PodNamespace); err != nil {
|
|
return fmt.Errorf("invalid postsubmit job %s: %v", j.Name, err)
|
|
}
|
|
}
|
|
|
|
// validate no duplicated periodics
|
|
validPeriodics := sets.NewString()
|
|
// Ensure that the periodic durations are valid and specs exist.
|
|
for _, p := range c.AllPeriodics() {
|
|
if validPeriodics.Has(p.Name) {
|
|
return fmt.Errorf("duplicated periodic job : %s", p.Name)
|
|
}
|
|
validPeriodics.Insert(p.Name)
|
|
if err := validateJobBase(p.JobBase, prowjobv1.PeriodicJob, c.PodNamespace); err != nil {
|
|
return fmt.Errorf("invalid periodic job %s: %v", p.Name, err)
|
|
}
|
|
}
|
|
// Set the interval on the periodic jobs. It doesn't make sense to do this
|
|
// for child jobs.
|
|
for j, p := range c.Periodics {
|
|
if p.Cron != "" && p.Interval != "" {
|
|
return fmt.Errorf("cron and interval cannot be both set in periodic %s", p.Name)
|
|
} else if p.Cron == "" && p.Interval == "" {
|
|
return fmt.Errorf("cron and interval cannot be both empty in periodic %s", p.Name)
|
|
} else if p.Cron != "" {
|
|
if _, err := cron.Parse(p.Cron); err != nil {
|
|
return fmt.Errorf("invalid cron string %s in periodic %s: %v", p.Cron, p.Name, err)
|
|
}
|
|
} else {
|
|
d, err := time.ParseDuration(c.Periodics[j].Interval)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot parse duration for %s: %v", c.Periodics[j].Name, err)
|
|
}
|
|
c.Periodics[j].interval = d
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseProwConfig(c *Config) error {
|
|
if err := ValidateController(&c.Plank.Controller); err != nil {
|
|
return fmt.Errorf("validating plank config: %v", err)
|
|
}
|
|
|
|
if c.Plank.PodPendingTimeoutString == "" {
|
|
c.Plank.PodPendingTimeout = 24 * time.Hour
|
|
} else {
|
|
podPendingTimeout, err := time.ParseDuration(c.Plank.PodPendingTimeoutString)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot parse duration for plank.pod_pending_timeout: %v", err)
|
|
}
|
|
c.Plank.PodPendingTimeout = podPendingTimeout
|
|
}
|
|
|
|
if c.Gerrit.TickIntervalString == "" {
|
|
c.Gerrit.TickInterval = time.Minute
|
|
} else {
|
|
tickInterval, err := time.ParseDuration(c.Gerrit.TickIntervalString)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot parse duration for c.gerrit.tick_interval: %v", err)
|
|
}
|
|
c.Gerrit.TickInterval = tickInterval
|
|
}
|
|
|
|
if c.Gerrit.RateLimit == 0 {
|
|
c.Gerrit.RateLimit = 5
|
|
}
|
|
|
|
for i := range c.JenkinsOperators {
|
|
if err := ValidateController(&c.JenkinsOperators[i].Controller); err != nil {
|
|
return fmt.Errorf("validating jenkins_operators config: %v", err)
|
|
}
|
|
sel, err := labels.Parse(c.JenkinsOperators[i].LabelSelectorString)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid jenkins_operators.label_selector option: %v", err)
|
|
}
|
|
c.JenkinsOperators[i].LabelSelector = sel
|
|
// TODO: Invalidate overlapping selectors more
|
|
if len(c.JenkinsOperators) > 1 && c.JenkinsOperators[i].LabelSelectorString == "" {
|
|
return errors.New("selector overlap: cannot use an empty label_selector with multiple selectors")
|
|
}
|
|
if len(c.JenkinsOperators) == 1 && c.JenkinsOperators[0].LabelSelectorString != "" {
|
|
return errors.New("label_selector is invalid when used for a single jenkins-operator")
|
|
}
|
|
}
|
|
|
|
for i, agentToTmpl := range c.Deck.ExternalAgentLogs {
|
|
urlTemplate, err := template.New(agentToTmpl.Agent).Parse(agentToTmpl.URLTemplateString)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing template for agent %q: %v", agentToTmpl.Agent, err)
|
|
}
|
|
c.Deck.ExternalAgentLogs[i].URLTemplate = urlTemplate
|
|
// we need to validate selectors used by deck since these are not
|
|
// sent to the api server.
|
|
s, err := labels.Parse(c.Deck.ExternalAgentLogs[i].SelectorString)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing selector %q: %v", c.Deck.ExternalAgentLogs[i].SelectorString, err)
|
|
}
|
|
c.Deck.ExternalAgentLogs[i].Selector = s
|
|
}
|
|
|
|
if c.Deck.TideUpdatePeriodString == "" {
|
|
c.Deck.TideUpdatePeriod = time.Second * 10
|
|
} else {
|
|
period, err := time.ParseDuration(c.Deck.TideUpdatePeriodString)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot parse duration for deck.tide_update_period: %v", err)
|
|
}
|
|
c.Deck.TideUpdatePeriod = period
|
|
}
|
|
|
|
if c.Deck.Spyglass.SizeLimit == 0 {
|
|
c.Deck.Spyglass.SizeLimit = 100e6
|
|
} else if c.Deck.Spyglass.SizeLimit <= 0 {
|
|
return fmt.Errorf("invalid value for deck.spyglass.size_limit, must be >=0")
|
|
}
|
|
|
|
c.Deck.Spyglass.RegexCache = make(map[string]*regexp.Regexp)
|
|
for k := range c.Deck.Spyglass.Viewers {
|
|
r, err := regexp.Compile(k)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot compile regexp %s, err: %v", k, err)
|
|
}
|
|
c.Deck.Spyglass.RegexCache[k] = r
|
|
}
|
|
|
|
if c.PushGateway.IntervalString == "" {
|
|
c.PushGateway.Interval = time.Minute
|
|
} else {
|
|
interval, err := time.ParseDuration(c.PushGateway.IntervalString)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot parse duration for push_gateway.interval: %v", err)
|
|
}
|
|
c.PushGateway.Interval = interval
|
|
}
|
|
|
|
if c.Sinker.ResyncPeriodString == "" {
|
|
c.Sinker.ResyncPeriod = time.Hour
|
|
} else {
|
|
resyncPeriod, err := time.ParseDuration(c.Sinker.ResyncPeriodString)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot parse duration for sinker.resync_period: %v", err)
|
|
}
|
|
c.Sinker.ResyncPeriod = resyncPeriod
|
|
}
|
|
|
|
if c.Sinker.MaxProwJobAgeString == "" {
|
|
c.Sinker.MaxProwJobAge = 7 * 24 * time.Hour
|
|
} else {
|
|
maxProwJobAge, err := time.ParseDuration(c.Sinker.MaxProwJobAgeString)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot parse duration for max_prowjob_age: %v", err)
|
|
}
|
|
c.Sinker.MaxProwJobAge = maxProwJobAge
|
|
}
|
|
|
|
if c.Sinker.MaxPodAgeString == "" {
|
|
c.Sinker.MaxPodAge = 24 * time.Hour
|
|
} else {
|
|
maxPodAge, err := time.ParseDuration(c.Sinker.MaxPodAgeString)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot parse duration for max_pod_age: %v", err)
|
|
}
|
|
c.Sinker.MaxPodAge = maxPodAge
|
|
}
|
|
|
|
if c.Tide.SyncPeriodString == "" {
|
|
c.Tide.SyncPeriod = time.Minute
|
|
} else {
|
|
period, err := time.ParseDuration(c.Tide.SyncPeriodString)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot parse duration for tide.sync_period: %v", err)
|
|
}
|
|
c.Tide.SyncPeriod = period
|
|
}
|
|
if c.Tide.StatusUpdatePeriodString == "" {
|
|
c.Tide.StatusUpdatePeriod = c.Tide.SyncPeriod
|
|
} else {
|
|
period, err := time.ParseDuration(c.Tide.StatusUpdatePeriodString)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot parse duration for tide.status_update_period: %v", err)
|
|
}
|
|
c.Tide.StatusUpdatePeriod = period
|
|
}
|
|
|
|
if c.Tide.MaxGoroutines == 0 {
|
|
c.Tide.MaxGoroutines = 20
|
|
}
|
|
if c.Tide.MaxGoroutines <= 0 {
|
|
return fmt.Errorf("tide has invalid max_goroutines (%d), it needs to be a positive number", c.Tide.MaxGoroutines)
|
|
}
|
|
|
|
for name, method := range c.Tide.MergeType {
|
|
if method != gitserver.MergeMerge &&
|
|
method != gitserver.MergeRebase &&
|
|
method != gitserver.MergeSquash {
|
|
return fmt.Errorf("merge type %q for %s is not a valid type", method, name)
|
|
}
|
|
}
|
|
|
|
for i, tq := range c.Tide.Queries {
|
|
if err := tq.Validate(); err != nil {
|
|
return fmt.Errorf("tide query (index %d) is invalid: %v", i, err)
|
|
}
|
|
}
|
|
|
|
if c.ProwJobNamespace == "" {
|
|
c.ProwJobNamespace = "default"
|
|
}
|
|
if c.PodNamespace == "" {
|
|
c.PodNamespace = "default"
|
|
}
|
|
|
|
if c.LogLevel == "" {
|
|
c.LogLevel = "info"
|
|
}
|
|
lvl, err := logrus.ParseLevel(c.LogLevel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
logrus.SetLevel(lvl)
|
|
|
|
if c.Deck.Host == "" {
|
|
c.Deck.Host = "localhost:8080"
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *JobConfig) decorationRequested() bool {
|
|
for _, vs := range c.Presubmits {
|
|
for i := range vs {
|
|
if vs[i].Decorate {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, js := range c.Postsubmits {
|
|
for i := range js {
|
|
if js[i].Decorate {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
for i := range c.Periodics {
|
|
if c.Periodics[i].Decorate {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func validateLabels(labels map[string]string) error {
|
|
for label, value := range labels {
|
|
for _, prowLabel := range decorate.Labels() {
|
|
if label == prowLabel {
|
|
return fmt.Errorf("label %s is reserved for decoration", label)
|
|
}
|
|
}
|
|
if errs := validation.IsQualifiedName(label); len(errs) != 0 {
|
|
return fmt.Errorf("invalid label %s: %v", label, errs)
|
|
}
|
|
if errs := validation.IsValidLabelValue(labels[label]); len(errs) != 0 {
|
|
return fmt.Errorf("label %s has invalid value %s: %v", label, value, errs)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateAgent(v JobBase, podNamespace string) error {
|
|
k := string(prowjobv1.KubernetesAgent)
|
|
b := string(prowjobv1.KnativeBuildAgent)
|
|
j := string(prowjobv1.JenkinsAgent)
|
|
agents := sets.NewString(k, b, j)
|
|
agent := v.Agent
|
|
switch {
|
|
case !agents.Has(agent):
|
|
return fmt.Errorf("agent must be one of %s (found %q)", strings.Join(agents.List(), ", "), agent)
|
|
case v.Spec != nil && agent != k:
|
|
return fmt.Errorf("job specs require agent: %s (found %q)", k, agent)
|
|
case agent == k && v.Spec == nil:
|
|
return errors.New("kubernetes jobs require a spec")
|
|
case v.BuildSpec != nil && agent != b:
|
|
return fmt.Errorf("job build_specs require agent: %s (found %q)", b, agent)
|
|
case agent == b && v.BuildSpec == nil:
|
|
return errors.New("knative-build jobs require a build_spec")
|
|
case v.DecorationConfig != nil && agent != k:
|
|
// TODO(fejta): support decoration
|
|
return fmt.Errorf("decoration requires agent: %s (found %q)", k, agent)
|
|
case v.ErrorOnEviction && agent != k:
|
|
return fmt.Errorf("error_on_eviction only applies to agent: %s (found %q)", k, agent)
|
|
case v.Namespace == nil || *v.Namespace == "":
|
|
return fmt.Errorf("failed to default namespace")
|
|
case *v.Namespace != podNamespace && agent != b:
|
|
// TODO(fejta): update plank to allow this (depends on client change)
|
|
return fmt.Errorf("namespace customization requires agent: %s (found %q)", b, agent)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateDecoration(container v1.Container, config *kube.DecorationConfig) error {
|
|
if config == nil {
|
|
return nil
|
|
}
|
|
|
|
if err := config.Validate(); err != nil {
|
|
return fmt.Errorf("invalid decoration config: %v", err)
|
|
}
|
|
var args []string
|
|
args = append(append(args, container.Command...), container.Args...)
|
|
if len(args) == 0 || args[0] == "" {
|
|
return errors.New("decorated job containers must specify command and/or args")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func resolvePresets(name string, labels map[string]string, spec *v1.PodSpec, presets []Preset) error {
|
|
for _, preset := range presets {
|
|
if err := mergePreset(preset, labels, spec); err != nil {
|
|
return fmt.Errorf("job %s failed to merge presets: %v", name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validatePodSpec(jobType kube.ProwJobType, spec *v1.PodSpec) error {
|
|
if spec == nil {
|
|
return nil
|
|
}
|
|
|
|
if len(spec.InitContainers) != 0 {
|
|
return errors.New("pod spec may not use init containers")
|
|
}
|
|
|
|
if n := len(spec.Containers); n != 1 {
|
|
return fmt.Errorf("pod spec must specify exactly 1 container, found: %d", n)
|
|
}
|
|
|
|
for _, env := range spec.Containers[0].Env {
|
|
for _, prowEnv := range downwardapi.EnvForType(jobType) {
|
|
if env.Name == prowEnv {
|
|
// TODO(fejta): consider allowing this
|
|
return fmt.Errorf("env %s is reserved", env.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, mount := range spec.Containers[0].VolumeMounts {
|
|
for _, prowMount := range decorate.VolumeMounts() {
|
|
if mount.Name == prowMount {
|
|
return fmt.Errorf("volumeMount name %s is reserved for decoration", prowMount)
|
|
}
|
|
}
|
|
for _, prowMountPath := range decorate.VolumeMountPaths() {
|
|
if strings.HasPrefix(mount.MountPath, prowMountPath) || strings.HasPrefix(prowMountPath, mount.MountPath) {
|
|
return fmt.Errorf("mount %s at %s conflicts with decoration mount at %s", mount.Name, mount.MountPath, prowMountPath)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, volume := range spec.Volumes {
|
|
for _, prowVolume := range decorate.VolumeMounts() {
|
|
if volume.Name == prowVolume {
|
|
return fmt.Errorf("volume %s is a reserved for decoration", volume.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateTriggering(job Presubmit) error {
|
|
if job.AlwaysRun && job.RunIfChanged != "" {
|
|
return fmt.Errorf("job %s is set to always run but also declares run_if_changed targets, which are mutually exclusive", job.Name)
|
|
}
|
|
|
|
if !job.SkipReport && job.Context == "" {
|
|
return fmt.Errorf("job %s is set to report but has no context configured", job.Name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateController validates the provided controller config.
|
|
func ValidateController(c *Controller) error {
|
|
urlTmpl, err := template.New("JobURL").Parse(c.JobURLTemplateString)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing template: %v", err)
|
|
}
|
|
c.JobURLTemplate = urlTmpl
|
|
|
|
reportTmpl, err := template.New("Report").Parse(c.ReportTemplateString)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing template: %v", err)
|
|
}
|
|
c.ReportTemplate = reportTmpl
|
|
if c.MaxConcurrency < 0 {
|
|
return fmt.Errorf("controller has invalid max_concurrency (%d), it needs to be a non-negative number", c.MaxConcurrency)
|
|
}
|
|
if c.MaxGoroutines == 0 {
|
|
c.MaxGoroutines = 20
|
|
}
|
|
if c.MaxGoroutines <= 0 {
|
|
return fmt.Errorf("controller has invalid max_goroutines (%d), it needs to be a positive number", c.MaxGoroutines)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DefaultTriggerFor returns the default regexp string used to match comments
|
|
// that should trigger the job with this name.
|
|
func DefaultTriggerFor(name string) string {
|
|
return fmt.Sprintf(`(?m)^/test( | .* )%s,?($|\s.*)`, name)
|
|
}
|
|
|
|
// DefaultRerunCommandFor returns the default rerun command for the job with
|
|
// this name.
|
|
func DefaultRerunCommandFor(name string) string {
|
|
return fmt.Sprintf("/test %s", name)
|
|
}
|
|
|
|
// defaultJobBase configures common parameters, currently Agent and Namespace.
|
|
func (c *ProwConfig) defaultJobBase(base *JobBase) {
|
|
if base.Agent == "" { // Use kubernetes by default
|
|
base.Agent = string(kube.KubernetesAgent)
|
|
}
|
|
if base.Namespace == nil || *base.Namespace == "" {
|
|
s := c.PodNamespace
|
|
base.Namespace = &s
|
|
}
|
|
if base.Cluster == "" {
|
|
base.Cluster = kube.DefaultClusterAlias
|
|
}
|
|
}
|
|
|
|
func (c *ProwConfig) defaultPresubmitFields(js []Presubmit) {
|
|
for i := range js {
|
|
c.defaultJobBase(&js[i].JobBase)
|
|
if js[i].Context == "" {
|
|
js[i].Context = js[i].Name
|
|
}
|
|
// Default the values of Trigger and RerunCommand if both fields are
|
|
// specified. Otherwise let validation fail as both or neither should have
|
|
// been specified.
|
|
if js[i].Trigger == "" && js[i].RerunCommand == "" {
|
|
js[i].Trigger = DefaultTriggerFor(js[i].Name)
|
|
js[i].RerunCommand = DefaultRerunCommandFor(js[i].Name)
|
|
}
|
|
c.defaultPresubmitFields(js[i].RunAfterSuccess)
|
|
}
|
|
}
|
|
|
|
func (c *ProwConfig) defaultPostsubmitFields(js []Postsubmit) {
|
|
for i := range js {
|
|
c.defaultJobBase(&js[i].JobBase)
|
|
c.defaultPostsubmitFields(js[i].RunAfterSuccess)
|
|
}
|
|
}
|
|
|
|
func (c *ProwConfig) defaultPeriodicFields(js []Periodic) {
|
|
for i := range js {
|
|
c.defaultJobBase(&js[i].JobBase)
|
|
c.defaultPeriodicFields(js[i].RunAfterSuccess)
|
|
}
|
|
}
|
|
|
|
// SetPresubmitRegexes compiles and validates all the regular expressions for
|
|
// the provided presubmits.
|
|
func SetPresubmitRegexes(js []Presubmit) error {
|
|
for i, j := range js {
|
|
if re, err := regexp.Compile(j.Trigger); err == nil {
|
|
js[i].re = re
|
|
} else {
|
|
return fmt.Errorf("could not compile trigger regex for %s: %v", j.Name, err)
|
|
}
|
|
if !js[i].re.MatchString(j.RerunCommand) {
|
|
return fmt.Errorf("for job %s, rerun command \"%s\" does not match trigger \"%s\"", j.Name, j.RerunCommand, j.Trigger)
|
|
}
|
|
if j.RunIfChanged != "" {
|
|
re, err := regexp.Compile(j.RunIfChanged)
|
|
if err != nil {
|
|
return fmt.Errorf("could not compile changes regex for %s: %v", j.Name, err)
|
|
}
|
|
js[i].reChanges = re
|
|
}
|
|
b, err := setBrancherRegexes(j.Brancher)
|
|
if err != nil {
|
|
return fmt.Errorf("could not set branch regexes for %s: %v", j.Name, err)
|
|
}
|
|
js[i].Brancher = b
|
|
|
|
if err := SetPresubmitRegexes(j.RunAfterSuccess); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// setBrancherRegexes compiles and validates all the regular expressions for
|
|
// the provided branch specifiers.
|
|
func setBrancherRegexes(br Brancher) (Brancher, error) {
|
|
if len(br.Branches) > 0 {
|
|
if re, err := regexp.Compile(strings.Join(br.Branches, `|`)); err == nil {
|
|
br.re = re
|
|
} else {
|
|
return br, fmt.Errorf("could not compile positive branch regex: %v", err)
|
|
}
|
|
}
|
|
if len(br.SkipBranches) > 0 {
|
|
if re, err := regexp.Compile(strings.Join(br.SkipBranches, `|`)); err == nil {
|
|
br.reSkip = re
|
|
} else {
|
|
return br, fmt.Errorf("could not compile negative branch regex: %v", err)
|
|
}
|
|
}
|
|
return br, nil
|
|
}
|
|
|
|
func setChangeRegexes(cm RegexpChangeMatcher) (RegexpChangeMatcher, error) {
|
|
if cm.RunIfChanged != "" {
|
|
re, err := regexp.Compile(cm.RunIfChanged)
|
|
if err != nil {
|
|
return cm, fmt.Errorf("could not compile run_if_changed regex: %v", err)
|
|
}
|
|
cm.reChanges = re
|
|
}
|
|
return cm, nil
|
|
}
|
|
|
|
// SetPostsubmitRegexes compiles and validates all the regular expressions for
|
|
// the provided postsubmits.
|
|
func SetPostsubmitRegexes(ps []Postsubmit) error {
|
|
for i, j := range ps {
|
|
b, err := setBrancherRegexes(j.Brancher)
|
|
if err != nil {
|
|
return fmt.Errorf("could not set branch regexes for %s: %v", j.Name, err)
|
|
}
|
|
ps[i].Brancher = b
|
|
c, err := setChangeRegexes(j.RegexpChangeMatcher)
|
|
if err != nil {
|
|
return fmt.Errorf("could not set change regexes for %s: %v", j.Name, err)
|
|
}
|
|
ps[i].RegexpChangeMatcher = c
|
|
if err := SetPostsubmitRegexes(j.RunAfterSuccess); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|