Skip to content

Commit

Permalink
Add config command and config validate subcommand to nomad CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
th0m committed Jan 20, 2022
1 parent 35c22bc commit 45ec419
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 3 deletions.
4 changes: 2 additions & 2 deletions command/agent/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion command/agent/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
10 changes: 10 additions & 0 deletions command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
38 changes: 38 additions & 0 deletions command/config.go
Original file line number Diff line number Diff line change
@@ -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 <subcommand> [options] [args]
This command groups subcommands for interacting with configurations.
Users can validate configurations for the Nomad agent.
Validate configuration:
$ nomad config validate <config_path> [<config_path>...]
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
}
85 changes: 85 additions & 0 deletions command/config_validate.go
Original file line number Diff line number Diff line change
@@ -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 <config_path> [<config_path...>]
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
}
106 changes: 106 additions & 0 deletions command/config_validate_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 45ec419

Please sign in to comment.