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:
parent
8c9c55609b
commit
9e8101ff3a
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue