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  }