From 1a5ec825de5cfcba7d58b5fb72e24af1d82804cb Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Mon, 27 Jul 2020 10:25:28 -0500 Subject: [PATCH 1/2] Add users ls command --- ci/integration/integration_test.go | 24 ++++++++ cmd/coder/main.go | 1 + cmd/coder/users.go | 93 ++++++++++++++++++++++++++++++ cmd/coder/version.go | 2 +- internal/entclient/me.go | 12 +++- internal/entclient/users.go | 11 ++++ 6 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 cmd/coder/users.go create mode 100644 internal/entclient/users.go diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go index e14edd6e..a452cd18 100644 --- a/ci/integration/integration_test.go +++ b/ci/integration/integration_test.go @@ -2,6 +2,7 @@ package integration import ( "context" + "encoding/json" "fmt" "os" "os/exec" @@ -11,6 +12,8 @@ import ( "time" "cdr.dev/coder-cli/ci/tcli" + "cdr.dev/coder-cli/internal/entclient" + "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest/assert" ) @@ -110,6 +113,19 @@ func TestCoderCLI(t *testing.T) { tcli.Error(), ) + var user entclient.User + c.Run(ctx, `coder users ls -o json | jq -c '.[] | select( .username == "charlie")'`).Assert(t, + tcli.Success(), + jsonUnmarshals(&user), + ) + assert.Equal(t, "user email is as expected", "charlie@coder.com", user.Email) + assert.Equal(t, "username is as expected", "Charlie", user.Name) + + c.Run(ctx, "coder users ls -o human | grep charlie").Assert(t, + tcli.Success(), + tcli.StdoutMatches("charlie"), + ) + c.Run(ctx, "coder logout").Assert(t, tcli.Success(), ) @@ -118,3 +134,11 @@ func TestCoderCLI(t *testing.T) { tcli.Error(), ) } + +func jsonUnmarshals(target interface{}) tcli.Assertion { + return func(t *testing.T, r *tcli.CommandResult) { + slog.Helper() + err := json.Unmarshal(r.Stdout, target) + assert.Success(t, "json unmarshals", err) + } +} diff --git a/cmd/coder/main.go b/cmd/coder/main.go index 604775b5..93ebf022 100644 --- a/cmd/coder/main.go +++ b/cmd/coder/main.go @@ -42,6 +42,7 @@ func (r *rootCmd) Subcommands() []cli.Command { &urlsCmd{}, &versionCmd{}, &configSSHCmd{}, + &usersCmd{}, } } diff --git a/cmd/coder/users.go b/cmd/coder/users.go new file mode 100644 index 00000000..bef0d7c0 --- /dev/null +++ b/cmd/coder/users.go @@ -0,0 +1,93 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "reflect" + "strings" + "text/tabwriter" + + "github.com/spf13/pflag" + + "go.coder.com/cli" + "go.coder.com/flog" +) + +type usersCmd struct { +} + +func (cmd usersCmd) Spec() cli.CommandSpec { + return cli.CommandSpec{ + Name: "users", + Usage: "[subcommand] ", + Desc: "interact with user accounts", + } +} + +func (cmd usersCmd) Run(fl *pflag.FlagSet) { + exitUsage(fl) +} + +func (cmd *usersCmd) Subcommands() []cli.Command { + return []cli.Command{ + &listCmd{}, + } +} + +type listCmd struct { + outputFmt string +} + +func tabDelimited(data interface{}) string { + v := reflect.ValueOf(data) + s := &strings.Builder{} + for i := 0; i < v.NumField(); i++ { + s.WriteString(fmt.Sprintf("%s\t", v.Field(i).Interface())) + } + return s.String() +} + +func (cmd *listCmd) Run(fl *pflag.FlagSet) { + entClient := requireAuth() + + users, err := entClient.Users() + if err != nil { + flog.Fatal("failed to get users: %v", err) + } + + switch cmd.outputFmt { + case "human": + w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) + for _, u := range users { + _, err = fmt.Fprintln(w, tabDelimited(u)) + if err != nil { + flog.Fatal("failed to write: %v", err) + } + } + err = w.Flush() + if err != nil { + flog.Fatal("failed to flush writer: %v", err) + } + case "json": + err = json.NewEncoder(os.Stdout).Encode(users) + if err != nil { + flog.Fatal("failed to encode users to json: %v", err) + } + default: + exitUsage(fl) + } + +} + +func (cmd *listCmd) RegisterFlags(fl *pflag.FlagSet) { + fl.StringVarP(&cmd.outputFmt, "output", "o", "human", "output format (human | json)") +} + +func (cmd *listCmd) Spec() cli.CommandSpec { + return cli.CommandSpec{ + Name: "ls", + Usage: "", + Desc: "list all users", + } +} diff --git a/cmd/coder/version.go b/cmd/coder/version.go index 9568bf75..a1825843 100644 --- a/cmd/coder/version.go +++ b/cmd/coder/version.go @@ -15,7 +15,7 @@ func (versionCmd) Spec() cli.CommandSpec { return cli.CommandSpec{ Name: "version", Usage: "", - Desc: "Print the currently installed CLI version", + Desc: "print the currently installed CLI version", } } diff --git a/internal/entclient/me.go b/internal/entclient/me.go index 69c3bde5..2b44debb 100644 --- a/internal/entclient/me.go +++ b/internal/entclient/me.go @@ -1,10 +1,16 @@ package entclient +import ( + "time" +) + // User describes a Coder user account type User struct { - ID string `json:"id"` - Email string `json:"email"` - Username string `json:"username"` + ID string `json:"id"` + Email string `json:"email"` + Username string `json:"username"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` } // Me gets the details of the authenticated user diff --git a/internal/entclient/users.go b/internal/entclient/users.go new file mode 100644 index 00000000..2354e568 --- /dev/null +++ b/internal/entclient/users.go @@ -0,0 +1,11 @@ +package entclient + +// Users gets the list of user accounts +func (c Client) Users() ([]User, error) { + var u []User + err := c.requestBody("GET", "/api/users", nil, &u) + if err != nil { + return nil, err + } + return u, nil +} From 5e43b71261f5715d726e98ce685c526923670e4c Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Wed, 29 Jul 2020 13:32:00 -0500 Subject: [PATCH 2/2] Improve command descriptions --- cmd/coder/envs.go | 2 +- cmd/coder/logout.go | 2 +- cmd/coder/shell.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/coder/envs.go b/cmd/coder/envs.go index 5abd5f28..9e45df1f 100644 --- a/cmd/coder/envs.go +++ b/cmd/coder/envs.go @@ -14,7 +14,7 @@ type envsCmd struct { func (cmd envsCmd) Spec() cli.CommandSpec { return cli.CommandSpec{ Name: "envs", - Desc: "get a list of active environment", + Desc: "get a list of environments owned by the authenticated user", } } diff --git a/cmd/coder/logout.go b/cmd/coder/logout.go index c0dba553..6120f527 100644 --- a/cmd/coder/logout.go +++ b/cmd/coder/logout.go @@ -17,7 +17,7 @@ type logoutCmd struct { func (cmd logoutCmd) Spec() cli.CommandSpec { return cli.CommandSpec{ Name: "logout", - Desc: "remote local authentication credentials (if any)", + Desc: "remove local authentication credentials (if any)", } } diff --git a/cmd/coder/shell.go b/cmd/coder/shell.go index 82a31436..7e8b70ef 100644 --- a/cmd/coder/shell.go +++ b/cmd/coder/shell.go @@ -27,7 +27,7 @@ func (cmd *shellCmd) Spec() cli.CommandSpec { return cli.CommandSpec{ Name: "sh", Usage: " []", - Desc: "executes a remote command on the environment\nIf no command is specified, the default shell is opened.", + Desc: "execute a remote command on the environment\nIf no command is specified, the default shell is opened.", RawArgs: true, } }