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 }