github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/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  package config
    19  
    20  import (
    21  	"fmt"
    22  	"io/ioutil"
    23  	"regexp"
    24  	"text/template"
    25  	"time"
    26  
    27  	"github.com/ghodss/yaml"
    28  	"github.com/sirupsen/logrus"
    29  
    30  	"k8s.io/test-infra/prow/kube"
    31  )
    32  
    33  // Config is a read-only snapshot of the config.
    34  type Config struct {
    35  	// Full repo name (such as "kubernetes/kubernetes") -> list of jobs.
    36  	Presubmits  map[string][]Presubmit  `json:"presubmits,omitempty"`
    37  	Postsubmits map[string][]Postsubmit `json:"postsubmits,omitempty"`
    38  
    39  	// Periodics are not associated with any repo.
    40  	Periodics []Periodic `json:"periodics,omitempty"`
    41  
    42  	Tide   Tide   `json:"tide,omitempty"`
    43  	Plank  Plank  `json:"plank,omitempty"`
    44  	Sinker Sinker `json:"sinker,omitempty"`
    45  
    46  	// TODO: Move this out of the main config.
    47  	JenkinsOperator JenkinsOperator `json:"jenkins_operator,omitempty"`
    48  
    49  	// ProwJobNamespace is the namespace in the cluster that prow
    50  	// components will use for looking up ProwJobs. The namespace
    51  	// needs to exist and will not be created by prow.
    52  	// Defaults to "default".
    53  	ProwJobNamespace string `json:"prowjob_namespace,omitempty"`
    54  	// PodNamespace is the namespace in the cluster that prow
    55  	// components will use for looking up Pods owned by ProwJobs.
    56  	// The namespace needs to exist and will not be created by prow.
    57  	// Defaults to "default".
    58  	PodNamespace string `json:"pod_namespace,omitempty"`
    59  
    60  	// LogLevel enables dynamically updating the log level of the
    61  	// standard logger that is used by all prow components.
    62  	//
    63  	// Valid values:
    64  	//
    65  	// "debug", "info", "warn", "warning", "error", "fatal", "panic"
    66  	//
    67  	// Defaults to "info".
    68  	LogLevel string `json:"log_level,omitempty"`
    69  
    70  	// PushGateway is a prometheus push gateway.
    71  	PushGateway PushGateway `json:"push_gateway,omitempty"`
    72  }
    73  
    74  type PushGateway struct {
    75  	Endpoint string `json:"endpoint,omitempty"`
    76  }
    77  
    78  // Tide is config for the tide pool.
    79  type Tide struct {
    80  	// These must be valid GitHub search queries. They should not overlap,
    81  	// which is to say two queries should never return the same PR.
    82  	Queries []string `json:"queries,omitempty"`
    83  }
    84  
    85  // Plank is config for the plank controller.
    86  type Plank struct {
    87  	// JobURLTemplateString compiles into JobURLTemplate at load time.
    88  	JobURLTemplateString string `json:"job_url_template,omitempty"`
    89  	// JobURLTemplate is compiled at load time from JobURLTemplateString. It
    90  	// will be passed a kube.ProwJob and is used to set the URL for the
    91  	// "details" link on GitHub as well as the link from deck.
    92  	JobURLTemplate *template.Template `json:"-"`
    93  
    94  	// ReportTemplateString compiles into ReportTemplate at load time.
    95  	ReportTemplateString string `json:"report_template,omitempty"`
    96  	// ReportTemplate is compiled at load time from ReportTemplateString. It
    97  	// will be passed a kube.ProwJob and can provide an optional blurb below
    98  	// the test failures comment.
    99  	ReportTemplate *template.Template `json:"-"`
   100  
   101  	// MaxConcurrency is the maximum number of tests running concurrently that
   102  	// will be allowed by plank. 0 implies no limit.
   103  	MaxConcurrency int `json:"max_concurrency,omitempty"`
   104  
   105  	// AllowCancellations enables aborting presubmit jobs for commits that
   106  	// have been superseded by newer commits in Github pull requests.
   107  	AllowCancellations bool `json:"allow_cancellations"`
   108  }
   109  
   110  // JenkinsOperator is config for the jenkins-operator controller.
   111  type JenkinsOperator struct {
   112  	// JobURLTemplateString compiles into JobURLTemplate at load time.
   113  	JobURLTemplateString string `json:"job_url_template,omitempty"`
   114  	// JobURLTemplate is compiled at load time from JobURLTemplateString. It
   115  	// will be passed a kube.ProwJob and is used to set the URL for the
   116  	// "details" link on GitHub as well as the link from deck.
   117  	JobURLTemplate *template.Template `json:"-"`
   118  
   119  	// ReportTemplateString compiles into ReportTemplate at load time.
   120  	ReportTemplateString string `json:"report_template,omitempty"`
   121  	// ReportTemplate is compiled at load time from ReportTemplateString. It
   122  	// will be passed a kube.ProwJob and can provide an optional blurb below
   123  	// the test failures comment.
   124  	ReportTemplate *template.Template `json:"-"`
   125  
   126  	// MaxConcurrency is the maximum number of tests running concurrently that
   127  	// will be allowed by jenkins-operator. 0 implies no limit.
   128  	MaxConcurrency int `json:"max_concurrency,omitempty"`
   129  
   130  	// AllowCancellations enables aborting presubmit jobs for commits that
   131  	// have been superseded by newer commits in Github pull requests.
   132  	AllowCancellations bool `json:"allow_cancellations"`
   133  }
   134  
   135  // Sinker is config for the sinker controller.
   136  type Sinker struct {
   137  	// ResyncPeriodString compiles into ResyncPeriod at load time.
   138  	ResyncPeriodString string `json:"resync_period,omitempty"`
   139  	// ResyncPeriod is how often the controller will perform a garbage
   140  	// collection. Defaults to one hour.
   141  	ResyncPeriod time.Duration `json:"-"`
   142  	// MaxProwJobAgeString compiles into MaxProwJobAge at load time.
   143  	MaxProwJobAgeString string `json:"max_prowjob_age,omitempty"`
   144  	// MaxProwJobAge is how old a ProwJob can be before it is garbage-collected.
   145  	// Defaults to one week.
   146  	MaxProwJobAge time.Duration `json:"-"`
   147  	// MaxPodAgeString compiles into MaxPodAge at load time.
   148  	MaxPodAgeString string `json:"max_pod_age,omitempty"`
   149  	// MaxPodAge is how old a Pod can be before it is garbage-collected.
   150  	// Defaults to one day.
   151  	MaxPodAge time.Duration `json:"-"`
   152  }
   153  
   154  // Load loads and parses the config at path.
   155  func Load(path string) (*Config, error) {
   156  	b, err := ioutil.ReadFile(path)
   157  	if err != nil {
   158  		return nil, fmt.Errorf("error reading %s: %v", path, err)
   159  	}
   160  	nc := &Config{}
   161  	if err := yaml.Unmarshal(b, nc); err != nil {
   162  		return nil, fmt.Errorf("error unmarshaling %s: %v", path, err)
   163  	}
   164  	if err := parseConfig(nc); err != nil {
   165  		return nil, err
   166  	}
   167  	return nc, nil
   168  }
   169  
   170  func parseConfig(c *Config) error {
   171  	// Ensure that presubmit regexes are valid.
   172  	for _, vs := range c.Presubmits {
   173  		if err := SetRegexes(vs); err != nil {
   174  			return fmt.Errorf("could not set regex: %v", err)
   175  		}
   176  	}
   177  
   178  	// Validate presubmits.
   179  	for _, v := range c.AllPresubmits(nil) {
   180  		name := v.Name
   181  		agent := v.Agent
   182  		// Ensure that k8s presubmits have a pod spec.
   183  		if agent == string(kube.KubernetesAgent) && v.Spec == nil {
   184  			return fmt.Errorf("job %s has no spec", name)
   185  		}
   186  		// Ensure agent is a known value.
   187  		if agent != string(kube.KubernetesAgent) && agent != string(kube.JenkinsAgent) {
   188  			return fmt.Errorf("job %s has invalid agent (%s), it needs to be one of the following: %s %s",
   189  				name, agent, kube.KubernetesAgent, kube.JenkinsAgent)
   190  		}
   191  		// Ensure max_concurrency is non-negative.
   192  		if v.MaxConcurrency < 0 {
   193  			return fmt.Errorf("job %s jas invalid max_concurrency (%d), it needs to be a non-negative number", name, v.MaxConcurrency)
   194  		}
   195  	}
   196  
   197  	// Validate postsubmits.
   198  	for _, j := range c.AllPostsubmits(nil) {
   199  		name := j.Name
   200  		agent := j.Agent
   201  		// Ensure that k8s postsubmits have a pod spec.
   202  		if agent == string(kube.KubernetesAgent) && j.Spec == nil {
   203  			return fmt.Errorf("job %s has no spec", name)
   204  		}
   205  		// Ensure agent is a known value.
   206  		if agent != string(kube.KubernetesAgent) && agent != string(kube.JenkinsAgent) {
   207  			return fmt.Errorf("job %s has invalid agent (%s), it needs to be one of the following: %s %s",
   208  				name, agent, kube.KubernetesAgent, kube.JenkinsAgent)
   209  		}
   210  		// Ensure max_concurrency is non-negative.
   211  		if j.MaxConcurrency < 0 {
   212  			return fmt.Errorf("job %s jas invalid max_concurrency (%d), it needs to be a non-negative number", name, j.MaxConcurrency)
   213  		}
   214  	}
   215  
   216  	// Ensure that the periodic durations are valid and specs exist.
   217  	for _, p := range c.AllPeriodics() {
   218  		name := p.Name
   219  		agent := p.Agent
   220  		if agent == string(kube.KubernetesAgent) && p.Spec == nil {
   221  			return fmt.Errorf("job %s has no spec", name)
   222  		}
   223  		if agent != string(kube.KubernetesAgent) && agent != string(kube.JenkinsAgent) {
   224  			return fmt.Errorf("job %s has invalid agent (%s), it needs to be one of the following: %s %s",
   225  				name, agent, kube.KubernetesAgent, kube.JenkinsAgent)
   226  		}
   227  	}
   228  	// Set the interval on the periodic jobs. It doesn't make sense to do this
   229  	// for child jobs.
   230  	for j := range c.Periodics {
   231  		d, err := time.ParseDuration(c.Periodics[j].Interval)
   232  		if err != nil {
   233  			return fmt.Errorf("cannot parse duration for %s: %v", c.Periodics[j].Name, err)
   234  		}
   235  		c.Periodics[j].interval = d
   236  	}
   237  
   238  	urlTmpl, err := template.New("JobURL").Parse(c.Plank.JobURLTemplateString)
   239  	if err != nil {
   240  		return fmt.Errorf("parsing template: %v", err)
   241  	}
   242  	c.Plank.JobURLTemplate = urlTmpl
   243  
   244  	reportTmpl, err := template.New("Report").Parse(c.Plank.ReportTemplateString)
   245  	if err != nil {
   246  		return fmt.Errorf("parsing template: %v", err)
   247  	}
   248  	c.Plank.ReportTemplate = reportTmpl
   249  	if c.Plank.MaxConcurrency < 0 {
   250  		return fmt.Errorf("plank has invalid max_concurrency (%d), it needs to be a non-negative number", c.Plank.MaxConcurrency)
   251  	}
   252  
   253  	jenkinsURLTmpl, err := template.New("JobURL").Parse(c.JenkinsOperator.JobURLTemplateString)
   254  	if err != nil {
   255  		return fmt.Errorf("parsing template: %v", err)
   256  	}
   257  	c.JenkinsOperator.JobURLTemplate = jenkinsURLTmpl
   258  
   259  	jenkinsReportTmpl, err := template.New("Report").Parse(c.JenkinsOperator.ReportTemplateString)
   260  	if err != nil {
   261  		return fmt.Errorf("parsing template: %v", err)
   262  	}
   263  	c.JenkinsOperator.ReportTemplate = jenkinsReportTmpl
   264  	if c.JenkinsOperator.MaxConcurrency < 0 {
   265  		return fmt.Errorf("jenkins-operator has invalid max_concurrency (%d), it needs to be a non-negative number", c.JenkinsOperator.MaxConcurrency)
   266  	}
   267  
   268  	if c.Sinker.ResyncPeriodString == "" {
   269  		c.Sinker.ResyncPeriod = time.Hour
   270  	} else {
   271  		resyncPeriod, err := time.ParseDuration(c.Sinker.ResyncPeriodString)
   272  		if err != nil {
   273  			return fmt.Errorf("cannot parse duration for resync_period: %v", err)
   274  		}
   275  		c.Sinker.ResyncPeriod = resyncPeriod
   276  	}
   277  
   278  	if c.Sinker.MaxProwJobAgeString == "" {
   279  		c.Sinker.MaxProwJobAge = 7 * 24 * time.Hour
   280  	} else {
   281  		maxProwJobAge, err := time.ParseDuration(c.Sinker.MaxProwJobAgeString)
   282  		if err != nil {
   283  			return fmt.Errorf("cannot parse duration for max_prowjob_age: %v", err)
   284  		}
   285  		c.Sinker.MaxProwJobAge = maxProwJobAge
   286  	}
   287  
   288  	if c.Sinker.MaxPodAgeString == "" {
   289  		c.Sinker.MaxPodAge = 24 * time.Hour
   290  	} else {
   291  		maxPodAge, err := time.ParseDuration(c.Sinker.MaxPodAgeString)
   292  		if err != nil {
   293  			return fmt.Errorf("cannot parse duration for max_pod_age: %v", err)
   294  		}
   295  		c.Sinker.MaxPodAge = maxPodAge
   296  	}
   297  
   298  	if c.ProwJobNamespace == "" {
   299  		c.ProwJobNamespace = "default"
   300  	}
   301  	if c.PodNamespace == "" {
   302  		c.PodNamespace = "default"
   303  	}
   304  
   305  	if c.LogLevel == "" {
   306  		c.LogLevel = "info"
   307  	}
   308  	lvl, err := logrus.ParseLevel(c.LogLevel)
   309  	if err != nil {
   310  		return err
   311  	}
   312  	logrus.SetLevel(lvl)
   313  
   314  	return nil
   315  }
   316  
   317  // SetRegexes compiles and validates all the regural expressions for
   318  // the provided presubmits.
   319  func SetRegexes(js []Presubmit) error {
   320  	for i, j := range js {
   321  		if re, err := regexp.Compile(j.Trigger); err == nil {
   322  			js[i].re = re
   323  		} else {
   324  			return fmt.Errorf("could not compile trigger regex for %s: %v", j.Name, err)
   325  		}
   326  		if !js[i].re.MatchString(j.RerunCommand) {
   327  			return fmt.Errorf("for job %s, rerun command \"%s\" does not match trigger \"%s\"", j.Name, j.RerunCommand, j.Trigger)
   328  		}
   329  		if err := SetRegexes(j.RunAfterSuccess); err != nil {
   330  			return err
   331  		}
   332  		if j.RunIfChanged != "" {
   333  			re, err := regexp.Compile(j.RunIfChanged)
   334  			if err != nil {
   335  				return fmt.Errorf("could not compile changes regex for %s: %v", j.Name, err)
   336  			}
   337  			js[i].reChanges = re
   338  		}
   339  	}
   340  	return nil
   341  }