github.com/yrj2011/jx-test-infra@v0.0.0-20190529031832-7a2065ee98eb/prow/config/tide.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
    18  
    19  import (
    20  	"fmt"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/sirupsen/logrus"
    25  
    26  	"k8s.io/apimachinery/pkg/util/sets"
    27  	"k8s.io/test-infra/prow/github"
    28  )
    29  
    30  const timeFormatISO8601 = "2006-01-02T15:04:05Z"
    31  
    32  // TideQueries is a TideQuery slice.
    33  type TideQueries []TideQuery
    34  
    35  // TideContextPolicy configures options about how to handle various contexts.
    36  type TideContextPolicy struct {
    37  	// whether to consider unknown contexts optional (skip) or required.
    38  	SkipUnknownContexts *bool    `json:"skip-unknown-contexts,omitempty"`
    39  	RequiredContexts    []string `json:"required-contexts,omitempty"`
    40  	OptionalContexts    []string `json:"optional-contexts,omitempty"`
    41  	// Infer required and optional jobs from Branch Protection configuration
    42  	FromBranchProtection *bool `json:"from-branch-protection,omitempty"`
    43  }
    44  
    45  // TideOrgContextPolicy overrides the policy for an org, and any repo overrides.
    46  type TideOrgContextPolicy struct {
    47  	TideContextPolicy
    48  	Repos map[string]TideRepoContextPolicy `json:"repos,omitempty"`
    49  }
    50  
    51  // TideRepoContextPolicy overrides the policy for repo, and any branch overrides.
    52  type TideRepoContextPolicy struct {
    53  	TideContextPolicy
    54  	Branches map[string]TideContextPolicy `json:"branches,omitempty"`
    55  }
    56  
    57  // TideContextPolicyOptions holds the default policy, and any org overrides.
    58  type TideContextPolicyOptions struct {
    59  	TideContextPolicy
    60  	// Github Orgs
    61  	Orgs map[string]TideOrgContextPolicy `json:"orgs,omitempty"`
    62  }
    63  
    64  // Tide is config for the tide pool.
    65  type Tide struct {
    66  	// SyncPeriodString compiles into SyncPeriod at load time.
    67  	SyncPeriodString string `json:"sync_period,omitempty"`
    68  	// SyncPeriod specifies how often Tide will sync jobs with Github. Defaults to 1m.
    69  	SyncPeriod time.Duration `json:"-"`
    70  	// StatusUpdatePeriodString compiles into StatusUpdatePeriod at load time.
    71  	StatusUpdatePeriodString string `json:"status_update_period,omitempty"`
    72  	// StatusUpdatePeriod specifies how often Tide will update Github status contexts.
    73  	// Defaults to the value of SyncPeriod.
    74  	StatusUpdatePeriod time.Duration `json:"-"`
    75  	// Queries must not overlap. It must be impossible for any two queries to
    76  	// ever return the same PR.
    77  	// TODO: This will only be possible when we allow specifying orgs. At that
    78  	//       point, verify the above condition.
    79  	Queries TideQueries `json:"queries,omitempty"`
    80  
    81  	// A key/value pair of an org/repo as the key and merge method to override
    82  	// the default method of merge. Valid options are squash, rebase, and merge.
    83  	MergeType map[string]github.PullRequestMergeType `json:"merge_method,omitempty"`
    84  
    85  	// URL for tide status contexts.
    86  	// We can consider allowing this to be set separately for separate repos, or
    87  	// allowing it to be a template.
    88  	TargetURL string `json:"target_url,omitempty"`
    89  
    90  	// PRStatusBaseURL is the base URL for the PR status page.
    91  	// This is used to link to a merge requirements overview
    92  	// in the tide status context.
    93  	PRStatusBaseURL string `json:"pr_status_base_url,omitempty"`
    94  
    95  	// BlockerLabel is an optional label that is used to identify merge blocking
    96  	// Github issues.
    97  	// Leave this blank to disable this feature and save 1 API token per sync loop.
    98  	BlockerLabel string `json:"blocker_label,omitempty"`
    99  
   100  	// MaxGoroutines is the maximum number of goroutines spawned inside the
   101  	// controller to handle org/repo:branch pools. Defaults to 20. Needs to be a
   102  	// positive number.
   103  	MaxGoroutines int `json:"max_goroutines,omitempty"`
   104  
   105  	// TideContextPolicyOptions defines merge options for context. If not set it will infer
   106  	// the required and optional contexts from the prow jobs configured and use the github
   107  	// combined status; otherwise it may apply the branch protection setting or let user
   108  	// define their own options in case branch protection is not used.
   109  	ContextOptions TideContextPolicyOptions `json:"context_options,omitempty"`
   110  }
   111  
   112  // MergeMethod returns the merge method to use for a repo. The default of merge is
   113  // returned when not overridden.
   114  func (t *Tide) MergeMethod(org, repo string) github.PullRequestMergeType {
   115  	name := org + "/" + repo
   116  
   117  	v, ok := t.MergeType[name]
   118  	if !ok {
   119  		if ov, found := t.MergeType[org]; found {
   120  			return ov
   121  		}
   122  
   123  		return github.MergeMerge
   124  	}
   125  
   126  	return v
   127  }
   128  
   129  // TideQuery is turned into a GitHub search query. See the docs for details:
   130  // https://help.github.com/articles/searching-issues-and-pull-requests/
   131  type TideQuery struct {
   132  	Orgs  []string `json:"orgs,omitempty"`
   133  	Repos []string `json:"repos,omitempty"`
   134  
   135  	ExcludedBranches []string `json:"excludedBranches,omitempty"`
   136  	IncludedBranches []string `json:"includedBranches,omitempty"`
   137  
   138  	Labels        []string `json:"labels,omitempty"`
   139  	MissingLabels []string `json:"missingLabels,omitempty"`
   140  
   141  	Milestone string `json:"milestone,omitempty"`
   142  
   143  	ReviewApprovedRequired bool `json:"reviewApprovedRequired,omitempty"`
   144  }
   145  
   146  // Query returns the corresponding github search string for the tide query.
   147  func (tq *TideQuery) Query() string {
   148  	toks := []string{"is:pr", "state:open"}
   149  	for _, o := range tq.Orgs {
   150  		toks = append(toks, fmt.Sprintf("org:\"%s\"", o))
   151  	}
   152  	for _, r := range tq.Repos {
   153  		toks = append(toks, fmt.Sprintf("repo:\"%s\"", r))
   154  	}
   155  	for _, b := range tq.ExcludedBranches {
   156  		toks = append(toks, fmt.Sprintf("-base:\"%s\"", b))
   157  	}
   158  	for _, b := range tq.IncludedBranches {
   159  		toks = append(toks, fmt.Sprintf("base:\"%s\"", b))
   160  	}
   161  	for _, l := range tq.Labels {
   162  		toks = append(toks, fmt.Sprintf("label:\"%s\"", l))
   163  	}
   164  	for _, l := range tq.MissingLabels {
   165  		toks = append(toks, fmt.Sprintf("-label:\"%s\"", l))
   166  	}
   167  	if tq.Milestone != "" {
   168  		toks = append(toks, fmt.Sprintf("milestone:\"%s\"", tq.Milestone))
   169  	}
   170  	if tq.ReviewApprovedRequired {
   171  		toks = append(toks, "review:approved")
   172  	}
   173  	return strings.Join(toks, " ")
   174  }
   175  
   176  // AllPRsSince returns all open PRs in the repos covered by the query that
   177  // have changed since time t.
   178  func (tqs TideQueries) AllPRsSince(t time.Time) string {
   179  	toks := []string{"is:pr", "state:open"}
   180  
   181  	orgs, repos := tqs.OrgsAndRepos()
   182  	for _, o := range orgs.List() {
   183  		toks = append(toks, fmt.Sprintf("org:\"%s\"", o))
   184  	}
   185  	for _, r := range repos.List() {
   186  		toks = append(toks, fmt.Sprintf("repo:\"%s\"", r))
   187  	}
   188  	// Github's GraphQL API silently fails if you provide it with an invalid time
   189  	// string.
   190  	// Dates before 1970 are considered invalid.
   191  	if t.Year() >= 1970 {
   192  		toks = append(toks, fmt.Sprintf("updated:>=%s", t.Format(timeFormatISO8601)))
   193  	}
   194  	return strings.Join(toks, " ")
   195  }
   196  
   197  // OrgsAndRepos returns the set of orgs and repos present in any query.
   198  func (tqs TideQueries) OrgsAndRepos() (sets.String, sets.String) {
   199  	orgs := sets.NewString()
   200  	repos := sets.NewString()
   201  	for i := range tqs {
   202  		orgs.Insert(tqs[i].Orgs...)
   203  		repos.Insert(tqs[i].Repos...)
   204  	}
   205  	return orgs, repos
   206  }
   207  
   208  // QueryMap is a mapping from ("org/repo" or "org") -> TideQueries that
   209  // apply to that org or repo.
   210  type QueryMap map[string]TideQueries
   211  
   212  // QueryMap creates a QueryMap from TideQueries
   213  func (tqs TideQueries) QueryMap() QueryMap {
   214  	res := make(map[string]TideQueries)
   215  	for _, tq := range tqs {
   216  		for _, org := range tq.Orgs {
   217  			res[org] = append(res[org], tq)
   218  		}
   219  		for _, repo := range tq.Repos {
   220  			res[repo] = append(res[repo], tq)
   221  		}
   222  	}
   223  	return res
   224  }
   225  
   226  // ForRepo returns the tide queries that apply to a repo.
   227  func (qm QueryMap) ForRepo(org, repo string) TideQueries {
   228  	qs := TideQueries(nil)
   229  	qs = append(qs, qm[org]...)
   230  	qs = append(qs, qm[fmt.Sprintf("%s/%s", org, repo)]...)
   231  	return qs
   232  }
   233  
   234  // Validate returns an error if the query has any errors.
   235  //
   236  // Examples include:
   237  // * an org name that is empty or includes a /
   238  // * repos that are not org/repo
   239  // * a label that is in both the labels and missing_labels section
   240  // * a branch that is in both included and excluded branch set.
   241  func (tq *TideQuery) Validate() error {
   242  	for o := range tq.Orgs {
   243  		if strings.Contains(tq.Orgs[o], "/") {
   244  			return fmt.Errorf("orgs[%d]: %q contains a '/' which is not valid", o, tq.Orgs[o])
   245  		}
   246  		if len(tq.Orgs[o]) == 0 {
   247  			return fmt.Errorf("orgs[%d]: is an empty string", o)
   248  		}
   249  	}
   250  
   251  	for r := range tq.Repos {
   252  		parts := strings.Split(tq.Repos[r], "/")
   253  		if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
   254  			return fmt.Errorf("repos[%d]: %q is not of the form \"org/repo\"", r, tq.Repos[r])
   255  		}
   256  		for o := range tq.Orgs {
   257  			if tq.Orgs[o] == parts[0] {
   258  				return fmt.Errorf("repos[%d]: %q is already included via orgs[%d]: %q", r, tq.Repos[r], o, tq.Orgs[o])
   259  			}
   260  		}
   261  	}
   262  
   263  	if invalids := sets.NewString(tq.Labels...).Intersection(sets.NewString(tq.MissingLabels...)); len(invalids) > 0 {
   264  		return fmt.Errorf("the labels: %q are both required and forbidden", invalids.List())
   265  	}
   266  
   267  	// Warnings
   268  	if len(tq.ExcludedBranches) > 0 && len(tq.IncludedBranches) > 0 {
   269  		logrus.Warning("Smell: Both included and excluded branches are specified (excluded branches have no effect).")
   270  	}
   271  
   272  	return nil
   273  }
   274  
   275  // Validate returns an error if any contexts are both required and optional.
   276  func (cp *TideContextPolicy) Validate() error {
   277  	inter := sets.NewString(cp.RequiredContexts...).Intersection(sets.NewString(cp.OptionalContexts...))
   278  	if inter.Len() > 0 {
   279  		return fmt.Errorf("contexts %s are defined has required and optional", strings.Join(inter.List(), ", "))
   280  	}
   281  	return nil
   282  }
   283  
   284  func mergeTideContextPolicy(a, b TideContextPolicy) TideContextPolicy {
   285  	mergeBool := func(a, b *bool) *bool {
   286  		if b == nil {
   287  			return a
   288  		}
   289  		return b
   290  	}
   291  	c := TideContextPolicy{}
   292  	c.FromBranchProtection = mergeBool(a.FromBranchProtection, b.FromBranchProtection)
   293  	c.SkipUnknownContexts = mergeBool(a.SkipUnknownContexts, b.SkipUnknownContexts)
   294  	required := sets.NewString(a.RequiredContexts...)
   295  	optional := sets.NewString(a.OptionalContexts...)
   296  	required.Insert(b.RequiredContexts...)
   297  	optional.Insert(b.OptionalContexts...)
   298  	if required.Len() > 0 {
   299  		c.RequiredContexts = required.List()
   300  	}
   301  	if optional.Len() > 0 {
   302  		c.OptionalContexts = optional.List()
   303  	}
   304  	return c
   305  }
   306  
   307  func parseTideContextPolicyOptions(org, repo, branch string, options TideContextPolicyOptions) TideContextPolicy {
   308  	option := options.TideContextPolicy
   309  	if o, ok := options.Orgs[org]; ok {
   310  		option = mergeTideContextPolicy(option, o.TideContextPolicy)
   311  		if r, ok := o.Repos[repo]; ok {
   312  			option = mergeTideContextPolicy(option, r.TideContextPolicy)
   313  			if b, ok := r.Branches[branch]; ok {
   314  				option = mergeTideContextPolicy(option, b)
   315  			}
   316  		}
   317  	}
   318  	return option
   319  }
   320  
   321  // GetTideContextPolicy parses the prow config to find context merge options.
   322  // If none are set, it will use the prow jobs configured and use the default github combined status.
   323  // Otherwise if set it will use the branch protection setting, or the listed jobs.
   324  func (c Config) GetTideContextPolicy(org, repo, branch string) (*TideContextPolicy, error) {
   325  	options := parseTideContextPolicyOptions(org, repo, branch, c.Tide.ContextOptions)
   326  	// Adding required and optional contexts from options
   327  	required := sets.NewString(options.RequiredContexts...)
   328  	optional := sets.NewString(options.OptionalContexts...)
   329  
   330  	// automatically generate required and optional entries for Prow Jobs
   331  	prowRequired, prowOptional := BranchRequirements(org, repo, branch, c.Presubmits)
   332  	required.Insert(prowRequired...)
   333  	optional.Insert(prowOptional...)
   334  
   335  	// Using Branch protection configuration
   336  	if options.FromBranchProtection != nil && *options.FromBranchProtection {
   337  		bp, err := c.GetBranchProtection(org, repo, branch)
   338  		if err != nil {
   339  			logrus.WithError(err).Warningf("Error getting branch protection for %s/%s+%s", org, repo, branch)
   340  		} else if bp == nil {
   341  			logrus.Warningf("branch protection not set for %s/%s+%s", org, repo, branch)
   342  		} else if bp.Protect != nil && *bp.Protect {
   343  			required.Insert(bp.RequiredStatusChecks.Contexts...)
   344  		}
   345  	}
   346  
   347  	t := &TideContextPolicy{
   348  		RequiredContexts:    required.List(),
   349  		OptionalContexts:    optional.List(),
   350  		SkipUnknownContexts: options.SkipUnknownContexts,
   351  	}
   352  	if err := t.Validate(); err != nil {
   353  		return t, err
   354  	}
   355  	return t, nil
   356  }
   357  
   358  // IsOptional checks whether a context can be ignored.
   359  // Will return true if
   360  // - context is registered as optional
   361  // - required contexts are registered and the context provided is not required
   362  // Will return false otherwise. Every context is required.
   363  func (cp *TideContextPolicy) IsOptional(c string) bool {
   364  	if sets.NewString(cp.OptionalContexts...).Has(c) {
   365  		return true
   366  	}
   367  	if sets.NewString(cp.RequiredContexts...).Has(c) {
   368  		return false
   369  	}
   370  	if cp.SkipUnknownContexts != nil && *cp.SkipUnknownContexts {
   371  		return true
   372  	}
   373  	return false
   374  }
   375  
   376  // MissingRequiredContexts discard the optional contexts and only look of extra required contexts that are not provided.
   377  func (cp *TideContextPolicy) MissingRequiredContexts(contexts []string) []string {
   378  	if len(cp.RequiredContexts) == 0 {
   379  		return nil
   380  	}
   381  	existingContexts := sets.NewString()
   382  	for _, c := range contexts {
   383  		existingContexts.Insert(c)
   384  	}
   385  	var missingContexts []string
   386  	for c := range sets.NewString(cp.RequiredContexts...).Difference(existingContexts) {
   387  		missingContexts = append(missingContexts, c)
   388  	}
   389  	return missingContexts
   390  }