github.com/chasestarr/deis@v1.13.5-0.20170519182049-1d9e59fbdbfc/builder/git/git.go (about) 1 package git 2 3 // This file just contains the Git-specific portions of sshd. 4 5 import ( 6 "bytes" 7 "errors" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "strings" 15 "sync" 16 "text/template" 17 18 "github.com/Masterminds/cookoo" 19 "github.com/Masterminds/cookoo/log" 20 "golang.org/x/crypto/ssh" 21 ) 22 23 // PrereceiveHookTmpl is a pre-receive hook. 24 // 25 // This is overridable. The following template variables are passed into it: 26 // 27 // .GitHome: the path to Git's home directory. 28 var PrereceiveHookTpl = `#!/bin/bash 29 strip_remote_prefix() { 30 stdbuf -i0 -o0 -e0 sed "s/^/"$'\e[1G'"/" 31 } 32 33 set -eo pipefail 34 35 while read oldrev newrev refname 36 do 37 LOCKFILE="/tmp/$RECEIVE_REPO.lock" 38 if ( set -o noclobber; echo "$$" > "$LOCKFILE" ) 2> /dev/null; then 39 trap 'rm -f "$LOCKFILE"; exit 1' INT TERM EXIT 40 41 # check for authorization on this repo 42 {{.GitHome}}/receiver "$RECEIVE_REPO" "$newrev" "$RECEIVE_USER" "$RECEIVE_FINGERPRINT" 43 rc=$? 44 if [[ $rc != 0 ]] ; then 45 echo " ERROR: failed on rev $newrev - push denied" 46 exit $rc 47 fi 48 # builder assumes that we are running this script from $GITHOME 49 cd {{.GitHome}} 50 # if we're processing a receive-pack on an existing repo, run a build 51 if [[ $SSH_ORIGINAL_COMMAND == git-receive-pack* ]]; then 52 {{.GitHome}}/builder "$RECEIVE_USER" "$RECEIVE_REPO" "$newrev" 2>&1 | strip_remote_prefix 53 fi 54 55 rm -f "$LOCKFILE" 56 trap - INT TERM EXIT 57 else 58 echo "Another git push is ongoing. Aborting..." 59 exit 1 60 fi 61 done 62 ` 63 64 // Receive receives a Git repo. 65 // This will only work for git-receive-pack. 66 // 67 // Params: 68 // - operation (string): e.g. git-receive-pack 69 // - repoName (string): The repository name, in the form '/REPO.git'. 70 // - channel (ssh.Channel): The channel. 71 // - request (*ssh.Request): The channel. 72 // - gitHome (string): Defaults to /home/git. 73 // - fingerprint (string): The fingerprint of the user's SSH key. 74 // - user (string): The name of the Deis user. 75 // 76 // Returns: 77 // - nothing 78 func Receive(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { 79 if ok, z := p.Requires("channel", "request", "fingerprint", "permissions"); !ok { 80 return nil, fmt.Errorf("Missing requirements %q", z) 81 } 82 repoName := p.Get("repoName", "").(string) 83 operation := p.Get("operation", "").(string) 84 channel := p.Get("channel", nil).(ssh.Channel) 85 gitHome := p.Get("gitHome", "/home/git").(string) 86 fingerprint := p.Get("fingerprint", nil).(string) 87 user := p.Get("user", "").(string) 88 89 repo, err := cleanRepoName(repoName) 90 if err != nil { 91 log.Warnf(c, "Illegal repo name: %s.", err) 92 channel.Stderr().Write([]byte("No repo given")) 93 return nil, err 94 } 95 repo += ".git" 96 97 if _, err := createRepo(c, filepath.Join(gitHome, repo), gitHome); err != nil { 98 log.Infof(c, "Did not create new repo: %s", err) 99 } 100 cmd := exec.Command("git-shell", "-c", fmt.Sprintf("%s '%s'", operation, repo)) 101 log.Infof(c, strings.Join(cmd.Args, " ")) 102 103 var errbuff bytes.Buffer 104 105 cmd.Dir = gitHome 106 cmd.Env = []string{ 107 fmt.Sprintf("RECEIVE_USER=%s", user), 108 fmt.Sprintf("RECEIVE_REPO=%s", repo), 109 fmt.Sprintf("RECEIVE_FINGERPRINT=%s", fingerprint), 110 fmt.Sprintf("SSH_ORIGINAL_COMMAND=%s '%s'", operation, repo), 111 fmt.Sprintf("SSH_CONNECTION=%s", c.Get("SSH_CONNECTION", "0 0 0 0").(string)), 112 } 113 cmd.Env = append(cmd.Env, os.Environ()...) 114 115 done := plumbCommand(cmd, channel, &errbuff) 116 117 if err := cmd.Start(); err != nil { 118 log.Warnf(c, "Failed git receive immediately: %s %s", err, errbuff.Bytes()) 119 return nil, err 120 } 121 fmt.Printf("Waiting for git-receive to run.\n") 122 done.Wait() 123 fmt.Printf("Waiting for deploy.\n") 124 if err := cmd.Wait(); err != nil { 125 log.Errf(c, "Error on command: %s %s", err, errbuff.Bytes()) 126 return nil, err 127 } 128 if errbuff.Len() > 0 { 129 log.Warnf(c, "Unreported error: %s", errbuff.Bytes()) 130 } 131 log.Infof(c, "Deploy complete.\n") 132 133 return nil, nil 134 } 135 136 func execAs(user, cmd string, args ...string) *exec.Cmd { 137 fullCmd := cmd + " " + strings.Join(args, " ") 138 return exec.Command("su", user, "-c", fullCmd) 139 } 140 141 // cleanRepoName cleans a repository name for a git-sh operation. 142 func cleanRepoName(name string) (string, error) { 143 if len(name) == 0 { 144 return name, errors.New("Empty repo name.") 145 } 146 if strings.Contains(name, "..") { 147 return "", errors.New("Cannot change directory in file name.") 148 } 149 name = strings.Replace(name, "'", "", -1) 150 return strings.TrimPrefix(strings.TrimSuffix(name, ".git"), "/"), nil 151 } 152 153 // plumbCommand connects the exec in/output and the channel in/output. 154 // 155 // The sidechannel is for sending errors to logs. 156 func plumbCommand(cmd *exec.Cmd, channel ssh.Channel, sidechannel io.Writer) *sync.WaitGroup { 157 var wg sync.WaitGroup 158 inpipe, _ := cmd.StdinPipe() 159 go func() { 160 io.Copy(inpipe, channel) 161 inpipe.Close() 162 }() 163 164 cmd.Stdout = channel 165 cmd.Stderr = channel.Stderr() 166 167 return &wg 168 } 169 170 var createLock sync.Mutex 171 172 // initRepo create a directory and init a new Git repo 173 func initRepo(repoPath, gitHome string, c cookoo.Context) (bool, error) { 174 log.Infof(c, "Creating new directory at %s", repoPath) 175 // Create directory 176 if err := os.MkdirAll(repoPath, 0755); err != nil { 177 log.Warnf(c, "Failed to create repository: %s", err) 178 return false, err 179 } 180 cmd := exec.Command("git", "init", "--bare") 181 cmd.Dir = repoPath 182 if out, err := cmd.CombinedOutput(); err != nil { 183 log.Warnf(c, "git init output: %s", out) 184 return false, err 185 } 186 187 hook, err := prereceiveHook(map[string]string{"GitHome": gitHome}) 188 if err != nil { 189 return true, err 190 } 191 ioutil.WriteFile(filepath.Join(repoPath, "hooks", "pre-receive"), hook, 0755) 192 193 return true, nil 194 } 195 196 // createRepo creates a new Git repo if it is not present already. 197 // 198 // Largely inspired by gitreceived from Flynn. 199 // 200 // Returns a bool indicating whether a project was created (true) or already 201 // existed (false). 202 func createRepo(c cookoo.Context, repoPath, gitHome string) (bool, error) { 203 createLock.Lock() 204 defer createLock.Unlock() 205 206 if fi, err := os.Stat(repoPath); err == nil { 207 if fi.IsDir() { 208 configPath := filepath.Join(repoPath, "config") 209 if _, cerr := os.Stat(configPath); cerr == nil { 210 log.Debugf(c, "Directory '%s' already exists.", repoPath) 211 return true, nil 212 } else { 213 log.Warnf(c, "No config file found at `%s`; removing it and recreating.", repoPath) 214 if err := os.RemoveAll(repoPath); err != nil { 215 return false, fmt.Errorf("Unable to remove path '%s': %s", repoPath, err) 216 } 217 } 218 } else { 219 log.Warnf(c, "Path '%s' is not a directory; removing it and recreating.", repoPath) 220 if err := os.RemoveAll(repoPath); err != nil { 221 return false, fmt.Errorf("Unable to remove path '%s': %s", repoPath, err) 222 } 223 } 224 } else if os.IsNotExist(err) { 225 log.Debugf(c, "Unable to get stat for path '%s': %s .", repoPath, err) 226 } else { 227 return false, err 228 } 229 return initRepo(repoPath, gitHome, c) 230 } 231 232 //prereceiveHook templates a pre-receive hook for Git. 233 func prereceiveHook(vars map[string]string) ([]byte, error) { 234 var out bytes.Buffer 235 // We parse the template anew each receive in case it has changed. 236 t, err := template.New("hooks").Parse(PrereceiveHookTpl) 237 if err != nil { 238 return []byte{}, err 239 } 240 241 err = t.Execute(&out, vars) 242 return out.Bytes(), err 243 }