Skip to content

Commit e33bde8

Browse files
committed
feat: add JSON output format to many CLI commands
1 parent 457cd51 commit e33bde8

21 files changed

+207
-111
lines changed

cli/cliui/table.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
107107
column := strings.ToLower(strings.ReplaceAll(column, "_", " "))
108108
h, ok := headersMap[column]
109109
if !ok {
110-
return "", xerrors.Errorf(`specified filter column %q not found in table headers, available columns are "%v"`, sort, strings.Join(headersRaw, `", "`))
110+
return "", xerrors.Errorf(`specified filter column %q not found in table headers, available columns are "%v"`, column, strings.Join(headersRaw, `", "`))
111111
}
112112

113113
// Autocorrect

cli/list.go

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cli
22

33
import (
44
"fmt"
5-
"strings"
65
"time"
76

87
"github.com/google/uuid"
@@ -14,14 +13,21 @@ import (
1413
"github.com/coder/coder/codersdk"
1514
)
1615

16+
// workspaceListRow is the type provided to the OutputFormatter. This is a bit
17+
// dodgy but it's the only way to do complex display code for one format vs. the
18+
// other.
1719
type workspaceListRow struct {
18-
Workspace string `table:"workspace"`
19-
Template string `table:"template"`
20-
Status string `table:"status"`
21-
LastBuilt string `table:"last built"`
22-
Outdated bool `table:"outdated"`
23-
StartsAt string `table:"starts at"`
24-
StopsAfter string `table:"stops after"`
20+
// For JSON format:
21+
codersdk.Workspace `table:"-"`
22+
23+
// For table format:
24+
WorkspaceName string `json:"-" table:"workspace,default_sort"`
25+
Template string `json:"-" table:"template"`
26+
Status string `json:"-" table:"status"`
27+
LastBuilt string `json:"-" table:"last built"`
28+
Outdated bool `json:"-" table:"outdated"`
29+
StartsAt string `json:"-" table:"starts at"`
30+
StopsAfter string `json:"-" table:"stops after"`
2531
}
2632

2733
func workspaceListRowFromWorkspace(now time.Time, usersByID map[uuid.UUID]codersdk.User, workspace codersdk.Workspace) workspaceListRow {
@@ -47,24 +53,27 @@ func workspaceListRowFromWorkspace(now time.Time, usersByID map[uuid.UUID]coders
4753

4854
user := usersByID[workspace.OwnerID]
4955
return workspaceListRow{
50-
Workspace: user.Username + "/" + workspace.Name,
51-
Template: workspace.TemplateName,
52-
Status: status,
53-
LastBuilt: durationDisplay(lastBuilt),
54-
Outdated: workspace.Outdated,
55-
StartsAt: autostartDisplay,
56-
StopsAfter: autostopDisplay,
56+
Workspace: workspace,
57+
WorkspaceName: user.Username + "/" + workspace.Name,
58+
Template: workspace.TemplateName,
59+
Status: status,
60+
LastBuilt: durationDisplay(lastBuilt),
61+
Outdated: workspace.Outdated,
62+
StartsAt: autostartDisplay,
63+
StopsAfter: autostopDisplay,
5764
}
5865
}
5966

6067
func list() *cobra.Command {
6168
var (
6269
all bool
63-
columns []string
6470
defaultQuery = "owner:me"
6571
searchQuery string
66-
me bool
6772
displayWorkspaces []workspaceListRow
73+
formatter = cliui.NewOutputFormatter(
74+
cliui.TableFormat([]workspaceListRow{}, nil),
75+
cliui.JSONFormat(),
76+
)
6877
)
6978
cmd := &cobra.Command{
7079
Annotations: workspaceCommand,
@@ -85,14 +94,6 @@ func list() *cobra.Command {
8594
filter.FilterQuery = ""
8695
}
8796

88-
if me {
89-
myUser, err := client.User(cmd.Context(), codersdk.Me)
90-
if err != nil {
91-
return err
92-
}
93-
filter.Owner = myUser.Username
94-
}
95-
9697
res, err := client.Workspaces(cmd.Context(), filter)
9798
if err != nil {
9899
return err
@@ -121,7 +122,7 @@ func list() *cobra.Command {
121122
displayWorkspaces[i] = workspaceListRowFromWorkspace(now, usersByID, workspace)
122123
}
123124

124-
out, err := cliui.DisplayTable(displayWorkspaces, "workspace", columns)
125+
out, err := formatter.Format(cmd.Context(), displayWorkspaces)
125126
if err != nil {
126127
return err
127128
}
@@ -131,14 +132,10 @@ func list() *cobra.Command {
131132
},
132133
}
133134

134-
// TODO: fix this
135-
availColumns := []string{"name"}
136-
columnString := strings.Join(availColumns[:], ", ")
137-
138135
cmd.Flags().BoolVarP(&all, "all", "a", false,
139136
"Specifies whether all workspaces will be listed or not.")
140-
cmd.Flags().StringArrayVarP(&columns, "column", "c", nil,
141-
fmt.Sprintf("Specify a column to filter in the table. Available columns are: %v", columnString))
142137
cmd.Flags().StringVar(&searchQuery, "search", defaultQuery, "Search for a workspace with a query.")
138+
139+
formatter.AttachFlags(cmd)
143140
return cmd
144141
}

cli/list_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package cli_test
22

33
import (
4+
"bytes"
45
"context"
6+
"encoding/json"
57
"testing"
68

79
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
811

912
"github.com/coder/coder/cli/clitest"
1013
"github.com/coder/coder/coderd/coderdtest"
14+
"github.com/coder/coder/codersdk"
1115
"github.com/coder/coder/pty/ptytest"
1216
"github.com/coder/coder/testutil"
1317
)
@@ -42,4 +46,27 @@ func TestList(t *testing.T) {
4246
cancelFunc()
4347
<-done
4448
})
49+
50+
t.Run("JSON", func(t *testing.T) {
51+
t.Parallel()
52+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
53+
user := coderdtest.CreateFirstUser(t, client)
54+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
55+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
56+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
57+
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
58+
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
59+
60+
cmd, root := clitest.New(t, "list", "--output=json")
61+
clitest.SetupConfig(t, client, root)
62+
63+
out := bytes.NewBuffer(nil)
64+
cmd.SetOut(out)
65+
err := cmd.Execute()
66+
require.NoError(t, err)
67+
68+
var templates []codersdk.Workspace
69+
require.NoError(t, json.Unmarshal(out.Bytes(), &templates))
70+
require.Len(t, templates, 1)
71+
})
4572
}

cli/parameterslist.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ import (
1212
)
1313

1414
func parameterList() *cobra.Command {
15-
var (
16-
columns []string
15+
formatter := cliui.NewOutputFormatter(
16+
cliui.TableFormat([]codersdk.Parameter{}, []string{"name", "scope", "destination scheme"}),
17+
cliui.JSONFormat(),
1718
)
19+
1820
cmd := &cobra.Command{
1921
Use: "list",
2022
Aliases: []string{"ls"},
@@ -71,16 +73,16 @@ func parameterList() *cobra.Command {
7173
return xerrors.Errorf("fetch params: %w", err)
7274
}
7375

74-
out, err := cliui.DisplayTable(params, "name", columns)
76+
out, err := formatter.Format(cmd.Context(), params)
7577
if err != nil {
76-
return xerrors.Errorf("render table: %w", err)
78+
return xerrors.Errorf("render output: %w", err)
7779
}
7880

7981
_, err = fmt.Fprintln(cmd.OutOrStdout(), out)
8082
return err
8183
},
8284
}
83-
cmd.Flags().StringArrayVarP(&columns, "column", "c", []string{"name", "scope", "destination scheme"},
84-
"Specify a column to filter in the table.")
85+
86+
formatter.AttachFlags(cmd)
8587
return cmd
8688
}

cli/ssh_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,9 @@ func TestSSH_ForwardGPG(t *testing.T) {
306306
// same process.
307307
t.Skip("Test not supported on windows")
308308
}
309+
if testing.Short() {
310+
t.SkipNow()
311+
}
309312

310313
// This key is for dean@coder.com.
311314
const randPublicKeyFingerprint = "7BDFBA0CC7F5A96537C806C427BC6335EB5117F1"

cli/templatelist.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ import (
55

66
"github.com/fatih/color"
77
"github.com/spf13/cobra"
8+
9+
"github.com/coder/coder/cli/cliui"
810
)
911

1012
func templateList() *cobra.Command {
11-
var (
12-
columns []string
13+
formatter := cliui.NewOutputFormatter(
14+
cliui.TableFormat([]templateTableRow{}, []string{"name", "last updated", "used by"}),
15+
cliui.JSONFormat(),
1316
)
17+
1418
cmd := &cobra.Command{
1519
Use: "list",
1620
Short: "List all the templates available for the organization",
@@ -35,7 +39,8 @@ func templateList() *cobra.Command {
3539
return nil
3640
}
3741

38-
out, err := displayTemplates(columns, templates...)
42+
rows := templatesToRows(templates...)
43+
out, err := formatter.Format(cmd.Context(), rows)
3944
if err != nil {
4045
return err
4146
}
@@ -44,7 +49,7 @@ func templateList() *cobra.Command {
4449
return err
4550
},
4651
}
47-
cmd.Flags().StringArrayVarP(&columns, "column", "c", []string{"name", "last_updated", "used_by"},
48-
"Specify a column to filter in the table.")
52+
53+
formatter.AttachFlags(cmd)
4954
return cmd
5055
}

cli/templatelist_test.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package cli_test
22

33
import (
4+
"bytes"
5+
"encoding/json"
46
"sort"
57
"testing"
68

79
"github.com/stretchr/testify/require"
810

911
"github.com/coder/coder/cli/clitest"
1012
"github.com/coder/coder/coderd/coderdtest"
13+
"github.com/coder/coder/codersdk"
1114
"github.com/coder/coder/pty/ptytest"
1215
)
1316

@@ -37,7 +40,7 @@ func TestTemplateList(t *testing.T) {
3740
errC <- cmd.Execute()
3841
}()
3942

40-
// expect that templates are listed alphebetically
43+
// expect that templates are listed alphabetically
4144
var templatesList = []string{firstTemplate.Name, secondTemplate.Name}
4245
sort.Strings(templatesList)
4346

@@ -47,6 +50,30 @@ func TestTemplateList(t *testing.T) {
4750
pty.ExpectMatch(name)
4851
}
4952
})
53+
t.Run("ListTemplatesJSON", func(t *testing.T) {
54+
t.Parallel()
55+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
56+
user := coderdtest.CreateFirstUser(t, client)
57+
firstVersion := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
58+
_ = coderdtest.AwaitTemplateVersionJob(t, client, firstVersion.ID)
59+
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, firstVersion.ID)
60+
61+
secondVersion := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
62+
_ = coderdtest.AwaitTemplateVersionJob(t, client, secondVersion.ID)
63+
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, secondVersion.ID)
64+
65+
cmd, root := clitest.New(t, "templates", "list", "--output=json")
66+
clitest.SetupConfig(t, client, root)
67+
68+
out := bytes.NewBuffer(nil)
69+
cmd.SetOut(out)
70+
err := cmd.Execute()
71+
require.NoError(t, err)
72+
73+
var templates []codersdk.Template
74+
require.NoError(t, json.Unmarshal(out.Bytes(), &templates))
75+
require.Len(t, templates, 2)
76+
})
5077
t.Run("NoTemplates", func(t *testing.T) {
5178
t.Parallel()
5279
client := coderdtest.New(t, &coderdtest.Options{})

cli/templates.go

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,23 +50,27 @@ func templates() *cobra.Command {
5050
}
5151

5252
type templateTableRow struct {
53-
Name string `table:"name"`
54-
CreatedAt string `table:"created at"`
55-
LastUpdated string `table:"last updated"`
56-
OrganizationID uuid.UUID `table:"organization id"`
57-
Provisioner codersdk.ProvisionerType `table:"provisioner"`
58-
ActiveVersionID uuid.UUID `table:"active version id"`
59-
UsedBy string `table:"used by"`
60-
DefaultTTL time.Duration `table:"default ttl"`
53+
// Used by json format:
54+
Template codersdk.Template
55+
56+
// Used by table format:
57+
Name string `json:"-" table:"name,default_sort"`
58+
CreatedAt string `json:"-" table:"created at"`
59+
LastUpdated string `json:"-" table:"last updated"`
60+
OrganizationID uuid.UUID `json:"-" table:"organization id"`
61+
Provisioner codersdk.ProvisionerType `json:"-" table:"provisioner"`
62+
ActiveVersionID uuid.UUID `json:"-" table:"active version id"`
63+
UsedBy string `json:"-" table:"used by"`
64+
DefaultTTL time.Duration `json:"-" table:"default ttl"`
6165
}
6266

63-
// displayTemplates will return a table displaying all templates passed in.
64-
// filterColumns must be a subset of the template fields and will determine which
65-
// columns to display
66-
func displayTemplates(filterColumns []string, templates ...codersdk.Template) (string, error) {
67+
// templateToRows converts a list of templates to a list of templateTableRow for
68+
// outputting.
69+
func templatesToRows(templates ...codersdk.Template) []templateTableRow {
6770
rows := make([]templateTableRow, len(templates))
6871
for i, template := range templates {
6972
rows[i] = templateTableRow{
73+
Template: template,
7074
Name: template.Name,
7175
CreatedAt: template.CreatedAt.Format("January 2, 2006"),
7276
LastUpdated: template.UpdatedAt.Format("January 2, 2006"),
@@ -78,5 +82,5 @@ func displayTemplates(filterColumns []string, templates ...codersdk.Template) (s
7882
}
7983
}
8084

81-
return cliui.DisplayTable(rows, "name", filterColumns)
85+
return rows
8286
}

0 commit comments

Comments
 (0)