github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/steampipeconfig/steampipeconfig.go (about) 1 package steampipeconfig 2 3 import ( 4 "fmt" 5 "log" 6 "os" 7 "strings" 8 9 "github.com/hashicorp/go-version" 10 "github.com/turbot/go-kit/types" 11 typehelpers "github.com/turbot/go-kit/types" 12 "github.com/turbot/steampipe-plugin-sdk/v5/sperr" 13 "github.com/turbot/steampipe/pkg/constants" 14 "github.com/turbot/steampipe/pkg/error_helpers" 15 "github.com/turbot/steampipe/pkg/filepaths" 16 "github.com/turbot/steampipe/pkg/ociinstaller" 17 "github.com/turbot/steampipe/pkg/ociinstaller/versionfile" 18 "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig" 19 "github.com/turbot/steampipe/pkg/steampipeconfig/options" 20 ) 21 22 // SteampipeConfig is a struct to hold Connection map and Steampipe options 23 type SteampipeConfig struct { 24 // map of plugin configs, keyed by plugin image ref 25 // (for each image ref we store an array of configs) 26 Plugins map[string][]*modconfig.Plugin 27 // map of plugin configs, keyed by plugin instance 28 PluginsInstances map[string]*modconfig.Plugin 29 // map of connection name to partially parsed connection config 30 Connections map[string]*modconfig.Connection 31 32 // Steampipe options 33 DefaultConnectionOptions *options.Connection 34 DatabaseOptions *options.Database 35 DashboardOptions *options.GlobalDashboard 36 TerminalOptions *options.Terminal 37 GeneralOptions *options.General 38 PluginOptions *options.Plugin 39 // map of installed plugin versions, keyed by plugin image ref 40 PluginVersions map[string]*versionfile.InstalledVersion 41 } 42 43 func NewSteampipeConfig(commandName string) *SteampipeConfig { 44 return &SteampipeConfig{ 45 Connections: make(map[string]*modconfig.Connection), 46 Plugins: make(map[string][]*modconfig.Plugin), 47 PluginsInstances: make(map[string]*modconfig.Plugin), 48 } 49 } 50 51 // Validate validates all connections 52 // connections with validation errors are removed 53 func (c *SteampipeConfig) Validate() (validationWarnings, validationErrors []string) { 54 for connectionName, connection := range c.Connections { 55 // if the connection is an aggregator, populate the child connections 56 // this resolves any wildcards in the connection list 57 if connection.Type == modconfig.ConnectionTypeAggregator { 58 aggregatorFailures := connection.PopulateChildren(c.Connections) 59 validationWarnings = append(validationWarnings, aggregatorFailures...) 60 } 61 w, e := connection.Validate(c.Connections) 62 validationWarnings = append(validationWarnings, w...) 63 validationErrors = append(validationErrors, e...) 64 // if this connection validation remove 65 if len(e) > 0 { 66 delete(c.Connections, connectionName) 67 } 68 } 69 70 return 71 } 72 73 // ConfigMap creates a config map to pass to viper 74 func (c *SteampipeConfig) ConfigMap() map[string]interface{} { 75 res := modconfig.ConfigMap{} 76 77 // build flat config map with order or precedence (low to high): general, database, terminal 78 // this means if (for example) 'search-path' is set in both database and terminal options, 79 // the value from terminal options will have precedence 80 // however, we also store all values scoped by their options type, so we will store: 81 // 'database.search-path', 'terminal.search-path' AND 'search-path' (which will be equal to 'terminal.search-path') 82 if c.GeneralOptions != nil { 83 res.PopulateConfigMapForOptions(c.GeneralOptions) 84 } 85 if c.DatabaseOptions != nil { 86 res.PopulateConfigMapForOptions(c.DatabaseOptions) 87 } 88 if c.DashboardOptions != nil { 89 res.PopulateConfigMapForOptions(c.DashboardOptions) 90 } 91 if c.PluginOptions != nil { 92 res.PopulateConfigMapForOptions(c.PluginOptions) 93 } 94 95 return res 96 } 97 98 func (c *SteampipeConfig) SetOptions(opts options.Options) (errorsAndWarnings error_helpers.ErrorAndWarnings) { 99 errorsAndWarnings = error_helpers.NewErrorsAndWarning(nil) 100 101 switch o := opts.(type) { 102 case *options.Database: 103 if c.DatabaseOptions == nil { 104 c.DatabaseOptions = o 105 } else { 106 c.DatabaseOptions.Merge(o) 107 } 108 case *options.GlobalDashboard: 109 if c.DashboardOptions == nil { 110 c.DashboardOptions = o 111 } else { 112 c.DashboardOptions.Merge(o) 113 } 114 case *options.General: 115 if c.GeneralOptions == nil { 116 c.GeneralOptions = o 117 } else { 118 c.GeneralOptions.Merge(o) 119 } 120 case *options.Plugin: 121 if c.PluginOptions == nil { 122 c.PluginOptions = o 123 } else { 124 c.PluginOptions.Merge(o) 125 } 126 } 127 return errorsAndWarnings 128 } 129 130 var defaultCacheEnabled = true 131 var defaultTTL = 300 132 133 // if default connection options have been set, assign them to any connection which do not define specific options 134 func (c *SteampipeConfig) setDefaultConnectionOptions() { 135 if c.DefaultConnectionOptions == nil { 136 c.DefaultConnectionOptions = &options.Connection{} 137 } 138 139 // precedence for the default is (high to low): 140 // env var 141 // default connection config 142 // base default 143 144 // As connection options are alco loaded by the FDW, which does not have access to viper, 145 // we must manually apply env var defaulting 146 147 // if CacheEnabledEnvVar is set, overwrite the value in DefaultConnectionOptions 148 if envStr, ok := os.LookupEnv(constants.EnvCacheEnabled); ok { 149 if parsedEnv, err := types.ToBool(envStr); err == nil { 150 c.DefaultConnectionOptions.Cache = &parsedEnv 151 } 152 } 153 if c.DefaultConnectionOptions.Cache == nil { 154 // if DefaultConnectionOptions.Cache value is NOT set, default it to true 155 c.DefaultConnectionOptions.Cache = &defaultCacheEnabled 156 } 157 158 // if CacheTTLEnvVar is set, overwrite the value in DefaultConnectionOptions 159 if ttlString, ok := os.LookupEnv(constants.EnvCacheTTL); ok { 160 if parsed, err := types.ToInt64(ttlString); err == nil { 161 ttl := int(parsed) 162 c.DefaultConnectionOptions.CacheTTL = &ttl 163 } 164 } 165 166 if c.DefaultConnectionOptions.CacheTTL == nil { 167 // if DefaultConnectionOptions.CacheTTL value is NOT set, default it to true 168 c.DefaultConnectionOptions.CacheTTL = &defaultTTL 169 } 170 } 171 172 func (c *SteampipeConfig) GetConnectionOptions(connectionName string) *options.Connection { 173 log.Printf("[TRACE] GetConnectionOptions for %s", connectionName) 174 connection, ok := c.Connections[connectionName] 175 if !ok { 176 log.Printf("[TRACE] connection %s not found - returning default \n%v", connectionName, c.DefaultConnectionOptions) 177 // if we can't find connection, just return defaults 178 return c.DefaultConnectionOptions 179 } 180 // does the connection have connection options set - if not, return the default 181 if connection.Options == nil { 182 log.Printf("[TRACE] connection %s has no options - returning default \n%v", connectionName, c.DefaultConnectionOptions) 183 return c.DefaultConnectionOptions 184 } 185 // so there are connection options, ensure all fields are set 186 log.Printf("[TRACE] connection %s defines options %v", connectionName, connection.Options) 187 188 // create a copy of the options to return 189 result := &options.Connection{ 190 Cache: c.DefaultConnectionOptions.Cache, 191 CacheTTL: c.DefaultConnectionOptions.CacheTTL, 192 } 193 if connection.Options.Cache != nil { 194 log.Printf("[TRACE] connection defines cache option %v", *connection.Options.Cache) 195 result.Cache = connection.Options.Cache 196 } 197 if connection.Options.CacheTTL != nil { 198 result.CacheTTL = connection.Options.CacheTTL 199 } 200 201 return result 202 } 203 204 func (c *SteampipeConfig) String() string { 205 var connectionStrings []string 206 for _, c := range c.Connections { 207 connectionStrings = append(connectionStrings, c.String()) 208 } 209 210 str := fmt.Sprintf(` 211 Connections: 212 %s 213 ---- 214 DefaultConnectionOptions: 215 %s`, strings.Join(connectionStrings, "\n"), c.DefaultConnectionOptions.String()) 216 217 if c.DatabaseOptions != nil { 218 str += fmt.Sprintf(` 219 220 DatabaseOptions: 221 %s`, c.DatabaseOptions.String()) 222 } 223 if c.DashboardOptions != nil { 224 str += fmt.Sprintf(` 225 226 DashboardOptions: 227 %s`, c.DashboardOptions.String()) 228 } 229 if c.TerminalOptions != nil { 230 str += fmt.Sprintf(` 231 232 TerminalOptions: 233 %s`, c.TerminalOptions.String()) 234 } 235 if c.GeneralOptions != nil { 236 str += fmt.Sprintf(` 237 238 GeneralOptions: 239 %s`, c.GeneralOptions.String()) 240 } 241 if c.PluginOptions != nil { 242 str += fmt.Sprintf(` 243 244 PluginOptions: 245 %s`, c.PluginOptions.String()) 246 } 247 248 return str 249 } 250 251 func (c *SteampipeConfig) ConnectionsForPlugin(pluginLongName string, pluginVersion *version.Version) []*modconfig.Connection { 252 var res []*modconfig.Connection 253 for _, con := range c.Connections { 254 // extract constraint from plugin 255 ref := ociinstaller.NewSteampipeImageRef(con.Plugin) 256 org, plugin, constraint := ref.GetOrgNameAndConstraint() 257 longName := fmt.Sprintf("%s/%s", org, plugin) 258 if longName == pluginLongName { 259 if constraint == "latest" { 260 res = append(res, con) 261 } else { 262 connectionPluginVersion, err := version.NewVersion(constraint) 263 if err != nil && connectionPluginVersion.LessThanOrEqual(pluginVersion) { 264 res = append(res, con) 265 } 266 } 267 } 268 } 269 return res 270 } 271 272 // ConnectionNames returns a flat list of connection names 273 func (c *SteampipeConfig) ConnectionNames() []string { 274 res := make([]string, len(c.Connections)) 275 idx := 0 276 for connectionName := range c.Connections { 277 res[idx] = connectionName 278 idx++ 279 } 280 return res 281 } 282 283 func (c *SteampipeConfig) ConnectionList() []*modconfig.Connection { 284 res := make([]*modconfig.Connection, len(c.Connections)) 285 idx := 0 286 for _, c := range c.Connections { 287 res[idx] = c 288 idx++ 289 } 290 return res 291 } 292 293 // add a plugin config to PluginsInstances and Plugins 294 // NOTE: this returns an error if we already have a config with the same label 295 func (c *SteampipeConfig) addPlugin(plugin *modconfig.Plugin) error { 296 if existingPlugin, exists := c.PluginsInstances[plugin.Instance]; exists { 297 return duplicatePluginError(existingPlugin, plugin) 298 } 299 300 // get the image ref to key the map 301 imageRef := plugin.Plugin 302 303 pluginVersion, ok := c.PluginVersions[imageRef] 304 if !ok { 305 // just log it 306 log.Printf("[WARN] addPlugin called for plugin '%s' which is not installed", imageRef) 307 return nil 308 } 309 // populate the version from the plugin version file data 310 plugin.Version = pluginVersion.Version 311 312 // add to list of plugin configs for this image ref 313 c.Plugins[imageRef] = append(c.Plugins[imageRef], plugin) 314 c.PluginsInstances[plugin.Instance] = plugin 315 316 return nil 317 } 318 319 func duplicatePluginError(existingPlugin, newPlugin *modconfig.Plugin) error { 320 return sperr.New("duplicate plugin instance: '%s'\n\t(%s:%d)\n\t(%s:%d)", 321 existingPlugin.Instance, *existingPlugin.FileName, *existingPlugin.StartLineNumber, 322 *newPlugin.FileName, *newPlugin.StartLineNumber) 323 } 324 325 // ensure we have a plugin config struct for all plugins mentioned in connection config, 326 // even if there is not an explicit HCL config for it 327 // NOTE: this populates the Plugin and PluginInstance field of the connections 328 func (c *SteampipeConfig) initializePlugins() { 329 for _, connection := range c.Connections { 330 plugin, err := c.resolvePluginInstanceForConnection(connection) 331 if err != nil { 332 log.Printf("[WARN] cannot resolve plugin for connection '%s': %s", connection.Name, err.Error()) 333 connection.Error = err 334 continue 335 } 336 // if plugin is nil, but there is no error, it must be referring to a plugin which has no instance config 337 // and is not installed - set the plugin error 338 if plugin == nil { 339 // set the Plugin to the image ref of the plugin 340 connection.Plugin = ociinstaller.NewSteampipeImageRef(connection.PluginAlias).DisplayImageRef() 341 connection.Error = fmt.Errorf(constants.ConnectionErrorPluginNotInstalled) 342 log.Printf("[INFO] connection '%s' requires plugin '%s' which is not loaded and has no instance config", connection.Name, connection.PluginAlias) 343 continue 344 } 345 // set the PluginAlias on the connection 346 347 // set the PluginAlias and Plugin property on the connection 348 pluginImageRef := plugin.Plugin 349 connection.PluginAlias = plugin.Alias 350 connection.Plugin = pluginImageRef 351 if pluginPath, _ := filepaths.GetPluginPath(pluginImageRef, plugin.Alias); pluginPath != "" { 352 // plugin is installed - set the instance and the plugin path 353 connection.PluginInstance = &plugin.Instance 354 connection.PluginPath = &pluginPath 355 } else { 356 // set the plugin error 357 connection.Error = fmt.Errorf(constants.ConnectionErrorPluginNotInstalled) 358 // leave instance unset 359 log.Printf("[INFO] connection '%s' requires plugin '%s' - this is not installed", connection.Name, plugin.Alias) 360 } 361 362 } 363 364 } 365 366 /* 367 find a plugin instance which satisfies the Plugin field of the connection 368 resolution steps: 369 1) if PluginInstance is already set, the connection must have a HCL reference to a plugin block 370 - just validate the block exists 371 2) handle local??? 372 3) have we already created a default plugin config for this plugin 373 4) is there a SINGLE plugin config for the image ref resolved from the connection 'plugin' field 374 NOTE: if there is more than one config for the plugin this is an error 375 5) create a default config for the plugin (with the label set to the image ref) 376 */ 377 func (c *SteampipeConfig) resolvePluginInstanceForConnection(connection *modconfig.Connection) (*modconfig.Plugin, error) { 378 // NOTE: at this point, c.Plugin is NOT populated, only either c.PluginAlias or c.PluginInstance 379 // we populate c.Plugin AFTER resolving the plugin 380 381 // if PluginInstance is already set, the connection must have a HCL reference to a plugin block 382 // find the block 383 if connection.PluginInstance != nil { 384 p := c.PluginsInstances[*connection.PluginInstance] 385 if p == nil { 386 return nil, fmt.Errorf("connection '%s' specifies 'plugin=\"plugin.%s\"' but 'plugin.%s' does not exist. (%s:%d)", 387 connection.Name, 388 typehelpers.SafeString(connection.PluginInstance), 389 typehelpers.SafeString(connection.PluginInstance), 390 connection.DeclRange.Filename, 391 connection.DeclRange.Start.Line, 392 ) 393 } 394 return p, nil 395 } 396 397 // resolve the image ref (this handles the special case of locally developed plugins in the plugins/local folder) 398 imageRef := modconfig.ResolvePluginImageRef(connection.PluginAlias) 399 400 // verify the plugin is installed - if not return nil 401 if _, ok := c.PluginVersions[imageRef]; !ok { 402 // tactical - check if the plugin binary exists 403 pluginBinaryPath := filepaths.PluginBinaryPath(imageRef, connection.PluginAlias) 404 if _, err := os.Stat(pluginBinaryPath); err != nil { 405 log.Printf("[INFO] plugin '%s' is not installed", imageRef) 406 return nil, nil 407 } 408 409 // so the plugin binary exists but it does not exist in the versions.json 410 // this is probably because it has been built locally - add a version entry with version set to 'local' 411 c.PluginVersions[imageRef] = &versionfile.InstalledVersion{ 412 Version: "local", 413 } 414 } 415 416 // how many plugin instances are there for this image ref? 417 pluginsForImageRef := c.Plugins[imageRef] 418 419 switch len(pluginsForImageRef) { 420 case 0: 421 // there is no plugin instance for this connection - add an implicit plugin instance 422 p := modconfig.NewImplicitPlugin(connection, imageRef) 423 424 // now add to our map 425 if err := c.addPlugin(p); err != nil { 426 // log the error but do not return it - we 427 return nil, err 428 } 429 return p, nil 430 431 case 1: 432 // ok we can resolve 433 return pluginsForImageRef[0], nil 434 435 default: 436 // so there is more than one plugin config for the plugin, and the connection DOES NOT specify which one to use 437 // this is an error 438 var strs = make([]string, len(pluginsForImageRef)) 439 for i, p := range pluginsForImageRef { 440 strs[i] = fmt.Sprintf("\t%s (%s:%d)", p.Instance, *p.FileName, *p.StartLineNumber) 441 } 442 return nil, sperr.New("connection '%s' specifies 'plugin=\"%s\"' but the correct instance cannot be uniquely resolved. There are %d plugin instances matching that configuration:\n%s", connection.Name, connection.PluginAlias, len(pluginsForImageRef), strings.Join(strs, "\n")) 443 } 444 }