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
This commit is contained in:
Samantha 2023-05-15 14:16:04 -04:00 committed by GitHub
parent 8c9c55609b
commit 9e8101ff3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 90 additions and 74 deletions

View File

@ -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 <subcommand> [flags]
Each boulder component has its own subcommand. Use --list to see
a list of the available components. Use <subcommand> --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 <subcommand>.\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()
}

View File

@ -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())

View File

@ -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

View File

@ -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 <component> -config <config file>
$ ./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