diff --git a/internal/api/api.go b/internal/api/api.go index 111d809..faa26b1 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -7,7 +7,6 @@ import ( "os" "path/filepath" "strings" - "time" "git.rootprojects.org/root/gitdeploy/internal/jobs" "git.rootprojects.org/root/gitdeploy/internal/log" @@ -17,14 +16,248 @@ import ( "github.com/go-chi/chi" ) -// HookResponse is a GitRef but with a little extra as HTTP response -type HookResponse struct { - RepoID string `json:"repo_id"` - CreatedAt time.Time `json:"created_at"` - EndedAt time.Time `json:"ended_at"` - ExitCode *int `json:"exit_code,omitempty"` - Log string `json:"log"` - LogURL string `json:"log_url"` +type HTTPError struct { + Success bool `json:"success"` + Code string `json:"code,omitempty"` + Message string `json:"message,omitempty"` + Detail string `json:"detail,omitempty"` +} + +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 @@ -39,200 +272,3 @@ type Repo struct { CloneURL string `json:"clone_url"` 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')) - - }) - }) - -} diff --git a/internal/api/api_test.go b/internal/api/api_test.go new file mode 100644 index 0000000..b8a40d1 --- /dev/null +++ b/internal/api/api_test.go @@ -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)) +} diff --git a/internal/api/testdata/deploy.sh b/internal/api/testdata/deploy.sh new file mode 100644 index 0000000..7600d25 --- /dev/null +++ b/internal/api/testdata/deploy.sh @@ -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" diff --git a/internal/jobs/backlog.go b/internal/jobs/backlog.go index 946ec05..d38f7d8 100644 --- a/internal/jobs/backlog.go +++ b/internal/jobs/backlog.go @@ -42,13 +42,11 @@ func debounce(hook *webhooks.Ref, runOpts *options.ServerConfig) { } // this will not cause a mutual lock because it is async debounceTimers[refID] = time.AfterFunc(runOpts.DebounceDelay, func() { - //fmt.Println("DEBUG [1] wait for jobs and timers") jobsTimersMux.Lock() delete(debounceTimers, refID) jobsTimersMux.Unlock() 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() // Sets cmd.Stdout and cmd.Stderr - f := setOutput(runOpts.LogDir, j) + txtFile := setOutput(runOpts.LogDir, j) if err := cmd.Start(); nil != err { log.Printf("gitdeploy exec error: %s\n", err) @@ -197,28 +195,32 @@ func run(curHook *webhooks.Ref, runOpts *options.ServerConfig) { } else { log.Printf("gitdeploy job for %s#%s finished\n", hook.HTTPSURL, hook.RefName) } - if nil != f { - _ = f.Close() + if nil != txtFile { + _ = txtFile.Close() } + // TODO move to deathRow only? + updateExitStatus(j) + // Switch ID to the more specific RevID j.ID = string(j.GitRef.GetRevID()) // replace the text log with a json log - if f, err := getJobFile(runOpts.LogDir, j.GitRef, ".json"); nil != err { - // f.Name() should be the full path + if jsonFile, err := getJobFile(runOpts.LogDir, j.GitRef, ".json"); nil != err { + // jsonFile.Name() should be the full path log.Printf("[warn] could not create log file '%s': %v", runOpts.LogDir, err) } else { - enc := json.NewEncoder(f) + enc := json.NewEncoder(jsonFile) enc.SetIndent("", " ") 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 { logdir, logname, _ := getJobFilePath(runOpts.LogDir, j.GitRef, ".log") _ = os.Remove(filepath.Join(logdir, logname)) } - _ = f.Close() - log.Printf("[DEBUG] wrote log to %s", f.Name()) + _ = jsonFile.Close() } + + // TODO move to deathRow only? j.Logs = []Log{} // this will completely clear the finished job diff --git a/internal/jobs/job.go b/internal/jobs/job.go index 9858537..28266a0 100644 --- a/internal/jobs/job.go +++ b/internal/jobs/job.go @@ -19,6 +19,30 @@ import ( var initialized = false 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 func Start(runOpts *options.ServerConfig) { go Run(runOpts) @@ -82,36 +106,12 @@ func Stop() { initialized = false } -// Promotions channel -var Promotions = make(chan Promotion) - // Promotion is a channel message type Promotion struct { PromoteTo string 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 type KillMsg struct { JobID string `json:"job_id"` @@ -130,14 +130,18 @@ type Job struct { EndedAt time.Time `json:"ended_at,omitempty"` // empty when running // extra 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:"-"` mux sync.Mutex `json:"-"` } // Report should have many items 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 @@ -195,6 +199,26 @@ func All() []*Job { 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 func Remove(gitID webhooks.URLSafeRefID /*, nokill bool*/) { activeID, err := @@ -213,7 +237,7 @@ func getEnvs(addr, activeID string, repoList string, hook *webhooks.Ref) []strin envs := []string{ "GIT_DEPLOY_JOB_ID=" + activeID, "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_TYPE=" + hook.RefType, "GIT_REPO_ID=" + hook.RepoID, @@ -321,23 +345,30 @@ func remove(activeID webhooks.RefID /*, nokill bool*/) { value, ok := Actives.Load(activeID) if !ok { + log.Printf("[warn] could not find job to kill by RefID %s", activeID) return } job := value.(*Job) 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 job.ID = string(job.GitRef.GetRevID()) 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 { // is not yet finished if nil != job.Cmd.Process { // but definitely was started - err := job.Cmd.Process.Kill() - log.Printf("error killing job:\n%v", err) + if err := job.Cmd.Process.Kill(); nil != err { + log.Printf("error killing job:\n%v", err) + } } } if nil != job.Cmd.ProcessState { diff --git a/internal/jobs/job_test.go b/internal/jobs/job_test.go index 6980b01..617d513 100644 --- a/internal/jobs/job_test.go +++ b/internal/jobs/job_test.go @@ -90,7 +90,6 @@ func TestDebounce(t *testing.T) { Repo: "repo", }) - // TODO make debounce time configurable t.Log("sleep so job can debounce and start") time.Sleep(debounceDelay) @@ -227,7 +226,6 @@ func TestRecents(t *testing.T) { } Debounce(hook) - // TODO make debounce time configurable t.Log("sleep so job can debounce and start") time.Sleep(debounceDelay) time.Sleep(jobDelay) @@ -254,6 +252,14 @@ func TestRecents(t *testing.T) { 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) //Stop() diff --git a/internal/jobs/logs.go b/internal/jobs/logs.go index 0d756ef..86e4f5e 100644 --- a/internal/jobs/logs.go +++ b/internal/jobs/logs.go @@ -85,7 +85,7 @@ func WalkLogs(runOpts *options.ServerConfig) ([]*Job, error) { // ExpiredLogAge can be 0 for testing, // even when StaleLogAge is > 0 if age >= runOpts.ExpiredLogAge { - log.Printf("[DEBUG] remove log file: %s", logpath) + log.Printf("[info] remove log file: %s", logpath) os.Remove(logpath) } @@ -95,6 +95,8 @@ func WalkLogs(runOpts *options.ServerConfig) ([]*Job, error) { return oldJobs, err } +//func GetReport(runOpts *options.ServerConfig, safeID webhooks.URLSafeGitID) (*Job, error) {} + // LoadLogs will log logs for a job func LoadLogs(runOpts *options.ServerConfig, safeID webhooks.URLSafeGitID) (*Job, error) { b, err := base64.RawURLEncoding.DecodeString(string(safeID)) @@ -127,7 +129,6 @@ func LoadLogs(runOpts *options.ServerConfig, safeID webhooks.URLSafeGitID) (*Job dec := json.NewDecoder(f) j := &Job{} if err := dec.Decode(j); nil != err { - log.Printf("[DEBUG] decode error: %v", err) return nil, errors.New("couldn't read log file") } j.ID = string(gitID) diff --git a/internal/jobs/testdata/deploy.sh b/internal/jobs/testdata/deploy.sh index 36cc51c..6ae22b5 100644 --- a/internal/jobs/testdata/deploy.sh +++ b/internal/jobs/testdata/deploy.sh @@ -1,13 +1,8 @@ #!/bin/bash set -e set -u -set -x +#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" - -# TODO start/end? duration? -#curl -X POST "${GIT_DEPLOY_CALLBACK_URL}" -d ' -# { "report": [ { "name":"sleep", "code":"PASS", "message":"", "details":"" } ] } -#'