mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
feat(cli): add --output={text,json} to version cmd (#7010)
* feat(cliui): add TextFormat * feat(cli): add --format={text,json} to version cmd
This commit is contained in:
@ -3,6 +3,7 @@ package cliui
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -171,3 +172,23 @@ func (jsonFormat) Format(_ context.Context, data any) (string, error) {
|
|||||||
|
|
||||||
return string(outBytes), nil
|
return string(outBytes), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type textFormat struct{}
|
||||||
|
|
||||||
|
var _ OutputFormat = textFormat{}
|
||||||
|
|
||||||
|
// TextFormat is a formatter that just outputs unstructured text.
|
||||||
|
// It uses fmt.Sprintf under the hood.
|
||||||
|
func TextFormat() OutputFormat {
|
||||||
|
return textFormat{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (textFormat) ID() string {
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (textFormat) AttachOptions(_ *clibase.OptionSet) {}
|
||||||
|
|
||||||
|
func (textFormat) Format(_ context.Context, data any) (string, error) {
|
||||||
|
return fmt.Sprintf("%s", data), nil
|
||||||
|
}
|
||||||
|
@ -50,6 +50,9 @@ func Test_OutputFormatter(t *testing.T) {
|
|||||||
require.Panics(t, func() {
|
require.Panics(t, func() {
|
||||||
cliui.NewOutputFormatter(cliui.JSONFormat())
|
cliui.NewOutputFormatter(cliui.JSONFormat())
|
||||||
})
|
})
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
cliui.NewOutputFormatter(cliui.JSONFormat(), cliui.TextFormat())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("NoMissingFormatID", func(t *testing.T) {
|
t.Run("NoMissingFormatID", func(t *testing.T) {
|
||||||
|
32
cli/root.go
32
cli/root.go
@ -82,7 +82,7 @@ func (r *RootCmd) Core() []*clibase.Cmd {
|
|||||||
r.templates(),
|
r.templates(),
|
||||||
r.users(),
|
r.users(),
|
||||||
r.tokens(),
|
r.tokens(),
|
||||||
r.version(),
|
r.version(defaultVersionInfo),
|
||||||
|
|
||||||
// Workspace Commands
|
// Workspace Commands
|
||||||
r.configSSH(),
|
r.configSSH(),
|
||||||
@ -370,36 +370,6 @@ func LoggerFromContext(ctx context.Context) (slog.Logger, bool) {
|
|||||||
return l, ok
|
return l, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// version prints the coder version
|
|
||||||
func (*RootCmd) version() *clibase.Cmd {
|
|
||||||
return &clibase.Cmd{
|
|
||||||
Use: "version",
|
|
||||||
Short: "Show coder version",
|
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
|
||||||
var str strings.Builder
|
|
||||||
_, _ = str.WriteString("Coder ")
|
|
||||||
if buildinfo.IsAGPL() {
|
|
||||||
_, _ = str.WriteString("(AGPL) ")
|
|
||||||
}
|
|
||||||
_, _ = str.WriteString(buildinfo.Version())
|
|
||||||
buildTime, valid := buildinfo.Time()
|
|
||||||
if valid {
|
|
||||||
_, _ = str.WriteString(" " + buildTime.Format(time.UnixDate))
|
|
||||||
}
|
|
||||||
_, _ = str.WriteString("\r\n" + buildinfo.ExternalURL() + "\r\n\r\n")
|
|
||||||
|
|
||||||
if buildinfo.IsSlim() {
|
|
||||||
_, _ = str.WriteString(fmt.Sprintf("Slim build of Coder, does not support the %s subcommand.\n", cliui.Styles.Code.Render("server")))
|
|
||||||
} else {
|
|
||||||
_, _ = str.WriteString(fmt.Sprintf("Full build of Coder, supports the %s subcommand.\n", cliui.Styles.Code.Render("server")))
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = fmt.Fprint(inv.Stdout, str.String())
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isTest() bool {
|
func isTest() bool {
|
||||||
return flag.Lookup("test.v") != nil
|
return flag.Lookup("test.v") != nil
|
||||||
}
|
}
|
||||||
|
6
cli/testdata/coder_version_--help.golden
vendored
6
cli/testdata/coder_version_--help.golden
vendored
@ -1,6 +1,10 @@
|
|||||||
Usage: coder version
|
Usage: coder version [flags]
|
||||||
|
|
||||||
Show coder version
|
Show coder version
|
||||||
|
|
||||||
|
[1mOptions[0m
|
||||||
|
-o, --output string (default: text)
|
||||||
|
Output format. Available formats: text, json.
|
||||||
|
|
||||||
---
|
---
|
||||||
Run `coder --help` for a list of global options.
|
Run `coder --help` for a list of global options.
|
||||||
|
84
cli/version.go
Normal file
84
cli/version.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coder/coder/buildinfo"
|
||||||
|
"github.com/coder/coder/cli/clibase"
|
||||||
|
"github.com/coder/coder/cli/cliui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// versionInfo wraps the stuff we get from buildinfo so that it's
|
||||||
|
// easier to emit in different formats.
|
||||||
|
type versionInfo struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
BuildTime time.Time `json:"build_time"`
|
||||||
|
ExternalURL string `json:"external_url"`
|
||||||
|
Slim bool `json:"slim"`
|
||||||
|
AGPL bool `json:"agpl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// String() implements Stringer
|
||||||
|
func (vi versionInfo) String() string {
|
||||||
|
var str strings.Builder
|
||||||
|
_, _ = str.WriteString("Coder ")
|
||||||
|
if vi.AGPL {
|
||||||
|
_, _ = str.WriteString("(AGPL) ")
|
||||||
|
}
|
||||||
|
_, _ = str.WriteString(vi.Version)
|
||||||
|
|
||||||
|
if !vi.BuildTime.IsZero() {
|
||||||
|
_, _ = str.WriteString(" " + vi.BuildTime.Format(time.UnixDate))
|
||||||
|
}
|
||||||
|
_, _ = str.WriteString("\r\n" + vi.ExternalURL + "\r\n\r\n")
|
||||||
|
|
||||||
|
if vi.Slim {
|
||||||
|
_, _ = str.WriteString(fmt.Sprintf("Slim build of Coder, does not support the %s subcommand.", cliui.Styles.Code.Render("server")))
|
||||||
|
} else {
|
||||||
|
_, _ = str.WriteString(fmt.Sprintf("Full build of Coder, supports the %s subcommand.", cliui.Styles.Code.Render("server")))
|
||||||
|
}
|
||||||
|
return str.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultVersionInfo() *versionInfo {
|
||||||
|
buildTime, _ := buildinfo.Time()
|
||||||
|
return &versionInfo{
|
||||||
|
Version: buildinfo.Version(),
|
||||||
|
BuildTime: buildTime,
|
||||||
|
ExternalURL: buildinfo.ExternalURL(),
|
||||||
|
Slim: buildinfo.IsSlim(),
|
||||||
|
AGPL: buildinfo.IsAGPL(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// version prints the coder version
|
||||||
|
func (*RootCmd) version(versionInfo func() *versionInfo) *clibase.Cmd {
|
||||||
|
var (
|
||||||
|
formatter = cliui.NewOutputFormatter(
|
||||||
|
cliui.TextFormat(),
|
||||||
|
cliui.JSONFormat(),
|
||||||
|
)
|
||||||
|
vi = versionInfo()
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := &clibase.Cmd{
|
||||||
|
Use: "version",
|
||||||
|
Short: "Show coder version",
|
||||||
|
Options: clibase.OptionSet{},
|
||||||
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
|
out, err := formatter.Format(inv.Context(), vi)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fmt.Fprintln(inv.Stdout, out)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter.AttachOptions(&cmd.Options)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
66
cli/version_test.go
Normal file
66
cli/version_test.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package cli_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/coder/coder/cli/clitest"
|
||||||
|
"github.com/coder/coder/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVersion(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
expectedText := `Coder v0.0.0-devel
|
||||||
|
https://github.com/coder/coder
|
||||||
|
|
||||||
|
Full build of Coder, supports the [;mserver[0m subcommand.
|
||||||
|
`
|
||||||
|
expectedJSON := `{
|
||||||
|
"version": "v0.0.0-devel",
|
||||||
|
"build_time": "0001-01-01T00:00:00Z",
|
||||||
|
"external_url": "https://github.com/coder/coder",
|
||||||
|
"slim": false,
|
||||||
|
"agpl": false
|
||||||
|
}
|
||||||
|
`
|
||||||
|
for _, tt := range []struct {
|
||||||
|
Name string
|
||||||
|
Args []string
|
||||||
|
Expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "Defaults to human-readable output",
|
||||||
|
Args: []string{"version"},
|
||||||
|
Expected: expectedText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "JSON output",
|
||||||
|
Args: []string{"version", "--output=json"},
|
||||||
|
Expected: expectedJSON,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Text output",
|
||||||
|
Args: []string{"version", "--output=text"},
|
||||||
|
Expected: expectedText,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.Name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
inv, _ := clitest.New(t, tt.Args...)
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
inv.Stdout = buf
|
||||||
|
err := inv.WithContext(ctx).Run()
|
||||||
|
require.NoError(t, err)
|
||||||
|
actual := buf.String()
|
||||||
|
actual = strings.ReplaceAll(actual, "\r\n", "\n")
|
||||||
|
require.Equal(t, tt.Expected, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -7,5 +7,16 @@ Show coder version
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```console
|
```console
|
||||||
coder version
|
coder version [flags]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### -o, --output
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| ------- | ------------------- |
|
||||||
|
| Type | <code>string</code> |
|
||||||
|
| Default | <code>text</code> |
|
||||||
|
|
||||||
|
Output format. Available formats: text, json.
|
||||||
|
Reference in New Issue
Block a user