Files
steampipe/db/start_database.go

405 lines
12 KiB
Go

package db
import (
"fmt"
"log"
"net"
"os"
"os/exec"
"strings"
"syscall"
psutils "github.com/shirou/gopsutil/process"
"github.com/turbot/go-kit/helpers"
"github.com/turbot/steampipe/constants"
"github.com/turbot/steampipe/display"
)
// StartResult :: pseudoEnum for outcomes of Start
type StartResult int
// StartListenType :: pseudoEnum of network binding for postgres
type StartListenType string
// Invoker :: pseudoEnum for what starts the service
type Invoker string
const (
// ServiceStarted :: StartResult - Service was started
ServiceStarted StartResult = iota
// ServiceAlreadyRunning :: StartResult - Service was already running
ServiceAlreadyRunning
// ServiceFailedToStart :: StartResult - Could not start service
ServiceFailedToStart
)
const (
// ListenTypeNetwork :: StartListenType - bind to all known interfaces
ListenTypeNetwork StartListenType = "network"
// ListenTypeLocal :: StartListenType - bind to localhost only
ListenTypeLocal = "local"
)
const (
// InvokerService :: Invoker - when invoked by `service start`
InvokerService Invoker = "service"
// InvokerQuery :: Invoker - when invoked by `query`
InvokerQuery = "query"
// InvokerCheck :: Invoker - when invoked by `check`
InvokerCheck = "check"
// InvokerInstaller :: Invoker - when invoked by the `installer`
InvokerInstaller = "installer"
// InvokerPlugin :: Invoker - when invoked by the `pluginmanager`
InvokerPlugin = "plugin"
)
// IsValid :: validator for StartListenType known values
func (slt StartListenType) IsValid() error {
switch slt {
case ListenTypeNetwork, ListenTypeLocal:
return nil
}
return fmt.Errorf("Invalid listen type. Can be one of '%v' or '%v'", ListenTypeNetwork, ListenTypeLocal)
}
// IsValid :: validator for Invoker known values
func (slt Invoker) IsValid() error {
switch slt {
case InvokerService, InvokerQuery, InvokerCheck, InvokerInstaller, InvokerPlugin:
return nil
}
return fmt.Errorf("Invalid invoker. Can be one of '%v', '%v', '%v', '%v' or '%v'", InvokerService, InvokerQuery, InvokerInstaller, InvokerPlugin, InvokerCheck)
}
// StartDB :: start the database is not already running
func StartDB(port int, listen StartListenType, invoker Invoker, refreshConnections bool) (startResult StartResult, err error) {
var client *Client
defer func() {
// if there was an error and we started the service, stop it again
if err != nil {
if startResult == ServiceStarted {
StopDB(false, invoker)
}
}
if client != nil {
client.Close()
}
}()
info, err := GetStatus()
if err != nil {
return ServiceFailedToStart, err
}
if info != nil {
// check whether the stated PID actually exists
processRunning, err := PidExists(info.Pid)
if err != nil {
return ServiceFailedToStart, err
}
// Process with declared PID exists.
// Check if the service was started by another `service` command
// if not, throw an error.
if processRunning {
if info.Invoker != InvokerService {
return ServiceAlreadyRunning, fmt.Errorf("You have a %s session open. Close this session before running %s.\nTo force kill all existing sessions, run %s", constants.Bold(fmt.Sprintf("steampipe %s", info.Invoker)), constants.Bold("steampipe service start"), constants.Bold("steampipe service stop --force"))
}
return ServiceAlreadyRunning, nil
}
}
// we need to start the process
// remove the stale info file, ignoring errors - will overwrite anyway
_ = removeRunningInstanceInfo()
listenAddresses := "localhost"
if listen == ListenTypeNetwork {
listenAddresses = "*"
}
if !isPortBindable(port) {
return ServiceFailedToStart, fmt.Errorf("Cannot listen on port %d. To start the service with a different port, use %s", constants.Bold(port), constants.Bold("--database-port <number>"))
}
postgresCmd := exec.Command(
getPostgresBinaryExecutablePath(),
// by this time, we are sure that the port if free to listen to
"-p", fmt.Sprint(port),
"-c", fmt.Sprintf("listen_addresses=\"%s\"", listenAddresses),
// NOTE: If quoted, the application name includes the quotes. Worried about
// having spaces in the APPNAME, but leaving it unquoted since currently
// the APPNAME is hardcoded to be steampipe.
"-c", fmt.Sprintf("application_name=%s", constants.APPNAME),
"-c", fmt.Sprintf("cluster_name=%s", constants.APPNAME),
"-c", "autovacuum=off",
"-c", "bgwriter_lru_maxpages=0",
"-c", "effective-cache-size=64kB",
"-c", "fsync=off",
"-c", "full_page_writes=off",
"-c", "maintenance-work-mem=1024kB",
"-c", "password_encryption=scram-sha-256",
"-c", "random-page-cost=0.01",
"-c", "seq-page-cost=0.01",
// If the shared buffers are too small then large tables in memory can create
// "no unpinned buffers available" errors.
// "-c", "shared-buffers=128kB",
// If synchronous_commit=off then the setup process can fail because the
// installation of the foreign server is not committed before the DB shutsdown.
// Steampipe does very few commits in general, so leaving this on will have
// very little impact on performance.
// "-c", "synchronous_commit=off",
"-c", "temp-buffers=800kB",
"-c", "timezone=UTC",
"-c", "track_activities=off",
"-c", "track_counts=off",
"-c", "wal-buffers=32kB",
"-c", "work-mem=64kB",
"-c", "jit=off",
// postgres log collection
"-c", "log_statement=all",
"-c", "log_min_duration_statement=2000",
"-c", "logging_collector=on",
"-c", "log_min_error_statement=error",
"-c", fmt.Sprintf("log_directory=%s", constants.LogDir()),
"-c", fmt.Sprintf("log_filename=%s", "database-%Y-%m-%d.log"),
// Data Directory
"-D", getDataLocation())
postgresCmd.Env = append(os.Environ(), fmt.Sprintf("STEAMPIPE_INSTALL_DIR=%s", constants.SteampipeDir))
log.Println("[TRACE] postgres start command: ", postgresCmd.String())
postgresCmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Foreground: false,
}
err = postgresCmd.Start()
if err != nil {
return ServiceFailedToStart, err
}
// get the password file
passwords, err := getPasswords()
runningInfo := new(RunningDBInstanceInfo)
runningInfo.Pid = postgresCmd.Process.Pid
runningInfo.Port = port
runningInfo.User = constants.DatabaseUser
runningInfo.Password = passwords.Steampipe
runningInfo.Database = constants.DatabaseName
runningInfo.ListenType = listen
runningInfo.Invoker = invoker
runningInfo.Listen = constants.DatabaseListenAddresses
if listen == ListenTypeNetwork {
addrs, _ := localAddresses()
runningInfo.Listen = append(runningInfo.Listen, addrs...)
}
if err := postgresCmd.Process.Release(); err != nil {
return ServiceStarted, err
}
if err := saveRunningInstanceInfo(runningInfo); err != nil {
return ServiceStarted, err
}
// create a client
// pass 'false' to disable auto refreshing connections
//- we will explicitly refresh connections after ensuring the steampipe server exists
client, err = NewClient(false)
if err != nil {
return ServiceFailedToStart, handleStartFailure(err)
}
err = ensureSteampipeServer()
if err != nil {
// there was a problem with the installation
StopDB(true, invoker)
return ServiceFailedToStart, err
}
err = ensureTempTablePermissions()
if err != nil {
// there was a problem with the installation
StopDB(true, invoker)
return ServiceFailedToStart, err
}
// refresh plugin connections - ensure db schemas are in sync with connection config
// NOTE: refresh defaults to true but will be set to false if this service start command has been invoked
// internally by a different command which needs the service
if refreshConnections {
if _, err = client.RefreshConnections(); err != nil {
return ServiceStarted, err
}
if err = refreshFunctions(); err != nil {
return ServiceStarted, err
}
}
err = client.SetServiceSearchPath()
return ServiceStarted, err
}
// ensures that the `steampipe` fdw server exists
// checks for it - (re)install FDW and creates server if it doesn't
func ensureSteampipeServer() error {
rootClient, err := createSteampipeRootDbClient()
if err != nil {
return err
}
defer rootClient.Close()
out := rootClient.QueryRow("select srvname from pg_catalog.pg_foreign_server where srvname='steampipe'")
var serverName string
err = out.Scan(&serverName)
if err != nil {
return installSteampipeHub()
}
return nil
}
// ensures that the `steampipe_users` role has permissions to work with temporary tables
// this is done during database installation, but we need to migrate current installations
func ensureTempTablePermissions() error {
rootClient, err := createSteampipeRootDbClient()
if err != nil {
return err
}
defer rootClient.Close()
_, err = rootClient.Exec("grant temporary on database steampipe to steampipe_users")
if err != nil {
return err
}
return nil
}
func handleStartFailure(err error) error {
// if we got an error here, then there probably was a problem
// starting up the process. this may be because of a stray
// steampipe postgres running or another one from a different installation.
checkedPreviousInstances := make(chan bool, 1)
s := display.StartSpinnerAfterDelay("Checking for running instances...", constants.SpinnerShowTimeout, checkedPreviousInstances)
otherProcess := findSteampipePostgresInstance()
close(checkedPreviousInstances)
display.StopSpinner(s)
if otherProcess != nil {
return fmt.Errorf("Another Steampipe service is already running. Use %s to kill all running instances before continuing.", constants.Bold("steampipe service stop --force"))
}
// there was nothing to kill.
// this is some other problem that we are not accounting for
return err
}
func isPortBindable(port int) bool {
l, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
return false
}
defer l.Close()
return true
}
// kill all postgres processes that were started as part of steampipe (if any)
func killInstanceIfAny() bool {
processes, err := FindAllSteampipePostgresInstances()
if err != nil {
return false
}
for _, process := range processes {
killProcessTree(process)
}
return len(processes) > 0
}
func FindAllSteampipePostgresInstances() ([]*psutils.Process, error) {
instances := []*psutils.Process{}
allProcesses, err := psutils.Processes()
if err != nil {
return nil, err
}
for _, p := range allProcesses {
if cmdLine, err := p.CmdlineSlice(); err == nil {
if isSteampipePostgresProcess(cmdLine) {
instances = append(instances, p)
}
} else {
return nil, err
}
}
return instances, nil
}
func findSteampipePostgresInstance() *psutils.Process {
allProcesses, _ := psutils.Processes()
for _, p := range allProcesses {
cmdLine, _ := p.CmdlineSlice()
if isSteampipePostgresProcess(cmdLine) {
return p
}
}
return nil
}
func isSteampipePostgresProcess(cmdline []string) bool {
if len(cmdline) < 1 {
return false
}
if strings.Contains(cmdline[0], "postgres") {
// this is a postgres process - but is it a steampipe service?
return helpers.StringSliceContains(cmdline, fmt.Sprintf("application_name=%s", constants.APPNAME))
}
return false
}
func killProcessTree(p *psutils.Process) error {
// find it's children
children, err := p.Children()
if err != nil {
return err
}
for _, child := range children {
// and kill them first
killProcessTree(child)
}
p.Kill()
return nil
}
func localAddresses() ([]string, error) {
addresses := []string{}
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
for _, i := range ifaces {
addrs, err := i.Addrs()
if err != nil {
continue
}
for _, a := range addrs {
switch v := a.(type) {
case *net.IPNet:
isToInclude := v.IP.IsGlobalUnicast() && (v.IP.To4() != nil)
if isToInclude {
addresses = append(addresses, v.IP.String())
}
}
}
}
return addresses, nil
}