Skip to content
Merged
4 changes: 2 additions & 2 deletions cli/cliui/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,10 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
// Special type formatting.
switch val := v.(type) {
case time.Time:
v = val.Format(time.Stamp)
v = val.Format(time.RFC3339)
case *time.Time:
if val != nil {
v = val.Format(time.Stamp)
v = val.Format(time.RFC3339)
}
case fmt.Stringer:
if val != nil {
Expand Down
22 changes: 11 additions & 11 deletions cli/cliui/table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@ func Test_DisplayTable(t *testing.T) {
t.Parallel()

expected := `
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } Aug 2 15:49:10 Aug 2 15:49:10
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } Aug 2 15:49:10 <nil>
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } Aug 2 15:49:10 <nil>
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
`

// Test with non-pointer values.
Expand All @@ -158,10 +158,10 @@ baz 30 [] baz1 31 <nil> <nil> baz3
t.Parallel()

expected := `
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } Aug 2 15:49:10 <nil>
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } Aug 2 15:49:10 <nil>
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } Aug 2 15:49:10 Aug 2 15:49:10
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
`

out, err := cliui.DisplayTable(in, "name", nil)
Expand All @@ -175,9 +175,9 @@ foo 10 [a b c] foo1 11 foo2 12 foo3

expected := `
NAME SUB 1 NAME SUB 3 INNER NAME TIME
foo foo1 foo3 Aug 2 15:49:10
bar bar1 bar3 Aug 2 15:49:10
baz baz1 baz3 Aug 2 15:49:10
foo foo1 foo3 2022-08-02T15:49:10Z
bar bar1 bar3 2022-08-02T15:49:10Z
baz baz1 baz3 2022-08-02T15:49:10Z
`

out, err := cliui.DisplayTable(in, "", []string{"name", "sub_1_name", "sub_3 inner name", "time"})
Expand Down
155 changes: 155 additions & 0 deletions cli/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package cli

import (
"fmt"
"strings"
"time"

"github.com/spf13/cobra"
"golang.org/x/xerrors"

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

func tokens() *cobra.Command {
cmd := &cobra.Command{
Use: "tokens",
Short: "Manage personal access tokens",
Long: "Tokens are used to authenticate automated clients to Coder.",
Aliases: []string{"token"},
Example: formatExamples(
example{
Description: "Create a token for automation",
Command: "coder tokens create",
},
example{
Description: "List your tokens",
Command: "coder tokens ls",
},
example{
Description: "Remove a token by ID",
Command: "coder tokens rm WuoWs4ZsMX",
},
),
}
cmd.AddCommand(
createToken(),
listTokens(),
removeToken(),
)

return cmd
}

func createToken() *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Create a tokens",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := CreateClient(cmd)
if err != nil {
return xerrors.Errorf("create codersdk client: %w", err)
}

res, err := client.CreateToken(cmd.Context(), codersdk.Me)
if err != nil {
return xerrors.Errorf("create tokens: %w", err)
}

cmd.Println(cliui.Styles.Wrap.Render(
"Here is your token. 🪄",
))
cmd.Println()
cmd.Println(cliui.Styles.Code.Render(strings.TrimSpace(res.Key)))
cmd.Println()
cmd.Println(cliui.Styles.Wrap.Render(
fmt.Sprintf("You can use this token by setting the --%s CLI flag, the %s environment variable, or the %q HTTP header.", varToken, envSessionToken, codersdk.SessionTokenKey),
))

return nil
},
}

return cmd
}

type tokenRow struct {
ID string `table:"ID"`
LastUsed time.Time `table:"Last Used"`
ExpiresAt time.Time `table:"Expires At"`
CreatedAt time.Time `table:"Created At"`
}

func listTokens() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List tokens",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := CreateClient(cmd)
if err != nil {
return xerrors.Errorf("create codersdk client: %w", err)
}

keys, err := client.GetTokens(cmd.Context(), codersdk.Me)
if err != nil {
return xerrors.Errorf("create tokens: %w", err)
}

if len(keys) == 0 {
cmd.Println(cliui.Styles.Wrap.Render(
"No tokens found.",
))
}

var rows []tokenRow
for _, key := range keys {
rows = append(rows, tokenRow{
ID: key.ID,
LastUsed: key.LastUsed,
ExpiresAt: key.ExpiresAt,
CreatedAt: key.CreatedAt,
})
}

out, err := cliui.DisplayTable(rows, "", nil)
if err != nil {
return err
}

_, err = fmt.Fprintln(cmd.OutOrStdout(), out)
return err
},
}

return cmd
}

func removeToken() *cobra.Command {
cmd := &cobra.Command{
Use: "remove [id]",
Aliases: []string{"rm"},
Short: "Delete a token",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := CreateClient(cmd)
if err != nil {
return xerrors.Errorf("create codersdk client: %w", err)
}

err = client.DeleteAPIKey(cmd.Context(), codersdk.Me, args[0])
if err != nil {
return xerrors.Errorf("delete api key: %w", err)
}

cmd.Println(cliui.Styles.Wrap.Render(
"Token has been deleted.",
))

return nil
},
}

return cmd
}
66 changes: 66 additions & 0 deletions cli/keys_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package cli_test

import (
"bytes"
"regexp"
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
)

func TestMachineKeys(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)

// helpful empty response
cmd, root := clitest.New(t, "tokens", "ls")
clitest.SetupConfig(t, client, root)
buf := new(bytes.Buffer)
cmd.SetOut(buf)
err := cmd.Execute()
require.NoError(t, err)
res := buf.String()
require.Contains(t, res, "tokens found")

cmd, root = clitest.New(t, "tokens", "create")
clitest.SetupConfig(t, client, root)
buf = new(bytes.Buffer)
cmd.SetOut(buf)
err = cmd.Execute()
require.NoError(t, err)
res = buf.String()
require.NotEmpty(t, res)
// find API key in format "XXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXX"
r := regexp.MustCompile("[a-zA-Z0-9]{10}-[a-zA-Z0-9]{22}")
require.Regexp(t, r, res)
key := r.FindString(res)
id := key[:10]

cmd, root = clitest.New(t, "tokens", "ls")
clitest.SetupConfig(t, client, root)
buf = new(bytes.Buffer)
cmd.SetOut(buf)
err = cmd.Execute()
require.NoError(t, err)
res = buf.String()
require.NotEmpty(t, res)
require.Contains(t, res, "ID")
require.Contains(t, res, "EXPIRES AT")
require.Contains(t, res, "CREATED AT")
require.Contains(t, res, "LAST USED")
require.Contains(t, res, id)

cmd, root = clitest.New(t, "tokens", "rm", id)
clitest.SetupConfig(t, client, root)
buf = new(bytes.Buffer)
cmd.SetOut(buf)
err = cmd.Execute()
require.NoError(t, err)
res = buf.String()
require.NotEmpty(t, res)
require.Contains(t, res, "deleted")
}
1 change: 1 addition & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func Core() []*cobra.Command {
users(),
versionCmd(),
workspaceAgent(),
tokens(),
}
}

Expand Down
Loading