add API to list, kill and promote
This commit is contained in:
parent
c66aa70f07
commit
15feae2d5e
|
@ -0,0 +1,20 @@
|
|||
#!/bin/bash
|
||||
|
||||
for x in $@; do
|
||||
echo "$x"
|
||||
done
|
||||
|
||||
my_envs='GIT_DEPLOY_JOB_ID
|
||||
GIT_DEPLOY_PROMOTE_TO
|
||||
GIT_REF_NAME
|
||||
GIT_REF_TYPE
|
||||
GIT_REPO_OWNER
|
||||
GIT_REPO_NAME
|
||||
GIT_CLONE_URL'
|
||||
|
||||
echo 'Doing "work" ...'
|
||||
sleep 5
|
||||
|
||||
for x in $my_envs; do
|
||||
echo "$x=${!x}"
|
||||
done
|
|
@ -13,29 +13,29 @@ import (
|
|||
// Repo ex: example
|
||||
// Org ex: example
|
||||
type Ref struct {
|
||||
HTTPSURL string
|
||||
SSHURL string
|
||||
Rev string
|
||||
Ref string
|
||||
RefType string // tags, heads
|
||||
RefName string
|
||||
Branch string
|
||||
Tag string
|
||||
Owner string
|
||||
Repo string
|
||||
HTTPSURL string `json:"clone_url"`
|
||||
SSHURL string `json:"-"`
|
||||
Rev string `json:"-"`
|
||||
Ref string `json:"-"` // refs/tags/v0.0.1, refs/heads/master
|
||||
RefType string `json:"ref_type"` // tag, branch
|
||||
RefName string `json:"ref_name"`
|
||||
Branch string `json:"-"`
|
||||
Tag string `json:"-"`
|
||||
Owner string `json:"repo_owner"`
|
||||
Repo string `json:"repo_name"`
|
||||
}
|
||||
|
||||
var Providers = make(map[string]func())
|
||||
var Webhooks = make(map[string]func(chi.Router))
|
||||
|
||||
var hooks = make(chan Ref)
|
||||
var Hooks = make(chan Ref)
|
||||
|
||||
func Hook(r Ref) {
|
||||
hooks <- r
|
||||
Hooks <- r
|
||||
}
|
||||
|
||||
func Accept() Ref {
|
||||
return <-hooks
|
||||
return <-Hooks
|
||||
}
|
||||
|
||||
func AddProvider(name string, initProvider func()) {
|
||||
|
|
236
main.go
236
main.go
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"compress/flate"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
@ -44,13 +45,17 @@ type job struct {
|
|||
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,16 +269,33 @@ func serve() {
|
|||
}
|
||||
|
||||
go func() {
|
||||
// TODO read from backlog
|
||||
for {
|
||||
hook := webhooks.Accept()
|
||||
// TODO os.Exec
|
||||
//hook := webhooks.Accept()
|
||||
select {
|
||||
case hook := <-webhooks.Hooks:
|
||||
runHook(hook)
|
||||
case jobID := <-killers:
|
||||
kill(jobID)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := srv.ListenAndServe(); nil != err {
|
||||
fmt.Fprintf(os.Stderr, "%s", err)
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func runHook(hook webhooks.Ref) {
|
||||
fmt.Printf("%#v\n", hook)
|
||||
jobID := base64.URLEncoding.EncodeToString([]byte(
|
||||
jobID := base64.RawURLEncoding.EncodeToString([]byte(
|
||||
fmt.Sprintf("%s#%s", hook.HTTPSURL, hook.RefName),
|
||||
))
|
||||
|
||||
args := []string{
|
||||
runOpts.Exec,
|
||||
runOpts.Exec + "/deploy.sh",
|
||||
jobID,
|
||||
hook.RefName,
|
||||
hook.RefType,
|
||||
|
@ -206,20 +333,105 @@ func serve() {
|
|||
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()
|
||||
delete(jobs, jobID)
|
||||
killers <- jobID
|
||||
log.Printf("git-deploy job for %s#%s finished\n", hook.HTTPSURL, hook.RefName)
|
||||
// TODO check for backlog
|
||||
}()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if err := srv.ListenAndServe(); nil != err {
|
||||
fmt.Fprintf(os.Stderr, "%s", err)
|
||||
os.Exit(1)
|
||||
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
|
||||
}()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue