add ui stub
This commit is contained in:
parent
554083128b
commit
dac1910200
7
go.mod
7
go.mod
@ -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
48
go.sum
@ -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
209
server/api/adapter.go
Normal 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))
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
// +build !linux
|
||||
|
||||
package internal
|
||||
package api
|
||||
|
||||
import "os/exec"
|
||||
|
@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package api
|
||||
|
||||
import (
|
||||
"os/exec"
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
@ -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
5
server/ui/mapper.go
Normal file
@ -0,0 +1,5 @@
|
||||
package ui
|
||||
|
||||
func withParams() {
|
||||
|
||||
}
|
72
server/ui/router.go
Normal file
72
server/ui/router.go
Normal 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
|
||||
}
|
@ -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
18
templates/unit-info.html
Normal 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
17
templates/units-list.html
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user