add ui stub

This commit is contained in:
Alexander Baryshnikov 2020-09-28 21:46:37 +08:00
parent 554083128b
commit dac1910200
15 changed files with 466 additions and 296 deletions

7
go.mod
View File

@ -3,11 +3,18 @@ module nano-run
go 1.14
require (
github.com/Masterminds/goutils v1.1.0 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/dgraph-io/badger v1.6.1
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-gonic/gin v1.6.3
github.com/google/uuid v1.1.2
github.com/gorilla/mux v1.8.0
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/jessevdk/go-flags v1.4.1-0.20200711081900-c17162fe8fd7
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/stretchr/testify v1.4.0
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
gopkg.in/yaml.v2 v2.3.0

48
go.sum
View File

@ -1,6 +1,12 @@
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9 h1:HD8gA2tkByhMAwYaFAX9w2l7vxvBQ5NMoxDrkhqhtn4=
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
@ -24,24 +30,56 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.1-0.20200711081900-c17162fe8fd7 h1:Ug59miTxVKVg5Oi2S5uHlKOIV5jBx4Hb2u0jIxxDaSs=
github.com/jessevdk/go-flags v1.4.1-0.20200711081900-c17162fe8fd7/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -59,9 +97,14 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
@ -76,11 +119,16 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

209
server/api/adapter.go Normal file
View File

@ -0,0 +1,209 @@
package api
import (
"io"
"log"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"nano-run/services/meta"
"nano-run/worker"
)
// Expose worker as HTTP handler:
// POST / - post task async, returns 303 See Other and location.
// GET /:id - get task info.
// POST /:id - retry task, redirects to /:id
// GET /:id/completed - redirect to completed attempt (or 404 if task not yet complete)
// GET /:id/attempt/:atid - get attempt result (as-is).
// GET /:id/request - replay request (as-is).
func Expose(router gin.IRouter, wrk *worker.Worker) {
//TODO: wait
router.POST("/", func(gctx *gin.Context) {
id, err := wrk.Enqueue(gctx.Request)
if err != nil {
log.Println("failed to enqueue:", err)
gctx.AbortWithError(http.StatusInternalServerError, err)
return
}
gctx.Header("X-Correlation-Id", id)
gctx.Redirect(http.StatusSeeOther, id)
})
handler := &workerHandler{wrk: wrk}
taskRoutes := router.Group("/:id")
taskRoutes.GET("", handler.getTask)
taskRoutes.POST("", handler.retry)
taskRoutes.DELETE("", handler.completeRequest)
taskRoutes.GET("/completed", handler.getComplete)
// get attempt result as-is.
taskRoutes.GET("/attempt/:attemptId", handler.getAttempt)
// get recorded request.
taskRoutes.GET("/request", handler.getRequest)
}
type workerHandler struct {
wrk *worker.Worker
}
// get request meta information.
func (wh *workerHandler) getTask(gctx *gin.Context) {
requestID := gctx.Param("id")
info, err := wh.wrk.Meta().Get(requestID)
if err != nil {
log.Println("failed access request", requestID, ":", err)
gctx.AbortWithStatus(http.StatusNotFound)
return
}
gctx.Header("X-Correlation-Id", requestID)
gctx.Header("Content-Version", strconv.Itoa(len(info.Attempts)))
// modification time
setLastModify(gctx, info)
gctx.Header("Age", strconv.FormatInt(int64(time.Since(info.CreatedAt)/time.Second), 10))
if info.Complete {
gctx.Header("X-Status", "complete")
} else {
gctx.Header("X-Status", "processing")
}
if len(info.Attempts) > 0 {
gctx.Header("X-Last-Attempt", info.Attempts[len(info.Attempts)-1].ID)
gctx.Header("X-Last-Attempt-At", info.Attempts[len(info.Attempts)-1].CreatedAt.Format(time.RFC850))
}
if info.Complete {
lastAttempt := info.Attempts[len(info.Attempts)-1]
gctx.Request.URL.Path += "/attempt/" + lastAttempt.ID
gctx.Header("Location", gctx.Request.URL.String())
}
gctx.IndentedJSON(http.StatusOK, info)
}
func (wh *workerHandler) retry(gctx *gin.Context) {
requestID := gctx.Param("id")
id, err := wh.wrk.Retry(gctx.Request.Context(), requestID)
if err != nil {
log.Println("failed to retry:", err)
gctx.AbortWithError(http.StatusInternalServerError, err)
return
}
gctx.Header("X-Correlation-Id", id)
gctx.Redirect(http.StatusSeeOther, id)
}
func (wh *workerHandler) completeRequest(gctx *gin.Context) {
requestID := gctx.Param("id")
info, err := wh.wrk.Meta().Get(requestID)
if err != nil {
log.Println("failed access request", requestID, ":", err)
gctx.AbortWithStatus(http.StatusNotFound)
return
}
if !info.Complete {
err = wh.wrk.Meta().Complete(requestID)
if err != nil {
log.Println("failed to mark request as complete:", err)
gctx.AbortWithError(http.StatusInternalServerError, err)
return
}
}
gctx.AbortWithStatus(http.StatusNoContent)
}
func (wh *workerHandler) getComplete(gctx *gin.Context) {
requestID := gctx.Param("id")
info, err := wh.wrk.Meta().Get(requestID)
if err != nil {
log.Println("failed access request", requestID, ":", err)
gctx.AbortWithStatus(http.StatusNotFound)
return
}
if !info.Complete {
gctx.AbortWithStatus(http.StatusTooEarly)
return
}
lastAttempt := info.Attempts[len(info.Attempts)-1]
gctx.Redirect(http.StatusMovedPermanently, "attempt/"+lastAttempt.ID)
}
func (wh *workerHandler) getAttempt(gctx *gin.Context) {
requestID := gctx.Param("id")
attemptID := gctx.Param("attemptId")
info, err := wh.wrk.Meta().Get(requestID)
if err != nil {
log.Println("failed access request", requestID, ":", err)
gctx.AbortWithStatus(http.StatusNotFound)
return
}
var attempt meta.Attempt
var found bool
for _, atp := range info.Attempts {
if atp.ID == attemptID {
found = true
attempt = atp
break
}
}
if !found {
gctx.AbortWithStatus(http.StatusNotFound)
return
}
body, err := wh.wrk.Blobs().Get(attempt.ID)
if err != nil {
log.Println("failed to get body:", err)
gctx.AbortWithError(http.StatusInternalServerError, err)
return
}
defer body.Close()
gctx.Header("Last-Modified", attempt.CreatedAt.Format(time.RFC850))
if info.Complete {
gctx.Header("X-Status", "complete")
} else {
gctx.Header("X-Status", "processing")
}
gctx.Header("X-Processed", "true")
for k, v := range attempt.Headers {
gctx.Request.Header[k] = v
}
_, _ = io.Copy(gctx.Writer, body)
}
func (wh *workerHandler) getRequest(gctx *gin.Context) {
requestID := gctx.Param("id")
info, err := wh.wrk.Meta().Get(requestID)
if err != nil {
log.Println("failed access request", requestID, ":", err)
gctx.AbortWithStatus(http.StatusNotFound)
return
}
gctx.Header("Last-Modified", info.CreatedAt.Format(time.RFC850))
f, err := wh.wrk.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 setLastModify(gctx *gin.Context, info *meta.Request) {
if info.Complete {
gctx.Header("Last-Modified", info.CompleteAt.Format(time.RFC850))
} else if len(info.Attempts) > 0 {
gctx.Header("Last-Modified", info.Attempts[len(info.Attempts)-1].CreatedAt.Format(time.RFC850))
} else {
gctx.Header("Last-Modified", info.CreatedAt.Format(time.RFC850))
}
}

View File

@ -1,6 +1,6 @@
// +build !linux
package internal
package api
import "os/exec"

View File

@ -1,4 +1,4 @@
package internal
package api
import (
"os/exec"

View File

@ -5,10 +5,11 @@ import (
"net/http"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
)
func (cfg Unit) enableAuthorization() func(handler http.Handler) http.Handler {
func (cfg Unit) enableAuthorization() func(gctx *gin.Context) {
var handlers []AuthHandlerFunc
if cfg.Authorization.JWT.Enable {
handlers = append(handlers, cfg.Authorization.JWT.Create())
@ -22,26 +23,19 @@ func (cfg Unit) enableAuthorization() func(handler http.Handler) http.Handler {
if cfg.Authorization.Basic.Enable {
handlers = append(handlers, cfg.Authorization.Basic.Create())
}
if len(handlers) == 0 {
return func(handler http.Handler) http.Handler {
return handler
return func(gctx *gin.Context) {
var authorized = len(handlers) == 0
for _, h := range handlers {
if h(gctx.Request) {
authorized = true
break
}
}
}
return func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
var authorized bool
for _, h := range handlers {
if h(request) {
authorized = true
break
}
}
if !authorized {
writer.WriteHeader(http.StatusForbidden)
return
}
handler.ServeHTTP(writer, request)
})
if !authorized {
gctx.AbortWithStatus(http.StatusForbidden)
return
}
gctx.Next()
}
}

View File

@ -1,232 +0,0 @@
package internal
import (
"encoding/json"
"io"
"log"
"net/http"
"strconv"
"time"
"github.com/gorilla/mux"
"nano-run/services/meta"
"nano-run/worker"
)
// Expose worker as HTTP handler:
// POST / - post task async, returns 303 See Other and location.
// GET /:id - get task info.
// POST /:id - retry task, redirects to /:id
// GET /:id/completed - redirect to completed attempt (or 404 if task not yet complete)
// GET /:id/attempt/:atid - get attempt result (as-is).
// GET /:id/request - replay request (as-is).
func Expose(router *mux.Router, wrk *worker.Worker) {
//TODO: wait
router.Path("/").Methods("POST").HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
id, err := wrk.Enqueue(request)
if err != nil {
log.Println("failed to enqueue:", err)
http.Error(writer, "failed to enqueue", http.StatusInternalServerError)
return
}
writer.Header().Set("X-Correlation-Id", id)
http.Redirect(writer, request, id, http.StatusSeeOther)
})
router.Path("/{id}").Methods("GET").HandlerFunc(getTask(wrk))
router.Path("/{id}").Methods("POST").HandlerFunc(retry(wrk))
router.Path("/{id}").Methods("DELETE").HandlerFunc(completeRequest(wrk))
router.Path("/{id}/completed").Methods("GET").HandlerFunc(getComplete(wrk))
// get attempt result as-is.
router.Path("/{id}/attempt/{attemptId}").Methods("GET").HandlerFunc(getAttempt(wrk))
// get recorded request.
router.Path("/{id}/request").Methods("GET").HandlerFunc(getRequest(wrk))
}
// get request meta information.
func getTask(wrk *worker.Worker) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
params := mux.Vars(request)
requestID := params["id"]
info, err := wrk.Meta().Get(requestID)
if err != nil {
log.Println("failed access request", requestID, ":", err)
http.NotFound(writer, request)
return
}
data, err := json.Marshal(info)
if err != nil {
log.Println("failed to encode info:", err)
http.Error(writer, "encoding", http.StatusInternalServerError)
return
}
writer.Header().Set("X-Correlation-Id", requestID)
writer.Header().Set("Content-Type", "application/json")
writer.Header().Set("Content-Length", strconv.Itoa(len(data)))
writer.Header().Set("Content-Version", strconv.Itoa(len(info.Attempts)))
// modification time
setLastModify(writer, info)
writer.Header().Set("Age", strconv.FormatInt(int64(time.Since(info.CreatedAt)/time.Second), 10))
if info.Complete {
writer.Header().Set("X-Status", "complete")
} else {
writer.Header().Set("X-Status", "processing")
}
if len(info.Attempts) > 0 {
writer.Header().Set("X-Last-Attempt", info.Attempts[len(info.Attempts)-1].ID)
writer.Header().Set("X-Last-Attempt-At", info.Attempts[len(info.Attempts)-1].CreatedAt.Format(time.RFC850))
}
if info.Complete {
lastAttempt := info.Attempts[len(info.Attempts)-1]
request.URL.Path += "/attempt/" + lastAttempt.ID
writer.Header().Set("Location", request.URL.String())
}
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write(data)
}
}
func retry(wrk *worker.Worker) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
params := mux.Vars(request)
requestID := params["id"]
id, err := wrk.Retry(request.Context(), requestID)
if err != nil {
log.Println("failed to retry:", err)
http.Error(writer, "failed to enqueue", http.StatusInternalServerError)
return
}
writer.Header().Set("X-Correlation-Id", id)
http.Redirect(writer, request, id, http.StatusSeeOther)
}
}
func completeRequest(wrk *worker.Worker) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
params := mux.Vars(request)
requestID := params["id"]
info, err := wrk.Meta().Get(requestID)
if err != nil {
log.Println("failed access request", requestID, ":", err)
http.NotFound(writer, request)
return
}
if !info.Complete {
err = wrk.Meta().Complete(requestID)
if err != nil {
log.Println("failed to mark request as complete:", err)
http.Error(writer, "failed to complete", http.StatusInternalServerError)
return
}
}
writer.WriteHeader(http.StatusNoContent)
}
}
func getComplete(wrk *worker.Worker) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
params := mux.Vars(request)
requestID := params["id"]
info, err := wrk.Meta().Get(requestID)
if err != nil {
log.Println("failed access request", requestID, ":", err)
http.NotFound(writer, request)
return
}
if !info.Complete {
http.NotFound(writer, request)
return
}
lastAttempt := info.Attempts[len(info.Attempts)-1]
http.Redirect(writer, request, "attempt/"+lastAttempt.ID, http.StatusMovedPermanently)
}
}
func getAttempt(wrk *worker.Worker) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
params := mux.Vars(request)
requestID := params["id"]
attemptID := params["attemptId"]
info, err := wrk.Meta().Get(requestID)
if err != nil {
log.Println("failed access request", requestID, ":", err)
http.NotFound(writer, request)
return
}
var attempt meta.Attempt
var found bool
for _, atp := range info.Attempts {
if atp.ID == attemptID {
found = true
attempt = atp
break
}
}
if !found {
http.NotFound(writer, request)
return
}
body, err := wrk.Blobs().Get(attempt.ID)
if err != nil {
log.Println("failed to get body:", err)
http.Error(writer, "get body", http.StatusInternalServerError)
return
}
defer body.Close()
writer.Header().Set("Last-Modified", attempt.CreatedAt.Format(time.RFC850))
if info.Complete {
writer.Header().Set("X-Status", "complete")
} else {
writer.Header().Set("X-Status", "processing")
}
writer.Header().Set("X-Processed", "true")
for k, v := range attempt.Headers {
writer.Header()[k] = v
}
writer.WriteHeader(attempt.Code)
_, _ = io.Copy(writer, body)
}
}
func getRequest(wrk *worker.Worker) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
params := mux.Vars(request)
requestID := params["id"]
info, err := wrk.Meta().Get(requestID)
if err != nil {
log.Println("failed access request", requestID, ":", err)
http.NotFound(writer, request)
return
}
writer.Header().Set("Last-Modified", info.CreatedAt.Format(time.RFC850))
f, err := wrk.Blobs().Get(requestID)
if err != nil {
log.Println("failed to get data:", err)
http.Error(writer, "data", http.StatusInternalServerError)
return
}
defer f.Close()
writer.Header().Set("X-Method", info.Method)
writer.Header().Set("X-Request-Uri", info.URI)
for k, v := range info.Headers {
writer.Header()[k] = v
}
writer.WriteHeader(http.StatusOK)
_, _ = io.Copy(writer, f)
}
}
func setLastModify(writer http.ResponseWriter, info *meta.Request) {
if info.Complete {
writer.Header().Set("Last-Modified", info.CompleteAt.Format(time.RFC850))
} else if len(info.Attempts) > 0 {
writer.Header().Set("Last-Modified", info.Attempts[len(info.Attempts)-1].CreatedAt.Format(time.RFC850))
} else {
writer.Header().Set("Last-Modified", info.CreatedAt.Format(time.RFC850))
}
}

View File

@ -10,7 +10,7 @@ import (
"strings"
"time"
"nano-run/server/internal"
"nano-run/server/api"
)
type markerResponse struct {
@ -74,7 +74,7 @@ func (bh *binHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reques
cmd.Stdin = request.Body
cmd.Stdout = marker
cmd.Env = env
internal.SetBinFlags(cmd)
api.SetBinFlags(cmd)
err := cmd.Run()
if codeReset, ok := writer.(interface{ Status(status int) }); ok && err != nil {

View File

@ -1,8 +1,7 @@
package server
package runner
import (
"context"
"io"
"io/ioutil"
"log"
"net/http"
@ -10,12 +9,16 @@ import (
"path/filepath"
"time"
"github.com/gin-gonic/gin"
"gopkg.in/yaml.v2"
"nano-run/server"
"nano-run/server/ui"
"nano-run/worker"
)
type Config struct {
UIDirectory string `yaml:"ui_directory"`
WorkingDirectory string `yaml:"working_directory"`
ConfigDirectory string `yaml:"config_directory"`
Bind string `yaml:"bind"`
@ -37,6 +40,7 @@ func DefaultConfig() Config {
cfg.Bind = defaultBind
cfg.WorkingDirectory = filepath.Join("run")
cfg.ConfigDirectory = filepath.Join("conf.d")
cfg.UIDirectory = filepath.Join("ui")
cfg.GracefulShutdown = defaultGracefulShutdown
return cfg
}
@ -76,17 +80,28 @@ func (cfg Config) SaveFile(file string) error {
}
func (cfg Config) Create(global context.Context) (*Server, error) {
units, err := Units(cfg.ConfigDirectory)
units, err := server.Units(cfg.ConfigDirectory)
if err != nil {
return nil, err
}
workers, err := Workers(cfg.WorkingDirectory, units)
workers, err := server.Workers(cfg.WorkingDirectory, units)
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(global)
router := gin.Default()
server.Attach(router.Group("/api/"), units, workers)
ui.Attach(router.Group("/ui/"), units, cfg.UIDirectory)
router.Group("/", func(gctx *gin.Context) {
gctx.Redirect(http.StatusTemporaryRedirect, "ui")
})
//router.Path("/").Methods("GET").HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
// http.Redirect(writer, request, "ui", http.StatusTemporaryRedirect)
//})
srv := &Server{
Handler: Handler(units, workers),
Handler: router,
workers: workers,
units: units,
done: make(chan struct{}),
@ -132,30 +147,17 @@ func (cfg Config) Run(global context.Context) error {
return err
}
func limitRequest(maxSize int64, handler http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
body := request.Body
defer body.Close()
if request.ContentLength > maxSize {
http.Error(writer, "too big request", http.StatusBadRequest)
return
}
limiter := io.LimitReader(request.Body, maxSize)
request.Body = ioutil.NopCloser(limiter)
handler.ServeHTTP(writer, request)
})
}
type Server struct {
http.Handler
workers []*worker.Worker
units []Unit
units []server.Unit
cancel func()
done chan struct{}
err error
}
func (srv *Server) Units() []server.Unit { return srv.units }
func (srv *Server) Close() {
for _, wrk := range srv.workers {
wrk.Close()
@ -169,7 +171,7 @@ func (srv *Server) Err() error {
}
func (srv *Server) run(ctx context.Context) {
err := Run(ctx, srv.workers)
err := server.Run(ctx, srv.workers)
if err != nil {
log.Println("workers stopped:", err)
}

View File

@ -16,6 +16,7 @@ import (
"github.com/stretchr/testify/assert"
"nano-run/server"
"nano-run/server/runner"
"nano-run/services/meta"
)
@ -32,7 +33,7 @@ func TestMain(main *testing.M) {
os.Exit(code)
}
func testServer(t *testing.T, cfg server.Config, units map[string]server.Unit) *server.Server {
func testServer(t *testing.T, cfg runner.Config, units map[string]server.Unit) *runner.Server {
sub, err := ioutil.TempDir(tmpDir, "")
if !assert.NoError(t, err) {
t.Fatal("failed to create temp dir", err)
@ -60,28 +61,29 @@ func testServer(t *testing.T, cfg server.Config, units map[string]server.Unit) *
}
func Test_create(t *testing.T) {
srv := testServer(t, server.DefaultConfig(), map[string]server.Unit{
srv := testServer(t, runner.DefaultConfig(), map[string]server.Unit{
"hello": {
Command: "echo -n hello world",
},
})
defer srv.Close()
req := httptest.NewRequest(http.MethodPost, "/hello/", bytes.NewBufferString("hello world"))
req := httptest.NewRequest(http.MethodPost, "/api/hello/", bytes.NewBufferString("hello world"))
res := httptest.NewRecorder()
srv.ServeHTTP(res, req)
assert.Equal(t, http.StatusSeeOther, res.Code)
assert.NotEmpty(t, res.Header().Get("X-Correlation-Id"))
assert.Equal(t, "/hello/"+res.Header().Get("X-Correlation-Id"), res.Header().Get("Location"))
assert.Equal(t, "/api/hello/"+res.Header().Get("X-Correlation-Id"), res.Header().Get("Location"))
requestID := res.Header().Get("X-Correlation-Id")
infoURL := res.Header().Get("Location")
t.Log("Location:", infoURL)
req = httptest.NewRequest(http.MethodGet, infoURL, nil)
res = httptest.NewRecorder()
srv.ServeHTTP(res, req)
assert.Equal(t, http.StatusOK, res.Code)
assert.Equal(t, requestID, res.Header().Get("X-Correlation-Id"))
assert.Equal(t, "application/json", res.Header().Get("Content-Type"))
assert.Contains(t, res.Header().Get("Content-Type"), "application/json")
var info meta.Request
err := json.Unmarshal(res.Body.Bytes(), &info)
assert.NoError(t, err)
@ -96,7 +98,7 @@ func Test_create(t *testing.T) {
resultLocation = res.Header().Get("Location")
break
}
if !assert.Equal(t, http.StatusNotFound, res.Code) {
if !assert.Equal(t, http.StatusTooEarly, res.Code) {
return
}
time.Sleep(time.Second)
@ -110,19 +112,19 @@ func Test_create(t *testing.T) {
}
func Test_retryIfDataReturnedInBinMode(t *testing.T) {
srv := testServer(t, server.DefaultConfig(), map[string]server.Unit{
srv := testServer(t, runner.DefaultConfig(), map[string]server.Unit{
"hello": {
Command: "echo hello world; exit 1",
},
})
defer srv.Close()
req := httptest.NewRequest(http.MethodPost, "/hello/", bytes.NewBufferString("hello world"))
req := httptest.NewRequest(http.MethodPost, "/api/hello/", bytes.NewBufferString("hello world"))
res := httptest.NewRecorder()
srv.ServeHTTP(res, req)
assert.Equal(t, http.StatusSeeOther, res.Code)
assert.NotEmpty(t, res.Header().Get("X-Correlation-Id"))
assert.Equal(t, "/hello/"+res.Header().Get("X-Correlation-Id"), res.Header().Get("Location"))
assert.Equal(t, "/api/hello/"+res.Header().Get("X-Correlation-Id"), res.Header().Get("Location"))
location := res.Header().Get("Location")
// wait for first result

5
server/ui/mapper.go Normal file
View File

@ -0,0 +1,5 @@
package ui
func withParams() {
}

72
server/ui/router.go Normal file
View File

@ -0,0 +1,72 @@
package ui
import (
"html/template"
"net/http"
"path/filepath"
"github.com/Masterminds/sprig"
"github.com/gin-gonic/gin"
"nano-run/server"
)
func Expose(units []server.Unit, uiDir string) http.Handler {
router := gin.New()
Attach(router, units, uiDir)
return router
}
func Attach(router gin.IRouter, units []server.Unit, uiDir string) {
ui := &uiRouter{
dir: uiDir,
units: units,
}
router.GET("/units", ui.listUnits)
router.GET("/unit/:name", ui.unitInfo)
}
type uiRouter struct {
dir string
units []server.Unit
}
func (ui *uiRouter) unitInfo(gctx *gin.Context) {
name := gctx.Param("name")
var unit *server.Unit
for _, u := range ui.units {
if u.Name() == name {
unit = &u
break
}
}
if unit == nil {
gctx.AbortWithStatus(http.StatusNotFound)
return
}
var reply struct {
Unit *server.Unit
}
reply.Unit = unit
gctx.HTML(http.StatusOK, "unit-info.html", reply)
}
func (ui *uiRouter) listUnits(gctx *gin.Context) {
var reply struct {
Units []server.Unit
}
reply.Units = ui.units
gctx.HTML(http.StatusOK, "units-list.html", reply)
}
func (ui *uiRouter) getTemplate(name string) *template.Template {
t, err := template.New("").Funcs(sprig.HtmlFuncMap()).ParseFiles(filepath.Join(ui.dir, name))
if err == nil {
return t
}
t, err = template.New("").Parse("<html><body>Ooops... Page not found</body></html>")
if err != nil {
panic(err)
}
return t
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
@ -17,10 +18,10 @@ import (
"sync"
"time"
"github.com/gorilla/mux"
"github.com/gin-gonic/gin"
"gopkg.in/yaml.v2"
"nano-run/server/internal"
"nano-run/server/api"
"nano-run/worker"
)
@ -111,6 +112,15 @@ func (cfg Unit) SaveFile(file string) error {
return ioutil.WriteFile(file, data, 0600)
}
func (cfg Unit) Name() string { return cfg.name }
func (cfg Unit) Secured() bool {
return cfg.Authorization.Basic.Enable ||
cfg.Authorization.HeaderToken.Enable ||
cfg.Authorization.QueryToken.Enable ||
cfg.Authorization.JWT.Enable
}
func Units(configsDir string) ([]Unit, error) {
var configs []Unit
err := filepath.Walk(configsDir, func(path string, info os.FileInfo, err error) error {
@ -164,16 +174,19 @@ func Workers(workdir string, configurations []Unit) ([]*worker.Worker, error) {
}
func Handler(units []Unit, workers []*worker.Worker) http.Handler {
router := mux.NewRouter()
for i, unit := range units {
prefix := "/" + unit.name + "/"
subRouter := router.PathPrefix(prefix).Subrouter()
subRouter.Use(unit.enableAuthorization())
internal.Expose(subRouter, workers[i])
}
router := gin.New()
Attach(router, units, workers)
return router
}
func Attach(router gin.IRouter, units []Unit, workers []*worker.Worker) {
for i, unit := range units {
group := router.Group("/" + unit.name + "/")
group.Use(unit.enableAuthorization())
api.Expose(group, workers[i])
}
}
func Run(global context.Context, workers []*worker.Worker) error {
if len(workers) == 0 {
<-global.Done()
@ -263,3 +276,18 @@ func makeEnvList(content map[string]string) []string {
}
return ans
}
func limitRequest(maxSize int64, handler http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
body := request.Body
defer body.Close()
if request.ContentLength > maxSize {
http.Error(writer, "too big request", http.StatusBadRequest)
return
}
limiter := io.LimitReader(request.Body, maxSize)
request.Body = ioutil.NopCloser(limiter)
handler.ServeHTTP(writer, request)
})
}

18
templates/unit-info.html Normal file
View File

@ -0,0 +1,18 @@
<html>
<body>
<div>
<a href="unit/{{.Unit.Name}}">
<h2>{{.Unit.Name}}</h2>
{{if .Unit.Secured}}
secured
{{end}}
</a>
<p>
<span class="unit-mode unit-mode-{{.Unit.Mode}}">{{.Unit.Mode}}</span>,
<span class="unit-mode unit-mode-workers">{{.Unit.Workers}}</span>,
<span class="unit-mode unit-mode-interval">{{.Unit.Interval}}</span>,
<span class="unit-mode unit-mode-timeout">{{.Unit.Timeout}}</span>,
</p>
</div>
</body>
</html>

17
templates/units-list.html Normal file
View File

@ -0,0 +1,17 @@
<html>
<body>
{{range .Units}}
<div>
<a href="unit/{{.Name}}">
<h2>{{.Name}}</h2>
</a>
<p>
<span class="unit-mode unit-mode-{{.Mode}}">{{.Mode}}</span>,
<span class="unit-mode unit-mode-workers">{{.Workers}}</span>,
<span class="unit-mode unit-mode-interval">{{.Interval}}</span>,
<span class="unit-mode unit-mode-timeout">{{.Timeout}}</span>,
</p>
</div>
{{end}}
</body>
</html>