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 Revision to version CLI output and add JSON support #8268

Merged
merged 1 commit into from
Jul 10, 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
2 changes: 1 addition & 1 deletion command/commands_oss.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
69 changes: 69 additions & 0 deletions command/version/formatter.go
Original file line number Diff line number Diff line change
@@ -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
}
63 changes: 63 additions & 0 deletions command/version/formatter_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
10 changes: 10 additions & 0 deletions command/version/testdata/json.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"Version": "1.99.3",
"Revision": "5e5dbedd47a5f875b60e241c5555a9caab595246",
"Prerelease": "beta1",
"RPC": {
"Default": 2,
"Min": 1,
"Max": 3
}
}
3 changes: 3 additions & 0 deletions command/version/testdata/pretty.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Consul 1.99.3-beta1
Revision 5e5dbedd47a5f875b60e241c5555a9caab595246
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if this is an issue but there may be a tool/check in people's pipelines that parses the protocol from the 2nd line or tail -n 1 or something where this may break. In which case we would suggest them use the json formatter from now on.

Copy link
Member Author

@mkeeler mkeeler Jul 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably hold for v1.9 then. So merging to master and not backporting into release/1.8 is the way to go.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, thinking about it I am not sure whether we should consider this a breaking change. We provide no guarantees about the output.

Protocol 2 spoken by default, understands 1 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)
79 changes: 65 additions & 14 deletions command/version/version.go
Original file line number Diff line number Diff line change
@@ -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
}

type RPCVersionInfo struct {
Default int
Min int
Max int
}

type VersionInfo struct {
HumanVersion string `json:"-"`
Version string
Revision string
Prerelease string
RPC RPCVersionInfo
}

var supplement string
if rpcProtocol < consul.ProtocolVersionMax {
supplement = fmt.Sprintf(" (agent will automatically use protocol >%d when speaking to compatible agents)",
rpcProtocol)
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
}

Expand All @@ -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]
`
2 changes: 1 addition & 1 deletion command/version/version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
3 changes: 1 addition & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -43,7 +42,7 @@ func realMain() int {
}

if cli.IsVersion() {
cmd := version.New(ui, consulversion.GetHumanVersion())
cmd := version.New(ui)
return cmd.Run(nil)
}

Expand Down
27 changes: 26 additions & 1 deletion website/pages/docs/commands/version.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
```