github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/internal/config/application.go (about)

     1  package config
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"path"
     8  	"reflect"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/adrg/xdg"
    13  	"github.com/mitchellh/go-homedir"
    14  	"github.com/nextlinux/gosbom/gosbom/pkg/cataloger"
    15  	golangCataloger "github.com/nextlinux/gosbom/gosbom/pkg/cataloger/golang"
    16  	"github.com/nextlinux/gosbom/gosbom/pkg/cataloger/kernel"
    17  	"github.com/nextlinux/gosbom/internal"
    18  	"github.com/nextlinux/gosbom/internal/log"
    19  	"github.com/spf13/viper"
    20  	"gopkg.in/yaml.v2"
    21  
    22  	"github.com/anchore/go-logger"
    23  )
    24  
    25  var (
    26  	ErrApplicationConfigNotFound = fmt.Errorf("application config not found")
    27  	catalogerEnabledDefault      = false
    28  )
    29  
    30  type defaultValueLoader interface {
    31  	loadDefaultValues(*viper.Viper)
    32  }
    33  
    34  type parser interface {
    35  	parseConfigValues() error
    36  }
    37  
    38  // Application is the main gosbom application configuration.
    39  type Application struct {
    40  	// the location where the application config was read from (either from -c or discovered while loading); default .gosbom.yaml
    41  	ConfigPath string `yaml:"configPath,omitempty" json:"configPath" mapstructure:"config"`
    42  	Verbosity  uint   `yaml:"verbosity,omitempty" json:"verbosity" mapstructure:"verbosity"`
    43  	// -q, indicates to not show any status output to stderr (ETUI or logging UI)
    44  	Quiet                  bool               `yaml:"quiet" json:"quiet" mapstructure:"quiet"`
    45  	Outputs                []string           `yaml:"output" json:"output" mapstructure:"output"`                                           // -o, the format to use for output
    46  	OutputTemplatePath     string             `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output
    47  	File                   string             `yaml:"file" json:"file" mapstructure:"file"`                                                 // --file, the file to write report output to
    48  	CheckForAppUpdate      bool               `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
    49  	Dev                    development        `yaml:"dev" json:"dev" mapstructure:"dev"`
    50  	Log                    logging            `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options
    51  	Catalogers             []string           `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"`
    52  	Package                pkg                `yaml:"package" json:"package" mapstructure:"package"`
    53  	Golang                 golang             `yaml:"golang" json:"golang" mapstructure:"golang"`
    54  	LinuxKernel            linuxKernel        `yaml:"linux-kernel" json:"linux-kernel" mapstructure:"linux-kernel"`
    55  	Attest                 attest             `yaml:"attest" json:"attest" mapstructure:"attest"`
    56  	FileMetadata           FileMetadata       `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"`
    57  	FileClassification     fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"`
    58  	FileContents           fileContents       `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"`
    59  	Secrets                secrets            `yaml:"secrets" json:"secrets" mapstructure:"secrets"`
    60  	Registry               registry           `yaml:"registry" json:"registry" mapstructure:"registry"`
    61  	Exclusions             []string           `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
    62  	Platform               string             `yaml:"platform" json:"platform" mapstructure:"platform"`
    63  	Name                   string             `yaml:"name" json:"name" mapstructure:"name"`
    64  	SourceName             string             `yaml:"source-name" json:"source-name" mapstructure:"source-name"`
    65  	SourceVersion          string             `yaml:"source-version" json:"source-version" mapstructure:"source-version"`
    66  	Parallelism            int                `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"`                                           // the number of catalog workers to run in parallel
    67  	DefaultImagePullSource string             `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"` // specify default image pull source
    68  }
    69  
    70  func (cfg Application) ToCatalogerConfig() cataloger.Config {
    71  	return cataloger.Config{
    72  		Search: cataloger.SearchConfig{
    73  			IncludeIndexedArchives:   cfg.Package.SearchIndexedArchives,
    74  			IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives,
    75  			Scope:                    cfg.Package.Cataloger.ScopeOpt,
    76  		},
    77  		Catalogers:  cfg.Catalogers,
    78  		Parallelism: cfg.Parallelism,
    79  		Golang: golangCataloger.NewGoCatalogerOpts().
    80  			WithSearchLocalModCacheLicenses(cfg.Golang.SearchLocalModCacheLicenses).
    81  			WithLocalModCacheDir(cfg.Golang.LocalModCacheDir).
    82  			WithSearchRemoteLicenses(cfg.Golang.SearchRemoteLicenses).
    83  			WithProxy(cfg.Golang.Proxy).
    84  			WithNoProxy(cfg.Golang.NoProxy),
    85  		LinuxKernel: kernel.LinuxCatalogerConfig{
    86  			CatalogModules: cfg.LinuxKernel.CatalogModules,
    87  		},
    88  	}
    89  }
    90  
    91  func (cfg *Application) LoadAllValues(v *viper.Viper, configPath string) error {
    92  	// priority order: viper.Set, flag, env, config, kv, defaults
    93  	// flags have already been loaded into viper by command construction
    94  
    95  	// check if user specified config; otherwise read all possible paths
    96  	if err := loadConfig(v, configPath); err != nil {
    97  		var notFound *viper.ConfigFileNotFoundError
    98  		if errors.As(err, &notFound) {
    99  			log.Debugf("no config file found, using defaults")
   100  		} else {
   101  			return fmt.Errorf("unable to load config: %w", err)
   102  		}
   103  	}
   104  
   105  	// load default config values into viper
   106  	loadDefaultValues(v)
   107  
   108  	// load environment variables
   109  	v.SetEnvPrefix(internal.ApplicationName)
   110  	v.AllowEmptyEnv(true)
   111  	v.AutomaticEnv()
   112  
   113  	// unmarshal fully populated viper object onto config
   114  	err := v.Unmarshal(cfg)
   115  	if err != nil {
   116  		return err
   117  	}
   118  
   119  	// Convert all populated config options to their internal application values ex: scope string => scopeOpt source.Scope
   120  	return cfg.parseConfigValues()
   121  }
   122  
   123  func (cfg *Application) parseConfigValues() error {
   124  	// parse options on this struct
   125  	var catalogers []string
   126  	for _, c := range cfg.Catalogers {
   127  		for _, f := range strings.Split(c, ",") {
   128  			catalogers = append(catalogers, strings.TrimSpace(f))
   129  		}
   130  	}
   131  	sort.Strings(catalogers)
   132  	cfg.Catalogers = catalogers
   133  
   134  	// parse application config options
   135  	for _, optionFn := range []func() error{
   136  		cfg.parseLogLevelOption,
   137  		cfg.parseFile,
   138  	} {
   139  		if err := optionFn(); err != nil {
   140  			return err
   141  		}
   142  	}
   143  
   144  	if err := checkDefaultSourceValues(cfg.DefaultImagePullSource); err != nil {
   145  		return err
   146  	}
   147  
   148  	if cfg.Name != "" {
   149  		log.Warnf("name parameter is deprecated. please use: source-name. name will be removed in a future version")
   150  		if cfg.SourceName == "" {
   151  			cfg.SourceName = cfg.Name
   152  		}
   153  	}
   154  
   155  	// check for valid default source options
   156  	// parse nested config options
   157  	// for each field in the configuration struct, see if the field implements the parser interface
   158  	// note: the app config is a pointer, so we need to grab the elements explicitly (to traverse the address)
   159  	value := reflect.ValueOf(cfg).Elem()
   160  	for i := 0; i < value.NumField(); i++ {
   161  		// note: since the interface method of parser is a pointer receiver we need to get the value of the field as a pointer.
   162  		if parsable, ok := value.Field(i).Addr().Interface().(parser); ok {
   163  			// the field implements parser, call it
   164  			if err := parsable.parseConfigValues(); err != nil {
   165  				return err
   166  			}
   167  		}
   168  	}
   169  	return nil
   170  }
   171  
   172  func (cfg *Application) parseLogLevelOption() error {
   173  	switch {
   174  	case cfg.Quiet:
   175  		// TODO: this is bad: quiet option trumps all other logging options (such as to a file on disk)
   176  		// we should be able to quiet the console logging and leave file logging alone...
   177  		// ... this will be an enhancement for later
   178  		cfg.Log.Level = logger.DisabledLevel
   179  
   180  	case cfg.Verbosity > 0:
   181  		cfg.Log.Level = logger.LevelFromVerbosity(int(cfg.Verbosity), logger.WarnLevel, logger.InfoLevel, logger.DebugLevel, logger.TraceLevel)
   182  
   183  	case cfg.Log.Level != "":
   184  		var err error
   185  		cfg.Log.Level, err = logger.LevelFromString(string(cfg.Log.Level))
   186  		if err != nil {
   187  			return err
   188  		}
   189  
   190  		if logger.IsVerbose(cfg.Log.Level) {
   191  			cfg.Verbosity = 1
   192  		}
   193  	default:
   194  		cfg.Log.Level = logger.WarnLevel
   195  	}
   196  
   197  	return nil
   198  }
   199  
   200  func (cfg *Application) parseFile() error {
   201  	if cfg.File != "" {
   202  		expandedPath, err := homedir.Expand(cfg.File)
   203  		if err != nil {
   204  			return fmt.Errorf("unable to expand file path=%q: %w", cfg.File, err)
   205  		}
   206  		cfg.File = expandedPath
   207  	}
   208  	return nil
   209  }
   210  
   211  // init loads the default configuration values into the viper instance (before the config values are read and parsed).
   212  func loadDefaultValues(v *viper.Viper) {
   213  	// set the default values for primitive fields in this struct
   214  	v.SetDefault("quiet", false)
   215  	v.SetDefault("check-for-app-update", true)
   216  	v.SetDefault("catalogers", nil)
   217  	v.SetDefault("parallelism", 1)
   218  	v.SetDefault("default-image-pull-source", "")
   219  
   220  	// for each field in the configuration struct, see if the field implements the defaultValueLoader interface and invoke it if it does
   221  	value := reflect.ValueOf(Application{})
   222  	for i := 0; i < value.NumField(); i++ {
   223  		// note: the defaultValueLoader method receiver is NOT a pointer receiver.
   224  		if loadable, ok := value.Field(i).Interface().(defaultValueLoader); ok {
   225  			// the field implements defaultValueLoader, call it
   226  			loadable.loadDefaultValues(v)
   227  		}
   228  	}
   229  }
   230  
   231  func (cfg Application) String() string {
   232  	// yaml is pretty human friendly (at least when compared to json)
   233  	appaStr, err := yaml.Marshal(&cfg)
   234  
   235  	if err != nil {
   236  		return err.Error()
   237  	}
   238  
   239  	return string(appaStr)
   240  }
   241  
   242  // nolint:funlen
   243  func loadConfig(v *viper.Viper, configPath string) error {
   244  	var err error
   245  	// use explicitly the given user config
   246  	if configPath != "" {
   247  		v.SetConfigFile(configPath)
   248  		if err := v.ReadInConfig(); err != nil {
   249  			return fmt.Errorf("unable to read application config=%q : %w", configPath, err)
   250  		}
   251  		v.Set("config", v.ConfigFileUsed())
   252  		// don't fall through to other options if the config path was explicitly provided
   253  		return nil
   254  	}
   255  
   256  	// start searching for valid configs in order...
   257  	// 1. look for .<appname>.yaml (in the current directory)
   258  	confFilePath := "." + internal.ApplicationName
   259  
   260  	// TODO: Remove this before v1.0.0
   261  	// See gosbom #1634
   262  	v.AddConfigPath(".")
   263  	v.SetConfigName(confFilePath)
   264  
   265  	// check if config.yaml exists in the current directory
   266  	// DEPRECATED: this will be removed in v1.0.0
   267  	if _, err := os.Stat("config.yaml"); err == nil {
   268  		log.Warn("DEPRECATED: ./config.yaml as a configuration file is deprecated and will be removed as an option in v1.0.0, please rename to .gosbom.yaml")
   269  	}
   270  
   271  	if _, err := os.Stat(confFilePath + ".yaml"); err == nil {
   272  		if err = v.ReadInConfig(); err == nil {
   273  			v.Set("config", v.ConfigFileUsed())
   274  			return nil
   275  		} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
   276  			return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
   277  		}
   278  	}
   279  
   280  	// 2. look for .<appname>/config.yaml (in the current directory)
   281  	v.AddConfigPath("." + internal.ApplicationName)
   282  	v.SetConfigName("config")
   283  	if err = v.ReadInConfig(); err == nil {
   284  		v.Set("config", v.ConfigFileUsed())
   285  		return nil
   286  	} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
   287  		return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
   288  	}
   289  
   290  	// 3. look for ~/.<appname>.yaml
   291  	home, err := homedir.Dir()
   292  	if err == nil {
   293  		v.AddConfigPath(home)
   294  		v.SetConfigName("." + internal.ApplicationName)
   295  		if err = v.ReadInConfig(); err == nil {
   296  			v.Set("config", v.ConfigFileUsed())
   297  			return nil
   298  		} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
   299  			return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
   300  		}
   301  	}
   302  
   303  	// 4. look for <appname>/config.yaml in xdg locations (starting with xdg home config dir, then moving upwards)
   304  	v.SetConfigName("config")
   305  	configPath = path.Join(xdg.ConfigHome, internal.ApplicationName)
   306  	v.AddConfigPath(configPath)
   307  	for _, dir := range xdg.ConfigDirs {
   308  		v.AddConfigPath(path.Join(dir, internal.ApplicationName))
   309  	}
   310  	if err = v.ReadInConfig(); err == nil {
   311  		v.Set("config", v.ConfigFileUsed())
   312  		return nil
   313  	} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
   314  		return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
   315  	}
   316  	return nil
   317  }
   318  
   319  var validDefaultSourceValues = []string{"registry", "docker", "podman", ""}
   320  
   321  func checkDefaultSourceValues(source string) error {
   322  	validValues := internal.NewStringSet(validDefaultSourceValues...)
   323  	if !validValues.Contains(source) {
   324  		validValuesString := strings.Join(validDefaultSourceValues, ", ")
   325  		return fmt.Errorf("%s is not a valid default source; please use one of the following: %s''", source, validValuesString)
   326  	}
   327  
   328  	return nil
   329  }