From 18543b38a22a0f65b3be16adb874f0f9cf1675d9 Mon Sep 17 00:00:00 2001 From: Nader Khalil Date: Tue, 19 Dec 2023 00:26:07 -0800 Subject: [PATCH] fu command --- pkg/cmd/cmd.go | 2 + pkg/cmd/fu/fu.go | 107 +++++++++++++++++++++++++++++++++++++++++ pkg/cmd/fu/fu_test.go | 1 + pkg/store/workspace.go | 29 +++++++++++ 4 files changed, 139 insertions(+) create mode 100644 pkg/cmd/fu/fu.go create mode 100644 pkg/cmd/fu/fu_test.go diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index b2a54c0d..fd5072dd 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -16,6 +16,7 @@ import ( "github.com/brevdev/brev-cli/pkg/cmd/delete" "github.com/brevdev/brev-cli/pkg/cmd/envsetup" "github.com/brevdev/brev-cli/pkg/cmd/envvars" + "github.com/brevdev/brev-cli/pkg/cmd/fu" "github.com/brevdev/brev-cli/pkg/cmd/healthcheck" "github.com/brevdev/brev-cli/pkg/cmd/hello" "github.com/brevdev/brev-cli/pkg/cmd/importideconfig" @@ -249,6 +250,7 @@ func createCmdTree(cmd *cobra.Command, t *terminal.Terminal, loginCmdStore *stor cmd.AddCommand(clipboard.ForwardPort(t, loginCmdStore)) cmd.AddCommand(envvars.NewCmdEnvVars(t, loginCmdStore)) cmd.AddCommand(connect.NewCmdConnect(t, noLoginCmdStore)) + cmd.AddCommand(fu.NewCmdFu(t, loginCmdStore, noLoginCmdStore)) } else { _ = 0 // noop } diff --git a/pkg/cmd/fu/fu.go b/pkg/cmd/fu/fu.go new file mode 100644 index 00000000..1f0d3657 --- /dev/null +++ b/pkg/cmd/fu/fu.go @@ -0,0 +1,107 @@ +package fu + +import ( + _ "embed" + "fmt" + "time" + + "github.com/brevdev/brev-cli/pkg/cmd/completions" + "github.com/brevdev/brev-cli/pkg/entity" + breverrors "github.com/brevdev/brev-cli/pkg/errors" + "github.com/brevdev/brev-cli/pkg/store" + "github.com/brevdev/brev-cli/pkg/terminal" + "github.com/hashicorp/go-multierror" + "github.com/spf13/cobra" + stripmd "github.com/writeas/go-strip-markdown" +) + +var ( + fuLong string + fuExample = "brev fu " +) + +type FuStore interface { + completions.CompletionStore + DeleteWorkspace(workspaceID string) (*entity.Workspace, error) + GetWorkspaces(organizationID string, options *store.GetWorkspacesOptions) ([]entity.Workspace, error) + BanUser(userID string) error + GetWorkspaceByNameOrID(orgID string, nameOrID string) ([]entity.Workspace, error) + GetAllOrgsAsAdmin(userID string) ([]entity.Organization, error) +} + +func NewCmdFu(t *terminal.Terminal, loginFuStore FuStore, noLoginFuStore FuStore) *cobra.Command { + cmd := &cobra.Command{ + Annotations: map[string]string{"workspace": ""}, + Use: "fu", + DisableFlagsInUseLine: true, + Short: "Fetch all workspaces for a user and delete them", + Long: stripmd.Strip(fuLong), + Example: fuExample, + ValidArgsFunction: completions.GetAllWorkspaceNameCompletionHandler(noLoginFuStore, t), + RunE: func(cmd *cobra.Command, args []string) error { + var allError error + for _, userID := range args { + err := fuUser(userID, t, loginFuStore) + if err != nil { + allError = multierror.Append(allError, err) + } + } + if allError != nil { + return breverrors.WrapAndTrace(allError) + } + return nil + }, + } + + return cmd +} + +func fuUser(userID string, t *terminal.Terminal, fuStore FuStore) error { + orgs, err := fuStore.GetAllOrgsAsAdmin(userID) + if err != nil { + return breverrors.WrapAndTrace(err) + } + + var allWorkspaces []entity.Workspace + for _, org := range orgs { + workspaces, err := fuStore.GetWorkspaces(org.ID, nil) + if err != nil { + return breverrors.WrapAndTrace(err) + } + allWorkspaces = append(allWorkspaces, workspaces...) + } + + s := t.NewSpinner() + s.Suffix = " Fetching workspaces for user " + userID + s.Start() + time.Sleep(5 * time.Second) + s.Stop() + + confirm := terminal.PromptGetInput(terminal.PromptContent{ + Label: fmt.Sprintf("Are you sure you want to delete all %d workspaces for user %s? (y/n)", len(allWorkspaces), userID), + ErrorMsg: "You must confirm to proceed.", + AllowEmpty: false, + }) + if confirm != "y" { + return nil + } + + for _, workspace := range allWorkspaces { + _, err2 := fuStore.DeleteWorkspace(workspace.ID) + if err2 != nil { + t.Vprintf(t.Red("Failed to delete workspace with ID: %s\n", workspace.ID)) + t.Vprintf(t.Red("Error: %s\n", err.Error())) + continue + } + t.Vprintf("✅ Deleted workspace %s\n", workspace.Name) + } + + err = fuStore.BanUser(userID) + if err != nil { + t.Vprintf(t.Red("Failed to ban user with ID: %s\n", userID)) + t.Vprintf(t.Red("Error: %s\n", err.Error())) + } + t.Vprintf("✅ Banned user %s\n", userID) + + return nil +} diff --git a/pkg/cmd/fu/fu_test.go b/pkg/cmd/fu/fu_test.go new file mode 100644 index 00000000..5818a26b --- /dev/null +++ b/pkg/cmd/fu/fu_test.go @@ -0,0 +1 @@ +package fu diff --git a/pkg/store/workspace.go b/pkg/store/workspace.go index e0050ac9..5cb3f31e 100644 --- a/pkg/store/workspace.go +++ b/pkg/store/workspace.go @@ -339,6 +339,35 @@ func (s AuthHTTPStore) DeleteWorkspace(workspaceID string) (*entity.Workspace, e return &result, nil } +func (s AuthHTTPStore) BanUser(userID string) error { + res, err := s.authHTTPClient.restyClient.R(). + SetHeader("Content-Type", "application/json"). + Put(fmt.Sprintf("/api/users/%s/block", userID)) + if err != nil { + return breverrors.WrapAndTrace(err) + } + if res.IsError() { + return NewHTTPResponseError(res) + } + return nil +} + +func (s AuthHTTPStore) GetAllOrgsAsAdmin(userID string) ([]entity.Organization, error) { + var result []entity.Organization + res, err := s.authHTTPClient.restyClient.R(). + SetHeader("Content-Type", "application/json"). + SetQueryParam("user_id", userID). + SetResult(&result). + Get("/api/organizations") + if err != nil { + return nil, breverrors.WrapAndTrace(err) + } + if res.IsError() { + return nil, NewHTTPResponseError(res) + } + return result, nil +} + var ( workspaceMetadataPathPattern = fmt.Sprintf("%s/metadata", workspacePathPattern) workspaceMetadataPath = fmt.Sprintf(workspaceMetadataPathPattern, fmt.Sprintf("{%s}", workspaceIDParamName))