From 793f7e8fcc1df4b92b9b5df219333d4c4d6debf9 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/commands.go | 10 +++++ command/config.go | 38 ++++++++++++++++ command/config_validate.go | 80 +++++++++++++++++++++++++++++++++ command/config_validate_test.go | 76 +++++++++++++++++++++++++++++++ 4 files changed, 204 insertions(+) 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/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..28fcfee87136 --- /dev/null +++ b/command/config_validate.go @@ -0,0 +1,80 @@ +package command + +import ( + "fmt" + "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 + } + + var config *agent.Config + + for _, path := range configPath { + current, err := agent.LoadConfig(path) + if err != nil { + _ = multierror.Append(&mErr, fmt.Errorf( + "Error loading configuration from %s: %s", path, err)) + continue + } + + if config == nil { + config = current + } else { + config = config.Merge(current) + } + } + + if err := mErr.ErrorOrNil(); err != nil { + c.Ui.Error(err.Error()) + 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..5e6c4669bb64 --- /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 := 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_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 := 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_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 := 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) + } +}