Skip to content

feat: add JSON output format to many CLI commands #6082

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

Merged
merged 8 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat: add JSON output format to many CLI commands
  • Loading branch information
deansheather committed Feb 7, 2023
commit e33bde832f134c9ae38e91b76d3da19d51380b38
2 changes: 1 addition & 1 deletion cli/cliui/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
column := strings.ToLower(strings.ReplaceAll(column, "_", " "))
h, ok := headersMap[column]
if !ok {
return "", xerrors.Errorf(`specified filter column %q not found in table headers, available columns are "%v"`, sort, strings.Join(headersRaw, `", "`))
return "", xerrors.Errorf(`specified filter column %q not found in table headers, available columns are "%v"`, column, strings.Join(headersRaw, `", "`))
}

// Autocorrect
Expand Down
61 changes: 29 additions & 32 deletions cli/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cli

import (
"fmt"
"strings"
"time"

"github.com/google/uuid"
Expand All @@ -14,14 +13,21 @@ import (
"github.com/coder/coder/codersdk"
)

// workspaceListRow is the type provided to the OutputFormatter. This is a bit
// dodgy but it's the only way to do complex display code for one format vs. the
// other.
type workspaceListRow struct {
Workspace string `table:"workspace"`
Template string `table:"template"`
Status string `table:"status"`
LastBuilt string `table:"last built"`
Outdated bool `table:"outdated"`
StartsAt string `table:"starts at"`
StopsAfter string `table:"stops after"`
// For JSON format:
codersdk.Workspace `table:"-"`

// For table format:
WorkspaceName string `json:"-" table:"workspace,default_sort"`
Template string `json:"-" table:"template"`
Status string `json:"-" table:"status"`
LastBuilt string `json:"-" table:"last built"`
Outdated bool `json:"-" table:"outdated"`
StartsAt string `json:"-" table:"starts at"`
StopsAfter string `json:"-" table:"stops after"`
}

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

user := usersByID[workspace.OwnerID]
return workspaceListRow{
Workspace: user.Username + "/" + workspace.Name,
Template: workspace.TemplateName,
Status: status,
LastBuilt: durationDisplay(lastBuilt),
Outdated: workspace.Outdated,
StartsAt: autostartDisplay,
StopsAfter: autostopDisplay,
Workspace: workspace,
WorkspaceName: user.Username + "/" + workspace.Name,
Template: workspace.TemplateName,
Status: status,
LastBuilt: durationDisplay(lastBuilt),
Outdated: workspace.Outdated,
StartsAt: autostartDisplay,
StopsAfter: autostopDisplay,
}
}

func list() *cobra.Command {
var (
all bool
columns []string
defaultQuery = "owner:me"
searchQuery string
me bool
displayWorkspaces []workspaceListRow
formatter = cliui.NewOutputFormatter(
cliui.TableFormat([]workspaceListRow{}, nil),
cliui.JSONFormat(),
)
)
cmd := &cobra.Command{
Annotations: workspaceCommand,
Expand All @@ -85,14 +94,6 @@ func list() *cobra.Command {
filter.FilterQuery = ""
}

if me {
myUser, err := client.User(cmd.Context(), codersdk.Me)
if err != nil {
return err
}
filter.Owner = myUser.Username
}

res, err := client.Workspaces(cmd.Context(), filter)
if err != nil {
return err
Expand Down Expand Up @@ -121,7 +122,7 @@ func list() *cobra.Command {
displayWorkspaces[i] = workspaceListRowFromWorkspace(now, usersByID, workspace)
}

out, err := cliui.DisplayTable(displayWorkspaces, "workspace", columns)
out, err := formatter.Format(cmd.Context(), displayWorkspaces)
if err != nil {
return err
}
Expand All @@ -131,14 +132,10 @@ func list() *cobra.Command {
},
}

// TODO: fix this
availColumns := []string{"name"}
columnString := strings.Join(availColumns[:], ", ")

cmd.Flags().BoolVarP(&all, "all", "a", false,
"Specifies whether all workspaces will be listed or not.")
cmd.Flags().StringArrayVarP(&columns, "column", "c", nil,
fmt.Sprintf("Specify a column to filter in the table. Available columns are: %v", columnString))
cmd.Flags().StringVar(&searchQuery, "search", defaultQuery, "Search for a workspace with a query.")

formatter.AttachFlags(cmd)
return cmd
}
27 changes: 27 additions & 0 deletions cli/list_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package cli_test

import (
"bytes"
"context"
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/pty/ptytest"
"github.com/coder/coder/testutil"
)
Expand Down Expand Up @@ -42,4 +46,27 @@ func TestList(t *testing.T) {
cancelFunc()
<-done
})

t.Run("JSON", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)

cmd, root := clitest.New(t, "list", "--output=json")
clitest.SetupConfig(t, client, root)

out := bytes.NewBuffer(nil)
cmd.SetOut(out)
err := cmd.Execute()
require.NoError(t, err)

var templates []codersdk.Workspace
require.NoError(t, json.Unmarshal(out.Bytes(), &templates))
require.Len(t, templates, 1)
})
}
14 changes: 8 additions & 6 deletions cli/parameterslist.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import (
)

func parameterList() *cobra.Command {
var (
columns []string
formatter := cliui.NewOutputFormatter(
cliui.TableFormat([]codersdk.Parameter{}, []string{"name", "scope", "destination scheme"}),
cliui.JSONFormat(),
)

cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Expand Down Expand Up @@ -71,16 +73,16 @@ func parameterList() *cobra.Command {
return xerrors.Errorf("fetch params: %w", err)
}

out, err := cliui.DisplayTable(params, "name", columns)
out, err := formatter.Format(cmd.Context(), params)
if err != nil {
return xerrors.Errorf("render table: %w", err)
return xerrors.Errorf("render output: %w", err)
}

_, err = fmt.Fprintln(cmd.OutOrStdout(), out)
return err
},
}
cmd.Flags().StringArrayVarP(&columns, "column", "c", []string{"name", "scope", "destination scheme"},
"Specify a column to filter in the table.")

formatter.AttachFlags(cmd)
return cmd
}
3 changes: 3 additions & 0 deletions cli/ssh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@ func TestSSH_ForwardGPG(t *testing.T) {
// same process.
t.Skip("Test not supported on windows")
}
if testing.Short() {
t.SkipNow()
}

// This key is for dean@coder.com.
const randPublicKeyFingerprint = "7BDFBA0CC7F5A96537C806C427BC6335EB5117F1"
Expand Down
15 changes: 10 additions & 5 deletions cli/templatelist.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import (

"github.com/fatih/color"
"github.com/spf13/cobra"

"github.com/coder/coder/cli/cliui"
)

func templateList() *cobra.Command {
var (
columns []string
formatter := cliui.NewOutputFormatter(
cliui.TableFormat([]templateTableRow{}, []string{"name", "last updated", "used by"}),
cliui.JSONFormat(),
)

cmd := &cobra.Command{
Use: "list",
Short: "List all the templates available for the organization",
Expand All @@ -35,7 +39,8 @@ func templateList() *cobra.Command {
return nil
}

out, err := displayTemplates(columns, templates...)
rows := templatesToRows(templates...)
out, err := formatter.Format(cmd.Context(), rows)
if err != nil {
return err
}
Expand All @@ -44,7 +49,7 @@ func templateList() *cobra.Command {
return err
},
}
cmd.Flags().StringArrayVarP(&columns, "column", "c", []string{"name", "last_updated", "used_by"},
"Specify a column to filter in the table.")

formatter.AttachFlags(cmd)
return cmd
}
29 changes: 28 additions & 1 deletion cli/templatelist_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package cli_test

import (
"bytes"
"encoding/json"
"sort"
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/pty/ptytest"
)

Expand Down Expand Up @@ -37,7 +40,7 @@ func TestTemplateList(t *testing.T) {
errC <- cmd.Execute()
}()

// expect that templates are listed alphebetically
// expect that templates are listed alphabetically
var templatesList = []string{firstTemplate.Name, secondTemplate.Name}
sort.Strings(templatesList)

Expand All @@ -47,6 +50,30 @@ func TestTemplateList(t *testing.T) {
pty.ExpectMatch(name)
}
})
t.Run("ListTemplatesJSON", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
firstVersion := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, firstVersion.ID)
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, firstVersion.ID)

secondVersion := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, secondVersion.ID)
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, secondVersion.ID)

cmd, root := clitest.New(t, "templates", "list", "--output=json")
clitest.SetupConfig(t, client, root)

out := bytes.NewBuffer(nil)
cmd.SetOut(out)
err := cmd.Execute()
require.NoError(t, err)

var templates []codersdk.Template
require.NoError(t, json.Unmarshal(out.Bytes(), &templates))
require.Len(t, templates, 2)
})
t.Run("NoTemplates", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{})
Expand Down
30 changes: 17 additions & 13 deletions cli/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,27 @@ func templates() *cobra.Command {
}

type templateTableRow struct {
Name string `table:"name"`
CreatedAt string `table:"created at"`
LastUpdated string `table:"last updated"`
OrganizationID uuid.UUID `table:"organization id"`
Provisioner codersdk.ProvisionerType `table:"provisioner"`
ActiveVersionID uuid.UUID `table:"active version id"`
UsedBy string `table:"used by"`
DefaultTTL time.Duration `table:"default ttl"`
// Used by json format:
Template codersdk.Template

// Used by table format:
Name string `json:"-" table:"name,default_sort"`
CreatedAt string `json:"-" table:"created at"`
LastUpdated string `json:"-" table:"last updated"`
OrganizationID uuid.UUID `json:"-" table:"organization id"`
Provisioner codersdk.ProvisionerType `json:"-" table:"provisioner"`
ActiveVersionID uuid.UUID `json:"-" table:"active version id"`
UsedBy string `json:"-" table:"used by"`
DefaultTTL time.Duration `json:"-" table:"default ttl"`
}

// displayTemplates will return a table displaying all templates passed in.
// filterColumns must be a subset of the template fields and will determine which
// columns to display
func displayTemplates(filterColumns []string, templates ...codersdk.Template) (string, error) {
// templateToRows converts a list of templates to a list of templateTableRow for
// outputting.
func templatesToRows(templates ...codersdk.Template) []templateTableRow {
rows := make([]templateTableRow, len(templates))
for i, template := range templates {
rows[i] = templateTableRow{
Template: template,
Name: template.Name,
CreatedAt: template.CreatedAt.Format("January 2, 2006"),
LastUpdated: template.UpdatedAt.Format("January 2, 2006"),
Expand All @@ -78,5 +82,5 @@ func displayTemplates(filterColumns []string, templates ...codersdk.Template) (s
}
}

return cliui.DisplayTable(rows, "name", filterColumns)
return rows
}
Loading