remove dashboard service from service cmd

This commit is contained in:
Puskar Basu
2024-05-30 16:03:40 +05:30
parent e1b4fad4ff
commit 6db67846e9
2 changed files with 14 additions and 366 deletions

View File

@@ -17,7 +17,6 @@ import (
"github.com/turbot/steampipe-plugin-sdk/v5/sperr" "github.com/turbot/steampipe-plugin-sdk/v5/sperr"
"github.com/turbot/steampipe/pkg/cmdconfig" "github.com/turbot/steampipe/pkg/cmdconfig"
"github.com/turbot/steampipe/pkg/constants" "github.com/turbot/steampipe/pkg/constants"
"github.com/turbot/steampipe/pkg/dashboard/dashboardserver"
"github.com/turbot/steampipe/pkg/db/db_local" "github.com/turbot/steampipe/pkg/db/db_local"
"github.com/turbot/steampipe/pkg/display" "github.com/turbot/steampipe/pkg/display"
"github.com/turbot/steampipe/pkg/error_helpers" "github.com/turbot/steampipe/pkg/error_helpers"
@@ -69,20 +68,9 @@ connection from any Postgres compatible database client.`,
AddStringFlag(constants.ArgServicePassword, "", "Set the database password for this session"). AddStringFlag(constants.ArgServicePassword, "", "Set the database password for this session").
// default is false and hides the database user password from service start prompt // default is false and hides the database user password from service start prompt
AddBoolFlag(constants.ArgServiceShowPassword, false, "View database password for connecting from another machine"). AddBoolFlag(constants.ArgServiceShowPassword, false, "View database password for connecting from another machine").
// dashboard server
AddBoolFlag(constants.ArgDashboard, false, "Run the dashboard webserver with the service").
AddStringFlag(constants.ArgDashboardListen, string(dashboardserver.ListenTypeNetwork), "Accept connections from: local (localhost only) or network (open) (dashboard)").
AddIntFlag(constants.ArgDashboardPort, constants.DashboardServerDefaultPort, "Report server port").
// foreground enables the service to run in the foreground - till exit // foreground enables the service to run in the foreground - till exit
AddBoolFlag(constants.ArgForeground, false, "Run the service in the foreground"). AddBoolFlag(constants.ArgForeground, false, "Run the service in the foreground").
// flags relevant only if the --dashboard arg is used:
AddStringSliceFlag(constants.ArgVarFile, nil, "Specify an .spvar file containing variable values (only applies if '--dashboard' flag is also set)").
// 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 (only applies if '--dashboard' flag is also set)").
// hidden flags for internal use // hidden flags for internal use
AddStringFlag(constants.ArgInvoker, string(constants.InvokerService), "Invoked by \"service\" or \"query\"", cmdconfig.FlagOptions.Hidden()) AddStringFlag(constants.ArgInvoker, string(constants.InvokerService), "Invoked by \"service\" or \"query\"", cmdconfig.FlagOptions.Hidden())
@@ -179,17 +167,17 @@ func runServiceStartCmd(cmd *cobra.Command, _ []string) {
error_helpers.FailOnError(invoker.IsValid()) error_helpers.FailOnError(invoker.IsValid())
} }
startResult, dashboardState, dbServiceStarted := startService(ctx, listenAddresses, port, invoker) startResult, dbServiceStarted := startService(ctx, listenAddresses, port, invoker)
alreadyRunning := !dbServiceStarted alreadyRunning := !dbServiceStarted
printStatus(ctx, startResult.DbState, startResult.PluginManagerState, dashboardState, alreadyRunning) printStatus(ctx, startResult.DbState, startResult.PluginManagerState, alreadyRunning)
if viper.GetBool(constants.ArgForeground) { if viper.GetBool(constants.ArgForeground) {
runServiceInForeground(ctx) runServiceInForeground(ctx)
} }
} }
func startService(ctx context.Context, listenAddresses []string, port int, invoker constants.Invoker) (_ *db_local.StartResult, _ *dashboardserver.DashboardServiceState, dbServiceStarted bool) { func startService(ctx context.Context, listenAddresses []string, port int, invoker constants.Invoker) (_ *db_local.StartResult, dbServiceStarted bool) {
statushooks.Show(ctx) statushooks.Show(ctx)
defer statushooks.Done(ctx) defer statushooks.Done(ctx)
log.Printf("[TRACE] startService - listenAddresses=%q", listenAddresses) log.Printf("[TRACE] startService - listenAddresses=%q", listenAddresses)
@@ -233,25 +221,7 @@ func startService(ctx context.Context, listenAddresses []string, port int, invok
dbServiceStarted = startResult.Status == db_local.ServiceStarted dbServiceStarted = startResult.Status == db_local.ServiceStarted
var dashboardState *dashboardserver.DashboardServiceState return startResult, dbServiceStarted
if viper.GetBool(constants.ArgDashboard) {
dashboardState, err = dashboardserver.GetDashboardServiceState()
if err != nil {
tryToStopServices(ctx)
exitCode = constants.ExitCodeServiceStartupFailure
error_helpers.FailOnError(err)
}
if dashboardState == nil {
dashboardState, err = startDashboardServer(ctx)
if err != nil {
tryToStopServices(ctx)
exitCode = constants.ExitCodeServiceStartupFailure
error_helpers.FailOnError(err)
}
dbServiceStarted = true
}
}
return startResult, dashboardState, dbServiceStarted
} }
func startServiceAndRefreshConnections(ctx context.Context, listenAddresses []string, port int, invoker constants.Invoker) *db_local.StartResult { func startServiceAndRefreshConnections(ctx context.Context, listenAddresses []string, port int, invoker constants.Invoker) *db_local.StartResult {
@@ -277,44 +247,6 @@ func tryToStopServices(ctx context.Context) {
if _, err := db_local.StopServices(ctx, false, constants.InvokerService); err != nil { if _, err := db_local.StopServices(ctx, false, constants.InvokerService); err != nil {
error_helpers.ShowError(ctx, err) error_helpers.ShowError(ctx, err)
} }
// stop the dashboard service
if err := dashboardserver.StopDashboardService(ctx); err != nil {
error_helpers.ShowError(ctx, err)
}
}
func startDashboardServer(ctx context.Context) (*dashboardserver.DashboardServiceState, error) {
var dashboardState *dashboardserver.DashboardServiceState
var err error
serverPort := dashboardserver.ListenPort(viper.GetInt(constants.ArgDashboardPort))
serverListen := dashboardserver.ListenType(viper.GetString(constants.ArgDashboardListen))
dashboardState, err = dashboardserver.GetDashboardServiceState()
if err != nil {
return nil, err
}
if dashboardState == nil {
// try stopping the previous service
// StopDashboardService does nothing if the service is not running
err = dashboardserver.StopDashboardService(ctx)
if err != nil {
return nil, err
}
// start dashboard service
err = dashboardserver.RunForService(ctx, serverListen, serverPort)
if err != nil {
return nil, err
}
// get the updated state
dashboardState, err = dashboardserver.GetDashboardServiceState()
if err != nil {
error_helpers.ShowWarning(fmt.Sprintf("Started Dashboard server, but could not retrieve state: %v", err))
}
}
return dashboardState, err
} }
func runServiceInForeground(ctx context.Context) { func runServiceInForeground(ctx context.Context) {
@@ -342,7 +274,6 @@ func runServiceInForeground(ctx context.Context) {
} }
case <-sigIntChannel: case <-sigIntChannel:
fmt.Print("\r") fmt.Print("\r")
dashboardserver.StopDashboardService(ctx)
// if we have received this signal, then the user probably wants to shut down // if we have received this signal, then the user probably wants to shut down
// everything. Shutdowns MUST NOT happen in cancellable contexts // everything. Shutdowns MUST NOT happen in cancellable contexts
connectedClients, err := db_local.GetClientCount(context.Background()) connectedClients, err := db_local.GetClientCount(context.Background())
@@ -387,14 +318,14 @@ func runServiceRestartCmd(cmd *cobra.Command, _ []string) {
} }
}() }()
dbStartResult, currentDashboardState := restartService(ctx) dbStartResult := restartService(ctx)
if dbStartResult != nil { if dbStartResult != nil {
printStatus(ctx, dbStartResult.DbState, dbStartResult.PluginManagerState, currentDashboardState, false) printStatus(ctx, dbStartResult.DbState, dbStartResult.PluginManagerState, false)
} }
} }
func restartService(ctx context.Context) (_ *db_local.StartResult, _ *dashboardserver.DashboardServiceState) { func restartService(ctx context.Context) (_ *db_local.StartResult) {
statushooks.Show(ctx) statushooks.Show(ctx)
defer statushooks.Done(ctx) defer statushooks.Done(ctx)
@@ -406,10 +337,6 @@ func restartService(ctx context.Context) (_ *db_local.StartResult, _ *dashboards
return return
} }
// along with the current dashboard state - maybe nil
currentDashboardState, err := dashboardserver.GetDashboardServiceState()
error_helpers.FailOnError(err)
// stop db // stop db
stopStatus, err := db_local.StopServices(ctx, viper.GetBool(constants.ArgForce), constants.InvokerService) stopStatus, err := db_local.StopServices(ctx, viper.GetBool(constants.ArgForce), constants.InvokerService)
if err != nil { if err != nil {
@@ -429,13 +356,6 @@ to force a restart.
return return
} }
// stop the running dashboard server
err = dashboardserver.StopDashboardService(ctx)
if err != nil {
exitCode = constants.ExitCodeServiceStopFailure
error_helpers.FailOnErrorWithMessage(err, "could not stop dashboard service")
}
// the DB must be installed and therefore is a noop, // the DB must be installed and therefore is a noop,
// and EnsureDBInstalled also checks and installs the latest FDW // and EnsureDBInstalled also checks and installs the latest FDW
err = db_local.EnsureDBInstalled(ctx) err = db_local.EnsureDBInstalled(ctx)
@@ -455,17 +375,7 @@ to force a restart.
return return
} }
// if the dashboard was running, start it return dbStartResult
if currentDashboardState != nil {
err = dashboardserver.RunForService(ctx, dashboardserver.ListenType(currentDashboardState.ListenType), dashboardserver.ListenPort(currentDashboardState.Port))
error_helpers.FailOnError(err)
// reload the state
currentDashboardState, err = dashboardserver.GetDashboardServiceState()
error_helpers.FailOnError(err)
}
return dbStartResult, currentDashboardState
} }
func runServiceStatusCmd(cmd *cobra.Command, _ []string) { func runServiceStatusCmd(cmd *cobra.Command, _ []string) {
@@ -488,17 +398,16 @@ func runServiceStatusCmd(cmd *cobra.Command, _ []string) {
} else { } else {
dbState, dbStateErr := db_local.GetState() dbState, dbStateErr := db_local.GetState()
pmState, pmStateErr := pluginmanager.LoadState() pmState, pmStateErr := pluginmanager.LoadState()
dashboardState, dashboardStateErr := dashboardserver.GetDashboardServiceState()
if dbStateErr != nil || pmStateErr != nil { if dbStateErr != nil || pmStateErr != nil {
error_helpers.ShowError(ctx, composeStateError(dbStateErr, pmStateErr, dashboardStateErr)) error_helpers.ShowError(ctx, composeStateError(dbStateErr, pmStateErr))
return return
} }
printStatus(ctx, dbState, pmState, dashboardState, false) printStatus(ctx, dbState, pmState, false)
} }
} }
func composeStateError(dbStateErr error, pmStateErr error, dashboardStateErr error) error { func composeStateError(dbStateErr error, pmStateErr error) error {
msg := "could not get Steampipe service status:" msg := "could not get Steampipe service status:"
if dbStateErr != nil { if dbStateErr != nil {
@@ -509,10 +418,6 @@ func composeStateError(dbStateErr error, pmStateErr error, dashboardStateErr err
msg = fmt.Sprintf(`%s msg = fmt.Sprintf(`%s
failed to get plugin manager state: %s`, msg, pmStateErr.Error()) failed to get plugin manager state: %s`, msg, pmStateErr.Error())
} }
if dashboardStateErr != nil {
msg = fmt.Sprintf(`%s
failed to get dashboard server state: %s`, msg, pmStateErr.Error())
}
return errors.New(msg) return errors.New(msg)
} }
@@ -540,9 +445,8 @@ func runServiceStopCmd(cmd *cobra.Command, _ []string) {
force := cmdconfig.Viper().GetBool(constants.ArgForce) force := cmdconfig.Viper().GetBool(constants.ArgForce)
if force { if force {
dashboardStopError := dashboardserver.StopDashboardService(ctx)
status, dbStopError = db_local.StopServices(ctx, force, constants.InvokerService) status, dbStopError = db_local.StopServices(ctx, force, constants.InvokerService)
dbStopError = error_helpers.CombineErrors(dbStopError, dashboardStopError) dbStopError = error_helpers.CombineErrors(dbStopError)
if dbStopError != nil { if dbStopError != nil {
exitCode = constants.ExitCodeServiceStopFailure exitCode = constants.ExitCodeServiceStopFailure
error_helpers.FailOnError(dbStopError) error_helpers.FailOnError(dbStopError)
@@ -554,12 +458,6 @@ func runServiceStopCmd(cmd *cobra.Command, _ []string) {
error_helpers.FailOnErrorWithMessage(dbStopError, "could not stop Steampipe service") error_helpers.FailOnErrorWithMessage(dbStopError, "could not stop Steampipe service")
} }
dashboardState, err := dashboardserver.GetDashboardServiceState()
if err != nil {
exitCode = constants.ExitCodeServiceStopFailure
error_helpers.FailOnErrorWithMessage(err, "could not stop Steampipe service")
}
if dbState == nil { if dbState == nil {
fmt.Println("Steampipe service is not running.") fmt.Println("Steampipe service is not running.")
return return
@@ -569,14 +467,6 @@ func runServiceStopCmd(cmd *cobra.Command, _ []string) {
return return
} }
if dashboardState != nil {
err = dashboardserver.StopDashboardService(ctx)
if err != nil {
exitCode = constants.ExitCodeServiceStopFailure
error_helpers.FailOnErrorWithMessage(err, "could not stop dashboard server")
}
}
// check if there are any connected clients to the service // check if there are any connected clients to the service
connectedClients, err := db_local.GetClientCount(ctx) connectedClients, err := db_local.GetClientCount(ctx)
if err != nil { if err != nil {
@@ -666,7 +556,7 @@ func getServiceProcessDetails(process *psutils.Process) (string, string, string,
return fmt.Sprintf("%d", process.Pid), installDir, port, listenType return fmt.Sprintf("%d", process.Pid), installDir, port, listenType
} }
func printStatus(ctx context.Context, dbState *db_local.RunningDBInstanceInfo, pmState *pluginmanager.State, dashboardState *dashboardserver.DashboardServiceState, alreadyRunning bool) { func printStatus(ctx context.Context, dbState *db_local.RunningDBInstanceInfo, pmState *pluginmanager.State, alreadyRunning bool) {
if dbState == nil && !pmState.Running { if dbState == nil && !pmState.Running {
fmt.Println("Service is not running") fmt.Println("Service is not running")
return return
@@ -739,25 +629,11 @@ Database:
connectionStr, connectionStr,
) )
dashboardMsg := ""
if dashboardState != nil {
browserUrl := fmt.Sprintf("http://%s:%d/", dashboardState.Listen[0], dashboardState.Port)
dashboardMsg = fmt.Sprintf(`
Dashboard:
Host(s): %v
Port: %v
URL: %v
`, strings.Join(dashboardState.Listen, ", "), dashboardState.Port, browserUrl)
}
if dbState.Invoker == constants.InvokerService { if dbState.Invoker == constants.InvokerService {
statusMessage = fmt.Sprintf( statusMessage = fmt.Sprintf(
"%s%s%s%s", "%s%s%s",
prefix, prefix,
postgresMsg, postgresMsg,
dashboardMsg,
suffix, suffix,
) )
} else { } else {

View File

@@ -1,228 +0,0 @@
package dashboardserver
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"os/exec"
"syscall"
"time"
"github.com/shirou/gopsutil/process"
"github.com/spf13/viper"
"github.com/turbot/steampipe/pkg/constants"
"github.com/turbot/steampipe/pkg/dashboard/dashboardassets"
"github.com/turbot/steampipe/pkg/error_helpers"
"github.com/turbot/steampipe/pkg/filepaths"
"github.com/turbot/steampipe/pkg/utils"
)
type ServiceState string
const (
ServiceStateRunning ServiceState = "running"
ServiceStateError ServiceState = "error"
ServiceStateStructVersion = 20220411
)
type DashboardServiceState struct {
State ServiceState `json:"state"`
Error string `json:"error"`
Pid int `json:"pid"`
Port int `json:"port"`
ListenType string `json:"listen_type"`
Listen []string `json:"listen"`
StructVersion int64 `json:"struct_version"`
}
func loadServiceStateFile() (*DashboardServiceState, error) {
state := &DashboardServiceState{}
stateBytes, err := os.ReadFile(filepaths.DashboardServiceStateFilePath())
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
err = json.Unmarshal(stateBytes, state)
return state, err
}
func (s *DashboardServiceState) Save() error {
// set struct version
s.StructVersion = ServiceStateStructVersion
versionFilePath := filepaths.DashboardServiceStateFilePath()
return s.write(versionFilePath)
}
func (s *DashboardServiceState) write(path string) error {
versionFileJSON, err := json.MarshalIndent(s, "", " ")
if err != nil {
log.Println("Error while writing version file", err)
return err
}
return os.WriteFile(path, versionFileJSON, 0644)
}
func GetDashboardServiceState() (*DashboardServiceState, error) {
state, err := loadServiceStateFile()
if err != nil {
return nil, err
}
if state == nil {
return nil, nil
}
pidExists, err := utils.PidExists(state.Pid)
if err != nil {
return nil, err
}
if !pidExists {
return nil, os.Remove(filepaths.DashboardServiceStateFilePath())
}
return state, nil
}
func StopDashboardService(ctx context.Context) error {
state, err := GetDashboardServiceState()
if err != nil {
return err
}
if state == nil {
return nil
}
pidExists, err := utils.PidExists(state.Pid)
if err != nil {
return err
}
if !pidExists {
return nil
}
process, err := process.NewProcessWithContext(ctx, int32(state.Pid))
if err != nil {
return err
}
err = process.SendSignalWithContext(ctx, syscall.SIGINT)
if err != nil {
return err
}
return os.Remove(filepaths.DashboardServiceStateFilePath())
}
// RunForService spanws an execution of the 'steampipe dashboard' command.
// It is used when starting/restarting the steampipe service with the --dashboard flag set
func RunForService(ctx context.Context, serverListen ListenType, serverPort ListenPort) error {
self, err := os.Executable()
if err != nil {
return err
}
// remove the state file (if any)
os.Remove(filepaths.DashboardServiceStateFilePath())
err = dashboardassets.Ensure(ctx)
if err != nil {
return err
}
error_helpers.FailOnError(serverPort.IsValid())
error_helpers.FailOnError(serverListen.IsValid())
// NOTE: args must be specified <arg>=<arg val>, as each entry in this array is a separate arg passed to cobra
args := []string{
"dashboard",
fmt.Sprintf("--%s=%s", constants.ArgDashboardListen, string(serverListen)),
fmt.Sprintf("--%s=%d", constants.ArgDashboardPort, serverPort),
fmt.Sprintf("--%s=%s", constants.ArgInstallDir, filepaths.SteampipeDir),
fmt.Sprintf("--%s=%s", constants.ArgModLocation, viper.GetString(constants.ArgModLocation)),
fmt.Sprintf("--%s=true", constants.ArgServiceMode),
fmt.Sprintf("--%s=false", constants.ArgInput),
}
for _, variableArg := range viper.GetStringSlice(constants.ArgVariable) {
args = append(args, fmt.Sprintf("--%s=%s", constants.ArgVariable, variableArg))
}
for _, varFile := range viper.GetStringSlice(constants.ArgVarFile) {
args = append(args, fmt.Sprintf("--%s=%s", constants.ArgVarFile, varFile))
}
cmd := exec.Command(
self,
args...,
)
cmd.Env = os.Environ()
// set group pgid attributes on the command to ensure the process is not shutdown when its parent terminates
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Foreground: false,
}
err = cmd.Start()
if err != nil {
return err
}
return waitForDashboardService(ctx)
}
// when started as a service, 'steampipe dashboard' always writes a
// state file in 'internal' with the outcome - even on failures
// this function polls for the file and loads up the error, if any
func waitForDashboardService(ctx context.Context) error {
utils.LogTime("db.waitForDashboardServerStartup start")
defer utils.LogTime("db.waitForDashboardServerStartup end")
pingTimer := time.NewTicker(constants.ServicePingInterval)
timeoutAt := time.After(time.Duration(viper.GetInt(constants.ArgDashboardStartTimeout)) * time.Second)
defer pingTimer.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-pingTimer.C:
// poll for the state file.
// when it comes up, return it
state, err := loadServiceStateFile()
if err != nil {
if os.IsNotExist(err) {
// if the file hasn't been generated yet, that means 'dashboard' is still booting up
continue
}
// there was an unexpected error
return err
}
if state == nil {
// no state file yet
continue
}
// check the state file for an error
if len(state.Error) > 0 {
// there was an error during start up
// remove the state file, since we don't need it anymore
os.Remove(filepaths.DashboardServiceStateFilePath())
// and return the error from the state file
return errors.New(state.Error)
}
// we loaded the state and there was no error
return nil
case <-timeoutAt:
return fmt.Errorf("dashboard server startup timed out")
}
}
}
func WriteServiceStateFile(state *DashboardServiceState) error {
stateBytes, err := json.MarshalIndent(state, "", " ")
if err != nil {
return err
}
return os.WriteFile(filepaths.DashboardServiceStateFilePath(), stateBytes, 0666)
}