tide.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. /*
  2. Copyright 2017 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package config
  14. import (
  15. "errors"
  16. "fmt"
  17. "github.com/xanzy/go-gitlab"
  18. "strings"
  19. "sync"
  20. "time"
  21. "github.com/sirupsen/logrus"
  22. "k8s.io/apimachinery/pkg/util/sets"
  23. "k8s.io/test-infra/prow/gitserver"
  24. )
  25. // TideQueries is a TideQuery slice.
  26. type TideQueries []TideQuery
  27. // TideContextPolicy configures options about how to handle various contexts.
  28. type TideContextPolicy struct {
  29. // whether to consider unknown contexts optional (skip) or required.
  30. SkipUnknownContexts *bool `json:"skip-unknown-contexts,omitempty"`
  31. RequiredContexts []string `json:"required-contexts,omitempty"`
  32. OptionalContexts []string `json:"optional-contexts,omitempty"`
  33. // Infer required and optional jobs from Branch Protection configuration
  34. FromBranchProtection *bool `json:"from-branch-protection,omitempty"`
  35. }
  36. // TideOrgContextPolicy overrides the policy for an org, and any repo overrides.
  37. type TideOrgContextPolicy struct {
  38. TideContextPolicy
  39. Repos map[string]TideRepoContextPolicy `json:"repos,omitempty"`
  40. }
  41. // TideRepoContextPolicy overrides the policy for repo, and any branch overrides.
  42. type TideRepoContextPolicy struct {
  43. TideContextPolicy
  44. Branches map[string]TideContextPolicy `json:"branches,omitempty"`
  45. }
  46. // TideContextPolicyOptions holds the default policy, and any org overrides.
  47. type TideContextPolicyOptions struct {
  48. TideContextPolicy
  49. // Github Orgs
  50. Orgs map[string]TideOrgContextPolicy `json:"orgs,omitempty"`
  51. }
  52. // Tide is config for the tide pool.
  53. type Tide struct {
  54. // SyncPeriodString compiles into SyncPeriod at load time.
  55. SyncPeriodString string `json:"sync_period,omitempty"`
  56. // SyncPeriod specifies how often Tide will sync jobs with Github. Defaults to 1m.
  57. SyncPeriod time.Duration `json:"-"`
  58. // StatusUpdatePeriodString compiles into StatusUpdatePeriod at load time.
  59. StatusUpdatePeriodString string `json:"status_update_period,omitempty"`
  60. // StatusUpdatePeriod specifies how often Tide will update Github status contexts.
  61. // Defaults to the value of SyncPeriod.
  62. StatusUpdatePeriod time.Duration `json:"-"`
  63. // Queries represents a list of GitHub search queries that collectively
  64. // specify the set of PRs that meet merge requirements.
  65. Queries TideQueries `json:"queries,omitempty"`
  66. // A key/value pair of an org/repo as the key and merge method to override
  67. // the default method of merge. Valid options are squash, rebase, and merge.
  68. MergeType map[string]gitserver.PullRequestMergeType `json:"merge_method,omitempty"`
  69. // URL for tide status contexts.
  70. // We can consider allowing this to be set separately for separate repos, or
  71. // allowing it to be a template.
  72. TargetURL string `json:"target_url,omitempty"`
  73. // PRStatusBaseURL is the base URL for the PR status page.
  74. // This is used to link to a merge requirements overview
  75. // in the tide status context.
  76. PRStatusBaseURL string `json:"pr_status_base_url,omitempty"`
  77. // BlockerLabel is an optional label that is used to identify merge blocking
  78. // Github issues.
  79. // Leave this blank to disable this feature and save 1 API token per sync loop.
  80. BlockerLabel string `json:"blocker_label,omitempty"`
  81. // SquashLabel is an optional label that is used to identify PRs that should
  82. // always be squash merged.
  83. // Leave this blank to disable this feature.
  84. SquashLabel string `json:"squash_label,omitempty"`
  85. // MaxGoroutines is the maximum number of goroutines spawned inside the
  86. // controller to handle org/repo:branch pools. Defaults to 20. Needs to be a
  87. // positive number.
  88. MaxGoroutines int `json:"max_goroutines,omitempty"`
  89. // TideContextPolicyOptions defines merge options for context. If not set it will infer
  90. // the required and optional contexts from the prow jobs configured and use the github
  91. // combined status; otherwise it may apply the branch protection setting or let user
  92. // define their own options in case branch protection is not used.
  93. ContextOptions TideContextPolicyOptions `json:"context_options,omitempty"`
  94. }
  95. // MergeMethod returns the merge method to use for a repo. The default of merge is
  96. // returned when not overridden.
  97. func (t *Tide) MergeMethod(org, repo string) gitserver.PullRequestMergeType {
  98. name := org + "/" + repo
  99. v, ok := t.MergeType[name]
  100. if !ok {
  101. if ov, found := t.MergeType[org]; found {
  102. return ov
  103. }
  104. return gitserver.MergeMerge
  105. }
  106. return v
  107. }
  108. // TideQuery is turned into a GitHub search query. See the docs for details:
  109. // https://help.github.com/articles/searching-issues-and-pull-requests/
  110. type TideQuery struct {
  111. Orgs []string `json:"orgs,omitempty"`
  112. Repos []string `json:"repos,omitempty"`
  113. ExcludedRepos []string `json:"excludedRepos,omitempty"`
  114. ExcludedBranches []string `json:"excludedBranches,omitempty"`
  115. IncludedBranches []string `json:"includedBranches,omitempty"`
  116. Labels []string `json:"labels,omitempty"`
  117. MissingLabels []string `json:"missingLabels,omitempty"`
  118. Milestone string `json:"milestone,omitempty"`
  119. ReviewApprovedRequired bool `json:"reviewApprovedRequired,omitempty"`
  120. }
  121. // Query returns the corresponding github search string for the tide query.
  122. func (tq *TideQuery) Query() string {
  123. toks := []string{"is:pr", "state:open"}
  124. for _, o := range tq.Orgs {
  125. toks = append(toks, fmt.Sprintf("org:\"%s\"", o))
  126. }
  127. for _, r := range tq.Repos {
  128. toks = append(toks, fmt.Sprintf("repo:\"%s\"", r))
  129. }
  130. for _, r := range tq.ExcludedRepos {
  131. toks = append(toks, fmt.Sprintf("-repo:\"%s\"", r))
  132. }
  133. for _, b := range tq.ExcludedBranches {
  134. toks = append(toks, fmt.Sprintf("-base:\"%s\"", b))
  135. }
  136. for _, b := range tq.IncludedBranches {
  137. toks = append(toks, fmt.Sprintf("base:\"%s\"", b))
  138. }
  139. for _, l := range tq.Labels {
  140. toks = append(toks, fmt.Sprintf("label:\"%s\"", l))
  141. }
  142. for _, l := range tq.MissingLabels {
  143. toks = append(toks, fmt.Sprintf("-label:\"%s\"", l))
  144. }
  145. if tq.Milestone != "" {
  146. toks = append(toks, fmt.Sprintf("milestone:\"%s\"", tq.Milestone))
  147. }
  148. if tq.ReviewApprovedRequired {
  149. toks = append(toks, "review:approved")
  150. }
  151. return strings.Join(toks, " ")
  152. }
  153. func (tq *TideQuery) ListProjectMergeRequestsOptions(start, end *time.Time) *gitlab.ListProjectMergeRequestsOptions {
  154. opened := "opened"
  155. options := &gitlab.ListProjectMergeRequestsOptions{
  156. State: &opened,
  157. CreatedBefore: start,
  158. CreatedAfter: end,
  159. }
  160. if len(tq.Labels) > 0 {
  161. options.Labels = gitlab.Labels(tq.Labels)
  162. }
  163. if tq.Milestone != "" {
  164. options.Milestone = &tq.Milestone
  165. }
  166. return options
  167. }
  168. func (tq *TideQuery) ListProjectIssuesOptions(start, end *time.Time) *gitlab.ListProjectIssuesOptions {
  169. opened := "opened"
  170. options := &gitlab.ListProjectIssuesOptions{
  171. State: &opened,
  172. CreatedBefore: start,
  173. CreatedAfter: end,
  174. }
  175. if len(tq.Labels) > 0 {
  176. options.Labels = gitlab.Labels(tq.Labels)
  177. }
  178. if tq.Milestone != "" {
  179. options.Milestone = &tq.Milestone
  180. }
  181. return options
  182. }
  183. // Query returns the corresponding github search string for the tide query.
  184. func (tq *TideQuery) QueryGitlab() string {
  185. return ""
  186. }
  187. // ForRepo indicates if the tide query applies to the specified repo.
  188. func (tq TideQuery) ForRepo(org, repo string) bool {
  189. fullName := fmt.Sprintf("%s/%s", org, repo)
  190. for _, queryOrg := range tq.Orgs {
  191. if queryOrg != org {
  192. continue
  193. }
  194. // Check for repos excluded from the org.
  195. for _, excludedRepo := range tq.ExcludedRepos {
  196. if excludedRepo == fullName {
  197. return false
  198. }
  199. }
  200. return true
  201. }
  202. for _, queryRepo := range tq.Repos {
  203. if queryRepo == fullName {
  204. return true
  205. }
  206. }
  207. return false
  208. }
  209. func reposInOrg(org string, repos []string) []string {
  210. prefix := org + "/"
  211. var res []string
  212. for _, repo := range repos {
  213. if strings.HasPrefix(repo, prefix) {
  214. res = append(res, repo)
  215. }
  216. }
  217. return res
  218. }
  219. // OrgExceptionsAndRepos determines which orgs and repos a set of queries cover.
  220. // Output is returned as a mapping from 'included org'->'repos excluded in the org'
  221. // and a set of included repos.
  222. func (tqs TideQueries) OrgExceptionsAndRepos() (map[string]sets.String, sets.String) {
  223. orgs := make(map[string]sets.String)
  224. for i := range tqs {
  225. for _, org := range tqs[i].Orgs {
  226. applicableRepos := sets.NewString(reposInOrg(org, tqs[i].ExcludedRepos)...)
  227. if excepts, ok := orgs[org]; !ok {
  228. // We have not seen this org so the exceptions are just applicable
  229. // members of 'excludedRepos'.
  230. orgs[org] = applicableRepos
  231. } else {
  232. // We have seen this org so the exceptions are the applicable
  233. // members of 'excludedRepos' intersected with existing exceptions.
  234. orgs[org] = excepts.Intersection(applicableRepos)
  235. }
  236. }
  237. }
  238. repos := sets.NewString()
  239. for i := range tqs {
  240. repos.Insert(tqs[i].Repos...)
  241. }
  242. // Remove any org exceptions that are explicitly included in a different query.
  243. reposList := repos.UnsortedList()
  244. for _, excepts := range orgs {
  245. excepts.Delete(reposList...)
  246. }
  247. return orgs, repos
  248. }
  249. // QueryMap is a struct mapping from "org/repo" -> TideQueries that
  250. // apply to that org or repo. It is lazily populated, but threadsafe.
  251. type QueryMap struct {
  252. queries TideQueries
  253. cache map[string]TideQueries
  254. sync.Mutex
  255. }
  256. // QueryMap creates a QueryMap from TideQueries
  257. func (tqs TideQueries) QueryMap() *QueryMap {
  258. return &QueryMap{
  259. queries: tqs,
  260. cache: make(map[string]TideQueries),
  261. }
  262. }
  263. // ForRepo returns the tide queries that apply to a repo.
  264. func (qm *QueryMap) ForRepo(org, repo string) TideQueries {
  265. res := TideQueries(nil)
  266. fullName := fmt.Sprintf("%s/%s", org, repo)
  267. qm.Lock()
  268. defer qm.Unlock()
  269. if qs, ok := qm.cache[fullName]; ok {
  270. return append(res, qs...) // Return a copy.
  271. }
  272. // Cache miss. Need to determine relevant queries.
  273. for _, query := range qm.queries {
  274. if query.ForRepo(org, repo) {
  275. res = append(res, query)
  276. }
  277. }
  278. qm.cache[fullName] = res
  279. return res
  280. }
  281. // Validate returns an error if the query has any errors.
  282. //
  283. // Examples include:
  284. // * an org name that is empty or includes a /
  285. // * repos that are not org/repo
  286. // * a label that is in both the labels and missing_labels section
  287. // * a branch that is in both included and excluded branch set.
  288. func (tq *TideQuery) Validate() error {
  289. duplicates := func(field string, list []string) error {
  290. dups := sets.NewString()
  291. seen := sets.NewString()
  292. for _, elem := range list {
  293. if seen.Has(elem) {
  294. dups.Insert(elem)
  295. } else {
  296. seen.Insert(elem)
  297. }
  298. }
  299. dupCount := len(list) - seen.Len()
  300. if dupCount == 0 {
  301. return nil
  302. }
  303. return fmt.Errorf("%q contains %d duplicate entries: %s", field, dupCount, strings.Join(dups.List(), ", "))
  304. }
  305. orgs := sets.NewString()
  306. for o := range tq.Orgs {
  307. if strings.Contains(tq.Orgs[o], "/") {
  308. return fmt.Errorf("orgs[%d]: %q contains a '/' which is not valid", o, tq.Orgs[o])
  309. }
  310. if len(tq.Orgs[o]) == 0 {
  311. return fmt.Errorf("orgs[%d]: is an empty string", o)
  312. }
  313. orgs.Insert(tq.Orgs[o])
  314. }
  315. if err := duplicates("orgs", tq.Orgs); err != nil {
  316. return err
  317. }
  318. for r := range tq.Repos {
  319. parts := strings.Split(tq.Repos[r], "/")
  320. if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
  321. return fmt.Errorf("repos[%d]: %q is not of the form \"org/repo\"", r, tq.Repos[r])
  322. }
  323. if orgs.Has(parts[0]) {
  324. return fmt.Errorf("repos[%d]: %q is already included via org: %q", r, tq.Repos[r], parts[0])
  325. }
  326. }
  327. if err := duplicates("repos", tq.Repos); err != nil {
  328. return err
  329. }
  330. if len(tq.Orgs) == 0 && len(tq.Repos) == 0 {
  331. return errors.New("'orgs' and 'repos' cannot both be empty")
  332. }
  333. for er := range tq.ExcludedRepos {
  334. parts := strings.Split(tq.ExcludedRepos[er], "/")
  335. if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
  336. return fmt.Errorf("excludedRepos[%d]: %q is not of the form \"org/repo\"", er, tq.ExcludedRepos[er])
  337. }
  338. if !orgs.Has(parts[0]) {
  339. return fmt.Errorf("excludedRepos[%d]: %q has no effect because org %q is not included", er, tq.ExcludedRepos[er], parts[0])
  340. }
  341. // Note: At this point we also know that this excludedRepo is not found in 'repos'.
  342. }
  343. if err := duplicates("excludedRepos", tq.ExcludedRepos); err != nil {
  344. return err
  345. }
  346. if invalids := sets.NewString(tq.Labels...).Intersection(sets.NewString(tq.MissingLabels...)); len(invalids) > 0 {
  347. return fmt.Errorf("the labels: %q are both required and forbidden", invalids.List())
  348. }
  349. if err := duplicates("labels", tq.Labels); err != nil {
  350. return err
  351. }
  352. if err := duplicates("missingLabels", tq.MissingLabels); err != nil {
  353. return err
  354. }
  355. if len(tq.ExcludedBranches) > 0 && len(tq.IncludedBranches) > 0 {
  356. return errors.New("both 'includedBranches' and 'excludedBranches' are specified ('excludedBranches' have no effect)")
  357. }
  358. if err := duplicates("includedBranches", tq.IncludedBranches); err != nil {
  359. return err
  360. }
  361. if err := duplicates("excludedBranches", tq.ExcludedBranches); err != nil {
  362. return err
  363. }
  364. return nil
  365. }
  366. // Validate returns an error if any contexts are both required and optional.
  367. func (cp *TideContextPolicy) Validate() error {
  368. inter := sets.NewString(cp.RequiredContexts...).Intersection(sets.NewString(cp.OptionalContexts...))
  369. if inter.Len() > 0 {
  370. return fmt.Errorf("contexts %s are defined has required and optional", strings.Join(inter.List(), ", "))
  371. }
  372. return nil
  373. }
  374. func mergeTideContextPolicy(a, b TideContextPolicy) TideContextPolicy {
  375. mergeBool := func(a, b *bool) *bool {
  376. if b == nil {
  377. return a
  378. }
  379. return b
  380. }
  381. c := TideContextPolicy{}
  382. c.FromBranchProtection = mergeBool(a.FromBranchProtection, b.FromBranchProtection)
  383. c.SkipUnknownContexts = mergeBool(a.SkipUnknownContexts, b.SkipUnknownContexts)
  384. required := sets.NewString(a.RequiredContexts...)
  385. optional := sets.NewString(a.OptionalContexts...)
  386. required.Insert(b.RequiredContexts...)
  387. optional.Insert(b.OptionalContexts...)
  388. if required.Len() > 0 {
  389. c.RequiredContexts = required.List()
  390. }
  391. if optional.Len() > 0 {
  392. c.OptionalContexts = optional.List()
  393. }
  394. return c
  395. }
  396. func parseTideContextPolicyOptions(org, repo, branch string, options TideContextPolicyOptions) TideContextPolicy {
  397. option := options.TideContextPolicy
  398. if o, ok := options.Orgs[org]; ok {
  399. option = mergeTideContextPolicy(option, o.TideContextPolicy)
  400. if r, ok := o.Repos[repo]; ok {
  401. option = mergeTideContextPolicy(option, r.TideContextPolicy)
  402. if b, ok := r.Branches[branch]; ok {
  403. option = mergeTideContextPolicy(option, b)
  404. }
  405. }
  406. }
  407. return option
  408. }
  409. // GetTideContextPolicy parses the prow config to find context merge options.
  410. // If none are set, it will use the prow jobs configured and use the default github combined status.
  411. // Otherwise if set it will use the branch protection setting, or the listed jobs.
  412. func (c Config) GetTideContextPolicy(org, repo, branch string) (*TideContextPolicy, error) {
  413. options := parseTideContextPolicyOptions(org, repo, branch, c.Tide.ContextOptions)
  414. // Adding required and optional contexts from options
  415. required := sets.NewString(options.RequiredContexts...)
  416. optional := sets.NewString(options.OptionalContexts...)
  417. // automatically generate required and optional entries for Prow Jobs
  418. prowRequired, prowOptional := BranchRequirements(org, repo, branch, c.Presubmits)
  419. required.Insert(prowRequired...)
  420. optional.Insert(prowOptional...)
  421. // Using Branch protection configuration
  422. if options.FromBranchProtection != nil && *options.FromBranchProtection {
  423. bp, err := c.GetBranchProtection(org, repo, branch)
  424. if err != nil {
  425. logrus.WithError(err).Warningf("Error getting branch protection for %s/%s+%s", org, repo, branch)
  426. } else if bp == nil {
  427. logrus.Warningf("branch protection not set for %s/%s+%s", org, repo, branch)
  428. } else if bp.Protect != nil && *bp.Protect {
  429. required.Insert(bp.RequiredStatusChecks.Contexts...)
  430. }
  431. }
  432. t := &TideContextPolicy{
  433. RequiredContexts: required.List(),
  434. OptionalContexts: optional.List(),
  435. SkipUnknownContexts: options.SkipUnknownContexts,
  436. }
  437. if err := t.Validate(); err != nil {
  438. return t, err
  439. }
  440. return t, nil
  441. }
  442. // IsOptional checks whether a context can be ignored.
  443. // Will return true if
  444. // - context is registered as optional
  445. // - required contexts are registered and the context provided is not required
  446. // Will return false otherwise. Every context is required.
  447. func (cp *TideContextPolicy) IsOptional(c string) bool {
  448. if sets.NewString(cp.OptionalContexts...).Has(c) {
  449. return true
  450. }
  451. if sets.NewString(cp.RequiredContexts...).Has(c) {
  452. return false
  453. }
  454. if cp.SkipUnknownContexts != nil && *cp.SkipUnknownContexts {
  455. return true
  456. }
  457. return false
  458. }
  459. // MissingRequiredContexts discard the optional contexts and only look of extra required contexts that are not provided.
  460. func (cp *TideContextPolicy) MissingRequiredContexts(contexts []string) []string {
  461. if len(cp.RequiredContexts) == 0 {
  462. return nil
  463. }
  464. existingContexts := sets.NewString()
  465. for _, c := range contexts {
  466. existingContexts.Insert(c)
  467. }
  468. var missingContexts []string
  469. for c := range sets.NewString(cp.RequiredContexts...).Difference(existingContexts) {
  470. missingContexts = append(missingContexts, c)
  471. }
  472. return missingContexts
  473. }