From 9e8101ff3a45d53881c57c87912ffcbfa8d78b2c Mon Sep 17 00:00:00 2001 From: Samantha Date: Mon, 15 May 2023 14:16:04 -0400 Subject: [PATCH] main: Validate config files by default (#6885) - Make config validation run by default for all Boulder components with a registered validator. - Refactor main to parse `boulder` flags directly instead of declaring them as subcommands. - Remove the `validate` subcommand and update relevant docs. - Fix configuration validation for issuer (file source) OCSP responder. Fixes #6857 Fixes #6763 --- cmd/boulder/main.go | 125 +++++++++++++++++++++---------------- cmd/ocsp-responder/main.go | 8 +-- cmd/registry.go | 10 +-- docs/config-validation.md | 21 +++---- 4 files changed, 90 insertions(+), 74 deletions(-) diff --git a/cmd/boulder/main.go b/cmd/boulder/main.go index 12754701f..64ec2741e 100644 --- a/cmd/boulder/main.go +++ b/cmd/boulder/main.go @@ -1,9 +1,9 @@ package main import ( - "flag" "fmt" "os" + "strings" _ "github.com/letsencrypt/boulder/cmd/admin-revoker" _ "github.com/letsencrypt/boulder/cmd/akamai-purger" @@ -36,18 +36,14 @@ import ( "github.com/letsencrypt/boulder/cmd" ) -// readAndValidateConfigFile takes a file path as an argument and attempts to -// unmarshal the content of the file into a struct containing a configuration of -// a boulder component specified by name (e.g. boulder-ca, bad-key-revoker, -// etc.). Any config keys in the JSON file which do not correspond to expected -// keys in the config struct will result in errors. It also validates the config -// using the struct tags defined in the config struct. +// readAndValidateConfigFile uses the ConfigValidator registered for the given +// command to validate the provided config file. If the command does not have a +// registered ConfigValidator, this function does nothing. func readAndValidateConfigFile(name, filename string) error { - cv, err := cmd.LookupConfigValidator(name) - if err != nil { - return err + cv := cmd.LookupConfigValidator(name) + if cv == nil { + return nil } - file, err := os.Open(filename) if err != nil { return err @@ -60,57 +56,80 @@ func readAndValidateConfigFile(name, filename string) error { return cmd.ValidateJSONConfig(cv, file) } +// getConfigPath returns the path to the config file if it was provided as a +// command line flag. If the flag was not provided, it returns an empty string. +func getConfigPath() string { + for i := 0; i < len(os.Args); i++ { + arg := os.Args[i] + if arg == "--config" || arg == "-config" { + if i+1 < len(os.Args) { + return os.Args[i+1] + } + } + if strings.HasPrefix(arg, "--config=") { + return strings.TrimPrefix(arg, "--config=") + } + if strings.HasPrefix(arg, "-config=") { + return strings.TrimPrefix(arg, "-config=") + } + } + return "" +} + +var boulderUsage = fmt.Sprintf(`Usage: %s [flags] + + Each boulder component has its own subcommand. Use --list to see + a list of the available components. Use --help to + see the usage for a specific component. +`, + core.Command()) + func main() { - cmd.LookupCommand(core.Command())() -} - -func init() { - cmd.RegisterCommand("boulder", func() { + var command string + if core.Command() == "boulder" { + // Operator passed the boulder component as a subcommand. if len(os.Args) <= 1 { - fmt.Fprintf(os.Stderr, "Call with --list to list available subcommands. Run them like boulder .\n") + // No arguments passed. + fmt.Fprint(os.Stderr, boulderUsage) return } - subcommand := cmd.LookupCommand(os.Args[1]) - if subcommand == nil { - fmt.Fprintf(os.Stderr, "Unknown subcommand '%s'.\n", os.Args[1]) + + if os.Args[1] == "--help" || os.Args[1] == "-help" { + // Help flag passed. + fmt.Fprint(os.Stderr, boulderUsage) return } + + if os.Args[1] == "--list" || os.Args[1] == "-list" { + // List flag passed. + for _, c := range cmd.AvailableCommands() { + fmt.Println(c) + } + return + } + command = os.Args[1] + + // Remove the subcommand from the arguments. os.Args = os.Args[1:] - subcommand() - }, nil) - // TODO(#6763): Move this inside of main(). - cmd.RegisterCommand("--list", func() { - for _, c := range cmd.AvailableCommands() { - if c != "boulder" && c != "--list" { - fmt.Println(c) - } - } - }, nil) - // TODO(#6763): Move this inside of main(). - cmd.RegisterCommand("validate", func() { - if len(os.Args) <= 1 { - fmt.Fprintf(os.Stderr, "Call with --help to list usage.\n") - os.Exit(1) - } - list := flag.Bool("list", false, "List available components to validate configuration for.") - component := flag.String("component", "", "The name of the component to validate configuration for.") - configFile := flag.String("config", "", "The path to the configuration file to validate.") - flag.Parse() + } else { + // Operator ran a boulder component using a symlink. + command = core.Command() + } - if *list { - for _, c := range cmd.AvailableConfigValidators() { - fmt.Println(c) - } - return - } - if *component == "" || *configFile == "" { - fmt.Fprintf(os.Stderr, "Must provide a configuration file to validate.\n") - os.Exit(1) - } - err := readAndValidateConfigFile(*component, *configFile) + config := getConfigPath() + if config != "" { + // Config flag passed. + err := readAndValidateConfigFile(command, config) if err != nil { - fmt.Fprintf(os.Stderr, "Error validating configuration: %s\n", err) + fmt.Fprintf(os.Stderr, "Error validating config file %q for command %q: %s\n", config, command, err) os.Exit(1) } - }, nil) + } + + commandFunc := cmd.LookupCommand(command) + if commandFunc == nil { + fmt.Fprintf(os.Stderr, "Unknown subcommand %q.\n", command) + os.Exit(1) + } + commandFunc() } diff --git a/cmd/ocsp-responder/main.go b/cmd/ocsp-responder/main.go index 51dd4d4f8..39a0dac43 100644 --- a/cmd/ocsp-responder/main.go +++ b/cmd/ocsp-responder/main.go @@ -39,7 +39,7 @@ type Config struct { // can be a DBConnect string or a file URL. The file URL style is used // when responding from a static file for intermediates and roots. // If DBConfig has non-empty fields, it takes precedence over this. - Source string `validate:"required_without_all=DB.DBConnectFile SAService"` + Source string `validate:"required_without_all=DB.DBConnectFile SAService Redis"` // The list of issuer certificates, against which OCSP requests/responses // are checked to ensure we're not responding for anyone else's certs. @@ -88,10 +88,10 @@ type Config struct { // Configuration for using Redis as a cache. This configuration should // allow for both read and write access. - Redis rocsp_config.RedisConfig + Redis *rocsp_config.RedisConfig `validate:"required_without=Source"` // TLS client certificate, private key, and trusted root bundle. - TLS cmd.TLSConfig + TLS cmd.TLSConfig `validate:"required_without=Source,structonly"` // RAService configures how to communicate with the RA when it is necessary // to generate a fresh OCSP response. @@ -155,7 +155,7 @@ as generated by Boulder's ceremony command. cmd.FailOnError(err, fmt.Sprintf("Couldn't read file: %s", url.Path)) } else { // Set up the redis source and the combined multiplex source. - rocspRWClient, err := rocsp_config.MakeClient(&c.OCSPResponder.Redis, clk, scope) + rocspRWClient, err := rocsp_config.MakeClient(c.OCSPResponder.Redis, clk, scope) cmd.FailOnError(err, "Could not make redis client") err = rocspRWClient.Ping(context.Background()) diff --git a/cmd/registry.go b/cmd/registry.go index 5be2ae2d8..2c2240537 100644 --- a/cmd/registry.go +++ b/cmd/registry.go @@ -69,13 +69,13 @@ func AvailableCommands() []string { } // LookupConfigValidator constructs an instance of the *ConfigValidator for the -// given Boulder component name. If no *ConfigValidator was registered, an error -// is returned. -func LookupConfigValidator(name string) (*ConfigValidator, error) { +// given Boulder component name. If no *ConfigValidator was registered, nil is +// returned. +func LookupConfigValidator(name string) *ConfigValidator { registry.Lock() defer registry.Unlock() if registry.configs[name] == nil { - return nil, fmt.Errorf("no config validator found for %q", name) + return nil } // Create a new copy of the config struct so that we can validate it @@ -87,7 +87,7 @@ func LookupConfigValidator(name string) (*ConfigValidator, error) { return &ConfigValidator{ Config: copy, Validators: registry.configs[name].Validators, - }, nil + } } // AvailableConfigValidators returns a list of Boulder component names for which diff --git a/docs/config-validation.md b/docs/config-validation.md index bf5826e20..4f17d2561 100644 --- a/docs/config-validation.md +++ b/docs/config-validation.md @@ -5,24 +5,21 @@ at https://github.com/letsencrypt/validator. ## Usage -A `validate` subcommand has been included in the `boulder` binary. You can check -the usage with `boulder validate -help`. +By default Boulder validates config files for all components with a registered +validator. Validating a config file for a given component is as simple as +running the component directly: -Use the following syntax to validate a config file: ```shell -boulder validate -component -config +$ ./bin/boulder-observer -config test/config-next/observer.yml +Error validating config file "test/config-next/observer.yml": Key: 'ObsConf.MonConfs[1].Kind' Error:Field validation for 'Kind' failed on the 'oneof' tag ``` -For instance, to validate the `boulder-ca` config file you can run: +or by running the `boulder` binary and passing the component name as a +subcommand: ```shell -boulder validate -component boulder-ca -config test/config/ca.json` -``` - -For a complete list of `boulder` components which support config validation you -can run: -```shell -boulder validate -list` +$ ./bin/boulder boulder-observer -config test/config-next/observer.yml +Error validating config file "test/config-next/observer.yml": Key: 'ObsConf.MonConfs[1].Kind' Error:Field validation for 'Kind' failed on the 'oneof' tag ``` ## Struct Tag Tips