diff --git a/api/operator.go b/api/operator.go index edfeeaec2..9d977b318 100644 --- a/api/operator.go +++ b/api/operator.go @@ -235,4 +235,4 @@ func (op *Operator) AutopilotCASConfiguration(conf *AutopilotConfiguration, q *W res := strings.Contains(string(buf.Bytes()), "true") return res, nil -} \ No newline at end of file +} diff --git a/api/operator_test.go b/api/operator_test.go index efd75db3a..e5e1e122b 100644 --- a/api/operator_test.go +++ b/api/operator_test.go @@ -177,4 +177,4 @@ func TestOperator_AutopilotCASConfiguration(t *testing.T) { t.Fatalf("bad: %v", resp) } } -} \ No newline at end of file +} diff --git a/command/operator_autopilot.go b/command/operator_autopilot.go new file mode 100644 index 000000000..c187fa6a4 --- /dev/null +++ b/command/operator_autopilot.go @@ -0,0 +1,32 @@ +package command + +import ( + "strings" + + "github.com/hashicorp/consul/command/base" + "github.com/mitchellh/cli" +) + +type OperatorAutopilotCommand struct { + base.Command +} + +func (c *OperatorAutopilotCommand) Help() string { + helpText := ` +Usage: consul operator autopilot [options] + +The Autopilot operator command is used to interact with Consul's Autopilot +subsystem. The command can be used to view or modify the current configuration. + +` + + return strings.TrimSpace(helpText) +} + +func (c *OperatorAutopilotCommand) Synopsis() string { + return "Provides tools for modifying Autopilot configuration" +} + +func (c *OperatorAutopilotCommand) Run(args []string) int { + return cli.RunResultHelp +} diff --git a/command/operator_autopilot_get.go b/command/operator_autopilot_get.go new file mode 100644 index 000000000..ed714f881 --- /dev/null +++ b/command/operator_autopilot_get.go @@ -0,0 +1,61 @@ +package command + +import ( + "flag" + "fmt" + "strings" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/base" +) + +type OperatorAutopilotGetCommand struct { + base.Command +} + +func (c *OperatorAutopilotGetCommand) Help() string { + helpText := ` +Usage: consul operator autopilot get-config [options] + +Displays the current Autopilot configuration. + +` + c.Command.Help() + + return strings.TrimSpace(helpText) +} + +func (c *OperatorAutopilotGetCommand) Synopsis() string { + return "Display the current Autopilot configuration" +} + +func (c *OperatorAutopilotGetCommand) Run(args []string) int { + c.Command.NewFlagSet(c) + + if err := c.Command.Parse(args); err != nil { + if err == flag.ErrHelp { + return 0 + } + c.Ui.Error(fmt.Sprintf("Failed to parse args: %v", err)) + return 1 + } + + // Set up a client. + client, err := c.Command.HTTPClient() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) + return 1 + } + + // Fetch the current configuration. + opts := &api.QueryOptions{ + AllowStale: c.Command.HTTPStale(), + } + config, err := client.Operator().AutopilotGetConfiguration(opts) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying Autopilot configuration: %s", err)) + return 1 + } + c.Ui.Output(fmt.Sprintf("DeadServerCleanup = %v", config.DeadServerCleanup)) + + return 0 +} diff --git a/command/operator_autopilot_get_test.go b/command/operator_autopilot_get_test.go new file mode 100644 index 000000000..74496d0f1 --- /dev/null +++ b/command/operator_autopilot_get_test.go @@ -0,0 +1,37 @@ +package command + +import ( + "strings" + "testing" + + "github.com/hashicorp/consul/command/base" + "github.com/mitchellh/cli" +) + +func TestOperator_Autopilot_Get_Implements(t *testing.T) { + var _ cli.Command = &OperatorAutopilotGetCommand{} +} + +func TestOperator_Autopilot_Get(t *testing.T) { + a1 := testAgent(t) + defer a1.Shutdown() + waitForLeader(t, a1.httpAddr) + + ui := new(cli.MockUi) + c := OperatorAutopilotGetCommand{ + Command: base.Command{ + Ui: ui, + Flags: base.FlagSetHTTP, + }, + } + args := []string{"-http-addr=" + a1.httpAddr} + + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + output := strings.TrimSpace(ui.OutputWriter.String()) + if !strings.Contains(output, "DeadServerCleanup = true") { + t.Fatalf("bad: %s", output) + } +} diff --git a/command/operator_autopilot_set.go b/command/operator_autopilot_set.go new file mode 100644 index 000000000..fda4b65ce --- /dev/null +++ b/command/operator_autopilot_set.go @@ -0,0 +1,85 @@ +package command + +import ( + "flag" + "fmt" + "strings" + + "github.com/hashicorp/consul/command/base" +) + +type OperatorAutopilotSetCommand struct { + base.Command +} + +func (c *OperatorAutopilotSetCommand) Help() string { + helpText := ` +Usage: consul operator autopilot set-config [options] + +Modifies the current Autopilot configuration. + +` + c.Command.Help() + + return strings.TrimSpace(helpText) +} + +func (c *OperatorAutopilotSetCommand) Synopsis() string { + return "Modify the current Autopilot configuration" +} + +func (c *OperatorAutopilotSetCommand) Run(args []string) int { + var deadServerCleanup string + + f := c.Command.NewFlagSet(c) + + f.StringVar(&deadServerCleanup, "dead-server-cleanup", "", + "Controls whether Consul will automatically remove dead servers "+ + "when new ones are successfully added. Must be one of `true|false`.") + + if err := c.Command.Parse(args); err != nil { + if err == flag.ErrHelp { + return 0 + } + c.Ui.Error(fmt.Sprintf("Failed to parse args: %v", err)) + return 1 + } + + // Set up a client. + client, err := c.Command.HTTPClient() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) + return 1 + } + + // Fetch the current configuration. + operator := client.Operator() + conf, err := operator.AutopilotGetConfiguration(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying Autopilot configuration: %s", err)) + return 1 + } + + if deadServerCleanup != "" { + switch deadServerCleanup { + case "true": + conf.DeadServerCleanup = true + case "false": + conf.DeadServerCleanup = false + default: + c.Ui.Error(fmt.Sprintf("Invalid value for dead-server-cleanup: %q", deadServerCleanup)) + } + } + + result, err := operator.AutopilotCASConfiguration(conf, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error setting Autopilot configuration: %s", err)) + return 1 + } + if result { + c.Ui.Output("Configuration updated") + } else { + c.Ui.Output("Configuration could not be atomically updated") + } + + return 0 +} diff --git a/command/operator_autopilot_set_test.go b/command/operator_autopilot_set_test.go new file mode 100644 index 000000000..e9da49e41 --- /dev/null +++ b/command/operator_autopilot_set_test.go @@ -0,0 +1,50 @@ +package command + +import ( + "strings" + "testing" + + "github.com/hashicorp/consul/command/base" + "github.com/hashicorp/consul/consul/structs" + "github.com/mitchellh/cli" +) + +func TestOperator_Autopilot_Set_Implements(t *testing.T) { + var _ cli.Command = &OperatorAutopilotSetCommand{} +} + +func TestOperator_Autopilot_Set(t *testing.T) { + a1 := testAgent(t) + defer a1.Shutdown() + waitForLeader(t, a1.httpAddr) + + ui := new(cli.MockUi) + c := OperatorAutopilotSetCommand{ + Command: base.Command{ + Ui: ui, + Flags: base.FlagSetHTTP, + }, + } + args := []string{"-http-addr=" + a1.httpAddr, "-dead-server-cleanup=false"} + + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + output := strings.TrimSpace(ui.OutputWriter.String()) + if !strings.Contains(output, "Configuration updated") { + t.Fatalf("bad: %s", output) + } + + req := structs.DCSpecificRequest{ + Datacenter: "dc1", + } + var reply structs.AutopilotConfig + if err := a1.agent.RPC("Operator.AutopilotGetConfiguration", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + if reply.DeadServerCleanup { + t.Fatalf("bad: %#v", reply) + } +} diff --git a/command/operator_autopilot_test.go b/command/operator_autopilot_test.go new file mode 100644 index 000000000..6f4adb4a2 --- /dev/null +++ b/command/operator_autopilot_test.go @@ -0,0 +1,11 @@ +package command + +import ( + "testing" + + "github.com/mitchellh/cli" +) + +func TestOperator_Autopilot_Implements(t *testing.T) { + var _ cli.Command = &OperatorAutopilotCommand{} +} diff --git a/commands.go b/commands.go index a9ed849b7..8e061ca3d 100644 --- a/commands.go +++ b/commands.go @@ -213,6 +213,33 @@ func init() { }, nil }, + "operator autopilot": func() (cli.Command, error) { + return &command.OperatorAutopilotCommand{ + Command: base.Command{ + Flags: base.FlagSetNone, + Ui: ui, + }, + }, nil + }, + + "operator autopilot get-config": func() (cli.Command, error) { + return &command.OperatorAutopilotGetCommand{ + Command: base.Command{ + Flags: base.FlagSetHTTP, + Ui: ui, + }, + }, nil + }, + + "operator autopilot set-config": func() (cli.Command, error) { + return &command.OperatorAutopilotSetCommand{ + Command: base.Command{ + Flags: base.FlagSetHTTP, + Ui: ui, + }, + }, nil + }, + "operator raft": func() (cli.Command, error) { return &command.OperatorRaftCommand{ Command: base.Command{