set and get job reports

This commit is contained in:
AJ ONeal 2021-02-23 19:34:33 -07:00
parent 70291c3bce
commit 2873457cf1
8 changed files with 461 additions and 258 deletions

View File

@ -7,7 +7,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"git.rootprojects.org/root/gitdeploy/internal/jobs" "git.rootprojects.org/root/gitdeploy/internal/jobs"
"git.rootprojects.org/root/gitdeploy/internal/log" "git.rootprojects.org/root/gitdeploy/internal/log"
@ -17,14 +16,248 @@ import (
"github.com/go-chi/chi" "github.com/go-chi/chi"
) )
// HookResponse is a GitRef but with a little extra as HTTP response type HTTPError struct {
type HookResponse struct { Success bool `json:"success"`
RepoID string `json:"repo_id"` Code string `json:"code,omitempty"`
CreatedAt time.Time `json:"created_at"` Message string `json:"message,omitempty"`
EndedAt time.Time `json:"ended_at"` Detail string `json:"detail,omitempty"`
ExitCode *int `json:"exit_code,omitempty"` }
Log string `json:"log"`
LogURL string `json:"log_url"` type Report struct {
Report *jobs.Report `json:"report"`
}
// Route will set up the API and such
func Route(r chi.Router, runOpts *options.ServerConfig) {
jobs.Start(runOpts)
RouteStopped(r, runOpts)
}
// RouteStopped is for testing
func RouteStopped(r chi.Router, runOpts *options.ServerConfig) {
webhooks.RouteHandlers(r)
r.Route("/api", 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.Printf("TODO: handle authentication")
next.ServeHTTP(w, r)
})
})
r.Route("/admin", func(r chi.Router) {
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("/logs/{oldID}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
oldID := webhooks.URLSafeGitID(chi.URLParam(r, "oldID"))
// TODO add `since`
j, err := jobs.LoadLogs(runOpts, oldID)
if nil != err {
w.WriteHeader(404)
w.Write([]byte(
`{ "success": false, "error": "job log does not exist" }` + "\n",
))
return
}
b, _ := json.MarshalIndent(struct {
Success bool `json:"success"`
jobs.Job
}{
Success: true,
Job: *j,
}, "", " ")
w.Write(append(b, '\n'))
})
/*
r.Get("/logs/*", func(w http.ResponseWriter, r *http.Request) {
// TODO add ?since=
// TODO JSON logs
logPath := chi.URLParam(r, "*")
f, err := os.Open(filepath.Join(os.Getenv("LOG_DIR"), logPath))
if nil != err {
w.WriteHeader(404)
w.Write([]byte(
`{ "success": false, "error": "job log does not exist" }` + "\n",
))
return
}
io.Copy(w, f)
})
*/
r.Get("/jobs", func(w http.ResponseWriter, r *http.Request) {
all := jobs.All()
b, _ := json.Marshal(struct {
Success bool `json:"success"`
Jobs []*jobs.Job `json:"jobs"`
}{
Success: true,
Jobs: all,
})
w.Write(append(b, '\n'))
})
r.Post("/jobs", func(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
msg := &jobs.KillMsg{}
if err := decoder.Decode(msg); nil != err {
log.Printf("kill job invalid json:\n%v", err)
http.Error(w, "invalid json body", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
if _, ok := jobs.Actives.Load(webhooks.URLSafeRefID(msg.JobID)); !ok {
if _, ok := jobs.Pending.Load(webhooks.URLSafeRefID(msg.JobID)); !ok {
w.Write([]byte(
`{ "success": false, "error": "job does not exist" }` + "\n",
))
return
}
}
// killing a job *should* always succeed ...right?
jobs.Remove(webhooks.URLSafeRefID(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.Printf("promotion job invalid json:\n%v", err)
http.Error(w, "invalid json body", http.StatusBadRequest)
return
}
if "" == msg.HTTPSURL || "" == msg.RefName {
log.Printf("promotion job incomplete json %s", 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.Printf("promotion job invalid: cannot promote: %d", n)
http.Error(w, "invalid promotion", http.StatusBadRequest)
return
}
promoteTo := runOpts.Promotions[n]
jobs.Promote(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.Route("/local", func(r chi.Router) {
r.Post("/jobs/{jobID}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
decoder := json.NewDecoder(r.Body)
report := &Report{}
if err := decoder.Decode(report); nil != err {
w.WriteHeader(http.StatusBadRequest)
writeError(w, HTTPError{
Code: "E_PARSE",
Message: "could not parse request body",
Detail: err.Error(),
})
return
}
jobID := webhooks.URLSafeRefID(chi.URLParam(r, "jobID"))
if err := jobs.SetReport(jobID, report.Report); nil != err {
w.WriteHeader(http.StatusInternalServerError)
writeError(w, HTTPError{
Code: "E_SERVER",
Message: "could not update report",
Detail: err.Error(),
})
return
}
// Attach additional logs / reports to running job
w.Write([]byte(`{ "success": true }` + "\n"))
})
})
})
}
func writeError(w http.ResponseWriter, err HTTPError) {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
_ = enc.Encode(err)
w.Write([]byte("\n"))
} }
// ReposResponse is the successful response to /api/repos // ReposResponse is the successful response to /api/repos
@ -39,200 +272,3 @@ type Repo struct {
CloneURL string `json:"clone_url"` CloneURL string `json:"clone_url"`
Promotions []string `json:"_promotions"` Promotions []string `json:"_promotions"`
} }
// Route will set up the API and such
func Route(r chi.Router, runOpts *options.ServerConfig) {
jobs.Start(runOpts)
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.Printf("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("/logs/{oldID}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
oldID := webhooks.URLSafeGitID(chi.URLParam(r, "oldID"))
// TODO add `since`
j, err := jobs.LoadLogs(runOpts, oldID)
if nil != err {
w.WriteHeader(404)
w.Write([]byte(
`{ "success": false, "error": "job log does not exist" }` + "\n",
))
return
}
b, _ := json.Marshal(struct {
Success bool `json:"success"`
jobs.Job
}{
Success: true,
Job: *j,
})
w.Write(append(b, '\n'))
})
/*
r.Get("/logs/*", func(w http.ResponseWriter, r *http.Request) {
// TODO add ?since=
// TODO JSON logs
logPath := chi.URLParam(r, "*")
f, err := os.Open(filepath.Join(os.Getenv("LOG_DIR"), logPath))
if nil != err {
w.WriteHeader(404)
w.Write([]byte(
`{ "success": false, "error": "job log does not exist" }` + "\n",
))
return
}
io.Copy(w, f)
})
*/
r.Get("/jobs", func(w http.ResponseWriter, r *http.Request) {
all := jobs.All()
b, _ := json.Marshal(struct {
Success bool `json:"success"`
Jobs []*jobs.Job `json:"jobs"`
}{
Success: true,
Jobs: all,
})
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 := &jobs.KillMsg{}
if err := decoder.Decode(msg); nil != err {
log.Printf("kill job invalid json:\n%v", err)
http.Error(w, "invalid json body", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
if _, ok := jobs.Actives.Load(webhooks.URLSafeRefID(msg.JobID)); !ok {
if _, ok := jobs.Pending.Load(webhooks.URLSafeRefID(msg.JobID)); !ok {
w.Write([]byte(
`{ "success": false, "error": "job does not exist" }` + "\n",
))
return
}
}
// killing a job *should* always succeed ...right?
jobs.Remove(webhooks.URLSafeRefID(msg.JobID))
w.Write([]byte(
`{ "success": true }` + "\n",
))
})
r.Post("/jobs/{jobID}", func(w http.ResponseWriter, r *http.Request) {
// Attach additional logs / reports to running job
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.Printf("promotion job invalid json:\n%v", err)
http.Error(w, "invalid json body", http.StatusBadRequest)
return
}
if "" == msg.HTTPSURL || "" == msg.RefName {
log.Printf("promotion job incomplete json %s", 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.Printf("promotion job invalid: cannot promote: %d", n)
http.Error(w, "invalid promotion", http.StatusBadRequest)
return
}
promoteTo := runOpts.Promotions[n]
jobs.Promote(msg, promoteTo)
b, _ := json.Marshal(struct {
Success bool `json:"success"`
PromoteTo string `json:"promote_to"`
}{
Success: true,
PromoteTo: promoteTo,
})
w.Write(append(b, '\n'))
})
})
}

107
internal/api/api_test.go Normal file
View File

@ -0,0 +1,107 @@
package api
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
"git.rootprojects.org/root/gitdeploy/internal/jobs"
"git.rootprojects.org/root/gitdeploy/internal/options"
"git.rootprojects.org/root/gitdeploy/internal/webhooks"
"github.com/go-chi/chi"
)
var server *httptest.Server
var runOpts *options.ServerConfig
var debounceDelay time.Duration
var jobDelay time.Duration
var logDir string
func init() {
tmpDir, _ := ioutil.TempDir("", "gitdeploy-*")
runOpts = &options.ServerConfig{
//Addr: "localhost:4483",
ScriptsPath: "./testdata",
LogDir: "./test-logs/debounce",
TmpDir: tmpDir,
DebounceDelay: 25 * time.Millisecond,
StaleJobAge: 5 * time.Minute,
StaleLogAge: 5 * time.Minute,
ExpiredLogAge: 10 * time.Minute,
}
logDir, _ = filepath.Abs(runOpts.LogDir)
r := chi.NewRouter()
server = httptest.NewServer(r)
runOpts.Addr = server.Listener.Addr().String()
RouteStopped(r, runOpts)
os.Setenv("GIT_DEPLOY_TEST_WAIT", "0.1")
debounceDelay = 50 * time.Millisecond
jobDelay = 250 * time.Millisecond
jobs.Start(runOpts)
//server.Close()
}
func TestCallback(t *testing.T) {
// TODO use full API request with local webhook
t7 := time.Now().Add(-40 * time.Second)
r7 := "ef1234abcd"
// skip debounce
hook := webhooks.Ref{
Timestamp: t7,
RepoID: "git.example.com/owner/repo",
HTTPSURL: "https://git.example.com/owner/repo.git",
Rev: r7,
RefName: "master",
RefType: "branch",
Owner: "owner",
Repo: "repo",
}
// , _ := json.MarshallIndent(&hook , "", " ")
jobs.Debounce(hook)
/*
body := bytes.NewReader(hook)
r := httptest.NewRequest("POST", "/api/local/webhook", body)
//dec := json.NewDecoder(r.Body)
//dec.Decode()
*/
t.Log("sleep so job can debounce, start, and finish")
time.Sleep(debounceDelay)
time.Sleep(jobDelay)
// TODO test that the API gives this back to us
urlRevID := hook.GetURLSafeRevID()
// TODO needs auth
reqURL := fmt.Sprintf("http://%s/api/admin/logs/%s",
runOpts.Addr,
string(urlRevID),
)
resp, err := http.Get(reqURL)
if nil != err {
t.Logf("[DEBUG] Response Error: %v", err)
return
}
t.Logf("[DEBUG] Request URL: %s", reqURL)
t.Logf("[DEBUG] Response Headers: %d %#v", resp.StatusCode, resp.Header)
b, err := ioutil.ReadAll(resp.Body)
if nil != err {
t.Logf("[DEBUG] Response Error: %v", err)
return
}
t.Logf("[DEBUG] Response Body: %v", string(b))
}

25
internal/api/testdata/deploy.sh vendored Normal file
View File

@ -0,0 +1,25 @@
#!/bin/bash
set -e
set -u
#set -x
echo "[${GIT_REPO_ID:-}#${GIT_REF_NAME:-}] Started at ${GIT_DEPLOY_TIMESTAMP:-}"
sleep ${GIT_DEPLOY_TEST_WAIT:-0.1}
echo "[${GIT_REPO_ID:-}#${GIT_REF_NAME:-}] Finished"
echo "Reporting to ${GIT_DEPLOY_CALLBACK_URL} ..."
curl -fsSL "${GIT_DEPLOY_CALLBACK_URL}" \
-H 'Content-Type: application/json' \
-d '
{ "report":
{ "name": "sleep test",
"status": "PASS",
"message": "a top level result group",
"results": [
{ "name": "sub test", "status": "PASS", "message": "a sub group", "detail": "logs or smth" }
]
}
}
'
echo "[${GIT_REPO_ID:-}#${GIT_REF_NAME:-}] Generated Report"

View File

@ -42,13 +42,11 @@ func debounce(hook *webhooks.Ref, runOpts *options.ServerConfig) {
} }
// this will not cause a mutual lock because it is async // this will not cause a mutual lock because it is async
debounceTimers[refID] = time.AfterFunc(runOpts.DebounceDelay, func() { debounceTimers[refID] = time.AfterFunc(runOpts.DebounceDelay, func() {
//fmt.Println("DEBUG [1] wait for jobs and timers")
jobsTimersMux.Lock() jobsTimersMux.Lock()
delete(debounceTimers, refID) delete(debounceTimers, refID)
jobsTimersMux.Unlock() jobsTimersMux.Unlock()
debounced <- hook debounced <- hook
//fmt.Println("DEBUG [1] release jobs and timers")
}) })
} }
@ -181,7 +179,7 @@ func run(curHook *webhooks.Ref, runOpts *options.ServerConfig) {
} }
// TODO jobs.New() // TODO jobs.New()
// Sets cmd.Stdout and cmd.Stderr // Sets cmd.Stdout and cmd.Stderr
f := setOutput(runOpts.LogDir, j) txtFile := setOutput(runOpts.LogDir, j)
if err := cmd.Start(); nil != err { if err := cmd.Start(); nil != err {
log.Printf("gitdeploy exec error: %s\n", err) log.Printf("gitdeploy exec error: %s\n", err)
@ -197,28 +195,32 @@ func run(curHook *webhooks.Ref, runOpts *options.ServerConfig) {
} else { } else {
log.Printf("gitdeploy job for %s#%s finished\n", hook.HTTPSURL, hook.RefName) log.Printf("gitdeploy job for %s#%s finished\n", hook.HTTPSURL, hook.RefName)
} }
if nil != f { if nil != txtFile {
_ = f.Close() _ = txtFile.Close()
} }
// TODO move to deathRow only?
updateExitStatus(j)
// Switch ID to the more specific RevID // Switch ID to the more specific RevID
j.ID = string(j.GitRef.GetRevID()) j.ID = string(j.GitRef.GetRevID())
// replace the text log with a json log // replace the text log with a json log
if f, err := getJobFile(runOpts.LogDir, j.GitRef, ".json"); nil != err { if jsonFile, err := getJobFile(runOpts.LogDir, j.GitRef, ".json"); nil != err {
// f.Name() should be the full path // jsonFile.Name() should be the full path
log.Printf("[warn] could not create log file '%s': %v", runOpts.LogDir, err) log.Printf("[warn] could not create log file '%s': %v", runOpts.LogDir, err)
} else { } else {
enc := json.NewEncoder(f) enc := json.NewEncoder(jsonFile)
enc.SetIndent("", " ") enc.SetIndent("", " ")
if err := enc.Encode(j); nil != err { if err := enc.Encode(j); nil != err {
log.Printf("[warn] could not encode json log '%s': %v", f.Name(), err) log.Printf("[warn] could not encode json log '%s': %v", jsonFile.Name(), err)
} else { } else {
logdir, logname, _ := getJobFilePath(runOpts.LogDir, j.GitRef, ".log") logdir, logname, _ := getJobFilePath(runOpts.LogDir, j.GitRef, ".log")
_ = os.Remove(filepath.Join(logdir, logname)) _ = os.Remove(filepath.Join(logdir, logname))
} }
_ = f.Close() _ = jsonFile.Close()
log.Printf("[DEBUG] wrote log to %s", f.Name())
} }
// TODO move to deathRow only?
j.Logs = []Log{} j.Logs = []Log{}
// this will completely clear the finished job // this will completely clear the finished job

View File

@ -19,6 +19,30 @@ import (
var initialized = false var initialized = false
var done = make(chan struct{}) var done = make(chan struct{})
// Promotions channel
var Promotions = make(chan Promotion)
// Pending is the map of backlog jobs
// map[webhooks.RefID]*webhooks.GitRef
var Pending sync.Map
// Actives is the map of jobs
// map[webhooks.RefID]*Job
var Actives sync.Map
// Recents are jobs that are dead, but recent
// map[webhooks.RevID]*Job
var Recents sync.Map
// deathRow is for jobs to be killed
var deathRow = make(chan webhooks.RefID)
// debounced is for jobs that are ready to run
var debounced = make(chan *webhooks.Ref)
// debacklog is for debouncing without saving in the backlog
var debacklog = make(chan *webhooks.Ref)
// Start starts the job loop, channels, and cleanup routines // Start starts the job loop, channels, and cleanup routines
func Start(runOpts *options.ServerConfig) { func Start(runOpts *options.ServerConfig) {
go Run(runOpts) go Run(runOpts)
@ -82,36 +106,12 @@ func Stop() {
initialized = false initialized = false
} }
// Promotions channel
var Promotions = make(chan Promotion)
// Promotion is a channel message // Promotion is a channel message
type Promotion struct { type Promotion struct {
PromoteTo string PromoteTo string
GitRef *webhooks.Ref GitRef *webhooks.Ref
} }
// Pending is the map of backlog jobs
// map[webhooks.RefID]*webhooks.GitRef
var Pending sync.Map
// Actives is the map of jobs
// map[webhooks.RefID]*Job
var Actives sync.Map
// Recents are jobs that are dead, but recent
// map[webhooks.RevID]*Job
var Recents sync.Map
// deathRow is for jobs to be killed
var deathRow = make(chan webhooks.RefID)
// debounced is for jobs that are ready to run
var debounced = make(chan *webhooks.Ref)
// debacklog is for debouncing without saving in the backlog
var debacklog = make(chan *webhooks.Ref)
// KillMsg describes which job to kill // KillMsg describes which job to kill
type KillMsg struct { type KillMsg struct {
JobID string `json:"job_id"` JobID string `json:"job_id"`
@ -130,14 +130,18 @@ type Job struct {
EndedAt time.Time `json:"ended_at,omitempty"` // empty when running EndedAt time.Time `json:"ended_at,omitempty"` // empty when running
// extra // extra
Logs []Log `json:"logs"` // exist when requested Logs []Log `json:"logs"` // exist when requested
Report Report `json:"report,omitempty"` // empty unless given Report *Report `json:"report,omitempty"` // empty unless given
Cmd *exec.Cmd `json:"-"` Cmd *exec.Cmd `json:"-"`
mux sync.Mutex `json:"-"` mux sync.Mutex `json:"-"`
} }
// Report should have many items // Report should have many items
type Report struct { type Report struct {
Results []string `json:"results"` Name string `json:"name"`
Status string `json:"status,omitempty"`
Message string `json:"message,omitempty"`
Detail string `json:"detail,omitempty"`
Results []Report `json:"results,omitempty"`
} }
// All returns all jobs, including active, recent, and (TODO) historical // All returns all jobs, including active, recent, and (TODO) historical
@ -195,6 +199,26 @@ func All() []*Job {
return jobsCopy return jobsCopy
} }
// SetReport will update jobs' logs
func SetReport(urlRefID webhooks.URLSafeRefID, report *Report) error {
b, err := base64.RawURLEncoding.DecodeString(string(urlRefID))
if nil != err {
return err
}
refID := webhooks.RefID(b)
value, ok := Actives.Load(refID)
if !ok {
return errors.New("active job not found by " + string(refID))
}
job := value.(*Job)
job.Report = report
Actives.Store(refID, job)
return nil
}
// Remove will put a job on death row // Remove will put a job on death row
func Remove(gitID webhooks.URLSafeRefID /*, nokill bool*/) { func Remove(gitID webhooks.URLSafeRefID /*, nokill bool*/) {
activeID, err := activeID, err :=
@ -213,7 +237,7 @@ func getEnvs(addr, activeID string, repoList string, hook *webhooks.Ref) []strin
envs := []string{ envs := []string{
"GIT_DEPLOY_JOB_ID=" + activeID, "GIT_DEPLOY_JOB_ID=" + activeID,
"GIT_DEPLOY_TIMESTAMP=" + hook.Timestamp.Format(time.RFC3339), "GIT_DEPLOY_TIMESTAMP=" + hook.Timestamp.Format(time.RFC3339),
"GIT_DEPLOY_CALLBACK_URL=" + "http://localhost:" + port + "/api/jobs/" + activeID, "GIT_DEPLOY_CALLBACK_URL=" + "http://localhost:" + port + "/api/local/jobs/" + string(hook.GetURLSafeRefID()),
"GIT_REF_NAME=" + hook.RefName, "GIT_REF_NAME=" + hook.RefName,
"GIT_REF_TYPE=" + hook.RefType, "GIT_REF_TYPE=" + hook.RefType,
"GIT_REPO_ID=" + hook.RepoID, "GIT_REPO_ID=" + hook.RepoID,
@ -321,23 +345,30 @@ func remove(activeID webhooks.RefID /*, nokill bool*/) {
value, ok := Actives.Load(activeID) value, ok := Actives.Load(activeID)
if !ok { if !ok {
log.Printf("[warn] could not find job to kill by RefID %s", activeID)
return return
} }
job := value.(*Job) job := value.(*Job)
Actives.Delete(activeID) Actives.Delete(activeID)
// JSON should have been written to disk by this point
job.Logs = []Log{}
// transition to RevID for non-active, non-pending jobs // transition to RevID for non-active, non-pending jobs
job.ID = string(job.GitRef.GetRevID()) job.ID = string(job.GitRef.GetRevID())
Recents.Store(job.GitRef.GetRevID(), job) Recents.Store(job.GitRef.GetRevID(), job)
updateExitStatus(job)
// JSON should have been written to disk by this point
job.Logs = []Log{}
}
func updateExitStatus(job *Job) {
if nil == job.Cmd.ProcessState { if nil == job.Cmd.ProcessState {
// is not yet finished // is not yet finished
if nil != job.Cmd.Process { if nil != job.Cmd.Process {
// but definitely was started // but definitely was started
err := job.Cmd.Process.Kill() if err := job.Cmd.Process.Kill(); nil != err {
log.Printf("error killing job:\n%v", err) log.Printf("error killing job:\n%v", err)
}
} }
} }
if nil != job.Cmd.ProcessState { if nil != job.Cmd.ProcessState {

View File

@ -90,7 +90,6 @@ func TestDebounce(t *testing.T) {
Repo: "repo", Repo: "repo",
}) })
// TODO make debounce time configurable
t.Log("sleep so job can debounce and start") t.Log("sleep so job can debounce and start")
time.Sleep(debounceDelay) time.Sleep(debounceDelay)
@ -227,7 +226,6 @@ func TestRecents(t *testing.T) {
} }
Debounce(hook) Debounce(hook)
// TODO make debounce time configurable
t.Log("sleep so job can debounce and start") t.Log("sleep so job can debounce and start")
time.Sleep(debounceDelay) time.Sleep(debounceDelay)
time.Sleep(jobDelay) time.Sleep(jobDelay)
@ -254,6 +252,14 @@ func TestRecents(t *testing.T) {
return return
} }
if nil == j.ExitCode || 0 != *j.ExitCode {
t.Errorf("should zero exit status")
t.Fail()
return
}
t.Logf("[DEBUG] Report:\n%#v", j.Report)
t.Logf("Logs:\n%v", err) t.Logf("Logs:\n%v", err)
//Stop() //Stop()

View File

@ -85,7 +85,7 @@ func WalkLogs(runOpts *options.ServerConfig) ([]*Job, error) {
// ExpiredLogAge can be 0 for testing, // ExpiredLogAge can be 0 for testing,
// even when StaleLogAge is > 0 // even when StaleLogAge is > 0
if age >= runOpts.ExpiredLogAge { if age >= runOpts.ExpiredLogAge {
log.Printf("[DEBUG] remove log file: %s", logpath) log.Printf("[info] remove log file: %s", logpath)
os.Remove(logpath) os.Remove(logpath)
} }
@ -95,6 +95,8 @@ func WalkLogs(runOpts *options.ServerConfig) ([]*Job, error) {
return oldJobs, err return oldJobs, err
} }
//func GetReport(runOpts *options.ServerConfig, safeID webhooks.URLSafeGitID) (*Job, error) {}
// LoadLogs will log logs for a job // LoadLogs will log logs for a job
func LoadLogs(runOpts *options.ServerConfig, safeID webhooks.URLSafeGitID) (*Job, error) { func LoadLogs(runOpts *options.ServerConfig, safeID webhooks.URLSafeGitID) (*Job, error) {
b, err := base64.RawURLEncoding.DecodeString(string(safeID)) b, err := base64.RawURLEncoding.DecodeString(string(safeID))
@ -127,7 +129,6 @@ func LoadLogs(runOpts *options.ServerConfig, safeID webhooks.URLSafeGitID) (*Job
dec := json.NewDecoder(f) dec := json.NewDecoder(f)
j := &Job{} j := &Job{}
if err := dec.Decode(j); nil != err { if err := dec.Decode(j); nil != err {
log.Printf("[DEBUG] decode error: %v", err)
return nil, errors.New("couldn't read log file") return nil, errors.New("couldn't read log file")
} }
j.ID = string(gitID) j.ID = string(gitID)

View File

@ -1,13 +1,8 @@
#!/bin/bash #!/bin/bash
set -e set -e
set -u set -u
set -x #set -x
echo "[${GIT_REPO_ID:-}#${GIT_REF_NAME:-}] Started at ${GIT_DEPLOY_TIMESTAMP:-}" echo "[${GIT_REPO_ID:-}#${GIT_REF_NAME:-}] Started at ${GIT_DEPLOY_TIMESTAMP:-}"
sleep ${GIT_DEPLOY_TEST_WAIT:-0.1} sleep ${GIT_DEPLOY_TEST_WAIT:-0.1}
echo "[${GIT_REPO_ID:-}#${GIT_REF_NAME:-}] Finished" echo "[${GIT_REPO_ID:-}#${GIT_REF_NAME:-}] Finished"
# TODO start/end? duration?
#curl -X POST "${GIT_DEPLOY_CALLBACK_URL}" -d '
# { "report": [ { "name":"sleep", "code":"PASS", "message":"", "details":"" } ] }
#'