|
|
@ -3,6 +3,7 @@ package main |
|
|
|
import ( |
|
|
|
"compress/flate" |
|
|
|
"encoding/base64" |
|
|
|
"encoding/json" |
|
|
|
"flag" |
|
|
|
"fmt" |
|
|
|
"log" |
|
|
@ -41,16 +42,20 @@ func ver() { |
|
|
|
} |
|
|
|
|
|
|
|
type job struct { |
|
|
|
ID string // {HTTPSURL}#{BRANCH}
|
|
|
|
Cmd *exec.Cmd |
|
|
|
Ref webhooks.Ref |
|
|
|
ID string // {HTTPSURL}#{BRANCH}
|
|
|
|
Cmd *exec.Cmd |
|
|
|
Ref webhooks.Ref |
|
|
|
CreatedAt time.Time |
|
|
|
} |
|
|
|
|
|
|
|
var jobs = make(map[string]*job) |
|
|
|
var killers = make(chan string) |
|
|
|
|
|
|
|
var runOpts *options.ServerConfig |
|
|
|
var runFlags *flag.FlagSet |
|
|
|
var initFlags *flag.FlagSet |
|
|
|
var promotions = []string{"production", "staging", "master"} |
|
|
|
var names = map[string]string{"master": "Development", "production": "Production", "staging": "Staging"} |
|
|
|
|
|
|
|
func init() { |
|
|
|
runOpts = options.Server |
|
|
@ -64,7 +69,8 @@ func init() { |
|
|
|
"path to serve, falls back to built-in web app") |
|
|
|
runFlags.StringVar( |
|
|
|
&runOpts.Exec, "exec", "", |
|
|
|
"path to bash script to run with git info as arguments") |
|
|
|
"path to ./scripts/{deploy.sh,promote.sh,etc}") |
|
|
|
//"path to bash script to run with git info as arguments")
|
|
|
|
} |
|
|
|
|
|
|
|
func main() { |
|
|
@ -97,7 +103,7 @@ func main() { |
|
|
|
case "run": |
|
|
|
_ = runFlags.Parse(args[2:]) |
|
|
|
if "" == runOpts.Exec { |
|
|
|
fmt.Printf("--exec <path/to/script.sh> is a required flag") |
|
|
|
fmt.Printf("--exec <path/to/scripts/> is a required flag") |
|
|
|
os.Exit(1) |
|
|
|
return |
|
|
|
} |
|
|
@ -110,6 +116,20 @@ func main() { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Job is the JSON we send back through the API about jobs
|
|
|
|
type Job struct { |
|
|
|
JobID string `json:"job_id"` |
|
|
|
CreatedAt time.Time `json:"created_at"` |
|
|
|
Ref webhooks.Ref `json:"ref"` |
|
|
|
Promote bool `json:"promote,omitempty"` |
|
|
|
} |
|
|
|
|
|
|
|
// KillMsg describes which job to kill
|
|
|
|
type KillMsg struct { |
|
|
|
JobID string `json:"job_id"` |
|
|
|
Kill bool `json:"kill"` |
|
|
|
} |
|
|
|
|
|
|
|
func serve() { |
|
|
|
r := chi.NewRouter() |
|
|
|
|
|
|
@ -146,6 +166,96 @@ func serve() { |
|
|
|
|
|
|
|
webhooks.RouteHandlers(r) |
|
|
|
|
|
|
|
r.Route("/api/admin", func(r chi.Router) { |
|
|
|
r.Use(func(next http.Handler) http.Handler { |
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
|
|
// r.Body is always .Close()ed by Go's http server
|
|
|
|
r.Body = http.MaxBytesReader(w, r.Body, options.DefaultMaxBodySize) |
|
|
|
// TODO admin auth middleware
|
|
|
|
log.Println("TODO: handle authentication") |
|
|
|
next.ServeHTTP(w, r) |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
r.Get("/jobs", func(w http.ResponseWriter, r *http.Request) { |
|
|
|
// again, possible race condition, but not one that much matters
|
|
|
|
jjobs := []Job{} |
|
|
|
for jobID, job := range jobs { |
|
|
|
jjobs = append(jjobs, Job{ |
|
|
|
JobID: jobID, |
|
|
|
Ref: job.Ref, |
|
|
|
CreatedAt: job.CreatedAt, |
|
|
|
}) |
|
|
|
} |
|
|
|
b, _ := json.Marshal(struct { |
|
|
|
Success bool `json:"success"` |
|
|
|
Jobs []Job `json:"jobs"` |
|
|
|
}{ |
|
|
|
Success: true, |
|
|
|
Jobs: jjobs, |
|
|
|
}) |
|
|
|
w.Write(append(b, '\n')) |
|
|
|
}) |
|
|
|
r.Post("/jobs", func(w http.ResponseWriter, r *http.Request) { |
|
|
|
defer func() { |
|
|
|
_ = r.Body.Close() |
|
|
|
}() |
|
|
|
|
|
|
|
decoder := json.NewDecoder(r.Body) |
|
|
|
msg := &KillMsg{} |
|
|
|
if err := decoder.Decode(msg); nil != err { |
|
|
|
log.Println("kill job invalid json:", err) |
|
|
|
http.Error(w, "invalid json body", http.StatusBadRequest) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json") |
|
|
|
// possible race condition, but not the kind that should matter
|
|
|
|
if _, exists := jobs[msg.JobID]; !exists { |
|
|
|
w.Write([]byte( |
|
|
|
`{ "success": false, "error": "job does not exist" }` + "\n", |
|
|
|
)) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// killing a job *should* always succeed ...right?
|
|
|
|
killers <- msg.JobID |
|
|
|
w.Write([]byte( |
|
|
|
`{ "success": true }` + "\n", |
|
|
|
)) |
|
|
|
}) |
|
|
|
|
|
|
|
r.Post("/promote", func(w http.ResponseWriter, r *http.Request) { |
|
|
|
decoder := json.NewDecoder(r.Body) |
|
|
|
msg := &webhooks.Ref{} |
|
|
|
if err := decoder.Decode(msg); nil != err { |
|
|
|
log.Println("promotion job invalid json:", err) |
|
|
|
http.Error(w, "invalid json body", http.StatusBadRequest) |
|
|
|
return |
|
|
|
} |
|
|
|
if "" == msg.HTTPSURL || "" == msg.RefName { |
|
|
|
log.Println("promotion job incomplete json", msg) |
|
|
|
http.Error(w, "incomplete json body", http.StatusBadRequest) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
n := -2 |
|
|
|
for i := range promotions { |
|
|
|
if promotions[i] == msg.RefName { |
|
|
|
n = i - 1 |
|
|
|
break |
|
|
|
} |
|
|
|
} |
|
|
|
if n < 0 { |
|
|
|
log.Println("promotion job invalid: cannot promote:", n) |
|
|
|
http.Error(w, "invalid promotion", http.StatusBadRequest) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
promoteTo := promotions[n] |
|
|
|
runPromote(*msg, promoteTo) |
|
|
|
}) |
|
|
|
}) |
|
|
|
r.Get("/*", staticHandler) |
|
|
|
|
|
|
|
fmt.Println("Listening for http (with reasonable timeouts) on", runOpts.Addr) |
|
|
@ -159,61 +269,15 @@ func serve() { |
|
|
|
} |
|
|
|
|
|
|
|
go func() { |
|
|
|
// TODO read from backlog
|
|
|
|
for { |
|
|
|
hook := webhooks.Accept() |
|
|
|
// TODO os.Exec
|
|
|
|
fmt.Printf("%#v\n", hook) |
|
|
|
jobID := base64.URLEncoding.EncodeToString([]byte( |
|
|
|
fmt.Sprintf("%s#%s", hook.HTTPSURL, hook.RefName), |
|
|
|
)) |
|
|
|
|
|
|
|
args := []string{ |
|
|
|
runOpts.Exec, |
|
|
|
jobID, |
|
|
|
hook.RefName, |
|
|
|
hook.RefType, |
|
|
|
hook.Owner, |
|
|
|
hook.Repo, |
|
|
|
hook.HTTPSURL, |
|
|
|
} |
|
|
|
cmd := exec.Command("bash", args...) |
|
|
|
|
|
|
|
env := os.Environ() |
|
|
|
envs := []string{ |
|
|
|
"GIT_DEPLOY_JOB_ID=" + jobID, |
|
|
|
"GIT_REF_NAME=" + hook.RefName, |
|
|
|
"GIT_REF_TYPE=" + hook.RefType, |
|
|
|
"GIT_REPO_OWNER=" + hook.Owner, |
|
|
|
"GIT_REPO_NAME=" + hook.Repo, |
|
|
|
"GIT_CLONE_URL=" + hook.HTTPSURL, |
|
|
|
} |
|
|
|
cmd.Env = append(env, envs...) |
|
|
|
cmd.Stdout = os.Stdout |
|
|
|
cmd.Stderr = os.Stderr |
|
|
|
|
|
|
|
if _, exists := jobs[jobID]; exists { |
|
|
|
// TODO put job in backlog
|
|
|
|
log.Printf("git-deploy job already started for %s#%s\n", hook.HTTPSURL, hook.RefName) |
|
|
|
return |
|
|
|
//hook := webhooks.Accept()
|
|
|
|
select { |
|
|
|
case hook := <-webhooks.Hooks: |
|
|
|
runHook(hook) |
|
|
|
case jobID := <-killers: |
|
|
|
kill(jobID) |
|
|
|
} |
|
|
|
|
|
|
|
if err := cmd.Start(); nil != err { |
|
|
|
log.Printf("git-deploy exec error: %s\n", err) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
jobs[jobID] = &job{ |
|
|
|
ID: jobID, |
|
|
|
Cmd: cmd, |
|
|
|
Ref: hook, |
|
|
|
} |
|
|
|
|
|
|
|
go func() { |
|
|
|
log.Printf("git-deploy job for %s#%s started\n", hook.HTTPSURL, hook.RefName) |
|
|
|
_ = cmd.Wait() |
|
|
|
delete(jobs, jobID) |
|
|
|
log.Printf("git-deploy job for %s#%s finished\n", hook.HTTPSURL, hook.RefName) |
|
|
|
}() |
|
|
|
} |
|
|
|
}() |
|
|
|
|
|
|
@ -223,3 +287,151 @@ func serve() { |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func runHook(hook webhooks.Ref) { |
|
|
|
fmt.Printf("%#v\n", hook) |
|
|
|
jobID := base64.RawURLEncoding.EncodeToString([]byte( |
|
|
|
fmt.Sprintf("%s#%s", hook.HTTPSURL, hook.RefName), |
|
|
|
)) |
|
|
|
|
|
|
|
args := []string{ |
|
|
|
runOpts.Exec + "/deploy.sh", |
|
|
|
jobID, |
|
|
|
hook.RefName, |
|
|
|
hook.RefType, |
|
|
|
hook.Owner, |
|
|
|
hook.Repo, |
|
|
|
hook.HTTPSURL, |
|
|
|
} |
|
|
|
cmd := exec.Command("bash", args...) |
|
|
|
|
|
|
|
env := os.Environ() |
|
|
|
envs := []string{ |
|
|
|
"GIT_DEPLOY_JOB_ID=" + jobID, |
|
|
|
"GIT_REF_NAME=" + hook.RefName, |
|
|
|
"GIT_REF_TYPE=" + hook.RefType, |
|
|
|
"GIT_REPO_OWNER=" + hook.Owner, |
|
|
|
"GIT_REPO_NAME=" + hook.Repo, |
|
|
|
"GIT_CLONE_URL=" + hook.HTTPSURL, |
|
|
|
} |
|
|
|
cmd.Env = append(env, envs...) |
|
|
|
cmd.Stdout = os.Stdout |
|
|
|
cmd.Stderr = os.Stderr |
|
|
|
|
|
|
|
if _, exists := jobs[jobID]; exists { |
|
|
|
// TODO put job in backlog
|
|
|
|
log.Printf("git-deploy job already started for %s#%s\n", hook.HTTPSURL, hook.RefName) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
if err := cmd.Start(); nil != err { |
|
|
|
log.Printf("git-deploy exec error: %s\n", err) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
jobs[jobID] = &job{ |
|
|
|
ID: jobID, |
|
|
|
Cmd: cmd, |
|
|
|
Ref: hook, |
|
|
|
CreatedAt: time.Now(), |
|
|
|
} |
|
|
|
|
|
|
|
go func() { |
|
|
|
log.Printf("git-deploy job for %s#%s started\n", hook.HTTPSURL, hook.RefName) |
|
|
|
_ = cmd.Wait() |
|
|
|
killers <- jobID |
|
|
|
log.Printf("git-deploy job for %s#%s finished\n", hook.HTTPSURL, hook.RefName) |
|
|
|
// TODO check for backlog
|
|
|
|
}() |
|
|
|
} |
|
|
|
|
|
|
|
func kill(jobID string) { |
|
|
|
job, exists := jobs[jobID] |
|
|
|
if !exists { |
|
|
|
return |
|
|
|
} |
|
|
|
delete(jobs, jobID) |
|
|
|
|
|
|
|
if nil != job.Cmd.ProcessState { |
|
|
|
// is not yet finished
|
|
|
|
if nil != job.Cmd.Process { |
|
|
|
// but definitely was started
|
|
|
|
err := job.Cmd.Process.Kill() |
|
|
|
log.Println("error killing job:", err) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func runPromote(hook webhooks.Ref, promoteTo string) { |
|
|
|
// TODO create an origin-branch tag with a timestamp?
|
|
|
|
jobID1 := base64.RawURLEncoding.EncodeToString([]byte( |
|
|
|
fmt.Sprintf("%s#%s", hook.HTTPSURL, hook.RefName), |
|
|
|
)) |
|
|
|
jobID2 := base64.RawURLEncoding.EncodeToString([]byte( |
|
|
|
fmt.Sprintf("%s#%s", hook.HTTPSURL, promoteTo), |
|
|
|
)) |
|
|
|
|
|
|
|
args := []string{ |
|
|
|
runOpts.Exec + "/promote.sh", |
|
|
|
jobID1, |
|
|
|
promoteTo, |
|
|
|
hook.RefName, |
|
|
|
hook.RefType, |
|
|
|
hook.Owner, |
|
|
|
hook.Repo, |
|
|
|
hook.HTTPSURL, |
|
|
|
} |
|
|
|
cmd := exec.Command("bash", args...) |
|
|
|
|
|
|
|
env := os.Environ() |
|
|
|
envs := []string{ |
|
|
|
"GIT_DEPLOY_JOB_ID=" + jobID1, |
|
|
|
"GIT_DEPLOY_PROMOTE_TO=" + promoteTo, |
|
|
|
"GIT_REF_NAME=" + hook.RefName, |
|
|
|
"GIT_REF_TYPE=" + hook.RefType, |
|
|
|
"GIT_REPO_OWNER=" + hook.Owner, |
|
|
|
"GIT_REPO_NAME=" + hook.Repo, |
|
|
|
"GIT_CLONE_URL=" + hook.HTTPSURL, |
|
|
|
} |
|
|
|
cmd.Env = append(env, envs...) |
|
|
|
cmd.Stdout = os.Stdout |
|
|
|
cmd.Stderr = os.Stderr |
|
|
|
|
|
|
|
if _, exists := jobs[jobID1]; exists { |
|
|
|
// TODO put promote in backlog
|
|
|
|
log.Printf("git-deploy job already started for %s#%s\n", hook.HTTPSURL, hook.RefName) |
|
|
|
return |
|
|
|
} |
|
|
|
if _, exists := jobs[jobID2]; exists { |
|
|
|
// TODO put promote in backlog
|
|
|
|
log.Printf("git-deploy job already started for %s#%s\n", hook.HTTPSURL, promoteTo) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
if err := cmd.Start(); nil != err { |
|
|
|
log.Printf("git-deploy exec error: %s\n", err) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
jobs[jobID1] = &job{ |
|
|
|
ID: jobID2, |
|
|
|
Cmd: cmd, |
|
|
|
Ref: hook, |
|
|
|
CreatedAt: time.Now(), |
|
|
|
} |
|
|
|
jobs[jobID2] = &job{ |
|
|
|
ID: jobID2, |
|
|
|
Cmd: cmd, |
|
|
|
Ref: hook, |
|
|
|
CreatedAt: time.Now(), |
|
|
|
} |
|
|
|
|
|
|
|
go func() { |
|
|
|
log.Printf("git-deploy promote for %s#%s started\n", hook.HTTPSURL, hook.RefName) |
|
|
|
_ = cmd.Wait() |
|
|
|
killers <- jobID1 |
|
|
|
killers <- jobID2 |
|
|
|
log.Printf("git-deploy promote for %s#%s finished\n", hook.HTTPSURL, hook.RefName) |
|
|
|
// TODO check for backlog
|
|
|
|
}() |
|
|
|
} |
|
|
|