Merge pull request #8268 from hashicorp/feature/improved-version-output

Add Revision to version CLI output and add JSON support
This commit is contained in:
Matt Keeler 2020-07-10 10:01:57 -04:00 committed by GitHub
commit c9ccbab65a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 239 additions and 19 deletions

View File

@ -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 })
}

View File

@ -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
}

View File

@ -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
command/version/testdata/json.golden vendored Normal file
View File

@ -0,0 +1,10 @@
{
"Version": "1.99.3",
"Revision": "5e5dbedd47a5f875b60e241c5555a9caab595246",
"Prerelease": "beta1",
"RPC": {
"Default": 2,
"Min": 1,
"Max": 3
}
}

View File

@ -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)

View File

@ -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]
`

View File

@ -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")
}
}

View File

@ -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)
}

View File

@ -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
}
}
```