...

Source file src/edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/policy/disapproval/disapprove.go

Documentation: edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/policy/disapproval

     1  // Copyright 2018 Palantir Technologies, Inc.
     2  //
     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  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package disapproval
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sort"
    21  
    22  	"github.com/pkg/errors"
    23  	"github.com/rs/zerolog"
    24  
    25  	"edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/pull"
    26  
    27  	"edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/policy/common"
    28  	"edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/policy/predicate"
    29  )
    30  
    31  type Policy struct {
    32  	Predicates predicate.Predicates `yaml:"if,omitempty"`
    33  	Options    Options              `yaml:"options,omitempty"`
    34  	Requires   Requires             `yaml:"requires,omitempty"`
    35  }
    36  
    37  type Options struct {
    38  	Methods Methods `yaml:"methods,omitempty"`
    39  }
    40  
    41  type Methods struct {
    42  	Disapprove *common.Methods `yaml:"disapprove,omitempty"`
    43  	Revoke     *common.Methods `yaml:"revoke,omitempty"`
    44  }
    45  
    46  func (opts *Options) GetDisapproveMethods() *common.Methods {
    47  	m := opts.Methods.Disapprove
    48  	if m == nil {
    49  		githubReview := true
    50  		m = &common.Methods{
    51  			Comments: []string{
    52  				":-1:",
    53  				"👎",
    54  			},
    55  			GithubReview: &githubReview,
    56  		}
    57  	}
    58  
    59  	m.GithubReviewState = pull.ReviewChangesRequested
    60  	return m
    61  }
    62  
    63  func (opts *Options) GetRevokeMethods() *common.Methods {
    64  	m := opts.Methods.Revoke
    65  	if m == nil {
    66  		githubReview := true
    67  		m = &common.Methods{
    68  			Comments: []string{
    69  				":+1:",
    70  				"👍",
    71  			},
    72  			GithubReview: &githubReview,
    73  		}
    74  	}
    75  
    76  	m.GithubReviewState = pull.ReviewApproved
    77  	return m
    78  }
    79  
    80  type Requires struct {
    81  	common.Actors `yaml:",inline"`
    82  }
    83  
    84  func (p *Policy) Trigger() common.Trigger {
    85  	t := common.TriggerCommit
    86  
    87  	if !p.Requires.IsEmpty() {
    88  		dm := p.Options.GetDisapproveMethods()
    89  		rm := p.Options.GetRevokeMethods()
    90  
    91  		if len(dm.Comments) > 0 || len(rm.Comments) > 0 {
    92  			t |= common.TriggerComment
    93  		}
    94  		if dm.GithubReview != nil && *dm.GithubReview || rm.GithubReview != nil && *rm.GithubReview {
    95  			t |= common.TriggerReview
    96  		}
    97  	}
    98  
    99  	for _, predicate := range p.Predicates.Predicates() {
   100  		t |= predicate.Trigger()
   101  	}
   102  
   103  	return t
   104  }
   105  
   106  func (p *Policy) Evaluate(ctx context.Context, prctx pull.Context) (res common.Result) {
   107  	log := zerolog.Ctx(ctx)
   108  
   109  	res.Name = "disapproval"
   110  	res.Status = common.StatusSkipped
   111  	res.Requires = p.Requires.Actors
   112  
   113  	var predicateResults []*common.PredicateResult
   114  
   115  	for _, p := range p.Predicates.Predicates() {
   116  		result, err := p.Evaluate(ctx, prctx)
   117  		if err != nil {
   118  			res.Error = errors.Wrap(err, "failed to evaluate predicate")
   119  			return
   120  		}
   121  		predicateResults = append(predicateResults, result)
   122  
   123  		if result.Satisfied {
   124  			log.Debug().Msgf("disapproving, predicate of type %T was satisfied", p)
   125  
   126  			res.Status = common.StatusDisapproved
   127  
   128  			desc := result.Description
   129  			res.StatusDescription = desc
   130  			if desc == "" {
   131  				res.StatusDescription = "A precondition of this rule was satisfied"
   132  			}
   133  			res.PredicateResults = []*common.PredicateResult{result}
   134  			return
   135  		}
   136  	}
   137  	res.PredicateResults = predicateResults
   138  	if p.Requires.IsEmpty() {
   139  		log.Debug().Msg("no users are allowed to disapprove; skipping")
   140  
   141  		res.StatusDescription = "No disapproval policy is specified or the policy is empty"
   142  		return
   143  	}
   144  
   145  	disapproved, msg, err := p.IsDisapproved(ctx, prctx)
   146  	if err != nil {
   147  		res.Error = errors.WithMessage(err, "failed to compute disapproval status")
   148  		return
   149  	}
   150  
   151  	res.StatusDescription = msg
   152  	if disapproved {
   153  		res.Status = common.StatusDisapproved
   154  	} else {
   155  		res.Status = common.StatusSkipped
   156  	}
   157  	return
   158  }
   159  
   160  func (p *Policy) IsDisapproved(ctx context.Context, prctx pull.Context) (disapproved bool, msg string, err error) {
   161  	disapproveMethods := p.Options.GetDisapproveMethods()
   162  	revokeMethods := p.Options.GetRevokeMethods()
   163  
   164  	disapprover, err := p.lastActor(ctx, prctx, disapproveMethods, "disapproval")
   165  	if err != nil {
   166  		return false, "", errors.WithMessage(err, "failed to get last disapprover")
   167  	}
   168  
   169  	// exit early if there is no disapprover
   170  	if disapprover == nil {
   171  		msg = "No disapprovals"
   172  		return
   173  	}
   174  
   175  	revoker, err := p.lastActor(ctx, prctx, revokeMethods, "revocation")
   176  	if err != nil {
   177  		return false, "", errors.WithMessage(err, "failed to get last revoker")
   178  	}
   179  
   180  	switch {
   181  	// someone disapproved, but nobody has revoked
   182  	case revoker == nil:
   183  		disapproved = true
   184  		msg = fmt.Sprintf("Disapproved by %s", disapprover.User)
   185  
   186  	// the new disapproval appears after a revocation
   187  	case disapprover.CreatedAt.After(revoker.CreatedAt):
   188  		disapproved = true
   189  		msg = fmt.Sprintf("Disapproved by %s", disapprover.User)
   190  
   191  	// a disapproval has been revoked
   192  	default:
   193  		msg = fmt.Sprintf("Disapproval revoked by %s", revoker.User)
   194  	}
   195  	return
   196  }
   197  
   198  func (p *Policy) lastActor(ctx context.Context, prctx pull.Context, methods *common.Methods, kind string) (*common.Candidate, error) {
   199  	log := zerolog.Ctx(ctx)
   200  
   201  	candidates, err := methods.Candidates(ctx, prctx)
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	log.Debug().Msgf("found %d %s candidates", len(candidates), kind)
   207  
   208  	candidates, err = p.filter(ctx, prctx, candidates)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	sort.Stable(common.CandidatesByCreationTime(candidates))
   214  
   215  	return last(candidates), nil
   216  }
   217  
   218  func (p *Policy) filter(ctx context.Context, prctx pull.Context, candidates []*common.Candidate) ([]*common.Candidate, error) {
   219  	log := zerolog.Ctx(ctx)
   220  
   221  	var filtered []*common.Candidate
   222  	for _, c := range candidates {
   223  		ok, err := p.Requires.IsActor(ctx, prctx, c.User)
   224  		if err != nil {
   225  			return nil, errors.WithMessage(err, "failed to check candidate status")
   226  		}
   227  
   228  		if !ok {
   229  			log.Debug().Str("user", c.User).Msg("ignoring disapproval/revocation by non-whitelisted user")
   230  			continue
   231  		}
   232  
   233  		filtered = append(filtered, c)
   234  	}
   235  	return filtered, nil
   236  }
   237  
   238  func last(c []*common.Candidate) *common.Candidate {
   239  	if len(c) == 0 {
   240  		return nil
   241  	}
   242  	return c[len(c)-1]
   243  }
   244  

View as plain text