mirror of
https://github.com/turbot/steampipe.git
synced 2026-02-18 04:00:20 -05:00
407 lines
13 KiB
Go
407 lines
13 KiB
Go
package db
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/turbot/steampipe-plugin-sdk/logging"
|
|
"github.com/turbot/steampipe/cmdconfig"
|
|
"github.com/turbot/steampipe/constants"
|
|
"github.com/turbot/steampipe/ociinstaller"
|
|
"github.com/turbot/steampipe/ociinstaller/versionfile"
|
|
"github.com/turbot/steampipe/utils"
|
|
)
|
|
|
|
var ensureMux sync.Mutex
|
|
|
|
// EnsureDBInstalled makes sure that the embedded pg database is installed and running
|
|
func EnsureDBInstalled() {
|
|
logging.LogTime("setup start")
|
|
log.Println("[TRACE] ensure installed")
|
|
|
|
ensureMux.Lock()
|
|
defer func() {
|
|
log.Println("[TRACE] ensured installed")
|
|
logging.LogTime("setup end")
|
|
ensureMux.Unlock()
|
|
}()
|
|
|
|
if IsInstalled() {
|
|
// check if FDW needs to be updated
|
|
if fdwNeedsUpdate() {
|
|
_, err := installFDW(false)
|
|
if err != nil {
|
|
utils.ShowError(fmt.Errorf("%s could not be updated", constants.Bold("steampipe-postgres-fdw")))
|
|
} else {
|
|
fmt.Printf("%s was updated to %s. ", constants.Bold("steampipe-postgres-fdw"), constants.Bold(constants.FdwVersion))
|
|
currentStatus, err := GetStatus()
|
|
if err != nil || currentStatus != nil {
|
|
fmt.Printf("Run %s for change to take effect.", constants.Bold("steampipe service restart"))
|
|
}
|
|
fmt.Println()
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
log.Println("[TRACE] calling killPreviousInstanceIfAny")
|
|
killPreviousSpinner := utils.ShowSpinner(fmt.Sprintf("Cleanup any Steampipe processes..."))
|
|
killPreviousInstanceIfAny()
|
|
log.Println("[TRACE] calling removeRunningInstanceInfo")
|
|
err := removeRunningInstanceInfo()
|
|
utils.StopSpinner(killPreviousSpinner)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
utils.FailOnErrorWithMessage(err, "x Cleanup any Steampipe processes... FAILED!")
|
|
}
|
|
|
|
log.Println("[TRACE] removing previous installation")
|
|
dbCleanupSpinner := utils.ShowSpinner(fmt.Sprintf("Prepare database install location..."))
|
|
err = os.RemoveAll(getDatabaseLocation())
|
|
if err != nil {
|
|
utils.StopSpinner(dbCleanupSpinner)
|
|
utils.FailOnErrorWithMessage(err, "x Prepare database install location... FAILED!")
|
|
}
|
|
err = os.RemoveAll(getDataLocation())
|
|
if err != nil {
|
|
utils.StopSpinner(dbCleanupSpinner)
|
|
utils.FailOnErrorWithMessage(err, "x Prepare database install location... FAILED!")
|
|
}
|
|
utils.StopSpinner(dbCleanupSpinner)
|
|
|
|
dbInstallSpinner := utils.ShowSpinner(fmt.Sprintf("Download & install embedded PostgreSQL database..."))
|
|
_, err = ociinstaller.InstallDB(constants.DefaultEmbeddedPostgresImage, getDatabaseLocation())
|
|
utils.StopSpinner(dbInstallSpinner)
|
|
if err != nil {
|
|
utils.FailOnErrorWithMessage(err, "x Download & install embedded PostgreSQL database... FAILED!")
|
|
}
|
|
|
|
// installFDW takes care of the spinner, since it may need to run independently
|
|
_, err = installFDW(true)
|
|
|
|
dbInitSpinner := utils.ShowSpinner(fmt.Sprintf("Initializing database..."))
|
|
err = initDatabase()
|
|
utils.StopSpinner(dbInitSpinner)
|
|
if err != nil {
|
|
utils.FailOnErrorWithMessage(err, "x Initializing database... FAILED!")
|
|
}
|
|
|
|
pwSpinner := utils.ShowSpinner(fmt.Sprintf("Generating database passwords..."))
|
|
// Try for passwords of the form dbC-3Ji-d04d
|
|
steampipePassword := generatePassword()
|
|
rootPassword := generatePassword()
|
|
// write the passwords that were generated
|
|
err = writePasswordFile(steampipePassword, rootPassword)
|
|
utils.StopSpinner(pwSpinner)
|
|
if err != nil {
|
|
utils.FailOnErrorWithMessage(err, "x Generating database passwords... FAILED!")
|
|
}
|
|
|
|
startServiceSpinner := utils.ShowSpinner(fmt.Sprintf("Configuring database..."))
|
|
StartService(InvokerInstaller)
|
|
err = installSteampipeDatabase(steampipePassword, rootPassword)
|
|
utils.StopSpinner(startServiceSpinner)
|
|
if err != nil {
|
|
utils.FailOnErrorWithMessage(err, "x Configuring database... FAILED!")
|
|
}
|
|
|
|
installSteampipeSpinner := utils.ShowSpinner(fmt.Sprintf("Configuring Steampipe..."))
|
|
err = installSteampipeHub()
|
|
utils.StopSpinner(installSteampipeSpinner)
|
|
if err != nil {
|
|
utils.FailOnErrorWithMessage(err, "x Configuring Steampipe... FAILED!")
|
|
}
|
|
// force stop
|
|
StopDB(true, InvokerInstaller)
|
|
|
|
// write a signature after everything gets done!
|
|
// so that we can check for this later on
|
|
writeSignaturesSpinner := utils.ShowSpinner(fmt.Sprintf("Updating install records..."))
|
|
err = updateDownloadedBinarySignature()
|
|
utils.StopSpinner(writeSignaturesSpinner)
|
|
if err != nil {
|
|
utils.FailOnErrorWithMessage(err, "x Updating install records... FAILED!")
|
|
}
|
|
}
|
|
|
|
func installSteampipeDatabase(withSteampipePassword string, withRootPassword string) error {
|
|
rawClient, err := createPostgresDbClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
rawClient.Close()
|
|
}()
|
|
|
|
statements := []string{
|
|
|
|
// Lockdown all existing, and future, databases from use.
|
|
`revoke all on database postgres from public`,
|
|
`revoke all on database template1 from public`,
|
|
|
|
// Only the root user (who owns the postgres database) should be able to use
|
|
// or change it.
|
|
`revoke all privileges on schema public from public`,
|
|
|
|
// Create the steampipe database, used to hold all steampipe tables, views and data.
|
|
`create database steampipe`,
|
|
|
|
// Restrict permissions from general users to the steampipe database. We add them
|
|
// back progressively to allow appropriate read only access.
|
|
`revoke all on database steampipe from public`,
|
|
|
|
// The root user gets full rights to the steampipe database, ensuring we can actually
|
|
// configure and manage it properly.
|
|
`grant all on database steampipe to root`,
|
|
|
|
// The root user gets a password which will be used later on to connect
|
|
fmt.Sprintf(`alter user root with password '%s'`, withRootPassword),
|
|
|
|
//
|
|
// PERMISSIONS
|
|
//
|
|
// References:
|
|
// * https://dba.stackexchange.com/questions/117109/how-to-manage-default-privileges-for-users-on-a-database-vs-schema/117661#117661
|
|
//
|
|
|
|
// Create a role to represent all steampipe_users in the database.
|
|
// Grants and permissions can be managed on this role independent
|
|
// of the actual users in the system, giving us flexibility.
|
|
`create role steampipe_users`,
|
|
|
|
// Allow the steampipe user access to the steampipe database only
|
|
`grant connect on database steampipe to steampipe_users`,
|
|
|
|
// Create the steampipe user. By default they do not have superuser, createdb
|
|
// or createrole permissions.
|
|
`create user steampipe`,
|
|
|
|
// Set a random, complex password for the steampipe user. Done as a separate
|
|
// step from the create for clarity and reuse.
|
|
// TODO: need a complex random password here, that is available for sharing with the user when the do steampipe service
|
|
fmt.Sprintf(`alter user steampipe with password '%s'`, withSteampipePassword),
|
|
|
|
// Allow steampipe the privileges of steampipe_users.
|
|
`grant steampipe_users to steampipe`,
|
|
}
|
|
|
|
for _, statement := range statements {
|
|
// NOTE: This may print a password to the log file, but it doesn't matter
|
|
// since the password is stored in a config file anyway.
|
|
log.Println("[TRACE] Install steampipe database: ", statement)
|
|
if _, err := rawClient.Exec(statement); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func installSteampipeHub() error {
|
|
rawClient, err := createSteampipeRootDbClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
rawClient.Close()
|
|
}()
|
|
|
|
statements := []string{
|
|
// Install the FDW. The name must match the binary file.
|
|
`drop extension if exists "steampipe_postgres_fdw" cascade`,
|
|
`create extension if not exists "steampipe_postgres_fdw"`,
|
|
// Use steampipe for the server name, it's simplest
|
|
`create server "steampipe" foreign data wrapper "steampipe_postgres_fdw"`,
|
|
}
|
|
|
|
for _, statement := range statements {
|
|
log.Println("[TRACE] Install steampipe hub: ", statement)
|
|
if _, err := rawClient.Exec(statement); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// StartService :: invokes `steampipe service start --listen local --refresh=false --invoker query`
|
|
func StartService(invoker Invoker) {
|
|
log.Println("[TRACE] start service")
|
|
// spawn a process to start the service, passing refresh=false to ensure we DO NOT refresh connections
|
|
// (as we will do that ourselves)
|
|
cmd := exec.Command(os.Args[0], "service", "start", "--listen", "local", "--refresh=false", "--invoker", string(invoker), "--install-dir", constants.SteampipeDir)
|
|
startedAt := time.Now()
|
|
spinnerShown := false
|
|
startedChannel := make(chan bool, 1)
|
|
errorChannel := make(chan error, 1)
|
|
|
|
go func() {
|
|
out, err := cmd.CombinedOutput()
|
|
// we need to ignore errors when the invoker is the Installer
|
|
// since when the installer starts the service, it will not be a stable state
|
|
if err != nil && invoker != InvokerInstaller {
|
|
errorChannel <- fmt.Errorf("Could not start steampipe service: %s", string(out))
|
|
return
|
|
}
|
|
for {
|
|
st, err := loadRunningInstanceInfo()
|
|
if err != nil {
|
|
utils.ShowError(errors.New("could not retrieve service status"))
|
|
return
|
|
}
|
|
if st != nil {
|
|
startedChannel <- true
|
|
break
|
|
}
|
|
if time.Since(startedAt) > constants.SpinnerShowTimeout && !spinnerShown {
|
|
if cmdconfig.Viper().GetBool(constants.ShowInteractiveOutputConfigKey) {
|
|
s := utils.ShowSpinner("Waiting for database to start...")
|
|
defer utils.StopSpinner(s)
|
|
}
|
|
// set this anyway, so that next time it doesn't come in
|
|
spinnerShown = true
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
}()
|
|
|
|
timeoutAfter := time.After(1 * time.Minute)
|
|
|
|
select {
|
|
case <-timeoutAfter:
|
|
utils.ShowError(fmt.Errorf("x Waiting for database to start... FAILED!"))
|
|
case <-startedChannel:
|
|
// do nothing
|
|
case x := <-errorChannel:
|
|
utils.FailOnError(handleStartFailure(x))
|
|
}
|
|
}
|
|
|
|
func fdwNeedsUpdate() bool {
|
|
// check FDW version
|
|
versionInfo, err := versionfile.Load()
|
|
if err != nil {
|
|
utils.FailOnError(fmt.Errorf("could not verify required FDW version"))
|
|
}
|
|
return versionInfo.FdwExtension.Version != constants.FdwVersion
|
|
}
|
|
|
|
func installFDW(firstSetup bool) (string, error) {
|
|
status, err := GetStatus()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if status != nil {
|
|
defer func() {
|
|
if !firstSetup {
|
|
// update the signature
|
|
updateDownloadedBinarySignature()
|
|
}
|
|
}()
|
|
}
|
|
fdwInstallSpinner := utils.ShowSpinner(fmt.Sprintf("Download & install %s...", constants.Bold("steampipe-postgres-fdw")))
|
|
newDigest, err := ociinstaller.InstallFdw(constants.DefaultFdwImage, getDatabaseLocation())
|
|
utils.StopSpinner(fdwInstallSpinner)
|
|
if err != nil {
|
|
if firstSetup {
|
|
utils.FailOnErrorWithMessage(err, "x Download & install steampipe-postgres-fdw... FAILED!")
|
|
} else {
|
|
utils.ShowErrorWithMessage(err, "could not update steampipe-postgres-fdw")
|
|
}
|
|
}
|
|
return newDigest, err
|
|
}
|
|
|
|
// IsInstalled :: checks and reports whether the embedded database is installed and setup
|
|
func IsInstalled() bool {
|
|
// check that both postgres binary and initdb binary exist
|
|
// and are executable by us
|
|
|
|
if _, err := os.Stat(getInitDbBinaryExecutablePath()); os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
|
|
if _, err := os.Stat(getPostgresBinaryExecutablePath()); os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
|
|
if _, err := os.Stat(getFDWBinaryLocation()); os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
|
|
fdwSQLFile, fdwControlFile := getFDWSQLAndControlLocation()
|
|
|
|
if _, err := os.Stat(fdwSQLFile); os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
if _, err := os.Stat(fdwControlFile); os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
|
|
// TO DO:
|
|
// - move this out ot this function - make the "upgrade" optional
|
|
// - add function to get the latest digest
|
|
// // get the signature of the binary
|
|
// // we do this by having a signature file
|
|
// // which stores the md5 hash of the URL we downloaded
|
|
// // from and then comparing that against the has of the
|
|
// // URL we have in the constants
|
|
// installedSignature := getInstalledBinarySignature()
|
|
// desiredSignature := get
|
|
|
|
// if installedSignature != desiredSignature {
|
|
// return true
|
|
// }
|
|
|
|
return true
|
|
}
|
|
|
|
func updateDownloadedBinarySignature() error {
|
|
versionInfo, err := versionfile.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
installedSignature := fmt.Sprintf("%s|%s", versionInfo.EmbeddedDB.ImageDigest, versionInfo.FdwExtension.ImageDigest)
|
|
return ioutil.WriteFile(getDBSignatureLocation(), []byte(installedSignature), 0755)
|
|
}
|
|
|
|
func getInstalledBinarySignature() string {
|
|
sigBytes, err := ioutil.ReadFile(getDBSignatureLocation())
|
|
sig := ""
|
|
if os.IsNotExist(err) {
|
|
sig = ""
|
|
} else {
|
|
sig = string(sigBytes)
|
|
}
|
|
return sig
|
|
}
|
|
|
|
func initDatabase() error {
|
|
initDBExecutable := getInitDbBinaryExecutablePath()
|
|
initDbProcess := exec.Command(
|
|
initDBExecutable,
|
|
fmt.Sprintf("--auth=%s", "trust"),
|
|
fmt.Sprintf("--username=%s", constants.DatabaseSuperUser),
|
|
fmt.Sprintf("--pgdata=%s", getDataLocation()),
|
|
fmt.Sprintf("--encoding=%s", "UTF-8"),
|
|
fmt.Sprintf("--wal-segsize=%d", 1),
|
|
"--debug",
|
|
)
|
|
|
|
log.Printf("[TRACE] %s", initDbProcess.String())
|
|
|
|
runError := initDbProcess.Run()
|
|
if runError != nil {
|
|
return runError
|
|
}
|
|
|
|
return ioutil.WriteFile(getPgHbaConfLocation(), []byte(constants.PgHbaContent), 0600)
|
|
}
|