feat: support codewoners usrgrp (#826)

This commit is contained in:
Abhinav Singh 2023-11-30 04:02:28 +00:00 committed by Harness
parent 5cdbde8100
commit 04566e1cf9
10 changed files with 211 additions and 49 deletions

View File

@ -67,17 +67,34 @@ func mapCodeOwnerEvaluation(ownerEvaluation *codeowners.Evaluation) []types.Code
codeOwnerEvaluationEntries := make([]types.CodeOwnerEvaluationEntry, len(ownerEvaluation.EvaluationEntries)) codeOwnerEvaluationEntries := make([]types.CodeOwnerEvaluationEntry, len(ownerEvaluation.EvaluationEntries))
for i, entry := range ownerEvaluation.EvaluationEntries { for i, entry := range ownerEvaluation.EvaluationEntries {
ownerEvaluations := make([]types.OwnerEvaluation, len(entry.OwnerEvaluations)) ownerEvaluations := make([]types.OwnerEvaluation, len(entry.OwnerEvaluations))
userGroupOwnerEvaluations := make([]types.UserGroupOwnerEvaluation, len(entry.UserGroupOwnerEvaluations))
for j, owner := range entry.OwnerEvaluations { for j, owner := range entry.OwnerEvaluations {
ownerEvaluations[j] = types.OwnerEvaluation{ ownerEvaluations[j] = mapOwner(owner)
Owner: owner.Owner, }
ReviewDecision: owner.ReviewDecision, for j, userGroupOwnerEvaluation := range entry.UserGroupOwnerEvaluations {
ReviewSHA: owner.ReviewSHA, userGroupEvaluations := make([]types.OwnerEvaluation, len(userGroupOwnerEvaluation.Evaluations))
for k, userGroupOwner := range userGroupOwnerEvaluation.Evaluations {
userGroupEvaluations[k] = mapOwner(userGroupOwner)
}
userGroupOwnerEvaluations[j] = types.UserGroupOwnerEvaluation{
ID: userGroupOwnerEvaluation.ID,
Name: userGroupOwnerEvaluation.Name,
Evaluations: userGroupEvaluations,
} }
} }
codeOwnerEvaluationEntries[i] = types.CodeOwnerEvaluationEntry{ codeOwnerEvaluationEntries[i] = types.CodeOwnerEvaluationEntry{
Pattern: entry.Pattern, Pattern: entry.Pattern,
OwnerEvaluations: ownerEvaluations, OwnerEvaluations: ownerEvaluations,
UserGroupOwnerEvaluations: userGroupOwnerEvaluations,
} }
} }
return codeOwnerEvaluationEntries return codeOwnerEvaluationEntries
} }
func mapOwner(owner codeowners.OwnerEvaluation) types.OwnerEvaluation {
return types.OwnerEvaluation{
Owner: owner.Owner,
ReviewDecision: owner.ReviewDecision,
ReviewSHA: owner.ReviewSHA,
}
}

View File

@ -21,6 +21,7 @@ import (
"io" "io"
"strings" "strings"
"github.com/harness/gitness/app/services/usergroup"
"github.com/harness/gitness/app/store" "github.com/harness/gitness/app/store"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git" "github.com/harness/gitness/git"
@ -37,6 +38,8 @@ const (
// maxGetContentFileSize specifies the maximum number of bytes a file content response contains. // maxGetContentFileSize specifies the maximum number of bytes a file content response contains.
// If a file is any larger, the content is truncated. // If a file is any larger, the content is truncated.
maxGetContentFileSize = oneMegabyte * 4 // 4 MB maxGetContentFileSize = oneMegabyte * 4 // 4 MB
// userGroupPrefixMarker is a prefix which will be used to identify if a given codeowner is usergroup.
userGroupPrefixMarker = "@"
) )
var ( var (
@ -71,10 +74,11 @@ type Config struct {
} }
type Service struct { type Service struct {
repoStore store.RepoStore repoStore store.RepoStore
git git.Interface git git.Interface
principalStore store.PrincipalStore principalStore store.PrincipalStore
config Config config Config
userGroupResolver usergroup.Resolver
} }
type File struct { type File struct {
@ -99,8 +103,15 @@ type Evaluation struct {
} }
type EvaluationEntry struct { type EvaluationEntry struct {
Pattern string Pattern string
OwnerEvaluations []OwnerEvaluation OwnerEvaluations []OwnerEvaluation
UserGroupOwnerEvaluations []UserGroupOwnerEvaluation
}
type UserGroupOwnerEvaluation struct {
ID string
Name string
Evaluations []OwnerEvaluation
} }
type OwnerEvaluation struct { type OwnerEvaluation struct {
@ -114,12 +125,14 @@ func New(
git git.Interface, git git.Interface,
config Config, config Config,
principalStore store.PrincipalStore, principalStore store.PrincipalStore,
userGroupResolver usergroup.Resolver,
) *Service { ) *Service {
service := &Service{ service := &Service{
repoStore: repoStore, repoStore: repoStore,
git: git, git: git,
config: config, config: config,
principalStore: principalStore, principalStore: principalStore,
userGroupResolver: userGroupResolver,
} }
return service return service
} }
@ -284,11 +297,12 @@ func (s *Service) getApplicableCodeOwnersForPR(
}, err }, err
} }
//nolint:gocognit
func (s *Service) Evaluate( func (s *Service) Evaluate(
ctx context.Context, ctx context.Context,
repo *types.Repository, repo *types.Repository,
pr *types.PullReq, pr *types.PullReq,
reviewer []*types.PullReqReviewer, reviewers []*types.PullReqReviewer,
) (*Evaluation, error) { ) (*Evaluation, error) {
owners, err := s.getApplicableCodeOwnersForPR(ctx, repo, pr) owners, err := s.getApplicableCodeOwnersForPR(ctx, repo, pr)
if err != nil { if err != nil {
@ -299,36 +313,41 @@ func (s *Service) Evaluate(
return &Evaluation{}, nil return &Evaluation{}, nil
} }
flattenedReviewers := flattenReviewers(reviewer)
evaluationEntries := make([]EvaluationEntry, len(owners.Entries)) evaluationEntries := make([]EvaluationEntry, len(owners.Entries))
for i, entry := range owners.Entries { for i, entry := range owners.Entries {
ownerEvaluations := make([]OwnerEvaluation, 0, len(owners.Entries)) ownerEvaluations := make([]OwnerEvaluation, 0, len(owners.Entries))
userGroupOwnerEvaluations := make([]UserGroupOwnerEvaluation, 0, len(owners.Entries))
for _, owner := range entry.Owners { for _, owner := range entry.Owners {
if pullreqReviewer, ok := flattenedReviewers[owner]; ok { // check for usrgrp
ownerEvaluations = append(ownerEvaluations, OwnerEvaluation{ if strings.HasPrefix(owner, userGroupPrefixMarker) {
Owner: pullreqReviewer.Reviewer, userGroupCodeOwner, err := s.resolveUserGroupCodeOwner(ctx, owner[1:], reviewers)
ReviewDecision: pullreqReviewer.ReviewDecision, if errors.Is(err, usergroup.ErrNotFound) {
ReviewSHA: pullreqReviewer.SHA, log.Ctx(ctx).Debug().Msgf("usergroup %q not found hence skipping for code owner", owner)
}) continue
}
if err != nil {
return nil, fmt.Errorf("error resolving usergroup :%w", err)
}
userGroupOwnerEvaluations = append(userGroupOwnerEvaluations, *userGroupCodeOwner)
continue continue
} }
principal, err := s.principalStore.FindByEmail(ctx, owner) // user email based codeowner
userCodeOwner, err := s.resolveUserCodeOwnerByEmail(ctx, owner, reviewers)
if errors.Is(err, gitness_store.ErrResourceNotFound) { if errors.Is(err, gitness_store.ErrResourceNotFound) {
log.Ctx(ctx).Info().Msgf("user %s not found in database hence skipping for code owner: %v", log.Ctx(ctx).Debug().Msgf("user %q not found in database hence skipping for code owner", owner)
owner, err)
continue continue
} }
if err != nil { if err != nil {
return &Evaluation{}, fmt.Errorf("error finding user by email: %w", err) return nil, fmt.Errorf("error resolving user by email : %w", err)
} }
ownerEvaluations = append(ownerEvaluations, OwnerEvaluation{ ownerEvaluations = append(ownerEvaluations, *userCodeOwner)
Owner: *principal.ToPrincipalInfo(),
})
} }
evaluationEntries[i] = EvaluationEntry{ evaluationEntries[i] = EvaluationEntry{
Pattern: entry.Pattern, Pattern: entry.Pattern,
OwnerEvaluations: ownerEvaluations, OwnerEvaluations: ownerEvaluations,
UserGroupOwnerEvaluations: userGroupOwnerEvaluations,
} }
} }
@ -338,6 +357,62 @@ func (s *Service) Evaluate(
}, nil }, nil
} }
func (s *Service) resolveUserGroupCodeOwner(
ctx context.Context,
owner string,
reviewers []*types.PullReqReviewer,
) (*UserGroupOwnerEvaluation, error) {
usrgrp, err := s.userGroupResolver.Resolve(ctx, owner)
if err != nil {
return nil, fmt.Errorf("not able to resolve usergroup : %w", err)
}
userGroupEvaluation := &UserGroupOwnerEvaluation{
ID: usrgrp.ID,
Name: usrgrp.Name,
}
ownersEvaluations := make([]OwnerEvaluation, 0, len(usrgrp.Users))
for _, uid := range usrgrp.Users {
pullreqReviewer := findReviewerInList("", uid, reviewers)
// we don't append all the user of the user group in the owner evaluations and
// append it only if it is reviewed by a user.
if pullreqReviewer != nil {
ownersEvaluations = append(ownersEvaluations,
OwnerEvaluation{
Owner: pullreqReviewer.Reviewer,
ReviewDecision: pullreqReviewer.ReviewDecision,
ReviewSHA: pullreqReviewer.SHA,
},
)
continue
}
}
userGroupEvaluation.Evaluations = ownersEvaluations
return userGroupEvaluation, nil
}
func (s *Service) resolveUserCodeOwnerByEmail(
ctx context.Context,
owner string,
reviewers []*types.PullReqReviewer,
) (*OwnerEvaluation, error) {
pullreqReviewer := findReviewerInList(owner, "", reviewers)
if pullreqReviewer != nil {
return &OwnerEvaluation{
Owner: pullreqReviewer.Reviewer,
ReviewDecision: pullreqReviewer.ReviewDecision,
ReviewSHA: pullreqReviewer.SHA,
}, nil
}
principal, err := s.principalStore.FindByEmail(ctx, owner)
if err != nil {
return nil, fmt.Errorf("error finding user by email: %w", err)
}
return &OwnerEvaluation{
Owner: *principal.ToPrincipalInfo(),
}, nil
}
func (s *Service) Validate( func (s *Service) Validate(
ctx context.Context, ctx context.Context,
repo *types.Repository, repo *types.Repository,
@ -353,6 +428,10 @@ func (s *Service) Validate(
for _, entry := range codeowners.Entries { for _, entry := range codeowners.Entries {
// check for users in file // check for users in file
for _, owner := range entry.Owners { for _, owner := range entry.Owners {
// todo: handle usergroup better
if strings.HasPrefix(owner, userGroupPrefixMarker) {
continue
}
_, err := s.principalStore.FindByEmail(ctx, owner) _, err := s.principalStore.FindByEmail(ctx, owner)
if errors.Is(err, gitness_store.ErrResourceNotFound) { if errors.Is(err, gitness_store.ErrResourceNotFound) {
codeOwnerValidation.Addf(enum.CodeOwnerViolationCodeUserNotFound, codeOwnerValidation.Addf(enum.CodeOwnerViolationCodeUserNotFound,
@ -381,12 +460,14 @@ func (s *Service) Validate(
return &codeOwnerValidation, nil return &codeOwnerValidation, nil
} }
func flattenReviewers(reviewers []*types.PullReqReviewer) map[string]*types.PullReqReviewer { func findReviewerInList(email string, uid string, reviewers []*types.PullReqReviewer) *types.PullReqReviewer {
r := make(map[string]*types.PullReqReviewer)
for _, reviewer := range reviewers { for _, reviewer := range reviewers {
r[reviewer.Reviewer.Email] = reviewer if uid == reviewer.Reviewer.UID || email == reviewer.Reviewer.Email {
return reviewer
}
} }
return r
return nil
} }
// We match a pattern list against a target // We match a pattern list against a target

View File

@ -15,6 +15,7 @@
package codeowners package codeowners
import ( import (
"github.com/harness/gitness/app/services/usergroup"
"github.com/harness/gitness/app/store" "github.com/harness/gitness/app/store"
"github.com/harness/gitness/git" "github.com/harness/gitness/git"
@ -30,6 +31,7 @@ func ProvideCodeOwners(
repoStore store.RepoStore, repoStore store.RepoStore,
config Config, config Config,
principalStore store.PrincipalStore, principalStore store.PrincipalStore,
userGroupResolver usergroup.Resolver,
) *Service { ) *Service {
return New(repoStore, git, config, principalStore) return New(repoStore, git, config, principalStore, userGroupResolver)
} }

View File

@ -108,7 +108,7 @@ func (v *DefPullReq) MergeVerify(
if v.Approvals.RequireCodeOwners { if v.Approvals.RequireCodeOwners {
for _, entry := range in.CodeOwners.EvaluationEntries { for _, entry := range in.CodeOwners.EvaluationEntries {
reviewDecision, approvers := getCodeOwnerApprovalStatus(entry.OwnerEvaluations) reviewDecision, approvers := getCodeOwnerApprovalStatus(entry)
if reviewDecision == enum.PullReqReviewDecisionPending { if reviewDecision == enum.PullReqReviewDecisionPending {
violations.Addf(codePullReqApprovalReqCodeOwnersNoApproval, violations.Addf(codePullReqApprovalReqCodeOwnersNoApproval,
@ -295,10 +295,12 @@ func (v *DefPullReq) Sanitize() error {
} }
func getCodeOwnerApprovalStatus( func getCodeOwnerApprovalStatus(
ownerStatus []codeowners.OwnerEvaluation, entry codeowners.EvaluationEntry,
) (enum.PullReqReviewDecision, []codeowners.OwnerEvaluation) { ) (enum.PullReqReviewDecision, []codeowners.OwnerEvaluation) {
approvers := make([]codeowners.OwnerEvaluation, 0) approvers := make([]codeowners.OwnerEvaluation, 0)
for _, o := range ownerStatus {
// users
for _, o := range entry.OwnerEvaluations {
if o.ReviewDecision == enum.PullReqReviewDecisionChangeReq { if o.ReviewDecision == enum.PullReqReviewDecisionChangeReq {
return enum.PullReqReviewDecisionChangeReq, nil return enum.PullReqReviewDecisionChangeReq, nil
} }
@ -306,6 +308,19 @@ func getCodeOwnerApprovalStatus(
approvers = append(approvers, o) approvers = append(approvers, o)
} }
} }
// usergroups
for _, u := range entry.UserGroupOwnerEvaluations {
for _, o := range u.Evaluations {
if o.ReviewDecision == enum.PullReqReviewDecisionChangeReq {
return enum.PullReqReviewDecisionChangeReq, nil
}
if o.ReviewDecision == enum.PullReqReviewDecisionApproved {
approvers = append(approvers, o)
}
}
}
if len(approvers) > 0 { if len(approvers) > 0 {
return enum.PullReqReviewDecisionApproved, approvers return enum.PullReqReviewDecisionApproved, approvers
} }

View File

@ -17,14 +17,19 @@ package usergroup
import ( import (
"context" "context"
"github.com/harness/gitness/store"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
) )
type Service struct { var _ Resolver = (*GitnessResolver)(nil)
type GitnessResolver struct {
} }
func (s *Service) Resolve(context.Context, string) (*types.UserGroup, error) { func NewGitnessResolver() *GitnessResolver {
// todo: implement once usergroup is supported return &GitnessResolver{}
return nil, store.ErrResourceNotFound }
func (s *GitnessResolver) Resolve(context.Context, string) (*types.UserGroup, error) {
// todo: implement once usergroup is supported
return nil, ErrNotFound
} }

View File

@ -16,10 +16,13 @@ package usergroup
import ( import (
"context" "context"
"errors"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
) )
var ErrNotFound = errors.New("usergroup not found")
type Resolver interface { type Resolver interface {
Resolve(ctx context.Context, scopedID string) (*types.UserGroup, error) Resolve(ctx context.Context, scopedID string) (*types.UserGroup, error)
} }

View File

@ -0,0 +1,28 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package usergroup
import (
"github.com/google/wire"
)
// WireSet provides a wire set for this package.
var WireSet = wire.NewSet(
ProvideUserGroupResolver,
)
func ProvideUserGroupResolver() Resolver {
return NewGitnessResolver()
}

View File

@ -59,6 +59,7 @@ import (
"github.com/harness/gitness/app/services/protection" "github.com/harness/gitness/app/services/protection"
pullreqservice "github.com/harness/gitness/app/services/pullreq" pullreqservice "github.com/harness/gitness/app/services/pullreq"
"github.com/harness/gitness/app/services/trigger" "github.com/harness/gitness/app/services/trigger"
"github.com/harness/gitness/app/services/usergroup"
"github.com/harness/gitness/app/services/webhook" "github.com/harness/gitness/app/services/webhook"
"github.com/harness/gitness/app/sse" "github.com/harness/gitness/app/sse"
"github.com/harness/gitness/app/store" "github.com/harness/gitness/app/store"
@ -164,6 +165,7 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e
cliserver.ProvideKeywordSearchConfig, cliserver.ProvideKeywordSearchConfig,
keywordsearch.WireSet, keywordsearch.WireSet,
controllerkeywordsearch.WireSet, controllerkeywordsearch.WireSet,
usergroup.WireSet,
) )
return &cliserver.System{}, nil return &cliserver.System{}, nil
} }

View File

@ -58,6 +58,7 @@ import (
"github.com/harness/gitness/app/services/protection" "github.com/harness/gitness/app/services/protection"
"github.com/harness/gitness/app/services/pullreq" "github.com/harness/gitness/app/services/pullreq"
trigger2 "github.com/harness/gitness/app/services/trigger" trigger2 "github.com/harness/gitness/app/services/trigger"
"github.com/harness/gitness/app/services/usergroup"
"github.com/harness/gitness/app/services/webhook" "github.com/harness/gitness/app/services/webhook"
"github.com/harness/gitness/app/sse" "github.com/harness/gitness/app/sse"
"github.com/harness/gitness/app/store" "github.com/harness/gitness/app/store"
@ -161,7 +162,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
return nil, err return nil, err
} }
codeownersConfig := server.ProvideCodeOwnerConfig(config) codeownersConfig := server.ProvideCodeOwnerConfig(config)
codeownersService := codeowners.ProvideCodeOwners(gitInterface, repoStore, codeownersConfig, principalStore) resolver := usergroup.ProvideUserGroupResolver()
codeownersService := codeowners.ProvideCodeOwners(gitInterface, repoStore, codeownersConfig, principalStore, resolver)
eventsConfig := server.ProvideEventsConfig(config) eventsConfig := server.ProvideEventsConfig(config)
eventsSystem, err := events.ProvideSystem(eventsConfig, universalClient) eventsSystem, err := events.ProvideSystem(eventsConfig, universalClient)
if err != nil { if err != nil {

View File

@ -26,8 +26,15 @@ type CodeOwnerEvaluation struct {
} }
type CodeOwnerEvaluationEntry struct { type CodeOwnerEvaluationEntry struct {
Pattern string `json:"pattern"` Pattern string `json:"pattern"`
OwnerEvaluations []OwnerEvaluation `json:"owner_evaluations"` OwnerEvaluations []OwnerEvaluation `json:"owner_evaluations"`
UserGroupOwnerEvaluations []UserGroupOwnerEvaluation `json:"user_group_owner_evaluations"`
}
type UserGroupOwnerEvaluation struct {
ID string `json:"id"`
Name string `json:"name"`
Evaluations []OwnerEvaluation `json:"evaluations"`
} }
type OwnerEvaluation struct { type OwnerEvaluation struct {