Skip to content

Commit f5df548

Browse files
authored
feat: tokens (#4380)
1 parent fe7c9f8 commit f5df548

21 files changed

+685
-260
lines changed

cli/cliui/table.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,10 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
153153
// Special type formatting.
154154
switch val := v.(type) {
155155
case time.Time:
156-
v = val.Format(time.Stamp)
156+
v = val.Format(time.RFC3339)
157157
case *time.Time:
158158
if val != nil {
159-
v = val.Format(time.Stamp)
159+
v = val.Format(time.RFC3339)
160160
}
161161
case fmt.Stringer:
162162
if val != nil {

cli/cliui/table_test.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,10 @@ func Test_DisplayTable(t *testing.T) {
131131
t.Parallel()
132132

133133
expected := `
134-
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
135-
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } Aug 2 15:49:10 Aug 2 15:49:10
136-
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } Aug 2 15:49:10 <nil>
137-
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } Aug 2 15:49:10 <nil>
134+
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
135+
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
136+
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
137+
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
138138
`
139139

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

160160
expected := `
161-
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
162-
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } Aug 2 15:49:10 <nil>
163-
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } Aug 2 15:49:10 <nil>
164-
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } Aug 2 15:49:10 Aug 2 15:49:10
161+
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
162+
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
163+
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
164+
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
165165
`
166166

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

176176
expected := `
177177
NAME SUB 1 NAME SUB 3 INNER NAME TIME
178-
foo foo1 foo3 Aug 2 15:49:10
179-
bar bar1 bar3 Aug 2 15:49:10
180-
baz baz1 baz3 Aug 2 15:49:10
178+
foo foo1 foo3 2022-08-02T15:49:10Z
179+
bar bar1 bar3 2022-08-02T15:49:10Z
180+
baz baz1 baz3 2022-08-02T15:49:10Z
181181
`
182182

183183
out, err := cliui.DisplayTable(in, "", []string{"name", "sub_1_name", "sub_3 inner name", "time"})

cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ func Core() []*cobra.Command {
9393
users(),
9494
versionCmd(),
9595
workspaceAgent(),
96+
tokens(),
9697
}
9798
}
9899

cli/tokens.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"time"
7+
8+
"github.com/spf13/cobra"
9+
"golang.org/x/xerrors"
10+
11+
"github.com/coder/coder/cli/cliui"
12+
"github.com/coder/coder/codersdk"
13+
)
14+
15+
func tokens() *cobra.Command {
16+
cmd := &cobra.Command{
17+
Use: "tokens",
18+
Short: "Manage personal access tokens",
19+
Long: "Tokens are used to authenticate automated clients to Coder.",
20+
Aliases: []string{"token"},
21+
Example: formatExamples(
22+
example{
23+
Description: "Create a token for automation",
24+
Command: "coder tokens create",
25+
},
26+
example{
27+
Description: "List your tokens",
28+
Command: "coder tokens ls",
29+
},
30+
example{
31+
Description: "Remove a token by ID",
32+
Command: "coder tokens rm WuoWs4ZsMX",
33+
},
34+
),
35+
}
36+
cmd.AddCommand(
37+
createToken(),
38+
listTokens(),
39+
removeToken(),
40+
)
41+
42+
return cmd
43+
}
44+
45+
func createToken() *cobra.Command {
46+
cmd := &cobra.Command{
47+
Use: "create",
48+
Short: "Create a tokens",
49+
RunE: func(cmd *cobra.Command, args []string) error {
50+
client, err := CreateClient(cmd)
51+
if err != nil {
52+
return xerrors.Errorf("create codersdk client: %w", err)
53+
}
54+
55+
res, err := client.CreateToken(cmd.Context(), codersdk.Me)
56+
if err != nil {
57+
return xerrors.Errorf("create tokens: %w", err)
58+
}
59+
60+
cmd.Println(cliui.Styles.Wrap.Render(
61+
"Here is your token. 🪄",
62+
))
63+
cmd.Println()
64+
cmd.Println(cliui.Styles.Code.Render(strings.TrimSpace(res.Key)))
65+
cmd.Println()
66+
cmd.Println(cliui.Styles.Wrap.Render(
67+
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),
68+
))
69+
70+
return nil
71+
},
72+
}
73+
74+
return cmd
75+
}
76+
77+
type tokenRow struct {
78+
ID string `table:"ID"`
79+
LastUsed time.Time `table:"Last Used"`
80+
ExpiresAt time.Time `table:"Expires At"`
81+
CreatedAt time.Time `table:"Created At"`
82+
}
83+
84+
func listTokens() *cobra.Command {
85+
cmd := &cobra.Command{
86+
Use: "list",
87+
Aliases: []string{"ls"},
88+
Short: "List tokens",
89+
RunE: func(cmd *cobra.Command, args []string) error {
90+
client, err := CreateClient(cmd)
91+
if err != nil {
92+
return xerrors.Errorf("create codersdk client: %w", err)
93+
}
94+
95+
keys, err := client.GetTokens(cmd.Context(), codersdk.Me)
96+
if err != nil {
97+
return xerrors.Errorf("create tokens: %w", err)
98+
}
99+
100+
if len(keys) == 0 {
101+
cmd.Println(cliui.Styles.Wrap.Render(
102+
"No tokens found.",
103+
))
104+
}
105+
106+
var rows []tokenRow
107+
for _, key := range keys {
108+
rows = append(rows, tokenRow{
109+
ID: key.ID,
110+
LastUsed: key.LastUsed,
111+
ExpiresAt: key.ExpiresAt,
112+
CreatedAt: key.CreatedAt,
113+
})
114+
}
115+
116+
out, err := cliui.DisplayTable(rows, "", nil)
117+
if err != nil {
118+
return err
119+
}
120+
121+
_, err = fmt.Fprintln(cmd.OutOrStdout(), out)
122+
return err
123+
},
124+
}
125+
126+
return cmd
127+
}
128+
129+
func removeToken() *cobra.Command {
130+
cmd := &cobra.Command{
131+
Use: "remove [id]",
132+
Aliases: []string{"rm"},
133+
Short: "Delete a token",
134+
Args: cobra.ExactArgs(1),
135+
RunE: func(cmd *cobra.Command, args []string) error {
136+
client, err := CreateClient(cmd)
137+
if err != nil {
138+
return xerrors.Errorf("create codersdk client: %w", err)
139+
}
140+
141+
err = client.DeleteAPIKey(cmd.Context(), codersdk.Me, args[0])
142+
if err != nil {
143+
return xerrors.Errorf("delete api key: %w", err)
144+
}
145+
146+
cmd.Println(cliui.Styles.Wrap.Render(
147+
"Token has been deleted.",
148+
))
149+
150+
return nil
151+
},
152+
}
153+
154+
return cmd
155+
}

cli/tokens_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package cli_test
2+
3+
import (
4+
"bytes"
5+
"regexp"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/coder/coder/cli/clitest"
11+
"github.com/coder/coder/coderd/coderdtest"
12+
)
13+
14+
func TestTokens(t *testing.T) {
15+
t.Parallel()
16+
client := coderdtest.New(t, nil)
17+
_ = coderdtest.CreateFirstUser(t, client)
18+
19+
// helpful empty response
20+
cmd, root := clitest.New(t, "tokens", "ls")
21+
clitest.SetupConfig(t, client, root)
22+
buf := new(bytes.Buffer)
23+
cmd.SetOut(buf)
24+
err := cmd.Execute()
25+
require.NoError(t, err)
26+
res := buf.String()
27+
require.Contains(t, res, "tokens found")
28+
29+
cmd, root = clitest.New(t, "tokens", "create")
30+
clitest.SetupConfig(t, client, root)
31+
buf = new(bytes.Buffer)
32+
cmd.SetOut(buf)
33+
err = cmd.Execute()
34+
require.NoError(t, err)
35+
res = buf.String()
36+
require.NotEmpty(t, res)
37+
// find API key in format "XXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXX"
38+
r := regexp.MustCompile("[a-zA-Z0-9]{10}-[a-zA-Z0-9]{22}")
39+
require.Regexp(t, r, res)
40+
key := r.FindString(res)
41+
id := key[:10]
42+
43+
cmd, root = clitest.New(t, "tokens", "ls")
44+
clitest.SetupConfig(t, client, root)
45+
buf = new(bytes.Buffer)
46+
cmd.SetOut(buf)
47+
err = cmd.Execute()
48+
require.NoError(t, err)
49+
res = buf.String()
50+
require.NotEmpty(t, res)
51+
require.Contains(t, res, "ID")
52+
require.Contains(t, res, "EXPIRES AT")
53+
require.Contains(t, res, "CREATED AT")
54+
require.Contains(t, res, "LAST USED")
55+
require.Contains(t, res, id)
56+
57+
cmd, root = clitest.New(t, "tokens", "rm", id)
58+
clitest.SetupConfig(t, client, root)
59+
buf = new(bytes.Buffer)
60+
cmd.SetOut(buf)
61+
err = cmd.Execute()
62+
require.NoError(t, err)
63+
res = buf.String()
64+
require.NotEmpty(t, res)
65+
require.Contains(t, res, "deleted")
66+
}

0 commit comments

Comments
 (0)