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