diff --git a/command/agent.go b/command/agent/agent.go similarity index 66% rename from command/agent.go rename to command/agent/agent.go index 265cbc87f..cc932eb35 100644 --- a/command/agent.go +++ b/command/agent/agent.go @@ -1,14 +1,13 @@ -package command +package agent import ( - "encoding/json" + "flag" "fmt" "io" "log" "os" "os/signal" "path/filepath" - "regexp" "strings" "syscall" "time" @@ -18,6 +17,7 @@ import ( "github.com/armon/go-metrics/datadog" "github.com/hashicorp/consul/agent" "github.com/hashicorp/consul/agent/config" + "github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/logger" "github.com/hashicorp/go-checkpoint" @@ -26,77 +26,115 @@ import ( "github.com/mitchellh/cli" ) -// validDatacenter is used to validate a datacenter -var validDatacenter = regexp.MustCompile("^[a-zA-Z0-9_-]+$") +func New(ui cli.Ui, revision, version, versionPre, versionHuman string, shutdownCh <-chan struct{}) *cmd { + ui = &cli.PrefixedUi{ + OutputPrefix: "==> ", + InfoPrefix: " ", + ErrorPrefix: "==> ", + Ui: ui, + } + + c := &cmd{ + UI: ui, + revision: revision, + version: version, + versionPrerelease: versionPre, + versionHuman: versionHuman, + shutdownCh: shutdownCh, + } + c.init() + return c +} // AgentCommand is a Command implementation that runs a Consul agent. // The command will not end unless a shutdown message is sent on the // ShutdownCh. If two messages are sent on the ShutdownCh it will forcibly // exit. -type AgentCommand struct { - BaseCommand - Revision string - Version string - VersionPrerelease string - HumanVersion string - ShutdownCh <-chan struct{} +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + usage string + revision string + version string + versionPrerelease string + versionHuman string + shutdownCh <-chan struct{} args []string + flagArgs config.Flags logFilter *logutils.LevelFilter logOutput io.Writer logger *log.Logger } +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + config.AddFlags(c.flags, &c.flagArgs) + c.usage = flags.Usage(usage, c.flags, nil, nil) +} + +func (c *cmd) Synopsis() string { + return "Runs a Consul agent" +} + +func (c *cmd) Help() string { + return c.usage +} + +func (c *cmd) Run(args []string) int { + code := c.run(args) + if c.logger != nil { + c.logger.Println("[INFO] Exit code:", code) + } + return code +} + // readConfig is responsible for setup of our configuration using // the command line and any file configs -func (cmd *AgentCommand) readConfig() *config.RuntimeConfig { - cmd.InitFlagSet() - - var flags config.Flags - config.AddFlags(cmd.FlagSet, &flags) - - if err := cmd.FlagSet.Parse(cmd.args); err != nil { +func (c *cmd) readConfig() *config.RuntimeConfig { + if err := c.flags.Parse(c.args); err != nil { if !strings.Contains(err.Error(), "help requested") { - cmd.UI.Error(fmt.Sprintf("error parsing flags: %v", err)) + c.UI.Error(fmt.Sprintf("error parsing flags: %v", err)) } return nil } - b, err := config.NewBuilder(flags) + b, err := config.NewBuilder(c.flagArgs) if err != nil { - cmd.UI.Error(err.Error()) + c.UI.Error(err.Error()) return nil } cfg, err := b.BuildAndValidate() if err != nil { - cmd.UI.Error(err.Error()) + c.UI.Error(err.Error()) return nil } for _, w := range b.Warnings { - cmd.UI.Warn(w) + c.UI.Warn(w) } return &cfg } // checkpointResults is used to handler periodic results from our update checker -func (cmd *AgentCommand) checkpointResults(results *checkpoint.CheckResponse, err error) { +func (c *cmd) checkpointResults(results *checkpoint.CheckResponse, err error) { if err != nil { - cmd.UI.Error(fmt.Sprintf("Failed to check for updates: %v", err)) + c.UI.Error(fmt.Sprintf("Failed to check for updates: %v", err)) return } if results.Outdated { - cmd.UI.Error(fmt.Sprintf("Newer Consul version available: %s (currently running: %s)", results.CurrentVersion, cmd.Version)) + c.UI.Error(fmt.Sprintf("Newer Consul version available: %s (currently running: %s)", results.CurrentVersion, c.version)) } for _, alert := range results.Alerts { switch alert.Level { case "info": - cmd.UI.Info(fmt.Sprintf("Bulletin [%s]: %s (%s)", alert.Level, alert.Message, alert.URL)) + c.UI.Info(fmt.Sprintf("Bulletin [%s]: %s (%s)", alert.Level, alert.Message, alert.URL)) default: - cmd.UI.Error(fmt.Sprintf("Bulletin [%s]: %s (%s)", alert.Level, alert.Message, alert.URL)) + c.UI.Error(fmt.Sprintf("Bulletin [%s]: %s (%s)", alert.Level, alert.Message, alert.URL)) } } } -func (cmd *AgentCommand) startupUpdateCheck(config *config.RuntimeConfig) { +func (c *cmd) startupUpdateCheck(config *config.RuntimeConfig) { version := config.Version if config.VersionPrerelease != "" { version += fmt.Sprintf("-%s", config.VersionPrerelease) @@ -110,44 +148,44 @@ func (cmd *AgentCommand) startupUpdateCheck(config *config.RuntimeConfig) { } // Schedule a periodic check with expected interval of 24 hours - checkpoint.CheckInterval(updateParams, 24*time.Hour, cmd.checkpointResults) + checkpoint.CheckInterval(updateParams, 24*time.Hour, c.checkpointResults) // Do an immediate check within the next 30 seconds go func() { time.Sleep(lib.RandomStagger(30 * time.Second)) - cmd.checkpointResults(checkpoint.Check(updateParams)) + c.checkpointResults(checkpoint.Check(updateParams)) }() } // startupJoin is invoked to handle any joins specified to take place at start time -func (cmd *AgentCommand) startupJoin(agent *agent.Agent, cfg *config.RuntimeConfig) error { +func (c *cmd) startupJoin(agent *agent.Agent, cfg *config.RuntimeConfig) error { if len(cfg.StartJoinAddrsLAN) == 0 { return nil } - cmd.UI.Output("Joining cluster...") + c.UI.Output("Joining cluster...") n, err := agent.JoinLAN(cfg.StartJoinAddrsLAN) if err != nil { return err } - cmd.UI.Info(fmt.Sprintf("Join completed. Synced with %d initial agents", n)) + c.UI.Info(fmt.Sprintf("Join completed. Synced with %d initial agents", n)) return nil } // startupJoinWan is invoked to handle any joins -wan specified to take place at start time -func (cmd *AgentCommand) startupJoinWan(agent *agent.Agent, cfg *config.RuntimeConfig) error { +func (c *cmd) startupJoinWan(agent *agent.Agent, cfg *config.RuntimeConfig) error { if len(cfg.StartJoinAddrsWAN) == 0 { return nil } - cmd.UI.Output("Joining -wan cluster...") + c.UI.Output("Joining -wan cluster...") n, err := agent.JoinWAN(cfg.StartJoinAddrsWAN) if err != nil { return err } - cmd.UI.Info(fmt.Sprintf("Join -wan completed. Synced with %d initial agents", n)) + c.UI.Info(fmt.Sprintf("Join -wan completed. Synced with %d initial agents", n)) return nil } @@ -264,25 +302,10 @@ func startupTelemetry(conf *config.RuntimeConfig) (*metrics.InmemSink, error) { return memSink, nil } -func (cmd *AgentCommand) Run(args []string) int { - code := cmd.run(args) - if cmd.logger != nil { - cmd.logger.Println("[INFO] Exit code:", code) - } - return code -} - -func (cmd *AgentCommand) run(args []string) int { - cmd.UI = &cli.PrefixedUi{ - OutputPrefix: "==> ", - InfoPrefix: " ", - ErrorPrefix: "==> ", - Ui: cmd.UI, - } - +func (c *cmd) run(args []string) int { // Parse our configs - cmd.args = args - config := cmd.readConfig() + c.args = args + config := c.readConfig() if config == nil { return 1 } @@ -293,25 +316,25 @@ func (cmd *AgentCommand) run(args []string) int { EnableSyslog: config.EnableSyslog, SyslogFacility: config.SyslogFacility, } - logFilter, logGate, logWriter, logOutput, ok := logger.Setup(logConfig, cmd.UI) + logFilter, logGate, logWriter, logOutput, ok := logger.Setup(logConfig, c.UI) if !ok { return 1 } - cmd.logFilter = logFilter - cmd.logOutput = logOutput - cmd.logger = log.New(logOutput, "", log.LstdFlags) + c.logFilter = logFilter + c.logOutput = logOutput + c.logger = log.New(logOutput, "", log.LstdFlags) memSink, err := startupTelemetry(config) if err != nil { - cmd.UI.Error(err.Error()) + c.UI.Error(err.Error()) return 1 } // Create the agent - cmd.UI.Output("Starting Consul agent...") + c.UI.Output("Starting Consul agent...") agent, err := agent.New(config) if err != nil { - cmd.UI.Error(fmt.Sprintf("Error creating agent: %s", err)) + c.UI.Error(fmt.Sprintf("Error creating agent: %s", err)) return 1 } agent.LogOutput = logOutput @@ -319,7 +342,7 @@ func (cmd *AgentCommand) run(args []string) int { agent.MemSink = memSink if err := agent.Start(); err != nil { - cmd.UI.Error(fmt.Sprintf("Error starting agent: %s", err)) + c.UI.Error(fmt.Sprintf("Error starting agent: %s", err)) return 1 } @@ -328,16 +351,16 @@ func (cmd *AgentCommand) run(args []string) int { defer agent.ShutdownAgent() if !config.DisableUpdateCheck { - cmd.startupUpdateCheck(config) + c.startupUpdateCheck(config) } - if err := cmd.startupJoin(agent, config); err != nil { - cmd.UI.Error(err.Error()) + if err := c.startupJoin(agent, config); err != nil { + c.UI.Error(err.Error()) return 1 } - if err := cmd.startupJoinWan(agent, config); err != nil { - cmd.UI.Error(err.Error()) + if err := c.startupJoinWan(agent, config); err != nil { + c.UI.Error(err.Error()) return 1 } @@ -349,22 +372,22 @@ func (cmd *AgentCommand) run(args []string) int { segment = "" } - cmd.UI.Output("Consul agent running!") - cmd.UI.Info(fmt.Sprintf(" Version: '%s'", cmd.HumanVersion)) - cmd.UI.Info(fmt.Sprintf(" Node ID: '%s'", config.NodeID)) - cmd.UI.Info(fmt.Sprintf(" Node name: '%s'", config.NodeName)) - cmd.UI.Info(fmt.Sprintf(" Datacenter: '%s' (Segment: '%s')", config.Datacenter, segment)) - cmd.UI.Info(fmt.Sprintf(" Server: %v (Bootstrap: %v)", config.ServerMode, config.Bootstrap)) - cmd.UI.Info(fmt.Sprintf(" Client Addr: %v (HTTP: %d, HTTPS: %d, DNS: %d)", config.ClientAddrs, + c.UI.Output("Consul agent running!") + c.UI.Info(fmt.Sprintf(" Version: '%s'", c.versionHuman)) + c.UI.Info(fmt.Sprintf(" Node ID: '%s'", config.NodeID)) + c.UI.Info(fmt.Sprintf(" Node name: '%s'", config.NodeName)) + c.UI.Info(fmt.Sprintf(" Datacenter: '%s' (Segment: '%s')", config.Datacenter, segment)) + c.UI.Info(fmt.Sprintf(" Server: %v (Bootstrap: %v)", config.ServerMode, config.Bootstrap)) + c.UI.Info(fmt.Sprintf(" Client Addr: %v (HTTP: %d, HTTPS: %d, DNS: %d)", config.ClientAddrs, config.HTTPPort, config.HTTPSPort, config.DNSPort)) - cmd.UI.Info(fmt.Sprintf(" Cluster Addr: %v (LAN: %d, WAN: %d)", config.AdvertiseAddrLAN, + c.UI.Info(fmt.Sprintf(" Cluster Addr: %v (LAN: %d, WAN: %d)", config.AdvertiseAddrLAN, config.SerfPortLAN, config.SerfPortWAN)) - cmd.UI.Info(fmt.Sprintf(" Encrypt: Gossip: %v, TLS-Outgoing: %v, TLS-Incoming: %v", + c.UI.Info(fmt.Sprintf(" Encrypt: Gossip: %v, TLS-Outgoing: %v, TLS-Incoming: %v", agent.GossipEncrypted(), config.VerifyOutgoing, config.VerifyIncoming)) // Enable log streaming - cmd.UI.Info("") - cmd.UI.Output("Log data will now stream in as it occurs:\n") + c.UI.Info("") + c.UI.Output("Log data will now stream in as it occurs:\n") logGate.Flush() // wait for signal @@ -381,10 +404,10 @@ func (cmd *AgentCommand) run(args []string) int { case ch := <-agent.ReloadCh(): sig = syscall.SIGHUP reloadErrCh = ch - case <-cmd.ShutdownCh: + case <-c.shutdownCh: sig = os.Interrupt case err := <-agent.RetryJoinCh(): - cmd.logger.Println("[ERR] Retry join failed: ", err) + c.logger.Println("[ERR] Retry join failed: ", err) return 1 case <-agent.ShutdownCh(): // agent is already down! @@ -396,14 +419,14 @@ func (cmd *AgentCommand) run(args []string) int { continue case syscall.SIGHUP: - cmd.logger.Println("[INFO] Caught signal: ", sig) + c.logger.Println("[INFO] Caught signal: ", sig) - conf, err := cmd.handleReload(agent, config) + conf, err := c.handleReload(agent, config) if conf != nil { config = conf } if err != nil { - cmd.logger.Println("[ERR] Reload config failed: ", err) + c.logger.Println("[ERR] Reload config failed: ", err) } // Send result back if reload was called via HTTP if reloadErrCh != nil { @@ -411,19 +434,19 @@ func (cmd *AgentCommand) run(args []string) int { } default: - cmd.logger.Println("[INFO] Caught signal: ", sig) + c.logger.Println("[INFO] Caught signal: ", sig) graceful := (sig == os.Interrupt && !(config.SkipLeaveOnInt)) || (sig == syscall.SIGTERM && (config.LeaveOnTerm)) if !graceful { - cmd.logger.Println("[INFO] Graceful shutdown disabled. Exiting") + c.logger.Println("[INFO] Graceful shutdown disabled. Exiting") return 1 } - cmd.logger.Println("[INFO] Gracefully shutting down agent...") + c.logger.Println("[INFO] Gracefully shutting down agent...") gracefulCh := make(chan struct{}) go func() { if err := agent.Leave(); err != nil { - cmd.logger.Println("[ERR] Error on leave:", err) + c.logger.Println("[ERR] Error on leave:", err) return } close(gracefulCh) @@ -432,13 +455,13 @@ func (cmd *AgentCommand) run(args []string) int { gracefulTimeout := 15 * time.Second select { case <-signalCh: - cmd.logger.Printf("[INFO] Caught second signal %v. Exiting\n", sig) + c.logger.Printf("[INFO] Caught second signal %v. Exiting\n", sig) return 1 case <-time.After(gracefulTimeout): - cmd.logger.Println("[INFO] Timeout on graceful leave. Exiting") + c.logger.Println("[INFO] Timeout on graceful leave. Exiting") return 1 case <-gracefulCh: - cmd.logger.Println("[INFO] Graceful exit completed") + c.logger.Println("[INFO] Graceful exit completed") return 0 } } @@ -446,10 +469,10 @@ func (cmd *AgentCommand) run(args []string) int { } // handleReload is invoked when we should reload our configs, e.g. SIGHUP -func (cmd *AgentCommand) handleReload(agent *agent.Agent, cfg *config.RuntimeConfig) (*config.RuntimeConfig, error) { - cmd.logger.Println("[INFO] Reloading configuration...") +func (c *cmd) handleReload(agent *agent.Agent, cfg *config.RuntimeConfig) (*config.RuntimeConfig, error) { + c.logger.Println("[INFO] Reloading configuration...") var errs error - newCfg := cmd.readConfig() + newCfg := c.readConfig() if newCfg == nil { errs = multierror.Append(errs, fmt.Errorf("Failed to reload configs")) return cfg, errs @@ -457,12 +480,12 @@ func (cmd *AgentCommand) handleReload(agent *agent.Agent, cfg *config.RuntimeCon // Change the log level minLevel := logutils.LogLevel(strings.ToUpper(newCfg.LogLevel)) - if logger.ValidateLevelFilter(minLevel, cmd.logFilter) { - cmd.logFilter.SetMinLevel(minLevel) + if logger.ValidateLevelFilter(minLevel, c.logFilter) { + c.logFilter.SetMinLevel(minLevel) } else { errs = multierror.Append(fmt.Errorf( "Invalid log level: %s. Valid log levels are: %v", - minLevel, cmd.logFilter.Levels)) + minLevel, c.logFilter.Levels)) // Keep the current log level newCfg.LogLevel = cfg.LogLevel @@ -476,28 +499,7 @@ func (cmd *AgentCommand) handleReload(agent *agent.Agent, cfg *config.RuntimeCon return cfg, errs } -func (cmd *AgentCommand) Synopsis() string { - return "Runs a Consul agent" -} - -func (cmd *AgentCommand) Help() string { - cmd.InitFlagSet() - config.AddFlags(cmd.FlagSet, &config.Flags{}) - return cmd.HelpCommand(` -Usage: consul agent [options] +const usage = `Usage: consul agent [options] Starts the Consul agent and runs until an interrupt is received. The - agent represents a single node in a cluster. - - `) -} - -func printJSON(name string, v interface{}) { - fmt.Println(name) - b, err := json.MarshalIndent(v, "", " ") - if err != nil { - fmt.Printf("%#v\n", v) - return - } - fmt.Println(string(b)) -} + agent represents a single node in a cluster.` diff --git a/command/agent_test.go b/command/agent/agent_test.go similarity index 81% rename from command/agent_test.go rename to command/agent/agent_test.go index 2ab73e35e..fdc216989 100644 --- a/command/agent_test.go +++ b/command/agent/agent_test.go @@ -1,4 +1,4 @@ -package command +package agent import ( "fmt" @@ -16,42 +16,6 @@ import ( "github.com/mitchellh/cli" ) -func baseCommand(ui *cli.MockUi) BaseCommand { - return BaseCommand{ - Flags: FlagSetNone, - UI: ui, - } -} - -func TestCommand_implements(t *testing.T) { - t.Parallel() - var _ cli.Command = new(AgentCommand) -} - -func TestValidDatacenter(t *testing.T) { - t.Parallel() - shouldMatch := []string{ - "dc1", - "east-aws-001", - "PROD_aws01-small", - } - noMatch := []string{ - "east.aws", - "east!aws", - "first,second", - } - for _, m := range shouldMatch { - if !validDatacenter.MatchString(m) { - t.Fatalf("expected match: %s", m) - } - } - for _, m := range noMatch { - if validDatacenter.MatchString(m) { - t.Fatalf("expected no match: %s", m) - } - } -} - // TestConfigFail should test command line flags that lead to an immediate error. func TestConfigFail(t *testing.T) { t.Parallel() @@ -129,11 +93,8 @@ func TestRetryJoin(t *testing.T) { <-doneCh }() - cmd := &AgentCommand{ - Version: version.Version, - ShutdownCh: shutdownCh, - BaseCommand: baseCommand(cli.NewMockUi()), - } + ui := cli.NewMockUi() + cmd := New(ui, "", version.Version, "", "", shutdownCh) args := []string{ "-server", @@ -173,10 +134,8 @@ func TestRetryJoinFail(t *testing.T) { shutdownCh := make(chan struct{}) defer close(shutdownCh) - cmd := &AgentCommand{ - ShutdownCh: shutdownCh, - BaseCommand: baseCommand(cli.NewMockUi()), - } + ui := cli.NewMockUi() + cmd := New(ui, "", "", "", "", shutdownCh) args := []string{ "-bind", cfg.BindAddr.String(), @@ -201,10 +160,8 @@ func TestRetryJoinWanFail(t *testing.T) { shutdownCh := make(chan struct{}) defer close(shutdownCh) - cmd := &AgentCommand{ - ShutdownCh: shutdownCh, - BaseCommand: baseCommand(cli.NewMockUi()), - } + ui := cli.NewMockUi() + cmd := New(ui, "", "", "", "", shutdownCh) args := []string{ "-server", @@ -245,11 +202,9 @@ func TestProtectDataDir(t *testing.T) { } ui := cli.NewMockUi() - cmd := &AgentCommand{ - BaseCommand: baseCommand(ui), - args: []string{"-config-file=" + cfgFile.Name()}, - } - if conf := cmd.readConfig(); conf != nil { + cmd := New(ui, "", "", "", "", nil) + args := []string{"-config-file=" + cfgFile.Name()} + if code := cmd.Run(args); code == 0 { t.Fatalf("should fail") } if out := ui.ErrorWriter.String(); !strings.Contains(out, dir) { @@ -269,11 +224,9 @@ func TestBadDataDirPermissions(t *testing.T) { defer os.RemoveAll(dataDir) ui := cli.NewMockUi() - cmd := &AgentCommand{ - BaseCommand: baseCommand(ui), - args: []string{"-data-dir=" + dataDir, "-server=true", "-bind=10.0.0.1"}, - } - if conf := cmd.readConfig(); conf != nil { + cmd := New(ui, "", "", "", "", nil) + args := []string{"-data-dir=" + dataDir, "-server=true", "-bind=10.0.0.1"} + if code := cmd.Run(args); code == 0 { t.Fatalf("Should fail with bad data directory permissions") } if out := ui.ErrorWriter.String(); !strings.Contains(out, "Permission denied") { diff --git a/command/commands.go b/command/commands.go index 496755b27..23c5b8ebe 100644 --- a/command/commands.go +++ b/command/commands.go @@ -5,6 +5,7 @@ import ( "os/signal" "syscall" + agentcmd "github.com/hashicorp/consul/command/agent" "github.com/hashicorp/consul/command/cat" "github.com/hashicorp/consul/command/catlistdc" "github.com/hashicorp/consul/command/catlistnodes" @@ -55,17 +56,14 @@ func init() { Commands = map[string]cli.CommandFactory{ "agent": func() (cli.Command, error) { - return &AgentCommand{ - BaseCommand: BaseCommand{ - Flags: FlagSetNone, - UI: ui, - }, - Revision: version.GitCommit, - Version: version.Version, - VersionPrerelease: version.VersionPrerelease, - HumanVersion: version.GetHumanVersion(), - ShutdownCh: make(chan struct{}), - }, nil + return agentcmd.New( + ui, + version.GitCommit, + version.Version, + version.VersionPrerelease, + version.GetHumanVersion(), + make(chan struct{}), + ), nil }, "catalog": func() (cli.Command, error) {