Skip to content

Commit

Permalink
services/serve: refactor dev server handler (ignite#91)
Browse files Browse the repository at this point in the history
* services/serve: refactor dev server handler

* updated frontend bindings.
* added new `pkg/xhttp` helper.
* added new `pkg/httpstatuschecker` helper.

* services/serve: simplify server status check

and add concurrent checks.

* services/serve: rework def of dev server's /status enpoint
  • Loading branch information
ilgooz authored Jul 30, 2020
1 parent 2be6a49 commit ba8f0ef
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 70 deletions.
56 changes: 56 additions & 0 deletions starport/pkg/httpstatuschecker/httpstatuschecker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// httpstatuschecker is a tool check health of http pages.
package httpstatuschecker

import (
"net/http"
)

type checker struct {
c *http.Client
addr string
method string
}

// Option used to customize checker.
type Option func(*checker)

// Client configures http client.
func Client(c *http.Client) Option {
return func(cr *checker) {
cr.c = c
}
}

// Client configures http method.
func Method(name string) Option {
return func(cr *checker) {
cr.method = name
}
}

// Check checks if given http addr is alive by applying options.
func Check(addr string, options ...Option) (isAvailable bool, err error) {
cr := &checker{
c: http.DefaultClient,
addr: addr,
method: http.MethodGet,
}
for _, o := range options {
o(cr)
}
return cr.check()
}

func (c *checker) check() (bool, error) {
req, err := http.NewRequest(c.method, c.addr, nil)
if err != nil {
return false, err
}
res, err := c.c.Do(req)
if err != nil {
return false, nil
}
defer res.Body.Close()
isOKStatus := res.StatusCode >= http.StatusOK && res.StatusCode < http.StatusMultipleChoices
return isOKStatus, nil
}
39 changes: 39 additions & 0 deletions starport/pkg/httpstatuschecker/httpstatuschecker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package httpstatuschecker

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/require"
)

func TestCheckStatus(t *testing.T) {
cases := []struct {
name string
returnedStatus int
isAvaiable bool
}{
{"200 OK", 200, true},
{"202 Accepted ", 202, true},
{"404 Not Found", 404, false},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tt.returnedStatus)
}))
defer ts.Close()

isAvailable, err := Check(ts.URL)
require.NoError(t, err)
require.Equal(t, tt.isAvaiable, isAvailable)
})
}
}

func TestCheckServerUnreachable(t *testing.T) {
isAvailable, err := Check("http://localhost:63257")
require.NoError(t, err)
require.False(t, isAvailable)
}
41 changes: 41 additions & 0 deletions starport/pkg/xhttp/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package xhttp

import (
"encoding/json"
"errors"
"net/http"
)

// ResponseJSON writes a JSON response to w by using status as http status and data
// as payload.
func ResponseJSON(w http.ResponseWriter, status int, data interface{}) error {
bdata, err := json.Marshal(data)
if err != nil {
status = http.StatusInternalServerError
bdata, _ = json.Marshal(NewErrorResponse(errors.New(http.StatusText(status))))
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.Write(bdata)
return err
}

// ErrorResponseBody is the skeleton for error messages that should be sent to
// client.
type ErrorResponseBody struct {
Error ErrorResponse `json:"error"`
}

// ErrorResponse holds the error message.
type ErrorResponse struct {
Message string `json:"message"`
}

// NewErrorResponse creates a new http error response from err.
func NewErrorResponse(err error) ErrorResponseBody {
return ErrorResponseBody{
Error: ErrorResponse{
Message: err.Error(),
},
}
}
34 changes: 34 additions & 0 deletions starport/pkg/xhttp/response_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package xhttp

import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/require"
)

func TestResponseJSON(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]interface{}{"a": 1}
require.NoError(t, ResponseJSON(w, http.StatusCreated, data))
resp := w.Result()

require.Equal(t, http.StatusCreated, resp.StatusCode)
require.Equal(t, "application/json", resp.Header.Get("Content-Type"))

body, _ := ioutil.ReadAll(resp.Body)
dataJSON, _ := json.Marshal(data)
require.Equal(t, dataJSON, body)
}

func TestNewErrorResponse(t *testing.T) {
require.Equal(t, ErrorResponseBody{
Error: ErrorResponse{
Message: "error!",
},
}, NewErrorResponse(errors.New("error!")))
}
133 changes: 91 additions & 42 deletions starport/services/serve/dev.go
Original file line number Diff line number Diff line change
@@ -1,57 +1,106 @@
package starportserve

import (
"encoding/json"
"net/http"

"github.com/gobuffalo/packr/v2"
"github.com/gorilla/mux"
"github.com/tendermint/starport/starport/pkg/httpstatuschecker"
"github.com/tendermint/starport/starport/pkg/xhttp"
"golang.org/x/sync/errgroup"
)

// newDevHandler creates a new development server handler.
func newDevHandler(app App) http.Handler {
const (
appNodeInfoEndpoint = "/node_info"
)

// serviceStatusResponse holds the status of development environment and http services
// needed for development.
type statusResponse struct {
Status serviceStatus `json:"status"`
Env env `json:"env"`
}

// serviceStatus holds the availibity status of http services.
type serviceStatus struct {
IsConsensusEngineAlive bool `json:"is_consensus_engine_alive"`
IsMyAppBackendAlive bool `json:"is_my_app_backend_alive"`
IsMyAppFrontendAlive bool `json:"is_my_app_frontend_alive"`
}

// env holds info about development environment.
type env struct {
ChainID string `json:"chain_id"`
NodeJS bool `json:"node_js"`
}

// development handler builder.
type development struct {
app App
conf Config
}

// Config used to configure development handler.
type Config struct {
EngineAddr string
AppBackendAddr string
AppFrontendAddr string
DevFrontendAssetsPath string
}

// newDevHandler creates a new development server handler for app by given conf.
func newDevHandler(app App, conf Config) http.Handler {
dev := &development{app, conf}
router := mux.NewRouter()
devUI := packr.New("ui/dist", "../../ui/dist")
router.HandleFunc("/env", func(w http.ResponseWriter, r *http.Request) {
env := Env{app.Name, isCommandAvailable("node")}
js, err := json.Marshal(env)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
router.Handle("/status", dev.statusHandler()).Methods(http.MethodGet)
router.PathPrefix("/").Handler(dev.devAssetsHandler()).Methods(http.MethodGet)
return router
}

func (d *development) devAssetsHandler() http.Handler {
return http.FileServer(packr.New("ui/dist", d.conf.DevFrontendAssetsPath))
}

func (d *development) statusHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var (
engineStatus,
appBackendStatus,
appFrontendStatus bool
)
g := &errgroup.Group{}
g.Go(func() (err error) {
engineStatus, err = httpstatuschecker.Check(d.conf.EngineAddr)
return
})
g.Go(func() (err error) {
appBackendStatus, err = httpstatuschecker.Check(d.conf.AppBackendAddr + appNodeInfoEndpoint)
return
})
g.Go(func() (err error) {
appFrontendStatus, err = httpstatuschecker.Check(d.conf.AppFrontendAddr)
return
})
if err := g.Wait(); err != nil {
xhttp.ResponseJSON(w, http.StatusInternalServerError, xhttp.NewErrorResponse(err))
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(js)
})
router.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
res, err := http.Get("http://localhost:1317/node_info")
if err != nil || res.StatusCode != 200 {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("500 error"))
} else if res.StatusCode == 200 {
w.WriteHeader(http.StatusOK)
w.Write([]byte("200 ok"))
}
})
router.HandleFunc("/rpc", func(w http.ResponseWriter, r *http.Request) {
res, err := http.Get("http://localhost:26657")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
} else if res.StatusCode == 200 {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusInternalServerError)
}
})
router.HandleFunc("/frontend", func(w http.ResponseWriter, r *http.Request) {
res, err := http.Get("http://localhost:8080")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
} else if res.StatusCode == 200 {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusInternalServerError)

resp := statusResponse{
Env: d.env(),
Status: serviceStatus{
IsConsensusEngineAlive: engineStatus,
IsMyAppBackendAlive: appBackendStatus,
IsMyAppFrontendAlive: appFrontendStatus,
},
}
xhttp.ResponseJSON(w, http.StatusOK, resp)
})
router.PathPrefix("/").Handler(http.FileServer(devUI))
return router
}

func (d *development) env() env {
return env{
d.app.Name,
isCommandAvailable("node"),
}
}
14 changes: 7 additions & 7 deletions starport/services/serve/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,6 @@ func Serve(ctx context.Context, app App, verbose bool) error {
return w.Start(time.Millisecond * 1000)
}

// Env ...
type Env struct {
ChainID string `json:"chain_id"`
NodeJS bool `json:"node_js"`
}

func startServe(ctx context.Context, app App, verbose bool) error {
var (
steps step.Steps
Expand Down Expand Up @@ -191,7 +185,13 @@ func runDevServer(app App, verbose bool) error {
} else {
fmt.Printf("\n🚀 Get started: http://localhost:12345/\n\n")
}
return http.ListenAndServe(":12345", newDevHandler(app))
conf := Config{
EngineAddr: "http://localhost:26657",
AppBackendAddr: "http://localhost:1317",
AppFrontendAddr: "http://localhost:8080",
DevFrontendAssetsPath: "../../ui/dist",
} // TODO get vals from const
return http.ListenAndServe(":12345", newDevHandler(app, conf))
}

func isCommandAvailable(name string) bool {
Expand Down
Loading

0 comments on commit ba8f0ef

Please sign in to comment.