Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 9b1173e

Browse files
committed
Add table output to envs ls
1 parent b957f37 commit 9b1173e

File tree

8 files changed

+125
-25
lines changed

8 files changed

+125
-25
lines changed

ci/integration/integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func TestCoderCLI(t *testing.T) {
4949
headlessLogin(ctx, t, c)
5050

5151
c.Run(ctx, "coder envs").Assert(t,
52-
tcli.Success(),
52+
tcli.Error(),
5353
)
5454

5555
c.Run(ctx, "coder envs ls").Assert(t,

cmd/coder/envs.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
package main
22

33
import (
4-
"fmt"
4+
"encoding/json"
5+
"os"
56

7+
"cdr.dev/coder-cli/internal/x/xtabwriter"
68
"github.com/urfave/cli"
9+
10+
"go.coder.com/flog"
711
)
812

913
func makeEnvsCommand() cli.Command {
14+
var outputFmt string
1015
return cli.Command{
1116
Name: "envs",
1217
Usage: "Interact with Coder environments",
1318
Description: "Perform operations on the Coder environments owned by the active user.",
19+
Action: exitHelp,
1420
Subcommands: []cli.Command{
1521
{
1622
Name: "ls",
@@ -21,11 +27,27 @@ func makeEnvsCommand() cli.Command {
2127
entClient := requireAuth()
2228
envs := getEnvs(entClient)
2329

24-
for _, env := range envs {
25-
fmt.Println(env.Name)
30+
switch outputFmt {
31+
case "human":
32+
err := xtabwriter.WriteTable(len(envs), func(i int) interface{} {
33+
return envs[i]
34+
})
35+
requireSuccess(err, "failed to write table: %v", err)
36+
case "json":
37+
err := json.NewEncoder(os.Stdout).Encode(envs)
38+
requireSuccess(err, "failed to write json: %v", err)
39+
default:
40+
flog.Fatal("unknown --output value %q", outputFmt)
2641
}
2742
},
28-
Flags: nil,
43+
Flags: []cli.Flag{
44+
cli.StringFlag{
45+
Name: "output",
46+
Usage: "json | human",
47+
Value: "human",
48+
Destination: &outputFmt,
49+
},
50+
},
2951
},
3052
},
3153
}

cmd/coder/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ func main() {
4040
flog.Fatal("command %q not found", s)
4141
}
4242
app.Email = "support@coder.com"
43+
app.Action = exitHelp
4344

4445
app.Commands = []cli.Command{
4546
makeLoginCmd(),
@@ -64,3 +65,7 @@ func requireSuccess(err error, msg string, args ...interface{}) {
6465
flog.Fatal(msg, args...)
6566
}
6667
}
68+
69+
func exitHelp(c *cli.Context) {
70+
cli.ShowCommandHelpAndExit(c, c.Command.FullName(), 1)
71+
}

cmd/coder/secrets.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ func makeSecretsCmd() cli.Command {
1919
Name: "secrets",
2020
Usage: "Interact with Coder Secrets",
2121
Description: "Interact with secrets objects owned by the active user.",
22+
Action: exitHelp,
2223
Subcommands: []cli.Command{
2324
{
2425
Name: "ls",
@@ -30,7 +31,7 @@ func makeSecretsCmd() cli.Command {
3031
Name: "rm",
3132
Usage: "Remove one or more secrets by name",
3233
ArgsUsage: "[...secret_name]",
33-
Action: removeSecret,
34+
Action: removeSecrets,
3435
},
3536
{
3637
Name: "view",
@@ -171,7 +172,7 @@ func viewSecret(c *cli.Context) {
171172
requireSuccess(err, "failed to write: %v", err)
172173
}
173174

174-
func removeSecret(c *cli.Context) {
175+
func removeSecrets(c *cli.Context) {
175176
var (
176177
client = requireAuth()
177178
names = append([]string{c.Args().First()}, c.Args().Tail()...)

cmd/coder/users.go

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package main
22

33
import (
44
"encoding/json"
5-
"fmt"
65
"os"
76

87
"cdr.dev/coder-cli/internal/x/xtabwriter"
@@ -14,8 +13,9 @@ import (
1413
func makeUsersCmd() cli.Command {
1514
var output string
1615
return cli.Command{
17-
Name: "users",
18-
Usage: "Interact with Coder user accounts",
16+
Name: "users",
17+
Usage: "Interact with Coder user accounts",
18+
Action: exitHelp,
1919
Subcommands: []cli.Command{
2020
{
2121
Name: "ls",
@@ -24,7 +24,7 @@ func makeUsersCmd() cli.Command {
2424
Flags: []cli.Flag{
2525
cli.StringFlag{
2626
Name: "output",
27-
Usage: "(json | human)",
27+
Usage: "json | human",
2828
Value: "human",
2929
Destination: &output,
3030
},
@@ -43,17 +43,10 @@ func listUsers(outputFmt *string) func(c *cli.Context) {
4343

4444
switch *outputFmt {
4545
case "human":
46-
w := xtabwriter.NewWriter()
47-
if len(users) > 0 {
48-
_, err = fmt.Fprintln(w, xtabwriter.StructFieldNames(users[0]))
49-
requireSuccess(err, "failed to write: %v", err)
50-
}
51-
for _, u := range users {
52-
_, err = fmt.Fprintln(w, xtabwriter.StructValues(u))
53-
requireSuccess(err, "failed to write: %v", err)
54-
}
55-
err = w.Flush()
56-
requireSuccess(err, "failed to flush writer: %v", err)
46+
err := xtabwriter.WriteTable(len(users), func(i int) interface{} {
47+
return users[i]
48+
})
49+
requireSuccess(err, "failed to write table: %v", err)
5750
case "json":
5851
err = json.NewEncoder(os.Stdout).Encode(users)
5952
requireSuccess(err, "failed to encode users to json: %v", err)

internal/entclient/env.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,33 @@ import (
44
"context"
55
"time"
66

7+
"cdr.dev/coder-cli/internal/x/xjson"
78
"nhooyr.io/websocket"
89
)
910

1011
// Environment describes a Coder environment
1112
type Environment struct {
12-
Name string `json:"name"`
13-
ID string `json:"id"`
13+
ID string `json:"id" tab:"-"`
14+
Name string `json:"name"`
15+
ImageID string `json:"image_id" tab:"-"`
16+
ImageTag string `json:"image_tag"`
17+
OrganizationID string `json:"organization_id" tab:"-"`
18+
UserID string `json:"user_id" tab:"-"`
19+
LastBuiltAt time.Time `json:"last_built_at" tab:"-"`
20+
CPUCores float32 `json:"cpu_cores"`
21+
MemoryGB int `json:"memory_gb"`
22+
DiskGB int `json:"disk_gb"`
23+
GPUs int `json:"gpus"`
24+
Updating bool `json:"updating"`
25+
RebuildMessages []struct {
26+
Text string `json:"text"`
27+
Required bool `json:"required"`
28+
} `json:"rebuild_messages" tab:"-"`
29+
CreatedAt time.Time `json:"created_at" tab:"-"`
30+
UpdatedAt time.Time `json:"updated_at" tab:"-"`
31+
LastOpenedAt time.Time `json:"last_opened_at" tab:"-"`
32+
LastConnectionAt time.Time `json:"last_connection_at" tab:"-"`
33+
AutoOffThreshold xjson.Duration `json:"auto_off_threshold" tab:"-"`
1434
}
1535

1636
// Envs gets the list of environments owned by the authenticated user

internal/x/xjson/duration.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package xjson
2+
3+
import (
4+
"encoding/json"
5+
"strconv"
6+
"time"
7+
)
8+
9+
// Duration is a time.Duration that marshals to millisecond precision.
10+
// Most javascript applications expect durations to be in milliseconds.
11+
type Duration time.Duration
12+
13+
// MarshalJSON marshals the duration to millisecond precision.
14+
func (d Duration) MarshalJSON() ([]byte, error) {
15+
du := time.Duration(d)
16+
return json.Marshal(du.Milliseconds())
17+
}
18+
19+
// UnmarshalJSON unmarshals a millisecond-precision integer to
20+
// a time.Duration.
21+
func (d *Duration) UnmarshalJSON(b []byte) error {
22+
i, err := strconv.ParseInt(string(b), 10, 64)
23+
if err != nil {
24+
return err
25+
}
26+
27+
*d = Duration(time.Duration(i) * time.Millisecond)
28+
return nil
29+
}
30+
31+
func (d Duration) String() string {
32+
return time.Duration(d).String()
33+
}

internal/x/xtabwriter/tabwriter.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func StructValues(data interface{}) string {
2525
if shouldHideField(v.Type().Field(i)) {
2626
continue
2727
}
28-
s.WriteString(fmt.Sprintf("%s\t", v.Field(i).Interface()))
28+
s.WriteString(fmt.Sprintf("%v\t", v.Field(i).Interface()))
2929
}
3030
return s.String()
3131
}
@@ -46,6 +46,32 @@ func StructFieldNames(data interface{}) string {
4646
return s.String()
4747
}
4848

49+
// WriteTable writes the given list elements to stdout in a human readable
50+
// tabular format. Headers abide by the `tab` struct tag.
51+
//
52+
// `tab:"-"` omits the field and no tag defaults to the Go identifier.
53+
func WriteTable(length int, each func(i int) interface{}) error {
54+
if length < 1 {
55+
return nil
56+
}
57+
w := NewWriter()
58+
defer w.Flush()
59+
for ix := 0; ix < length; ix++ {
60+
item := each(ix)
61+
if ix == 0 {
62+
_, err := fmt.Fprintln(w, StructFieldNames(item))
63+
if err != nil {
64+
return err
65+
}
66+
}
67+
_, err := fmt.Fprintln(w, StructValues(item))
68+
if err != nil {
69+
return err
70+
}
71+
}
72+
return nil
73+
}
74+
4975
func shouldHideField(f reflect.StructField) bool {
5076
return f.Tag.Get(structFieldTagKey) == "-"
5177
}

0 commit comments

Comments
 (0)