232 lines
6.1 KiB
Go
232 lines
6.1 KiB
Go
package jobs
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.rootprojects.org/root/gitdeploy/internal/log"
|
|
"git.rootprojects.org/root/gitdeploy/internal/options"
|
|
"git.rootprojects.org/root/gitdeploy/internal/webhooks"
|
|
)
|
|
|
|
// Debounce puts a job in the queue, in time
|
|
func Debounce(hook webhooks.Ref) {
|
|
webhooks.Hooks <- hook
|
|
}
|
|
|
|
var jobsTimersMux sync.Mutex
|
|
var debounceTimers = make(map[webhooks.RefID]*time.Timer)
|
|
|
|
func debounce(hook *webhooks.Ref, runOpts *options.ServerConfig) {
|
|
jobsTimersMux.Lock()
|
|
defer jobsTimersMux.Unlock()
|
|
|
|
activeID := hook.GetRefID()
|
|
if _, ok := Actives.Load(activeID); ok {
|
|
log.Printf("Job in progress, not debouncing %s", hook)
|
|
return
|
|
}
|
|
|
|
refID := hook.GetRefID()
|
|
timer, ok := debounceTimers[refID]
|
|
if ok {
|
|
log.Printf("Replacing previous debounce timer for %s", hook)
|
|
timer.Stop()
|
|
}
|
|
// this will not cause a mutual lock because it is async
|
|
debounceTimers[refID] = time.AfterFunc(runOpts.DebounceDelay, func() {
|
|
//fmt.Println("DEBUG [1] wait for jobs and timers")
|
|
jobsTimersMux.Lock()
|
|
delete(debounceTimers, refID)
|
|
jobsTimersMux.Unlock()
|
|
|
|
debounced <- hook
|
|
//fmt.Println("DEBUG [1] release jobs and timers")
|
|
})
|
|
}
|
|
|
|
func getBacklogFilePath(baseDir string, hook *webhooks.Ref) (string, string, error) {
|
|
baseDir, _ = filepath.Abs(baseDir)
|
|
fileName := hook.RefName + ".json"
|
|
fileDir := filepath.Join(baseDir, hook.RepoID)
|
|
|
|
err := os.MkdirAll(fileDir, 0755)
|
|
|
|
return fileDir, fileName, err
|
|
}
|
|
|
|
func saveBacklog(hook *webhooks.Ref, runOpts *options.ServerConfig) {
|
|
pendingID := hook.GetRefID()
|
|
Pending.Store(pendingID, hook)
|
|
|
|
repoDir, repoFile, err := getBacklogFilePath(runOpts.TmpDir, hook)
|
|
if nil != err {
|
|
log.Printf("[WARN] could not create backlog dir %s:\n%v", repoDir, err)
|
|
return
|
|
}
|
|
|
|
f, err := ioutil.TempFile(repoDir, "tmp-*")
|
|
if nil != err {
|
|
log.Printf("[WARN] could not create backlog file %s:\n%v", f.Name(), err)
|
|
return
|
|
}
|
|
|
|
b, _ := json.MarshalIndent(hook, "", " ")
|
|
if _, err := f.Write(b); nil != err {
|
|
log.Printf("[WARN] could not write backlog file %s:\n%v", f.Name(), err)
|
|
return
|
|
}
|
|
|
|
replace := false
|
|
backlogPath := filepath.Join(repoDir, repoFile)
|
|
if _, err := os.Stat(backlogPath); nil == err {
|
|
replace = true
|
|
_ = os.Remove(backlogPath)
|
|
}
|
|
if err := os.Rename(f.Name(), backlogPath); nil != err {
|
|
log.Printf("[WARN] rename backlog json failed:\n%v", err)
|
|
return
|
|
}
|
|
|
|
if replace {
|
|
log.Printf("[backlog] replace backlog for %s", hook.GetRefID())
|
|
} else {
|
|
log.Printf("[backlog] create backlog for %s", hook.GetRefID())
|
|
}
|
|
}
|
|
|
|
func run(curHook *webhooks.Ref, runOpts *options.ServerConfig) {
|
|
// because we want to lock the whole transaction all of the state
|
|
jobsTimersMux.Lock()
|
|
defer jobsTimersMux.Unlock()
|
|
|
|
pendingID := curHook.GetRefID()
|
|
if _, ok := Actives.Load(pendingID); ok {
|
|
log.Printf("Job already in progress: %s", curHook.GetRefID())
|
|
return
|
|
}
|
|
|
|
var hook *webhooks.Ref
|
|
// Legacy, but would be nice to repurpose for resuming on reload
|
|
repoDir, repoFile, _ := getBacklogFilePath(runOpts.TmpDir, curHook)
|
|
backlogFile := filepath.Join(repoDir, repoFile)
|
|
if value, ok := Pending.Load(pendingID); ok {
|
|
hook = value.(*webhooks.Ref)
|
|
log.Printf("loaded from Pending state: %#v", hook)
|
|
} else {
|
|
// TODO add mutex (should not affect temp files)
|
|
_ = os.Remove(backlogFile + ".cur")
|
|
_ = os.Rename(backlogFile, backlogFile+".cur")
|
|
b, err := ioutil.ReadFile(backlogFile + ".cur")
|
|
if nil != err {
|
|
if !os.IsNotExist(err) {
|
|
log.Printf("[warn] could not read backlog file %s:\n%v", repoFile, err)
|
|
}
|
|
// doesn't exist => no backlog
|
|
log.Printf("[NO BACKLOG] no backlog for %s", repoFile)
|
|
return
|
|
}
|
|
|
|
hook = &webhooks.Ref{}
|
|
if err := json.Unmarshal(b, hook); nil != err {
|
|
log.Printf("[warn] could not parse backlog file %s:\n%v", repoFile, err)
|
|
return
|
|
}
|
|
hook = webhooks.New(*hook)
|
|
log.Printf("loaded from file: %#v", hook)
|
|
}
|
|
|
|
Pending.Delete(pendingID)
|
|
_ = os.Remove(backlogFile)
|
|
_ = os.Remove(backlogFile + ".cur")
|
|
|
|
env := os.Environ()
|
|
envs := getEnvs(runOpts.Addr, string(pendingID), runOpts.RepoList, hook)
|
|
envs = append(envs, "GIT_DEPLOY_JOB_ID="+string(pendingID))
|
|
|
|
scriptPath, _ := filepath.Abs(runOpts.ScriptsPath + "/deploy.sh")
|
|
args := []string{
|
|
"-i",
|
|
"--",
|
|
//strings.Join([]string{
|
|
scriptPath,
|
|
string(pendingID),
|
|
hook.RefName,
|
|
hook.RefType,
|
|
hook.Owner,
|
|
hook.Repo,
|
|
hook.HTTPSURL,
|
|
//}, " "),
|
|
}
|
|
|
|
args2 := append([]string{"[" + string(hook.GetRefID()) + "]", "bash"}, args...)
|
|
fmt.Println(strings.Join(args2, " "))
|
|
cmd := exec.Command("bash", args...)
|
|
cmd.Env = append(env, envs...)
|
|
|
|
now := time.Now()
|
|
j := &Job{
|
|
StartedAt: now,
|
|
Cmd: cmd,
|
|
GitRef: hook,
|
|
Logs: []Log{},
|
|
Promote: false,
|
|
}
|
|
// TODO jobs.New()
|
|
// Sets cmd.Stdout and cmd.Stderr
|
|
f := setOutput(runOpts.LogDir, j)
|
|
|
|
if err := cmd.Start(); nil != err {
|
|
log.Printf("gitdeploy exec error: %s\n", err)
|
|
return
|
|
}
|
|
|
|
Actives.Store(pendingID, j)
|
|
|
|
go func() {
|
|
log.Printf("Started job for %s", hook)
|
|
if err := cmd.Wait(); nil != err {
|
|
log.Printf("gitdeploy job for %s#%s exited with error: %v", hook.HTTPSURL, hook.RefName, err)
|
|
} else {
|
|
log.Printf("gitdeploy job for %s#%s finished\n", hook.HTTPSURL, hook.RefName)
|
|
}
|
|
if nil != f {
|
|
_ = f.Close()
|
|
}
|
|
|
|
// Switch ID to the more specific RevID
|
|
j.ID = string(j.GitRef.GetRevID())
|
|
// replace the text log with a json log
|
|
if f, err := getJobFile(runOpts.LogDir, j.GitRef, ".json"); nil != err {
|
|
// f.Name() should be the full path
|
|
log.Printf("[warn] could not create log file '%s': %v", runOpts.LogDir, err)
|
|
} else {
|
|
enc := json.NewEncoder(f)
|
|
enc.SetIndent("", " ")
|
|
if err := enc.Encode(j); nil != err {
|
|
log.Printf("[warn] could not encode json log '%s': %v", f.Name(), err)
|
|
} else {
|
|
logdir, logname, _ := getJobFilePath(runOpts.LogDir, j.GitRef, ".log")
|
|
_ = os.Remove(filepath.Join(logdir, logname))
|
|
}
|
|
_ = f.Close()
|
|
log.Printf("[DEBUG] wrote log to %s", f.Name())
|
|
}
|
|
j.Logs = []Log{}
|
|
|
|
// this will completely clear the finished job
|
|
deathRow <- pendingID
|
|
|
|
// debounces without saving in the backlog
|
|
// TODO move this into deathRow?
|
|
debacklog <- hook
|
|
}()
|
|
}
|