nano-run/server/ui/router.go
Alexander Baryshnikov 6fb7d88306 add ui for cron
2020-11-07 23:04:40 +08:00

374 lines
8.6 KiB
Go

package ui
import (
"bytes"
"io"
"log"
"net/http"
"path/filepath"
"sort"
"strconv"
"time"
"github.com/Masterminds/sprig"
"github.com/gin-gonic/gin"
"nano-run/server"
"nano-run/services/meta"
"nano-run/worker"
)
func Expose(units []server.Unit, workers []*worker.Worker, cronEntries []*server.CronEntry, uiDir string, auth Authorization) http.Handler {
router := gin.New()
router.SetFuncMap(sprig.HtmlFuncMap())
router.LoadHTMLGlob(filepath.Join(uiDir, "*.html"))
Attach(router, units, workers, cronEntries, auth)
return router
}
func Attach(router gin.IRouter, units []server.Unit, workers []*worker.Worker, cronEntries []*server.CronEntry, auth Authorization) {
ui := &uiRouter{
units: make(map[string]unitInfo),
}
var offset int
for i := range units {
u := units[i]
w := workers[i]
var ce []*server.CronEntry
var last int
for last = offset; last < len(cronEntries); last++ {
if cronEntries[last].Worker != w {
break
}
}
ce = cronEntries[offset:last]
offset = last
ui.units[u.Name()] = unitInfo{
Unit: u,
Worker: w,
CronEntries: ce,
}
}
sessions := &memorySessions{}
if r, ok := router.(*gin.RouterGroup); ok {
router.Use(func(gctx *gin.Context) {
gctx.Set("base", r.BasePath())
gctx.Next()
})
}
router.GET("", func(gctx *gin.Context) {
gctx.Redirect(http.StatusTemporaryRedirect, "unit/")
})
auth.attach(router.Group("/auth"), "login.html", sessions)
guard := auth.restrict(func(gctx *gin.Context) string {
b := base(gctx)
return b.Rel("/auth/")
}, sessions)
unitsRoutes := router.Group("/unit/").Use(guard)
unitsRoutes.GET("/", ui.listUnits)
unitsRoutes.GET("/:name/", ui.unitInfo)
unitsRoutes.POST("/:name/", ui.unitInvoke)
unitsRoutes.GET("/:name/history", ui.unitHistory)
unitsRoutes.GET("/:name/request/:request/", ui.unitRequestInfo)
unitsRoutes.POST("/:name/request/:request/", ui.unitRequestRetry)
unitsRoutes.GET("/:name/request/:request/payload", ui.unitRequestPayload)
unitsRoutes.GET("/:name/request/:request/attempt/:attempt/", ui.unitRequestAttemptInfo)
unitsRoutes.GET("/:name/request/:request/attempt/:attempt/result", ui.unitRequestAttemptResult)
unitsRoutes.GET("/:name/cron/:index", ui.unitCronInfo)
cronRoutes := router.Group("/cron/").Use(guard)
cronRoutes.GET("/", ui.listCron)
}
type uiRouter struct {
units map[string]unitInfo
}
func (ui *uiRouter) unitRequestAttemptResult(gctx *gin.Context) {
name := gctx.Param("name")
info, ok := ui.units[name]
if !ok {
gctx.AbortWithStatus(http.StatusNotFound)
return
}
attemptID := gctx.Param("attempt")
f, err := info.Worker.Blobs().Get(attemptID)
if err != nil {
_ = gctx.AbortWithError(http.StatusNotFound, err)
return
}
defer f.Close()
gctx.Status(http.StatusOK)
_, _ = io.Copy(gctx.Writer, f)
}
type attemptInfo struct {
requestInfo
AttemptID string
Attempt meta.Attempt
}
func (ui *uiRouter) unitRequestAttemptInfo(gctx *gin.Context) {
name := gctx.Param("name")
info, ok := ui.units[name]
if !ok {
gctx.AbortWithStatus(http.StatusNotFound)
return
}
requestID := gctx.Param("request")
request, err := info.Worker.Meta().Get(requestID)
if err != nil {
_ = gctx.AbortWithError(http.StatusNotFound, err)
return
}
attemptID := gctx.Param("attempt")
var found bool
var attempt meta.Attempt
for _, atp := range request.Attempts {
if atp.ID == attemptID {
attempt = atp
found = true
break
}
}
if !found {
gctx.AbortWithStatus(http.StatusNotFound)
return
}
gctx.HTML(http.StatusOK, "unit-request-attempt-info.html", attemptInfo{
requestInfo: requestInfo{
baseResponse: base(gctx),
unitInfo: info,
Request: request,
RequestID: requestID,
},
AttemptID: attemptID,
Attempt: attempt,
})
}
type requestInfo struct {
unitInfo
baseResponse
Request *meta.Request
RequestID string
}
func (ui *uiRouter) unitRequestInfo(gctx *gin.Context) {
name := gctx.Param("name")
info, ok := ui.units[name]
if !ok {
gctx.AbortWithStatus(http.StatusNotFound)
return
}
requestID := gctx.Param("request")
request, err := info.Worker.Meta().Get(requestID)
if err != nil {
_ = gctx.AbortWithError(http.StatusNotFound, err)
return
}
gctx.HTML(http.StatusOK, "unit-request-info.html", requestInfo{
baseResponse: base(gctx),
unitInfo: info,
Request: request,
RequestID: requestID,
})
}
func (ui *uiRouter) unitRequestRetry(gctx *gin.Context) {
name := gctx.Param("name")
item, ok := ui.units[name]
if !ok {
gctx.AbortWithStatus(http.StatusNotFound)
return
}
requestID := gctx.Param("request")
id, err := item.Worker.Retry(gctx.Request.Context(), requestID)
if err != nil {
log.Println("failed to retry:", err)
_ = gctx.AbortWithError(http.StatusInternalServerError, err)
return
}
gctx.Redirect(http.StatusSeeOther, base(gctx).Rel("/unit", name, "request", id))
}
func (ui *uiRouter) unitRequestPayload(gctx *gin.Context) {
name := gctx.Param("name")
item, ok := ui.units[name]
if !ok {
gctx.AbortWithStatus(http.StatusNotFound)
return
}
requestID := gctx.Param("request")
info, err := item.Worker.Meta().Get(requestID)
if !ok {
_ = gctx.AbortWithError(http.StatusNotFound, err)
return
}
gctx.Header("Last-Modified", info.CreatedAt.Format(time.RFC850))
f, err := item.Worker.Blobs().Get(requestID)
if err != nil {
log.Println("failed to get data:", err)
_ = gctx.AbortWithError(http.StatusInternalServerError, err)
return
}
defer f.Close()
gctx.Header("X-Method", info.Method)
gctx.Header("X-Request-Uri", info.URI)
for k, v := range info.Headers {
gctx.Request.Header[k] = v
}
gctx.Status(http.StatusOK)
_, _ = io.Copy(gctx.Writer, f)
}
func (ui *uiRouter) unitInfo(gctx *gin.Context) {
type viewUnit struct {
unitInfo
baseResponse
}
name := gctx.Param("name")
info, ok := ui.units[name]
if !ok {
gctx.AbortWithStatus(http.StatusNotFound)
return
}
gctx.HTML(http.StatusOK, "unit-info.html", viewUnit{
unitInfo: info,
baseResponse: base(gctx),
})
}
func (ui *uiRouter) unitInvoke(gctx *gin.Context) {
name := gctx.Param("name")
info, ok := ui.units[name]
if !ok {
gctx.AbortWithStatus(http.StatusNotFound)
return
}
data := gctx.PostForm("body")
req, err := http.NewRequestWithContext(gctx.Request.Context(), http.MethodPost, "/", bytes.NewBufferString(data))
if err != nil {
_ = gctx.AbortWithError(http.StatusInternalServerError, err)
return
}
id, err := info.Worker.Enqueue(req)
if err != nil {
_ = gctx.AbortWithError(http.StatusInternalServerError, err)
return
}
gctx.Redirect(http.StatusSeeOther, base(gctx).Rel("/unit", name, "request", id))
}
func (ui *uiRouter) listUnits(gctx *gin.Context) {
var reply struct {
baseResponse
Units []server.Unit
}
var units = make([]server.Unit, 0, len(ui.units))
for _, info := range ui.units {
units = append(units, info.Unit)
}
reply.baseResponse = base(gctx)
reply.Units = units
gctx.HTML(http.StatusOK, "units-list.html", reply)
}
func (ui *uiRouter) listCron(gctx *gin.Context) {
type uiEntry struct {
Index int
Entry *server.CronEntry
}
var reply struct {
baseResponse
Entries []uiEntry
}
var specs = make([]uiEntry, 0, len(ui.units))
for _, info := range ui.units {
for i, spec := range info.CronEntries {
specs = append(specs, uiEntry{
Index: i,
Entry: spec,
})
}
}
sort.Slice(specs, func(i, j int) bool {
if specs[i].Entry.Config.Name() < specs[j].Entry.Config.Name() {
return true
} else if specs[i].Entry.Config.Name() == specs[j].Entry.Config.Name() && specs[i].Index < specs[j].Index {
return true
}
return false
})
reply.baseResponse = base(gctx)
reply.Entries = specs
gctx.HTML(http.StatusOK, "cron-list.html", reply)
}
func (ui *uiRouter) unitHistory(gctx *gin.Context) {
type viewUnit struct {
unitInfo
baseResponse
}
name := gctx.Param("name")
info, ok := ui.units[name]
if !ok {
gctx.AbortWithStatus(http.StatusNotFound)
return
}
gctx.HTML(http.StatusOK, "unit-history.html", viewUnit{
unitInfo: info,
baseResponse: base(gctx),
})
}
func (ui *uiRouter) unitCronInfo(gctx *gin.Context) {
type viewUnit struct {
unitInfo
baseResponse
Cron *server.CronEntry
Label string
}
name := gctx.Param("name")
info, ok := ui.units[name]
if !ok {
gctx.AbortWithStatus(http.StatusNotFound)
return
}
strIndex := gctx.Param("index")
index, err := strconv.Atoi(strIndex)
if err != nil || index < 0 || index >= len(info.CronEntries) {
gctx.AbortWithStatus(http.StatusNotFound)
return
}
entry := info.CronEntries[index]
gctx.HTML(http.StatusOK, "unit-cron-info.html", viewUnit{
unitInfo: info,
baseResponse: base(gctx),
Cron: entry,
Label: entry.Spec.Label(strconv.Itoa(index)),
})
}