Browse Source

add API to list, kill and promote

master
AJ ONeal 4 years ago
parent
commit
15feae2d5e
  1. 0
      examples/deploy.sh
  2. 20
      examples/promote.sh
  3. 26
      internal/webhooks/webhooks.go
  4. 328
      main.go

0
example.sh → examples/deploy.sh

20
examples/promote.sh

@ -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

26
internal/webhooks/webhooks.go

@ -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()) {

328
main.go

@ -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
}()
}

Loading…
Cancel
Save