Skip to content

feat(cli): add coder users delete command #10115

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/testdata/coder_users_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ SUBCOMMANDS:
activate Update a user's status to 'active'. Active users can fully
interact with the platform
create
delete Delete a user by username or user_id.
list
show Show a single user. Use 'me' to indicate the currently
authenticated user.
Expand Down
11 changes: 11 additions & 0 deletions cli/testdata/coder_users_delete_--help.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
coder v0.0.0-devel

USAGE:
coder users delete <username|user_id>

Delete a user by username or user_id.

Aliases: rm

———
Run `coder --help` for a list of global options.
2 changes: 0 additions & 2 deletions cli/testdata/coder_users_suspend_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ USAGE:
Update a user's status to 'suspended'. A suspended user cannot log into the
platform

Aliases: rm, delete

$ coder users suspend example_user

OPTIONS:
Expand Down
165 changes: 165 additions & 0 deletions cli/user_delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package cli_test

import (
"context"
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/pty/ptytest"
)

func TestUserDelete(t *testing.T) {
t.Parallel()
t.Run("Username", func(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := coderdtest.New(t, nil)
aUser := coderdtest.CreateFirstUser(t, client)

pw, err := cryptorand.String(16)
require.NoError(t, err)

_, err = client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "colin5@coder.com",
Username: "coolin",
Password: pw,
UserLoginType: codersdk.LoginTypePassword,
OrganizationID: aUser.OrganizationID,
DisableLogin: false,
})
require.NoError(t, err)

inv, root := clitest.New(t, "users", "delete", "coolin")
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv)
errC := make(chan error)
go func() {
errC <- inv.Run()
}()
require.NoError(t, <-errC)
pty.ExpectMatch("coolin")
})

t.Run("UserID", func(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := coderdtest.New(t, nil)
aUser := coderdtest.CreateFirstUser(t, client)

pw, err := cryptorand.String(16)
require.NoError(t, err)

user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "colin5@coder.com",
Username: "coolin",
Password: pw,
UserLoginType: codersdk.LoginTypePassword,
OrganizationID: aUser.OrganizationID,
DisableLogin: false,
})
require.NoError(t, err)

inv, root := clitest.New(t, "users", "delete", user.ID.String())
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv)
errC := make(chan error)
go func() {
errC <- inv.Run()
}()
require.NoError(t, <-errC)
pty.ExpectMatch("coolin")
})

t.Run("UserID", func(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := coderdtest.New(t, nil)
aUser := coderdtest.CreateFirstUser(t, client)

pw, err := cryptorand.String(16)
require.NoError(t, err)

user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "colin5@coder.com",
Username: "coolin",
Password: pw,
UserLoginType: codersdk.LoginTypePassword,
OrganizationID: aUser.OrganizationID,
DisableLogin: false,
})
require.NoError(t, err)

inv, root := clitest.New(t, "users", "delete", user.ID.String())
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv)
errC := make(chan error)
go func() {
errC <- inv.Run()
}()
require.NoError(t, <-errC)
pty.ExpectMatch("coolin")
})

// TODO: reenable this test case. Fetching users without perms returns a
// "user "testuser@coder.com" must be a member of at least one organization"
// error.
// t.Run("NoPerms", func(t *testing.T) {
// t.Parallel()
// ctx := context.Background()
// client := coderdtest.New(t, nil)
// aUser := coderdtest.CreateFirstUser(t, client)

// pw, err := cryptorand.String(16)
// require.NoError(t, err)

// fmt.Println(aUser.OrganizationID)
// toDelete, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
// Email: "colin5@coder.com",
// Username: "coolin",
// Password: pw,
// UserLoginType: codersdk.LoginTypePassword,
// OrganizationID: aUser.OrganizationID,
// DisableLogin: false,
// })
// require.NoError(t, err)

// uClient, _ := coderdtest.CreateAnotherUser(t, client, aUser.OrganizationID)
// _ = uClient
// _ = toDelete

// inv, root := clitest.New(t, "users", "delete", "coolin")
// clitest.SetupConfig(t, uClient, root)
// require.ErrorContains(t, inv.Run(), "...")
// })

t.Run("DeleteSelf", func(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := coderdtest.New(t, nil)
aUser := coderdtest.CreateFirstUser(t, client)

pw, err := cryptorand.String(16)
require.NoError(t, err)

_, err = client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "colin5@coder.com",
Username: "coolin",
Password: pw,
UserLoginType: codersdk.LoginTypePassword,
OrganizationID: aUser.OrganizationID,
DisableLogin: false,
})
require.NoError(t, err)

coderdtest.CreateAnotherUser(t, client, aUser.OrganizationID)

inv, root := clitest.New(t, "users", "delete", "me")
clitest.SetupConfig(t, client, root)
require.ErrorContains(t, inv.Run(), "You cannot delete yourself!")
})
}
42 changes: 42 additions & 0 deletions cli/userdelete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package cli

import (
"fmt"

"golang.org/x/xerrors"

"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
)

func (r *RootCmd) userDelete() *clibase.Cmd {
client := new(codersdk.Client)
cmd := &clibase.Cmd{
Use: "delete <username|user_id>",
Short: "Delete a user by username or user_id.",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
ctx := inv.Context()
user, err := client.User(ctx, inv.Args[0])
if err != nil {
return xerrors.Errorf("fetch user: %w", err)
}

err = client.DeleteUser(ctx, user.ID)
if err != nil {
return xerrors.Errorf("delete user: %w", err)
}

_, _ = fmt.Fprintln(inv.Stderr,
"Successfully deleted "+pretty.Sprint(cliui.DefaultStyles.Keyword, user.Username)+".",
)
return nil
},
}
return cmd
}
1 change: 1 addition & 0 deletions cli/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func (r *RootCmd) users() *clibase.Cmd {
r.userCreate(),
r.userList(),
r.userSingle(),
r.userDelete(),
r.createUserStatusCommand(codersdk.UserStatusActive),
r.createUserStatusCommand(codersdk.UserStatusSuspended),
},
Expand Down
1 change: 0 additions & 1 deletion cli/userstatus.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ func (r *RootCmd) createUserStatusCommand(sdkStatus codersdk.UserStatus) *clibas
case codersdk.UserStatusSuspended:
verb = "suspend"
pastVerb = "suspended"
aliases = []string{"rm", "delete"}
short = "Update a user's status to 'suspended'. A suspended user cannot log into the platform"
default:
panic(fmt.Sprintf("%s is not supported", sdkStatus))
Expand Down
2 changes: 1 addition & 1 deletion coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion coderd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ func (api *API) deleteUser(rw http.ResponseWriter, r *http.Request) {
// @Security CoderSessionToken
// @Produce json
// @Tags Users
// @Param user path string true "User ID, name, or me"
// @Param user path string true "User ID, username, or me"
// @Success 200 {object} codersdk.User
// @Router /users/{user} [get]
func (api *API) userByName(rw http.ResponseWriter, r *http.Request) {
Expand Down
6 changes: 3 additions & 3 deletions docs/api/users.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/cli/users.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions docs/cli/users_delete.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions docs/cli/users_suspend.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,11 @@
"title": "users create",
"path": "cli/users_create.md"
},
{
"title": "users delete",
"description": "Delete a user by username or user_id.",
"path": "cli/users_delete.md"
},
{
"title": "users list",
"path": "cli/users_list.md"
Expand Down