move api to own file, add backlog
This commit is contained in:
parent
6b85b07c0d
commit
d17d521e4a
|
@ -0,0 +1,457 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/gitdeploy/internal/options"
|
||||||
|
"git.rootprojects.org/root/gitdeploy/internal/webhooks"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
type job struct {
|
||||||
|
ID string // {HTTPSURL}#{BRANCH}
|
||||||
|
Cmd *exec.Cmd
|
||||||
|
GitRef webhooks.Ref
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobs = make(map[string]*job)
|
||||||
|
var killers = make(chan string)
|
||||||
|
var tmpDir string
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
GitRef 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 init() {
|
||||||
|
var err error
|
||||||
|
tmpDir, err = ioutil.TempDir("", "gitdeploy-*")
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "could not create temporary directory")
|
||||||
|
os.Exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("TEMP_DIR=%s", tmpDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route will set up the API and such
|
||||||
|
func Route(r chi.Router, runOpts *options.ServerConfig) {
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// TODO read from backlog
|
||||||
|
for {
|
||||||
|
//hook := webhooks.Accept()
|
||||||
|
select {
|
||||||
|
case hook := <-webhooks.Hooks:
|
||||||
|
runHook(hook, runOpts)
|
||||||
|
case jobID := <-killers:
|
||||||
|
remove(jobID, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
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("/repos", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
repos := []Repo{}
|
||||||
|
|
||||||
|
for _, id := range strings.Fields(runOpts.RepoList) {
|
||||||
|
repos = append(repos, Repo{
|
||||||
|
ID: id,
|
||||||
|
CloneURL: fmt.Sprintf("https://%s.git", id),
|
||||||
|
Promotions: runOpts.Promotions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
err := filepath.Walk(runOpts.ScriptsPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if nil != err {
|
||||||
|
fmt.Printf("error walking %q: %v\n", path, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// "scripts/github.com/org/repo"
|
||||||
|
parts := strings.Split(filepath.ToSlash(path), "/")
|
||||||
|
if len(parts) < 3 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
path = strings.Join(parts[1:], "/")
|
||||||
|
if info.Mode().IsRegular() && "deploy.sh" == info.Name() && runOpts.ScriptsPath != path {
|
||||||
|
id := filepath.Dir(path)
|
||||||
|
repos = append(repos, Repo{
|
||||||
|
ID: id,
|
||||||
|
CloneURL: fmt.Sprintf("https://%s.git", id),
|
||||||
|
Promotions: runOpts.Promotions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, "the scripts directory disappeared", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, _ := json.MarshalIndent(ReposResponse{
|
||||||
|
Success: true,
|
||||||
|
Repos: repos,
|
||||||
|
}, "", " ")
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(append(b, '\n'))
|
||||||
|
})
|
||||||
|
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,
|
||||||
|
GitRef: job.GitRef,
|
||||||
|
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 runOpts.Promotions {
|
||||||
|
if runOpts.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 := runOpts.Promotions[n]
|
||||||
|
runPromote(*msg, promoteTo, runOpts)
|
||||||
|
|
||||||
|
b, _ := json.Marshal(struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
PromoteTo string `json:"promote_to"`
|
||||||
|
}{
|
||||||
|
Success: true,
|
||||||
|
PromoteTo: promoteTo,
|
||||||
|
})
|
||||||
|
w.Write(append(b, '\n'))
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func runHook(hook webhooks.Ref, runOpts *options.ServerConfig) {
|
||||||
|
fmt.Printf("%#v\n", hook)
|
||||||
|
jobID := base64.RawURLEncoding.EncodeToString([]byte(
|
||||||
|
fmt.Sprintf("%s#%s", hook.HTTPSURL, hook.RefName),
|
||||||
|
))
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
runOpts.ScriptsPath + "/deploy.sh",
|
||||||
|
jobID,
|
||||||
|
hook.RefName,
|
||||||
|
hook.RefType,
|
||||||
|
hook.Owner,
|
||||||
|
hook.Repo,
|
||||||
|
hook.HTTPSURL,
|
||||||
|
}
|
||||||
|
cmd := exec.Command("bash", args...)
|
||||||
|
|
||||||
|
// https://git.example.com/example/project.git
|
||||||
|
// => git.example.com/example/project
|
||||||
|
repoID := strings.TrimPrefix(hook.HTTPSURL, "https://")
|
||||||
|
repoID = strings.TrimPrefix(repoID, "https://")
|
||||||
|
repoID = strings.TrimSuffix(repoID, ".git")
|
||||||
|
jobName := fmt.Sprintf("%s#%s", strings.ReplaceAll(repoID, "/", "-"), hook.RefName)
|
||||||
|
|
||||||
|
env := os.Environ()
|
||||||
|
envs := []string{
|
||||||
|
"GIT_DEPLOY_JOB_ID=" + jobID,
|
||||||
|
"GIT_REF_NAME=" + hook.RefName,
|
||||||
|
"GIT_REF_TYPE=" + hook.RefType,
|
||||||
|
"GIT_REPO_ID=" + repoID,
|
||||||
|
"GIT_REPO_OWNER=" + hook.Owner,
|
||||||
|
"GIT_REPO_NAME=" + hook.Repo,
|
||||||
|
"GIT_CLONE_URL=" + hook.HTTPSURL, // deprecated
|
||||||
|
"GIT_HTTPS_URL=" + hook.HTTPSURL,
|
||||||
|
"GIT_SSH_URL=" + hook.SSHURL,
|
||||||
|
}
|
||||||
|
for _, repo := range strings.Fields(runOpts.RepoList) {
|
||||||
|
last := len(repo) - 1
|
||||||
|
if len(repo) < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
repoID = strings.ToLower(repoID)
|
||||||
|
if '*' == repo[last] {
|
||||||
|
// Wildcard match a prefix, for example:
|
||||||
|
// github.com/whatever/* MATCHES github.com/whatever/foo
|
||||||
|
// github.com/whatever/ProjectX-* MATCHES github.com/whatever/ProjectX-Foo
|
||||||
|
if strings.HasPrefix(repoID, repo[:last]) {
|
||||||
|
envs = append(envs, "GIT_REPO_TRUSTED=true")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if repo == repoID {
|
||||||
|
envs = append(envs, "GIT_REPO_TRUSTED=true")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd.Env = append(env, envs...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if _, exists := jobs[jobID]; exists {
|
||||||
|
saveBacklog(hook, jobName, jobID)
|
||||||
|
log.Printf("[runHook] gitdeploy job already started for %s#%s\n", hook.HTTPSURL, hook.RefName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); nil != err {
|
||||||
|
log.Printf("gitdeploy exec error: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs[jobID] = &job{
|
||||||
|
ID: jobID,
|
||||||
|
Cmd: cmd,
|
||||||
|
GitRef: hook,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Printf("gitdeploy job for %s#%s started\n", hook.HTTPSURL, hook.RefName)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
remove(jobID, true)
|
||||||
|
restoreBacklog(jobName, jobID)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(jobID string, nokill bool) {
|
||||||
|
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, runOpts *options.ServerConfig) {
|
||||||
|
// 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.ScriptsPath + "/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("[promote] gitdeploy job already started for %s#%s\n", hook.HTTPSURL, hook.RefName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, exists := jobs[jobID2]; exists {
|
||||||
|
// TODO put promote in backlog
|
||||||
|
log.Printf("[promote] gitdeploy job already started for %s#%s\n", hook.HTTPSURL, promoteTo)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); nil != err {
|
||||||
|
log.Printf("gitdeploy exec error: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs[jobID1] = &job{
|
||||||
|
ID: jobID2,
|
||||||
|
Cmd: cmd,
|
||||||
|
GitRef: hook,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
jobs[jobID2] = &job{
|
||||||
|
ID: jobID2,
|
||||||
|
Cmd: cmd,
|
||||||
|
GitRef: hook,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Printf("gitdeploy promote for %s#%s started\n", hook.HTTPSURL, hook.RefName)
|
||||||
|
_ = cmd.Wait()
|
||||||
|
killers <- jobID1
|
||||||
|
killers <- jobID2
|
||||||
|
log.Printf("gitdeploy promote for %s#%s finished\n", hook.HTTPSURL, hook.RefName)
|
||||||
|
// TODO check for backlog
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveBacklog(hook webhooks.Ref, jobName, jobID string) {
|
||||||
|
b, _ := json.MarshalIndent(hook, "", " ")
|
||||||
|
f, err := ioutil.TempFile(tmpDir, "tmp-*")
|
||||||
|
if nil != err {
|
||||||
|
log.Printf("[warn] could not create backlog file for %s:\n%v", jobID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := f.Write(b); nil != err {
|
||||||
|
log.Printf("[warn] could not write backlog file for %s:\n%v", jobID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobFile := filepath.Join(tmpDir, jobName)
|
||||||
|
_ = os.Remove(jobFile)
|
||||||
|
if err := os.Rename(f.Name(), jobFile); nil != err {
|
||||||
|
log.Printf("[warn] could not rename file %s => %s:\n%v", f.Name(), jobFile, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("[BACKLOG] new backlog job for %s", jobName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreBacklog(jobName, jobID string) {
|
||||||
|
jobFile := filepath.Join(tmpDir, jobName)
|
||||||
|
_ = os.Remove(jobFile + ".cur")
|
||||||
|
_ = os.Rename(jobFile, jobFile+".cur")
|
||||||
|
|
||||||
|
b, err := ioutil.ReadFile(jobFile + ".cur")
|
||||||
|
if nil != err {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
log.Printf("[warn] could not create backlog file for %s:\n%v", jobID, err)
|
||||||
|
}
|
||||||
|
// doesn't exist => no backlog
|
||||||
|
log.Printf("[NO BACKLOG] no backlog items for %s", jobName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ref := webhooks.Ref{}
|
||||||
|
if err := json.Unmarshal(b, &ref); nil != err {
|
||||||
|
log.Printf("[warn] could not parse backlog file for %s:\n%v", jobID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("[BACKLOG] pop backlog for %s", jobName)
|
||||||
|
webhooks.Hook(ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReposResponse is the successful response to /api/repos
|
||||||
|
type ReposResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Repos []Repo `json:"repos"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repo is one of the elements of /api/repos
|
||||||
|
type Repo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
CloneURL string `json:"clone_url"`
|
||||||
|
Promotions []string `json:"_promotions"`
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ type ServerConfig struct {
|
||||||
Compress bool
|
Compress bool
|
||||||
ServePath string
|
ServePath string
|
||||||
ScriptsPath string
|
ScriptsPath string
|
||||||
|
Promotions []string
|
||||||
}
|
}
|
||||||
|
|
||||||
var ServerFlags *flag.FlagSet
|
var ServerFlags *flag.FlagSet
|
||||||
|
|
|
@ -16,45 +16,55 @@ import (
|
||||||
// Repo ex: example
|
// Repo ex: example
|
||||||
// Org ex: example
|
// Org ex: example
|
||||||
type Ref struct {
|
type Ref struct {
|
||||||
HTTPSURL string `json:"clone_url"`
|
HTTPSURL string `json:"https_url"`
|
||||||
SSHURL string `json:"-"`
|
SSHURL string `json:"ssh_url"`
|
||||||
Rev string `json:"-"`
|
Rev string `json:"rev"`
|
||||||
Ref string `json:"-"` // refs/tags/v0.0.1, refs/heads/master
|
Ref string `json:"ref"` // refs/tags/v0.0.1, refs/heads/master
|
||||||
RefType string `json:"ref_type"` // tag, branch
|
RefType string `json:"ref_type"` // tag, branch
|
||||||
RefName string `json:"ref_name"`
|
RefName string `json:"ref_name"`
|
||||||
Branch string `json:"-"`
|
Branch string `json:"branch"`
|
||||||
Tag string `json:"-"`
|
Tag string `json:"tag"`
|
||||||
Owner string `json:"repo_owner"`
|
Owner string `json:"repo_owner"`
|
||||||
Repo string `json:"repo_name"`
|
Repo string `json:"repo_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Providers is a map of the git webhook providers
|
||||||
var Providers = make(map[string]func())
|
var Providers = make(map[string]func())
|
||||||
|
|
||||||
|
// Webhooks is a map of routes
|
||||||
var Webhooks = make(map[string]func(chi.Router))
|
var Webhooks = make(map[string]func(chi.Router))
|
||||||
|
|
||||||
|
// Hooks is a channel of incoming webhooks
|
||||||
var Hooks = make(chan Ref)
|
var Hooks = make(chan Ref)
|
||||||
|
|
||||||
|
// Hook will put a Git Ref on the queue
|
||||||
func Hook(r Ref) {
|
func Hook(r Ref) {
|
||||||
Hooks <- r
|
Hooks <- r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accept will pop a Git Ref off the queue
|
||||||
func Accept() Ref {
|
func Accept() Ref {
|
||||||
return <-Hooks
|
return <-Hooks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddProvider registers a git webhook provider
|
||||||
func AddProvider(name string, initProvider func()) {
|
func AddProvider(name string, initProvider func()) {
|
||||||
Providers[name] = initProvider
|
Providers[name] = initProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddRouteHandler registers a git webhook route
|
||||||
func AddRouteHandler(name string, route func(router chi.Router)) {
|
func AddRouteHandler(name string, route func(router chi.Router)) {
|
||||||
Webhooks[name] = route
|
Webhooks[name] = route
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustRegisterAll registers all webhook route functions
|
||||||
func MustRegisterAll() {
|
func MustRegisterAll() {
|
||||||
for _, addHandler := range Providers {
|
for _, addHandler := range Providers {
|
||||||
addHandler()
|
addHandler()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RouteHandlers registers the webhook functions to the route
|
||||||
func RouteHandlers(r chi.Router) {
|
func RouteHandlers(r chi.Router) {
|
||||||
r.Route("/api/webhooks", func(r chi.Router) {
|
r.Route("/api/webhooks", func(r chi.Router) {
|
||||||
for provider, handler := range Webhooks {
|
for provider, handler := range Webhooks {
|
||||||
|
@ -65,6 +75,7 @@ func RouteHandlers(r chi.Router) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseSecrets grabs secrets from the ENV at runtime
|
||||||
func ParseSecrets(providername, secretList, envname string) [][]byte {
|
func ParseSecrets(providername, secretList, envname string) [][]byte {
|
||||||
if 0 == len(secretList) {
|
if 0 == len(secretList) {
|
||||||
secretList = os.Getenv(envname)
|
secretList = os.Getenv(envname)
|
||||||
|
|
418
main.go
418
main.go
|
@ -2,26 +2,26 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"compress/flate"
|
"compress/flate"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.rootprojects.org/root/gitdeploy/assets/examples"
|
"git.rootprojects.org/root/gitdeploy/assets/examples"
|
||||||
"git.rootprojects.org/root/gitdeploy/assets/public"
|
"git.rootprojects.org/root/gitdeploy/assets/public"
|
||||||
|
"git.rootprojects.org/root/gitdeploy/internal/api"
|
||||||
"git.rootprojects.org/root/gitdeploy/internal/options"
|
"git.rootprojects.org/root/gitdeploy/internal/options"
|
||||||
"git.rootprojects.org/root/gitdeploy/internal/webhooks"
|
"git.rootprojects.org/root/gitdeploy/internal/webhooks"
|
||||||
"git.rootprojects.org/root/vfscopy"
|
"git.rootprojects.org/root/vfscopy"
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/go-chi/chi/middleware"
|
"github.com/go-chi/chi/middleware"
|
||||||
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
_ "github.com/joho/godotenv/autoload"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,16 +44,6 @@ func ver() string {
|
||||||
return fmt.Sprintf("%s v%s (%s) %s", name, version, commit[:7], date)
|
return fmt.Sprintf("%s v%s (%s) %s", name, version, commit[:7], date)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 runOpts *options.ServerConfig
|
||||||
var runFlags *flag.FlagSet
|
var runFlags *flag.FlagSet
|
||||||
var initFlags *flag.FlagSet
|
var initFlags *flag.FlagSet
|
||||||
|
@ -171,13 +161,16 @@ func main() {
|
||||||
runOpts.RepoList = strings.ReplaceAll(runOpts.RepoList, " ", " ")
|
runOpts.RepoList = strings.ReplaceAll(runOpts.RepoList, " ", " ")
|
||||||
runOpts.RepoList = strings.ToLower(runOpts.RepoList)
|
runOpts.RepoList = strings.ToLower(runOpts.RepoList)
|
||||||
}
|
}
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
log.Printf("WORK_DIR=%s", cwd)
|
||||||
|
log.Printf("TRUST_REPOS=%s", runOpts.RepoList)
|
||||||
if 0 == len(promotionList) {
|
if 0 == len(promotionList) {
|
||||||
promotionList = os.Getenv("PROMOTIONS")
|
promotionList = os.Getenv("PROMOTIONS")
|
||||||
}
|
}
|
||||||
if 0 == len(promotionList) {
|
if 0 == len(promotionList) {
|
||||||
promotionList = defaultPromotionList
|
promotionList = defaultPromotionList
|
||||||
}
|
}
|
||||||
promotions = strings.Fields(
|
runOpts.Promotions = strings.Fields(
|
||||||
strings.ReplaceAll(promotionList, ",", " "),
|
strings.ReplaceAll(promotionList, ",", " "),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -190,20 +183,6 @@ 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 gdInit() {
|
func gdInit() {
|
||||||
vfs := vfscopy.NewVFS(examples.Assets)
|
vfs := vfscopy.NewVFS(examples.Assets)
|
||||||
_, err := os.Open("scripts")
|
_, err := os.Open("scripts")
|
||||||
|
@ -260,7 +239,25 @@ func serve() {
|
||||||
}
|
}
|
||||||
r.Use(middleware.Logger)
|
r.Use(middleware.Logger)
|
||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
r.Use(middleware.Recoverer)
|
|
||||||
|
r.Get("/version", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(append([]byte(ver()), '\n'))
|
||||||
|
})
|
||||||
|
r.Get("/api/version", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
b, _ := json.MarshalIndent(struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
Commit string `json:"commit"`
|
||||||
|
}{
|
||||||
|
Name: name,
|
||||||
|
Version: version,
|
||||||
|
Date: date,
|
||||||
|
Commit: commit,
|
||||||
|
}, "", " ")
|
||||||
|
w.Write(append(b, '\n'))
|
||||||
|
})
|
||||||
|
api.Route(r, runOpts)
|
||||||
|
|
||||||
var staticHandler http.HandlerFunc
|
var staticHandler http.HandlerFunc
|
||||||
pub := http.FileServer(public.Assets)
|
pub := http.FileServer(public.Assets)
|
||||||
|
@ -281,167 +278,6 @@ func serve() {
|
||||||
pub.ServeHTTP(w, r)
|
pub.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
webhooks.RouteHandlers(r)
|
|
||||||
|
|
||||||
r.Get("/version", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Write(append([]byte(ver()), '\n'))
|
|
||||||
})
|
|
||||||
r.Get("/api/version", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
b, _ := json.MarshalIndent(struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Date string `json:"date"`
|
|
||||||
Commit string `json:"commit"`
|
|
||||||
}{
|
|
||||||
Name: name,
|
|
||||||
Version: version,
|
|
||||||
Date: date,
|
|
||||||
Commit: commit,
|
|
||||||
}, "", " ")
|
|
||||||
w.Write(append(b, '\n'))
|
|
||||||
})
|
|
||||||
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("/repos", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
repos := []Repo{}
|
|
||||||
|
|
||||||
for _, id := range strings.Fields(runOpts.RepoList) {
|
|
||||||
repos = append(repos, Repo{
|
|
||||||
ID: id,
|
|
||||||
CloneURL: fmt.Sprintf("https://%s.git", id),
|
|
||||||
Promotions: promotions,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
err := filepath.Walk(runOpts.ScriptsPath, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if nil != err {
|
|
||||||
fmt.Printf("error walking %q: %v\n", path, err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// "scripts/github.com/org/repo"
|
|
||||||
parts := strings.Split(filepath.ToSlash(path), "/")
|
|
||||||
if len(parts) < 3 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
path = strings.Join(parts[1:], "/")
|
|
||||||
if info.Mode().IsRegular() && "deploy.sh" == info.Name() && runOpts.ScriptsPath != path {
|
|
||||||
id := filepath.Dir(path)
|
|
||||||
repos = append(repos, Repo{
|
|
||||||
ID: id,
|
|
||||||
CloneURL: fmt.Sprintf("https://%s.git", id),
|
|
||||||
Promotions: promotions,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if nil != err {
|
|
||||||
http.Error(w, "the scripts directory disappeared", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
b, _ := json.MarshalIndent(ReposResponse{
|
|
||||||
Success: true,
|
|
||||||
Repos: repos,
|
|
||||||
}, "", " ")
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write(append(b, '\n'))
|
|
||||||
})
|
|
||||||
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)
|
|
||||||
|
|
||||||
b, _ := json.Marshal(struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
PromoteTo string `json:"promote_to"`
|
|
||||||
}{
|
|
||||||
Success: true,
|
|
||||||
PromoteTo: promoteTo,
|
|
||||||
})
|
|
||||||
w.Write(append(b, '\n'))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
r.Get("/*", staticHandler)
|
r.Get("/*", staticHandler)
|
||||||
|
|
||||||
fmt.Println("Listening for http (with reasonable timeouts) on", runOpts.Addr)
|
fmt.Println("Listening for http (with reasonable timeouts) on", runOpts.Addr)
|
||||||
|
@ -453,213 +289,9 @@ func serve() {
|
||||||
WriteTimeout: 20 * time.Second,
|
WriteTimeout: 20 * time.Second,
|
||||||
MaxHeaderBytes: 1024 * 1024, // 1MiB
|
MaxHeaderBytes: 1024 * 1024, // 1MiB
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
|
||||||
// TODO read from backlog
|
|
||||||
for {
|
|
||||||
//hook := webhooks.Accept()
|
|
||||||
select {
|
|
||||||
case hook := <-webhooks.Hooks:
|
|
||||||
runHook(hook)
|
|
||||||
case jobID := <-killers:
|
|
||||||
remove(jobID, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := srv.ListenAndServe(); nil != err {
|
if err := srv.ListenAndServe(); nil != err {
|
||||||
fmt.Fprintf(os.Stderr, "%s", err)
|
fmt.Fprintf(os.Stderr, "%s", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
return
|
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.ScriptsPath + "/deploy.sh",
|
|
||||||
jobID,
|
|
||||||
hook.RefName,
|
|
||||||
hook.RefType,
|
|
||||||
hook.Owner,
|
|
||||||
hook.Repo,
|
|
||||||
hook.HTTPSURL,
|
|
||||||
}
|
|
||||||
cmd := exec.Command("bash", args...)
|
|
||||||
|
|
||||||
// https://git.example.com/example/project.git
|
|
||||||
// => git.example.com/example/project
|
|
||||||
repoID := strings.TrimPrefix(hook.HTTPSURL, "https://")
|
|
||||||
repoID = strings.TrimPrefix(repoID, "https://")
|
|
||||||
repoID = strings.TrimSuffix(repoID, ".git")
|
|
||||||
|
|
||||||
env := os.Environ()
|
|
||||||
envs := []string{
|
|
||||||
"GIT_DEPLOY_JOB_ID=" + jobID,
|
|
||||||
"GIT_REF_NAME=" + hook.RefName,
|
|
||||||
"GIT_REF_TYPE=" + hook.RefType,
|
|
||||||
"GIT_REPO_ID=" + repoID,
|
|
||||||
"GIT_REPO_OWNER=" + hook.Owner,
|
|
||||||
"GIT_REPO_NAME=" + hook.Repo,
|
|
||||||
"GIT_CLONE_URL=" + hook.HTTPSURL,
|
|
||||||
}
|
|
||||||
for _, repo := range strings.Fields(runOpts.RepoList) {
|
|
||||||
last := len(repo) - 1
|
|
||||||
if len(repo) < 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
repoID = strings.ToLower(repoID)
|
|
||||||
if '*' == repo[last] {
|
|
||||||
// Wildcard match a prefix, for example:
|
|
||||||
// github.com/whatever/* MATCHES github.com/whatever/foo
|
|
||||||
// github.com/whatever/ProjectX-* MATCHES github.com/whatever/ProjectX-Foo
|
|
||||||
if strings.HasPrefix(repoID, repo[:last]) {
|
|
||||||
envs = append(envs, "GIT_REPO_TRUSTED=true")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else if repo == repoID {
|
|
||||||
envs = append(envs, "GIT_REPO_TRUSTED=true")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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("gitdeploy job already started for %s#%s\n", hook.HTTPSURL, hook.RefName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); nil != err {
|
|
||||||
log.Printf("gitdeploy exec error: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jobs[jobID] = &job{
|
|
||||||
ID: jobID,
|
|
||||||
Cmd: cmd,
|
|
||||||
Ref: hook,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
log.Printf("gitdeploy job for %s#%s started\n", hook.HTTPSURL, hook.RefName)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
remove(jobID, true)
|
|
||||||
// TODO check for backlog
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func remove(jobID string, nokill bool) {
|
|
||||||
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.ScriptsPath + "/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("gitdeploy job already started for %s#%s\n", hook.HTTPSURL, hook.RefName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, exists := jobs[jobID2]; exists {
|
|
||||||
// TODO put promote in backlog
|
|
||||||
log.Printf("gitdeploy job already started for %s#%s\n", hook.HTTPSURL, promoteTo)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); nil != err {
|
|
||||||
log.Printf("gitdeploy 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("gitdeploy promote for %s#%s started\n", hook.HTTPSURL, hook.RefName)
|
|
||||||
_ = cmd.Wait()
|
|
||||||
killers <- jobID1
|
|
||||||
killers <- jobID2
|
|
||||||
log.Printf("gitdeploy promote for %s#%s finished\n", hook.HTTPSURL, hook.RefName)
|
|
||||||
// TODO check for backlog
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReposResponse is the successful response to /api/repos
|
|
||||||
type ReposResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Repos []Repo `json:"repos"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repo is one of the elements of /api/repos
|
|
||||||
type Repo struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
CloneURL string `json:"clone_url"`
|
|
||||||
Promotions []string `json:"_promotions"`
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue