gitdeploy/internal/api/api.go

239 lines
6.1 KiB
Go

package api
import (
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"git.rootprojects.org/root/gitdeploy/internal/jobs"
"git.rootprojects.org/root/gitdeploy/internal/log"
"git.rootprojects.org/root/gitdeploy/internal/options"
"git.rootprojects.org/root/gitdeploy/internal/webhooks"
"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"`
}
// ReposResponse is the successful response to /api/repos
type ReposResponse struct {
Success bool `json:"success"`
Repos []Repo `json:"repos"`
}
// Repo is one of the elements of /api/repos
type Repo struct {
ID string `json:"id"`
CloneURL string `json:"clone_url"`
Promotions []string `json:"_promotions"`
}
// 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'))
})
})
}