Files
kandev/apps/backend/internal/task/service/local_repository_initialization.go
T

375 lines
12 KiB
Go

package service
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/kandev/kandev/internal/task/gitinit"
"github.com/kandev/kandev/internal/task/models"
"go.uber.org/zap"
)
var (
ErrInvalidLocalRepositoryInitialization = errors.New("invalid local repository initialization")
ErrLocalRepositoryTargetExists = errors.New("local repository target exists")
errLocalRepositoryTargetChanged = errors.New("local repository target changed during initialization")
)
type initializeGitRepositoryFunc func(context.Context, string, *os.File) error
const windowsGOOS = "windows"
type localRepositoryStaging struct {
path string
directory *os.File
identity fs.FileInfo
}
// InitializeLocalRepository creates an empty Git repository and registers it with a workspace.
func (s *Service) InitializeLocalRepository(
ctx context.Context,
req *InitializeLocalRepositoryRequest,
) (*models.Repository, error) {
return s.initializeLocalRepository(ctx, req, initializeGitRepository)
}
func (s *Service) initializeLocalRepository(
ctx context.Context,
req *InitializeLocalRepositoryRequest,
initializeGit initializeGitRepositoryFunc,
) (*models.Repository, error) {
if _, err := s.workspaces.GetWorkspace(ctx, req.WorkspaceID); err != nil {
return nil, err
}
name := strings.TrimSpace(req.Name)
if err := validateLocalRepositoryName(name); err != nil {
return nil, err
}
parentPath, err := canonicalLocalRepositoryParent(req.ParentPath)
if err != nil {
return nil, err
}
targetPath := filepath.Join(parentPath, name)
if _, statErr := lstatLocalRepositoryPath(targetPath); statErr == nil {
return nil, fmt.Errorf("%w: %s", ErrLocalRepositoryTargetExists, targetPath)
} else if !errors.Is(statErr, fs.ErrNotExist) {
return nil, fmt.Errorf("%w: target path cannot be inspected", ErrInvalidLocalRepositoryInitialization)
}
staging, err := createLocalRepositoryStaging(parentPath)
if err != nil {
return nil, err
}
defer s.closeLocalRepositoryStaging(staging)
published := false
cleanup := func() {
cleanupPath := staging.path
if published {
cleanupPath = targetPath
}
s.cleanupInitializedLocalRepository(cleanupPath, staging.identity)
}
if initErr := initializeGit(ctx, staging.path, staging.directory); initErr != nil {
cleanup()
return nil, fmt.Errorf("initialize git repository: %w", initErr)
}
if !localRepositoryTargetMatches(staging.path, staging.identity) {
cleanup()
return nil, errLocalRepositoryTargetChanged
}
if publishErr := publishLocalRepository(staging.path, targetPath); publishErr != nil {
cleanup()
if errors.Is(publishErr, fs.ErrExist) {
return nil, fmt.Errorf("%w: %s", ErrLocalRepositoryTargetExists, targetPath)
}
return nil, fmt.Errorf("publish initialized local repository: %w", publishErr)
}
published = true
if chmodErr := setPublishedLocalRepositoryPermissions(staging.directory); chmodErr != nil {
cleanup()
return nil, fmt.Errorf("set initialized local repository permissions: %w", chmodErr)
}
if !localRepositoryTargetMatches(targetPath, staging.identity) {
cleanup()
return nil, errLocalRepositoryTargetChanged
}
repository, err := s.createRepositoryWithCanonicalPath(ctx, &CreateRepositoryRequest{
WorkspaceID: req.WorkspaceID,
Name: name,
SourceType: sourceTypeLocal,
LocalPath: targetPath,
DefaultBranch: "main",
})
if err != nil {
cleanup()
return nil, fmt.Errorf("persist initialized local repository: %w", err)
}
return repository, nil
}
func createLocalRepositoryStaging(parentPath string) (*localRepositoryStaging, error) {
filesystemRoot, parentRelativePath, err := openLocalRepositoryFilesystemRoot(parentPath)
if err != nil {
return nil, fmt.Errorf("%w: parent directory is not writable", ErrInvalidLocalRepositoryInitialization)
}
defer func() { _ = filesystemRoot.Close() }()
parentRoot, err := filesystemRoot.OpenRoot(parentRelativePath)
if err != nil {
return nil, fmt.Errorf("%w: parent directory is not writable", ErrInvalidLocalRepositoryInitialization)
}
defer func() { _ = parentRoot.Close() }()
stagingName, stagingDirectory, err := createLocalRepositoryStagingDirectory(parentRoot)
if err != nil {
return nil, err
}
stagingPath := filepath.Join(parentPath, stagingName)
stagingPathInfo, err := parentRoot.Lstat(stagingName)
if err != nil || !stagingPathInfo.IsDir() || stagingPathInfo.Mode()&os.ModeSymlink != 0 {
_ = stagingDirectory.Close()
_ = parentRoot.Remove(stagingName)
return nil, errLocalRepositoryTargetChanged
}
stagingIdentity, err := stagingDirectory.Stat()
if err != nil || !os.SameFile(stagingPathInfo, stagingIdentity) ||
!localRepositoryDirectoryOwnedByProcess(stagingIdentity) ||
!localRepositoryStagingPermissionsPrivate(stagingIdentity) {
_ = stagingDirectory.Close()
_ = parentRoot.Remove(stagingName)
return nil, errLocalRepositoryTargetChanged
}
if entries, readErr := stagingDirectory.ReadDir(1); len(entries) != 0 || !errors.Is(readErr, io.EOF) {
_ = stagingDirectory.Close()
_ = parentRoot.Remove(stagingName)
return nil, errLocalRepositoryTargetChanged
}
return &localRepositoryStaging{
path: stagingPath,
directory: stagingDirectory,
identity: stagingIdentity,
}, nil
}
func createLocalRepositoryStagingDirectory(root *os.Root) (string, *os.File, error) {
for range 8 {
name, err := localRepositoryStagingName()
if err != nil {
return "", nil, fmt.Errorf("create staging directory name: %w", err)
}
if err := root.Mkdir(name, 0o700); err != nil {
if errors.Is(err, fs.ErrExist) {
continue
}
return "", nil, fmt.Errorf("%w: parent directory is not writable", ErrInvalidLocalRepositoryInitialization)
}
directory, err := root.Open(name)
if err != nil {
_ = root.Remove(name)
return "", nil, fmt.Errorf("open local repository staging directory: %w", err)
}
return name, directory, nil
}
return "", nil, fmt.Errorf("%w: could not reserve staging directory", ErrInvalidLocalRepositoryInitialization)
}
func localRepositoryStagingName() (string, error) {
var suffix [12]byte
if _, err := rand.Read(suffix[:]); err != nil {
return "", err
}
return ".kandev-repository-init-" + hex.EncodeToString(suffix[:]), nil
}
func (s *Service) closeLocalRepositoryStaging(staging *localRepositoryStaging) {
if err := staging.directory.Close(); err != nil {
s.logger.Warn("failed to close local repository staging directory",
zap.String("path", staging.path), zap.Error(err))
}
}
func initializeGitRepository(ctx context.Context, targetPath string, targetDirectory *os.File) error {
command, err := gitinit.CommandContext(ctx, targetPath, targetDirectory)
if err != nil {
return err
}
output, err := command.CombinedOutput()
if err != nil {
return fmt.Errorf("%w: %s", err, strings.TrimSpace(string(output)))
}
return nil
}
func setPublishedLocalRepositoryPermissions(directory *os.File) error {
if runtime.GOOS == windowsGOOS {
return nil
}
return directory.Chmod(0o755)
}
func (s *Service) cleanupInitializedLocalRepository(
targetPath string,
targetIdentity fs.FileInfo,
) {
filesystemRoot, relativePath, err := openLocalRepositoryFilesystemRoot(targetPath)
if err != nil {
s.logger.Warn("failed to open local repository root for cleanup",
zap.String("path", targetPath), zap.Error(err))
return
}
defer func() { _ = filesystemRoot.Close() }()
targetDirectory, err := filesystemRoot.OpenRoot(relativePath)
if err != nil {
s.logger.Warn("refusing to remove replaced local repository directory",
zap.String("path", targetPath), zap.Error(err))
return
}
defer func() { _ = targetDirectory.Close() }()
targetInfo, err := targetDirectory.Stat(".")
if err != nil || !os.SameFile(targetIdentity, targetInfo) {
s.logger.Warn("refusing to remove replaced local repository directory",
zap.String("path", targetPath))
return
}
if err := removeLocalRepositoryRootTree(targetDirectory, ".git"); err != nil && !errors.Is(err, fs.ErrNotExist) {
s.logger.Warn("failed to clean up initialized Git metadata",
zap.String("path", targetPath), zap.Error(err))
}
if err := targetDirectory.Close(); err != nil {
s.logger.Warn("failed to close initialized local repository directory",
zap.String("path", targetPath), zap.Error(err))
return
}
if err := filesystemRoot.Remove(relativePath); err != nil {
s.logger.Warn("failed to remove initialized local repository directory",
zap.String("path", targetPath), zap.Error(err))
}
}
func removeLocalRepositoryRootTree(root *os.Root, name string) error {
directory, err := root.Open(name)
if err != nil {
return err
}
entries, readErr := directory.ReadDir(-1)
closeErr := directory.Close()
if readErr != nil {
return readErr
}
if closeErr != nil {
return closeErr
}
for _, entry := range entries {
entryName := filepath.Join(name, entry.Name())
if entry.IsDir() {
if err := removeLocalRepositoryRootTree(root, entryName); err != nil {
return err
}
continue
}
if err := root.Remove(entryName); err != nil {
return err
}
}
return root.Remove(name)
}
func localRepositoryTargetMatches(targetPath string, createdTarget fs.FileInfo) bool {
currentTarget, err := lstatLocalRepositoryPath(targetPath)
return err == nil && currentTarget.IsDir() && os.SameFile(createdTarget, currentTarget)
}
func validateLocalRepositoryName(name string) error {
if name == "" || name == "." || name == ".." || strings.ContainsAny(name, `/\`) || strings.ContainsRune(name, 0) {
return fmt.Errorf("%w: name must be a single path segment", ErrInvalidLocalRepositoryInitialization)
}
return nil
}
func canonicalLocalRepositoryParent(parentPath string) (string, error) {
if parentPath == "" || !filepath.IsAbs(parentPath) {
return "", fmt.Errorf("%w: parent_path must be absolute", ErrInvalidLocalRepositoryInitialization)
}
canonicalPath, err := ensureLocalRepositoryDirectory(filepath.Clean(parentPath))
if err != nil {
return "", fmt.Errorf("%w: parent directory cannot be accessed", ErrInvalidLocalRepositoryInitialization)
}
if err := validateLocalRepositoryParentDirectory(canonicalPath); err != nil {
return "", err
}
return canonicalPath, nil
}
func validateLocalRepositoryParentDirectory(canonicalPath string) error {
info, err := statLocalRepositoryPath(canonicalPath)
if err != nil || !info.IsDir() {
return fmt.Errorf("%w: parent_path must be an accessible directory", ErrInvalidLocalRepositoryInitialization)
}
if !localRepositoryParentWritable(info) {
return fmt.Errorf("%w: parent directory is not writable", ErrInvalidLocalRepositoryInitialization)
}
if err := validateLocalRepositoryParentChain(canonicalPath); err != nil {
return err
}
return nil
}
func validateLocalRepositoryParentChain(parentPath string) error {
filesystemRoot, relativePath, err := openLocalRepositoryFilesystemRoot(parentPath)
if err != nil {
return fmt.Errorf("%w: parent directory ownership is not trusted", ErrInvalidLocalRepositoryInitialization)
}
defer func() { _ = filesystemRoot.Close() }()
for path := relativePath; ; path = filepath.Dir(path) {
info, err := filesystemRoot.Lstat(path)
if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 ||
!localRepositoryDirectoryOwnerTrusted(info) {
return fmt.Errorf("%w: parent directory ownership is not trusted", ErrInvalidLocalRepositoryInitialization)
}
if localRepositoryParentSharedWritable(info) {
return fmt.Errorf("%w: parent directory must not be shared writable", ErrInvalidLocalRepositoryInitialization)
}
if path == "." {
return nil
}
}
}
func lstatLocalRepositoryPath(path string) (fs.FileInfo, error) {
filesystemRoot, relativePath, err := openLocalRepositoryFilesystemRoot(path)
if err != nil {
return nil, err
}
defer func() { _ = filesystemRoot.Close() }()
return filesystemRoot.Lstat(relativePath)
}
func statLocalRepositoryPath(path string) (fs.FileInfo, error) {
filesystemRoot, relativePath, err := openLocalRepositoryFilesystemRoot(path)
if err != nil {
return nil, err
}
defer func() { _ = filesystemRoot.Close() }()
return filesystemRoot.Stat(relativePath)
}
func openLocalRepositoryFilesystemRoot(path string) (*os.Root, string, error) {
rootPath, relativePath := splitAbsForRoot(path)
filesystemRoot, err := os.OpenRoot(rootPath)
if err != nil {
return nil, "", err
}
return filesystemRoot, relativePath, nil
}