mirror of
https://github.com/iLoveElysia/openbilibili.git
synced 2026-03-14 05:46:26 -05:00
531 lines
17 KiB
Go
531 lines
17 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
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"github.com/xanzy/go-gitlab"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
"k8s.io/test-infra/prow/gitserver"
|
|
)
|
|
|
|
// TideQueries is a TideQuery slice.
|
|
type TideQueries []TideQuery
|
|
|
|
// TideContextPolicy configures options about how to handle various contexts.
|
|
type TideContextPolicy struct {
|
|
// whether to consider unknown contexts optional (skip) or required.
|
|
SkipUnknownContexts *bool `json:"skip-unknown-contexts,omitempty"`
|
|
RequiredContexts []string `json:"required-contexts,omitempty"`
|
|
OptionalContexts []string `json:"optional-contexts,omitempty"`
|
|
// Infer required and optional jobs from Branch Protection configuration
|
|
FromBranchProtection *bool `json:"from-branch-protection,omitempty"`
|
|
}
|
|
|
|
// TideOrgContextPolicy overrides the policy for an org, and any repo overrides.
|
|
type TideOrgContextPolicy struct {
|
|
TideContextPolicy
|
|
Repos map[string]TideRepoContextPolicy `json:"repos,omitempty"`
|
|
}
|
|
|
|
// TideRepoContextPolicy overrides the policy for repo, and any branch overrides.
|
|
type TideRepoContextPolicy struct {
|
|
TideContextPolicy
|
|
Branches map[string]TideContextPolicy `json:"branches,omitempty"`
|
|
}
|
|
|
|
// TideContextPolicyOptions holds the default policy, and any org overrides.
|
|
type TideContextPolicyOptions struct {
|
|
TideContextPolicy
|
|
// Github Orgs
|
|
Orgs map[string]TideOrgContextPolicy `json:"orgs,omitempty"`
|
|
}
|
|
|
|
// Tide is config for the tide pool.
|
|
type Tide struct {
|
|
// SyncPeriodString compiles into SyncPeriod at load time.
|
|
SyncPeriodString string `json:"sync_period,omitempty"`
|
|
// SyncPeriod specifies how often Tide will sync jobs with Github. Defaults to 1m.
|
|
SyncPeriod time.Duration `json:"-"`
|
|
// StatusUpdatePeriodString compiles into StatusUpdatePeriod at load time.
|
|
StatusUpdatePeriodString string `json:"status_update_period,omitempty"`
|
|
// StatusUpdatePeriod specifies how often Tide will update Github status contexts.
|
|
// Defaults to the value of SyncPeriod.
|
|
StatusUpdatePeriod time.Duration `json:"-"`
|
|
// Queries represents a list of GitHub search queries that collectively
|
|
// specify the set of PRs that meet merge requirements.
|
|
Queries TideQueries `json:"queries,omitempty"`
|
|
|
|
// A key/value pair of an org/repo as the key and merge method to override
|
|
// the default method of merge. Valid options are squash, rebase, and merge.
|
|
MergeType map[string]gitserver.PullRequestMergeType `json:"merge_method,omitempty"`
|
|
|
|
// URL for tide status contexts.
|
|
// We can consider allowing this to be set separately for separate repos, or
|
|
// allowing it to be a template.
|
|
TargetURL string `json:"target_url,omitempty"`
|
|
|
|
// PRStatusBaseURL is the base URL for the PR status page.
|
|
// This is used to link to a merge requirements overview
|
|
// in the tide status context.
|
|
PRStatusBaseURL string `json:"pr_status_base_url,omitempty"`
|
|
|
|
// BlockerLabel is an optional label that is used to identify merge blocking
|
|
// Github issues.
|
|
// Leave this blank to disable this feature and save 1 API token per sync loop.
|
|
BlockerLabel string `json:"blocker_label,omitempty"`
|
|
|
|
// SquashLabel is an optional label that is used to identify PRs that should
|
|
// always be squash merged.
|
|
// Leave this blank to disable this feature.
|
|
SquashLabel string `json:"squash_label,omitempty"`
|
|
|
|
// MaxGoroutines is the maximum number of goroutines spawned inside the
|
|
// controller to handle org/repo:branch pools. Defaults to 20. Needs to be a
|
|
// positive number.
|
|
MaxGoroutines int `json:"max_goroutines,omitempty"`
|
|
|
|
// TideContextPolicyOptions defines merge options for context. If not set it will infer
|
|
// the required and optional contexts from the prow jobs configured and use the github
|
|
// combined status; otherwise it may apply the branch protection setting or let user
|
|
// define their own options in case branch protection is not used.
|
|
ContextOptions TideContextPolicyOptions `json:"context_options,omitempty"`
|
|
}
|
|
|
|
// MergeMethod returns the merge method to use for a repo. The default of merge is
|
|
// returned when not overridden.
|
|
func (t *Tide) MergeMethod(org, repo string) gitserver.PullRequestMergeType {
|
|
name := org + "/" + repo
|
|
|
|
v, ok := t.MergeType[name]
|
|
if !ok {
|
|
if ov, found := t.MergeType[org]; found {
|
|
return ov
|
|
}
|
|
|
|
return gitserver.MergeMerge
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
// TideQuery is turned into a GitHub search query. See the docs for details:
|
|
// https://help.github.com/articles/searching-issues-and-pull-requests/
|
|
type TideQuery struct {
|
|
Orgs []string `json:"orgs,omitempty"`
|
|
Repos []string `json:"repos,omitempty"`
|
|
ExcludedRepos []string `json:"excludedRepos,omitempty"`
|
|
|
|
ExcludedBranches []string `json:"excludedBranches,omitempty"`
|
|
IncludedBranches []string `json:"includedBranches,omitempty"`
|
|
|
|
Labels []string `json:"labels,omitempty"`
|
|
MissingLabels []string `json:"missingLabels,omitempty"`
|
|
|
|
Milestone string `json:"milestone,omitempty"`
|
|
|
|
ReviewApprovedRequired bool `json:"reviewApprovedRequired,omitempty"`
|
|
}
|
|
|
|
// Query returns the corresponding github search string for the tide query.
|
|
func (tq *TideQuery) Query() string {
|
|
toks := []string{"is:pr", "state:open"}
|
|
for _, o := range tq.Orgs {
|
|
toks = append(toks, fmt.Sprintf("org:\"%s\"", o))
|
|
}
|
|
for _, r := range tq.Repos {
|
|
toks = append(toks, fmt.Sprintf("repo:\"%s\"", r))
|
|
}
|
|
for _, r := range tq.ExcludedRepos {
|
|
toks = append(toks, fmt.Sprintf("-repo:\"%s\"", r))
|
|
}
|
|
for _, b := range tq.ExcludedBranches {
|
|
toks = append(toks, fmt.Sprintf("-base:\"%s\"", b))
|
|
}
|
|
for _, b := range tq.IncludedBranches {
|
|
toks = append(toks, fmt.Sprintf("base:\"%s\"", b))
|
|
}
|
|
for _, l := range tq.Labels {
|
|
toks = append(toks, fmt.Sprintf("label:\"%s\"", l))
|
|
}
|
|
for _, l := range tq.MissingLabels {
|
|
toks = append(toks, fmt.Sprintf("-label:\"%s\"", l))
|
|
}
|
|
if tq.Milestone != "" {
|
|
toks = append(toks, fmt.Sprintf("milestone:\"%s\"", tq.Milestone))
|
|
}
|
|
if tq.ReviewApprovedRequired {
|
|
toks = append(toks, "review:approved")
|
|
}
|
|
return strings.Join(toks, " ")
|
|
}
|
|
|
|
func (tq *TideQuery) ListProjectMergeRequestsOptions(start, end *time.Time) *gitlab.ListProjectMergeRequestsOptions {
|
|
opened := "opened"
|
|
options := &gitlab.ListProjectMergeRequestsOptions{
|
|
State: &opened,
|
|
CreatedBefore: start,
|
|
CreatedAfter: end,
|
|
}
|
|
if len(tq.Labels) > 0 {
|
|
options.Labels = gitlab.Labels(tq.Labels)
|
|
}
|
|
if tq.Milestone != "" {
|
|
options.Milestone = &tq.Milestone
|
|
}
|
|
return options
|
|
}
|
|
|
|
func (tq *TideQuery) ListProjectIssuesOptions(start, end *time.Time) *gitlab.ListProjectIssuesOptions {
|
|
opened := "opened"
|
|
options := &gitlab.ListProjectIssuesOptions{
|
|
State: &opened,
|
|
CreatedBefore: start,
|
|
CreatedAfter: end,
|
|
}
|
|
if len(tq.Labels) > 0 {
|
|
options.Labels = gitlab.Labels(tq.Labels)
|
|
}
|
|
if tq.Milestone != "" {
|
|
options.Milestone = &tq.Milestone
|
|
}
|
|
return options
|
|
}
|
|
|
|
// Query returns the corresponding github search string for the tide query.
|
|
func (tq *TideQuery) QueryGitlab() string {
|
|
return ""
|
|
}
|
|
|
|
// ForRepo indicates if the tide query applies to the specified repo.
|
|
func (tq TideQuery) ForRepo(org, repo string) bool {
|
|
fullName := fmt.Sprintf("%s/%s", org, repo)
|
|
for _, queryOrg := range tq.Orgs {
|
|
if queryOrg != org {
|
|
continue
|
|
}
|
|
// Check for repos excluded from the org.
|
|
for _, excludedRepo := range tq.ExcludedRepos {
|
|
if excludedRepo == fullName {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
for _, queryRepo := range tq.Repos {
|
|
if queryRepo == fullName {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func reposInOrg(org string, repos []string) []string {
|
|
prefix := org + "/"
|
|
var res []string
|
|
for _, repo := range repos {
|
|
if strings.HasPrefix(repo, prefix) {
|
|
res = append(res, repo)
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
// OrgExceptionsAndRepos determines which orgs and repos a set of queries cover.
|
|
// Output is returned as a mapping from 'included org'->'repos excluded in the org'
|
|
// and a set of included repos.
|
|
func (tqs TideQueries) OrgExceptionsAndRepos() (map[string]sets.String, sets.String) {
|
|
orgs := make(map[string]sets.String)
|
|
for i := range tqs {
|
|
for _, org := range tqs[i].Orgs {
|
|
applicableRepos := sets.NewString(reposInOrg(org, tqs[i].ExcludedRepos)...)
|
|
if excepts, ok := orgs[org]; !ok {
|
|
// We have not seen this org so the exceptions are just applicable
|
|
// members of 'excludedRepos'.
|
|
orgs[org] = applicableRepos
|
|
} else {
|
|
// We have seen this org so the exceptions are the applicable
|
|
// members of 'excludedRepos' intersected with existing exceptions.
|
|
orgs[org] = excepts.Intersection(applicableRepos)
|
|
}
|
|
}
|
|
}
|
|
repos := sets.NewString()
|
|
for i := range tqs {
|
|
repos.Insert(tqs[i].Repos...)
|
|
}
|
|
// Remove any org exceptions that are explicitly included in a different query.
|
|
reposList := repos.UnsortedList()
|
|
for _, excepts := range orgs {
|
|
excepts.Delete(reposList...)
|
|
}
|
|
return orgs, repos
|
|
}
|
|
|
|
// QueryMap is a struct mapping from "org/repo" -> TideQueries that
|
|
// apply to that org or repo. It is lazily populated, but threadsafe.
|
|
type QueryMap struct {
|
|
queries TideQueries
|
|
|
|
cache map[string]TideQueries
|
|
sync.Mutex
|
|
}
|
|
|
|
// QueryMap creates a QueryMap from TideQueries
|
|
func (tqs TideQueries) QueryMap() *QueryMap {
|
|
return &QueryMap{
|
|
queries: tqs,
|
|
cache: make(map[string]TideQueries),
|
|
}
|
|
}
|
|
|
|
// ForRepo returns the tide queries that apply to a repo.
|
|
func (qm *QueryMap) ForRepo(org, repo string) TideQueries {
|
|
res := TideQueries(nil)
|
|
fullName := fmt.Sprintf("%s/%s", org, repo)
|
|
|
|
qm.Lock()
|
|
defer qm.Unlock()
|
|
|
|
if qs, ok := qm.cache[fullName]; ok {
|
|
return append(res, qs...) // Return a copy.
|
|
}
|
|
// Cache miss. Need to determine relevant queries.
|
|
|
|
for _, query := range qm.queries {
|
|
if query.ForRepo(org, repo) {
|
|
res = append(res, query)
|
|
}
|
|
}
|
|
qm.cache[fullName] = res
|
|
return res
|
|
}
|
|
|
|
// Validate returns an error if the query has any errors.
|
|
//
|
|
// Examples include:
|
|
// * an org name that is empty or includes a /
|
|
// * repos that are not org/repo
|
|
// * a label that is in both the labels and missing_labels section
|
|
// * a branch that is in both included and excluded branch set.
|
|
func (tq *TideQuery) Validate() error {
|
|
duplicates := func(field string, list []string) error {
|
|
dups := sets.NewString()
|
|
seen := sets.NewString()
|
|
for _, elem := range list {
|
|
if seen.Has(elem) {
|
|
dups.Insert(elem)
|
|
} else {
|
|
seen.Insert(elem)
|
|
}
|
|
}
|
|
dupCount := len(list) - seen.Len()
|
|
if dupCount == 0 {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("%q contains %d duplicate entries: %s", field, dupCount, strings.Join(dups.List(), ", "))
|
|
}
|
|
|
|
orgs := sets.NewString()
|
|
for o := range tq.Orgs {
|
|
if strings.Contains(tq.Orgs[o], "/") {
|
|
return fmt.Errorf("orgs[%d]: %q contains a '/' which is not valid", o, tq.Orgs[o])
|
|
}
|
|
if len(tq.Orgs[o]) == 0 {
|
|
return fmt.Errorf("orgs[%d]: is an empty string", o)
|
|
}
|
|
orgs.Insert(tq.Orgs[o])
|
|
}
|
|
if err := duplicates("orgs", tq.Orgs); err != nil {
|
|
return err
|
|
}
|
|
|
|
for r := range tq.Repos {
|
|
parts := strings.Split(tq.Repos[r], "/")
|
|
if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
|
|
return fmt.Errorf("repos[%d]: %q is not of the form \"org/repo\"", r, tq.Repos[r])
|
|
}
|
|
if orgs.Has(parts[0]) {
|
|
return fmt.Errorf("repos[%d]: %q is already included via org: %q", r, tq.Repos[r], parts[0])
|
|
}
|
|
}
|
|
if err := duplicates("repos", tq.Repos); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(tq.Orgs) == 0 && len(tq.Repos) == 0 {
|
|
return errors.New("'orgs' and 'repos' cannot both be empty")
|
|
}
|
|
|
|
for er := range tq.ExcludedRepos {
|
|
parts := strings.Split(tq.ExcludedRepos[er], "/")
|
|
if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
|
|
return fmt.Errorf("excludedRepos[%d]: %q is not of the form \"org/repo\"", er, tq.ExcludedRepos[er])
|
|
}
|
|
if !orgs.Has(parts[0]) {
|
|
return fmt.Errorf("excludedRepos[%d]: %q has no effect because org %q is not included", er, tq.ExcludedRepos[er], parts[0])
|
|
}
|
|
// Note: At this point we also know that this excludedRepo is not found in 'repos'.
|
|
}
|
|
if err := duplicates("excludedRepos", tq.ExcludedRepos); err != nil {
|
|
return err
|
|
}
|
|
|
|
if invalids := sets.NewString(tq.Labels...).Intersection(sets.NewString(tq.MissingLabels...)); len(invalids) > 0 {
|
|
return fmt.Errorf("the labels: %q are both required and forbidden", invalids.List())
|
|
}
|
|
if err := duplicates("labels", tq.Labels); err != nil {
|
|
return err
|
|
}
|
|
if err := duplicates("missingLabels", tq.MissingLabels); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(tq.ExcludedBranches) > 0 && len(tq.IncludedBranches) > 0 {
|
|
return errors.New("both 'includedBranches' and 'excludedBranches' are specified ('excludedBranches' have no effect)")
|
|
}
|
|
if err := duplicates("includedBranches", tq.IncludedBranches); err != nil {
|
|
return err
|
|
}
|
|
if err := duplicates("excludedBranches", tq.ExcludedBranches); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Validate returns an error if any contexts are both required and optional.
|
|
func (cp *TideContextPolicy) Validate() error {
|
|
inter := sets.NewString(cp.RequiredContexts...).Intersection(sets.NewString(cp.OptionalContexts...))
|
|
if inter.Len() > 0 {
|
|
return fmt.Errorf("contexts %s are defined has required and optional", strings.Join(inter.List(), ", "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func mergeTideContextPolicy(a, b TideContextPolicy) TideContextPolicy {
|
|
mergeBool := func(a, b *bool) *bool {
|
|
if b == nil {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
c := TideContextPolicy{}
|
|
c.FromBranchProtection = mergeBool(a.FromBranchProtection, b.FromBranchProtection)
|
|
c.SkipUnknownContexts = mergeBool(a.SkipUnknownContexts, b.SkipUnknownContexts)
|
|
required := sets.NewString(a.RequiredContexts...)
|
|
optional := sets.NewString(a.OptionalContexts...)
|
|
required.Insert(b.RequiredContexts...)
|
|
optional.Insert(b.OptionalContexts...)
|
|
if required.Len() > 0 {
|
|
c.RequiredContexts = required.List()
|
|
}
|
|
if optional.Len() > 0 {
|
|
c.OptionalContexts = optional.List()
|
|
}
|
|
return c
|
|
}
|
|
|
|
func parseTideContextPolicyOptions(org, repo, branch string, options TideContextPolicyOptions) TideContextPolicy {
|
|
option := options.TideContextPolicy
|
|
if o, ok := options.Orgs[org]; ok {
|
|
option = mergeTideContextPolicy(option, o.TideContextPolicy)
|
|
if r, ok := o.Repos[repo]; ok {
|
|
option = mergeTideContextPolicy(option, r.TideContextPolicy)
|
|
if b, ok := r.Branches[branch]; ok {
|
|
option = mergeTideContextPolicy(option, b)
|
|
}
|
|
}
|
|
}
|
|
return option
|
|
}
|
|
|
|
// GetTideContextPolicy parses the prow config to find context merge options.
|
|
// If none are set, it will use the prow jobs configured and use the default github combined status.
|
|
// Otherwise if set it will use the branch protection setting, or the listed jobs.
|
|
func (c Config) GetTideContextPolicy(org, repo, branch string) (*TideContextPolicy, error) {
|
|
options := parseTideContextPolicyOptions(org, repo, branch, c.Tide.ContextOptions)
|
|
// Adding required and optional contexts from options
|
|
required := sets.NewString(options.RequiredContexts...)
|
|
optional := sets.NewString(options.OptionalContexts...)
|
|
|
|
// automatically generate required and optional entries for Prow Jobs
|
|
prowRequired, prowOptional := BranchRequirements(org, repo, branch, c.Presubmits)
|
|
required.Insert(prowRequired...)
|
|
optional.Insert(prowOptional...)
|
|
|
|
// Using Branch protection configuration
|
|
if options.FromBranchProtection != nil && *options.FromBranchProtection {
|
|
bp, err := c.GetBranchProtection(org, repo, branch)
|
|
if err != nil {
|
|
logrus.WithError(err).Warningf("Error getting branch protection for %s/%s+%s", org, repo, branch)
|
|
} else if bp == nil {
|
|
logrus.Warningf("branch protection not set for %s/%s+%s", org, repo, branch)
|
|
} else if bp.Protect != nil && *bp.Protect {
|
|
required.Insert(bp.RequiredStatusChecks.Contexts...)
|
|
}
|
|
}
|
|
|
|
t := &TideContextPolicy{
|
|
RequiredContexts: required.List(),
|
|
OptionalContexts: optional.List(),
|
|
SkipUnknownContexts: options.SkipUnknownContexts,
|
|
}
|
|
if err := t.Validate(); err != nil {
|
|
return t, err
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// IsOptional checks whether a context can be ignored.
|
|
// Will return true if
|
|
// - context is registered as optional
|
|
// - required contexts are registered and the context provided is not required
|
|
// Will return false otherwise. Every context is required.
|
|
func (cp *TideContextPolicy) IsOptional(c string) bool {
|
|
if sets.NewString(cp.OptionalContexts...).Has(c) {
|
|
return true
|
|
}
|
|
if sets.NewString(cp.RequiredContexts...).Has(c) {
|
|
return false
|
|
}
|
|
if cp.SkipUnknownContexts != nil && *cp.SkipUnknownContexts {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// MissingRequiredContexts discard the optional contexts and only look of extra required contexts that are not provided.
|
|
func (cp *TideContextPolicy) MissingRequiredContexts(contexts []string) []string {
|
|
if len(cp.RequiredContexts) == 0 {
|
|
return nil
|
|
}
|
|
existingContexts := sets.NewString()
|
|
for _, c := range contexts {
|
|
existingContexts.Insert(c)
|
|
}
|
|
var missingContexts []string
|
|
for c := range sets.NewString(cp.RequiredContexts...).Difference(existingContexts) {
|
|
missingContexts = append(missingContexts, c)
|
|
}
|
|
return missingContexts
|
|
}
|