forked from ignite/cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
services/serve: refactor dev server handler (ignite#91)
* 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
Showing
7 changed files
with
284 additions
and
70 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!"))) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.