mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-23 22:27:38 -05:00
Merge pull request #37 from abiosoft/master
git: post pull command. retries after pull failure.
This commit is contained in:
commit
9df9ad975d
3 changed files with 139 additions and 55 deletions
|
@ -7,6 +7,7 @@
|
||||||
// branch
|
// branch
|
||||||
// key
|
// key
|
||||||
// interval
|
// interval
|
||||||
|
// then command args
|
||||||
// }
|
// }
|
||||||
// repo - git repository
|
// repo - git repository
|
||||||
// compulsory. Both ssh (e.g. git@github.com:user/project.git)
|
// compulsory. Both ssh (e.g. git@github.com:user/project.git)
|
||||||
|
@ -15,7 +16,6 @@
|
||||||
//
|
//
|
||||||
// path - directory to pull into, relative to site root
|
// path - directory to pull into, relative to site root
|
||||||
// optional. Defaults to site root.
|
// optional. Defaults to site root.
|
||||||
// If set, must be a subdirectory to site root to be valid.
|
|
||||||
//
|
//
|
||||||
// branch - git branch or tag
|
// branch - git branch or tag
|
||||||
// optional. Defaults to master
|
// optional. Defaults to master
|
||||||
|
@ -26,6 +26,9 @@
|
||||||
// interval- interval between git pulls in seconds
|
// interval- interval between git pulls in seconds
|
||||||
// optional. Defaults to 3600 (1 Hour).
|
// optional. Defaults to 3600 (1 Hour).
|
||||||
//
|
//
|
||||||
|
// then - command to execute after successful pull
|
||||||
|
// optional. If set, will execute only when there are new changes.
|
||||||
|
//
|
||||||
// Examples :
|
// Examples :
|
||||||
//
|
//
|
||||||
// public repo pulled into site root
|
// public repo pulled into site root
|
||||||
|
@ -34,7 +37,7 @@
|
||||||
// public repo pulled into <root>/mysite
|
// public repo pulled into <root>/mysite
|
||||||
// git https://github.com/user/myproject mysite
|
// git https://github.com/user/myproject mysite
|
||||||
//
|
//
|
||||||
// private repo pulled into <root>/mysite with tag v1.0 and interval of 1 day
|
// private repo pulled into <root>/mysite with tag v1.0 and interval of 1 day.
|
||||||
// git {
|
// git {
|
||||||
// repo git@github.com:user/myproject
|
// repo git@github.com:user/myproject
|
||||||
// branch v1.0
|
// branch v1.0
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -56,7 +57,7 @@ func parse(c middleware.Controller) (*Repo, error) {
|
||||||
|
|
||||||
switch len(args) {
|
switch len(args) {
|
||||||
case 2:
|
case 2:
|
||||||
repo.Path = filepath.Join(c.Root(), args[1])
|
repo.Path = filepath.Clean(c.Root() + string(filepath.Separator) + args[1])
|
||||||
fallthrough
|
fallthrough
|
||||||
case 1:
|
case 1:
|
||||||
repo.Url = args[0]
|
repo.Url = args[0]
|
||||||
|
@ -73,7 +74,7 @@ func parse(c middleware.Controller) (*Repo, error) {
|
||||||
if !c.NextArg() {
|
if !c.NextArg() {
|
||||||
return nil, c.ArgErr()
|
return nil, c.ArgErr()
|
||||||
}
|
}
|
||||||
repo.Path = filepath.Join(c.Root(), c.Val())
|
repo.Path = filepath.Clean(c.Root() + string(filepath.Separator) + c.Val())
|
||||||
case "branch":
|
case "branch":
|
||||||
if !c.NextArg() {
|
if !c.NextArg() {
|
||||||
return nil, c.ArgErr()
|
return nil, c.ArgErr()
|
||||||
|
@ -92,6 +93,12 @@ func parse(c middleware.Controller) (*Repo, error) {
|
||||||
if t > 0 {
|
if t > 0 {
|
||||||
repo.Interval = time.Duration(t) * time.Second
|
repo.Interval = time.Duration(t) * time.Second
|
||||||
}
|
}
|
||||||
|
case "then":
|
||||||
|
thenArgs := c.RemainingArgs()
|
||||||
|
if len(thenArgs) == 0 {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
repo.Then = strings.Join(thenArgs, " ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,7 +132,7 @@ func parse(c middleware.Controller) (*Repo, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return repo, prepare(repo)
|
return repo, repo.prepare()
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanitizeHttp cleans up repository url and converts to https format
|
// sanitizeHttp cleans up repository url and converts to https format
|
||||||
|
@ -165,3 +172,11 @@ func sanitizeGit(repoUrl string) (string, string, error) {
|
||||||
host := hostUrl[:i]
|
host := hostUrl[:i]
|
||||||
return repoUrl, host, nil
|
return repoUrl, host, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
|
@ -1,20 +1,25 @@
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultInterval is the minimum interval to delay before
|
// DefaultInterval is the minimum interval to delay before
|
||||||
// requesting another git pull
|
// requesting another git pull
|
||||||
const DefaultInterval time.Duration = time.Hour * 1
|
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
|
// gitBinary holds the absolute path to git executable
|
||||||
var gitBinary string
|
var gitBinary string
|
||||||
|
|
||||||
|
@ -25,18 +30,21 @@ var initMutex sync.Mutex = sync.Mutex{}
|
||||||
// Repo is the structure that holds required information
|
// Repo is the structure that holds required information
|
||||||
// of a git repository.
|
// of a git repository.
|
||||||
type Repo struct {
|
type Repo struct {
|
||||||
Url string // Repository URL
|
Url string // Repository URL
|
||||||
Path string // Directory to pull to
|
Path string // Directory to pull to
|
||||||
Host string // Git domain host e.g. github.com
|
Host string // Git domain host e.g. github.com
|
||||||
Branch string // Git branch
|
Branch string // Git branch
|
||||||
KeyPath string // Path to private ssh key
|
KeyPath string // Path to private ssh key
|
||||||
Interval time.Duration // Interval between pulls
|
Interval time.Duration // Interval between pulls
|
||||||
pulled bool // true if there was a successful pull
|
Then string // Command to execute after successful git pull
|
||||||
lastPull time.Time // time of the last successful 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
|
sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pull performs git clone, or git pull if repository exists
|
// Pull attempts a git clone.
|
||||||
|
// It retries at most numRetries times if error occurs
|
||||||
func (r *Repo) Pull() error {
|
func (r *Repo) Pull() error {
|
||||||
r.Lock()
|
r.Lock()
|
||||||
defer r.Unlock()
|
defer r.Unlock()
|
||||||
|
@ -45,6 +53,33 @@ func (r *Repo) Pull() error {
|
||||||
return nil
|
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}
|
params := []string{"clone", "-b", r.Branch, r.Url, r.Path}
|
||||||
if r.pulled {
|
if r.pulled {
|
||||||
params = []string{"pull", "origin", r.Branch}
|
params = []string{"pull", "origin", r.Branch}
|
||||||
|
@ -52,35 +87,27 @@ func (r *Repo) Pull() error {
|
||||||
|
|
||||||
// if key is specified, pull using ssh key
|
// if key is specified, pull using ssh key
|
||||||
if r.KeyPath != "" {
|
if r.KeyPath != "" {
|
||||||
return pullWithKey(r, params)
|
return r.pullWithKey(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(gitBinary, params...)
|
dir := ""
|
||||||
cmd.Env = os.Environ()
|
|
||||||
cmd.Stdout = os.Stderr
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
if r.pulled {
|
if r.pulled {
|
||||||
cmd.Dir = r.Path
|
dir = r.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if err = cmd.Start(); err != nil {
|
if err = runCmd(gitBinary, params, dir); err == nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = cmd.Wait(); err == nil {
|
|
||||||
r.pulled = true
|
r.pulled = true
|
||||||
r.lastPull = time.Now()
|
r.lastPull = time.Now()
|
||||||
log.Printf("%v pulled.\n", r.Url)
|
logger().Printf("%v pulled.\n", r.Url)
|
||||||
|
r.lastCommit, err = r.getMostRecentCommit()
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// pullWithKey performs git clone or git pull if repository exists.
|
// pullWithKey is used for private repositories and requires an ssh key.
|
||||||
// It is used for private repositories and requires an ssh key.
|
|
||||||
// Note: currently only limited to Linux and OSX.
|
// Note: currently only limited to Linux and OSX.
|
||||||
func pullWithKey(r *Repo, params []string) error {
|
func (r *Repo) pullWithKey(params []string) error {
|
||||||
var gitSsh, script *os.File
|
var gitSsh, script *os.File
|
||||||
// ensure temporary files deleted after usage
|
// ensure temporary files deleted after usage
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -105,30 +132,23 @@ func pullWithKey(r *Repo, params []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// execute the git clone bash script
|
dir := ""
|
||||||
cmd := exec.Command(script.Name())
|
|
||||||
cmd.Env = os.Environ()
|
|
||||||
cmd.Stdout = os.Stderr
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
if r.pulled {
|
if r.pulled {
|
||||||
cmd.Dir = r.Path
|
dir = r.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = cmd.Start(); err != nil {
|
if err = runCmd(script.Name(), nil, dir); err == nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = cmd.Wait(); err == nil {
|
|
||||||
r.pulled = true
|
r.pulled = true
|
||||||
r.lastPull = time.Now()
|
r.lastPull = time.Now()
|
||||||
log.Printf("%v pulled.\n", r.Url)
|
logger().Printf("%v pulled.\n", r.Url)
|
||||||
|
r.lastCommit, err = r.getMostRecentCommit()
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepare prepares for a git pull
|
// prepare prepares for a git pull
|
||||||
// and validates the configured directory
|
// and validates the configured directory
|
||||||
func prepare(r *Repo) error {
|
func (r *Repo) prepare() error {
|
||||||
// check if directory exists or is empty
|
// check if directory exists or is empty
|
||||||
// if not, create directory
|
// if not, create directory
|
||||||
fs, err := ioutil.ReadDir(r.Path)
|
fs, err := ioutil.ReadDir(r.Path)
|
||||||
|
@ -148,7 +168,7 @@ func prepare(r *Repo) error {
|
||||||
if isGit {
|
if isGit {
|
||||||
// check if same repository
|
// check if same repository
|
||||||
var repoUrl string
|
var repoUrl string
|
||||||
if repoUrl, err = getRepoUrl(r.Path); err == nil && repoUrl == r.Url {
|
if repoUrl, err = r.getRepoUrl(); err == nil && repoUrl == r.Url {
|
||||||
r.pulled = true
|
r.pulled = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -160,23 +180,42 @@ func prepare(r *Repo) error {
|
||||||
return fmt.Errorf("Cannot git clone into %v, directory not empty.", 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
|
// getRepoUrl retrieves remote origin url for the git repository at path
|
||||||
func getRepoUrl(path string) (string, error) {
|
func (r *Repo) getRepoUrl() (string, error) {
|
||||||
|
_, err := os.Stat(r.Path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
args := []string{"config", "--get", "remote.origin.url"}
|
args := []string{"config", "--get", "remote.origin.url"}
|
||||||
|
return runCmdOutput(gitBinary, args, r.Path)
|
||||||
|
}
|
||||||
|
|
||||||
_, err := os.Stat(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 {
|
if err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(gitBinary, args...)
|
if err = runCmd(c, args, r.Path); err == nil {
|
||||||
cmd.Dir = path
|
logger().Printf("Command %v successful.\n", r.Then)
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
return strings.TrimSpace(string(output)), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// initGit validates git installation and locates the git executable
|
// initGit validates git installation and locates the git executable
|
||||||
|
@ -199,6 +238,33 @@ func initGit() error {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 := exec.Command(command, args...)
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdout = 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 := exec.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.
|
// writeScriptFile writes content to a temporary file.
|
||||||
// It changes the temporary file mode to executable and
|
// It changes the temporary file mode to executable and
|
||||||
// closes it to prepare it for execution.
|
// closes it to prepare it for execution.
|
||||||
|
|
Loading…
Reference in a new issue