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, ¬Found) { 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 }