Files
portainer/api/git/git.go
T

190 lines
4.4 KiB
Go

package git
import (
"context"
"os"
"path/filepath"
"strings"
"github.com/portainer/portainer/api/filesystem"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/object"
gogitfs "github.com/go-git/go-git/v5/storage/filesystem"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
// noSymlinkFS wraps a billy.Filesystem and rejects symlink creation to prevent
// symlink traversal attacks from untrusted git repositories
type noSymlinkFS struct {
billy.Filesystem
}
func (fs noSymlinkFS) Symlink(_, _ string) error {
return gittypes.ErrSymlinkDetected
}
// NewNoSymlinkFS wraps fs and rejects any symlink creation
func NewNoSymlinkFS(fs billy.Filesystem) billy.Filesystem {
return noSymlinkFS{fs}
}
type gitClient struct {
preserveGitDirectory bool
}
func NewGitClient(preserveGitDir bool) *gitClient {
return &gitClient{
preserveGitDirectory: preserveGitDir,
}
}
func (c *gitClient) Download(ctx context.Context, dst string, opt *git.CloneOptions) error {
resolved, err := filepath.EvalSymlinks(dst)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return errors.Wrap(err, "failed to resolve destination path")
}
if err == nil {
dst = resolved
}
wt := NewNoSymlinkFS(osfs.New(dst))
dot := osfs.New(filesystem.JoinPaths(dst, ".git"))
storer := gogitfs.NewStorage(dot, cache.NewObjectLRU(0))
_, err = git.CloneContext(ctx, storer, wt, opt)
if err != nil {
if err.Error() == "authentication required" {
return gittypes.ErrAuthenticationFailure
}
return errors.Wrap(err, "failed to clone git repository")
}
if c.preserveGitDirectory {
return nil
}
if err := os.RemoveAll(filesystem.JoinPaths(dst, ".git")); err != nil {
log.Error().Err(err).Msg("failed to remove .git directory")
}
return nil
}
func (c *gitClient) LatestCommitID(ctx context.Context, repositoryUrl, referenceName string, opt *git.ListOptions) (string, error) {
remote := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{
Name: "origin",
URLs: []string{repositoryUrl},
})
refs, err := remote.ListContext(ctx, opt)
if err != nil {
if err.Error() == "authentication required" {
return "", gittypes.ErrAuthenticationFailure
}
return "", errors.Wrap(err, "failed to list repository refs")
}
if referenceName == "" {
for _, ref := range refs {
if strings.EqualFold(ref.Name().String(), "HEAD") {
referenceName = ref.Target().String()
}
}
}
for _, ref := range refs {
if strings.EqualFold(ref.Name().String(), referenceName) {
return ref.Hash().String(), nil
}
}
return "", errors.Errorf("could not find ref %q in the repository", referenceName)
}
func (c *gitClient) ListRefs(ctx context.Context, repositoryUrl string, opt *git.ListOptions) ([]string, error) {
rem := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{
Name: "origin",
URLs: []string{repositoryUrl},
})
refs, err := rem.ListContext(ctx, opt)
if err != nil {
return nil, checkGitError(err)
}
var ret []string
for _, ref := range refs {
if ref.Name().String() == "HEAD" {
continue
}
ret = append(ret, ref.Name().String())
}
return ret, nil
}
// listFiles list all filenames under the specific repository
func (c *gitClient) ListFiles(ctx context.Context, dirOnly bool, opt *git.CloneOptions) ([]string, error) {
repo, err := git.Clone(memory.NewStorage(), nil, opt)
if err != nil {
return nil, checkGitError(err)
}
head, err := repo.Head()
if err != nil {
return nil, err
}
commit, err := repo.CommitObject(head.Hash())
if err != nil {
return nil, err
}
tree, err := commit.Tree()
if err != nil {
return nil, err
}
var allPaths []string
w := object.NewTreeWalker(tree, true, nil)
defer w.Close()
for {
name, entry, err := w.Next()
if err != nil {
break
}
isDir := entry.Mode == filemode.Dir
if dirOnly == isDir {
allPaths = append(allPaths, name)
}
}
return allPaths, nil
}
func checkGitError(err error) error {
errMsg := err.Error()
if strings.Contains(errMsg, "repository not found") {
return gittypes.ErrIncorrectRepositoryURL
} else if errMsg == "authentication required" {
return gittypes.ErrAuthenticationFailure
}
return err
}