Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

services/serve: refactor dev server handler #91

Merged
merged 3 commits into from
Jul 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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