diff --git a/command/commands_oss.go b/command/commands_oss.go index e7237a1884..551e259528 100644 --- a/command/commands_oss.go +++ b/command/commands_oss.go @@ -220,6 +220,6 @@ func init() { Register("tls cert", func(ui cli.Ui) (cli.Command, error) { return tlscert.New(), nil }) Register("tls cert create", func(ui cli.Ui) (cli.Command, error) { return tlscertcreate.New(ui), nil }) Register("validate", func(ui cli.Ui) (cli.Command, error) { return validate.New(ui), nil }) - Register("version", func(ui cli.Ui) (cli.Command, error) { return version.New(ui, verHuman), nil }) + Register("version", func(ui cli.Ui) (cli.Command, error) { return version.New(ui), nil }) Register("watch", func(ui cli.Ui) (cli.Command, error) { return watch.New(ui, MakeShutdownCh()), nil }) } diff --git a/command/version/formatter.go b/command/version/formatter.go new file mode 100644 index 0000000000..b6d995ae25 --- /dev/null +++ b/command/version/formatter.go @@ -0,0 +1,69 @@ +package version + +import ( + "bytes" + "encoding/json" + "fmt" +) + +const ( + PrettyFormat string = "pretty" + JSONFormat string = "json" +) + +type Formatter interface { + Format(info *VersionInfo) (string, error) +} + +func GetSupportedFormats() []string { + return []string{PrettyFormat, JSONFormat} +} + +func NewFormatter(format string) (Formatter, error) { + switch format { + case PrettyFormat: + return newPrettyFormatter(), nil + case JSONFormat: + return newJSONFormatter(), nil + default: + return nil, fmt.Errorf("Unknown format: %s", format) + } +} + +type prettyFormatter struct{} + +func newPrettyFormatter() Formatter { + return &prettyFormatter{} +} + +func (_ *prettyFormatter) Format(info *VersionInfo) (string, error) { + var buffer bytes.Buffer + buffer.WriteString(fmt.Sprintf("Consul %s\n", info.HumanVersion)) + if info.Revision != "" { + buffer.WriteString(fmt.Sprintf("Revision %s\n", info.Revision)) + } + + var supplement string + if info.RPC.Default < info.RPC.Max { + supplement = fmt.Sprintf(" (agent will automatically use protocol >%d when speaking to compatible agents)", + info.RPC.Default) + } + buffer.WriteString(fmt.Sprintf("Protocol %d spoken by default, understands %d to %d%s\n", + info.RPC.Default, info.RPC.Min, info.RPC.Max, supplement)) + + return buffer.String(), nil +} + +type jsonFormatter struct{} + +func newJSONFormatter() Formatter { + return &jsonFormatter{} +} + +func (_ *jsonFormatter) Format(info *VersionInfo) (string, error) { + b, err := json.MarshalIndent(info, "", " ") + if err != nil { + return "", fmt.Errorf("Failed to marshal version info: %v", err) + } + return string(b), nil +} diff --git a/command/version/formatter_test.go b/command/version/formatter_test.go new file mode 100644 index 0000000000..b9c6090ac7 --- /dev/null +++ b/command/version/formatter_test.go @@ -0,0 +1,63 @@ +package version + +import ( + "flag" + "fmt" + "io/ioutil" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +// update allows golden files to be updated based on the current output. +var update = flag.Bool("update", false, "update golden files") + +// golden reads and optionally writes the expected data to the golden file, +// returning the contents as a string. +func golden(t *testing.T, name, got string) string { + t.Helper() + + golden := filepath.Join("testdata", name+".golden") + if *update && got != "" { + err := ioutil.WriteFile(golden, []byte(got), 0644) + require.NoError(t, err) + } + + expected, err := ioutil.ReadFile(golden) + require.NoError(t, err) + + return string(expected) +} + +func TestFormat(t *testing.T) { + info := VersionInfo{ + HumanVersion: "1.99.3-beta1", + Version: "1.99.3", + Prerelease: "beta1", + Revision: "5e5dbedd47a5f875b60e241c5555a9caab595246", + RPC: RPCVersionInfo{ + Default: 2, + Min: 1, + Max: 3, + }, + } + + formatters := map[string]Formatter{ + "pretty": newPrettyFormatter(), + // the JSON formatter ignores the showMeta + "json": newJSONFormatter(), + } + + for fmtName, formatter := range formatters { + t.Run(fmtName, func(t *testing.T) { + actual, err := formatter.Format(&info) + require.NoError(t, err) + + gName := fmt.Sprintf("%s", fmtName) + + expected := golden(t, gName, actual) + require.Equal(t, expected, actual) + }) + } +} diff --git a/command/version/testdata/json.golden b/command/version/testdata/json.golden new file mode 100644 index 0000000000..20c4a80c07 --- /dev/null +++ b/command/version/testdata/json.golden @@ -0,0 +1,10 @@ +{ + "Version": "1.99.3", + "Revision": "5e5dbedd47a5f875b60e241c5555a9caab595246", + "Prerelease": "beta1", + "RPC": { + "Default": 2, + "Min": 1, + "Max": 3 + } +} \ No newline at end of file diff --git a/command/version/testdata/pretty.golden b/command/version/testdata/pretty.golden new file mode 100644 index 0000000000..09d5118326 --- /dev/null +++ b/command/version/testdata/pretty.golden @@ -0,0 +1,3 @@ +Consul 1.99.3-beta1 +Revision 5e5dbedd47a5f875b60e241c5555a9caab595246 +Protocol 2 spoken by default, understands 1 to 3 (agent will automatically use protocol >2 when speaking to compatible agents) diff --git a/command/version/version.go b/command/version/version.go index 7736cb36aa..423a6f97da 100644 --- a/command/version/version.go +++ b/command/version/version.go @@ -1,34 +1,80 @@ package version import ( + "flag" "fmt" + "strings" "github.com/hashicorp/consul/agent/consul" + "github.com/hashicorp/consul/command/flags" + "github.com/hashicorp/consul/version" "github.com/mitchellh/cli" ) -func New(ui cli.Ui, version string) *cmd { - return &cmd{UI: ui, version: version} +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c } type cmd struct { - UI cli.Ui - version string + UI cli.Ui + flags *flag.FlagSet + format string + help string } -func (c *cmd) Run(_ []string) int { - c.UI.Output(fmt.Sprintf("Consul %s", c.version)) +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.help = flags.Usage(help, c.flags) - const rpcProtocol = consul.DefaultRPCProtocol +} - var supplement string - if rpcProtocol < consul.ProtocolVersionMax { - supplement = fmt.Sprintf(" (agent will automatically use protocol >%d when speaking to compatible agents)", - rpcProtocol) +type RPCVersionInfo struct { + Default int + Min int + Max int +} + +type VersionInfo struct { + HumanVersion string `json:"-"` + Version string + Revision string + Prerelease string + RPC RPCVersionInfo +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + return 1 } - c.UI.Output(fmt.Sprintf("Protocol %d spoken by default, understands %d to %d%s", - rpcProtocol, consul.ProtocolVersionMin, consul.ProtocolVersionMax, supplement)) + formatter, err := NewFormatter(c.format) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + out, err := formatter.Format(&VersionInfo{ + HumanVersion: version.GetHumanVersion(), + Version: version.Version, + Revision: version.GitCommit, + Prerelease: version.VersionPrerelease, + RPC: RPCVersionInfo{ + Default: consul.DefaultRPCProtocol, + Min: int(consul.ProtocolVersionMin), + Max: consul.ProtocolVersionMax, + }, + }) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + c.UI.Output(out) return 0 } @@ -37,5 +83,10 @@ func (c *cmd) Synopsis() string { } func (c *cmd) Help() string { - return "" + return flags.Usage(c.help, nil) } + +const synopsis = "Output Consul version information" +const help = ` +Usage: consul version [options] +` diff --git a/command/version/version_test.go b/command/version/version_test.go index 4cfdf4fde7..d795f9715c 100644 --- a/command/version/version_test.go +++ b/command/version/version_test.go @@ -9,7 +9,7 @@ import ( func TestVersionCommand_noTabs(t *testing.T) { t.Parallel() - if strings.ContainsRune(New(cli.NewMockUi(), "").Help(), '\t') { + if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { t.Fatal("help has tabs") } } diff --git a/main.go b/main.go index 208062a2f3..6ffcd431f0 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,6 @@ import ( "github.com/hashicorp/consul/command/version" "github.com/hashicorp/consul/lib" _ "github.com/hashicorp/consul/service_os" - consulversion "github.com/hashicorp/consul/version" "github.com/mitchellh/cli" ) @@ -43,7 +42,7 @@ func realMain() int { } if cli.IsVersion() { - cmd := version.New(ui, consulversion.GetHumanVersion()) + cmd := version.New(ui) return cmd.Run(nil) } diff --git a/website/pages/docs/commands/version.mdx b/website/pages/docs/commands/version.mdx index ef51a4015f..eae2aa99e0 100644 --- a/website/pages/docs/commands/version.mdx +++ b/website/pages/docs/commands/version.mdx @@ -13,8 +13,33 @@ Command: `consul version` The `version` command prints the version of Consul and the protocol versions it understands for speaking to other agents. +## Usage + +Usage: `consul version [options]` + +### Command Options + +- `-format={pretty|json}` - Command output format. The default value is `pretty`. + +## Plain Output ```shell-session $ consul version -Consul v0.7.4 +Consul v1.7.0 +Revision d1fc59061 Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents) ``` + +## JSON Output +```shell-session +$ consul version -format=json +{ + "Version": "1.8.0", + "Revision": "d1fc59061", + "Prerelease": "dev", + "RPC": { + "Default": 2, + "Min": 2, + "Max": 3 + } +} +```