From 45ec419514084358c67bdebea4065218c65d9a64 Mon Sep 17 00:00:00 2001 From: Thomas Lefebvre Date: Mon, 26 Oct 2020 17:13:42 -0700 Subject: [PATCH] Add config command and config validate subcommand to nomad CLI --- command/agent/command.go | 4 +- command/agent/command_test.go | 2 +- command/commands.go | 10 +++ command/config.go | 38 ++++++++++++ command/config_validate.go | 85 +++++++++++++++++++++++++ command/config_validate_test.go | 106 ++++++++++++++++++++++++++++++++ 6 files changed, 242 insertions(+), 3 deletions(-) create mode 100644 command/config.go create mode 100644 command/config_validate.go create mode 100644 command/config_validate_test.go diff --git a/command/agent/command.go b/command/agent/command.go index 558e63ee120e..fe8d5c76b159 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -294,14 +294,14 @@ func (c *Command) readConfig() *Config { config.Server.DefaultSchedulerConfig.Canonicalize() - if !c.isValidConfig(config, cmdConfig) { + if !c.IsValidConfig(config, cmdConfig) { return nil } return config } -func (c *Command) isValidConfig(config, cmdConfig *Config) bool { +func (c *Command) IsValidConfig(config, cmdConfig *Config) bool { // Check that the server is running in at least one mode. if !(config.Server.Enabled || config.Client.Enabled) { diff --git a/command/agent/command_test.go b/command/agent/command_test.go index 6b2c80618e00..e155e4c2b5b3 100644 --- a/command/agent/command_test.go +++ b/command/agent/command_test.go @@ -370,7 +370,7 @@ func TestIsValidConfig(t *testing.T) { mui := cli.NewMockUi() cmd := &Command{Ui: mui} config := DefaultConfig().Merge(&tc.conf) - result := cmd.isValidConfig(config, DefaultConfig()) + result := cmd.IsValidConfig(config, DefaultConfig()) if tc.err == "" { // No error expected assert.True(t, result, mui.ErrorWriter.String()) diff --git a/command/commands.go b/command/commands.go index 87f46b8f4ad0..016502bc41bc 100644 --- a/command/commands.go +++ b/command/commands.go @@ -204,6 +204,16 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory { Meta: meta, }, nil }, + "config": func() (cli.Command, error) { + return &ConfigCommand{ + Meta: meta, + }, nil + }, + "config validate": func() (cli.Command, error) { + return &ConfigValidateCommand{ + Meta: meta, + }, nil + }, // operator debug was released in 0.12 as debug. This top-level alias preserves compatibility "debug": func() (cli.Command, error) { return &OperatorDebugCommand{ diff --git a/command/config.go b/command/config.go new file mode 100644 index 000000000000..16b47d578d2e --- /dev/null +++ b/command/config.go @@ -0,0 +1,38 @@ +package command + +import ( + "strings" + + "github.com/mitchellh/cli" +) + +type ConfigCommand struct { + Meta +} + +func (f *ConfigCommand) Help() string { + helpText := ` +Usage: nomad config [options] [args] + + This command groups subcommands for interacting with configurations. + Users can validate configurations for the Nomad agent. + + Validate configuration: + + $ nomad config validate [...] + + Please see the individual subcommand help for detailed usage information. +` + + return strings.TrimSpace(helpText) +} + +func (f *ConfigCommand) Synopsis() string { + return "Interact with configurations" +} + +func (f *ConfigCommand) Name() string { return "config" } + +func (f *ConfigCommand) Run(args []string) int { + return cli.RunResultHelp +} diff --git a/command/config_validate.go b/command/config_validate.go new file mode 100644 index 000000000000..b0ae110a3e0d --- /dev/null +++ b/command/config_validate.go @@ -0,0 +1,85 @@ +package command + +import ( + "fmt" + "reflect" + "strings" + + multierror "github.com/hashicorp/go-multierror" + agent "github.com/hashicorp/nomad/command/agent" +) + +type ConfigValidateCommand struct { + Meta +} + +func (c *ConfigValidateCommand) Help() string { + helpText := ` +Usage: nomad config validate [] + + Performs a thorough sanity test on Nomad configuration files. For each file + or directory given, the validate command will attempt to parse the contents + just as the "nomad agent" command would, and catch any errors. + + This is useful to do a test of the configuration only, without actually + starting the agent. This performs all of the validation the agent would, so + this should be given the complete set of configuration files that are going + to be loaded by the agent. This command cannot operate on partial + configuration fragments since those won't pass the full agent validation. + + Returns 0 if the configuration is valid, or 1 if there are problems. +` + + return strings.TrimSpace(helpText) +} + +func (c *ConfigValidateCommand) Synopsis() string { + return "Validate config files/directories" +} + +func (c *ConfigValidateCommand) Name() string { return "config validate" } + +func (c *ConfigValidateCommand) Run(args []string) int { + var mErr multierror.Error + flags := c.Meta.FlagSet(c.Name(), FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.Help()) } + if err := flags.Parse(args); err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + configPath := flags.Args() + if len(configPath) < 1 { + c.Ui.Error("Must specify at least one config file or directory") + return 1 + } + + config := agent.DefaultConfig() + + for _, path := range configPath { + fc, err := agent.LoadConfig(path) + if fc == nil || reflect.DeepEqual(fc, &agent.Config{}) { + c.Ui.Warn(fmt.Sprintf("No configuration loaded from %s", path)) + } + if err != nil { + multierror.Append(&mErr, fmt.Errorf( + "Error loading configuration from %s: %s", path, err)) + continue + } + + config = config.Merge(fc) + } + if err := mErr.ErrorOrNil(); err != nil { + c.Ui.Error(err.Error()) + return 1 + } + cmd := agent.Command{Ui: c.Ui} + valid := cmd.IsValidConfig(config, agent.DefaultConfig()) + if !valid { + c.Ui.Error("Configuration is invalid") + return 1 + } + + c.Ui.Output("Configuration is valid!") + return 0 +} diff --git a/command/config_validate_test.go b/command/config_validate_test.go new file mode 100644 index 000000000000..80ac91fe1e93 --- /dev/null +++ b/command/config_validate_test.go @@ -0,0 +1,106 @@ +package command + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/mitchellh/cli" +) + +func TestConfigValidateCommand_FailWithEmptyDir(t *testing.T) { + t.Parallel() + fh, err := ioutil.TempDir("", "nomad") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(fh) + + ui := cli.NewMockUi() + cmd := &ConfigValidateCommand{Meta: Meta{Ui: ui}} + args := []string{fh} + + code := cmd.Run(args) + if code != 1 { + t.Fatalf("expected exit 1, actual: %d", code) + } +} + +func TestConfigValidateCommand_SucceedWithMinimalConfigFile(t *testing.T) { + t.Parallel() + fh, err := ioutil.TempDir("", "nomad") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(fh) + + fp := filepath.Join(fh, "config.hcl") + err = ioutil.WriteFile(fp, []byte(`data_dir="/" + client { + enabled = true + }`), 0644) + if err != nil { + t.Fatalf("err: %s", err) + } + + ui := cli.NewMockUi() + cmd := &ConfigValidateCommand{Meta: Meta{Ui: ui}} + args := []string{fh} + + code := cmd.Run(args) + if code != 0 { + t.Fatalf("expected exit 0, actual: %d", code) + } +} + +func TestConfigValidateCommand_FailOnParseBadConfigFile(t *testing.T) { + t.Parallel() + fh, err := ioutil.TempDir("", "nomad") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(fh) + + fp := filepath.Join(fh, "config.hcl") + err = ioutil.WriteFile(fp, []byte(`a: b`), 0644) + if err != nil { + t.Fatalf("err: %s", err) + } + + ui := cli.NewMockUi() + cmd := &ConfigValidateCommand{Meta: Meta{Ui: ui}} + args := []string{fh} + + code := cmd.Run(args) + if code != 1 { + t.Fatalf("expected exit 1, actual: %d", code) + } +} + +func TestConfigValidateCommand_FailOnValidateParsableConfigFile(t *testing.T) { + t.Parallel() + fh, err := ioutil.TempDir("", "nomad") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(fh) + + fp := filepath.Join(fh, "config.hcl") + err = ioutil.WriteFile(fp, []byte(`data_dir="../" + client { + enabled = true + }`), 0644) + if err != nil { + t.Fatalf("err: %s", err) + } + + ui := cli.NewMockUi() + cmd := &ConfigValidateCommand{Meta: Meta{Ui: ui}} + args := []string{fh} + + code := cmd.Run(args) + if code != 1 { + t.Fatalf("expected exit 1, actual: %d", code) + } +}