...

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

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

     1  // Copyright 2019 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 reviewer
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"math/rand"
    21  	"sort"
    22  
    23  	"github.com/pkg/errors"
    24  	"github.com/rs/zerolog"
    25  
    26  	"edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/pull"
    27  
    28  	"edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/policy/common"
    29  )
    30  
    31  const (
    32  	LogKeyLeafNode = "leaf_node"
    33  )
    34  
    35  type Selection struct {
    36  	Users []string
    37  	Teams []string
    38  }
    39  
    40  // Difference returns a new Selection with the users and teams that must be
    41  // added to the pull request given the reviewers that already exist. Reviewers
    42  // that were explicitly removed are not added again.
    43  func (s Selection) Difference(reviewers []*pull.Reviewer) Selection {
    44  	users := make(map[string]bool)
    45  	teams := make(map[string]bool)
    46  	for _, r := range reviewers {
    47  		switch r.Type {
    48  		case pull.ReviewerUser:
    49  			users[r.Name] = true
    50  		case pull.ReviewerTeam:
    51  			teams[r.Name] = true
    52  		}
    53  	}
    54  
    55  	var newUsers []string
    56  	for _, u := range s.Users {
    57  		if !users[u] {
    58  			newUsers = append(newUsers, u)
    59  		}
    60  	}
    61  
    62  	var newTeams []string
    63  	for _, t := range s.Teams {
    64  		if !teams[t] {
    65  			newTeams = append(newTeams, t)
    66  		}
    67  	}
    68  
    69  	return Selection{
    70  		Users: newUsers,
    71  		Teams: newTeams,
    72  	}
    73  }
    74  
    75  // IsEmpty returns true if the Selection has no users or teams.
    76  func (s Selection) IsEmpty() bool {
    77  	return len(s.Users) == 0 && len(s.Teams) == 0
    78  }
    79  
    80  // FindRequests returns all pending leaf results with review requests enabled.
    81  func FindRequests(result *common.Result) []*common.Result {
    82  	if result.Status != common.StatusPending {
    83  		return nil
    84  	}
    85  
    86  	var reqs []*common.Result
    87  	for _, c := range result.Children {
    88  		reqs = append(reqs, FindRequests(c)...)
    89  	}
    90  	if len(result.Children) == 0 && result.ReviewRequestRule != nil && result.Error == nil {
    91  		reqs = append(reqs, result)
    92  	}
    93  	return reqs
    94  }
    95  
    96  // select n random values from the list of users without reuse
    97  func selectRandomUsers(n int, users []string, r *rand.Rand) []string {
    98  	var selections []string
    99  	if n == 0 {
   100  		return selections
   101  	}
   102  	if n >= len(users) {
   103  		return users
   104  	}
   105  
   106  	selected := make(map[int]bool)
   107  	for i := 0; i < n; i++ {
   108  		j := 0
   109  		for {
   110  			// Upper bound the number of attempts to uniquely select random users to n*5
   111  			if j > n*5 {
   112  				// We haven't been able to select a random value, bail loudly
   113  				panic(fmt.Sprintf("failed to select random value for %d %d", n, len(users)))
   114  			}
   115  			m := r.Intn(len(users))
   116  			if !selected[m] {
   117  				selected[m] = true
   118  				selections = append(selections, users[m])
   119  				break
   120  			}
   121  			j++
   122  		}
   123  	}
   124  	return selections
   125  }
   126  
   127  func selectTeamMembers(prctx pull.Context, allTeams []string) (map[string][]string, error) {
   128  	var allTeamsMembers = make(map[string][]string)
   129  	for _, team := range allTeams {
   130  		teamMembers, err := prctx.TeamMembers(team)
   131  		if err != nil {
   132  			return nil, errors.Wrapf(err, "failed to get member listing for team %s", team)
   133  		}
   134  		allTeamsMembers[team] = teamMembers
   135  	}
   136  
   137  	return allTeamsMembers, nil
   138  }
   139  
   140  func selectOrgMembers(prctx pull.Context, allOrgs []string) ([]string, error) {
   141  	var allOrgsMembers []string
   142  	for _, org := range allOrgs {
   143  		orgMembers, err := prctx.OrganizationMembers(org)
   144  		if err != nil {
   145  			return nil, errors.Wrapf(err, "failed to get member listing for org %s", org)
   146  		}
   147  		allOrgsMembers = append(allOrgsMembers, orgMembers...)
   148  	}
   149  
   150  	return allOrgsMembers, nil
   151  }
   152  
   153  func getPossibleReviewers(prctx pull.Context, users map[string]struct{}, collaborators []*pull.Collaborator) []string {
   154  	var possibleReviewers []string
   155  	for _, c := range collaborators {
   156  		_, exists := users[c.Name]
   157  		if c.Name != prctx.Author() && exists {
   158  			possibleReviewers = append(possibleReviewers, c.Name)
   159  		}
   160  	}
   161  
   162  	// We need reviewer selection to be consistent when using a fixed random
   163  	// seed, so sort the reviewers before returning.
   164  	sort.Strings(possibleReviewers)
   165  	return possibleReviewers
   166  }
   167  
   168  func SelectReviewers(ctx context.Context, prctx pull.Context, results []*common.Result, r *rand.Rand) (Selection, error) {
   169  	selection := Selection{}
   170  
   171  	for _, child := range results {
   172  		logger := zerolog.Ctx(ctx).With().Str(LogKeyLeafNode, child.Name).Logger()
   173  		childCtx := logger.WithContext(ctx)
   174  
   175  		switch child.ReviewRequestRule.Mode {
   176  		case common.RequestModeTeams:
   177  			if err := selectTeamReviewers(childCtx, prctx, &selection, child); err != nil {
   178  				return selection, err
   179  			}
   180  		case common.RequestModeAllUsers, common.RequestModeRandomUsers:
   181  			if err := selectUserReviewers(childCtx, prctx, &selection, child, r); err != nil {
   182  				return selection, err
   183  			}
   184  		default:
   185  			return selection, fmt.Errorf("unknown reviewer selection mode: %s", child.ReviewRequestRule.Mode)
   186  		}
   187  	}
   188  	return selection, nil
   189  }
   190  
   191  func selectTeamReviewers(ctx context.Context, prctx pull.Context, selection *Selection, result *common.Result) error {
   192  	logger := zerolog.Ctx(ctx)
   193  
   194  	eligibleTeams, err := prctx.Teams()
   195  	if err != nil {
   196  		return err
   197  	}
   198  
   199  	var teams []string
   200  	for team, perm := range eligibleTeams {
   201  		switch {
   202  		case requestsTeam(result, prctx.RepositoryOwner()+"/"+team):
   203  			teams = append(teams, team)
   204  		case requestsPermission(result, perm):
   205  			teams = append(teams, team)
   206  		}
   207  	}
   208  
   209  	logger.Debug().Msgf("Requesting %d teams for review", len(teams))
   210  	selection.Teams = append(selection.Teams, teams...)
   211  	return nil
   212  }
   213  
   214  func selectUserReviewers(ctx context.Context, prctx pull.Context, selection *Selection, result *common.Result, r *rand.Rand) error {
   215  	logger := zerolog.Ctx(ctx)
   216  
   217  	allUsers := make(map[string]struct{})
   218  	for _, user := range result.ReviewRequestRule.Users {
   219  		allUsers[user] = struct{}{}
   220  	}
   221  
   222  	if len(result.ReviewRequestRule.Teams) > 0 {
   223  		logger.Debug().Msg("Selecting from teams for review")
   224  		teamsToUsers, err := selectTeamMembers(prctx, result.ReviewRequestRule.Teams)
   225  		if err != nil {
   226  			logger.Warn().Err(err).Msgf("failed to get member listing for teams, skipping team member selection")
   227  		}
   228  		for _, users := range teamsToUsers {
   229  			for _, user := range users {
   230  				allUsers[user] = struct{}{}
   231  			}
   232  		}
   233  	}
   234  
   235  	if len(result.ReviewRequestRule.Organizations) > 0 {
   236  		logger.Debug().Msg("Selecting from organizations for review")
   237  		orgMembers, err := selectOrgMembers(prctx, result.ReviewRequestRule.Organizations)
   238  		if err != nil {
   239  			logger.Warn().Err(err).Msg("failed to get member listing for org, skipping org member selection")
   240  		}
   241  		for _, user := range orgMembers {
   242  			allUsers[user] = struct{}{}
   243  		}
   244  	}
   245  
   246  	collaborators, err := prctx.RepositoryCollaborators()
   247  	if err != nil {
   248  		return errors.Wrap(err, "failed to list repository collaborators")
   249  	}
   250  
   251  	if len(result.ReviewRequestRule.Permissions) > 0 {
   252  		logger.Debug().Msg("Selecting from collaborators by permission for review")
   253  		for _, c := range collaborators {
   254  			for _, cp := range c.Permissions {
   255  				if cp.ViaRepo && requestsPermission(result, cp.Permission) {
   256  					allUsers[c.Name] = struct{}{}
   257  				}
   258  			}
   259  		}
   260  	}
   261  
   262  	possibleReviewers := getPossibleReviewers(prctx, allUsers, collaborators)
   263  	if len(possibleReviewers) == 0 {
   264  		logger.Debug().Msg("Found 0 eligible reviewers; skipping review request")
   265  		return nil
   266  	}
   267  
   268  	switch result.ReviewRequestRule.Mode {
   269  	case common.RequestModeAllUsers:
   270  		logger.Debug().Msgf("Found %d eligible reviewers; selecting all", len(possibleReviewers))
   271  		selection.Users = append(selection.Users, possibleReviewers...)
   272  
   273  	case common.RequestModeRandomUsers:
   274  		count := result.ReviewRequestRule.RequiredCount
   275  		selectedUsers := selectRandomUsers(count, possibleReviewers, r)
   276  
   277  		logger.Debug().Msgf("Found %d eligible reviewers; randomly selecting %d", len(possibleReviewers), count)
   278  		selection.Users = append(selection.Users, selectedUsers...)
   279  	}
   280  	return nil
   281  }
   282  
   283  func requestsTeam(r *common.Result, team string) bool {
   284  	for _, t := range r.ReviewRequestRule.Teams {
   285  		if t == team {
   286  			return true
   287  		}
   288  	}
   289  	return false
   290  }
   291  
   292  func requestsPermission(r *common.Result, perm pull.Permission) bool {
   293  	for _, p := range r.ReviewRequestRule.Permissions {
   294  		if p == perm {
   295  			return true
   296  		}
   297  	}
   298  	return false
   299  }
   300  

View as plain text