From af0508e511032a4c173154676cb68a87b9037b2f Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 5 Apr 2023 11:11:09 +0100 Subject: [PATCH 01/10] feat(cli): add --json output to version cmd --- cli/root.go | 30 ----------------- cli/version.go | 79 +++++++++++++++++++++++++++++++++++++++++++++ cli/version_test.go | 67 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 30 deletions(-) create mode 100644 cli/version.go create mode 100644 cli/version_test.go diff --git a/cli/root.go b/cli/root.go index 59096f4900bc7..8fe9c1979fe39 100644 --- a/cli/root.go +++ b/cli/root.go @@ -370,36 +370,6 @@ func LoggerFromContext(ctx context.Context) (slog.Logger, bool) { 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 { return flag.Lookup("test.v") != nil } diff --git a/cli/version.go b/cli/version.go new file mode 100644 index 0000000000000..a308d0f2c288b --- /dev/null +++ b/cli/version.go @@ -0,0 +1,79 @@ +package cli + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/coder/coder/buildinfo" + "github.com/coder/coder/cli/clibase" + "github.com/coder/coder/cli/cliui" +) + +// version prints the coder version +func (*RootCmd) version() *clibase.Cmd { + handleHuman := 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 + } + + handleJSON := func(inv *clibase.Invocation) error { + buildTime, _ := buildinfo.Time() + versionInfo := struct { + Version string `json:"version"` + BuildTime string `json:"build_time"` + ExternalURL string `json:"external_url"` + Slim bool `json:"slim"` + AGPL bool `json:"agpl"` + }{ + Version: buildinfo.Version(), + BuildTime: buildTime.Format(time.UnixDate), + ExternalURL: buildinfo.ExternalURL(), + Slim: buildinfo.IsSlim(), + AGPL: buildinfo.IsAGPL(), + } + + enc := json.NewEncoder(inv.Stdout) + enc.SetIndent("", " ") + return enc.Encode(versionInfo) + } + + var outputJSON bool + + return &clibase.Cmd{ + Use: "version", + Short: "Show coder version", + Options: clibase.OptionSet{ + { + Flag: "json", + Description: "Emit version information in machine-readable JSON format.", + Value: clibase.BoolOf(&outputJSON), + }, + }, + Handler: func(inv *clibase.Invocation) error { + if outputJSON { + return handleJSON(inv) + } + return handleHuman(inv) + }, + } +} diff --git a/cli/version_test.go b/cli/version_test.go new file mode 100644 index 0000000000000..d91410b4f648c --- /dev/null +++ b/cli/version_test.go @@ -0,0 +1,67 @@ +package cli_test + +import ( + "bytes" + "context" + "regexp" + "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() + ansiExpr := regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))") + clean := func(s string) string { + s = ansiExpr.ReplaceAllString(s, "") + s = strings.Replace(s, "\r\n", "\n", -1) + return s + } + expectedHuman := `Coder v0.0.0-devel +https://github.com/coder/coder + +Full build of Coder, supports the server subcommand. +` + expectedJSON := `{ + "version": "v0.0.0-devel", + "build_time": "Mon Jan 1 00:00:00 UTC 0001", + "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: expectedHuman, + }, + { + Name: "JSON output", + Args: []string{"version", "--json"}, + Expected: expectedJSON, + }, + } { + 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 := clean(buf.String()) + require.Equal(t, tt.Expected, actual) + }) + } +} From c8c610e1260c6c23488d2506926a9af815e09d4b Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 5 Apr 2023 11:16:12 +0100 Subject: [PATCH 02/10] fixup! feat(cli): add --json output to version cmd --- cli/version.go | 2 +- cli/version_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/version.go b/cli/version.go index a308d0f2c288b..818e7f585ffa7 100644 --- a/cli/version.go +++ b/cli/version.go @@ -46,7 +46,7 @@ func (*RootCmd) version() *clibase.Cmd { AGPL bool `json:"agpl"` }{ Version: buildinfo.Version(), - BuildTime: buildTime.Format(time.UnixDate), + BuildTime: buildTime.Format(time.RFC3339), ExternalURL: buildinfo.ExternalURL(), Slim: buildinfo.IsSlim(), AGPL: buildinfo.IsAGPL(), diff --git a/cli/version_test.go b/cli/version_test.go index d91410b4f648c..6b990b33a326e 100644 --- a/cli/version_test.go +++ b/cli/version_test.go @@ -28,7 +28,7 @@ Full build of Coder, supports the server subcommand. ` expectedJSON := `{ "version": "v0.0.0-devel", - "build_time": "Mon Jan 1 00:00:00 UTC 0001", + "build_time": "0001-01-01T00:00:00Z", "external_url": "https://github.com/coder/coder", "slim": false, "agpl": false From d3d5a69c8f2bd8ff5ab72a9ecf12a8455f5965c5 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 5 Apr 2023 11:24:33 +0100 Subject: [PATCH 03/10] fixup! feat(cli): add --json output to version cmd --- docs/cli/version.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/cli/version.md b/docs/cli/version.md index b0b2754948c92..344dbd2aa4669 100644 --- a/docs/cli/version.md +++ b/docs/cli/version.md @@ -7,5 +7,15 @@ Show coder version ## Usage ```console -coder version +coder version [flags] ``` + +## Options + +### --json + +| | | +| ---- | ----------------- | +| Type | bool | + +Emit version information in machine-readable JSON format. From b9ada46e894f0b4e8747c1797949386e51d88f06 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 5 Apr 2023 11:29:45 +0100 Subject: [PATCH 04/10] fixup! feat(cli): add --json output to version cmd --- cli/version_test.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/cli/version_test.go b/cli/version_test.go index 6b990b33a326e..e610f1bfa4f48 100644 --- a/cli/version_test.go +++ b/cli/version_test.go @@ -3,7 +3,6 @@ package cli_test import ( "bytes" "context" - "regexp" "strings" "testing" @@ -15,16 +14,10 @@ import ( func TestVersion(t *testing.T) { t.Parallel() - ansiExpr := regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))") - clean := func(s string) string { - s = ansiExpr.ReplaceAllString(s, "") - s = strings.Replace(s, "\r\n", "\n", -1) - return s - } expectedHuman := `Coder v0.0.0-devel https://github.com/coder/coder -Full build of Coder, supports the server subcommand. +Full build of Coder, supports the server subcommand. ` expectedJSON := `{ "version": "v0.0.0-devel", @@ -60,7 +53,8 @@ Full build of Coder, supports the server subcommand. inv.Stdout = buf err := inv.WithContext(ctx).Run() require.NoError(t, err) - actual := clean(buf.String()) + actual := buf.String() + actual = strings.Replace(actual, "\r\n", "\n", -1) require.Equal(t, tt.Expected, actual) }) } From caf2b46452c28314f71a404cada875a00604e62b Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 5 Apr 2023 11:41:08 +0100 Subject: [PATCH 05/10] fixup! feat(cli): add --json output to version cmd --- cli/testdata/coder_version_--help.golden | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cli/testdata/coder_version_--help.golden b/cli/testdata/coder_version_--help.golden index 7e7d26274f539..49a13c37c9b81 100644 --- a/cli/testdata/coder_version_--help.golden +++ b/cli/testdata/coder_version_--help.golden @@ -1,6 +1,10 @@ -Usage: coder version +Usage: coder version [flags] Show coder version +Options + --json bool + Emit version information in machine-readable JSON format. + --- Run `coder --help` for a list of global options. From f218bcf67d79778bc6bfabe12d3804e594104e31 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 5 Apr 2023 12:59:55 +0100 Subject: [PATCH 06/10] feat(cliui): add TextFormat --- cli/cliui/output.go | 21 +++++++++++++++++++++ cli/cliui/output_test.go | 3 +++ 2 files changed, 24 insertions(+) diff --git a/cli/cliui/output.go b/cli/cliui/output.go index cf3a981fd5a86..c9ed34677971c 100644 --- a/cli/cliui/output.go +++ b/cli/cliui/output.go @@ -3,6 +3,7 @@ package cliui import ( "context" "encoding/json" + "fmt" "reflect" "strings" @@ -171,3 +172,23 @@ func (jsonFormat) Format(_ context.Context, data any) (string, error) { 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 +} diff --git a/cli/cliui/output_test.go b/cli/cliui/output_test.go index 6dbe2fa144b62..22ef241fba7ea 100644 --- a/cli/cliui/output_test.go +++ b/cli/cliui/output_test.go @@ -50,6 +50,9 @@ func Test_OutputFormatter(t *testing.T) { require.Panics(t, func() { cliui.NewOutputFormatter(cliui.JSONFormat()) }) + require.NotPanics(t, func() { + cliui.NewOutputFormatter(cliui.JSONFormat(), cliui.TextFormat()) + }) }) t.Run("NoMissingFormatID", func(t *testing.T) { From 1f403a203e4df1602796676678bccae296c4daf4 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 5 Apr 2023 13:00:25 +0100 Subject: [PATCH 07/10] use --output specifier instead --- cli/version.go | 113 +++++++++++++++++++++++--------------------- cli/version_test.go | 11 +++-- 2 files changed, 67 insertions(+), 57 deletions(-) diff --git a/cli/version.go b/cli/version.go index 818e7f585ffa7..9be348257d861 100644 --- a/cli/version.go +++ b/cli/version.go @@ -1,7 +1,6 @@ package cli import ( - "encoding/json" "fmt" "strings" "time" @@ -11,69 +10,75 @@ import ( "github.com/coder/coder/cli/cliui" ) -// version prints the coder version -func (*RootCmd) version() *clibase.Cmd { - handleHuman := 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") +// 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"` +} - 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"))) - } +// 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) - _, _ = fmt.Fprint(inv.Stdout, str.String()) - return nil + if !vi.BuildTime.IsZero() { + _, _ = str.WriteString(" " + vi.BuildTime.Format(time.UnixDate)) } + _, _ = str.WriteString("\r\n" + vi.ExternalURL + "\r\n\r\n") - handleJSON := func(inv *clibase.Invocation) error { - buildTime, _ := buildinfo.Time() - versionInfo := struct { - Version string `json:"version"` - BuildTime string `json:"build_time"` - ExternalURL string `json:"external_url"` - Slim bool `json:"slim"` - AGPL bool `json:"agpl"` - }{ - Version: buildinfo.Version(), - BuildTime: buildTime.Format(time.RFC3339), - ExternalURL: buildinfo.ExternalURL(), - Slim: buildinfo.IsSlim(), - AGPL: buildinfo.IsAGPL(), - } + 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() +} - enc := json.NewEncoder(inv.Stdout) - enc.SetIndent("", " ") - return enc.Encode(versionInfo) +func defaultVersionInfo() *versionInfo { + buildTime, _ := buildinfo.Time() + return &versionInfo{ + Version: buildinfo.Version(), + BuildTime: buildTime, + ExternalURL: buildinfo.ExternalURL(), + Slim: buildinfo.IsSlim(), + AGPL: buildinfo.IsAGPL(), } +} - var outputJSON bool +// version prints the coder version +func (*RootCmd) version(versionInfo func() *versionInfo) *clibase.Cmd { + var ( + formatter = cliui.NewOutputFormatter( + cliui.TextFormat(), + cliui.JSONFormat(), + ) + vi = versionInfo() + ) - return &clibase.Cmd{ - Use: "version", - Short: "Show coder version", - Options: clibase.OptionSet{ - { - Flag: "json", - Description: "Emit version information in machine-readable JSON format.", - Value: clibase.BoolOf(&outputJSON), - }, - }, + cmd := &clibase.Cmd{ + Use: "version", + Short: "Show coder version", + Options: clibase.OptionSet{}, Handler: func(inv *clibase.Invocation) error { - if outputJSON { - return handleJSON(inv) + out, err := formatter.Format(inv.Context(), vi) + if err != nil { + return err } - return handleHuman(inv) + + _, err = fmt.Fprintln(inv.Stdout, out) + return err }, } + + formatter.AttachOptions(&cmd.Options) + + return cmd } diff --git a/cli/version_test.go b/cli/version_test.go index e610f1bfa4f48..e9114f0fa5004 100644 --- a/cli/version_test.go +++ b/cli/version_test.go @@ -14,7 +14,7 @@ import ( func TestVersion(t *testing.T) { t.Parallel() - expectedHuman := `Coder v0.0.0-devel + expectedText := `Coder v0.0.0-devel https://github.com/coder/coder Full build of Coder, supports the server subcommand. @@ -35,13 +35,18 @@ Full build of Coder, supports the server subcommand. { Name: "Defaults to human-readable output", Args: []string{"version"}, - Expected: expectedHuman, + Expected: expectedText, }, { Name: "JSON output", - Args: []string{"version", "--json"}, + 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) { From a7ec68d58052a849d3ba8fd87cd2b6f7ed07a8d8 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 5 Apr 2023 13:00:39 +0100 Subject: [PATCH 08/10] fixup! use --output specifier instead --- cli/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/root.go b/cli/root.go index 8fe9c1979fe39..070b8fa0db441 100644 --- a/cli/root.go +++ b/cli/root.go @@ -82,7 +82,7 @@ func (r *RootCmd) Core() []*clibase.Cmd { r.templates(), r.users(), r.tokens(), - r.version(), + r.version(defaultVersionInfo), // Workspace Commands r.configSSH(), From 747e63f454b229ee8a62727b970d455404f6ef19 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 5 Apr 2023 13:01:25 +0100 Subject: [PATCH 09/10] gen --- cli/testdata/coder_version_--help.golden | 4 ++-- docs/cli/version.md | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/cli/testdata/coder_version_--help.golden b/cli/testdata/coder_version_--help.golden index 49a13c37c9b81..4fa4b6b2c5561 100644 --- a/cli/testdata/coder_version_--help.golden +++ b/cli/testdata/coder_version_--help.golden @@ -3,8 +3,8 @@ Usage: coder version [flags] Show coder version Options - --json bool - Emit version information in machine-readable JSON format. + -o, --output string (default: text) + Output format. Available formats: text, json. --- Run `coder --help` for a list of global options. diff --git a/docs/cli/version.md b/docs/cli/version.md index 344dbd2aa4669..365b5ac1d47cd 100644 --- a/docs/cli/version.md +++ b/docs/cli/version.md @@ -12,10 +12,11 @@ coder version [flags] ## Options -### --json +### -o, --output -| | | -| ---- | ----------------- | -| Type | bool | +| | | +| ------- | ------------------- | +| Type | string | +| Default | text | -Emit version information in machine-readable JSON format. +Output format. Available formats: text, json. From b099e5c58c9f8dbb2fbbded3d559da7923cab3f8 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 5 Apr 2023 13:03:29 +0100 Subject: [PATCH 10/10] strings.ReplaceAll --- cli/version_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/version_test.go b/cli/version_test.go index e9114f0fa5004..6c2067886219f 100644 --- a/cli/version_test.go +++ b/cli/version_test.go @@ -59,7 +59,7 @@ Full build of Coder, supports the server subcommand. err := inv.WithContext(ctx).Run() require.NoError(t, err) actual := buf.String() - actual = strings.Replace(actual, "\r\n", "\n", -1) + actual = strings.ReplaceAll(actual, "\r\n", "\n") require.Equal(t, tt.Expected, actual) }) }