github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/db/db_local/start_services.go (about) 1 package db_local 2 3 import ( 4 "bufio" 5 "context" 6 "fmt" 7 "log" 8 "os" 9 "os/exec" 10 "strings" 11 "sync" 12 "syscall" 13 14 "github.com/jackc/pgx/v5" 15 psutils "github.com/shirou/gopsutil/process" 16 "github.com/spf13/viper" 17 "github.com/turbot/go-kit/helpers" 18 "github.com/turbot/steampipe-plugin-sdk/v5/sperr" 19 "github.com/turbot/steampipe/pkg/constants" 20 "github.com/turbot/steampipe/pkg/db/db_common" 21 "github.com/turbot/steampipe/pkg/error_helpers" 22 "github.com/turbot/steampipe/pkg/filepaths" 23 "github.com/turbot/steampipe/pkg/pluginmanager" 24 "github.com/turbot/steampipe/pkg/statushooks" 25 "github.com/turbot/steampipe/pkg/utils" 26 ) 27 28 // StartResult is a pseudoEnum for outcomes of StartNewInstance 29 type StartResult struct { 30 error_helpers.ErrorAndWarnings 31 Status StartDbStatus 32 DbState *RunningDBInstanceInfo 33 PluginManagerState *pluginmanager.State 34 PluginManager *pluginmanager.PluginManagerClient 35 } 36 37 func (r *StartResult) SetError(err error) *StartResult { 38 r.Error = err 39 r.Status = ServiceFailedToStart 40 return r 41 } 42 43 // StartDbStatus is a pseudoEnum for outcomes of starting the db 44 type StartDbStatus int 45 46 const ( 47 // start from 1 to prevent confusion with int zero-value 48 ServiceStarted StartDbStatus = iota + 1 49 ServiceAlreadyRunning 50 ServiceFailedToStart 51 ) 52 53 // StartListenType is a pseudoEnum of network binding for postgres 54 type StartListenType string 55 56 const ( 57 // ListenTypeNetwork - bind to all known interfaces 58 ListenTypeNetwork StartListenType = "network" 59 // ListenTypeLocal - bind to localhost only 60 ListenTypeLocal = "local" 61 ) 62 63 // ToListenAddresses is transforms StartListenType known aliases into their actual value 64 func (slt StartListenType) ToListenAddresses() []string { 65 switch slt { 66 case ListenTypeNetwork: 67 return []string{"*"} 68 case ListenTypeLocal: 69 return []string{"localhost"} 70 } 71 return strings.Split(string(slt), ",") 72 } 73 74 func StartServices(ctx context.Context, listenAddresses []string, port int, invoker constants.Invoker) *StartResult { 75 utils.LogTime("db_local.StartServices start") 76 defer utils.LogTime("db_local.StartServices end") 77 78 // we want the service to always listen on IPv4 loopback 79 if !utils.ListenAddressesContainsOneOfAddresses(listenAddresses, []string{"127.0.0.1", "*", "localhost"}) { 80 log.Println("[TRACE] StartServices - prepending 127.0.0.1 to listenAddresses") 81 listenAddresses = append([]string{"127.0.0.1"}, listenAddresses...) 82 } 83 84 res := &StartResult{} 85 86 // if we were not successful, stop services again 87 defer func() { 88 if res.Status == ServiceStarted && res.Error != nil { 89 StopServices(ctx, false, invoker) 90 res.Status = ServiceFailedToStart 91 } 92 }() 93 94 res.DbState, res.Error = GetState() 95 if res.Error != nil { 96 return res 97 } 98 99 if res.DbState == nil { 100 res = startDB(ctx, listenAddresses, port, invoker) 101 if res.Error != nil { 102 return res 103 } 104 } else { 105 res.Status = ServiceAlreadyRunning 106 107 // if the service is already running, also load the state of the plugin manager 108 pluginManagerState, err := pluginmanager.LoadState() 109 if err != nil { 110 res.Error = err 111 return res 112 } 113 res.PluginManagerState = pluginManagerState 114 } 115 116 if res.Status == ServiceStarted { 117 // execute post startup setup 118 if err := postServiceStart(ctx, res); err != nil { 119 // NOTE do not update res.Status - this will be done by defer block 120 res.Error = err 121 return res 122 } 123 124 // start plugin manager if needed 125 pluginManager, pluginManagerState, err := ensurePluginManager(ctx) 126 res.PluginManagerState = pluginManagerState 127 res.PluginManager = pluginManager 128 if err != nil { 129 res.Error = err 130 return res 131 } 132 133 statushooks.SetStatus(ctx, "Service startup complete") 134 135 } 136 return res 137 } 138 139 func ensurePluginManager(ctx context.Context) (*pluginmanager.PluginManagerClient, *pluginmanager.State, error) { 140 // start the plugin manager if needed 141 state, err := pluginmanager.LoadState() 142 if err != nil { 143 return nil, nil, err 144 } 145 146 if !state.Running { 147 // get the location of the currently running steampipe process 148 executable, err := os.Executable() 149 if err != nil { 150 log.Printf("[WARN] plugin manager start() - failed to get steampipe executable path: %s", err) 151 return nil, nil, err 152 } 153 if state, err = pluginmanager.StartNewInstance(executable); err != nil { 154 log.Printf("[WARN] StartServices plugin manager failed to start: %s", err) 155 return nil, nil, err 156 } 157 } 158 client, err := pluginmanager.NewPluginManagerClient(state) 159 if err != nil { 160 return nil, state, err 161 } 162 return client, state, nil 163 } 164 165 func postServiceStart(ctx context.Context, res *StartResult) error { 166 conn, err := CreateLocalDbConnection(ctx, &CreateDbOptions{DatabaseName: res.DbState.Database, Username: constants.DatabaseSuperUser}) 167 if err != nil { 168 return err 169 } 170 defer conn.Close(ctx) 171 172 // setup internal schema 173 if err := setupInternal(ctx, conn); err != nil { 174 return err 175 } 176 177 statushooks.SetStatus(ctx, "Initialize steampipe_connection table") 178 179 // ensure connection state table contains entries for all connections in connection config 180 // (this is to allow for the race condition between polling connection state and calling refresh connections, 181 // which does not update the connection_state with added connections until it has built the ConnectionUpdates 182 if err := initializeConnectionStateTable(ctx, conn); err != nil { 183 return err 184 } 185 if err := PopulatePluginTable(ctx, conn); err != nil { 186 return err 187 } 188 189 statushooks.SetStatus(ctx, "Create steampipe_server_settings table") 190 // create the server settings table 191 // this table contains configuration that this instance of the service 192 // is booting with 193 if err := setupServerSettingsTable(ctx, conn); err != nil { 194 return err 195 } 196 197 // create the clone_foreign_schema function 198 if _, err := executeSqlAsRoot(ctx, cloneForeignSchemaSQL); err != nil { 199 return sperr.WrapWithMessage(err, "failed to create clone_foreign_schema function") 200 } 201 // create the clone_comments function 202 if _, err := executeSqlAsRoot(ctx, cloneCommentsSQL); err != nil { 203 return sperr.WrapWithMessage(err, "failed to create clone_comments function") 204 } 205 206 // if there is an unprocessed db backup file, restore it now 207 if err := restoreDBBackup(ctx); err != nil { 208 return sperr.WrapWithMessage(err, "failed to migrate db public schema") 209 } 210 211 return nil 212 } 213 214 // StartDB starts the database if not already running 215 func startDB(ctx context.Context, listenAddresses []string, port int, invoker constants.Invoker) (res *StartResult) { 216 log.Printf("[TRACE] StartDB invoker %s (listenAddresses=%s, port=%d)", invoker, listenAddresses, port) 217 utils.LogTime("db.StartDB start") 218 defer utils.LogTime("db.StartDB end") 219 var postgresCmd *exec.Cmd 220 221 res = &StartResult{} 222 defer func() { 223 if r := recover(); r != nil { 224 res.Error = helpers.ToError(r) 225 } 226 // if there was an error and we started the service, stop it again 227 if res.Error != nil { 228 if res.Status == ServiceStarted { 229 StopServices(ctx, false, invoker) 230 } 231 // remove the state file if we are going back with an error 232 removeRunningInstanceInfo() 233 // we are going back with an error 234 // if the process was started, 235 if postgresCmd != nil && postgresCmd.Process != nil { 236 // kill it 237 postgresCmd.Process.Kill() 238 } 239 } 240 }() 241 242 // remove the stale info file, ignoring errors - will overwrite anyway 243 _ = removeRunningInstanceInfo() 244 245 if err := utils.EnsureDirectoryPermission(filepaths.GetDataLocation()); err != nil { 246 return res.SetError(fmt.Errorf("%s does not have the necessary permissions to start the service", filepaths.GetDataLocation())) 247 } 248 249 // Remove any old and expiring certificates 250 if err := removeExpiringSelfIssuedCertificates(); err != nil { 251 error_helpers.ShowWarning("failed to remove expired certificates") 252 log.Println("[TRACE] failed to remove expired certificates", err) 253 } 254 255 // Generate the certificate if it fails then set the ssl to off 256 if err := ensureCertificates(); err != nil { 257 error_helpers.ShowWarning("self signed certificate creation failed, connecting to the database without SSL") 258 } 259 260 if err := utils.IsPortBindable(utils.GetFirstListenAddress(listenAddresses), port); err != nil { 261 return res.SetError(fmt.Errorf("cannot listen on port %d and %s %s. To check if there's any other steampipe services running, use %s", constants.Bold(port), utils.Pluralize("address", len(listenAddresses)), constants.Bold(strings.Join(listenAddresses, ",")), constants.Bold("steampipe service status --all"))) 262 } 263 264 if err := migrateLegacyPasswordFile(); err != nil { 265 return res.SetError(err) 266 } 267 268 password, err := resolvePassword() 269 if err != nil { 270 return res.SetError(err) 271 } 272 273 postgresCmd, err = startPostgresProcess(ctx, listenAddresses, port, invoker) 274 if err != nil { 275 return res.SetError(err) 276 } 277 278 // create a RunningInfo with empty database name 279 // we need this to connect to the service using 'root', required retrieve the name of the installed database 280 res.DbState = newRunningDBInstanceInfo(postgresCmd, listenAddresses, port, "", password, invoker) 281 err = res.DbState.Save() 282 if err != nil { 283 return res.SetError(err) 284 } 285 286 databaseName, err := getDatabaseName(ctx, port) 287 if err != nil { 288 return res.SetError(err) 289 } 290 291 res.DbState, err = updateDatabaseNameInRunningInfo(ctx, databaseName) 292 if err != nil { 293 return res.SetError(err) 294 } 295 296 err = setServicePassword(ctx, password) 297 if err != nil { 298 return res.SetError(err) 299 } 300 301 err = ensureService(ctx, databaseName) 302 if err != nil { 303 return res.SetError(err) 304 } 305 306 // release the process - let the OS adopt it, so that we can exit 307 err = postgresCmd.Process.Release() 308 if err != nil { 309 return res.SetError(err) 310 } 311 312 utils.LogTime("postgresCmd end") 313 res.Status = ServiceStarted 314 return res 315 } 316 317 func ensureService(ctx context.Context, databaseName string) error { 318 connection, err := CreateLocalDbConnection(ctx, &CreateDbOptions{DatabaseName: databaseName, Username: constants.DatabaseSuperUser}) 319 if err != nil { 320 return err 321 } 322 defer connection.Close(ctx) 323 324 // ensure the foreign server exists in the database 325 err = ensureSteampipeServer(ctx, connection) 326 if err != nil { 327 return err 328 } 329 330 // ensure that the necessary extensions are installed in the database 331 err = ensurePgExtensions(ctx, connection) 332 if err != nil { 333 // there was a problem with the installation 334 return err 335 } 336 337 // ensure permissions for writing to temp tables 338 err = ensureTempTablePermissions(ctx, databaseName, connection) 339 if err != nil { 340 return err 341 } 342 343 return nil 344 } 345 346 // getDatabaseName connects to the service and retrieves the database name 347 func getDatabaseName(ctx context.Context, port int) (string, error) { 348 databaseName, err := retrieveDatabaseNameFromService(ctx, port) 349 if err != nil { 350 return "", err 351 } 352 if len(databaseName) == 0 { 353 return "", fmt.Errorf("could not find database to connect to") 354 } 355 return databaseName, nil 356 } 357 358 func resolvePassword() (string, error) { 359 // get the password from the password file 360 password, err := readPasswordFile() 361 if err != nil { 362 return "", err 363 } 364 365 // if a password was set through the `STEAMPIPE_DATABASE_PASSWORD` environment variable 366 // or through the `--database-password` cmdline flag, then use that for this session 367 // instead of the default one 368 if viper.IsSet(constants.ArgServicePassword) { 369 password = viper.GetString(constants.ArgServicePassword) 370 } 371 return password, nil 372 } 373 374 func startPostgresProcess(ctx context.Context, listenAddresses []string, port int, invoker constants.Invoker) (*exec.Cmd, error) { 375 if error_helpers.IsContextCanceled(ctx) { 376 return nil, ctx.Err() 377 } 378 379 if err := writePGConf(ctx); err != nil { 380 return nil, err 381 } 382 383 postgresCmd := createCmd(ctx, port, listenAddresses) 384 log.Printf("[TRACE] startPostgresProcess - postgres command: %s", postgresCmd) 385 386 setupLogCollection(postgresCmd) 387 err := postgresCmd.Start() 388 if err != nil { 389 return nil, err 390 } 391 392 return postgresCmd, nil 393 } 394 395 func retrieveDatabaseNameFromService(ctx context.Context, port int) (string, error) { 396 connection, err := createMaintenanceClient(ctx, port) 397 if err != nil { 398 return "", fmt.Errorf("failed to connect to the database: %v - please try again or reset your steampipe database", err) 399 } 400 defer connection.Close(ctx) 401 402 out := connection.QueryRow(ctx, "select datname from pg_database where datistemplate=false AND datname <> 'postgres';") 403 404 var databaseName string 405 err = out.Scan(&databaseName) 406 if err != nil { 407 return "", err 408 } 409 410 return databaseName, nil 411 } 412 413 func writePGConf(ctx context.Context) error { 414 // Apply default settings in conf files 415 err := os.WriteFile(filepaths.GetPostgresqlConfLocation(), []byte(constants.PostgresqlConfContent), 0600) 416 if err != nil { 417 return err 418 } 419 err = os.WriteFile(filepaths.GetSteampipeConfLocation(), []byte(constants.SteampipeConfContent), 0600) 420 if err != nil { 421 return err 422 } 423 424 // create the postgresql.conf.d location, don't fail if it errors 425 err = os.MkdirAll(filepaths.GetPostgresqlConfDLocation(), 0700) 426 if err != nil { 427 return err 428 } 429 return nil 430 } 431 432 func updateDatabaseNameInRunningInfo(ctx context.Context, databaseName string) (*RunningDBInstanceInfo, error) { 433 runningInfo, err := loadRunningInstanceInfo() 434 if err != nil { 435 return runningInfo, err 436 } 437 runningInfo.Database = databaseName 438 return runningInfo, runningInfo.Save() 439 } 440 441 func createCmd(ctx context.Context, port int, listenAddresses []string) *exec.Cmd { 442 postgresCmd := exec.Command( 443 filepaths.GetPostgresBinaryExecutablePath(), 444 // by this time, we are sure that the port is free to listen to 445 "-p", fmt.Sprint(port), 446 "-c", fmt.Sprintf("listen_addresses=%s", strings.Join(listenAddresses, ",")), 447 "-c", fmt.Sprintf("application_name=%s", constants.AppName), 448 "-c", fmt.Sprintf("cluster_name=%s", constants.AppName), 449 450 // log directory 451 "-c", fmt.Sprintf("log_directory=%s", filepaths.EnsureLogDir()), 452 453 // If ssl is off it doesnot matter what we pass in the ssl_cert_file and ssl_key_file 454 // SSL will only get validated if ssl is on 455 "-c", fmt.Sprintf("ssl=%s", sslStatus()), 456 "-c", fmt.Sprintf("ssl_cert_file=%s", filepaths.GetServerCertLocation()), 457 "-c", fmt.Sprintf("ssl_key_file=%s", filepaths.GetServerCertKeyLocation()), 458 459 // Data Directory 460 "-D", filepaths.GetDataLocation()) 461 462 if sslpassword := viper.GetString(constants.ArgDatabaseSSLPassword); sslpassword != "" { 463 postgresCmd.Args = append( 464 postgresCmd.Args, 465 "-c", fmt.Sprintf("ssl_passphrase_command_supports_reload=%s", "true"), 466 "-c", fmt.Sprintf("ssl_passphrase_command=%s", "echo "+sslpassword), 467 ) 468 } 469 470 postgresCmd.Env = append(os.Environ(), fmt.Sprintf("STEAMPIPE_INSTALL_DIR=%s", filepaths.SteampipeDir)) 471 472 // Check if the /etc/ssl directory exist in os 473 dirExist, _ := os.Stat(constants.SslConfDir) 474 _, envVariableExist := os.LookupEnv("OPENSSL_CONF") 475 476 // This is particularly required for debian:buster 477 // https://github.com/kelaberetiv/TagUI/issues/787 478 // For other os the env variable OPENSSL_CONF 479 // does not matter so its safe to put 480 // this in env variable 481 // Tested in amazonlinux, debian:buster, ubuntu, mac 482 if dirExist != nil && !envVariableExist { 483 postgresCmd.Env = append(os.Environ(), fmt.Sprintf("OPENSSL_CONF=%s", constants.SslConfDir)) 484 } 485 486 // set group pgid attributes on the command to ensure the process is not shutdown when its parent terminates 487 postgresCmd.SysProcAttr = &syscall.SysProcAttr{ 488 Setpgid: true, 489 Foreground: false, 490 } 491 492 return postgresCmd 493 } 494 495 func setupLogCollection(cmd *exec.Cmd) { 496 logChannel, stopListenFn, err := setupLogCollector(cmd) 497 if err == nil { 498 go traceoutServiceLogs(logChannel, stopListenFn) 499 } else { 500 // this is a convenience and therefore, we shouldn't error out if we 501 // are not able to capture the logs. 502 // instead, log to TRACE that we couldn't and continue 503 log.Println("[TRACE] Warning: Could not attach to service logs") 504 } 505 } 506 507 func traceoutServiceLogs(logChannel chan string, stopLogStreamFn func()) { 508 for logLine := range logChannel { 509 log.Printf("[TRACE] SERVICE: %s\n", logLine) 510 if strings.Contains(logLine, "Future log output will appear in") { 511 stopLogStreamFn() 512 break 513 } 514 } 515 } 516 517 func setServicePassword(ctx context.Context, password string) error { 518 connection, err := CreateLocalDbConnection(ctx, &CreateDbOptions{DatabaseName: "postgres", Username: constants.DatabaseSuperUser}) 519 if err != nil { 520 return err 521 } 522 defer connection.Close(ctx) 523 statements := []string{ 524 "LOCK TABLE pg_user IN SHARE ROW EXCLUSIVE MODE;", 525 fmt.Sprintf(`ALTER USER steampipe WITH PASSWORD '%s';`, password), 526 } 527 _, err = ExecuteSqlInTransaction(ctx, connection, statements...) 528 return err 529 } 530 531 func setupLogCollector(postgresCmd *exec.Cmd) (chan string, func(), error) { 532 var publishChannel chan string 533 534 stdoutPipe, err := postgresCmd.StdoutPipe() 535 if err != nil { 536 return nil, nil, err 537 } 538 stderrPipe, err := postgresCmd.StderrPipe() 539 if err != nil { 540 return nil, nil, err 541 } 542 closeFunction := func() { 543 // close the sources to make sure they don't send anymore data 544 stdoutPipe.Close() 545 stderrPipe.Close() 546 547 // always close from the sender 548 close(publishChannel) 549 } 550 stdoutScanner := bufio.NewScanner(stdoutPipe) 551 stderrScanner := bufio.NewScanner(stderrPipe) 552 553 stdoutScanner.Split(bufio.ScanLines) 554 stderrScanner.Split(bufio.ScanLines) 555 556 // create a channel with a big buffer, so that it doesn't choke 557 publishChannel = make(chan string, 1000) 558 559 go func() { 560 for stdoutScanner.Scan() { 561 line := stdoutScanner.Text() 562 if len(line) > 0 { 563 publishChannel <- line 564 } 565 } 566 }() 567 568 go func() { 569 for stderrScanner.Scan() { 570 line := stderrScanner.Text() 571 if len(line) > 0 { 572 publishChannel <- line 573 } 574 } 575 }() 576 577 return publishChannel, closeFunction, nil 578 } 579 580 // ensures that the necessary extensions are installed on the database 581 func ensurePgExtensions(ctx context.Context, rootClient *pgx.Conn) error { 582 extensions := []string{ 583 "tablefunc", 584 "ltree", 585 } 586 587 var errors []error 588 for _, extn := range extensions { 589 _, err := rootClient.Exec(ctx, fmt.Sprintf("create extension if not exists %s", db_common.PgEscapeName(extn))) 590 if err != nil { 591 errors = append(errors, err) 592 } 593 } 594 return error_helpers.CombineErrors(errors...) 595 } 596 597 // ensures that the 'steampipe' foreign server exists 598 // 599 // (re)install FDW and creates server if it doesn't 600 func ensureSteampipeServer(ctx context.Context, rootClient *pgx.Conn) error { 601 res := rootClient.QueryRow(ctx, "select srvname from pg_catalog.pg_foreign_server where srvname='steampipe'") 602 603 var serverName string 604 err := res.Scan(&serverName) 605 // if there is an error, we need to reinstall the foreign server 606 if err != nil { 607 return installForeignServer(ctx, rootClient) 608 } 609 return nil 610 } 611 612 // ensures that the 'steampipe_users' role has permissions to work with temporary tables 613 // this is done during database installation, but we need to migrate current installations 614 func ensureTempTablePermissions(ctx context.Context, databaseName string, rootClient *pgx.Conn) error { 615 statements := []string{ 616 "lock table pg_namespace;", 617 fmt.Sprintf("grant temporary on database %s to %s", databaseName, constants.DatabaseUser), 618 } 619 if _, err := ExecuteSqlInTransaction(ctx, rootClient, statements...); err != nil { 620 return err 621 } 622 return nil 623 } 624 625 // kill all postgres processes that were started as part of steampipe (if any) 626 func killInstanceIfAny(ctx context.Context) bool { 627 processes, err := FindAllSteampipePostgresInstances(ctx) 628 if err != nil { 629 return false 630 } 631 wg := sync.WaitGroup{} 632 for _, process := range processes { 633 wg.Add(1) 634 go func(p *psutils.Process) { 635 doThreeStepPostgresExit(ctx, p) 636 wg.Done() 637 }(process) 638 } 639 wg.Wait() 640 return len(processes) > 0 641 } 642 643 func FindAllSteampipePostgresInstances(ctx context.Context) ([]*psutils.Process, error) { 644 var instances []*psutils.Process 645 allProcesses, err := psutils.ProcessesWithContext(ctx) 646 if err != nil { 647 return nil, err 648 } 649 for _, p := range allProcesses { 650 cmdLine, err := p.CmdlineSliceWithContext(ctx) 651 if err != nil { 652 return nil, err 653 } 654 if isSteampipePostgresProcess(ctx, cmdLine) { 655 instances = append(instances, p) 656 } 657 } 658 return instances, nil 659 } 660 661 func isSteampipePostgresProcess(ctx context.Context, cmdline []string) bool { 662 if len(cmdline) < 1 { 663 return false 664 } 665 if strings.Contains(cmdline[0], "postgres") { 666 // this is a postgres process - but is it a steampipe service? 667 return helpers.StringSliceContains(cmdline, fmt.Sprintf("application_name=%s", constants.AppName)) 668 } 669 return false 670 }