branch_protection.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. /*
  2. Copyright 2018 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/sirupsen/logrus"
  18. "k8s.io/apimachinery/pkg/util/sets"
  19. )
  20. // Policy for the config/org/repo/branch.
  21. // When merging policies, a nil value results in inheriting the parent policy.
  22. type Policy struct {
  23. deprecatedPolicy
  24. deprecatedWarning bool // true if a warning message was sent
  25. // Protect overrides whether branch protection is enabled if set.
  26. Protect *bool `json:"protect,omitempty"`
  27. // RequiredStatusChecks configures github contexts
  28. RequiredStatusChecks *ContextPolicy `json:"required_status_checks,omitempty"`
  29. // Admins overrides whether protections apply to admins if set.
  30. Admins *bool `json:"enforce_admins,omitempty"`
  31. // Restrictions limits who can merge
  32. Restrictions *Restrictions `json:"restrictions,omitempty"`
  33. // RequiredPullRequestReviews specifies github approval/review criteria.
  34. RequiredPullRequestReviews *ReviewPolicy `json:"required_pull_request_reviews,omitempty"`
  35. }
  36. // deprecatedPolicy deserializes fields that are no longer in use
  37. type deprecatedPolicy struct {
  38. DeprecatedProtect *bool `json:"protect-by-default,omitempty"`
  39. DeprecatedContexts []string `json:"require-contexts,omitempty"`
  40. DeprecatedPushers []string `json:"allow-push,omitempty"`
  41. }
  42. func (d deprecatedPolicy) defined() bool {
  43. return d.DeprecatedProtect != nil || d.DeprecatedContexts != nil || d.DeprecatedPushers != nil
  44. }
  45. func (p Policy) defined() bool {
  46. return p.Protect != nil || p.RequiredStatusChecks != nil || p.Admins != nil || p.Restrictions != nil || p.RequiredPullRequestReviews != nil
  47. }
  48. // HasProtect returns true if the policy or deprecated policy defines protection
  49. func (p Policy) HasProtect() bool {
  50. return p.Protect != nil || p.deprecatedPolicy.DeprecatedProtect != nil
  51. }
  52. // ContextPolicy configures required github contexts.
  53. // When merging policies, contexts are appended to context list from parent.
  54. // Strict determines whether merging to the branch invalidates existing contexts.
  55. type ContextPolicy struct {
  56. // Contexts appends required contexts that must be green to merge
  57. Contexts []string `json:"contexts,omitempty"`
  58. // Strict overrides whether new commits in the base branch require updating the PR if set
  59. Strict *bool `json:"strict,omitempty"`
  60. }
  61. // ReviewPolicy specifies github approval/review criteria.
  62. // Any nil values inherit the policy from the parent, otherwise bool/ints are overridden.
  63. // Non-empty lists are appended to parent lists.
  64. type ReviewPolicy struct {
  65. // Restrictions appends users/teams that are allowed to merge
  66. DismissalRestrictions *Restrictions `json:"dismissal_restrictions,omitempty"`
  67. // DismissStale overrides whether new commits automatically dismiss old reviews if set
  68. DismissStale *bool `json:"dismiss_stale_reviews,omitempty"`
  69. // RequireOwners overrides whether CODEOWNERS must approve PRs if set
  70. RequireOwners *bool `json:"require_code_owner_reviews,omitempty"`
  71. // Approvals overrides the number of approvals required if set (set to 0 to disable)
  72. Approvals *int `json:"required_approving_review_count,omitempty"`
  73. }
  74. // Restrictions limits who can merge
  75. // Users and Teams items are appended to parent lists.
  76. type Restrictions struct {
  77. Users []string `json:"users"`
  78. Teams []string `json:"teams"`
  79. }
  80. // selectInt returns the child if set, else parent
  81. func selectInt(parent, child *int) *int {
  82. if child != nil {
  83. return child
  84. }
  85. return parent
  86. }
  87. // selectBool returns the child argument if set, otherwise the parent
  88. func selectBool(parent, child *bool) *bool {
  89. if child != nil {
  90. return child
  91. }
  92. return parent
  93. }
  94. // unionStrings merges the parent and child items together
  95. func unionStrings(parent, child []string) []string {
  96. if child == nil {
  97. return parent
  98. }
  99. if parent == nil {
  100. return child
  101. }
  102. s := sets.NewString(parent...)
  103. s.Insert(child...)
  104. return s.List()
  105. }
  106. func mergeContextPolicy(parent, child *ContextPolicy) *ContextPolicy {
  107. if child == nil {
  108. return parent
  109. }
  110. if parent == nil {
  111. return child
  112. }
  113. return &ContextPolicy{
  114. Contexts: unionStrings(parent.Contexts, child.Contexts),
  115. Strict: selectBool(parent.Strict, child.Strict),
  116. }
  117. }
  118. func mergeReviewPolicy(parent, child *ReviewPolicy) *ReviewPolicy {
  119. if child == nil {
  120. return parent
  121. }
  122. if parent == nil {
  123. return child
  124. }
  125. return &ReviewPolicy{
  126. DismissalRestrictions: mergeRestrictions(parent.DismissalRestrictions, child.DismissalRestrictions),
  127. DismissStale: selectBool(parent.DismissStale, child.DismissStale),
  128. RequireOwners: selectBool(parent.RequireOwners, child.RequireOwners),
  129. Approvals: selectInt(parent.Approvals, child.Approvals),
  130. }
  131. }
  132. func mergeRestrictions(parent, child *Restrictions) *Restrictions {
  133. if child == nil {
  134. return parent
  135. }
  136. if parent == nil {
  137. return child
  138. }
  139. return &Restrictions{
  140. Users: unionStrings(parent.Users, child.Users),
  141. Teams: unionStrings(parent.Teams, child.Teams),
  142. }
  143. }
  144. // Apply returns a policy that merges the child into the parent
  145. func (p Policy) Apply(child Policy) (Policy, error) {
  146. if old := child.deprecatedPolicy.defined(); old && child.defined() {
  147. return p, errors.New("cannot mix Policy and deprecatedPolicy branch protection fields")
  148. } else if old {
  149. if !p.deprecatedWarning {
  150. p.deprecatedWarning = true
  151. logrus.Warn("WARNING: protect-by-default, require-contexts, allow-push are deprecated. Please replace them before July 2018")
  152. }
  153. d := child.deprecatedPolicy
  154. child = Policy{
  155. Protect: d.DeprecatedProtect,
  156. }
  157. if d.DeprecatedContexts != nil {
  158. child.RequiredStatusChecks = &ContextPolicy{
  159. Contexts: d.DeprecatedContexts,
  160. }
  161. }
  162. if d.DeprecatedPushers != nil {
  163. child.Restrictions = &Restrictions{
  164. Teams: d.DeprecatedPushers,
  165. }
  166. }
  167. }
  168. return Policy{
  169. Protect: selectBool(p.Protect, child.Protect),
  170. RequiredStatusChecks: mergeContextPolicy(p.RequiredStatusChecks, child.RequiredStatusChecks),
  171. Admins: selectBool(p.Admins, child.Admins),
  172. Restrictions: mergeRestrictions(p.Restrictions, child.Restrictions),
  173. RequiredPullRequestReviews: mergeReviewPolicy(p.RequiredPullRequestReviews, child.RequiredPullRequestReviews),
  174. deprecatedWarning: p.deprecatedWarning,
  175. }, nil
  176. }
  177. // BranchProtection specifies the global branch protection policy
  178. type BranchProtection struct {
  179. Policy
  180. ProtectTested bool `json:"protect-tested-repos,omitempty"`
  181. Orgs map[string]Org `json:"orgs,omitempty"`
  182. AllowDisabledPolicies bool `json:"allow_disabled_policies,omitempty"`
  183. warned bool // warn if deprecated fields are use
  184. }
  185. // Org holds the default protection policy for an entire org, as well as any repo overrides.
  186. type Org struct {
  187. Policy
  188. Repos map[string]Repo `json:"repos,omitempty"`
  189. }
  190. // Repo holds protection policy overrides for all branches in a repo, as well as specific branch overrides.
  191. type Repo struct {
  192. Policy
  193. Branches map[string]Branch `json:"branches,omitempty"`
  194. }
  195. // Branch holds protection policy overrides for a particular branch.
  196. type Branch struct {
  197. Policy
  198. }
  199. // GetBranchProtection returns the policy for a given branch.
  200. //
  201. // Handles merging any policies defined at repo/org/global levels into the branch policy.
  202. func (c *Config) GetBranchProtection(org, repo, branch string) (*Policy, error) {
  203. bp := c.BranchProtection
  204. var policy Policy
  205. policy, err := policy.Apply(bp.Policy)
  206. if err != nil {
  207. return nil, err
  208. }
  209. if o, ok := bp.Orgs[org]; ok {
  210. policy, err = policy.Apply(o.Policy)
  211. if err != nil {
  212. return nil, err
  213. }
  214. if r, ok := o.Repos[repo]; ok {
  215. policy, err = policy.Apply(r.Policy)
  216. if err != nil {
  217. return nil, err
  218. }
  219. if b, ok := r.Branches[branch]; ok {
  220. policy, err = policy.Apply(b.Policy)
  221. if err != nil {
  222. return nil, err
  223. }
  224. if policy.Protect == nil {
  225. return nil, errors.New("defined branch policies must set protect")
  226. }
  227. }
  228. }
  229. } else {
  230. return nil, nil
  231. }
  232. // Automatically require any required prow jobs
  233. if prowContexts, _ := BranchRequirements(org, repo, branch, c.Presubmits); len(prowContexts) > 0 {
  234. // Error if protection is disabled
  235. if policy.Protect != nil && !*policy.Protect {
  236. return nil, fmt.Errorf("required prow jobs require branch protection")
  237. }
  238. ps := Policy{
  239. RequiredStatusChecks: &ContextPolicy{
  240. Contexts: prowContexts,
  241. },
  242. }
  243. // Require protection by default if ProtectTested is true
  244. if bp.ProtectTested {
  245. yes := true
  246. ps.Protect = &yes
  247. }
  248. policy, err = policy.Apply(ps)
  249. if err != nil {
  250. return nil, err
  251. }
  252. }
  253. if policy.Protect != nil && !*policy.Protect {
  254. // Ensure that protection is false => no protection settings
  255. var old *bool
  256. old, policy.Protect = policy.Protect, old
  257. switch {
  258. case policy.defined() && bp.AllowDisabledPolicies:
  259. logrus.Warnf("%s/%s=%s defines a policy but has protect: false", org, repo, branch)
  260. policy = Policy{
  261. Protect: policy.Protect,
  262. }
  263. case policy.defined():
  264. return nil, fmt.Errorf("%s/%s=%s defines a policy, which requires protect: true", org, repo, branch)
  265. }
  266. policy.Protect = old
  267. }
  268. if !policy.defined() {
  269. return nil, nil
  270. }
  271. return &policy, nil
  272. }
  273. func jobRequirements(jobs []Presubmit, branch string, after bool) ([]string, []string) {
  274. var required, optional []string
  275. for _, j := range jobs {
  276. if !j.Brancher.RunsAgainstBranch(branch) {
  277. continue
  278. }
  279. // Does this job require a context or have kids that might need one?
  280. if !after && !j.AlwaysRun && j.RunIfChanged == "" {
  281. continue // No
  282. }
  283. if j.ContextRequired() { // This job needs a context
  284. required = append(required, j.Context)
  285. } else {
  286. optional = append(optional, j.Context)
  287. }
  288. // Check which children require contexts
  289. r, o := jobRequirements(j.RunAfterSuccess, branch, true)
  290. required = append(required, r...)
  291. optional = append(optional, o...)
  292. }
  293. return required, optional
  294. }
  295. // BranchRequirements returns required and optional presubmits prow jobs for a given org, repo branch.
  296. func BranchRequirements(org, repo, branch string, presubmits map[string][]Presubmit) ([]string, []string) {
  297. p, ok := presubmits[org+"/"+repo]
  298. if !ok {
  299. return nil, nil
  300. }
  301. return jobRequirements(p, branch, false)
  302. }