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  }