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