Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a CLI command for retrieving the autopilot configuration. #9142

Merged
merged 1 commit into from
Nov 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions agent/operator_endpoint_oss.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ func autopilotToAPIServerEnterprise(_ *autopilot.ServerState, _ *api.AutopilotSe
// noop in oss
}

func autopilotToAPIStateEnterprise(_ *autopilot.State, _ *api.AutopilotState) {
// noop in oss
func autopilotToAPIStateEnterprise(state *autopilot.State, apiState *api.AutopilotState) {
// without the enterprise features there is no different between these two and we don't want to
// alarm anyone by leaving this as the zero value.
apiState.OptimisticFailureTolerance = state.FailureTolerance
}
7 changes: 4 additions & 3 deletions agent/operator_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -656,9 +656,10 @@ func TestAutopilotStateToAPIConversion(t *testing.T) {
}

expected := api.AutopilotState{
Healthy: true,
FailureTolerance: 1,
Leader: string(leaderID),
Healthy: true,
FailureTolerance: 1,
OptimisticFailureTolerance: 1,
Leader: string(leaderID),
Voters: []string{
string(leaderID),
string(follower1ID),
Expand Down
4 changes: 2 additions & 2 deletions api/operator_autopilot.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ type OperatorHealthReply struct {
type AutopilotState struct {
Healthy bool
FailureTolerance int
OptimisitcFailureTolerance int
OptimisticFailureTolerance int

Servers map[string]AutopilotServer
Leader string
Expand All @@ -137,7 +137,7 @@ type AutopilotServer struct {
StableSince time.Time
RedundancyZone string `json:",omitempty"`
UpgradeVersion string `json:",omitempty"`
ReadReplica bool `json:",omitempty"`
ReadReplica bool
Status AutopilotServerStatus
Meta map[string]string
NodeType AutopilotServerType
Expand Down
2 changes: 2 additions & 0 deletions command/commands_oss.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import (
operauto "github.com/hashicorp/consul/command/operator/autopilot"
operautoget "github.com/hashicorp/consul/command/operator/autopilot/get"
operautoset "github.com/hashicorp/consul/command/operator/autopilot/set"
operautostate "github.com/hashicorp/consul/command/operator/autopilot/state"
operraft "github.com/hashicorp/consul/command/operator/raft"
operraftlist "github.com/hashicorp/consul/command/operator/raft/listpeers"
operraftremove "github.com/hashicorp/consul/command/operator/raft/removepeer"
Expand Down Expand Up @@ -202,6 +203,7 @@ func init() {
Register("operator autopilot", func(cli.Ui) (cli.Command, error) { return operauto.New(), nil })
Register("operator autopilot get-config", func(ui cli.Ui) (cli.Command, error) { return operautoget.New(ui), nil })
Register("operator autopilot set-config", func(ui cli.Ui) (cli.Command, error) { return operautoset.New(ui), nil })
Register("operator autopilot state", func(ui cli.Ui) (cli.Command, error) { return operautostate.New(ui), nil })
Register("operator raft", func(cli.Ui) (cli.Command, error) { return operraft.New(), nil })
Register("operator raft list-peers", func(ui cli.Ui) (cli.Command, error) { return operraftlist.New(ui), nil })
Register("operator raft remove-peer", func(ui cli.Ui) (cli.Command, error) { return operraftremove.New(ui), nil })
Expand Down
206 changes: 206 additions & 0 deletions command/operator/autopilot/state/formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package state

import (
"bytes"
"encoding/json"
"fmt"
"sort"

"github.com/hashicorp/consul/api"
)

const (
PrettyFormat string = "pretty"
JSONFormat string = "json"
)

// Formatter defines methods provided by an autopilot state output formatter
type Formatter interface {
FormatState(state *api.AutopilotState) (string, error)
}

// GetSupportedFormats returns supported formats
func GetSupportedFormats() []string {
return []string{PrettyFormat, JSONFormat}
}

// NewFormatter returns Formatter implementation
func NewFormatter(format string) (formatter Formatter, err error) {
switch format {
case PrettyFormat:
formatter = newPrettyFormatter()
case JSONFormat:
formatter = newJSONFormatter()
default:
err = fmt.Errorf("Unknown format: %s", format)
}

return formatter, err
}

func newPrettyFormatter() Formatter {
return &prettyFormatter{}
}

type prettyFormatter struct {
}

func outputStringSlice(buffer *bytes.Buffer, indent string, values []string) {
for _, val := range values {
buffer.WriteString(fmt.Sprintf("%s%s\n", indent, val))
}
}

type mapOutput struct {
key string
value string
}

func formatZone(zoneName string, zone *api.AutopilotZone) string {
var buffer bytes.Buffer

buffer.WriteString(fmt.Sprintf(" %s:\n", zoneName))
buffer.WriteString(fmt.Sprintf(" Failure Tolerance: %d\n", zone.FailureTolerance))
buffer.WriteString(" Voters:\n")
outputStringSlice(&buffer, " ", zone.Voters)
buffer.WriteString(" Servers:\n")
outputStringSlice(&buffer, " ", zone.Servers)

return buffer.String()
}

func formatServer(srv *api.AutopilotServer) string {
var buffer bytes.Buffer

buffer.WriteString(fmt.Sprintf(" %s\n", srv.ID))
buffer.WriteString(fmt.Sprintf(" Name: %s\n", srv.Name))
buffer.WriteString(fmt.Sprintf(" Address: %s\n", srv.Address))
buffer.WriteString(fmt.Sprintf(" Version: %s\n", srv.Version))
buffer.WriteString(fmt.Sprintf(" Status: %s\n", srv.Status))
buffer.WriteString(fmt.Sprintf(" Node Type: %s\n", srv.NodeType))
buffer.WriteString(fmt.Sprintf(" Node Status: %s\n", srv.NodeStatus))
buffer.WriteString(fmt.Sprintf(" Healthy: %t\n", srv.Healthy))
buffer.WriteString(fmt.Sprintf(" Last Contact: %s\n", srv.LastContact.String()))
buffer.WriteString(fmt.Sprintf(" Last Term: %d\n", srv.LastTerm))
buffer.WriteString(fmt.Sprintf(" Last Index: %d\n", srv.LastIndex))
if srv.RedundancyZone != "" {
buffer.WriteString(fmt.Sprintf(" Redundancy Zone: %s\n", srv.RedundancyZone))
}
if srv.UpgradeVersion != "" {
buffer.WriteString(fmt.Sprintf(" Upgrade Version: %s\n", srv.UpgradeVersion))
}
if srv.ReadReplica {
buffer.WriteString(fmt.Sprintf(" Read Replica: %t\n", srv.ReadReplica))
}
if len(srv.Meta) > 0 {
buffer.WriteString(fmt.Sprintf(" Meta\n"))
var outputs []mapOutput
for k, v := range srv.Meta {
outputs = append(outputs, mapOutput{key: k, value: fmt.Sprintf(" %q: %q\n", k, v)})
}

sort.Slice(outputs, func(i, j int) bool {
return outputs[i].key < outputs[j].key
})

for _, output := range outputs {
buffer.WriteString(output.value)
}
}

return buffer.String()
}

func (f *prettyFormatter) FormatState(state *api.AutopilotState) (string, error) {

var buffer bytes.Buffer

buffer.WriteString(fmt.Sprintf("Healthy: %t\n", state.Healthy))
buffer.WriteString(fmt.Sprintf("Failure Tolerance: %d\n", state.FailureTolerance))
buffer.WriteString(fmt.Sprintf("Optimistic Failure Tolerance: %d\n", state.OptimisticFailureTolerance))
buffer.WriteString(fmt.Sprintf("Leader: %s\n", state.Leader))
buffer.WriteString("Voters:\n")
outputStringSlice(&buffer, " ", state.Voters)

if len(state.ReadReplicas) > 0 {
buffer.WriteString("Read Replicas:\n")
outputStringSlice(&buffer, " ", state.ReadReplicas)
}

if len(state.RedundancyZones) > 0 {
var outputs []mapOutput
buffer.WriteString("Redundancy Zones:\n")
for zoneName, zone := range state.RedundancyZones {
outputs = append(outputs, mapOutput{key: zoneName, value: formatZone(zoneName, &zone)})
}
sort.Slice(outputs, func(i, j int) bool {
return outputs[i].key < outputs[j].key
})

for _, output := range outputs {
buffer.WriteString(output.value)
}
}

if state.Upgrade != nil {
u := state.Upgrade
buffer.WriteString("Upgrade:\n")
buffer.WriteString(fmt.Sprintf(" Status: %s\n", u.Status))
buffer.WriteString(fmt.Sprintf(" Target Version: %s\n", u.TargetVersion))
if len(u.TargetVersionVoters) > 0 {
buffer.WriteString(" Target Version Voters:\n")
outputStringSlice(&buffer, " ", u.TargetVersionVoters)
}
if len(u.TargetVersionNonVoters) > 0 {
buffer.WriteString(" Target Version Non-Voters:\n")
outputStringSlice(&buffer, " ", u.TargetVersionNonVoters)
}
if len(u.TargetVersionReadReplicas) > 0 {
buffer.WriteString(" Target Version ReadReplicas:\n")
outputStringSlice(&buffer, " ", u.TargetVersionReadReplicas)
}
if len(u.OtherVersionVoters) > 0 {
buffer.WriteString(" Other Version Voters:\n")
outputStringSlice(&buffer, " ", u.OtherVersionVoters)
}
if len(u.OtherVersionNonVoters) > 0 {
buffer.WriteString(" Other Version Non-Voters:\n")
outputStringSlice(&buffer, " ", u.OtherVersionNonVoters)
}
if len(u.OtherVersionReadReplicas) > 0 {
buffer.WriteString(" Other Version ReadReplicas:\n")
outputStringSlice(&buffer, " ", u.OtherVersionReadReplicas)
}
}

buffer.WriteString("Servers:\n")
var outputs []mapOutput
for id, srv := range state.Servers {
outputs = append(outputs, mapOutput{key: id, value: formatServer(&srv)})
}

sort.Slice(outputs, func(i, j int) bool {
return outputs[i].key < outputs[j].key
})

for _, output := range outputs {
buffer.WriteString(output.value)
}

return buffer.String(), nil
}

func newJSONFormatter() Formatter {
return &jsonFormatter{}
}

type jsonFormatter struct {
}

func (f *jsonFormatter) FormatState(state *api.AutopilotState) (string, error) {
b, err := json.MarshalIndent(state, "", " ")
if err != nil {
return "", fmt.Errorf("Failed to marshal token: %v", err)
}
return string(b), nil
}
99 changes: 99 additions & 0 deletions command/operator/autopilot/state/operator_autopilot_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package state

import (
"flag"
"fmt"
"strings"

"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)

func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}

type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string

format string
}

func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.StringVar(
&c.format,
"format",
PrettyFormat,
fmt.Sprintf("Output format {%s}", strings.Join(GetSupportedFormats(), "|")),
)
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}

func (c *cmd) Run(args []string) int {
if err := c.flags.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.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}

// Fetch the current configuration.
opts := &api.QueryOptions{
AllowStale: c.http.Stale(),
}
state, err := client.Operator().AutopilotState(opts)
if err != nil {
c.UI.Error(fmt.Sprintf("Error querying Autopilot state: %s", err))
return 1
}

formatter, err := NewFormatter(c.format)
if err != nil {
c.UI.Error(err.Error())
return 1
}

out, err := formatter.FormatState(state)
if err != nil {
c.UI.Error(err.Error())
return 1
}

if out != "" {
c.UI.Info(out)
}
return 0
}

func (c *cmd) Synopsis() string {
return synopsis
}

func (c *cmd) Help() string {
return c.help
}

const synopsis = "Display the current Autopilot configuration"
const help = `
Usage: consul operator autopilot get-config [options]

Displays the current Autopilot configuration.
`
Loading