github.com/argoproj/argo-events@v1.9.1/sensors/artifacts/git.go (about)

     1  /*
     2  Copyright 2018 BlackRock, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package artifacts
    18  
    19  import (
    20  	"fmt"
    21  	"os"
    22  	"path"
    23  	"strings"
    24  
    25  	"github.com/go-git/go-git/v5"
    26  	"github.com/go-git/go-git/v5/config"
    27  	"github.com/go-git/go-git/v5/plumbing"
    28  	"github.com/go-git/go-git/v5/plumbing/transport"
    29  	"github.com/go-git/go-git/v5/plumbing/transport/http"
    30  	go_git_ssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
    31  	"golang.org/x/crypto/ssh"
    32  
    33  	"github.com/argoproj/argo-events/common"
    34  	"github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1"
    35  )
    36  
    37  const (
    38  	DefaultRemote = "origin"
    39  	DefaultBranch = "master"
    40  )
    41  
    42  var (
    43  	fetchRefSpec = []config.RefSpec{
    44  		"refs/*:refs/*",
    45  		"HEAD:refs/heads/HEAD",
    46  	}
    47  
    48  	notAllowedInPath = []string{"..", "~", "\\"}
    49  )
    50  
    51  type GitArtifactReader struct {
    52  	artifact *v1alpha1.GitArtifact
    53  }
    54  
    55  // NewGitReader returns a new git reader
    56  func NewGitReader(gitArtifact *v1alpha1.GitArtifact) (*GitArtifactReader, error) {
    57  	if gitArtifact == nil {
    58  		return nil, fmt.Errorf("nil git artifact")
    59  	}
    60  	for _, na := range notAllowedInPath {
    61  		if strings.Contains(gitArtifact.FilePath, na) {
    62  			return nil, fmt.Errorf("%q is not allowed in the filePath", na)
    63  		}
    64  		if strings.Contains(gitArtifact.CloneDirectory, na) {
    65  			return nil, fmt.Errorf("%q is not allowed in the cloneDirectory", na)
    66  		}
    67  	}
    68  
    69  	return &GitArtifactReader{
    70  		artifact: gitArtifact,
    71  	}, nil
    72  }
    73  
    74  func (g *GitArtifactReader) getRemote() string {
    75  	if g.artifact.Remote != nil {
    76  		return g.artifact.Remote.Name
    77  	}
    78  	return DefaultRemote
    79  }
    80  
    81  func getSSHKeyAuth(sshKeyFile string, insecureIgnoreHostKey bool) (transport.AuthMethod, error) {
    82  	sshKey, err := os.ReadFile(sshKeyFile)
    83  	if err != nil {
    84  		return nil, fmt.Errorf("failed to read ssh key file. err: %+v", err)
    85  	}
    86  	signer, err := ssh.ParsePrivateKey(sshKey)
    87  	if err != nil {
    88  		return nil, fmt.Errorf("failed to parse ssh key. err: %+v", err)
    89  	}
    90  	auth := &go_git_ssh.PublicKeys{User: "git", Signer: signer}
    91  	if insecureIgnoreHostKey {
    92  		auth.HostKeyCallback = ssh.InsecureIgnoreHostKey()
    93  	}
    94  	return auth, nil
    95  }
    96  
    97  func (g *GitArtifactReader) getGitAuth() (transport.AuthMethod, error) {
    98  	if g.artifact.Creds != nil {
    99  		username, err := common.GetSecretFromVolume(g.artifact.Creds.Username)
   100  		if err != nil {
   101  			return nil, fmt.Errorf("failed to retrieve username, %w", err)
   102  		}
   103  		password, err := common.GetSecretFromVolume(g.artifact.Creds.Password)
   104  		if err != nil {
   105  			return nil, fmt.Errorf("failed to retrieve password, %w", err)
   106  		}
   107  		return &http.BasicAuth{
   108  			Username: username,
   109  			Password: password,
   110  		}, nil
   111  	}
   112  	if g.artifact.SSHKeySecret != nil {
   113  		sshKeyPath, err := common.GetSecretVolumePath(g.artifact.SSHKeySecret)
   114  		if err != nil {
   115  			return nil, fmt.Errorf("failed to get SSH key from mounted volume, %w", err)
   116  		}
   117  		return getSSHKeyAuth(sshKeyPath, g.artifact.InsecureIgnoreHostKey)
   118  	}
   119  	return nil, nil
   120  }
   121  
   122  func (g *GitArtifactReader) readFromRepository(r *git.Repository, dir string) ([]byte, error) {
   123  	auth, err := g.getGitAuth()
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	if g.artifact.Remote != nil {
   129  		_, err := r.CreateRemote(&config.RemoteConfig{
   130  			Name: g.artifact.Remote.Name,
   131  			URLs: g.artifact.Remote.URLS,
   132  		})
   133  		if err != nil {
   134  			return nil, fmt.Errorf("failed to create remote. err: %w", err)
   135  		}
   136  
   137  		fetchOptions := &git.FetchOptions{
   138  			RemoteName: g.artifact.Remote.Name,
   139  			RefSpecs:   fetchRefSpec,
   140  			Force:      true,
   141  		}
   142  		if auth != nil {
   143  			fetchOptions.Auth = auth
   144  		}
   145  
   146  		if err := r.Fetch(fetchOptions); err != nil {
   147  			return nil, fmt.Errorf("failed to fetch remote %s. err: %w", g.artifact.Remote.Name, err)
   148  		}
   149  	}
   150  
   151  	w, err := r.Worktree()
   152  	if err != nil {
   153  		return nil, fmt.Errorf("failed to get working tree. err: %w", err)
   154  	}
   155  
   156  	fetchOptions := &git.FetchOptions{
   157  		RemoteName: g.getRemote(),
   158  		RefSpecs:   fetchRefSpec,
   159  		Force:      true,
   160  	}
   161  	if auth != nil {
   162  		fetchOptions.Auth = auth
   163  	}
   164  
   165  	// In the case of a specific given ref, it isn't necessary to fetch anything
   166  	// but the single ref
   167  	if g.artifact.Ref != "" {
   168  		fetchOptions.Depth = 1
   169  		fetchOptions.RefSpecs = []config.RefSpec{config.RefSpec(g.artifact.Ref + ":" + g.artifact.Ref)}
   170  	}
   171  
   172  	if err := r.Fetch(fetchOptions); err != nil && err != git.NoErrAlreadyUpToDate {
   173  		return nil, fmt.Errorf("failed to fetch. err: %v", err)
   174  	}
   175  
   176  	if err := w.Checkout(g.getBranchOrTag()); err != nil {
   177  		return nil, fmt.Errorf("failed to checkout. err: %+v", err)
   178  	}
   179  
   180  	// In the case of a specific given ref, it shouldn't be necessary to pull
   181  	if g.artifact.Ref != "" {
   182  		pullOpts := &git.PullOptions{
   183  			RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
   184  			ReferenceName:     g.getBranchOrTag().Branch,
   185  			Force:             true,
   186  		}
   187  		if auth != nil {
   188  			pullOpts.Auth = auth
   189  		}
   190  
   191  		if err := w.Pull(pullOpts); err != nil && err != git.NoErrAlreadyUpToDate {
   192  			return nil, fmt.Errorf("failed to pull latest updates. err: %+v", err)
   193  		}
   194  	}
   195  	filePath := fmt.Sprintf("%s/%s", dir, g.artifact.FilePath)
   196  	// symbol link is not allowed due to security concern
   197  	isSymbolLink, err := isSymbolLink(filePath)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  	if isSymbolLink {
   202  		return nil, fmt.Errorf("%q is a symbol link which is not allowed", g.artifact.FilePath)
   203  	}
   204  	return os.ReadFile(filePath)
   205  }
   206  
   207  func (g *GitArtifactReader) getBranchOrTag() *git.CheckoutOptions {
   208  	opts := &git.CheckoutOptions{}
   209  
   210  	opts.Branch = plumbing.NewBranchReferenceName(DefaultBranch)
   211  
   212  	if g.artifact.Branch != "" {
   213  		opts.Branch = plumbing.NewBranchReferenceName(g.artifact.Branch)
   214  	}
   215  	if g.artifact.Tag != "" {
   216  		opts.Branch = plumbing.NewTagReferenceName(g.artifact.Tag)
   217  	}
   218  	if g.artifact.Ref != "" {
   219  		opts.Branch = plumbing.ReferenceName(g.artifact.Ref)
   220  	}
   221  
   222  	return opts
   223  }
   224  
   225  func (g *GitArtifactReader) Read() ([]byte, error) {
   226  	cloneDir := g.artifact.CloneDirectory
   227  	if cloneDir == "" {
   228  		tempDir, err := os.MkdirTemp("", "git-tmp")
   229  		if err != nil {
   230  			return nil, fmt.Errorf("failed to create a temp file to clone the repository, %w", err)
   231  		}
   232  		defer os.Remove(tempDir)
   233  		cloneDir = tempDir
   234  	}
   235  
   236  	r, err := git.PlainOpen(cloneDir)
   237  	if err != nil {
   238  		if err != git.ErrRepositoryNotExists {
   239  			return nil, fmt.Errorf("failed to open repository. err: %w", err)
   240  		}
   241  
   242  		cloneOpt := &git.CloneOptions{
   243  			URL:               g.artifact.URL,
   244  			RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
   245  		}
   246  
   247  		auth, err := g.getGitAuth()
   248  		if err != nil {
   249  			return nil, err
   250  		}
   251  		if auth != nil {
   252  			cloneOpt.Auth = auth
   253  		}
   254  
   255  		// In the case of a specific given ref, it isn't necessary to have branch
   256  		// histories
   257  		if g.artifact.Ref != "" {
   258  			cloneOpt.Depth = 1
   259  		}
   260  
   261  		r, err = git.PlainClone(cloneDir, false, cloneOpt)
   262  		if err != nil {
   263  			return nil, fmt.Errorf("failed to clone repository. err: %+v", err)
   264  		}
   265  	}
   266  	return g.readFromRepository(r, cloneDir)
   267  }
   268  
   269  func isSymbolLink(filepath string) (bool, error) {
   270  	fi, err := os.Lstat(path.Clean(filepath))
   271  	if err != nil {
   272  		return false, err
   273  	}
   274  	if fi.Mode()&os.ModeSymlink != 0 {
   275  		return true, nil
   276  	}
   277  	return false, nil
   278  }