fix: [CODE-965]: codeowners file parse (#684)

pull/3417/head
Abhinav Singh 2023-10-18 06:38:50 +00:00 committed by Harness
parent c6dca7d7ae
commit 1a07ee90d2
9 changed files with 319 additions and 7 deletions

View File

@ -25,6 +25,7 @@ import (
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/app/auth/authz"
"github.com/harness/gitness/app/githook"
"github.com/harness/gitness/app/services/codeowners"
"github.com/harness/gitness/app/services/importer"
"github.com/harness/gitness/app/services/protection"
"github.com/harness/gitness/app/store"
@ -50,6 +51,7 @@ type Controller struct {
protectionManager *protection.Manager
gitRPCClient gitrpc.Interface
importer *importer.Repository
codeOwners *codeowners.Service
}
func NewController(
@ -66,6 +68,7 @@ func NewController(
protectionManager *protection.Manager,
gitRPCClient gitrpc.Interface,
importer *importer.Repository,
codeOwners *codeowners.Service,
) *Controller {
return &Controller{
defaultBranch: defaultBranch,
@ -81,6 +84,7 @@ func NewController(
protectionManager: protectionManager,
gitRPCClient: gitRPCClient,
importer: importer,
codeOwners: codeOwners,
}
}

View File

@ -16,6 +16,7 @@ package repo
import (
"github.com/harness/gitness/app/auth/authz"
"github.com/harness/gitness/app/services/codeowners"
"github.com/harness/gitness/app/services/importer"
"github.com/harness/gitness/app/services/protection"
"github.com/harness/gitness/app/store"
@ -37,11 +38,11 @@ func ProvideController(config *types.Config, tx dbtx.Transactor, urlProvider url
uidCheck check.PathUID, authorizer authz.Authorizer, repoStore store.RepoStore,
spaceStore store.SpaceStore, pipelineStore store.PipelineStore,
principalStore store.PrincipalStore, ruleStore store.RuleStore, protectionManager *protection.Manager,
rpcClient gitrpc.Interface, importer *importer.Repository,
rpcClient gitrpc.Interface, importer *importer.Repository, codeOwners *codeowners.Service,
) *Controller {
return NewController(config.Git.DefaultBranch, tx, urlProvider,
uidCheck, authorizer, repoStore,
spaceStore, pipelineStore,
principalStore, ruleStore, protectionManager,
rpcClient, importer)
rpcClient, importer, codeOwners)
}

View File

@ -0,0 +1,165 @@
// 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 codeowners
import (
"bufio"
"context"
"fmt"
"io"
"strings"
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/gitrpc"
"github.com/harness/gitness/types"
)
const (
// maxGetContentFileSize specifies the maximum number of bytes a file content response contains.
// If a file is any larger, the content is truncated.
maxGetContentFileSize = 1 << 20 // 1 MB
)
type Config struct {
CodeOwnerFilePath string
}
type Service struct {
repoStore store.RepoStore
git gitrpc.Interface
Config Config
}
type codeOwnerFile struct {
Content string
SHA string
}
type CodeOwners struct {
CodeOwnerFileSha string
CodeOwnerDetails []codeOwnerDetail
}
type codeOwnerDetail struct {
Pattern string
Owners []string
}
func New(
repoStore store.RepoStore,
git gitrpc.Interface,
config Config,
) (*Service, error) {
service := &Service{
repoStore: repoStore,
git: git,
Config: config,
}
return service, nil
}
func (s *Service) Get(ctx context.Context,
repoID int64) (*CodeOwners, error) {
repo, err := s.repoStore.Find(ctx, repoID)
if err != nil {
return nil, fmt.Errorf("unable to retrieve repo %w", err)
}
codeOwnerFile, err := s.getCodeOwnerFile(ctx, repo)
if err != nil {
return nil, fmt.Errorf("unable to get codeowner details %w", err)
}
owner, err := s.ParseCodeOwner(codeOwnerFile.Content)
if err != nil {
return nil, fmt.Errorf("unable to parse codeowner %w", err)
}
return &CodeOwners{
CodeOwnerFileSha: codeOwnerFile.SHA,
CodeOwnerDetails: owner,
}, nil
}
func (s *Service) ParseCodeOwner(codeOwnersContent string) ([]codeOwnerDetail, error) {
var codeOwners []codeOwnerDetail
scanner := bufio.NewScanner(strings.NewReader(codeOwnersContent))
for scanner.Scan() {
line := scanner.Text()
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.Split(line, " ")
if len(parts) < 2 {
return nil, fmt.Errorf("invalid line: %s", line)
}
pattern := parts[0]
owners := parts[1:]
codeOwner := codeOwnerDetail{
Pattern: pattern,
Owners: owners,
}
codeOwners = append(codeOwners, codeOwner)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading input: %v", err)
}
return codeOwners, nil
}
func (s *Service) getCodeOwnerFile(ctx context.Context,
repo *types.Repository,
) (*codeOwnerFile, error) {
params := gitrpc.CreateRPCReadParams(repo)
node, err := s.git.GetTreeNode(ctx, &gitrpc.GetTreeNodeParams{
ReadParams: params,
GitREF: "refs/heads/" + repo.DefaultBranch,
Path: s.Config.CodeOwnerFilePath,
})
if err != nil {
// todo: check for path not found and return empty codeowners
return nil, fmt.Errorf("unable to retrieve codeowner file %w", err)
}
if node.Node.Mode != gitrpc.TreeNodeModeFile {
return nil, fmt.Errorf("codeowner file not of right format")
}
output, err := s.git.GetBlob(ctx, &gitrpc.GetBlobParams{
ReadParams: params,
SHA: node.Node.SHA,
SizeLimit: maxGetContentFileSize,
})
if err != nil {
return nil, fmt.Errorf("failed to get file content: %w", err)
}
content, err := io.ReadAll(output.Content)
if err != nil {
return nil, fmt.Errorf("failed to read blob content: %w", err)
}
return &codeOwnerFile{
Content: string(content),
SHA: output.SHA,
}, nil
}

View File

@ -0,0 +1,83 @@
package codeowners
import (
"reflect"
"testing"
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/gitrpc"
)
func TestService_ParseCodeOwner(t *testing.T) {
content1 := "**/contracts/openapi/v1/ mankrit.singh@harness.io ashish.sanodia@harness.io\n"
content2 := "**/contracts/openapi/v1/ mankrit.singh@harness.io ashish.sanodia@harness.io\n/scripts/api mankrit.singh@harness.io ashish.sanodia@harness.io"
content3 := "# codeowner file \n**/contracts/openapi/v1/ mankrit.singh@harness.io ashish.sanodia@harness.io\n#\n/scripts/api mankrit.singh@harness.io ashish.sanodia@harness.io"
type fields struct {
repoStore store.RepoStore
git gitrpc.Interface
Config Config
}
type args struct {
codeOwnersContent string
}
tests := []struct {
name string
fields fields
args args
want []codeOwnerDetail
wantErr bool
}{
{
name: "Code owners Single",
args: args{codeOwnersContent: content1},
want: []codeOwnerDetail{{
Pattern: "**/contracts/openapi/v1/",
Owners: []string{"mankrit.singh@harness.io", "ashish.sanodia@harness.io"},
},
},
},
{
name: "Code owners Multiple",
args: args{codeOwnersContent: content2},
want: []codeOwnerDetail{{
Pattern: "**/contracts/openapi/v1/",
Owners: []string{"mankrit.singh@harness.io", "ashish.sanodia@harness.io"},
},
{
Pattern: "/scripts/api",
Owners: []string{"mankrit.singh@harness.io", "ashish.sanodia@harness.io"},
},
},
},
{
name: "Code owners With comments",
args: args{codeOwnersContent: content3},
want: []codeOwnerDetail{{
Pattern: "**/contracts/openapi/v1/",
Owners: []string{"mankrit.singh@harness.io", "ashish.sanodia@harness.io"},
},
{
Pattern: "/scripts/api",
Owners: []string{"mankrit.singh@harness.io", "ashish.sanodia@harness.io"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Service{
repoStore: tt.fields.repoStore,
git: tt.fields.git,
Config: tt.fields.Config,
}
got, err := s.ParseCodeOwner(tt.args.codeOwnersContent)
if (err != nil) != tt.wantErr {
t.Errorf("ParseCodeOwner() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseCodeOwner() got = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -0,0 +1,39 @@
// 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 codeowners
import (
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/gitrpc"
"github.com/google/wire"
)
var WireSet = wire.NewSet(
ProvideCodeOwners,
)
func ProvideCodeOwners(
gitRPCClient gitrpc.Interface,
repoStore store.RepoStore,
config Config,
) (*Service, error) {
service, err := New(repoStore, gitRPCClient, config)
if err != nil {
return nil, err
}
return service, nil
}

View File

@ -23,6 +23,7 @@ import (
"unicode"
"github.com/harness/gitness/app/services/cleanup"
"github.com/harness/gitness/app/services/codeowners"
"github.com/harness/gitness/app/services/trigger"
"github.com/harness/gitness/app/services/webhook"
"github.com/harness/gitness/blob"
@ -334,3 +335,10 @@ func ProvideCleanupConfig(config *types.Config) cleanup.Config {
WebhookExecutionsRetentionTime: config.Webhook.RetentionTime,
}
}
// ProvideCodeOwnerConfig loads the codeowner config from the main config.
func ProvideCodeOwnerConfig(config *types.Config) codeowners.Config {
return codeowners.Config{
CodeOwnerFilePath: config.CodeOwners.CodeOwnerFilePath,
}
}

View File

@ -9,8 +9,6 @@ package main
import (
"context"
"github.com/harness/gitness/app/api/controller/upload"
"github.com/harness/gitness/blob"
checkcontroller "github.com/harness/gitness/app/api/controller/check"
"github.com/harness/gitness/app/api/controller/connector"
@ -29,6 +27,7 @@ import (
"github.com/harness/gitness/app/api/controller/system"
"github.com/harness/gitness/app/api/controller/template"
controllertrigger "github.com/harness/gitness/app/api/controller/trigger"
"github.com/harness/gitness/app/api/controller/upload"
"github.com/harness/gitness/app/api/controller/user"
controllerwebhook "github.com/harness/gitness/app/api/controller/webhook"
"github.com/harness/gitness/app/auth/authn"
@ -49,6 +48,7 @@ import (
"github.com/harness/gitness/app/services"
"github.com/harness/gitness/app/services/cleanup"
"github.com/harness/gitness/app/services/codecomments"
"github.com/harness/gitness/app/services/codeowners"
"github.com/harness/gitness/app/services/exporter"
"github.com/harness/gitness/app/services/importer"
"github.com/harness/gitness/app/services/job"
@ -63,6 +63,7 @@ import (
"github.com/harness/gitness/app/store/database"
"github.com/harness/gitness/app/store/logs"
"github.com/harness/gitness/app/url"
"github.com/harness/gitness/blob"
cliserver "github.com/harness/gitness/cli/server"
"github.com/harness/gitness/encrypt"
"github.com/harness/gitness/events"
@ -156,6 +157,8 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e
canceler.WireSet,
exporter.WireSet,
metric.WireSet,
cliserver.ProvideCodeOwnerConfig,
codeowners.WireSet,
)
return &cliserver.System{}, nil
}

View File

@ -8,6 +8,7 @@ package main
import (
"context"
check2 "github.com/harness/gitness/app/api/controller/check"
"github.com/harness/gitness/app/api/controller/connector"
"github.com/harness/gitness/app/api/controller/execution"
@ -46,6 +47,7 @@ import (
"github.com/harness/gitness/app/services"
"github.com/harness/gitness/app/services/cleanup"
"github.com/harness/gitness/app/services/codecomments"
"github.com/harness/gitness/app/services/codeowners"
"github.com/harness/gitness/app/services/exporter"
"github.com/harness/gitness/app/services/importer"
"github.com/harness/gitness/app/services/job"
@ -73,9 +75,7 @@ import (
"github.com/harness/gitness/store/database/dbtx"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/check"
)
import (
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
)
@ -151,7 +151,12 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
if err != nil {
return nil, err
}
repoController := repo.ProvideController(config, transactor, provider, pathUID, authorizer, repoStore, spaceStore, pipelineStore, principalStore, ruleStore, protectionManager, gitrpcInterface, repository)
codeownersConfig := server.ProvideCodeOwnerConfig(config)
codeownersService, err := codeowners.ProvideCodeOwners(gitrpcInterface, repoStore, codeownersConfig)
if err != nil {
return nil, err
}
repoController := repo.ProvideController(config, transactor, provider, pathUID, authorizer, repoStore, spaceStore, pipelineStore, principalStore, ruleStore, protectionManager, gitrpcInterface, repository, codeownersService)
executionStore := database.ProvideExecutionStore(db)
checkStore := database.ProvideCheckStore(db, principalInfoCache)
stageStore := database.ProvideStageStore(db)

View File

@ -281,4 +281,8 @@ type Config struct {
Endpoint string `envconfig:"GITNESS_METRIC_ENDPOINT" default:"https://stats.drone.ci/api/v1/gitness"`
Token string `envconfig:"GITNESS_METRIC_TOKEN"`
}
CodeOwners struct {
CodeOwnerFilePath string `envconfig:"GITNESS_CODEOWNERS_FILEPATH" default:".gitness/CODEOWNERS"`
}
}