Skip to content

Commit 9e622d0

Browse files
authored
feat(cli): add coder users delete command (#10115)
1 parent 24c80bf commit 9e622d0

15 files changed

+247
-14
lines changed

cli/testdata/coder_users_--help.golden

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ SUBCOMMANDS:
1111
activate Update a user's status to 'active'. Active users can fully
1212
interact with the platform
1313
create
14+
delete Delete a user by username or user_id.
1415
list
1516
show Show a single user. Use 'me' to indicate the currently
1617
authenticated user.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
coder v0.0.0-devel
2+
3+
USAGE:
4+
coder users delete <username|user_id>
5+
6+
Delete a user by username or user_id.
7+
8+
Aliases: rm
9+
10+
———
11+
Run `coder --help` for a list of global options.

cli/testdata/coder_users_suspend_--help.golden

-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ USAGE:
66
Update a user's status to 'suspended'. A suspended user cannot log into the
77
platform
88

9-
Aliases: rm, delete
10-
119
$ coder users suspend example_user
1210

1311
OPTIONS:

cli/user_delete_test.go

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package cli_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/coder/coder/v2/cli/clitest"
10+
"github.com/coder/coder/v2/coderd/coderdtest"
11+
"github.com/coder/coder/v2/codersdk"
12+
"github.com/coder/coder/v2/cryptorand"
13+
"github.com/coder/coder/v2/pty/ptytest"
14+
)
15+
16+
func TestUserDelete(t *testing.T) {
17+
t.Parallel()
18+
t.Run("Username", func(t *testing.T) {
19+
t.Parallel()
20+
ctx := context.Background()
21+
client := coderdtest.New(t, nil)
22+
aUser := coderdtest.CreateFirstUser(t, client)
23+
24+
pw, err := cryptorand.String(16)
25+
require.NoError(t, err)
26+
27+
_, err = client.CreateUser(ctx, codersdk.CreateUserRequest{
28+
Email: "colin5@coder.com",
29+
Username: "coolin",
30+
Password: pw,
31+
UserLoginType: codersdk.LoginTypePassword,
32+
OrganizationID: aUser.OrganizationID,
33+
DisableLogin: false,
34+
})
35+
require.NoError(t, err)
36+
37+
inv, root := clitest.New(t, "users", "delete", "coolin")
38+
clitest.SetupConfig(t, client, root)
39+
pty := ptytest.New(t).Attach(inv)
40+
errC := make(chan error)
41+
go func() {
42+
errC <- inv.Run()
43+
}()
44+
require.NoError(t, <-errC)
45+
pty.ExpectMatch("coolin")
46+
})
47+
48+
t.Run("UserID", func(t *testing.T) {
49+
t.Parallel()
50+
ctx := context.Background()
51+
client := coderdtest.New(t, nil)
52+
aUser := coderdtest.CreateFirstUser(t, client)
53+
54+
pw, err := cryptorand.String(16)
55+
require.NoError(t, err)
56+
57+
user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
58+
Email: "colin5@coder.com",
59+
Username: "coolin",
60+
Password: pw,
61+
UserLoginType: codersdk.LoginTypePassword,
62+
OrganizationID: aUser.OrganizationID,
63+
DisableLogin: false,
64+
})
65+
require.NoError(t, err)
66+
67+
inv, root := clitest.New(t, "users", "delete", user.ID.String())
68+
clitest.SetupConfig(t, client, root)
69+
pty := ptytest.New(t).Attach(inv)
70+
errC := make(chan error)
71+
go func() {
72+
errC <- inv.Run()
73+
}()
74+
require.NoError(t, <-errC)
75+
pty.ExpectMatch("coolin")
76+
})
77+
78+
t.Run("UserID", func(t *testing.T) {
79+
t.Parallel()
80+
ctx := context.Background()
81+
client := coderdtest.New(t, nil)
82+
aUser := coderdtest.CreateFirstUser(t, client)
83+
84+
pw, err := cryptorand.String(16)
85+
require.NoError(t, err)
86+
87+
user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
88+
Email: "colin5@coder.com",
89+
Username: "coolin",
90+
Password: pw,
91+
UserLoginType: codersdk.LoginTypePassword,
92+
OrganizationID: aUser.OrganizationID,
93+
DisableLogin: false,
94+
})
95+
require.NoError(t, err)
96+
97+
inv, root := clitest.New(t, "users", "delete", user.ID.String())
98+
clitest.SetupConfig(t, client, root)
99+
pty := ptytest.New(t).Attach(inv)
100+
errC := make(chan error)
101+
go func() {
102+
errC <- inv.Run()
103+
}()
104+
require.NoError(t, <-errC)
105+
pty.ExpectMatch("coolin")
106+
})
107+
108+
// TODO: reenable this test case. Fetching users without perms returns a
109+
// "user "testuser@coder.com" must be a member of at least one organization"
110+
// error.
111+
// t.Run("NoPerms", func(t *testing.T) {
112+
// t.Parallel()
113+
// ctx := context.Background()
114+
// client := coderdtest.New(t, nil)
115+
// aUser := coderdtest.CreateFirstUser(t, client)
116+
117+
// pw, err := cryptorand.String(16)
118+
// require.NoError(t, err)
119+
120+
// fmt.Println(aUser.OrganizationID)
121+
// toDelete, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
122+
// Email: "colin5@coder.com",
123+
// Username: "coolin",
124+
// Password: pw,
125+
// UserLoginType: codersdk.LoginTypePassword,
126+
// OrganizationID: aUser.OrganizationID,
127+
// DisableLogin: false,
128+
// })
129+
// require.NoError(t, err)
130+
131+
// uClient, _ := coderdtest.CreateAnotherUser(t, client, aUser.OrganizationID)
132+
// _ = uClient
133+
// _ = toDelete
134+
135+
// inv, root := clitest.New(t, "users", "delete", "coolin")
136+
// clitest.SetupConfig(t, uClient, root)
137+
// require.ErrorContains(t, inv.Run(), "...")
138+
// })
139+
140+
t.Run("DeleteSelf", func(t *testing.T) {
141+
t.Parallel()
142+
ctx := context.Background()
143+
client := coderdtest.New(t, nil)
144+
aUser := coderdtest.CreateFirstUser(t, client)
145+
146+
pw, err := cryptorand.String(16)
147+
require.NoError(t, err)
148+
149+
_, err = client.CreateUser(ctx, codersdk.CreateUserRequest{
150+
Email: "colin5@coder.com",
151+
Username: "coolin",
152+
Password: pw,
153+
UserLoginType: codersdk.LoginTypePassword,
154+
OrganizationID: aUser.OrganizationID,
155+
DisableLogin: false,
156+
})
157+
require.NoError(t, err)
158+
159+
coderdtest.CreateAnotherUser(t, client, aUser.OrganizationID)
160+
161+
inv, root := clitest.New(t, "users", "delete", "me")
162+
clitest.SetupConfig(t, client, root)
163+
require.ErrorContains(t, inv.Run(), "You cannot delete yourself!")
164+
})
165+
}

cli/userdelete.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
6+
"golang.org/x/xerrors"
7+
8+
"github.com/coder/coder/v2/cli/clibase"
9+
"github.com/coder/coder/v2/cli/cliui"
10+
"github.com/coder/coder/v2/codersdk"
11+
"github.com/coder/pretty"
12+
)
13+
14+
func (r *RootCmd) userDelete() *clibase.Cmd {
15+
client := new(codersdk.Client)
16+
cmd := &clibase.Cmd{
17+
Use: "delete <username|user_id>",
18+
Short: "Delete a user by username or user_id.",
19+
Middleware: clibase.Chain(
20+
clibase.RequireNArgs(1),
21+
r.InitClient(client),
22+
),
23+
Handler: func(inv *clibase.Invocation) error {
24+
ctx := inv.Context()
25+
user, err := client.User(ctx, inv.Args[0])
26+
if err != nil {
27+
return xerrors.Errorf("fetch user: %w", err)
28+
}
29+
30+
err = client.DeleteUser(ctx, user.ID)
31+
if err != nil {
32+
return xerrors.Errorf("delete user: %w", err)
33+
}
34+
35+
_, _ = fmt.Fprintln(inv.Stderr,
36+
"Successfully deleted "+pretty.Sprint(cliui.DefaultStyles.Keyword, user.Username)+".",
37+
)
38+
return nil
39+
},
40+
}
41+
return cmd
42+
}

cli/users.go

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ func (r *RootCmd) users() *clibase.Cmd {
1717
r.userCreate(),
1818
r.userList(),
1919
r.userSingle(),
20+
r.userDelete(),
2021
r.createUserStatusCommand(codersdk.UserStatusActive),
2122
r.createUserStatusCommand(codersdk.UserStatusSuspended),
2223
},

cli/userstatus.go

-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ func (r *RootCmd) createUserStatusCommand(sdkStatus codersdk.UserStatus) *clibas
2828
case codersdk.UserStatusSuspended:
2929
verb = "suspend"
3030
pastVerb = "suspended"
31-
aliases = []string{"rm", "delete"}
3231
short = "Update a user's status to 'suspended'. A suspended user cannot log into the platform"
3332
default:
3433
panic(fmt.Sprintf("%s is not supported", sdkStatus))

coderd/apidoc/docs.go

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/users.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ func (api *API) deleteUser(rw http.ResponseWriter, r *http.Request) {
501501
// @Security CoderSessionToken
502502
// @Produce json
503503
// @Tags Users
504-
// @Param user path string true "User ID, name, or me"
504+
// @Param user path string true "User ID, username, or me"
505505
// @Success 200 {object} codersdk.User
506506
// @Router /users/{user} [get]
507507
func (api *API) userByName(rw http.ResponseWriter, r *http.Request) {

docs/api/users.md

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/cli/users.md

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/cli/users_delete.md

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/cli/users_suspend.md

-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/manifest.json

+5
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,11 @@
885885
"title": "users create",
886886
"path": "cli/users_create.md"
887887
},
888+
{
889+
"title": "users delete",
890+
"description": "Delete a user by username or user_id.",
891+
"path": "cli/users_delete.md"
892+
},
888893
{
889894
"title": "users list",
890895
"path": "cli/users_list.md"

0 commit comments

Comments
 (0)