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 }