mirror of
https://github.com/turbot/steampipe.git
synced 2026-02-23 08:00:51 -05:00
201 lines
7.1 KiB
Go
201 lines
7.1 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
|
|
"github.com/turbot/steampipe/statushooks"
|
|
"github.com/turbot/steampipe/workspace"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
"github.com/turbot/go-kit/helpers"
|
|
"github.com/turbot/steampipe-plugin-sdk/v3/logging"
|
|
"github.com/turbot/steampipe/cmdconfig"
|
|
"github.com/turbot/steampipe/constants"
|
|
"github.com/turbot/steampipe/contexthelpers"
|
|
"github.com/turbot/steampipe/dashboard"
|
|
"github.com/turbot/steampipe/dashboard/dashboardassets"
|
|
"github.com/turbot/steampipe/dashboard/dashboardserver"
|
|
"github.com/turbot/steampipe/utils"
|
|
)
|
|
|
|
func dashboardCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "dashboard",
|
|
TraverseChildren: true,
|
|
Args: cobra.ArbitraryArgs,
|
|
Run: runDashboardCmd,
|
|
Short: "Start the local dashboard UI",
|
|
Long: `Starts a local web server that enables real-time development of dashboards within the current mod.
|
|
|
|
The current mod is the working directory, or the directory specified by the --workspace-chdir flag.`,
|
|
}
|
|
|
|
cmdconfig.OnCmd(cmd).
|
|
AddBoolFlag(constants.ArgHelp, "h", false, "Help for dashboard").
|
|
AddBoolFlag(constants.ArgModInstall, "", true, "Specify whether to install mod dependencies before running the dashboard").
|
|
AddStringFlag(constants.ArgDashboardListen, "", string(dashboardserver.ListenTypeLocal), "Accept connections from: local (localhost only) or network (open)").
|
|
AddIntFlag(constants.ArgDashboardPort, "", constants.DashboardServerDefaultPort, "Dashboard server port.").
|
|
AddBoolFlag(constants.ArgBrowser, "", true, "Specify whether to launch the browser after starting the dashboard server").
|
|
AddStringSliceFlag(constants.ArgSearchPath, "", nil, "Set a custom search_path for the steampipe user for a check session (comma-separated)").
|
|
AddStringSliceFlag(constants.ArgSearchPathPrefix, "", nil, "Set a prefix to the current search path for a check session (comma-separated)").
|
|
AddStringSliceFlag(constants.ArgVarFile, "", nil, "Specify an .spvar file containing variable values").
|
|
// NOTE: use StringArrayFlag for ArgVariable, not StringSliceFlag
|
|
// Cobra will interpret values passed to a StringSliceFlag as CSV,
|
|
// where args passed to StringArrayFlag are not parsed and used raw
|
|
AddStringArrayFlag(constants.ArgVariable, "", nil, "Specify the value of a variable").
|
|
AddBoolFlag(constants.ArgInput, "", true, "Enable interactive prompts").
|
|
// hidden flags that are used internally
|
|
AddBoolFlag(constants.ArgServiceMode, "", false, "Hidden flag to specify whether this is starting as a service", cmdconfig.FlagOptions.Hidden())
|
|
|
|
return cmd
|
|
}
|
|
|
|
func runDashboardCmd(cmd *cobra.Command, args []string) {
|
|
dashboardCtx := cmd.Context()
|
|
|
|
logging.LogTime("runDashboardCmd start")
|
|
defer func() {
|
|
logging.LogTime("runDashboardCmd end")
|
|
if r := recover(); r != nil {
|
|
utils.ShowError(dashboardCtx, helpers.ToError(r))
|
|
if isRunningAsService() {
|
|
saveErrorToDashboardState(helpers.ToError(r))
|
|
}
|
|
}
|
|
}()
|
|
|
|
serverPort := dashboardserver.ListenPort(viper.GetInt(constants.ArgDashboardPort))
|
|
utils.FailOnError(serverPort.IsValid())
|
|
|
|
serverListen := dashboardserver.ListenType(viper.GetString(constants.ArgDashboardListen))
|
|
utils.FailOnError(serverListen.IsValid())
|
|
|
|
if err := utils.IsPortBindable(int(serverPort)); err != nil {
|
|
exitCode = constants.ExitCodeBindPortUnavailable
|
|
utils.FailOnError(err)
|
|
}
|
|
|
|
// create context for the dashboard execution
|
|
dashboardCtx, cancel := context.WithCancel(dashboardCtx)
|
|
contexthelpers.StartCancelHandler(cancel)
|
|
|
|
// ensure dashboard assets are present and extract if not
|
|
err := dashboardassets.Ensure(dashboardCtx)
|
|
utils.FailOnError(err)
|
|
|
|
// disable all status messages
|
|
dashboardCtx = statushooks.DisableStatusHooks(dashboardCtx)
|
|
|
|
// load the workspace
|
|
dashboardserver.OutputWait(dashboardCtx, "Loading Workspace")
|
|
w, err := loadWorkspacePromptingForVariables(dashboardCtx)
|
|
utils.FailOnErrorWithMessage(err, "failed to load workspace")
|
|
|
|
initData := dashboard.NewInitData(dashboardCtx, w)
|
|
// shutdown the service on exit
|
|
defer initData.Cleanup(dashboardCtx)
|
|
|
|
err = handleDashboardInitResult(dashboardCtx, initData)
|
|
// if there was an error, display it
|
|
utils.FailOnError(err)
|
|
|
|
server, err := dashboardserver.NewServer(dashboardCtx, initData.Client, initData.Workspace)
|
|
if err != nil {
|
|
utils.FailOnError(err)
|
|
}
|
|
// start the server asynchronously - this returns a chan which is signalled when the internal API server terminates
|
|
doneChan := server.Start()
|
|
|
|
// cleanup
|
|
defer server.Shutdown()
|
|
|
|
// server has started - update state file/start browser, as required
|
|
onServerStarted(serverPort, serverListen, err)
|
|
|
|
// wait for API server to terminate
|
|
<-doneChan
|
|
|
|
log.Println("[TRACE] runDashboardCmd exiting")
|
|
}
|
|
|
|
// inspect the init result ands
|
|
func handleDashboardInitResult(ctx context.Context, initData *dashboard.InitData) error {
|
|
// if there is an error or cancellation we bomb out
|
|
if err := initData.Result.Error; err != nil {
|
|
setExitCodeForDashboardError(err)
|
|
return initData.Result.Error
|
|
}
|
|
// cancelled?
|
|
if ctx != nil && ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
// if there is a usage warning we display it
|
|
initData.Result.DisplayMessages()
|
|
|
|
return nil
|
|
}
|
|
|
|
func setExitCodeForDashboardError(err error) {
|
|
if err == workspace.ErrorNoModDefinition {
|
|
exitCode = constants.ExitCodeNoModFile
|
|
} else {
|
|
exitCode = constants.ExitCodeUnknownErrorPanic
|
|
}
|
|
}
|
|
|
|
// execute any required actions after successfult server startup
|
|
func onServerStarted(serverPort dashboardserver.ListenPort, serverListen dashboardserver.ListenType, err error) {
|
|
if isRunningAsService() {
|
|
// for service mode only, save the state
|
|
saveDashboardState(serverPort, serverListen)
|
|
} else {
|
|
// start browser if required
|
|
if viper.GetBool(constants.ArgBrowser) {
|
|
if err = dashboardserver.OpenBrowser(fmt.Sprintf("http://localhost:%d", serverPort)); err != nil {
|
|
log.Println("[TRACE] dashboard server started but failed to start client", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// is this dashboard server running as a service?
|
|
func isRunningAsService() bool {
|
|
return viper.GetBool(constants.ArgServiceMode)
|
|
}
|
|
|
|
// persist the error to the dashboard state file
|
|
func saveErrorToDashboardState(err error) {
|
|
state, _ := dashboardserver.GetDashboardServiceState()
|
|
if state == nil {
|
|
// write the state file with an error, only if it doesn't exist already
|
|
// if it exists, that means dashboard stated properly and 'service start' already known about it
|
|
state = &dashboardserver.DashboardServiceState{
|
|
State: dashboardserver.ServiceStateError,
|
|
Error: err.Error(),
|
|
}
|
|
dashboardserver.WriteServiceStateFile(state)
|
|
}
|
|
}
|
|
|
|
// save the dashboard state file
|
|
func saveDashboardState(serverPort dashboardserver.ListenPort, serverListen dashboardserver.ListenType) {
|
|
state := &dashboardserver.DashboardServiceState{
|
|
State: dashboardserver.ServiceStateRunning,
|
|
Error: "",
|
|
Pid: os.Getpid(),
|
|
Port: int(serverPort),
|
|
ListenType: string(serverListen),
|
|
Listen: constants.DatabaseListenAddresses,
|
|
}
|
|
|
|
if serverListen == dashboardserver.ListenTypeNetwork {
|
|
addrs, _ := utils.LocalAddresses()
|
|
state.Listen = append(state.Listen, addrs...)
|
|
}
|
|
utils.FailOnError(dashboardserver.WriteServiceStateFile(state))
|
|
}
|