drone/gitrpc/internal/service/repo.go

248 lines
7.0 KiB
Go

// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package service
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"path"
"regexp"
"github.com/harness/gitness/gitrpc/internal/types"
"github.com/harness/gitness/gitrpc/rpc"
"github.com/rs/zerolog/log"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
maxFileSize = 1 << 20
gitRepoSuffix = "git"
gitReferenceNamePrefixBranch = "refs/heads/"
gitReferenceNamePrefixTag = "refs/tags/"
gitHooksDir = "hooks"
)
var (
// TODO: should be coming from caller ALWAYS.
SystemIdentity = &rpc.Identity{
Name: "gitness",
Email: "system@gitness",
}
gitServerHookNames = []string{
"pre-receive",
// "update", // update is disabled for performance reasons (called once for every ref)
"post-receive",
}
// gitSHARegex defines the valid SHA format accepted by GIT (full form and short forms).
// Note: as of now SHA is at most 40 characters long, but in the future it's moving to sha256
// which is 64 chars - keep this forward-compatible.
gitSHARegex = regexp.MustCompile("^[0-9a-f]{4,64}$")
)
type Storage interface {
Save(filePath string, data bytes.Buffer) (string, error)
}
type RepositoryService struct {
rpc.UnimplementedRepositoryServiceServer
adapter GitAdapter
store Storage
reposRoot string
gitHookPath string
reposGraveyard string
}
func NewRepositoryService(adapter GitAdapter, store Storage, reposRoot string,
gitHookPath string, reposGraveyard string) (*RepositoryService, error) {
return &RepositoryService{
adapter: adapter,
store: store,
reposRoot: reposRoot,
gitHookPath: gitHookPath,
reposGraveyard: reposGraveyard,
}, nil
}
//nolint:gocognit,funlen // need to refactor this code
func (s RepositoryService) CreateRepository(stream rpc.RepositoryService_CreateRepositoryServer) error {
ctx := stream.Context()
log := log.Ctx(ctx)
// first get repo params from stream
request, err := stream.Recv()
if err != nil {
return status.Errorf(codes.Internal, "cannot receive create repository data")
}
header := request.GetHeader()
if header == nil {
return status.Errorf(codes.Internal, "expected header to be first message in stream")
}
log.Info().Msgf("received a create repository request %v", header)
base := header.GetBase()
if base == nil {
return types.ErrBaseCannotBeEmpty
}
repoPath := getFullPathForRepo(s.reposRoot, base.GetRepoUid())
if _, err = os.Stat(repoPath); !os.IsNotExist(err) {
return status.Errorf(codes.AlreadyExists, "repository exists already: %v", repoPath)
}
// create repository in repos folder
ctx, cancel := context.WithCancel(ctx)
defer cancel()
err = s.adapter.InitRepository(ctx, repoPath, true)
// delete repo dir on error
defer func() {
if err != nil {
cleanuperr := s.DeleteRepositoryBestEffort(ctx, base.GetRepoUid())
if cleanuperr != nil {
log.Warn().Err(cleanuperr).Msg("failed to cleanup repo dir")
}
}
}()
if err != nil {
return processGitErrorf(err, "failed to initialize the repository")
}
// update default branch (currently set to non-existent branch)
err = s.adapter.SetDefaultBranch(ctx, repoPath, header.GetDefaultBranch(), true)
if err != nil {
return processGitErrorf(err, "error updating default branch for repo '%s'", base.GetRepoUid())
}
// we need temp dir for cloning
tempDir, err := os.MkdirTemp("", "*-"+base.GetRepoUid())
if err != nil {
return fmt.Errorf("error creating temp dir for repo %s: %w", base.GetRepoUid(), err)
}
defer func(path string) {
// when repo is successfully created remove temp dir
errRm := os.RemoveAll(path)
if errRm != nil {
log.Err(errRm).Msg("failed to cleanup temporary dir.")
}
}(tempDir)
// Clone repository to temp dir
if err = s.adapter.Clone(ctx, repoPath, tempDir, types.CloneRepoOptions{}); err != nil {
return processGitErrorf(err, "failed to clone repo")
}
// logic for receiving files
filePaths := make([]string, 0, 16)
for {
var filePath string
filePath, err = s.handleFileUploadIfAvailable(ctx, tempDir, func() (*rpc.FileUpload, error) {
m, errStream := stream.Recv()
if errStream != nil {
return nil, errStream
}
return m.GetFile(), nil
})
if errors.Is(err, io.EOF) {
log.Info().Msg("received stream EOF")
break
}
if err != nil {
return status.Errorf(codes.Internal, "failed to receive file: %v", err)
}
filePaths = append(filePaths, filePath)
}
if len(filePaths) > 0 {
committer := base.GetActor()
if header.GetCommitter() != nil {
committer = header.GetCommitter()
}
author := committer
if header.GetAuthor() != nil {
author = header.GetAuthor()
}
// NOTE: This creates the branch in origin repo (as it doesn't exist as of now)
// TODO: this should at least be a constant and not hardcoded?
if err = s.AddFilesAndPush(ctx, tempDir, filePaths, "HEAD:"+header.GetDefaultBranch(), author, committer,
"origin", "initial commit"); err != nil {
return err
}
}
// setup server hook symlinks pointing to configured server hook binary
for _, hook := range gitServerHookNames {
hookPath := path.Join(repoPath, gitHooksDir, hook)
err = os.Symlink(s.gitHookPath, hookPath)
if err != nil {
return status.Errorf(codes.Internal,
"failed to setup symlink for hook '%s' ('%s' -> '%s'): %s", hook, hookPath, s.gitHookPath, err)
}
}
res := &rpc.CreateRepositoryResponse{}
err = stream.SendAndClose(res)
if err != nil {
return status.Errorf(codes.Internal, "cannot send completion response: %v", err)
}
log.Info().Msgf("repository created. Path: %s", repoPath)
return nil
}
// isValidGitSHA returns true iff the provided string is a valid git sha (short or long form).
func isValidGitSHA(sha string) bool {
return gitSHARegex.MatchString(sha)
}
func (s RepositoryService) DeleteRepository(
ctx context.Context,
request *rpc.DeleteRepositoryRequest,
) (*rpc.DeleteRepositoryResponse, error) {
base := request.GetBase()
if base == nil {
return nil, types.ErrBaseCannotBeEmpty
}
repoPath := getFullPathForRepo(s.reposRoot, base.RepoUid)
if _, err := os.Stat(repoPath); err != nil && os.IsNotExist(err) {
return nil, ErrNotFound(err)
} else if err != nil {
return nil, fmt.Errorf("failed to check the status of the repository %v: %w", repoPath, err)
}
rmerr := s.DeleteRepositoryBestEffort(ctx, base.RepoUid)
return &rpc.DeleteRepositoryResponse{}, rmerr
}
func (s *RepositoryService) DeleteRepositoryBestEffort(ctx context.Context, repoUID string) error {
repoPath := getFullPathForRepo(s.reposRoot, repoUID)
tempPath := path.Join(s.reposGraveyard, repoUID)
// move current dir to a temp dir (prevent partial deletion)
if err := os.Rename(repoPath, tempPath); err != nil {
return fmt.Errorf("couldn't move dir %s to %s : %w", repoPath, tempPath, err)
}
if err := os.RemoveAll(tempPath); err != nil {
log.Ctx(ctx).Warn().Err(err).Msgf("failed to delete dir %s from graveyard", tempPath)
}
return nil
}