set and get job reports
This commit is contained in:
parent
70291c3bce
commit
2873457cf1
|
@ -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'))
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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"
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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":"" } ] }
|
||||
#'
|
||||
|
|
Loading…
Reference in New Issue