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  }