github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/cmdconfig/cmd_hooks.go (about) 1 package cmdconfig 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "log" 9 "os" 10 "runtime/debug" 11 "strings" 12 "time" 13 14 "github.com/fatih/color" 15 "github.com/hashicorp/go-hclog" 16 "github.com/mattn/go-isatty" 17 "github.com/spf13/cobra" 18 "github.com/spf13/viper" 19 filehelpers "github.com/turbot/go-kit/files" 20 "github.com/turbot/go-kit/helpers" 21 "github.com/turbot/go-kit/logging" 22 sdklogging "github.com/turbot/steampipe-plugin-sdk/v5/logging" 23 "github.com/turbot/steampipe-plugin-sdk/v5/plugin" 24 "github.com/turbot/steampipe-plugin-sdk/v5/sperr" 25 "github.com/turbot/steampipe/pkg/cloud" 26 "github.com/turbot/steampipe/pkg/constants" 27 "github.com/turbot/steampipe/pkg/constants/runtime" 28 "github.com/turbot/steampipe/pkg/error_helpers" 29 "github.com/turbot/steampipe/pkg/filepaths" 30 "github.com/turbot/steampipe/pkg/ociinstaller/versionfile" 31 "github.com/turbot/steampipe/pkg/steampipeconfig" 32 "github.com/turbot/steampipe/pkg/task" 33 "github.com/turbot/steampipe/pkg/utils" 34 "github.com/turbot/steampipe/pkg/version" 35 ) 36 37 var waitForTasksChannel chan struct{} 38 var tasksCancelFn context.CancelFunc 39 40 // postRunHook is a function that is executed after the PostRun of every command handler 41 func postRunHook(cmd *cobra.Command, args []string) { 42 utils.LogTime("cmdhook.postRunHook start") 43 defer utils.LogTime("cmdhook.postRunHook end") 44 45 if waitForTasksChannel != nil { 46 // wait for the async tasks to finish 47 select { 48 case <-time.After(100 * time.Millisecond): 49 tasksCancelFn() 50 return 51 case <-waitForTasksChannel: 52 return 53 } 54 } 55 } 56 57 // preRunHook is a function that is executed before the PreRun of every command handler 58 func preRunHook(cmd *cobra.Command, args []string) { 59 utils.LogTime("cmdhook.preRunHook start") 60 defer utils.LogTime("cmdhook.preRunHook end") 61 62 ctx := cmd.Context() 63 64 viper.Set(constants.ConfigKeyActiveCommand, cmd) 65 viper.Set(constants.ConfigKeyActiveCommandArgs, args) 66 viper.Set(constants.ConfigKeyIsTerminalTTY, isatty.IsTerminal(os.Stdout.Fd())) 67 68 // steampipe completion should not create INSTALL DIR or seup/init global config 69 if cmd.Name() == "completion" { 70 return 71 } 72 73 // create a buffer which can be used as a sink for log writes 74 // till INSTALL_DIR is setup in initGlobalConfig 75 logBuffer := bytes.NewBuffer([]byte{}) 76 77 // create a logger before initGlobalConfig - we may need to reinitialize the logger 78 // depending on the value of the log_level value in global general options 79 createLogger(logBuffer, cmd) 80 81 // set up the global viper config with default values from 82 // config files and ENV variables 83 ew := initGlobalConfig() 84 // display any warnings 85 ew.ShowWarnings() 86 // check for error 87 error_helpers.FailOnError(ew.Error) 88 89 // if the log level was set in the general config 90 if logLevelNeedsReset() { 91 logLevel := viper.GetString(constants.ArgLogLevel) 92 // set my environment to the desired log level 93 // so that this gets inherited by any other process 94 // started by this process (postgres/plugin-manager) 95 error_helpers.FailOnErrorWithMessage( 96 os.Setenv(sdklogging.EnvLogLevel, logLevel), 97 "Failed to setup logging", 98 ) 99 } 100 101 // recreate the logger 102 // this will put the new log level (if any) to effect as well as start streaming to the 103 // log file. 104 createLogger(logBuffer, cmd) 105 106 // runScheduledTasks skips running tasks if this instance is the plugin manager 107 waitForTasksChannel = runScheduledTasks(ctx, cmd, args, ew) 108 109 // ensure all plugin installation directories have a version.json file 110 // (this is to handle the case of migrating an existing installation from v0.20.x) 111 // no point doing this for the plugin-manager since that would have been done by the initiating CLI process 112 if !task.IsPluginManagerCmd(cmd) { 113 err := versionfile.EnsureVersionFilesInPluginDirectories(ctx) 114 error_helpers.FailOnError(sperr.WrapWithMessage(err, "failed to ensure version files in plugin directories")) 115 } 116 117 // set the max memory if specified 118 setMemoryLimit() 119 } 120 121 func setMemoryLimit() { 122 maxMemoryBytes := viper.GetInt64(constants.ArgMemoryMaxMb) * 1024 * 1024 123 if maxMemoryBytes > 0 { 124 // set the max memory 125 debug.SetMemoryLimit(maxMemoryBytes) 126 } 127 } 128 129 // runScheduledTasks runs the task runner and returns a channel which is closed when 130 // task run is complete 131 // 132 // runScheduledTasks skips running tasks if this instance is the plugin manager 133 func runScheduledTasks(ctx context.Context, cmd *cobra.Command, args []string, ew error_helpers.ErrorAndWarnings) chan struct{} { 134 // skip running the task runner if this is the plugin manager 135 // since it's supposed to be a daemon 136 if task.IsPluginManagerCmd(cmd) { 137 return nil 138 } 139 140 // display deprecation warning for check, mod and dashboard commands 141 if task.IsCheckCmd(cmd) || task.IsDashboardCmd(cmd) || task.IsModCmd(cmd) { 142 displayPpDeprecationWarning() 143 } 144 145 taskUpdateCtx, cancelFn := context.WithCancel(ctx) 146 tasksCancelFn = cancelFn 147 148 return task.RunTasks( 149 taskUpdateCtx, 150 cmd, 151 args, 152 // pass the config value in rather than runRasks querying viper directly - to avoid concurrent map access issues 153 // (we can use the update-check viper config here, since initGlobalConfig has already set it up 154 // with values from the config files and ENV settings - update-check cannot be set from the command line) 155 task.WithUpdateCheck(viper.GetBool(constants.ArgUpdateCheck)), 156 // show deprecation warnings 157 task.WithPreHook(func(_ context.Context) { 158 displayDeprecationWarnings(ew) 159 }), 160 ) 161 162 } 163 164 // the log level will need resetting if 165 // 166 // this process does not have a log level set in it's environment 167 // the GlobalConfig has a loglevel set 168 func logLevelNeedsReset() bool { 169 envLogLevelIsSet := envLogLevelSet() 170 generalOptionsSet := (steampipeconfig.GlobalConfig.GeneralOptions != nil && steampipeconfig.GlobalConfig.GeneralOptions.LogLevel != nil) 171 172 return !envLogLevelIsSet && generalOptionsSet 173 } 174 175 // envLogLevelSet checks whether any of the current or legacy log level env vars are set 176 func envLogLevelSet() bool { 177 _, ok := os.LookupEnv(sdklogging.EnvLogLevel) 178 if ok { 179 return ok 180 } 181 // handle legacy env vars 182 for _, e := range sdklogging.LegacyLogLevelEnvVars { 183 _, ok = os.LookupEnv(e) 184 if ok { 185 return ok 186 } 187 } 188 return false 189 } 190 191 // initGlobalConfig reads in config file and ENV variables if set. 192 func initGlobalConfig() error_helpers.ErrorAndWarnings { 193 utils.LogTime("cmdconfig.initGlobalConfig start") 194 defer utils.LogTime("cmdconfig.initGlobalConfig end") 195 196 var cmd = viper.Get(constants.ConfigKeyActiveCommand).(*cobra.Command) 197 ctx := cmd.Context() 198 199 // load workspace profile from the configured install dir 200 loader, err := getWorkspaceProfileLoader(ctx) 201 if err != nil { 202 return error_helpers.NewErrorsAndWarning(err) 203 } 204 205 // set global workspace profile 206 steampipeconfig.GlobalWorkspaceProfile = loader.GetActiveWorkspaceProfile() 207 208 // set-up viper with defaults from the env and default workspace profile 209 err = bootstrapViper(loader, cmd) 210 if err != nil { 211 return error_helpers.NewErrorsAndWarning(err) 212 } 213 214 // set global containing the configured install dir (create directory if needed) 215 ensureInstallDir() 216 217 // load the connection config and HCL options 218 config, loadConfigErrorsAndWarnings := steampipeconfig.LoadSteampipeConfig(ctx, viper.GetString(constants.ArgModLocation), cmd.Name()) 219 if loadConfigErrorsAndWarnings.Error != nil { 220 return loadConfigErrorsAndWarnings 221 } 222 223 // store global config 224 steampipeconfig.GlobalConfig = config 225 226 // set viper defaults from this config 227 SetDefaultsFromConfig(steampipeconfig.GlobalConfig.ConfigMap()) 228 229 // set the rest of the defaults from ENV 230 // ENV takes precedence over any default configuration 231 setDefaultsFromEnv() 232 233 // if an explicit workspace profile was set, add to viper as highest precedence default 234 // NOTE: if install_dir/mod_location are set these will already have been passed to viper by BootstrapViper 235 // since the "ConfiguredProfile" is passed in through a cmdline flag, it will always take precedence 236 if loader.ConfiguredProfile != nil { 237 SetDefaultsFromConfig(loader.ConfiguredProfile.ConfigMap(cmd)) 238 } 239 240 // handle deprecated cloud-host and cloud-token args and env vars 241 ew := handleDeprecations() 242 if ew.Error != nil { 243 return ew 244 } 245 246 // NOTE: we need to resolve the token separately 247 // - that is because we need the resolved value of ArgPipesHost in order to load any saved token 248 // and we cannot get this until the other config has been resolved 249 err = setCloudTokenDefault(loader) 250 if err != nil { 251 loadConfigErrorsAndWarnings.Error = err 252 return loadConfigErrorsAndWarnings 253 } 254 255 loadConfigErrorsAndWarnings.Merge(ew) 256 // now validate all config values have appropriate values 257 ew = validateConfig() 258 if ew.Error != nil { 259 return ew 260 } 261 loadConfigErrorsAndWarnings.Merge(ew) 262 263 return loadConfigErrorsAndWarnings 264 } 265 266 func handleDeprecations() error_helpers.ErrorAndWarnings { 267 var ew = error_helpers.ErrorAndWarnings{} 268 // if deprecated cloud-token or cloud-host is set, show a warning and copy the value to the new arg 269 if viper.IsSet(constants.ArgCloudToken) { 270 if viper.IsSet(constants.ArgPipesToken) { 271 ew.Error = sperr.New("Only one of flags --%s and --%s may be set", constants.ArgCloudToken, constants.ArgPipesToken) 272 return ew 273 } 274 viper.Set(constants.ArgPipesToken, viper.GetString(constants.ArgCloudToken)) 275 } 276 if viper.IsSet(constants.ArgCloudHost) { 277 if viper.IsSet(constants.ArgPipesHost) { 278 ew.Error = sperr.New("Only one of flags --%s and --%s may be set", constants.ArgCloudHost, constants.ArgPipesHost) 279 return ew 280 } 281 viper.Set(constants.ArgPipesHost, viper.GetString(constants.ArgCloudHost)) 282 } 283 284 // is deprecated STEAMPIPE_CLOUD_TOKEN env var set? 285 if _, isCloudTokenSet := os.LookupEnv(constants.EnvCloudToken); isCloudTokenSet { 286 // is PIPES_TOKEN also set? This is an error 287 if _, isPipesTokenSet := os.LookupEnv(constants.EnvPipesToken); isPipesTokenSet { 288 ew.Error = sperr.New("Only one of env vars %s and %s may be set", constants.EnvCloudToken, constants.EnvPipesToken) 289 return ew 290 } 291 // otherwise, show a warning 292 ew.AddWarning(fmt.Sprintf("The %s env var is deprecated - use %s", constants.EnvCloudToken, constants.EnvPipesToken)) 293 } 294 // the same for STEAMPIPE_CLOUD_HOST 295 if _, isCloudTokenSet := os.LookupEnv(constants.EnvCloudHost); isCloudTokenSet { 296 if _, isPipesTokenSet := os.LookupEnv(constants.EnvPipesHost); isPipesTokenSet { 297 ew.Error = sperr.New("Only one of env vars %s and %s may be set", constants.EnvCloudHost, constants.EnvPipesHost) 298 return ew 299 } 300 ew.AddWarning(fmt.Sprintf("The %s env var is deprecated - use %s", constants.EnvCloudHost, constants.EnvPipesHost)) 301 } 302 return ew 303 } 304 305 func setCloudTokenDefault(loader *steampipeconfig.WorkspaceProfileLoader) error { 306 /* 307 saved cloud token 308 cloud_token in default workspace 309 explicit env var (STEAMIPE_CLOUD_TOKEN ) wins over 310 cloud_token in specific workspace 311 */ 312 // set viper defaults in order of increasing precedence 313 // 1) saved cloud token 314 savedToken, err := cloud.LoadToken() 315 if err != nil { 316 return err 317 } 318 if savedToken != "" { 319 viper.SetDefault(constants.ArgPipesToken, savedToken) 320 } 321 // 2) default profile pipes token 322 if loader.DefaultProfile.PipesToken != nil { 323 viper.SetDefault(constants.ArgPipesToken, *loader.DefaultProfile.PipesToken) 324 } 325 // deprecated - cloud token 326 if loader.DefaultProfile.CloudToken != nil { 327 viper.SetDefault(constants.ArgPipesToken, *loader.DefaultProfile.CloudToken) 328 } 329 // 3) env var (STEAMIPE_CLOUD_TOKEN ) 330 SetDefaultFromEnv(constants.EnvPipesToken, constants.ArgPipesToken, String) 331 // deprecated env var 332 SetDefaultFromEnv(constants.EnvCloudToken, constants.ArgPipesToken, String) 333 334 // 4) explicit workspace profile 335 if p := loader.ConfiguredProfile; p != nil && p.PipesToken != nil { 336 viper.SetDefault(constants.ArgPipesToken, *p.PipesToken) 337 } 338 // deprecated - cloud token 339 if p := loader.ConfiguredProfile; p != nil && p.CloudToken != nil { 340 viper.SetDefault(constants.ArgPipesToken, *p.CloudToken) 341 } 342 return nil 343 } 344 345 func getWorkspaceProfileLoader(ctx context.Context) (*steampipeconfig.WorkspaceProfileLoader, error) { 346 // set viper default for workspace profile, using EnvWorkspaceProfile env var 347 SetDefaultFromEnv(constants.EnvWorkspaceProfile, constants.ArgWorkspaceProfile, String) 348 // set viper default for install dir, using EnvInstallDir env var 349 SetDefaultFromEnv(constants.EnvInstallDir, constants.ArgInstallDir, String) 350 351 // resolve the workspace profile dir 352 installDir, err := filehelpers.Tildefy(viper.GetString(constants.ArgInstallDir)) 353 if err != nil { 354 return nil, err 355 } 356 357 workspaceProfileDir, err := filepaths.WorkspaceProfileDir(installDir) 358 if err != nil { 359 return nil, err 360 } 361 362 // create loader 363 loader, err := steampipeconfig.NewWorkspaceProfileLoader(ctx, workspaceProfileDir) 364 if err != nil { 365 return nil, err 366 } 367 368 return loader, nil 369 } 370 371 // now validate config values have appropriate values 372 // (currently validates telemetry) 373 func validateConfig() error_helpers.ErrorAndWarnings { 374 var res = error_helpers.ErrorAndWarnings{} 375 telemetry := viper.GetString(constants.ArgTelemetry) 376 if !helpers.StringSliceContains(constants.TelemetryLevels, telemetry) { 377 res.Error = sperr.New(`invalid value of 'telemetry' (%s), must be one of: %s`, telemetry, strings.Join(constants.TelemetryLevels, ", ")) 378 return res 379 } 380 if _, legacyDiagnosticsSet := os.LookupEnv(plugin.EnvLegacyDiagnosticsLevel); legacyDiagnosticsSet { 381 res.AddWarning(fmt.Sprintf("Environment variable %s is deprecated - use %s", plugin.EnvLegacyDiagnosticsLevel, plugin.EnvDiagnosticsLevel)) 382 } 383 res.Error = plugin.ValidateDiagnosticsEnvVar() 384 385 return res 386 } 387 388 // create a hclog logger with the level specified by the SP_LOG env var 389 func createLogger(logBuffer *bytes.Buffer, cmd *cobra.Command) { 390 if task.IsPluginManagerCmd(cmd) { 391 // nothing to do here - plugin manager sets up it's own logger 392 // refer https://github.com/turbot/steampipe/blob/710a96d45fd77294de8d63d77bf78db65133e5ca/cmd/plugin_manager.go#L102 393 return 394 } 395 396 level := sdklogging.LogLevel() 397 var logDestination io.Writer 398 if len(filepaths.SteampipeDir) == 0 { 399 // write to the buffer - this is to make sure that we don't lose logs 400 // till the time we get the log directory 401 logDestination = logBuffer 402 } else { 403 logDestination = logging.NewRotatingLogWriter(filepaths.EnsureLogDir(), "steampipe") 404 405 // write out the buffered contents 406 _, _ = logDestination.Write(logBuffer.Bytes()) 407 } 408 409 hcLevel := hclog.LevelFromString(level) 410 411 options := &hclog.LoggerOptions{ 412 // make the name unique so that logs from this instance can be filtered 413 Name: fmt.Sprintf("steampipe [%s]", runtime.ExecutionID), 414 Level: hcLevel, 415 Output: logDestination, 416 TimeFn: func() time.Time { return time.Now().UTC() }, 417 TimeFormat: "2006-01-02 15:04:05.000 UTC", 418 } 419 logger := sdklogging.NewLogger(options) 420 log.SetOutput(logger.StandardWriter(&hclog.StandardLoggerOptions{InferLevels: true})) 421 log.SetPrefix("") 422 log.SetFlags(0) 423 424 // if the buffer is empty then this is the first time the logger is getting setup 425 // write out a banner 426 if logBuffer.Len() == 0 { 427 // pump in the initial set of logs 428 // this will also write out the Execution ID - enabling easy filtering of logs for a single execution 429 // we need to do this since all instances will log to a single file and logs will be interleaved 430 log.Printf("[INFO] ********************************************************\n") 431 log.Printf("[INFO] steampipe %s [%s]", cmd.Name(), runtime.ExecutionID) 432 log.Printf("[INFO] Version: v%s\n", version.VersionString) 433 log.Printf("[INFO] Log level: %s\n", sdklogging.LogLevel()) 434 log.Printf("[INFO] Log date: %s\n", time.Now().Format("2006-01-02")) 435 log.Printf("[INFO] ********************************************************\n") 436 } 437 } 438 439 func ensureInstallDir() { 440 pipesInstallDir := viper.GetString(constants.ArgPipesInstallDir) 441 installDir := viper.GetString(constants.ArgInstallDir) 442 443 log.Printf("[TRACE] ensureInstallDir %s", installDir) 444 if _, err := os.Stat(installDir); os.IsNotExist(err) { 445 log.Printf("[TRACE] creating install dir") 446 err = os.MkdirAll(installDir, 0755) 447 error_helpers.FailOnErrorWithMessage(err, fmt.Sprintf("could not create installation directory: %s", installDir)) 448 } 449 450 if _, err := os.Stat(pipesInstallDir); os.IsNotExist(err) { 451 log.Printf("[TRACE] creating install dir") 452 err = os.MkdirAll(pipesInstallDir, 0755) 453 error_helpers.FailOnErrorWithMessage(err, fmt.Sprintf("could not create pipes installation directory: %s", pipesInstallDir)) 454 } 455 456 // store as SteampipeDir and PipesInstallDir 457 filepaths.SteampipeDir = installDir 458 filepaths.PipesInstallDir = pipesInstallDir 459 } 460 461 // displayDeprecationWarnings shows the deprecated warnings in a formatted way 462 func displayDeprecationWarnings(errorsAndWarnings error_helpers.ErrorAndWarnings) { 463 if len(errorsAndWarnings.Warnings) > 0 { 464 fmt.Println(color.YellowString(fmt.Sprintf("\nDeprecation %s:", utils.Pluralize("warning", len(errorsAndWarnings.Warnings))))) 465 for _, warning := range errorsAndWarnings.Warnings { 466 fmt.Printf("%s\n\n", warning) 467 } 468 fmt.Println("For more details, see https://steampipe.io/docs/reference/config-files/workspace") 469 fmt.Println() 470 } 471 } 472 473 func displayPpDeprecationWarning() { 474 fmt.Fprintf(color.Error, "\n%s Steampipe mods and dashboards have been moved to %s. This command %s in a future version. Migration guide - https://powerpipe.io/blog/migrating-from-steampipe \n", color.YellowString("Deprecation warning:"), constants.Bold("Powerpipe"), constants.Bold("will be removed")) 475 }