From 81ead2776bc0519d9d8004a56822989e0f4001b6 Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Thu, 20 Jul 2023 11:05:20 +0200 Subject: [PATCH] Use TOML as config file format in downloader (#405) * Use TOML as config file format. * Parse command line a second time if config file was loaded. * Handle default values correctly. * Use same names for config file options and command line options. --- cmd/csaf_downloader/config.go | 139 +++++++++++++++++++++++++++++----- cmd/csaf_downloader/main.go | 40 +--------- docs/csaf_downloader.md | 34 ++++----- 3 files changed, 136 insertions(+), 77 deletions(-) diff --git a/cmd/csaf_downloader/config.go b/cmd/csaf_downloader/config.go index 61a5d0d..3688da1 100644 --- a/cmd/csaf_downloader/config.go +++ b/cmd/csaf_downloader/config.go @@ -9,44 +9,130 @@ package main import ( + "fmt" "log" "net/http" "os" + "github.com/BurntSushi/toml" + "github.com/csaf-poc/csaf_distribution/v2/util" + "github.com/jessevdk/go-flags" "github.com/mitchellh/go-homedir" ) -const defaultWorker = 2 +const ( + defaultWorker = 2 + defaultPreset = "mandatory" +) type config struct { - Directory *string `short:"d" long:"directory" description:"DIRectory to store the downloaded files in" value-name:"DIR"` - Insecure bool `long:"insecure" description:"Do not check TLS certificates from provider"` - IgnoreSignatureCheck bool `long:"ignoresigcheck" description:"Ignore signature check results, just warn on mismatch"` - Version bool `long:"version" description:"Display version of the binary" no-ini:"true"` - Verbose bool `long:"verbose" short:"v" description:"Verbose output"` - Rate *float64 `long:"rate" short:"r" description:"The average upper limit of https operations per second (defaults to unlimited)"` - Worker int `long:"worker" short:"w" description:"NUMber of concurrent downloads" value-name:"NUM"` + Directory *string `short:"d" long:"directory" description:"DIRectory to store the downloaded files in" value-name:"DIR" toml:"directory"` + Insecure bool `long:"insecure" description:"Do not check TLS certificates from provider" toml:"insecure"` + IgnoreSignatureCheck bool `long:"ignoresigcheck" description:"Ignore signature check results, just warn on mismatch" toml:"ignoresigcheck"` + Version bool `long:"version" description:"Display version of the binary" toml:"-"` + Verbose bool `long:"verbose" short:"v" description:"Verbose output" toml:"verbose"` + Rate *float64 `long:"rate" short:"r" description:"The average upper limit of https operations per second (defaults to unlimited)" toml:"rate"` + Worker int `long:"worker" short:"w" description:"NUMber of concurrent downloads" value-name:"NUM" toml:"worker"` - ExtraHeader http.Header `long:"header" short:"H" description:"One or more extra HTTP header fields"` + ExtraHeader http.Header `long:"header" short:"H" description:"One or more extra HTTP header fields" toml:"header"` - RemoteValidator string `long:"validator" description:"URL to validate documents remotely" value-name:"URL"` - RemoteValidatorCache string `long:"validatorcache" description:"FILE to cache remote validations" value-name:"FILE"` - RemoteValidatorPresets []string `long:"validatorpreset" description:"One or more presets to validate remotely" default:"mandatory"` + RemoteValidator string `long:"validator" description:"URL to validate documents remotely" value-name:"URL" toml:"validator"` + RemoteValidatorCache string `long:"validatorcache" description:"FILE to cache remote validations" value-name:"FILE" toml:"validatorcache"` + RemoteValidatorPresets []string `long:"validatorpreset" description:"One or more PRESETS to validate remotely" value-name:"PRESETS" toml:"validatorpreset"` - Config *string `short:"c" long:"config" description:"Path to config ini file" value-name:"INI-FILE" no-ini:"true"` + Config string `short:"c" long:"config" description:"Path to config TOML file" value-name:"TOML-FILE" toml:"-"` } -// iniPaths are the potential file locations of the the config file. -var iniPaths = []string{ - "~/.config/csaf/downloader.ini", - "~/.csaf_downloader.ini", - "csaf_downloader.ini", +// configPaths are the potential file locations of the the config file. +var configPaths = []string{ + "~/.config/csaf/downloader.toml", + "~/.csaf_downloader.toml", + "csaf_downloader.toml", } -// findIniFile looks for a file in the pre-defined paths in "iniPaths". +// newConfig returns a new configuration. +func newConfig() *config { + return &config{ + Worker: defaultWorker, + RemoteValidatorPresets: []string{defaultPreset}, + } +} + +// parseArgsConfig parses the command line and if need a config file. +func parseArgsConfig() ([]string, *config, error) { + + // Parse the command line first. + cmdLineCfg := newConfig() + parser := flags.NewParser(cmdLineCfg, flags.Default) + parser.Usage = "[OPTIONS] domain..." + args, err := parser.Parse() + if err != nil { + return nil, nil, err + } + + // Directly quit if the version flag was set. + if cmdLineCfg.Version { + fmt.Println(util.SemVersion) + os.Exit(0) + } + + var path string + // Do we have a config file explicitly given by command line? + if cmdLineCfg.Config != "" { + path = cmdLineCfg.Config + } else { + path = findConfigFile() + } + // No config file -> We are good. + if path == "" { + return args, cmdLineCfg, nil + } + + if path, err = homedir.Expand(path); err != nil { + return nil, nil, err + } + + // Load the config file + fileCfg := &config{} + if err := fileCfg.load(path); err != nil { + return nil, nil, err + } + + // Parse command line a second time to overwrite the + // loaded config at places where explicitly command line + // options where given. + args, err = flags.NewParser(fileCfg, flags.Default).Parse() + if err != nil { + return nil, nil, err + } + + // Re-establish default values. + if fileCfg.Worker == 0 { + fileCfg.Worker = defaultWorker + } + if fileCfg.RemoteValidatorPresets == nil { + fileCfg.RemoteValidatorPresets = []string{defaultPreset} + } + + return args, fileCfg, nil +} + +// load loads a configuration from file. +func (cfg *config) load(path string) error { + md, err := toml.DecodeFile(path, &cfg) + if err != nil { + return err + } + if undecoded := md.Undecoded(); len(undecoded) != 0 { + return fmt.Errorf("could not parse %q from %q", undecoded, path) + } + return nil +} + +// findConfigFile looks for a file in the pre-defined paths in "configPath". // The returned value will be the name of file if found, otherwise an empty string. -func findIniFile() string { - for _, f := range iniPaths { +func findConfigFile() string { + for _, f := range configPaths { name, err := homedir.Expand(f) if err != nil { log.Printf("warn: %v\n", err) @@ -64,3 +150,14 @@ func (cfg *config) prepare() error { // TODO: Implement me! return nil } + +// errCheck checks if err is not nil and terminates +// the program if so. +func errCheck(err error) { + if err != nil { + if flags.WroteHelp(err) { + os.Exit(0) + } + log.Fatalf("error: %v\n", err) + } +} diff --git a/cmd/csaf_downloader/main.go b/cmd/csaf_downloader/main.go index cc3b6ef..1b405b7 100644 --- a/cmd/csaf_downloader/main.go +++ b/cmd/csaf_downloader/main.go @@ -11,25 +11,11 @@ package main import ( "context" - "fmt" "log" "os" "os/signal" - - "github.com/csaf-poc/csaf_distribution/v2/util" - "github.com/jessevdk/go-flags" - "github.com/mitchellh/go-homedir" ) -func errCheck(err error) { - if err != nil { - if flags.WroteHelp(err) { - os.Exit(0) - } - log.Fatalf("error: %v\n", err) - } -} - func run(cfg *config, domains []string) error { d, err := newDownloader(cfg) if err != nil { @@ -47,32 +33,8 @@ func run(cfg *config, domains []string) error { func main() { - cfg := &config{ - Worker: defaultWorker, - } - - parser := flags.NewParser(cfg, flags.Default) - parser.Usage = "[OPTIONS] domain..." - domains, err := parser.Parse() + domains, cfg, err := parseArgsConfig() errCheck(err) - - if cfg.Version { - fmt.Println(util.SemVersion) - return - } - - if cfg.Config != nil { - iniParser := flags.NewIniParser(parser) - iniParser.ParseAsDefaults = true - name, err := homedir.Expand(*cfg.Config) - errCheck(err) - errCheck(iniParser.ParseFile(name)) - } else if iniFile := findIniFile(); iniFile != "" { - iniParser := flags.NewIniParser(parser) - iniParser.ParseAsDefaults = true - errCheck(iniParser.ParseFile(iniFile)) - } - errCheck(cfg.prepare()) if len(domains) == 0 { diff --git a/docs/csaf_downloader.md b/docs/csaf_downloader.md index 9540fb0..250d53b 100644 --- a/docs/csaf_downloader.md +++ b/docs/csaf_downloader.md @@ -7,21 +7,21 @@ A tool to download CSAF documents from CSAF providers. csaf_downloader [OPTIONS] domain... Application Options: - -d, --directory=DIR DIRectory to store the downloaded files in - --insecure Do not check TLS certificates from provider - --ignoresigcheck Ignore signature check results, just warn on mismatch - --version Display version of the binary - -v, --verbose Verbose output - -r, --rate= The average upper limit of https operations per second (defaults to unlimited) - -w, --worker=NUM NUMber of concurrent downloads (default: 2) - -H, --header= One or more extra HTTP header fields - --validator=URL URL to validate documents remotely - --validatorcache=FILE FILE to cache remote validations - --validatorpreset= One or more presets to validate remotely (default: mandatory) - -c, --config=INI-FILE Path to config ini file + -d, --directory=DIR DIRectory to store the downloaded files in + --insecure Do not check TLS certificates from provider + --ignoresigcheck Ignore signature check results, just warn on mismatch + --version Display version of the binary + -v, --verbose Verbose output + -r, --rate= The average upper limit of https operations per second (defaults to unlimited) + -w, --worker=NUM NUMber of concurrent downloads (default: 2) + -H, --header= One or more extra HTTP header fields + --validator=URL URL to validate documents remotely + --validatorcache=FILE FILE to cache remote validations + --validatorpreset=PRESETS One or more PRESETS to validate remotely (default: [mandatory]) + -c, --config=TOML-FILE Path to config TOML file Help Options: - -h, --help Show this help message + -h, --help Show this help message ``` Will download all CSAF documents for the given _domains_, by trying each as a CSAF provider. @@ -35,9 +35,9 @@ have taken countermeasures to limit this. If no config file is explictly given the follwing places are searched for a config file: ``` -~/.config/csaf/downloader.ini -~/.csaf_downloader.ini -csaf_downloader.ini +~/.config/csaf/downloader.toml +~/.csaf_downloader.toml +csaf_downloader.toml ``` with `~` expanding to `$HOME` on unixoid systems and `%HOMEPATH` on Windows systems. @@ -53,5 +53,5 @@ worker = 2 # header # not set by default # validator # not set by default # validatorcache # not set by default -validatorpreset = "mandatory" +validatorpreset = ["mandatory"] ```