diff --git a/command/base/command.go b/command/base/command.go index a9fd6db66..048f31f0a 100644 --- a/command/base/command.go +++ b/command/base/command.go @@ -33,6 +33,7 @@ type Command struct { Flags FlagSetFlags flagSet *flag.FlagSet + hidden *flag.FlagSet // These are the options which correspond to the HTTP API options httpAddr stringValue @@ -137,10 +138,18 @@ func (c *Command) NewFlagSet(command cli.Command) *flag.FlagSet { f.SetOutput(errW) c.flagSet = f + c.hidden = flag.NewFlagSet("", flag.ContinueOnError) return f } +// HideFlags is used to set hidden flags that will not be shown in help text +func (c *Command) HideFlags(flags ...string) { + for _, f := range flags { + c.hidden.String(f, "", "") + } +} + // Parse is used to parse the underlying flag set. func (c *Command) Parse(args []string) error { return c.flagSet.Parse(args) @@ -199,7 +208,7 @@ func (c *Command) helpFlagsFor(f *flag.FlagSet) string { firstCommand := true f.VisitAll(func(f *flag.Flag) { // Skip HTTP flags as they will be grouped separately - if flagContains(httpFlagsClient, f) || flagContains(httpFlagsServer, f) { + if flagContains(httpFlagsClient, f) || flagContains(httpFlagsServer, f) || flagContains(c.hidden, f) { return } if firstCommand { @@ -240,7 +249,7 @@ func flagContains(fs *flag.FlagSet, f *flag.Flag) bool { return } - if f.Name == hf.Name && f.Usage == hf.Usage { + if f.Name == hf.Name { skip = true return } diff --git a/command/configtest.go b/command/configtest.go index 81eafef51..48f954196 100644 --- a/command/configtest.go +++ b/command/configtest.go @@ -16,7 +16,9 @@ type ConfigTestCommand struct { func (c *ConfigTestCommand) Help() string { helpText := ` -Usage: consul configtest [options] +Usage: consul configtest [options] FILE_OR_DIRECTORY + + DEPRECATED. Use the 'consul validate' command instead. Performs a basic sanity test on Consul configuration files. For each file or directory given, the configtest command will attempt to parse the @@ -59,5 +61,5 @@ func (c *ConfigTestCommand) Run(args []string) int { } func (c *ConfigTestCommand) Synopsis() string { - return "Validate config file" + return "DEPRECATED. Use the validate command instead" } diff --git a/command/validate.go b/command/validate.go new file mode 100644 index 000000000..55f7c7699 --- /dev/null +++ b/command/validate.go @@ -0,0 +1,70 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/hashicorp/consul/command/agent" + "github.com/hashicorp/consul/command/base" +) + +// ValidateCommand is a Command implementation that is used to +// verify config files +type ValidateCommand struct { + base.Command +} + +func (c *ValidateCommand) Help() string { + helpText := ` +Usage: consul validate [options] FILE_OR_DIRECTORY + + Performs a basic sanity test on Consul configuration files. For each file + or directory given, the validate command will attempt to parse the + contents just as the "consul agent" command would, and catch any errors. + This is useful to do a test of the configuration only, without actually + starting the agent. + + Returns 0 if the configuration is valid, or 1 if there are problems. + +` + c.Command.Help() + + return strings.TrimSpace(helpText) +} + +func (c *ValidateCommand) Run(args []string) int { + var configFiles []string + + f := c.Command.NewFlagSet(c) + f.Var((*agent.AppendSliceValue)(&configFiles), "config-file", + "Path to a JSON file to read configuration from. This can be specified multiple times.") + f.Var((*agent.AppendSliceValue)(&configFiles), "config-dir", + "Path to a directory to read configuration files from. This will read every file ending in "+ + ".json as configuration in this directory in alphabetical order.") + c.Command.HideFlags("config-file", "config-dir") + + if err := c.Command.Parse(args); err != nil { + return 1 + } + + if len(f.Args()) > 0 { + configFiles = append(configFiles, f.Args()...) + } + + if len(configFiles) < 1 { + c.Ui.Error("Must specify at least one config file or directory") + return 1 + } + + _, err := agent.ReadConfigPaths(configFiles) + if err != nil { + c.Ui.Error(fmt.Sprintf("Config validation failed: %v", err.Error())) + return 1 + } + + c.Ui.Output("Configuration is valid!") + return 0 +} + +func (c *ValidateCommand) Synopsis() string { + return "Validate config files/directories" +} diff --git a/command/validate_test.go b/command/validate_test.go new file mode 100644 index 000000000..0476efef8 --- /dev/null +++ b/command/validate_test.go @@ -0,0 +1,121 @@ +package command + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/hashicorp/consul/command/base" + "github.com/mitchellh/cli" +) + +func testValidateCommand(t *testing.T) (*cli.MockUi, *ValidateCommand) { + ui := new(cli.MockUi) + return ui, &ValidateCommand{ + Command: base.Command{ + Ui: ui, + Flags: base.FlagSetNone, + }, + } +} + +func TestValidateCommand_implements(t *testing.T) { + var _ cli.Command = &ValidateCommand{} +} + +func TestValidateCommandFailOnEmptyFile(t *testing.T) { + tmpFile, err := ioutil.TempFile("", "consul") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(tmpFile.Name()) + + _, cmd := testValidateCommand(t) + + args := []string{tmpFile.Name()} + + if code := cmd.Run(args); code == 0 { + t.Fatalf("bad: %d", code) + } +} + +func TestValidateCommandSucceedOnEmptyDir(t *testing.T) { + td, err := ioutil.TempDir("", "consul") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(td) + + ui, cmd := testValidateCommand(t) + + args := []string{td} + + if code := cmd.Run(args); code != 0 { + t.Fatalf("bad: %d, %s", code, ui.ErrorWriter.String()) + } +} + +func TestValidateCommandSucceedOnMinimalConfigFile(t *testing.T) { + td, err := ioutil.TempDir("", "consul") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(td) + + fp := filepath.Join(td, "config.json") + err = ioutil.WriteFile(fp, []byte(`{}`), 0644) + if err != nil { + t.Fatalf("err: %s", err) + } + + _, cmd := testValidateCommand(t) + + args := []string{fp} + + if code := cmd.Run(args); code != 0 { + t.Fatalf("bad: %d", code) + } +} + +func TestValidateCommandSucceedOnMinimalConfigDir(t *testing.T) { + td, err := ioutil.TempDir("", "consul") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(td) + + err = ioutil.WriteFile(filepath.Join(td, "config.json"), []byte(`{}`), 0644) + if err != nil { + t.Fatalf("err: %s", err) + } + + _, cmd := testValidateCommand(t) + + args := []string{td} + + if code := cmd.Run(args); code != 0 { + t.Fatalf("bad: %d", code) + } +} + +func TestValidateCommandSucceedOnConfigDirWithEmptyFile(t *testing.T) { + td, err := ioutil.TempDir("", "consul") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(td) + + err = ioutil.WriteFile(filepath.Join(td, "config.json"), []byte{}, 0644) + if err != nil { + t.Fatalf("err: %s", err) + } + + _, cmd := testValidateCommand(t) + + args := []string{td} + + if code := cmd.Run(args); code != 0 { + t.Fatalf("bad: %d", code) + } +} diff --git a/commands.go b/commands.go index 6ead48b95..f79c025f9 100644 --- a/commands.go +++ b/commands.go @@ -264,6 +264,15 @@ func init() { }, nil }, + "validate": func() (cli.Command, error) { + return &command.ValidateCommand{ + Command: base.Command{ + Flags: base.FlagSetNone, + Ui: ui, + }, + }, nil + }, + "version": func() (cli.Command, error) { return &command.VersionCommand{ HumanVersion: version.GetHumanVersion(),