From a61afad5bba0f46bac1cb19a2a778d89e53238ca Mon Sep 17 00:00:00 2001 From: Chris Baker Date: Mon, 7 Jan 2019 16:42:44 +0000 Subject: [PATCH 1/8] added validation on client metadata keys --- command/agent/command.go | 8 ++++++++ command/agent/command_test.go | 12 ++++++++++++ command/agent/config_parse.go | 8 ++++++++ command/agent/config_test.go | 25 +++++++++++++++++++++++++ 4 files changed, 53 insertions(+) diff --git a/command/agent/command.go b/command/agent/command.go index c9cdab3df..fdab3e91d 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -9,6 +9,7 @@ import ( "os/signal" "path/filepath" "reflect" + "regexp" "sort" "strconv" "strings" @@ -186,6 +187,8 @@ func (c *Command) readConfig() *Config { // Parse the meta flags. metaLength := len(meta) if metaLength != 0 { + validKeyRe, _ := regexp.Compile(`^[^.]+(\.[^.]+)*$`) + cmdConfig.Client.Meta = make(map[string]string, metaLength) for _, kv := range meta { parts := strings.SplitN(kv, "=", 2) @@ -194,6 +197,11 @@ func (c *Command) readConfig() *Config { return nil } + if !validKeyRe.MatchString(parts[0]) { + c.Ui.Error(fmt.Sprintf("Invalid Client.Meta key: %v", parts[0])) + return nil + } + cmdConfig.Client.Meta[parts[0]] = parts[1] } } diff --git a/command/agent/command_test.go b/command/agent/command_test.go index ee39d7d3f..68de02c5b 100644 --- a/command/agent/command_test.go +++ b/command/agent/command_test.go @@ -48,6 +48,18 @@ func TestCommand_Args(t *testing.T) { []string{"-client", "-alloc-dir="}, "Must specify the state, alloc dir, and plugin dir if data-dir is omitted.", }, + { + []string{"-client", "-data-dir=" + tmpDir, "-meta=invalid..key=inaccessible-value"}, + "Invalid Client.Meta key: invalid..key", + }, + { + []string{"-client", "-data-dir=" + tmpDir, "-meta=.invalid=inaccessible-value"}, + "Invalid Client.Meta key: .invalid", + }, + { + []string{"-client", "-data-dir=" + tmpDir, "-meta=invalid.=inaccessible-value"}, + "Invalid Client.Meta key: invalid.", + }, } for _, tc := range tcases { // Make a new command. We preemptively close the shutdownCh diff --git a/command/agent/config_parse.go b/command/agent/config_parse.go index 641a10c2c..5edfe0d41 100644 --- a/command/agent/config_parse.go +++ b/command/agent/config_parse.go @@ -6,6 +6,7 @@ import ( "io" "os" "path/filepath" + "regexp" "time" multierror "github.com/hashicorp/go-multierror" @@ -437,6 +438,13 @@ func parseClient(result **ClientConfig, list *ast.ObjectList) error { return err } } + + validKeyRe, _ := regexp.Compile(`^[^.]+(\.[^.]+)*$`) + for k, _ := range config.Meta { + if !validKeyRe.MatchString(k) { + return fmt.Errorf("invalid Client.Meta key: %v", k) + } + } } // Parse out chroot_env fields. These are in HCL as a list so we need to diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 4c0d97f6e..90bc613b0 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "reflect" + "strings" "testing" "time" @@ -418,6 +419,30 @@ func TestConfig_ParseConfigFile(t *testing.T) { } } +func TestConfig_ParseInvalidClientMeta(t *testing.T) { + tcases := []string{ + "foo..invalid", + ".invalid", + "invalid.", + } + + for _, tc := range tcases { + reader := strings.NewReader(fmt.Sprintf(`client{ + enabled = true + meta = { + "valid" = "yes" + "` + tc + `" = "kaboom!" + "nested.var" = "is nested" + "deeply.nested.var" = "is deeply nested" + } + }`)) + + if _, err := ParseConfig(reader); err == nil { + t.Fatalf("expected load error, got nothing") + } + } +} + func TestConfig_LoadConfigDir(t *testing.T) { // Fails if the dir doesn't exist. if _, err := LoadConfigDir("/unicorns/leprechauns"); err == nil { From 6d279f57ad67432d779794a58597684a3daac402 Mon Sep 17 00:00:00 2001 From: Chris Baker Date: Mon, 7 Jan 2019 17:32:45 +0000 Subject: [PATCH 2/8] updated CHANGELOG to note backward incompatibility in node metadata validation --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe5fbcf95..a0b93e2af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ __BACKWARDS INCOMPATIBILITIES:__ * client: Task config interpolation requires names to be valid identifiers (`node.region` or `NOMAD_DC`). Interpolating other variables requires a new indexing syntax: `env[".invalid.identifier."]`. [[GH-4843](https://github.com/hashicorp/nomad/issues/4843)] + * client: Node metadata variables must have valid identifiers, whether + specified in the config file (`.client.meta` stanza) or on the command line + (`-meta`). [[GH-5158](https://github.com/hashicorp/nomad/pull/5158)] IMPROVEMENTS: * core: Added advertise address to client node meta data [[GH-4390](https://github.com/hashicorp/nomad/issues/4390)] From f99e18aaf457e038f4361e9ac56050358d7475a9 Mon Sep 17 00:00:00 2001 From: Chris Baker Date: Mon, 7 Jan 2019 18:01:59 +0000 Subject: [PATCH 3/8] gofmt to make check happy --- command/agent/config_parse.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/agent/config_parse.go b/command/agent/config_parse.go index 5edfe0d41..6e681b54b 100644 --- a/command/agent/config_parse.go +++ b/command/agent/config_parse.go @@ -440,7 +440,7 @@ func parseClient(result **ClientConfig, list *ast.ObjectList) error { } validKeyRe, _ := regexp.Compile(`^[^.]+(\.[^.]+)*$`) - for k, _ := range config.Meta { + for k := range config.Meta { if !validKeyRe.MatchString(k) { return fmt.Errorf("invalid Client.Meta key: %v", k) } From 1984805f866dcfe1181184d07df3712369d94f7e Mon Sep 17 00:00:00 2001 From: Michael Schurter Date: Mon, 7 Jan 2019 18:59:26 -0500 Subject: [PATCH 4/8] Update CHANGELOG.md Co-Authored-By: cgbaker --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0b93e2af..63773d749 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ __BACKWARDS INCOMPATIBILITIES:__ (`node.region` or `NOMAD_DC`). Interpolating other variables requires a new indexing syntax: `env[".invalid.identifier."]`. [[GH-4843](https://github.com/hashicorp/nomad/issues/4843)] * client: Node metadata variables must have valid identifiers, whether - specified in the config file (`.client.meta` stanza) or on the command line + specified in the config file (`client.meta` stanza) or on the command line (`-meta`). [[GH-5158](https://github.com/hashicorp/nomad/pull/5158)] IMPROVEMENTS: From bf00f93d873d67d0af1fc5b38896d4cb021aad90 Mon Sep 17 00:00:00 2001 From: Chris Baker Date: Tue, 8 Jan 2019 00:09:21 +0000 Subject: [PATCH 5/8] moved interp key regex out to a helper function --- command/agent/command.go | 19 +++++++++---------- command/agent/config_parse.go | 4 +--- helper/funcs.go | 13 +++++++++++++ 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/command/agent/command.go b/command/agent/command.go index fdab3e91d..5740f8e91 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -9,24 +9,25 @@ import ( "os/signal" "path/filepath" "reflect" - "regexp" "sort" "strconv" "strings" "syscall" "time" - metrics "github.com/armon/go-metrics" + "github.com/hashicorp/nomad/helper" + + "github.com/armon/go-metrics" "github.com/armon/go-metrics/circonus" "github.com/armon/go-metrics/datadog" "github.com/armon/go-metrics/prometheus" "github.com/hashicorp/consul/lib" - checkpoint "github.com/hashicorp/go-checkpoint" - discover "github.com/hashicorp/go-discover" - gsyslog "github.com/hashicorp/go-syslog" + "github.com/hashicorp/go-checkpoint" + "github.com/hashicorp/go-discover" + "github.com/hashicorp/go-syslog" "github.com/hashicorp/logutils" - flaghelper "github.com/hashicorp/nomad/helper/flag-helpers" - gatedwriter "github.com/hashicorp/nomad/helper/gated-writer" + "github.com/hashicorp/nomad/helper/flag-helpers" + "github.com/hashicorp/nomad/helper/gated-writer" "github.com/hashicorp/nomad/nomad/structs/config" "github.com/hashicorp/nomad/version" "github.com/mitchellh/cli" @@ -187,8 +188,6 @@ func (c *Command) readConfig() *Config { // Parse the meta flags. metaLength := len(meta) if metaLength != 0 { - validKeyRe, _ := regexp.Compile(`^[^.]+(\.[^.]+)*$`) - cmdConfig.Client.Meta = make(map[string]string, metaLength) for _, kv := range meta { parts := strings.SplitN(kv, "=", 2) @@ -197,7 +196,7 @@ func (c *Command) readConfig() *Config { return nil } - if !validKeyRe.MatchString(parts[0]) { + if !helper.IsValidInterpVariable(parts[0]) { c.Ui.Error(fmt.Sprintf("Invalid Client.Meta key: %v", parts[0])) return nil } diff --git a/command/agent/config_parse.go b/command/agent/config_parse.go index 6e681b54b..ed8e09dbb 100644 --- a/command/agent/config_parse.go +++ b/command/agent/config_parse.go @@ -6,7 +6,6 @@ import ( "io" "os" "path/filepath" - "regexp" "time" multierror "github.com/hashicorp/go-multierror" @@ -439,9 +438,8 @@ func parseClient(result **ClientConfig, list *ast.ObjectList) error { } } - validKeyRe, _ := regexp.Compile(`^[^.]+(\.[^.]+)*$`) for k := range config.Meta { - if !validKeyRe.MatchString(k) { + if !helper.IsValidInterpVariable(k) { return fmt.Errorf("invalid Client.Meta key: %v", k) } } diff --git a/helper/funcs.go b/helper/funcs.go index cf9cc3dae..1daa8c8ff 100644 --- a/helper/funcs.go +++ b/helper/funcs.go @@ -15,6 +15,11 @@ import ( // validUUID is used to check if a given string looks like a UUID var validUUID = regexp.MustCompile(`(?i)^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$`) +// validInterpVarKey matches valid dotted variable names for interpolation. The +// string must begin with one or more non-dot characters which may be followed +// by sequences containing a dot followed by a one or more non-dot characters. +var validInterpVarKey = regexp.MustCompile(`^[^.]+(\.[^.]+)*$`) + // IsUUID returns true if the given string is a valid UUID. func IsUUID(str string) bool { const uuidLen = 36 @@ -25,6 +30,14 @@ func IsUUID(str string) bool { return validUUID.MatchString(str) } +// IsValidInterpVariable returns true if a valid dotted variable names for +// interpolation. The string must begin with one or more non-dot characters +// which may be followed by sequences containing a dot followed by a one or more +// non-dot characters. +func IsValidInterpVariable(str string) bool { + return validInterpVarKey.MatchString(str) +} + // HashUUID takes an input UUID and returns a hashed version of the UUID to // ensure it is well distributed. func HashUUID(input string) (output string, hashed bool) { From 220e9e838fa94df452079ebf0a7d605321f30d71 Mon Sep 17 00:00:00 2001 From: Chris Baker Date: Tue, 8 Jan 2019 15:07:36 +0000 Subject: [PATCH 6/8] refactored config validation into a new method, modified Meta.Client tests appropriately --- command/agent/command.go | 56 +++++++++++++++++++++-------------- command/agent/command_test.go | 54 +++++++++++++++++++++++++++++++++ command/agent/config_parse.go | 6 ---- command/agent/config_test.go | 25 ---------------- 4 files changed, 88 insertions(+), 53 deletions(-) diff --git a/command/agent/command.go b/command/agent/command.go index 5740f8e91..8420a9cc2 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -195,12 +195,6 @@ func (c *Command) readConfig() *Config { c.Ui.Error(fmt.Sprintf("Error parsing Client.Meta value: %v", kv)) return nil } - - if !helper.IsValidInterpVariable(parts[0]) { - c.Ui.Error(fmt.Sprintf("Invalid Client.Meta key: %v", parts[0])) - return nil - } - cmdConfig.Client.Meta[parts[0]] = parts[1] } } @@ -264,15 +258,6 @@ func (c *Command) readConfig() *Config { } } - // Set up the TLS configuration properly if we have one. - // XXX chelseakomlo: set up a TLSConfig New method which would wrap - // constructor-type actions like this. - if config.TLSConfig != nil && !config.TLSConfig.IsEmpty() { - if err := config.TLSConfig.SetChecksum(); err != nil { - c.Ui.Error(fmt.Sprintf("WARNING: Error when parsing TLS configuration: %v", err)) - } - } - // Default the plugin directory to be under that of the data directory if it // isn't explicitly specified. if config.PluginDir == "" && config.DataDir != "" { @@ -284,10 +269,28 @@ func (c *Command) readConfig() *Config { return config } + if !c.isValidConfig(config) { + return nil + } + + return config +} + +func (c *Command) isValidConfig(config *Config) bool { + // Set up the TLS configuration properly if we have one. + // XXX chelseakomlo: set up a TLSConfig New method which would wrap + // constructor-type actions like this. + if config.TLSConfig != nil && !config.TLSConfig.IsEmpty() { + if err := config.TLSConfig.SetChecksum(); err != nil { + c.Ui.Error(fmt.Sprintf("WARNING: Error when parsing TLS configuration: %v", err)) + return false + } + } + if config.Server.EncryptKey != "" { if _, err := config.Server.EncryptBytes(); err != nil { c.Ui.Error(fmt.Sprintf("Invalid encryption key: %s", err)) - return nil + return false } keyfile := filepath.Join(config.DataDir, serfKeyring) if _, err := os.Stat(keyfile); err == nil { @@ -298,7 +301,7 @@ func (c *Command) readConfig() *Config { // Check that the server is running in at least one mode. if !(config.Server.Enabled || config.Client.Enabled) { c.Ui.Error("Must specify either server, client or dev mode for the agent.") - return nil + return false } // Verify the paths are absolute. @@ -315,14 +318,14 @@ func (c *Command) readConfig() *Config { if !filepath.IsAbs(dir) { c.Ui.Error(fmt.Sprintf("%s must be given as an absolute path: got %v", k, dir)) - return nil + return false } } // Ensure that we have the directories we need to run. if config.Server.Enabled && config.DataDir == "" { c.Ui.Error("Must specify data directory") - return nil + return false } // The config is valid if the top-level data-dir is set or if both @@ -330,20 +333,29 @@ func (c *Command) readConfig() *Config { if config.Client.Enabled && config.DataDir == "" { if config.Client.AllocDir == "" || config.Client.StateDir == "" || config.PluginDir == "" { c.Ui.Error("Must specify the state, alloc dir, and plugin dir if data-dir is omitted.") - return nil + return false + } + } + + if config.Client.Enabled { + for k := range config.Client.Meta { + if !helper.IsValidInterpVariable(k) { + c.Ui.Error(fmt.Sprintf("Invalid Client.Meta key: %v", k)) + return false + } } } // Check the bootstrap flags if config.Server.BootstrapExpect > 0 && !config.Server.Enabled { c.Ui.Error("Bootstrap requires server mode to be enabled") - return nil + return false } if config.Server.BootstrapExpect == 1 { c.Ui.Error("WARNING: Bootstrap mode enabled! Potentially unsafe operation.") } - return config + return true } // setupLoggers is used to setup the logGate, logWriter, and our logOutput diff --git a/command/agent/command_test.go b/command/agent/command_test.go index 68de02c5b..eed6bba82 100644 --- a/command/agent/command_test.go +++ b/command/agent/command_test.go @@ -3,6 +3,7 @@ package agent import ( "io/ioutil" "os" + "path/filepath" "strings" "testing" @@ -88,3 +89,56 @@ func TestCommand_Args(t *testing.T) { } } } + +func TestCommand_MetaConfigValidation(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "nomad") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(tmpDir) + + tcases := []string{ + "foo..invalid", + ".invalid", + "invalid.", + } + for _, tc := range tcases { + configFile := filepath.Join(tmpDir, "conf1.hcl") + err = ioutil.WriteFile(configFile, []byte(`client{ + enabled = true + meta = { + "valid" = "yes" + "`+tc+`" = "kaboom!" + "nested.var" = "is nested" + "deeply.nested.var" = "is deeply nested" + } + }`), 0600) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Make a new command. We preemptively close the shutdownCh + // so that the command exits immediately instead of blocking. + ui := new(cli.MockUi) + shutdownCh := make(chan struct{}) + close(shutdownCh) + cmd := &Command{ + Version: version.GetVersion(), + Ui: ui, + ShutdownCh: shutdownCh, + } + + // To prevent test failures on hosts whose hostname resolves to + // a loopback address, we must append a bind address + args := []string{"-client", "-data-dir=" + tmpDir, "-config=" + configFile, "-bind=169.254.0.1"} + if code := cmd.Run(args); code != 1 { + t.Fatalf("args: %v\nexit: %d\n", args, code) + } + + expect := "Invalid Client.Meta key: " + tc + out := ui.ErrorWriter.String() + if !strings.Contains(out, expect) { + t.Fatalf("expect to find %q\n\n%s", expect, out) + } + } +} diff --git a/command/agent/config_parse.go b/command/agent/config_parse.go index ed8e09dbb..641a10c2c 100644 --- a/command/agent/config_parse.go +++ b/command/agent/config_parse.go @@ -437,12 +437,6 @@ func parseClient(result **ClientConfig, list *ast.ObjectList) error { return err } } - - for k := range config.Meta { - if !helper.IsValidInterpVariable(k) { - return fmt.Errorf("invalid Client.Meta key: %v", k) - } - } } // Parse out chroot_env fields. These are in HCL as a list so we need to diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 90bc613b0..4c0d97f6e 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -7,7 +7,6 @@ import ( "os" "path/filepath" "reflect" - "strings" "testing" "time" @@ -419,30 +418,6 @@ func TestConfig_ParseConfigFile(t *testing.T) { } } -func TestConfig_ParseInvalidClientMeta(t *testing.T) { - tcases := []string{ - "foo..invalid", - ".invalid", - "invalid.", - } - - for _, tc := range tcases { - reader := strings.NewReader(fmt.Sprintf(`client{ - enabled = true - meta = { - "valid" = "yes" - "` + tc + `" = "kaboom!" - "nested.var" = "is nested" - "deeply.nested.var" = "is deeply nested" - } - }`)) - - if _, err := ParseConfig(reader); err == nil { - t.Fatalf("expected load error, got nothing") - } - } -} - func TestConfig_LoadConfigDir(t *testing.T) { // Fails if the dir doesn't exist. if _, err := LoadConfigDir("/unicorns/leprechauns"); err == nil { From d8a3a74c43ad7af4f337f5ace59d5dd722eb2c46 Mon Sep 17 00:00:00 2001 From: Chris Baker Date: Tue, 8 Jan 2019 22:21:48 +0000 Subject: [PATCH 7/8] move `if dev` check into config validation, to support dev-mod validation in the future --- command/agent/command.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/command/agent/command.go b/command/agent/command.go index 8420a9cc2..8319bd52a 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -264,11 +264,6 @@ func (c *Command) readConfig() *Config { config.PluginDir = filepath.Join(config.DataDir, "plugins") } - if dev { - // Skip validation for dev mode - return config - } - if !c.isValidConfig(config) { return nil } @@ -277,13 +272,24 @@ func (c *Command) readConfig() *Config { } func (c *Command) isValidConfig(config *Config) bool { + + if config.DevMode { + // Skip the rest of the validation for dev mode + return true + } + + // Check that the server is running in at least one mode. + if !(config.Server.Enabled || config.Client.Enabled) { + c.Ui.Error("Must specify either server, client or dev mode for the agent.") + return false + } + // Set up the TLS configuration properly if we have one. // XXX chelseakomlo: set up a TLSConfig New method which would wrap // constructor-type actions like this. if config.TLSConfig != nil && !config.TLSConfig.IsEmpty() { if err := config.TLSConfig.SetChecksum(); err != nil { c.Ui.Error(fmt.Sprintf("WARNING: Error when parsing TLS configuration: %v", err)) - return false } } @@ -298,12 +304,6 @@ func (c *Command) isValidConfig(config *Config) bool { } } - // Check that the server is running in at least one mode. - if !(config.Server.Enabled || config.Client.Enabled) { - c.Ui.Error("Must specify either server, client or dev mode for the agent.") - return false - } - // Verify the paths are absolute. dirs := map[string]string{ "data-dir": config.DataDir, From d5b1a56f3b85337247a5787cef60c8c818637094 Mon Sep 17 00:00:00 2001 From: Chris Baker Date: Wed, 9 Jan 2019 18:56:40 +0000 Subject: [PATCH 8/8] increased config validation coverage for dev mode --- command/agent/command.go | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/command/agent/command.go b/command/agent/command.go index 8319bd52a..c30f46caf 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -15,8 +15,6 @@ import ( "syscall" "time" - "github.com/hashicorp/nomad/helper" - "github.com/armon/go-metrics" "github.com/armon/go-metrics/circonus" "github.com/armon/go-metrics/datadog" @@ -26,6 +24,7 @@ import ( "github.com/hashicorp/go-discover" "github.com/hashicorp/go-syslog" "github.com/hashicorp/logutils" + "github.com/hashicorp/nomad/helper" "github.com/hashicorp/nomad/helper/flag-helpers" "github.com/hashicorp/nomad/helper/gated-writer" "github.com/hashicorp/nomad/nomad/structs/config" @@ -273,11 +272,6 @@ func (c *Command) readConfig() *Config { func (c *Command) isValidConfig(config *Config) bool { - if config.DevMode { - // Skip the rest of the validation for dev mode - return true - } - // Check that the server is running in at least one mode. if !(config.Server.Enabled || config.Client.Enabled) { c.Ui.Error("Must specify either server, client or dev mode for the agent.") @@ -322,6 +316,20 @@ func (c *Command) isValidConfig(config *Config) bool { } } + if config.Client.Enabled { + for k := range config.Client.Meta { + if !helper.IsValidInterpVariable(k) { + c.Ui.Error(fmt.Sprintf("Invalid Client.Meta key: %v", k)) + return false + } + } + } + + if config.DevMode { + // Skip the rest of the validation for dev mode + return true + } + // Ensure that we have the directories we need to run. if config.Server.Enabled && config.DataDir == "" { c.Ui.Error("Must specify data directory") @@ -337,15 +345,6 @@ func (c *Command) isValidConfig(config *Config) bool { } } - if config.Client.Enabled { - for k := range config.Client.Meta { - if !helper.IsValidInterpVariable(k) { - c.Ui.Error(fmt.Sprintf("Invalid Client.Meta key: %v", k)) - return false - } - } - } - // Check the bootstrap flags if config.Server.BootstrapExpect > 0 && !config.Server.Enabled { c.Ui.Error("Bootstrap requires server mode to be enabled")