From 299697ae2f9b6743422048008ca9668427bae4d4 Mon Sep 17 00:00:00 2001 From: Binaek Sarkar Date: Fri, 31 Mar 2023 19:42:25 +0530 Subject: [PATCH] Updates in cache configuration to allow disabling of all caching on server. Closes #3258 STEAMPIPE_CACHE environment variable resolves to service cache enabled as well as client cache enabled service cache enabled is used by the plugin manager to enable/disable caching on the plugins during startup (runtime toggle is not allowed) - with a max_ttl client cache enabled is used to enable/disable the cache on the database connection (fdw) A TTL can also be set on the client side capped to max_ttl on the server --- cmd/root.go | 7 +- go.mod | 4 +- go.sum | 8 +- pkg/cmdconfig/env_var_type.go | 11 +++ pkg/cmdconfig/envvartype_string.go | 25 +++++ pkg/cmdconfig/viper.go | 92 +++++++++++------- pkg/constants/args.go | 4 +- pkg/constants/db.go | 16 ++-- pkg/constants/env.go | 1 + pkg/constants/metaquery_commands.go | 1 + pkg/db/db_client/db_client_cache_control.go | 34 ------- pkg/db/db_client/db_client_session.go | 20 ++++ pkg/db/db_common/acquire_session_result.go | 2 +- pkg/db/db_common/cache_control.go | 41 ++++++++ pkg/db/db_common/client.go | 5 +- pkg/db/db_local/start_services.go | 4 +- pkg/initialisation/init_data.go | 4 +- pkg/interactive/interactive_client.go | 2 +- pkg/query/init_data.go | 1 + pkg/query/metaquery/definitions.go | 6 ++ pkg/query/metaquery/handlers.go | 95 +++++++++++++------ pkg/query/metaquery/validators.go | 2 +- .../modconfig/workspace_profile.go | 2 +- pkg/steampipeconfig/options/database.go | 50 +++++++++- pluginmanager_service/plugin_manager.go | 50 +++++++--- .../test_files/service_and_plugin.bats | 2 +- 26 files changed, 345 insertions(+), 144 deletions(-) create mode 100644 pkg/cmdconfig/env_var_type.go create mode 100644 pkg/cmdconfig/envvartype_string.go delete mode 100644 pkg/db/db_client/db_client_cache_control.go create mode 100644 pkg/db/db_common/cache_control.go diff --git a/cmd/root.go b/cmd/root.go index 86a78be4a0..4803f43637 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -61,7 +61,6 @@ var rootCmd = &cobra.Command{ defer func() { if r := recover(); r != nil { error_helpers.ShowError(cmd.Context(), helpers.ToError(r)) - debug.PrintStack() } }() @@ -283,7 +282,7 @@ func setCloudTokenDefault(loader *steampipeconfig.WorkspaceProfileLoader) error viper.SetDefault(constants.ArgCloudToken, *loader.DefaultProfile.CloudToken) } // 3) env var (STEAMIPE_CLOUD_TOKEN ) - cmdconfig.SetDefaultFromEnv(constants.EnvCloudToken, constants.ArgCloudToken, "string") + cmdconfig.SetDefaultFromEnv(constants.EnvCloudToken, constants.ArgCloudToken, cmdconfig.String) // 4) explicit workspace profile if p := loader.ConfiguredProfile; p != nil && p.CloudToken != nil { @@ -294,9 +293,9 @@ func setCloudTokenDefault(loader *steampipeconfig.WorkspaceProfileLoader) error func loadWorkspaceProfile() (*steampipeconfig.WorkspaceProfileLoader, error) { // set viper default for workspace profile, using STEAMPIPE_WORKSPACE env var - cmdconfig.SetDefaultFromEnv(constants.EnvWorkspaceProfile, constants.ArgWorkspaceProfile, "string") + cmdconfig.SetDefaultFromEnv(constants.EnvWorkspaceProfile, constants.ArgWorkspaceProfile, cmdconfig.String) // set viper default for install dir, using STEAMPIPE_INSTALL_DIR env var - cmdconfig.SetDefaultFromEnv(constants.EnvInstallDir, constants.ArgInstallDir, "string") + cmdconfig.SetDefaultFromEnv(constants.EnvInstallDir, constants.ArgInstallDir, cmdconfig.String) // resolve the workspace profile dir installDir, err := filehelpers.Tildefy(viper.GetString(constants.ArgInstallDir)) diff --git a/go.mod b/go.mod index 6d5d71ce48..50ea009331 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,7 @@ require ( github.com/stevenle/topsort v0.2.0 github.com/turbot/go-kit v0.5.0 github.com/turbot/steampipe-cloud-sdk-go v0.5.0 - github.com/turbot/steampipe-plugin-sdk/v5 v5.3.0-rc.0 + github.com/turbot/steampipe-plugin-sdk/v5 v5.4.0-rc.1 github.com/xlab/treeprint v1.2.0 github.com/zclconf/go-cty v1.13.1 github.com/zclconf/go-cty-yaml v1.0.3 @@ -232,7 +232,7 @@ require ( github.com/tklauser/go-sysconf v0.3.9 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect golang.org/x/crypto v0.6.0 // indirect - golang.org/x/mod v0.8.0 // indirect + golang.org/x/mod v0.9.0 // indirect golang.org/x/net v0.8.0 // indirect rsc.io/letsencrypt v0.0.3 // indirect ) diff --git a/go.sum b/go.sum index 4c6fbc8f80..610631616b 100644 --- a/go.sum +++ b/go.sum @@ -1035,8 +1035,8 @@ github.com/turbot/go-prompt v0.2.6-steampipe.0.0.20221028122246-eb118ec58d50 h1: github.com/turbot/go-prompt v0.2.6-steampipe.0.0.20221028122246-eb118ec58d50/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= github.com/turbot/steampipe-cloud-sdk-go v0.5.0 h1:sgMpUL/gLnT5/9v6LaUDITo40npvSn+RQD5maT03wRQ= github.com/turbot/steampipe-cloud-sdk-go v0.5.0/go.mod h1:8M2CspUHgCGqDCJV+FNn+boBPyLRHyzDinYnoZ/kZYw= -github.com/turbot/steampipe-plugin-sdk/v5 v5.3.0-rc.0 h1:tNKkRvzMftQ9SdPeTkbsKj+Hr515lotvyNgpu3ZAbxU= -github.com/turbot/steampipe-plugin-sdk/v5 v5.3.0-rc.0/go.mod h1:bfvdKaLwemr13EPOMbnr7/hOQOO7m4oM7HnVMdNotgY= +github.com/turbot/steampipe-plugin-sdk/v5 v5.4.0-rc.1 h1:X1mJovPJLyCiLDe12CN+XLoXica0elNz0LltSShn/WI= +github.com/turbot/steampipe-plugin-sdk/v5 v5.4.0-rc.1/go.mod h1:bfvdKaLwemr13EPOMbnr7/hOQOO7m4oM7HnVMdNotgY= github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= @@ -1205,8 +1205,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/pkg/cmdconfig/env_var_type.go b/pkg/cmdconfig/env_var_type.go new file mode 100644 index 0000000000..9d70a76054 --- /dev/null +++ b/pkg/cmdconfig/env_var_type.go @@ -0,0 +1,11 @@ +package cmdconfig + +type EnvVarType int + +const ( + String EnvVarType = iota + Int + Bool +) + +//go:generate go run golang.org/x/tools/cmd/stringer -type=EnvVarType diff --git a/pkg/cmdconfig/envvartype_string.go b/pkg/cmdconfig/envvartype_string.go new file mode 100644 index 0000000000..7d4b23579e --- /dev/null +++ b/pkg/cmdconfig/envvartype_string.go @@ -0,0 +1,25 @@ +// Code generated by "stringer -type=EnvVarType"; DO NOT EDIT. + +package cmdconfig + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[String-0] + _ = x[Int-1] + _ = x[Bool-2] +} + +const _EnvVarType_name = "StringIntBool" + +var _EnvVarType_index = [...]uint8{0, 6, 9, 13} + +func (i EnvVarType) String() string { + if i < 0 || i >= EnvVarType(len(_EnvVarType_index)-1) { + return "EnvVarType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _EnvVarType_name[_EnvVarType_index[i]:_EnvVarType_index[i+1]] +} diff --git a/pkg/cmdconfig/viper.go b/pkg/cmdconfig/viper.go index a6ac92ac7c..e69d2c0a73 100644 --- a/pkg/cmdconfig/viper.go +++ b/pkg/cmdconfig/viper.go @@ -83,15 +83,24 @@ func SetDefaultsFromConfig(configMap map[string]interface{}) { } // for keys which do not have a corresponding command flag, we need a separate defaulting mechanism +// any option setting, workspace profile property or env var which does not have a command line +// MUST have a default (unless we want the zero value to take effect) func setBaseDefaults() { defaults := map[string]interface{}{ - constants.ArgUpdateCheck: true, - constants.ArgTelemetry: constants.TelemetryInfo, + // global general options + constants.ArgTelemetry: constants.TelemetryInfo, + constants.ArgUpdateCheck: true, + + // workspace profile + constants.ArgAutoComplete: true, + constants.ArgIntrospection: constants.IntrospectionNone, + + // from global database options constants.ArgDatabasePort: constants.DatabaseDefaultPort, - constants.ArgMaxCacheSizeMb: constants.DefaultMaxCacheSizeMb, - constants.ArgAutoComplete: true, constants.ArgDatabaseStartTimeout: constants.DBStartTimeout.Seconds(), - constants.ArgIntrospection: constants.IntrospectionNone, + constants.ArgServiceCacheEnabled: true, + constants.ArgCacheMaxTtl: 300, + constants.ArgMaxCacheSizeMb: constants.DefaultMaxCacheSizeMb, } for k, v := range defaults { @@ -100,21 +109,20 @@ func setBaseDefaults() { } type envMapping struct { - configVar string - // "string", "int", "bool" - varType string + configVar []string + varType EnvVarType } // set default values of INSTALL_DIR and ModLocation from env vars func setDirectoryDefaultsFromEnv() { envMappings := map[string]envMapping{ - constants.EnvInstallDir: {constants.ArgInstallDir, "string"}, - constants.EnvWorkspaceChDir: {constants.ArgModLocation, "string"}, - constants.EnvModLocation: {constants.ArgModLocation, "string"}, + constants.EnvInstallDir: {[]string{constants.ArgInstallDir}, String}, + constants.EnvWorkspaceChDir: {[]string{constants.ArgModLocation}, String}, + constants.EnvModLocation: {[]string{constants.ArgModLocation}, String}, } - for k, v := range envMappings { - SetDefaultFromEnv(k, v.configVar, v.varType) + for envVar, mapping := range envMappings { + setConfigFromEnv(envVar, mapping.configVar, mapping.varType) } } @@ -125,40 +133,52 @@ func SetDefaultsFromEnv() { // a map of known environment variables to map to viper keys envMappings := map[string]envMapping{ - constants.EnvInstallDir: {constants.ArgInstallDir, "string"}, - constants.EnvWorkspaceChDir: {constants.ArgModLocation, "string"}, - constants.EnvModLocation: {constants.ArgModLocation, "string"}, - constants.EnvIntrospection: {constants.ArgIntrospection, "string"}, - constants.EnvTelemetry: {constants.ArgTelemetry, "string"}, - constants.EnvUpdateCheck: {constants.ArgUpdateCheck, "bool"}, - constants.EnvCloudHost: {constants.ArgCloudHost, "string"}, - constants.EnvCloudToken: {constants.ArgCloudToken, "string"}, - constants.EnvSnapshotLocation: {constants.ArgSnapshotLocation, "string"}, - constants.EnvWorkspaceDatabase: {constants.ArgWorkspaceDatabase, "string"}, - constants.EnvServicePassword: {constants.ArgServicePassword, "string"}, - constants.EnvCheckDisplayWidth: {constants.ArgCheckDisplayWidth, "int"}, - constants.EnvMaxParallel: {constants.ArgMaxParallel, "int"}, - constants.EnvQueryTimeout: {constants.ArgDatabaseQueryTimeout, "int"}, - constants.EnvDatabaseStartTimeout: {constants.ArgDatabaseStartTimeout, "int"}, - constants.EnvCacheEnabled: {constants.ArgCache, "bool"}, - constants.EnvCacheTTL: {constants.ArgCacheTtl, "int"}, + constants.EnvInstallDir: {[]string{constants.ArgInstallDir}, String}, + constants.EnvWorkspaceChDir: {[]string{constants.ArgModLocation}, String}, + constants.EnvModLocation: {[]string{constants.ArgModLocation}, String}, + constants.EnvIntrospection: {[]string{constants.ArgIntrospection}, String}, + constants.EnvTelemetry: {[]string{constants.ArgTelemetry}, String}, + constants.EnvUpdateCheck: {[]string{constants.ArgUpdateCheck}, Bool}, + constants.EnvCloudHost: {[]string{constants.ArgCloudHost}, String}, + constants.EnvCloudToken: {[]string{constants.ArgCloudToken}, String}, + constants.EnvSnapshotLocation: {[]string{constants.ArgSnapshotLocation}, String}, + constants.EnvWorkspaceDatabase: {[]string{constants.ArgWorkspaceDatabase}, String}, + constants.EnvServicePassword: {[]string{constants.ArgServicePassword}, String}, + constants.EnvCheckDisplayWidth: {[]string{constants.ArgCheckDisplayWidth}, Int}, + constants.EnvMaxParallel: {[]string{constants.ArgMaxParallel}, Int}, + constants.EnvQueryTimeout: {[]string{constants.ArgDatabaseQueryTimeout}, Int}, + constants.EnvDatabaseStartTimeout: {[]string{constants.ArgDatabaseStartTimeout}, Int}, + constants.EnvCacheTTL: {[]string{constants.ArgCacheTtl}, Int}, + constants.EnvCacheMaxTTL: {[]string{constants.ArgCacheMaxTtl}, Int}, + + // we need this value to go into different locations + constants.EnvCacheEnabled: {[]string{ + constants.ArgClientCacheEnabled, + constants.ArgServiceCacheEnabled, + }, Bool}, } - for k, v := range envMappings { - SetDefaultFromEnv(k, v.configVar, v.varType) + for envVar, v := range envMappings { + setConfigFromEnv(envVar, v.configVar, v.varType) + } +} + +func setConfigFromEnv(envVar string, configs []string, varType EnvVarType) { + for _, configVar := range configs { + SetDefaultFromEnv(envVar, configVar, varType) } } -func SetDefaultFromEnv(k string, configVar string, varType string) { +func SetDefaultFromEnv(k string, configVar string, varType EnvVarType) { if val, ok := os.LookupEnv(k); ok { switch varType { - case "string": + case String: viper.SetDefault(configVar, val) - case "bool": + case Bool: if boolVal, err := types.ToBool(val); err == nil { viper.SetDefault(configVar, boolVal) } - case "int": + case Int: if intVal, err := types.ToInt64(val); err == nil { viper.SetDefault(configVar, intVal) } diff --git a/pkg/constants/args.go b/pkg/constants/args.go index c0925dd14e..72c297458a 100644 --- a/pkg/constants/args.go +++ b/pkg/constants/args.go @@ -50,8 +50,10 @@ const ( ArgInput = "input" ArgDashboardInput = "dashboard-input" ArgMaxCacheSizeMb = "max-cache-size-mb" - ArgCache = "cache" ArgCacheTtl = "cache-ttl" + ArgClientCacheEnabled = "client-cache-enabled" + ArgServiceCacheEnabled = "service-cache-enabled" + ArgCacheMaxTtl = "cache-max-ttl" ArgIntrospection = "introspection" ArgShare = "share" ArgSnapshot = "snapshot" diff --git a/pkg/constants/db.go b/pkg/constants/db.go index ac05869364..1bc0e6cc55 100644 --- a/pkg/constants/db.go +++ b/pkg/constants/db.go @@ -6,7 +6,7 @@ import ( "github.com/turbot/steampipe/pkg/schema" ) -// dbClient constants +// Client constants const ( // MaxParallelClientInits is the number of clients to initialize in parallel // if we start initializing all clients together, it leads to bad performance on all @@ -32,7 +32,7 @@ const ( // constants for installing db and fdw images const ( DatabaseVersion = "14.2.0" - FdwVersion = "1.6.2" + FdwVersion = "1.7.0-rc.0" // PostgresImageRef is the OCI Image ref for the database binaries PostgresImageRef = "us-docker.pkg.dev/steampipe/steampipe/db:14.2.0" @@ -50,11 +50,13 @@ const ( // CommandSchema is the schema which is used to send commands to the FDW CommandSchema = "steampipe_command" - CommandTableCache = "cache" - CommandTableCacheOperationColumn = "operation" - CommandCacheOn = "cache_on" - CommandCacheOff = "cache_off" - CommandCacheClear = "cache_clear" + CommandTableSettings = "settings" + CommandTableSettingsKeyColumn = "name" + CommandTableSettingsValueColumn = "value" + + CommandTableSettingsCacheKey = "cache" + CommandTableSettingsCacheTtlKey = "cache_ttl" + CommandTableSettingsCacheClearTimeKey = "cache_clear_time" CommandTableScanMetadata = "scan_metadata" ) diff --git a/pkg/constants/env.go b/pkg/constants/env.go index 72cc4daba5..712ac1d54b 100644 --- a/pkg/constants/env.go +++ b/pkg/constants/env.go @@ -19,6 +19,7 @@ const ( EnvCheckDisplayWidth = "STEAMPIPE_CHECK_DISPLAY_WIDTH" EnvCacheEnabled = "STEAMPIPE_CACHE" EnvCacheTTL = "STEAMPIPE_CACHE_TTL" + EnvCacheMaxTTL = "STEAMPIPE_CACHE_MAX_TTL" EnvCacheMaxSize = "STEAMPIPE_CACHE_MAX_SIZE_MB" EnvQueryTimeout = "STEAMPIPE_QUERY_TIMEOUT" diff --git a/pkg/constants/metaquery_commands.go b/pkg/constants/metaquery_commands.go index 83c1a45737..31bdb890f3 100644 --- a/pkg/constants/metaquery_commands.go +++ b/pkg/constants/metaquery_commands.go @@ -20,6 +20,7 @@ const ( CmdSearchPath = ".search_path" // Set or show search-path CmdSearchPathPrefix = ".search_path_prefix" // set search path prefix CmdCache = ".cache" // cache control + CmdCacheTtl = ".cache_ttl" // set cache ttl CmdAutoComplete = ".autocomplete" // enable or disable auto complete ) diff --git a/pkg/db/db_client/db_client_cache_control.go b/pkg/db/db_client/db_client_cache_control.go deleted file mode 100644 index 97a27ff2ca..0000000000 --- a/pkg/db/db_client/db_client_cache_control.go +++ /dev/null @@ -1,34 +0,0 @@ -package db_client - -import ( - "context" - "fmt" - - "github.com/turbot/steampipe/pkg/constants" -) - -// CacheOn implements Client -func (c *DbClient) CacheOn(ctx context.Context) error { - return c.executeCacheCommand(ctx, constants.CommandCacheOn) -} - -// CacheOff implements Client -func (c *DbClient) CacheOff(ctx context.Context) error { - return c.executeCacheCommand(ctx, constants.CommandCacheOff) -} - -// CacheClear implements Client -func (c *DbClient) CacheClear(ctx context.Context) error { - return c.executeCacheCommand(ctx, constants.CommandCacheClear) -} - -func (c *DbClient) executeCacheCommand(ctx context.Context, controlCommand string) error { - _, err := c.pool.Exec(ctx, fmt.Sprintf( - "insert into %s.%s (%s) values ('%s')", - constants.CommandSchema, - constants.CommandTableCache, - constants.CommandTableCacheOperationColumn, - controlCommand, - )) - return err -} diff --git a/pkg/db/db_client/db_client_session.go b/pkg/db/db_client/db_client_session.go index 628aefa2df..ddf3639ec6 100644 --- a/pkg/db/db_client/db_client_session.go +++ b/pkg/db/db_client/db_client_session.go @@ -4,8 +4,11 @@ import ( "context" "fmt" "log" + "time" "github.com/jackc/pgx/v5/pgxpool" + "github.com/spf13/viper" + "github.com/turbot/steampipe/pkg/constants" "github.com/turbot/steampipe/pkg/db/db_common" "github.com/turbot/steampipe/pkg/utils" ) @@ -65,10 +68,27 @@ func (c *DbClient) AcquireSession(ctx context.Context) (sessionResult *db_common // make sure that we close the acquired session, in case of error defer func() { if sessionResult.Error != nil && databaseConnection != nil { + sessionResult.Session = nil databaseConnection.Release() } }() + // if the cache is set on the workspace profile + // then override the default cache setting of the connection + if viper.IsSet(constants.ArgClientCacheEnabled) { + if err := db_common.SetCacheEnabled(ctx, viper.GetBool(constants.ArgClientCacheEnabled), databaseConnection.Conn()); err != nil { + sessionResult.Error = err + return sessionResult + } + } + if viper.IsSet(constants.ArgCacheTtl) { + ttl := time.Duration(viper.GetInt(constants.ArgCacheTtl)) * time.Second + if err := db_common.SetCacheTtl(ctx, ttl, databaseConnection.Conn()); err != nil { + sessionResult.Error = err + return sessionResult + } + } + // update required session search path if needed err = c.ensureSessionSearchPath(ctx, session) if err != nil { diff --git a/pkg/db/db_common/acquire_session_result.go b/pkg/db/db_common/acquire_session_result.go index d1583b032d..af91d51266 100644 --- a/pkg/db/db_common/acquire_session_result.go +++ b/pkg/db/db_common/acquire_session_result.go @@ -3,6 +3,6 @@ package db_common import "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig" type AcquireSessionResult struct { - modconfig.ErrorAndWarnings Session *DatabaseSession + modconfig.ErrorAndWarnings } diff --git a/pkg/db/db_common/cache_control.go b/pkg/db/db_common/cache_control.go new file mode 100644 index 0000000000..351d652afb --- /dev/null +++ b/pkg/db/db_common/cache_control.go @@ -0,0 +1,41 @@ +package db_common + +import ( + "context" + "fmt" + "time" + + "github.com/jackc/pgx/v5" + "github.com/turbot/steampipe/pkg/constants" +) + +// SetCacheTtl set the cache ttl on the client +func SetCacheTtl(ctx context.Context, duration time.Duration, connection *pgx.Conn) error { + duration = duration.Truncate(time.Second) + seconds := int(duration.Seconds()) + return executeCacheCommand(ctx, constants.CommandTableSettingsCacheTtlKey, fmt.Sprint(seconds), connection) +} + +// CacheClear resets the max time on the cache +// anything below this is not accepted +func CacheClear(ctx context.Context, connection *pgx.Conn) error { + return executeCacheCommand(ctx, constants.CommandTableSettingsCacheClearTimeKey, "", connection) +} + +// SetCacheEnabled enables/disables the cache +func SetCacheEnabled(ctx context.Context, enabled bool, connection *pgx.Conn) error { + return executeCacheCommand(ctx, constants.CommandTableSettingsCacheKey, fmt.Sprint(enabled), connection) +} + +func executeCacheCommand(ctx context.Context, settingName string, settingValue string, connection *pgx.Conn) error { + _, err := connection.Exec(ctx, fmt.Sprintf( + "insert into %s.%s (%s,%s) values ('%s','%s')", + constants.CommandSchema, + constants.CommandTableSettings, + constants.CommandTableSettingsKeyColumn, + constants.CommandTableSettingsValueColumn, + settingName, + settingValue, + )) + return err +} diff --git a/pkg/db/db_common/client.go b/pkg/db/db_common/client.go index 80efded020..7634ac523c 100644 --- a/pkg/db/db_common/client.go +++ b/pkg/db/db_common/client.go @@ -3,6 +3,7 @@ package db_common import ( "context" "database/sql" + "github.com/turbot/steampipe/pkg/query/queryresult" "github.com/turbot/steampipe/pkg/schema" ) @@ -28,10 +29,6 @@ type Client interface { ExecuteSyncInSession(context.Context, *DatabaseSession, string, ...any) (*queryresult.SyncQueryResult, error) ExecuteInSession(context.Context, *DatabaseSession, func(), string, ...any) (*queryresult.Result, error) - CacheOn(context.Context) error - CacheOff(context.Context) error - CacheClear(context.Context) error - RefreshSessions(ctx context.Context) *AcquireSessionResult GetSchemaFromDB(context.Context, ...string) (*schema.Metadata, error) } diff --git a/pkg/db/db_local/start_services.go b/pkg/db/db_local/start_services.go index 2b062125c5..0c3b569a3e 100644 --- a/pkg/db/db_local/start_services.go +++ b/pkg/db/db_local/start_services.go @@ -4,7 +4,6 @@ import ( "bufio" "context" "fmt" - "github.com/turbot/steampipe/pkg/statushooks" "log" "os" "os/exec" @@ -20,6 +19,7 @@ import ( "github.com/turbot/steampipe/pkg/db/db_common" "github.com/turbot/steampipe/pkg/error_helpers" "github.com/turbot/steampipe/pkg/filepaths" + "github.com/turbot/steampipe/pkg/statushooks" "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig" "github.com/turbot/steampipe/pkg/utils" "github.com/turbot/steampipe/pluginmanager" @@ -579,8 +579,8 @@ func ensureCommandSchema(ctx context.Context, rootClient *pgx.Conn) error { commandSchemaStatements := []string{ "lock table pg_namespace;", getUpdateConnectionQuery(constants.CommandSchema, constants.CommandSchema), - fmt.Sprintf("grant insert on %s.%s to steampipe_users;", constants.CommandSchema, constants.CommandTableCache), fmt.Sprintf("grant select on %s.%s to steampipe_users;", constants.CommandSchema, constants.CommandTableScanMetadata), + fmt.Sprintf("grant insert on %s.%s to steampipe_users;", constants.CommandSchema, constants.CommandTableSettings), } if _, err := executeSqlInTransaction(ctx, rootClient, commandSchemaStatements...); err != nil { return err diff --git a/pkg/initialisation/init_data.go b/pkg/initialisation/init_data.go index a5c28bd6a5..7508b50cb8 100644 --- a/pkg/initialisation/init_data.go +++ b/pkg/initialisation/init_data.go @@ -3,6 +3,7 @@ package initialisation import ( "context" "fmt" + "github.com/jackc/pgx/v5" "github.com/spf13/viper" "github.com/turbot/go-kit/helpers" @@ -77,8 +78,7 @@ func (i *InitData) Init(ctx context.Context, invoker constants.Invoker) { if viper.GetBool(constants.ArgModInstall) { statushooks.SetStatus(ctx, "Installing workspace dependencies") opts := &modinstaller.InstallOpts{WorkspacePath: viper.GetString(constants.ArgModLocation)} - _, err := modinstaller.InstallWorkspaceDependencies(opts) - if err != nil { + if _, err := modinstaller.InstallWorkspaceDependencies(opts); err != nil { i.Result.Error = err return } diff --git a/pkg/interactive/interactive_client.go b/pkg/interactive/interactive_client.go index a7bcaf3972..87f14166e6 100644 --- a/pkg/interactive/interactive_client.go +++ b/pkg/interactive/interactive_client.go @@ -520,7 +520,7 @@ func (c *InteractiveClient) executeMetaquery(ctx context.Context, query string) // validation passed, now we will run return metaquery.Handle(ctx, &metaquery.HandlerInput{ Query: query, - Executor: client, + Client: client, Schema: c.schemaMetadata, Connections: c.initData.ConnectionMap, Prompt: c.interactivePrompt, diff --git a/pkg/query/init_data.go b/pkg/query/init_data.go index c8f0a9b84e..5b365170a1 100644 --- a/pkg/query/init_data.go +++ b/pkg/query/init_data.go @@ -3,6 +3,7 @@ package query import ( "context" "fmt" + "github.com/spf13/viper" "github.com/turbot/steampipe/pkg/constants" "github.com/turbot/steampipe/pkg/export" diff --git a/pkg/query/metaquery/definitions.go b/pkg/query/metaquery/definitions.go index f09fbe08b1..026e21eb1d 100644 --- a/pkg/query/metaquery/definitions.go +++ b/pkg/query/metaquery/definitions.go @@ -110,6 +110,12 @@ func init() { }, completer: completerFromArgsOf(constants.CmdCache), }, + constants.CmdCacheTtl: { + title: constants.CmdCacheTtl, + handler: cacheTTL, + validator: exactlyNArgs(1), + description: "Set the cache ttl (time-to-live)", + }, constants.CmdInspect: { title: constants.CmdInspect, handler: inspect, diff --git a/pkg/query/metaquery/handlers.go b/pkg/query/metaquery/handlers.go index 4f4f9cb42a..2f66577e90 100644 --- a/pkg/query/metaquery/handlers.go +++ b/pkg/query/metaquery/handlers.go @@ -5,7 +5,9 @@ import ( "fmt" "regexp" "sort" + "strconv" "strings" + "time" "github.com/c-bata/go-prompt" "github.com/jedib0t/go-pretty/v6/table" @@ -14,30 +16,23 @@ import ( typeHelpers "github.com/turbot/go-kit/types" "github.com/turbot/steampipe/pkg/cmdconfig" "github.com/turbot/steampipe/pkg/constants" + "github.com/turbot/steampipe/pkg/db/db_common" "github.com/turbot/steampipe/pkg/display" "github.com/turbot/steampipe/pkg/schema" "github.com/turbot/steampipe/pkg/steampipeconfig" + "github.com/turbot/steampipe/sperr" ) var commonCmds = []string{constants.CmdHelp, constants.CmdInspect, constants.CmdExit} -// QueryExecutor :: this is a container interface which allows us to call into the db/Client object -type QueryExecutor interface { - SetRequiredSessionSearchPath(context.Context) error - GetCurrentSearchPath(context.Context) ([]string, error) - CacheOn(context.Context) error - CacheOff(context.Context) error - CacheClear(context.Context) error -} - // HandlerInput :: input interface for the metaquery handler type HandlerInput struct { - Query string - Executor QueryExecutor + Client db_common.Client Schema *schema.Metadata Connections steampipeconfig.ConnectionDataMap Prompt *prompt.Prompt ClosePrompt func() + Query string } type PromptControl interface { Clear() @@ -63,7 +58,7 @@ func Handle(ctx context.Context, input *HandlerInput) error { func setOrGetSearchPath(ctx context.Context, input *HandlerInput) error { if len(input.args()) == 0 { - currentPath, err := input.Executor.GetCurrentSearchPath(ctx) + currentPath, err := input.Client.GetCurrentSearchPath(ctx) if err != nil { return err } @@ -88,7 +83,7 @@ func setOrGetSearchPath(ctx context.Context, input *HandlerInput) error { // now that the viper is set, call back into the client (exposed via QueryExecutor) which // already knows how to setup the search_paths with the viper values - return input.Executor.SetRequiredSessionSearchPath(ctx) + return input.Client.SetRequiredSessionSearchPath(ctx) } return nil } @@ -105,17 +100,17 @@ func setSearchPathPrefix(ctx context.Context, input *HandlerInput) error { // now that the viper is set, call back into the client (exposed via QueryExecutor) which // already knows how to setup the search_paths with the viper values - return input.Executor.SetRequiredSessionSearchPath(ctx) + return input.Client.SetRequiredSessionSearchPath(ctx) } // set the ArgHeader viper key with the boolean value evaluated from arg[0] -func setHeader(ctx context.Context, input *HandlerInput) error { +func setHeader(_ context.Context, input *HandlerInput) error { cmdconfig.Viper().Set(constants.ArgHeader, typeHelpers.StringToBool(input.args()[0])) return nil } // set the ArgMulti viper key with the boolean value evaluated from arg[0] -func setMultiLine(ctx context.Context, input *HandlerInput) error { +func setMultiLine(_ context.Context, input *HandlerInput) error { cmdconfig.Viper().Set(constants.ArgMultiLine, typeHelpers.StringToBool(input.args()[0])) return nil } @@ -123,40 +118,78 @@ func setMultiLine(ctx context.Context, input *HandlerInput) error { // controls the cache in the connected FDW func cacheControl(ctx context.Context, input *HandlerInput) error { command := input.args()[0] + // just get the active session from the connection pool + // and set the cache parameters on it. + // this works because the interactive client + // always has only one active connection due to the way it works + sessionResult := input.Client.AcquireSession(ctx) + if sessionResult.Error != nil { + return sessionResult.Error + } + conn := sessionResult.Session.Connection.Conn() + defer func() { + // we need to do this in a closure, otherwise the ctx will be evaluated immediately + // and not in call-time + sessionResult.Session.Close(false) + }() switch command { case constants.ArgOn: - return input.Executor.CacheOn(ctx) + viper.Set(constants.ArgClientCacheEnabled, true) + return db_common.SetCacheEnabled(ctx, true, sessionResult.Session.Connection.Conn()) case constants.ArgOff: - return input.Executor.CacheOff(ctx) + viper.Set(constants.ArgClientCacheEnabled, false) + return db_common.SetCacheEnabled(ctx, false, sessionResult.Session.Connection.Conn()) case constants.ArgClear: - return input.Executor.CacheClear(ctx) + return db_common.CacheClear(ctx, conn) } return fmt.Errorf("invalid command") } +// sets the cache TTL +func cacheTTL(ctx context.Context, input *HandlerInput) error { + seconds, err := strconv.Atoi(input.args()[0]) + if err != nil { + return sperr.WrapWithMessage(err, "valid value is the number of seconds") + } + if seconds < 0 { + return sperr.New("ttl must be greater than 0") + } + sessionResult := input.Client.AcquireSession(ctx) + if sessionResult.Error != nil { + return sessionResult.Error + } + defer func() { + // we need to do this in a closure, otherwise the ctx will be evaluated immediately + // and not in call-time + sessionResult.Session.Close(false) + viper.Set(constants.ArgCacheTtl, seconds) + }() + return db_common.SetCacheTtl(ctx, time.Duration(seconds)*time.Second, sessionResult.Session.Connection.Conn()) +} + // set the ArgHeader viper key with the boolean value evaluated from arg[0] -func setTiming(ctx context.Context, input *HandlerInput) error { +func setTiming(_ context.Context, input *HandlerInput) error { cmdconfig.Viper().Set(constants.ArgTiming, typeHelpers.StringToBool(input.args()[0])) return nil } // set the value of `viperKey` in `viper` with the value from `args[0]` func setViperConfigFromArg(viperKey string) handler { - return func(ctx context.Context, input *HandlerInput) error { + return func(_ context.Context, input *HandlerInput) error { cmdconfig.Viper().Set(viperKey, input.args()[0]) return nil } } // exit -func doExit(ctx context.Context, input *HandlerInput) error { +func doExit(_ context.Context, input *HandlerInput) error { input.ClosePrompt() return nil } // help -func doHelp(ctx context.Context, input *HandlerInput) error { +func doHelp(_ context.Context, _ *HandlerInput) error { commonCmdRows := getMetaQueryHelpRows(commonCmds, false) var advanceCmds []string for cmd := range metaQueryDefinitions { @@ -198,7 +231,7 @@ func getMetaQueryHelpRows(cmds []string, arrange bool) [][]string { } // list all the tables in the schema -func listTables(ctx context.Context, input *HandlerInput) error { +func listTables(_ context.Context, input *HandlerInput) error { if len(input.args()) == 0 { schemas := input.Schema.GetSchemas() @@ -226,7 +259,7 @@ To get information about the columns in a table, run %s // treat this as a wild card regexp, err := regexp.Compile(arg) if err != nil { - return fmt.Errorf("Invalid search string %s", arg) + return fmt.Errorf("invalid search string %s", arg) } header := []string{"Table", "Schema"} rows := [][]string{} @@ -291,7 +324,7 @@ func inspect(ctx context.Context, input *HandlerInput) error { // still here - the last sledge hammer is to go through // the schema names one by one - searchPath, _ := input.Executor.GetCurrentSearchPath(ctx) + searchPath, _ := input.Client.GetCurrentSearchPath(ctx) // add the temporary schema to the search_path so that it becomes searchable // for the next step @@ -327,7 +360,7 @@ To get information about the columns in a table, run %s return inspectTable(tokens[0], tokens[1], input) } -func listConnections(ctx context.Context, input *HandlerInput) error { +func listConnections(_ context.Context, input *HandlerInput) error { header := []string{"connection", "plugin"} var rows [][]string @@ -383,7 +416,7 @@ func inspectConnection(connectionName string, input *HandlerInput) bool { return true } -func clearScreen(ctx context.Context, input *HandlerInput) error { +func clearScreen(_ context.Context, input *HandlerInput) error { input.Prompt.ClearScreen() return nil } @@ -394,11 +427,11 @@ func inspectTable(connectionName string, tableName string, input *HandlerInput) schema, found := input.Schema.Schemas[connectionName] if !found { - return fmt.Errorf("Could not find connection called '%s'", connectionName) + return fmt.Errorf("could not find connection called '%s'", connectionName) } tableSchema, found := schema[tableName] if !found { - return fmt.Errorf("Could not find table '%s' in '%s'", tableName, connectionName) + return fmt.Errorf("could not find table '%s' in '%s'", tableName, connectionName) } for _, columnSchema := range tableSchema.Columns { @@ -439,7 +472,7 @@ func buildTable(rows [][]string, autoMerge bool) string { return t.Render() } -func setAutoComplete(ctx context.Context, input *HandlerInput) error { +func setAutoComplete(_ context.Context, input *HandlerInput) error { cmdconfig.Viper().Set(constants.ArgAutoComplete, typeHelpers.StringToBool(input.args()[0])) return nil } diff --git a/pkg/query/metaquery/validators.go b/pkg/query/metaquery/validators.go index 3835f364a6..79b5d22914 100644 --- a/pkg/query/metaquery/validators.go +++ b/pkg/query/metaquery/validators.go @@ -13,8 +13,8 @@ import ( // ValidationResult :: response for Validate type ValidationResult struct { Err error - ShouldRun bool Message string + ShouldRun bool } type validator func(val []string) ValidationResult diff --git a/pkg/steampipeconfig/modconfig/workspace_profile.go b/pkg/steampipeconfig/modconfig/workspace_profile.go index 206bd48098..4cf1ea479c 100644 --- a/pkg/steampipeconfig/modconfig/workspace_profile.go +++ b/pkg/steampipeconfig/modconfig/workspace_profile.go @@ -200,7 +200,7 @@ func (p *WorkspaceProfile) ConfigMap(cmd *cobra.Command) map[string]interface{} res.SetBoolItem(p.Input, constants.ArgInput) res.SetBoolItem(p.Progress, constants.ArgProgress) res.SetStringItem(p.Theme, constants.ArgTheme) - res.SetBoolItem(p.Cache, constants.ArgCache) + res.SetBoolItem(p.Cache, constants.ArgClientCacheEnabled) res.SetIntItem(p.CacheTTL, constants.ArgCacheTtl) if cmd.Name() == constants.CmdNameQuery && p.QueryOptions != nil { diff --git a/pkg/steampipeconfig/options/database.go b/pkg/steampipeconfig/options/database.go index 88e1d9b2d8..5c576a75f9 100644 --- a/pkg/steampipeconfig/options/database.go +++ b/pkg/steampipeconfig/options/database.go @@ -17,7 +17,6 @@ type Database struct { SearchPathPrefix *string `hcl:"search_path_prefix"` Cache *bool `hcl:"cache"` CacheMaxTtl *int `hcl:"cache_max_ttl"` - CacheDefaultTtl *int `hcl:"cache_default_ttl"` CacheMaxSizeMb *int `hcl:"cache_max_size_mb"` } @@ -40,6 +39,20 @@ func (d *Database) ConfigMap() map[string]interface{} { } else { res[constants.ArgDatabaseStartTimeout] = constants.DBStartTimeout.Seconds() } + + if d.SearchPathPrefix != nil { + // convert from string to array + res[constants.ArgSearchPathPrefix] = searchPathToArray(*d.SearchPathPrefix) + } + if d.Cache != nil { + res[constants.ArgServiceCacheEnabled] = d.Cache + } + if d.CacheMaxTtl != nil { + res[constants.ArgCacheMaxTtl] = d.CacheMaxTtl + } + if d.CacheMaxSizeMb != nil { + res[constants.ArgMaxCacheSizeMb] = d.CacheMaxSizeMb + } return res } @@ -57,6 +70,21 @@ func (d *Database) Merge(otherOptions Options) { if o.SearchPath != nil { d.SearchPath = o.SearchPath } + if o.StartTimeout != nil { + d.StartTimeout = o.StartTimeout + } + if o.SearchPathPrefix != nil { + d.SearchPathPrefix = o.SearchPathPrefix + } + if o.Cache != nil { + d.Cache = o.Cache + } + if o.CacheMaxSizeMb != nil { + d.CacheMaxSizeMb = o.CacheMaxSizeMb + } + if o.CacheMaxTtl != nil { + d.CacheMaxTtl = o.CacheMaxTtl + } } } @@ -85,5 +113,25 @@ func (d *Database) String() string { } else { str = append(str, fmt.Sprintf(" ServiceStartTimeout: %d", *d.StartTimeout)) } + if d.SearchPathPrefix == nil { + str = append(str, " SearchPathPrefix: nil") + } else { + str = append(str, fmt.Sprintf(" SearchPathPrefix: %s", *d.SearchPathPrefix)) + } + if d.Cache == nil { + str = append(str, " Cache: nil") + } else { + str = append(str, fmt.Sprintf(" Cache: %t", *d.Cache)) + } + if d.CacheMaxSizeMb == nil { + str = append(str, " CacheMaxSizeMb: nil") + } else { + str = append(str, fmt.Sprintf(" CacheMaxSizeMb: %d", *d.CacheMaxSizeMb)) + } + if d.CacheMaxTtl == nil { + str = append(str, " CacheMaxTtl: nil") + } else { + str = append(str, fmt.Sprintf(" CacheMaxTtl: %d", *d.CacheMaxTtl)) + } return strings.Join(str, "\n") } diff --git a/pluginmanager_service/plugin_manager.go b/pluginmanager_service/plugin_manager.go index a0941e9c0b..85fbfa3c94 100644 --- a/pluginmanager_service/plugin_manager.go +++ b/pluginmanager_service/plugin_manager.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" + "github.com/spf13/viper" "github.com/turbot/go-kit/helpers" sdkgrpc "github.com/turbot/steampipe-plugin-sdk/v5/grpc" sdkproto "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" @@ -310,10 +311,8 @@ func (m *PluginManager) handleUpdatedConnections(updatedConnections map[string][ req = &sdkproto.UpdateConnectionConfigsRequest{} } - for _, connection := range connections { - // add to updateConnectionConfigsRequest - req.Changed = append(req.Changed, connection) - } + // add to updateConnectionConfigsRequest + req.Changed = append(req.Changed, connections...) // write back to map requestMap[p] = req } @@ -651,14 +650,26 @@ func (m *PluginManager) startPlugin(connectionName string) (_ *plugin.Client, _ if supportedOperations.MultipleConnections { // send the connection config for all connections for this plugin // this returns a list of all connections provided by this plugin - connectionNames, err = m.setAllConnectionConfigs(pluginClient, pluginName) + connectionNames, err = m.setAllConnectionConfigs(pluginClient, pluginName, supportedOperations) + if err != nil { + log.Printf("[WARN] failed to set connection config: %s", err.Error()) + return nil, nil, err + } } else { // send the connection config using legacy single connection function err = m.setSingleConnectionConfig(pluginClient, connectionName) + if err != nil { + log.Printf("[WARN] failed to set connection config: %s", err.Error()) + return nil, nil, err + } } - if err != nil { - log.Printf("[WARN] failed to set connection config: %s", err.Error()) - return nil, nil, err + // if this plugin supports setting cache options, do so + if supportedOperations.SetCacheOptions { + err = m.setCacheOptions(pluginClient) + if err != nil { + log.Printf("[WARN] failed to set cache options: %s", err.Error()) + return nil, nil, err + } } reattach := proto.NewReattachConfig(pluginName, client.ReattachConfig(), proto.SupportedOperationsFromSdk(supportedOperations), connectionNames) @@ -702,16 +713,23 @@ func (m *PluginManager) getConnectionsForPlugin(pluginName string) []string { // set connection config for multiple connection, for compatible plugins // NOTE: we DO NOT set connection config for aggregator connections -func (m *PluginManager) setAllConnectionConfigs(pluginClient *sdkgrpc.PluginClient, pluginName string) ([]string, error) { +func (m *PluginManager) setAllConnectionConfigs(pluginClient *sdkgrpc.PluginClient, pluginName string, supportedOperations *sdkproto.GetSupportedOperationsResponse) ([]string, error) { configs, ok := m.pluginConnectionConfigMap[pluginName] if !ok { // should never happen return nil, fmt.Errorf("no config loaded for plugin '%s'", pluginName) } req := &sdkproto.SetAllConnectionConfigsRequest{ - Configs: configs, - MaxCacheSizeMb: m.pluginCacheSizeMap[pluginName], + Configs: configs, + // NOTE: set MaxCacheSizeMb to -1so that query cache is not created until we call SetCacheOptions (if supported) + MaxCacheSizeMb: -1, + } + // if plugin _does not_ support setting the cache options separately, pass the max size now + // (if it does support SetCacheOptions, it will be called after we return) + if !supportedOperations.SetCacheOptions { + req.MaxCacheSizeMb = m.pluginCacheSizeMap[pluginName] } + // build list of connections connections := make([]string, len(configs)) for i, config := range configs { @@ -736,6 +754,16 @@ func (m *PluginManager) setSingleConnectionConfig(pluginClient *sdkgrpc.PluginCl return pluginClient.SetConnectionConfig(req) } +func (m *PluginManager) setCacheOptions(pluginClient *sdkgrpc.PluginClient) error { + req := &sdkproto.SetCacheOptionsRequest{ + Enabled: viper.GetBool(constants.ArgServiceCacheEnabled), + Ttl: viper.GetInt64(constants.ArgCacheMaxTtl), + MaxSizeMb: viper.GetInt64(constants.ArgMaxCacheSizeMb), + } + _, err := pluginClient.SetCacheOptions(req) + return err +} + // update the schema for the specified connection // called from the message server after receiving a PluginMessageType_SCHEMA_UPDATED message from plugin func (m *PluginManager) updateConnectionSchema(ctx context.Context, connectionName string) { diff --git a/tests/acceptance/test_files/service_and_plugin.bats b/tests/acceptance/test_files/service_and_plugin.bats index b377bfd2c6..abcb9e639e 100644 --- a/tests/acceptance/test_files/service_and_plugin.bats +++ b/tests/acceptance/test_files/service_and_plugin.bats @@ -308,7 +308,7 @@ load "$LIB_BATS_SUPPORT/load.bash" # remove the config file rm -f $STEAMPIPE_INSTALL_DIR/config/chaos2.json - assert_output --partial 'Error: duplicate connection name' + assert_output --partial 'duplicate connection name' } @test "steampipe yaml connection config" {