graphql-engine/cli/commands/console.go

234 lines
5.8 KiB
Go
Raw Normal View History

2018-06-24 16:40:48 +03:00
package commands
import (
"fmt"
"net/http"
"net/url"
"path/filepath"
"runtime"
"sync"
"github.com/fatih/color"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
2018-06-24 16:47:01 +03:00
"github.com/hasura/graphql-engine/cli"
2018-06-24 16:40:48 +03:00
"github.com/hasura/graphql-engine/cli/migrate/api"
2018-06-24 16:47:01 +03:00
"github.com/hasura/graphql-engine/cli/util"
2018-06-24 16:40:48 +03:00
"github.com/pkg/errors"
"github.com/skratchdot/open-golang/open"
"github.com/spf13/cobra"
2018-06-24 16:47:01 +03:00
"github.com/spf13/viper"
2018-06-24 16:40:48 +03:00
)
func NewConsoleCmd(ec *cli.ExecutionContext) *cobra.Command {
2018-06-24 16:47:01 +03:00
v := viper.New()
2018-06-24 16:40:48 +03:00
opts := &consoleOptions{
EC: ec,
}
consoleCmd := &cobra.Command{
Use: "console",
Short: "Open console to manage database and try out APIs",
Long: "Run a web server to serve Hasura Console for GraphQL Engine to manage database and build queries",
Example: ` # Start console:
hasura console
# Start console on a different address and ports:
hasura console --address 0.0.0.0 --console-port 8080 --api-port 8081`,
SilenceUsage: true,
2018-06-24 16:47:01 +03:00
PreRunE: func(cmd *cobra.Command, args []string) error {
ec.Viper = v
return ec.Validate()
},
2018-06-24 16:40:48 +03:00
RunE: func(cmd *cobra.Command, args []string) error {
return opts.Run()
},
}
f := consoleCmd.Flags()
f.StringVar(&opts.APIPort, "api-port", "9693", "port for serving migrate api")
f.StringVar(&opts.ConsolePort, "console-port", "9695", "port for serving console")
f.StringVar(&opts.Address, "address", "localhost", "address to use")
2018-06-27 15:04:09 +03:00
f.BoolVar(&opts.DontOpenBrowser, "no-browser", false, "do not automatically open console in browser")
2018-06-24 16:47:01 +03:00
f.String("endpoint", "", "http(s) endpoint for Hasura GraphQL Engine")
f.String("access-key", "", "access key for Hasura GraphQL Engine")
// need to create a new viper because https://github.com/spf13/viper/issues/233
v.BindPFlag("endpoint", f.Lookup("endpoint"))
v.BindPFlag("access_key", f.Lookup("access-key"))
2018-06-24 16:40:48 +03:00
return consoleCmd
}
type consoleOptions struct {
EC *cli.ExecutionContext
APIPort string
ConsolePort string
Address string
2018-06-27 15:04:09 +03:00
DontOpenBrowser bool
WG *sync.WaitGroup
2018-06-24 16:40:48 +03:00
}
func (o *consoleOptions) Run() error {
log := o.EC.Logger
// Switch to "release" mode in production.
gin.SetMode(gin.ReleaseMode)
// An Engine instance with the Logger and Recovery middleware already attached.
r := gin.New()
r.Use(allowCors())
// My Router struct
router := &consoleRouter{
r,
}
u, err := url.Parse(o.EC.Config.Endpoint)
if err != nil {
return errors.Wrap(err, "cannot parse endpoint as url")
}
router.setRoutes(u.Host, o.EC.Config.AccessKey, o.EC.MigrationDir)
var releaseSuffix string
if !o.EC.IsStableRelease {
releaseSuffix = "-stg"
}
consoleRouter, err := serveConsole(gin.H{
"apiHost": "http://" + o.Address,
"apiPort": o.APIPort,
"cliVersion": "",
"dataApiUrl": o.EC.Config.Endpoint,
"dataApiVersion": "",
"accessKey": o.EC.Config.AccessKey,
"releaseSuffix": releaseSuffix,
})
if err != nil {
return errors.Wrap(err, "error serving console")
}
// Create WaitGroup for running 3 servers
wg := &sync.WaitGroup{}
2018-06-27 15:04:09 +03:00
o.WG = wg
2018-06-24 16:40:48 +03:00
wg.Add(1)
go func() {
err = router.Run(o.Address + ":" + o.APIPort)
if err != nil {
o.EC.Logger.WithError(err).Errorf("error listening on port %s", o.APIPort)
}
wg.Done()
}()
wg.Add(1)
go func() {
err = consoleRouter.Run(o.Address + ":" + o.ConsolePort)
if err != nil {
o.EC.Logger.WithError(err).Errorf("error listening on port %s", o.ConsolePort)
}
wg.Done()
}()
consoleURL := fmt.Sprintf("http://%s:%s", o.Address, o.ConsolePort)
2018-06-27 15:04:09 +03:00
if !o.DontOpenBrowser {
o.EC.Spin(color.CyanString("Opening console using default browser..."))
defer o.EC.Spinner.Stop()
2018-06-24 16:40:48 +03:00
2018-06-27 15:04:09 +03:00
err = open.Run(consoleURL)
if err != nil {
o.EC.Logger.WithError(err).Warn("Error opening browser, try to open the url manually?")
}
2018-06-24 16:40:48 +03:00
}
o.EC.Spinner.Stop()
log.Infof("console running at: %s", consoleURL)
wg.Wait()
return nil
}
type consoleRouter struct {
*gin.Engine
}
func (router *consoleRouter) setRoutes(host, accessKey, migrationDir string) {
apis := router.Group("/apis")
{
// Migrate api endpoints and middleware
migrateAPIs := apis.Group("/migrate")
{
migrateAPIs.Use(setFilePath(migrationDir))
migrateAPIs.Use(setDataPath(host, accessKey))
settingsAPIs := migrateAPIs.Group("/settings")
{
settingsAPIs.Any("", api.SettingsAPI)
}
migrateAPIs.Any("", api.MigrateAPI)
}
// Migrate api endpoints and middleware
metadataAPIs := apis.Group("/metadata")
{
metadataAPIs.Use(setFilePath(migrationDir))
metadataAPIs.Use(setDataPath(host, accessKey))
metadataAPIs.Any("", api.MetadataAPI)
}
}
}
func setDataPath(hostName, accessKey string) gin.HandlerFunc {
return func(c *gin.Context) {
host := url.URL{
Scheme: "hasuradb",
User: url.UserPassword("admin", accessKey),
Host: hostName,
}
c.Set("dbpath", host)
c.Next()
}
}
func setFilePath(dir string) gin.HandlerFunc {
return func(c *gin.Context) {
if runtime.GOOS == "windows" {
c.Set("filedir", "file:///"+filepath.Clean(dir))
} else {
c.Set("filedir", "file://"+filepath.Clean(dir))
}
c.Next()
}
}
func allowCors() gin.HandlerFunc {
config := cors.DefaultConfig()
config.AddAllowHeaders("X-Hasura-User-Id")
config.AddAllowHeaders("X-Hasura-Access-Key")
config.AddAllowHeaders("X-Hasura-Role")
config.AddAllowHeaders("X-Hasura-Allowed-Roles")
config.AddAllowMethods("DELETE")
config.AllowAllOrigins = true
config.AllowCredentials = false
return cors.New(config)
}
func serveConsole(opts gin.H) (*gin.Engine, error) {
// An Engine instance with the Logger and Recovery middleware already attached.
r := gin.New()
// Template index.html
templateRender, err := util.LoadTemplates("assets/", "console.html")
2018-06-24 16:40:48 +03:00
if err != nil {
return nil, errors.Wrap(err, "cannot fetch template")
}
r.HTMLRender = templateRender
r.Any("/*action", func(c *gin.Context) {
c.HTML(http.StatusOK, "console.html", &opts)
})
return r, nil
}