From 501babd2008431e4562065a438648fced2c9f1c2 Mon Sep 17 00:00:00 2001 From: Thomas Lefebvre Date: Mon, 26 Oct 2020 17:13:42 -0700 Subject: [PATCH] Add config-validate command to nomad CLI --- command/commands.go | 5 +++ command/config_validate.go | 73 +++++++++++++++++++++++++++++++ command/config_validate_test.go | 76 +++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 command/config_validate.go create mode 100644 command/config_validate_test.go diff --git a/command/commands.go b/command/commands.go index d099c2a5cd4e..5e2b0cd13d43 100644 --- a/command/commands.go +++ b/command/commands.go @@ -201,6 +201,11 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory { 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_validate.go b/command/config_validate.go new file mode 100644 index 000000000000..656c87c2a063 --- /dev/null +++ b/command/config_validate.go @@ -0,0 +1,73 @@ +package command + +import ( + "fmt" + "strings" + + agent "github.com/hashicorp/nomad/command/agent" +) + +type ConfigValidateCommand struct { + Meta +} + +func (c *ConfigValidateCommand) Help() string { + helpText := ` +Usage: nomad config-validate [options] FILE_OR_DIRECTORY... + + 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 { + 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 + } + + var config *agent.Config + + for _, path := range configPath { + current, err := agent.LoadConfig(path) + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Error loading configuration from %s: %s", path, err)) + return 1 + } + + if config == nil { + config = current + } else { + config = config.Merge(current) + } + } + + 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..7646618e7176 --- /dev/null +++ b/command/config_validate_test.go @@ -0,0 +1,76 @@ +package command + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/mitchellh/cli" +) + +func TestConfigValidateCommand_SucceedWithEmptyDir(t *testing.T) { + t.Parallel() + fh, err := ioutil.TempDir("", "nomad") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(fh) + + ui := new(cli.MockUi) + 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_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(`datacenter = "abc01"`), 0644) + if err != nil { + t.Fatalf("err: %s", err) + } + + ui := new(cli.MockUi) + 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_FailOnBadConfigFile(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 := new(cli.MockUi) + cmd := &ConfigValidateCommand{Meta: Meta{Ui: ui}} + args := []string{fh} + + code := cmd.Run(args) + if code != 1 { + t.Fatalf("expected exit 1, actual: %d", code) + } +}