github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/db/db_local/install.go (about) 1 package db_local 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "log" 8 "os" 9 "os/exec" 10 "sync" 11 12 "github.com/fatih/color" 13 "github.com/jackc/pgx/v5" 14 psutils "github.com/shirou/gopsutil/process" 15 filehelpers "github.com/turbot/go-kit/files" 16 "github.com/turbot/go-kit/helpers" 17 "github.com/turbot/steampipe/pkg/constants" 18 "github.com/turbot/steampipe/pkg/filepaths" 19 "github.com/turbot/steampipe/pkg/ociinstaller" 20 "github.com/turbot/steampipe/pkg/ociinstaller/versionfile" 21 "github.com/turbot/steampipe/pkg/statushooks" 22 "github.com/turbot/steampipe/pkg/utils" 23 ) 24 25 var ensureMux sync.Mutex 26 27 func noBackupWarning() string { 28 warningMessage := `Steampipe database has been upgraded from Postgres 12 to Postgres 14. 29 30 Unfortunately the data in your public schema failed migration using the standard pg_dump and pg_restore tools. Your data has been preserved in the ~/.steampipe/db directory. 31 32 If you need to restore the contents of your public schema, please open an issue at https://github.com/turbot/steampipe.` 33 34 return fmt.Sprintf("%s: %v\n", color.YellowString("Warning"), warningMessage) 35 } 36 37 // EnsureDBInstalled makes sure that the embedded postgres database is installed and ready to run 38 func EnsureDBInstalled(ctx context.Context) (err error) { 39 utils.LogTime("db_local.EnsureDBInstalled start") 40 41 ensureMux.Lock() 42 43 doneChan := make(chan bool, 1) 44 defer func() { 45 if r := recover(); r != nil { 46 err = helpers.ToError(r) 47 } 48 49 utils.LogTime("db_local.EnsureDBInstalled end") 50 ensureMux.Unlock() 51 close(doneChan) 52 }() 53 54 if IsDBInstalled() { 55 // check if the FDW need updating, and init the db if required 56 err := prepareDb(ctx) 57 return err 58 } 59 60 // handle the case that the previous db version may still be running 61 dbState, err := GetState() 62 if err != nil { 63 log.Println("[TRACE] Error while loading database state", err) 64 return err 65 } 66 if dbState != nil { 67 return fmt.Errorf("cannot install service - a previous version of the Steampipe service is still running. To stop running services, use %s ", constants.Bold("steampipe service stop")) 68 } 69 70 log.Println("[TRACE] calling removeRunningInstanceInfo") 71 err = removeRunningInstanceInfo() 72 if err != nil && !os.IsNotExist(err) { 73 log.Printf("[TRACE] removeRunningInstanceInfo failed: %v", err) 74 return fmt.Errorf("Cleanup any Steampipe processes... FAILED!") 75 } 76 77 statushooks.SetStatus(ctx, "Installing database…") 78 79 err = downloadAndInstallDbFiles(ctx) 80 if err != nil { 81 return err 82 } 83 84 statushooks.SetStatus(ctx, "Preparing backups…") 85 86 // call prepareBackup to generate the db dump file if necessary 87 // NOTE: this returns the existing database name - we use this when creating the new database 88 dbName, err := prepareBackup(ctx) 89 if err != nil { 90 if errors.Is(err, errDbInstanceRunning) { 91 // remove the installation - otherwise, the backup won't get triggered, even if the user stops the service 92 os.RemoveAll(filepaths.DatabaseInstanceDir()) 93 return err 94 } 95 // ignore all other errors with the backup, displaying a warning instead 96 statushooks.Message(ctx, noBackupWarning()) 97 } 98 99 // install the fdw 100 _, err = installFDW(ctx, true) 101 if err != nil { 102 log.Printf("[TRACE] installFDW failed: %v", err) 103 return fmt.Errorf("Download & install steampipe-postgres-fdw... FAILED!") 104 } 105 106 // run the database installation 107 err = runInstall(ctx, dbName) 108 if err != nil { 109 return err 110 } 111 112 // write a signature after everything gets done! 113 // so that we can check for this later on 114 statushooks.SetStatus(ctx, "Updating install records…") 115 err = updateDownloadedBinarySignature() 116 if err != nil { 117 log.Printf("[TRACE] updateDownloadedBinarySignature failed: %v", err) 118 return fmt.Errorf("Updating install records... FAILED!") 119 } 120 121 return nil 122 } 123 124 func downloadAndInstallDbFiles(ctx context.Context) error { 125 statushooks.SetStatus(ctx, "Prepare database install location…") 126 // clear all db files 127 err := os.RemoveAll(filepaths.GetDatabaseLocation()) 128 if err != nil { 129 log.Printf("[TRACE] %v", err) 130 return fmt.Errorf("Prepare database install location... FAILED!") 131 } 132 133 statushooks.SetStatus(ctx, "Download & install embedded PostgreSQL database…") 134 _, err = ociinstaller.InstallDB(ctx, filepaths.GetDatabaseLocation()) 135 if err != nil { 136 log.Printf("[TRACE] %v", err) 137 return fmt.Errorf("Download & install embedded PostgreSQL database... FAILED!") 138 } 139 return nil 140 } 141 142 // IsDBInstalled checks and reports whether the embedded database binaries are available 143 func IsDBInstalled() bool { 144 utils.LogTime("db_local.IsInstalled start") 145 defer utils.LogTime("db_local.IsInstalled end") 146 // check that both postgres binary and initdb binary exist 147 if _, err := os.Stat(filepaths.GetInitDbBinaryExecutablePath()); os.IsNotExist(err) { 148 return false 149 } 150 if _, err := os.Stat(filepaths.GetPostgresBinaryExecutablePath()); os.IsNotExist(err) { 151 return false 152 } 153 return true 154 } 155 156 // IsFDWInstalled chceks whether all files required for the Steampipe FDW are available 157 func IsFDWInstalled() bool { 158 fdwSQLFile, fdwControlFile := filepaths.GetFDWSQLAndControlLocation() 159 if _, err := os.Stat(fdwSQLFile); os.IsNotExist(err) { 160 return false 161 } 162 if _, err := os.Stat(fdwControlFile); os.IsNotExist(err) { 163 return false 164 } 165 if _, err := os.Stat(filepaths.GetFDWBinaryLocation()); os.IsNotExist(err) { 166 return false 167 } 168 return true 169 } 170 171 // prepareDb updates the db binaries and FDW if needed, and inits the database if required 172 func prepareDb(ctx context.Context) error { 173 // load the db version info file 174 utils.LogTime("db_local.LoadDatabaseVersionFile start") 175 versionInfo, err := versionfile.LoadDatabaseVersionFile() 176 utils.LogTime("db_local.LoadDatabaseVersionFile end") 177 if err != nil { 178 return err 179 } 180 181 // check if db needs to be updated 182 // this means that the db version number has NOT changed but the package has changed 183 // we can just drop in the new binaries 184 if dbNeedsUpdate(versionInfo) { 185 statushooks.SetStatus(ctx, "Updating database…") 186 187 // install new db binaries 188 if err = downloadAndInstallDbFiles(ctx); err != nil { 189 return err 190 } 191 // write a signature after everything gets done! 192 // so that we can check for this later on 193 statushooks.SetStatus(ctx, "Updating install records…") 194 if err = updateDownloadedBinarySignature(); err != nil { 195 log.Printf("[TRACE] updateDownloadedBinarySignature failed: %v", err) 196 return fmt.Errorf("Updating install records... FAILED!") 197 } 198 } 199 200 // if the FDW is not installed, or needs an update 201 if !IsFDWInstalled() || fdwNeedsUpdate(versionInfo) { 202 // install fdw 203 if _, err := installFDW(ctx, false); err != nil { 204 log.Printf("[TRACE] installFDW failed: %v", err) 205 return fmt.Errorf("Update steampipe-postgres-fdw... FAILED!") 206 } 207 208 // get the message renderer from the context 209 // this allows the interactive client init to inject a custom renderer 210 messageRenderer := statushooks.MessageRendererFromContext(ctx) 211 messageRenderer("%s updated to %s.", constants.Bold("steampipe-postgres-fdw"), constants.Bold(constants.FdwVersion)) 212 } 213 214 if needsInit() { 215 statushooks.SetStatus(ctx, "Cleanup any Steampipe processes…") 216 killInstanceIfAny(ctx) 217 if err := runInstall(ctx, nil); err != nil { 218 return err 219 } 220 } 221 return nil 222 } 223 224 func fdwNeedsUpdate(versionInfo *versionfile.DatabaseVersionFile) bool { 225 return versionInfo.FdwExtension.Version != constants.FdwVersion 226 } 227 228 func dbNeedsUpdate(versionInfo *versionfile.DatabaseVersionFile) bool { 229 return versionInfo.EmbeddedDB.ImageDigest != constants.PostgresImageDigest 230 } 231 232 func installFDW(ctx context.Context, firstSetup bool) (string, error) { 233 utils.LogTime("db_local.installFDW start") 234 defer utils.LogTime("db_local.installFDW end") 235 236 state, err := GetState() 237 if err != nil { 238 return "", err 239 } 240 if state != nil { 241 defer func() { 242 if !firstSetup { 243 // update the signature 244 updateDownloadedBinarySignature() 245 } 246 }() 247 } 248 statushooks.SetStatus(ctx, fmt.Sprintf("Download & install %s…", constants.Bold("steampipe-postgres-fdw"))) 249 return ociinstaller.InstallFdw(ctx, filepaths.GetDatabaseLocation()) 250 } 251 252 func needsInit() bool { 253 utils.LogTime("db_local.needsInit start") 254 defer utils.LogTime("db_local.needsInit end") 255 256 // test whether pg_hba.conf exists in our target directory 257 return !filehelpers.FileExists(filepaths.GetPgHbaConfLocation()) 258 } 259 260 func runInstall(ctx context.Context, oldDbName *string) error { 261 utils.LogTime("db_local.runInstall start") 262 defer utils.LogTime("db_local.runInstall end") 263 264 statushooks.SetStatus(ctx, "Cleaning up…") 265 266 err := utils.RemoveDirectoryContents(filepaths.GetDataLocation()) 267 if err != nil { 268 log.Printf("[TRACE] %v", err) 269 return fmt.Errorf("Prepare database install location... FAILED!") 270 } 271 272 statushooks.SetStatus(ctx, "Initializing database…") 273 err = initDatabase() 274 if err != nil { 275 log.Printf("[TRACE] initDatabase failed: %v", err) 276 return fmt.Errorf("Initializing database... FAILED!") 277 } 278 279 statushooks.SetStatus(ctx, "Starting database…") 280 port, err := utils.GetNextFreePort() 281 if err != nil { 282 log.Printf("[TRACE] getNextFreePort failed: %v", err) 283 return fmt.Errorf("Starting database... FAILED!") 284 } 285 286 process, err := startServiceForInstall(port) 287 if err != nil { 288 log.Printf("[TRACE] startServiceForInstall failed: %v", err) 289 return fmt.Errorf("Starting database... FAILED!") 290 } 291 292 statushooks.SetStatus(ctx, "Connection to database…") 293 client, err := createMaintenanceClient(ctx, port) 294 if err != nil { 295 return fmt.Errorf("Connection to database... FAILED!") 296 } 297 defer func() { 298 statushooks.SetStatus(ctx, "Completing configuration") 299 client.Close(ctx) 300 doThreeStepPostgresExit(ctx, process) 301 }() 302 303 statushooks.SetStatus(ctx, "Generating database passwords…") 304 // generate a password file for use later 305 _, err = readPasswordFile() 306 if err != nil { 307 log.Printf("[TRACE] readPassword failed: %v", err) 308 return fmt.Errorf("Generating database passwords... FAILED!") 309 } 310 311 // resolve the name of the database that is to be installed 312 databaseName := resolveDatabaseName(oldDbName) 313 // validate db name 314 if !isValidDatabaseName(databaseName) { 315 return fmt.Errorf("Invalid database name '%s' - must start with either a lowercase character or an underscore", databaseName) 316 } 317 318 statushooks.SetStatus(ctx, "Configuring database…") 319 err = installDatabaseWithPermissions(ctx, databaseName, client) 320 if err != nil { 321 log.Printf("[TRACE] installSteampipeDatabaseAndUser failed: %v", err) 322 return fmt.Errorf("Configuring database... FAILED!") 323 } 324 325 statushooks.SetStatus(ctx, "Configuring Steampipe…") 326 err = installForeignServer(ctx, client) 327 if err != nil { 328 log.Printf("[TRACE] installForeignServer failed: %v", err) 329 return fmt.Errorf("Configuring Steampipe... FAILED!") 330 } 331 332 return nil 333 } 334 335 func resolveDatabaseName(oldDbName *string) string { 336 // resolve the name of the database that is to be installed 337 // use the application constant as default 338 if oldDbName != nil { 339 return *oldDbName 340 } 341 databaseName := constants.DatabaseName 342 if envValue, exists := os.LookupEnv(constants.EnvInstallDatabase); exists && len(envValue) > 0 { 343 // use whatever is supplied, if available 344 databaseName = envValue 345 } 346 return databaseName 347 } 348 349 func startServiceForInstall(port int) (*psutils.Process, error) { 350 postgresCmd := exec.Command( 351 filepaths.GetPostgresBinaryExecutablePath(), 352 // by this time, we are sure that the port if free to listen to 353 "-p", fmt.Sprint(port), 354 "-c", "listen_addresses=127.0.0.1", 355 // NOTE: If quoted, the application name includes the quotes. Worried about 356 // having spaces in the APPNAME, but leaving it unquoted since currently 357 // the APPNAME is hardcoded to be steampipe. 358 "-c", fmt.Sprintf("application_name=%s", constants.AppName), 359 "-c", fmt.Sprintf("cluster_name=%s", constants.AppName), 360 361 // log directory 362 "-c", fmt.Sprintf("log_directory=%s", filepaths.EnsureLogDir()), 363 364 // Data Directory 365 "-D", filepaths.GetDataLocation()) 366 367 setupLogCollection(postgresCmd) 368 369 err := postgresCmd.Start() 370 if err != nil { 371 return nil, err 372 } 373 374 return psutils.NewProcess(int32(postgresCmd.Process.Pid)) 375 } 376 377 func isValidDatabaseName(databaseName string) bool { 378 return databaseName[0] == '_' || (databaseName[0] >= 'a' && databaseName[0] <= 'z') 379 } 380 381 func initDatabase() error { 382 utils.LogTime("db_local.install.initDatabase start") 383 defer utils.LogTime("db_local.install.initDatabase end") 384 385 // initdb sometimes fail due to invalid locale settings, to avoid this we update 386 // the locale settings to use 'C' only for the initdb process to complete, and 387 // then return to the existing locale settings of the user. 388 // set LC_ALL env variable to override current locale settings 389 err := os.Setenv("LC_ALL", "C") 390 if err != nil { 391 log.Printf("[TRACE] failed to update locale settings:\n %s", err.Error()) 392 return err 393 } 394 395 initDBExecutable := filepaths.GetInitDbBinaryExecutablePath() 396 initDbProcess := exec.Command( 397 initDBExecutable, 398 // Steampipe runs Postgres as a local, embedded database so trust local 399 // users to login without a password. 400 fmt.Sprintf("--auth=%s", "trust"), 401 // Ensure the name of the database superuser is consistent across installs. 402 // By default it would be based on the user running the install of this 403 // embedded database. 404 fmt.Sprintf("--username=%s", constants.DatabaseSuperUser), 405 // Postgres data should placed under the Steampipe install directory. 406 fmt.Sprintf("--pgdata=%s", filepaths.GetDataLocation()), 407 // Ensure the encoding is consistent across installs. By default it would 408 // be based on the system locale. 409 fmt.Sprintf("--encoding=%s", "UTF-8"), 410 ) 411 412 log.Printf("[TRACE] initdb start: %s", initDbProcess.String()) 413 414 output, runError := initDbProcess.CombinedOutput() 415 if runError != nil { 416 log.Printf("[TRACE] initdb failed:\n %s", string(output)) 417 return runError 418 } 419 420 // unset LC_ALL to return to original locale settings 421 err = os.Unsetenv("LC_ALL") 422 if err != nil { 423 log.Printf("[TRACE] failed to return back to original locale settings:\n %s", err.Error()) 424 return err 425 } 426 427 // intentionally overwriting existing pg_hba.conf with a minimal config which only allows root 428 // so that we can setup the database and permissions 429 return os.WriteFile(filepaths.GetPgHbaConfLocation(), []byte(constants.MinimalPgHbaContent), 0600) 430 } 431 432 func installDatabaseWithPermissions(ctx context.Context, databaseName string, rawClient *pgx.Conn) error { 433 utils.LogTime("db_local.install.installDatabaseWithPermissions start") 434 defer utils.LogTime("db_local.install.installDatabaseWithPermissions end") 435 436 log.Println("[TRACE] installing database with name", databaseName) 437 438 statements := []string{ 439 440 // Lockdown all existing, and future, databases from use. 441 `revoke all on database postgres from public`, 442 `revoke all on database template1 from public`, 443 444 // Only the root user (who owns the postgres database) should be able to use 445 // or change it. 446 `revoke all privileges on schema public from public`, 447 448 // Create the steampipe database, used to hold all steampipe tables, views and data. 449 fmt.Sprintf(`create database %s`, databaseName), 450 451 // Restrict permissions from general users to the steampipe database. We add them 452 // back progressively to allow appropriate read only access. 453 fmt.Sprintf("revoke all on database %s from public", databaseName), 454 455 // The root user gets full rights to the steampipe database, ensuring we can actually 456 // configure and manage it properly. 457 fmt.Sprintf("grant all on database %s to root", databaseName), 458 459 // The root user gets a password which will be used later on to connect 460 fmt.Sprintf(`alter user root with password '%s'`, generatePassword()), 461 462 // 463 // PERMISSIONS 464 // 465 // References: 466 // * https://dba.stackexchange.com/questions/117109/how-to-manage-default-privileges-for-users-on-a-database-vs-schema/117661#117661 467 // 468 469 // Create a role to represent all steampipe_users in the database. 470 // Grants and permissions can be managed on this role independent 471 // of the actual users in the system, giving us flexibility. 472 fmt.Sprintf(`create role %s`, constants.DatabaseUsersRole), 473 474 // Allow the steampipe user access to the steampipe database only 475 fmt.Sprintf("grant connect on database %s to %s", databaseName, constants.DatabaseUsersRole), 476 477 // Create the steampipe user. By default they do not have superuser, createdb 478 // or createrole permissions. 479 fmt.Sprintf("create user %s", constants.DatabaseUser), 480 481 // Allow the steampipe user to manage temporary tables 482 fmt.Sprintf("grant temporary on database %s to %s", databaseName, constants.DatabaseUsersRole), 483 484 // No need to set a password to the 'steampipe' user 485 // The password gets set on every service start 486 487 // Allow steampipe the privileges of steampipe_users. 488 fmt.Sprintf("grant %s to %s", constants.DatabaseUsersRole, constants.DatabaseUser), 489 } 490 for _, statement := range statements { 491 // not logging here, since the password may get logged 492 // we don't want that 493 if _, err := rawClient.Exec(ctx, statement); err != nil { 494 return err 495 } 496 } 497 return writePgHbaContent(databaseName, constants.DatabaseUser) 498 } 499 500 func writePgHbaContent(databaseName string, username string) error { 501 content := fmt.Sprintf(constants.PgHbaTemplate, databaseName, username) 502 return os.WriteFile(filepaths.GetPgHbaConfLocation(), []byte(content), 0600) 503 } 504 505 func installForeignServer(ctx context.Context, rawClient *pgx.Conn) error { 506 utils.LogTime("db_local.installForeignServer start") 507 defer utils.LogTime("db_local.installForeignServer end") 508 509 statements := []string{ 510 // Install the FDW. The name must match the binary file. 511 `drop extension if exists "steampipe_postgres_fdw" cascade`, 512 `create extension if not exists "steampipe_postgres_fdw"`, 513 // Use steampipe for the server name, it's simplest 514 `create server "steampipe" foreign data wrapper "steampipe_postgres_fdw"`, 515 } 516 517 for _, statement := range statements { 518 // NOTE: This may print a password to the log file, but it doesn't matter 519 // since the password is stored in a config file anyway. 520 log.Println("[TRACE] Install Foreign Server: ", statement) 521 if _, err := rawClient.Exec(ctx, statement); err != nil { 522 return err 523 } 524 } 525 526 return nil 527 } 528 529 func updateDownloadedBinarySignature() error { 530 utils.LogTime("db_local.updateDownloadedBinarySignature start") 531 defer utils.LogTime("db_local.updateDownloadedBinarySignature end") 532 533 versionInfo, err := versionfile.LoadDatabaseVersionFile() 534 if err != nil { 535 return err 536 } 537 installedSignature := fmt.Sprintf("%s|%s", versionInfo.EmbeddedDB.ImageDigest, versionInfo.FdwExtension.ImageDigest) 538 return os.WriteFile(filepaths.GetDBSignatureLocation(), []byte(installedSignature), 0755) 539 }