mirror of https://github.com/harness/drone.git
fix: [CODE-965]: codeowners file parse (#684)
parent
c6dca7d7ae
commit
1a07ee90d2
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue