mirror of
https://github.com/caddyserver/caddy.git
synced 2024-11-29 20:24:11 +08:00
b4780a41d3
The webhook providers reside behind a small interface which determines if a provider should run. If a provider should run it delegates responsibility of the request to the provider. ghdeploy initial commit Added webhook functionality to the git middleware. The webhook providers reside behind a small interface which determines if a provider should run. If a provider should run it delegates responsibility of the request to the provider. Add tests Remove old implementation Fix inconsistency with git interval pulling. Remove '\n' from logging statements and put the initial pull into a startup function
368 lines
9.1 KiB
Go
368 lines
9.1 KiB
Go
package git
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/mholt/caddy/middleware"
|
|
"github.com/mholt/caddy/middleware/git/gitos"
|
|
)
|
|
|
|
// DefaultInterval is the minimum interval to delay before
|
|
// requesting another git pull
|
|
const DefaultInterval time.Duration = time.Hour * 1
|
|
|
|
// Number of retries if git pull fails
|
|
const numRetries = 3
|
|
|
|
// gitBinary holds the absolute path to git executable
|
|
var gitBinary string
|
|
|
|
// shell holds the shell to be used. Either sh or bash.
|
|
var shell string
|
|
|
|
// initMutex prevents parallel attempt to validate
|
|
// git requirements.
|
|
var initMutex = sync.Mutex{}
|
|
|
|
// Logger is used to log errors; if nil, the default log.Logger is used.
|
|
var Logger *log.Logger
|
|
|
|
// Services holds all git pulling services and provides the function to
|
|
// stop them.
|
|
var Services = &services{}
|
|
|
|
// logger is an helper function to retrieve the available logger
|
|
func logger() *log.Logger {
|
|
if Logger == nil {
|
|
Logger = log.New(os.Stderr, "", log.LstdFlags)
|
|
}
|
|
return Logger
|
|
}
|
|
|
|
// Repo is the structure that holds required information
|
|
// of a git repository.
|
|
type Repo struct {
|
|
URL string // Repository URL
|
|
Path string // Directory to pull to
|
|
Host string // Git domain host e.g. github.com
|
|
Branch string // Git branch
|
|
KeyPath string // Path to private ssh key
|
|
Interval time.Duration // Interval between pulls
|
|
Then string // Command to execute after successful git pull
|
|
pulled bool // true if there was a successful pull
|
|
lastPull time.Time // time of the last successful pull
|
|
lastCommit string // hash for the most recent commit
|
|
sync.Mutex
|
|
HookUrl string // url to listen on for webhooks
|
|
HookSecret string // secret to validate hooks
|
|
|
|
}
|
|
|
|
// Pull attempts a git clone.
|
|
// It retries at most numRetries times if error occurs
|
|
func (r *Repo) Pull() error {
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
|
|
// prevent a pull if the last one was less than 5 seconds ago
|
|
if gos.TimeSince(r.lastPull) < 5*time.Second {
|
|
return nil
|
|
}
|
|
|
|
// keep last commit hash for comparison later
|
|
lastCommit := r.lastCommit
|
|
|
|
var err error
|
|
// Attempt to pull at most numRetries times
|
|
for i := 0; i < numRetries; i++ {
|
|
if err = r.pull(); err == nil {
|
|
break
|
|
}
|
|
logger().Println(err)
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// check if there are new changes,
|
|
// then execute post pull command
|
|
if r.lastCommit == lastCommit {
|
|
logger().Println("No new changes.")
|
|
return nil
|
|
}
|
|
return r.postPullCommand()
|
|
}
|
|
|
|
// Pull performs git clone, or git pull if repository exists
|
|
func (r *Repo) pull() error {
|
|
params := []string{"clone", "-b", r.Branch, r.URL, r.Path}
|
|
if r.pulled {
|
|
params = []string{"pull", "origin", r.Branch}
|
|
}
|
|
|
|
// if key is specified, pull using ssh key
|
|
if r.KeyPath != "" {
|
|
return r.pullWithKey(params)
|
|
}
|
|
|
|
dir := ""
|
|
if r.pulled {
|
|
dir = r.Path
|
|
}
|
|
|
|
var err error
|
|
if err = runCmd(gitBinary, params, dir); err == nil {
|
|
r.pulled = true
|
|
r.lastPull = time.Now()
|
|
logger().Printf("%v pulled.\n", r.URL)
|
|
r.lastCommit, err = r.getMostRecentCommit()
|
|
}
|
|
return err
|
|
}
|
|
|
|
// pullWithKey is used for private repositories and requires an ssh key.
|
|
// Note: currently only limited to Linux and OSX.
|
|
func (r *Repo) pullWithKey(params []string) error {
|
|
var gitSSH, script gitos.File
|
|
// ensure temporary files deleted after usage
|
|
defer func() {
|
|
if gitSSH != nil {
|
|
gos.Remove(gitSSH.Name())
|
|
}
|
|
if script != nil {
|
|
gos.Remove(script.Name())
|
|
}
|
|
}()
|
|
|
|
var err error
|
|
// write git.sh script to temp file
|
|
gitSSH, err = writeScriptFile(gitWrapperScript())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// write git clone bash script to file
|
|
script, err = writeScriptFile(bashScript(gitSSH.Name(), r, params))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dir := ""
|
|
if r.pulled {
|
|
dir = r.Path
|
|
}
|
|
|
|
if err = runCmd(script.Name(), nil, dir); err == nil {
|
|
r.pulled = true
|
|
r.lastPull = time.Now()
|
|
logger().Printf("%v pulled.\n", r.URL)
|
|
r.lastCommit, err = r.getMostRecentCommit()
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Prepare prepares for a git pull
|
|
// and validates the configured directory
|
|
func (r *Repo) Prepare() error {
|
|
// check if directory exists or is empty
|
|
// if not, create directory
|
|
fs, err := gos.ReadDir(r.Path)
|
|
if err != nil || len(fs) == 0 {
|
|
return gos.MkdirAll(r.Path, os.FileMode(0755))
|
|
}
|
|
|
|
// validate git repo
|
|
isGit := false
|
|
for _, f := range fs {
|
|
if f.IsDir() && f.Name() == ".git" {
|
|
isGit = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if isGit {
|
|
// check if same repository
|
|
var repoURL string
|
|
if repoURL, err = r.getRepoURL(); err == nil {
|
|
// add .git suffix if missing for adequate comparison.
|
|
if !strings.HasSuffix(repoURL, ".git") {
|
|
repoURL += ".git"
|
|
}
|
|
if repoURL == r.URL {
|
|
r.pulled = true
|
|
return nil
|
|
}
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("Cannot retrieve repo url for %v Error: %v", r.Path, err)
|
|
}
|
|
return fmt.Errorf("Another git repo '%v' exists at %v", repoURL, r.Path)
|
|
}
|
|
return fmt.Errorf("Cannot git clone into %v, directory not empty.", r.Path)
|
|
}
|
|
|
|
// getMostRecentCommit gets the hash of the most recent commit to the
|
|
// repository. Useful for checking if changes occur.
|
|
func (r *Repo) getMostRecentCommit() (string, error) {
|
|
command := gitBinary + ` --no-pager log -n 1 --pretty=format:"%H"`
|
|
c, args, err := middleware.SplitCommandAndArgs(command)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return runCmdOutput(c, args, r.Path)
|
|
}
|
|
|
|
// getRepoURL retrieves remote origin url for the git repository at path
|
|
func (r *Repo) getRepoURL() (string, error) {
|
|
_, err := gos.Stat(r.Path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
args := []string{"config", "--get", "remote.origin.url"}
|
|
return runCmdOutput(gitBinary, args, r.Path)
|
|
}
|
|
|
|
// postPullCommand executes r.Then.
|
|
// It is trigged after successful git pull
|
|
func (r *Repo) postPullCommand() error {
|
|
if r.Then == "" {
|
|
return nil
|
|
}
|
|
c, args, err := middleware.SplitCommandAndArgs(r.Then)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = runCmd(c, args, r.Path); err == nil {
|
|
logger().Printf("Command %v successful.\n", r.Then)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Init validates git installation, locates the git executable
|
|
// binary in PATH and check for available shell to use.
|
|
func Init() error {
|
|
// prevent concurrent call
|
|
initMutex.Lock()
|
|
defer initMutex.Unlock()
|
|
|
|
// if validation has been done before and binary located in
|
|
// PATH, return.
|
|
if gitBinary != "" {
|
|
return nil
|
|
}
|
|
|
|
// locate git binary in path
|
|
var err error
|
|
if gitBinary, err = gos.LookPath("git"); err != nil {
|
|
return fmt.Errorf("Git middleware requires git installed. Cannot find git binary in PATH")
|
|
}
|
|
|
|
// locate bash in PATH. If not found, fallback to sh.
|
|
// If neither is found, return error.
|
|
shell = "bash"
|
|
if _, err = gos.LookPath("bash"); err != nil {
|
|
shell = "sh"
|
|
if _, err = gos.LookPath("sh"); err != nil {
|
|
return fmt.Errorf("Git middleware requires either bash or sh.")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// runCmd is a helper function to run commands.
|
|
// It runs command with args from directory at dir.
|
|
// The executed process outputs to os.Stderr
|
|
func runCmd(command string, args []string, dir string) error {
|
|
cmd := gos.Command(command, args...)
|
|
cmd.Stdout(os.Stderr)
|
|
cmd.Stderr(os.Stderr)
|
|
cmd.Dir(dir)
|
|
if err := cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
return cmd.Wait()
|
|
}
|
|
|
|
// runCmdOutput is a helper function to run commands and return output.
|
|
// It runs command with args from directory at dir.
|
|
// If successful, returns output and nil error
|
|
func runCmdOutput(command string, args []string, dir string) (string, error) {
|
|
cmd := gos.Command(command, args...)
|
|
cmd.Dir(dir)
|
|
var err error
|
|
if output, err := cmd.Output(); err == nil {
|
|
return string(bytes.TrimSpace(output)), nil
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
// writeScriptFile writes content to a temporary file.
|
|
// It changes the temporary file mode to executable and
|
|
// closes it to prepare it for execution.
|
|
func writeScriptFile(content []byte) (file gitos.File, err error) {
|
|
if file, err = gos.TempFile("", "caddy"); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err = file.Write(content); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = file.Chmod(os.FileMode(0755)); err != nil {
|
|
return nil, err
|
|
}
|
|
return file, file.Close()
|
|
}
|
|
|
|
// gitWrapperScript forms content for git.sh script
|
|
func gitWrapperScript() []byte {
|
|
return []byte(fmt.Sprintf(`#!/bin/%v
|
|
|
|
# The MIT License (MIT)
|
|
# Copyright (c) 2013 Alvin Abad
|
|
|
|
if [ $# -eq 0 ]; then
|
|
echo "Git wrapper script that can specify an ssh-key file
|
|
Usage:
|
|
git.sh -i ssh-key-file git-command
|
|
"
|
|
exit 1
|
|
fi
|
|
|
|
# remove temporary file on exit
|
|
trap 'rm -f /tmp/.git_ssh.$$' 0
|
|
|
|
if [ "$1" = "-i" ]; then
|
|
SSH_KEY=$2; shift; shift
|
|
echo "ssh -i $SSH_KEY \$@" > /tmp/.git_ssh.$$
|
|
chmod +x /tmp/.git_ssh.$$
|
|
export GIT_SSH=/tmp/.git_ssh.$$
|
|
fi
|
|
|
|
# in case the git command is repeated
|
|
[ "$1" = "git" ] && shift
|
|
|
|
# Run the git command
|
|
%v "$@"
|
|
|
|
`, shell, gitBinary))
|
|
}
|
|
|
|
// bashScript forms content of bash script to clone or update a repo using ssh
|
|
func bashScript(gitShPath string, repo *Repo, params []string) []byte {
|
|
return []byte(fmt.Sprintf(`#!/bin/%v
|
|
|
|
mkdir -p ~/.ssh;
|
|
touch ~/.ssh/known_hosts;
|
|
ssh-keyscan -t rsa,dsa %v 2>&1 | sort -u - ~/.ssh/known_hosts > ~/.ssh/tmp_hosts;
|
|
cat ~/.ssh/tmp_hosts >> ~/.ssh/known_hosts;
|
|
%v -i %v %v;
|
|
`, shell, repo.Host, gitShPath, repo.KeyPath, strings.Join(params, " ")))
|
|
}
|