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