diff --git a/command/configtest.go b/command/configtest.go new file mode 100644 index 0000000000..38c27a62d4 --- /dev/null +++ b/command/configtest.go @@ -0,0 +1,61 @@ +package command + +import ( + "flag" + "fmt" + "strings" + + "github.com/hashicorp/consul/command/agent" + "github.com/mitchellh/cli" +) + +// ConfigTestCommand is a Command implementation that is used to +// verify config files +type ConfigTestCommand struct { + Ui cli.Ui +} + +func (c *ConfigTestCommand) Help() string { + helpText := ` +Usage: consul configtest [options] + + Tests that config files are valid by attempting to parse them. Useful to ensure a configuration change will not cause consul to fail after a restart. + +Options: + + -config-file=foo Path to a JSON file to read configuration from. + This can be specified multiple times. + -config-dir=foo 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. + ` + return strings.TrimSpace(helpText) +} + +func (c *ConfigTestCommand) Run(args []string) int { + var configFiles []string + cmdFlags := flag.NewFlagSet("configtest", flag.ContinueOnError) + cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } + cmdFlags.Var((*agent.AppendSliceValue)(&configFiles), "config-file", "json file to read config from") + cmdFlags.Var((*agent.AppendSliceValue)(&configFiles), "config-dir", "directory of json files to read") + if err := cmdFlags.Parse(args); err != nil { + return 1 + } + + if len(configFiles) <= 0 { + c.Ui.Error("Must specify config using -config-file or -config-dir") + return 1 + } + + _, err := agent.ReadConfigPaths(configFiles) + if err != nil { + c.Ui.Error(fmt.Sprintf("Config validation failed: %v", err.Error())) + return 1 + } + return 0 +} + +func (c *ConfigTestCommand) Synopsis() string { + return "Validate config file" +} diff --git a/command/configtest_test.go b/command/configtest_test.go new file mode 100644 index 0000000000..43f6295092 --- /dev/null +++ b/command/configtest_test.go @@ -0,0 +1,105 @@ +package command + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/mitchellh/cli" +) + +func TestConfigTestCommand_implements(t *testing.T) { + var _ cli.Command = &ConfigTestCommand{} +} + +func TestConfigTestCommandFailOnEmptyFile(t *testing.T) { + tmpFile, err := ioutil.TempFile("", "consul") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(tmpFile.Name()) + + cmd := &ConfigTestCommand{ + Ui: new(cli.MockUi), + } + + args := []string{ + "-config-file", tmpFile.Name(), + } + + if code := cmd.Run(args); code == 0 { + t.Fatalf("bad: %d", code) + } +} + +func TestConfigTestCommandSucceedOnEmptyDir(t *testing.T) { + td, err := ioutil.TempDir("", "consul") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(td) + + cmd := &ConfigTestCommand{ + Ui: new(cli.MockUi), + } + + args := []string{ + "-config-dir", td, + } + + if code := cmd.Run(args); code != 0 { + t.Fatalf("bad: %d", code) + } +} + +func TestConfigTestCommandSucceedOnMinimalConfigFile(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 := &ConfigTestCommand{ + Ui: new(cli.MockUi), + } + + args := []string{ + "-config-file", fp, + } + + if code := cmd.Run(args); code != 0 { + t.Fatalf("bad: %d", code) + } +} + +func TestConfigTestCommandSucceedOnMinimalConfigDir(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 := &ConfigTestCommand{ + Ui: new(cli.MockUi), + } + + args := []string{ + "-config-dir", td, + } + + if code := cmd.Run(args); code != 0 { + t.Fatalf("bad: %d", code) + } +} diff --git a/commands.go b/commands.go index bd3d66a33b..eca071390f 100644 --- a/commands.go +++ b/commands.go @@ -27,6 +27,12 @@ func init() { }, nil }, + "configtest": func() (cli.Command, error) { + return &command.ConfigTestCommand{ + Ui: ui, + }, nil + }, + "event": func() (cli.Command, error) { return &command.EventCommand{ Ui: ui, diff --git a/website/source/docs/commands/configtest.html.markdown b/website/source/docs/commands/configtest.html.markdown new file mode 100644 index 0000000000..927b8f25fd --- /dev/null +++ b/website/source/docs/commands/configtest.html.markdown @@ -0,0 +1,36 @@ +--- +layout: "docs" +page_title: "Commands: ConfigTest" +sidebar_current: "docs-commands-configtest" +description: |- + The `consul configtest` command tests that config files are valid by attempting to parse them. Useful to ensure a configuration change will not cause consul to fail after a restart. +--- + +# Consul ConfigTest + +The `consul configtest` command tests that config files are valid by attempting to parse them. Useful to ensure a configuration change will not cause consul to fail after a restart. + +## Usage + +Usage: `consul configtest [options]` + +At least one `-config-file` or `-config-dir` paramater must be given. The list of available flags are: + +- `-config-file` - config file. may be specified multiple times +- `-config-dir` - config directory. all files ending in `.json` in the directory will be included. may be specified multiple times. + +* `-config-file` - A configuration file + to load. For more information on + the format of this file, read the [Configuration Files](/docs/agent/options.html#configuration_files) section in the agent option documentation. + This option can be specified multiple times to load multiple configuration + files. If it is specified multiple times, configuration files loaded later + will merge with configuration files loaded earlier. During a config merge, + single-value keys (string, int, bool) will simply have their values replaced + while list types will be appended together. + +* `-config-dir` - A directory of + configuration files to load. Consul will + load all files in this directory with the suffix ".json". The load order + is alphabetical, and the the same merge routine is used as with the + [`config-file`](#_config_file) option above. For more information + on the format of the configuration files, see the [Configuration Files](/docs/agent/options.html#configuration_files) section in the agent option documentation.