diff --git a/docs/auth0_actions_delete.md b/docs/auth0_actions_delete.md index c7c9b2d49..753b2a538 100644 --- a/docs/auth0_actions_delete.md +++ b/docs/auth0_actions_delete.md @@ -23,6 +23,8 @@ auth0 actions delete [flags] auth0 actions rm auth0 actions delete auth0 actions delete --force + auth0 actions delete + auth0 actions delete --force ``` diff --git a/docs/auth0_apis_delete.md b/docs/auth0_apis_delete.md index 15df2480a..4777fb789 100644 --- a/docs/auth0_apis_delete.md +++ b/docs/auth0_apis_delete.md @@ -23,6 +23,8 @@ auth0 apis delete [flags] auth0 apis rm auth0 apis delete auth0 apis delete --force + auth0 apis delete + auth0 apis delete --force ``` diff --git a/docs/auth0_apps_delete.md b/docs/auth0_apps_delete.md index 3fe06b0ca..ba4058335 100644 --- a/docs/auth0_apps_delete.md +++ b/docs/auth0_apps_delete.md @@ -23,6 +23,8 @@ auth0 apps delete [flags] auth0 apps rm auth0 apps delete auth0 apps delete --force + auth0 apps delete + auth0 apps delete --force ``` diff --git a/docs/auth0_domains_delete.md b/docs/auth0_domains_delete.md index 13af4235b..6fd486dc2 100644 --- a/docs/auth0_domains_delete.md +++ b/docs/auth0_domains_delete.md @@ -23,6 +23,8 @@ auth0 domains delete [flags] auth0 domains rm auth0 domains delete auth0 domains delete --force + auth0 domains delete + auth0 domains delete --force ``` diff --git a/docs/auth0_logs_streams_delete.md b/docs/auth0_logs_streams_delete.md index 9e680b7bc..a2bae1d57 100644 --- a/docs/auth0_logs_streams_delete.md +++ b/docs/auth0_logs_streams_delete.md @@ -23,6 +23,8 @@ auth0 logs streams delete [flags] auth0 logs streams rm auth0 logs streams delete auth0 logs streams delete --force + auth0 logs streams delete + auth0 logs streams delete --force ``` diff --git a/docs/auth0_orgs_delete.md b/docs/auth0_orgs_delete.md index c65758374..547fd1004 100644 --- a/docs/auth0_orgs_delete.md +++ b/docs/auth0_orgs_delete.md @@ -23,6 +23,8 @@ auth0 orgs delete [flags] auth0 orgs rm auth0 orgs delete auth0 orgs delete --force + auth0 orgs delete + auth0 orgs delete --force ``` diff --git a/docs/auth0_roles_delete.md b/docs/auth0_roles_delete.md index e85284a9b..915279be8 100644 --- a/docs/auth0_roles_delete.md +++ b/docs/auth0_roles_delete.md @@ -23,6 +23,8 @@ auth0 roles delete [flags] auth0 roles rm auth0 roles delete auth0 roles delete --force + auth0 roles delete + auth0 roles delete --force ``` diff --git a/docs/auth0_rules_delete.md b/docs/auth0_rules_delete.md index f8bd8794a..cb72e58a0 100644 --- a/docs/auth0_rules_delete.md +++ b/docs/auth0_rules_delete.md @@ -25,6 +25,8 @@ auth0 rules delete [flags] auth0 rules rm auth0 rules delete auth0 rules delete --force + auth0 rules delete + auth0 rules delete --force ``` diff --git a/docs/auth0_users_blocks_unblock.md b/docs/auth0_users_blocks_unblock.md index c0d8d2201..f84002051 100644 --- a/docs/auth0_users_blocks_unblock.md +++ b/docs/auth0_users_blocks_unblock.md @@ -15,7 +15,9 @@ auth0 users blocks unblock [flags] ## Examples ``` + auth0 users blocks unblock auth0 users blocks unblock + auth0 users blocks unblock ``` diff --git a/docs/auth0_users_delete.md b/docs/auth0_users_delete.md index 9609609cd..aae614965 100644 --- a/docs/auth0_users_delete.md +++ b/docs/auth0_users_delete.md @@ -23,6 +23,8 @@ auth0 users delete [flags] auth0 users rm auth0 users delete auth0 users delete --force + auth0 users delete + auth0 users delete --force ``` diff --git a/go.mod b/go.mod index cd665b765..4b37d3512 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/auth0/auth0-cli -go 1.20 +go 1.21 require ( github.com/AlecAivazis/survey/v2 v2.3.7 diff --git a/internal/cli/actions.go b/internal/cli/actions.go index a73019cf4..5885c11ff 100644 --- a/internal/cli/actions.go +++ b/internal/cli/actions.go @@ -358,14 +358,9 @@ func updateActionCmd(cli *cli) *cobra.Command { } func deleteActionCmd(cli *cli) *cobra.Command { - var inputs struct { - ID string - } - cmd := &cobra.Command{ Use: "delete", Aliases: []string{"rm"}, - Args: cobra.MaximumNArgs(1), Short: "Delete an action", Long: "Delete an action.\n\n" + "To delete interactively, use `auth0 actions delete` with no arguments.\n\n" + @@ -373,14 +368,17 @@ func deleteActionCmd(cli *cli) *cobra.Command { Example: ` auth0 actions delete auth0 actions rm auth0 actions delete - auth0 actions delete --force`, + auth0 actions delete --force + auth0 actions delete + auth0 actions delete --force`, RunE: func(cmd *cobra.Command, args []string) error { + ids := make([]string, len(args)) if len(args) == 0 { - if err := actionID.Pick(cmd, &inputs.ID, cli.actionPickerOptions); err != nil { + if err := actionID.PickMany(cmd, &ids, cli.actionPickerOptions); err != nil { return err } } else { - inputs.ID = args[0] + ids = append(ids, args...) } if !cli.force && canPrompt(cmd) { @@ -389,8 +387,16 @@ func deleteActionCmd(cli *cli) *cobra.Command { } } - return ansi.Spinner("Deleting action", func() error { - return cli.api.Action.Delete(cmd.Context(), inputs.ID) + return ansi.Spinner("Deleting action(s)", func() error { + var errs []error + for _, id := range ids { + if id != "" { + if err := cli.api.Action.Delete(cmd.Context(), id); err != nil { + errs = append(errs, err) + } + } + } + return errors.Join(errs...) }) }, } diff --git a/internal/cli/apis.go b/internal/cli/apis.go index 135b699b3..d7aa66a5a 100644 --- a/internal/cli/apis.go +++ b/internal/cli/apis.go @@ -416,14 +416,9 @@ func updateAPICmd(cli *cli) *cobra.Command { } func deleteAPICmd(cli *cli) *cobra.Command { - var inputs struct { - ID string - } - cmd := &cobra.Command{ Use: "delete", Aliases: []string{"rm"}, - Args: cobra.MaximumNArgs(1), Short: "Delete an API", Long: "Delete an API.\n\n" + "To delete interactively, use `auth0 apis delete` with no arguments.\n\n" + @@ -431,15 +426,17 @@ func deleteAPICmd(cli *cli) *cobra.Command { Example: ` auth0 apis delete auth0 apis rm auth0 apis delete - auth0 apis delete --force`, + auth0 apis delete --force + auth0 apis delete + auth0 apis delete --force`, RunE: func(cmd *cobra.Command, args []string) error { + var ids []string if len(args) == 0 { - err := apiID.Pick(cmd, &inputs.ID, cli.apiPickerOptions) - if err != nil { + if err := apiID.PickMany(cmd, &ids, cli.apiPickerOptions); err != nil { return err } } else { - inputs.ID = args[0] + ids = append(ids, args...) } if !cli.force && canPrompt(cmd) { @@ -448,14 +445,21 @@ func deleteAPICmd(cli *cli) *cobra.Command { } } - return ansi.Spinner("Deleting API", func() error { - _, err := cli.api.ResourceServer.Read(cmd.Context(), url.PathEscape(inputs.ID)) + return ansi.Spinner("Deleting API(s)", func() error { + var errs []error + for _, id := range ids { + if id != "" { + if _, err := cli.api.ResourceServer.Read(cmd.Context(), url.PathEscape(id)); err != nil { + errs = append(errs, fmt.Errorf("Unable to delete API (%s): %w", id, err)) + continue + } - if err != nil { - return fmt.Errorf("Unable to delete API: %w", err) + if err := cli.api.ResourceServer.Delete(cmd.Context(), url.PathEscape(id)); err != nil { + errs = append(errs, fmt.Errorf("Unable to delete API (%s): %w", id, err)) + } + } } - - return cli.api.ResourceServer.Delete(cmd.Context(), url.PathEscape(inputs.ID)) + return errors.Join(errs...) }) }, } diff --git a/internal/cli/apps.go b/internal/cli/apps.go index c2fd01da8..d142affe2 100644 --- a/internal/cli/apps.go +++ b/internal/cli/apps.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "strings" "github.com/auth0/go-auth0/management" @@ -302,14 +303,9 @@ func showAppCmd(cli *cli) *cobra.Command { } func deleteAppCmd(cli *cli) *cobra.Command { - var inputs struct { - ID string - } - cmd := &cobra.Command{ Use: "delete", Aliases: []string{"rm"}, - Args: cobra.MaximumNArgs(1), Short: "Delete an application", Long: "Delete an application.\n\n" + "To delete interactively, use `auth0 apps delete` with no arguments.\n\n" + @@ -318,34 +314,44 @@ func deleteAppCmd(cli *cli) *cobra.Command { Example: ` auth0 apps delete auth0 apps rm auth0 apps delete - auth0 apps delete --force`, + auth0 apps delete --force + auth0 apps delete + auth0 apps delete --force`, RunE: func(cmd *cobra.Command, args []string) error { + ids := make([]string, len(args)) if len(args) == 0 { - err := appID.Pick(cmd, &inputs.ID, cli.appPickerOptions()) + err := appID.PickMany(cmd, &ids, cli.appPickerOptions()) if err != nil { return err } } else { - inputs.ID = args[0] + ids = append(ids, args...) } if !cli.force && canPrompt(cmd) { - if tenant, _ := cli.Config.GetTenant(cli.tenant); tenant.ClientID == inputs.ID { - cli.renderer.Warnf("Warning: You're about to delete the client used to authenticate the CLI. If deleted, the CLI will cease to operate once the access token has expired.", inputs.ID) + if tenant, _ := cli.Config.GetTenant(cli.tenant); slices.Contains(ids, tenant.ClientID) { + cli.renderer.Warnf("Warning: You're about to delete the client used to authenticate the CLI. If deleted, the CLI will cease to operate once the access token has expired.") } if confirmed := prompt.Confirm("Are you sure you want to proceed?"); !confirmed { return nil } } - return ansi.Spinner("Deleting Application", func() error { - _, err := cli.api.Client.Read(cmd.Context(), inputs.ID) + return ansi.Spinner("Deleting Application(s)", func() error { + var errs []error + for _, id := range ids { + if id != "" { + if _, err := cli.api.Client.Read(cmd.Context(), id); err != nil { + errs = append(errs, fmt.Errorf("Unable to delete application (%s): %w", id, err)) + continue + } - if err != nil { - return fmt.Errorf("Unable to delete application: %w", err) + if err := cli.api.Client.Delete(cmd.Context(), id); err != nil { + errs = append(errs, fmt.Errorf("Unable to delete application (%s): %w", id, err)) + } + } } - - return cli.api.Client.Delete(cmd.Context(), inputs.ID) + return errors.Join(errs...) }) }, } diff --git a/internal/cli/arguments.go b/internal/cli/arguments.go index a3f04ec39..5e1ea559d 100644 --- a/internal/cli/arguments.go +++ b/internal/cli/arguments.go @@ -58,6 +58,27 @@ func (a *Argument) Pick(cmd *cobra.Command, result *string, fn pickerOptionsFunc return nil } +func (a *Argument) PickMany(cmd *cobra.Command, result *[]string, fn pickerOptionsFunc) error { + var opts pickerOptions + err := ansi.Waiting(func() error { + var err error + opts, err = fn(cmd.Context()) + return err + }) + + if err != nil { + return err + } + + var values []string + if err := askMultiSelect(a, &values, opts.labels()...); err != nil { + return err + } + + *result = opts.getValues(values...) + return nil +} + func selectArgument(cmd *cobra.Command, a *Argument, value interface{}, options []string, defaultValue *string) error { if canPrompt(cmd) { return _select(a, value, options, defaultValue, false) diff --git a/internal/cli/custom_domains.go b/internal/cli/custom_domains.go index f651204c6..e36e234cc 100644 --- a/internal/cli/custom_domains.go +++ b/internal/cli/custom_domains.go @@ -2,6 +2,7 @@ package cli import ( "context" + "errors" "fmt" "net/url" @@ -323,14 +324,9 @@ func updateCustomDomainCmd(cli *cli) *cobra.Command { } func deleteCustomDomainCmd(cli *cli) *cobra.Command { - var inputs struct { - ID string - } - cmd := &cobra.Command{ Use: "delete", Aliases: []string{"rm"}, - Args: cobra.MaximumNArgs(1), Short: "Delete a custom domain", Long: "Delete a custom domain.\n\n" + "To delete interactively, use `auth0 domains delete` with no arguments.\n\n" + @@ -339,15 +335,18 @@ func deleteCustomDomainCmd(cli *cli) *cobra.Command { Example: ` auth0 domains delete auth0 domains rm auth0 domains delete - auth0 domains delete --force`, + auth0 domains delete --force + auth0 domains delete + auth0 domains delete --force`, RunE: func(cmd *cobra.Command, args []string) error { + ids := make([]string, len(args)) if len(args) == 0 { - err := customDomainID.Pick(cmd, &inputs.ID, cli.customDomainsPickerOptions) + err := customDomainID.PickMany(cmd, &ids, cli.customDomainsPickerOptions) if err != nil { return err } } else { - inputs.ID = args[0] + ids = append(ids, args...) } if !cli.force && canPrompt(cmd) { @@ -357,13 +356,20 @@ func deleteCustomDomainCmd(cli *cli) *cobra.Command { } return ansi.Spinner("Deleting custom domain", func() error { - _, err := cli.api.CustomDomain.Read(cmd.Context(), url.PathEscape(inputs.ID)) - - if err != nil { - return fmt.Errorf("Unable to delete custom domain: %w", err) + var errs []error + for _, id := range ids { + if id != "" { + if _, err := cli.api.CustomDomain.Read(cmd.Context(), url.PathEscape(id)); err != nil { + return fmt.Errorf("Unable to delete custom domain (%s): %w", id, err) + } + + if err := cli.api.CustomDomain.Delete(cmd.Context(), url.PathEscape(id)); err != nil { + return fmt.Errorf("Unable to delete custom domain (%s): %w", id, err) + } + } } - return cli.api.CustomDomain.Delete(cmd.Context(), url.PathEscape(inputs.ID)) + return errors.Join(errs...) }) }, } diff --git a/internal/cli/input.go b/internal/cli/input.go index 32d9d7ac6..f861dcbdb 100644 --- a/internal/cli/input.go +++ b/internal/cli/input.go @@ -3,6 +3,7 @@ package cli import ( "fmt" "os" + "reflect" "github.com/AlecAivazis/survey/v2/terminal" "github.com/auth0/go-auth0" @@ -58,6 +59,19 @@ func askPassword(i commandInput, value interface{}, isUpdate bool) error { return nil } +func askMultiSelect(i commandInput, value interface{}, options ...string) error { + v := reflect.ValueOf(value) + if v.Kind() != reflect.Slice || v.Len() <= 0 { + handleInputError(fmt.Errorf("there is not enough data to select from")) + } + + if err := prompt.AskMultiSelect(i.GetLabel(), value, options...); err != nil { + handleInputError(err) + } + + return nil +} + func _select(i commandInput, value interface{}, options []string, defaultValue *string, isUpdate bool) error { isRequired := isInputRequired(i, isUpdate) diff --git a/internal/cli/log_streams.go b/internal/cli/log_streams.go index 64d671016..d33e92247 100644 --- a/internal/cli/log_streams.go +++ b/internal/cli/log_streams.go @@ -168,14 +168,9 @@ func updateLogStreamCmd(cli *cli) *cobra.Command { } func deleteLogStreamCmd(cli *cli) *cobra.Command { - var inputs struct { - ID string - } - cmd := &cobra.Command{ Use: "delete", Aliases: []string{"rm"}, - Args: cobra.MaximumNArgs(1), Short: "Delete a log stream", Long: "Delete a log stream.\n\n" + "To delete interactively, use `auth0 logs streams delete` with no arguments.\n\n" + @@ -184,15 +179,18 @@ func deleteLogStreamCmd(cli *cli) *cobra.Command { Example: ` auth0 logs streams delete auth0 logs streams rm auth0 logs streams delete - auth0 logs streams delete --force`, + auth0 logs streams delete --force + auth0 logs streams delete + auth0 logs streams delete --force`, RunE: func(cmd *cobra.Command, args []string) error { + ids := make([]string, len(args)) if len(args) == 0 { - err := logStreamID.Pick(cmd, &inputs.ID, cli.allLogStreamsPickerOptions) + err := logStreamID.PickMany(cmd, &ids, cli.allLogStreamsPickerOptions) if err != nil { return err } } else { - inputs.ID = args[0] + ids = append(ids, args...) } if !cli.force && canPrompt(cmd) { @@ -201,14 +199,21 @@ func deleteLogStreamCmd(cli *cli) *cobra.Command { } } - return ansi.Spinner("Deleting Log Stream", func() error { - _, err := cli.api.LogStream.Read(cmd.Context(), inputs.ID) - - if err != nil { - return fmt.Errorf("Unable to delete log stream: %w", err) + return ansi.Spinner("Deleting Log Stream(s)", func() error { + var errs []error + for _, id := range ids { + if id != "" { + if _, err := cli.api.LogStream.Read(cmd.Context(), id); err != nil { + errs = append(errs, fmt.Errorf("Unable to delete log stream (%s): %w", id, err)) + continue + } + if err := cli.api.LogStream.Delete(cmd.Context(), id); err != nil { + errs = append(errs, fmt.Errorf("Unable to delete log stream (%s): %w", id, err)) + } + } } - return cli.api.LogStream.Delete(cmd.Context(), inputs.ID) + return errors.Join(errs...) }) }, } diff --git a/internal/cli/organizations.go b/internal/cli/organizations.go index ba5192eb3..b39e7e5e2 100644 --- a/internal/cli/organizations.go +++ b/internal/cli/organizations.go @@ -408,14 +408,9 @@ func updateOrganizationCmd(cli *cli) *cobra.Command { } func deleteOrganizationCmd(cli *cli) *cobra.Command { - var inputs struct { - ID string - } - cmd := &cobra.Command{ Use: "delete", Aliases: []string{"rm"}, - Args: cobra.MaximumNArgs(1), Short: "Delete an organization", Long: "Delete an organization.\n\n" + "To delete interactively, use `auth0 orgs delete` with no arguments.\n\n" + @@ -424,15 +419,18 @@ func deleteOrganizationCmd(cli *cli) *cobra.Command { Example: ` auth0 orgs delete auth0 orgs rm auth0 orgs delete - auth0 orgs delete --force`, + auth0 orgs delete --force + auth0 orgs delete + auth0 orgs delete --force`, RunE: func(cmd *cobra.Command, args []string) error { + ids := make([]string, len(args)) if len(args) == 0 { - err := organizationID.Pick(cmd, &inputs.ID, cli.organizationPickerOptions) + err := organizationID.PickMany(cmd, &ids, cli.organizationPickerOptions) if err != nil { return err } } else { - inputs.ID = args[0] + ids = append(ids, args...) } if !cli.force && canPrompt(cmd) { @@ -441,14 +439,21 @@ func deleteOrganizationCmd(cli *cli) *cobra.Command { } } - return ansi.Spinner("Deleting organization", func() error { - _, err := cli.api.Organization.Read(cmd.Context(), url.PathEscape(inputs.ID)) + return ansi.Spinner("Deleting organization(s)", func() error { + var errs []error + for _, id := range ids { + if id != "" { + if _, err := cli.api.Organization.Read(cmd.Context(), id); err != nil { + errs = append(errs, fmt.Errorf("Unable to delete organization (%s): %w", id, err)) + continue + } - if err != nil { - return fmt.Errorf("Unable to delete organization: %w", err) + if err := cli.api.Organization.Delete(cmd.Context(), id); err != nil { + errs = append(errs, fmt.Errorf("Unable to delete organization (%s): %w", id, err)) + } + } } - - return cli.api.Organization.Delete(cmd.Context(), url.PathEscape(inputs.ID)) + return errors.Join(errs...) }) }, } diff --git a/internal/cli/picker_options.go b/internal/cli/picker_options.go index e73d431ad..4feef793e 100644 --- a/internal/cli/picker_options.go +++ b/internal/cli/picker_options.go @@ -1,5 +1,7 @@ package cli +import "slices" + type pickerOptions []pickerOption func (p pickerOptions) labels() []string { @@ -26,6 +28,16 @@ func (p pickerOptions) getValue(label string) string { return "" } +func (p pickerOptions) getValues(labels ...string) []string { + var values []string + for _, o := range p { + if i := slices.Index(labels, o.label); i > -1 { + values = append(values, o.value) + } + } + return values +} + type pickerOption struct { label string value string diff --git a/internal/cli/picker_option_test.go b/internal/cli/picker_options_test.go similarity index 65% rename from internal/cli/picker_option_test.go rename to internal/cli/picker_options_test.go index 93b0e0068..01c200196 100644 --- a/internal/cli/picker_option_test.go +++ b/internal/cli/picker_options_test.go @@ -46,4 +46,20 @@ func TestPickerOptions(t *testing.T) { assert.Equal(t, want, got) }) + + t.Run("return the values for a given set of labels", func(t *testing.T) { + options := pickerOptions{pickerOption{label: "Foo", value: "0"}, pickerOption{label: "Bar", value: "1"}, pickerOption{label: "Baz", value: "2"}} + want := []string{"0", "2"} + got := options.getValues("Foo", "Baz") + + assert.Equal(t, want, got) + }) + + t.Run("returns empty values for a given set of non-existant labels", func(t *testing.T) { + options := pickerOptions{pickerOption{label: "Foo", value: "0"}, pickerOption{label: "Bar", value: "1"}, pickerOption{label: "Baz", value: "2"}} + want := []string(nil) + got := options.getValues("Buz") + + assert.Equal(t, want, got) + }) } diff --git a/internal/cli/roles.go b/internal/cli/roles.go index 96e25e420..e63091e55 100644 --- a/internal/cli/roles.go +++ b/internal/cli/roles.go @@ -282,14 +282,9 @@ func updateRoleCmd(cli *cli) *cobra.Command { } func deleteRoleCmd(cli *cli) *cobra.Command { - var inputs struct { - ID string - } - cmd := &cobra.Command{ Use: "delete", Aliases: []string{"rm"}, - Args: cobra.MaximumNArgs(1), Short: "Delete a role", Long: "Delete a role.\n\n" + "To delete interactively, use `auth0 roles delete`.\n\n" + @@ -297,15 +292,18 @@ func deleteRoleCmd(cli *cli) *cobra.Command { Example: ` auth0 roles delete auth0 roles rm auth0 roles delete - auth0 roles delete --force`, + auth0 roles delete --force + auth0 roles delete + auth0 roles delete --force`, RunE: func(cmd *cobra.Command, args []string) error { + ids := make([]string, len(args)) if len(args) == 0 { - err := roleID.Pick(cmd, &inputs.ID, cli.rolePickerOptions) + err := roleID.PickMany(cmd, &ids, cli.rolePickerOptions) if err != nil { return err } } else { - inputs.ID = args[0] + ids = append(ids, args...) } if !cli.force && canPrompt(cmd) { @@ -314,14 +312,21 @@ func deleteRoleCmd(cli *cli) *cobra.Command { } } - return ansi.Spinner("Deleting Role", func() error { - _, err := cli.api.Role.Read(cmd.Context(), inputs.ID) - - if err != nil { - return fmt.Errorf("Unable to delete role: %w", err) + return ansi.Spinner("Deleting Role(s)", func() error { + var errs []error + for _, id := range ids { + if id != "" { + if _, err := cli.api.Role.Read(cmd.Context(), id); err != nil { + errs = append(errs, fmt.Errorf("Unable to delete role (%s): %w", id, err)) + continue + } + + if err := cli.api.Role.Delete(cmd.Context(), id); err != nil { + errs = append(errs, fmt.Errorf("Unable to delete role (%s): %w", id, err)) + } + } } - - return cli.api.Role.Delete(cmd.Context(), inputs.ID) + return errors.Join(errs...) }) }, } diff --git a/internal/cli/rules.go b/internal/cli/rules.go index ac2f9d20b..5a317d2cb 100644 --- a/internal/cli/rules.go +++ b/internal/cli/rules.go @@ -249,14 +249,9 @@ func showRuleCmd(cli *cli) *cobra.Command { } func deleteRuleCmd(cli *cli) *cobra.Command { - var inputs struct { - ID string - } - cmd := &cobra.Command{ Use: "delete", Aliases: []string{"rm"}, - Args: cobra.MaximumNArgs(1), Short: "Delete a rule", Long: rulesDeprecationDocumentationText + "Delete a rule.\n\n" + "To delete interactively, use `auth0 rules delete` with no arguments.\n\n" + @@ -264,15 +259,18 @@ func deleteRuleCmd(cli *cli) *cobra.Command { Example: ` auth0 rules delete auth0 rules rm auth0 rules delete - auth0 rules delete --force`, + auth0 rules delete --force + auth0 rules delete + auth0 rules delete --force`, RunE: func(cmd *cobra.Command, args []string) error { - if len(args) > 0 { - inputs.ID = args[0] - } else { - err := ruleID.Pick(cmd, &inputs.ID, cli.rulePickerOptions) + ids := make([]string, len(args)) + if len(args) == 0 { + err := ruleID.PickMany(cmd, &ids, cli.rulePickerOptions) if err != nil { return err } + } else { + ids = append(ids, args...) } if !cli.force && canPrompt(cmd) { @@ -281,14 +279,21 @@ func deleteRuleCmd(cli *cli) *cobra.Command { } } - return ansi.Spinner("Deleting Rule", func() error { - _, err := cli.api.Rule.Read(cmd.Context(), inputs.ID) - - if err != nil { - return fmt.Errorf("Unable to delete rule: %w", err) + return ansi.Spinner("Deleting Rule(s)", func() error { + var errs []error + for _, id := range ids { + if id != "" { + if _, err := cli.api.Rule.Read(cmd.Context(), id); err != nil { + errs = append(errs, fmt.Errorf("Unable to delete rule (%s): %w", id, err)) + continue + } + + if err := cli.api.Rule.Delete(cmd.Context(), id); err != nil { + errs = append(errs, fmt.Errorf("Unable to delete rule (%s): %w", id, err)) + } + } } - - return cli.api.Rule.Delete(cmd.Context(), inputs.ID) + return errors.Join(errs...) }) }, } diff --git a/internal/cli/users.go b/internal/cli/users.go index 1e2fad6b6..2f458a2be 100644 --- a/internal/cli/users.go +++ b/internal/cli/users.go @@ -357,14 +357,9 @@ func showUserCmd(cli *cli) *cobra.Command { } func deleteUserCmd(cli *cli) *cobra.Command { - var inputs struct { - ID string - } - cmd := &cobra.Command{ Use: "delete", Aliases: []string{"rm"}, - Args: cobra.MaximumNArgs(1), Short: "Delete a user", Long: "Delete a user.\n\n" + "To delete interactively, use `auth0 users delete` with no arguments.\n\n" + @@ -372,14 +367,19 @@ func deleteUserCmd(cli *cli) *cobra.Command { Example: ` auth0 users delete auth0 users rm auth0 users delete - auth0 users delete --force`, + auth0 users delete --force + auth0 users delete + auth0 users delete --force`, RunE: func(cmd *cobra.Command, args []string) error { + ids := make([]string, len(args)) if len(args) == 0 { - if err := userID.Ask(cmd, &inputs.ID); err != nil { + var id string + if err := userID.Ask(cmd, &id); err != nil { return err } + ids = append(ids, id) } else { - inputs.ID = args[0] + ids = append(ids, args...) } if !cli.force && canPrompt(cmd) { @@ -388,14 +388,21 @@ func deleteUserCmd(cli *cli) *cobra.Command { } } - return ansi.Spinner("Deleting user", func() error { - _, err := cli.api.User.Read(cmd.Context(), inputs.ID) + return ansi.Spinner("Deleting user(s)", func() error { + var errs []error + for _, id := range ids { + if id != "" { + if _, err := cli.api.User.Read(cmd.Context(), id); err != nil { + errs = append(errs, fmt.Errorf("Unable to delete user (%s): %w", id, err)) + continue + } - if err != nil { - return fmt.Errorf("Unable to delete user: %w", err) + if err := cli.api.User.Delete(cmd.Context(), id); err != nil { + errs = append(errs, fmt.Errorf("Unable to delete user (%s): %w", id, err)) + } + } } - - return cli.api.User.Delete(cmd.Context(), inputs.ID) + return errors.Join(errs...) }) }, } diff --git a/internal/cli/users_blocks.go b/internal/cli/users_blocks.go index 657a930cf..223452812 100644 --- a/internal/cli/users_blocks.go +++ b/internal/cli/users_blocks.go @@ -1,6 +1,7 @@ package cli import ( + "errors" "fmt" "github.com/auth0/go-auth0/management" @@ -64,33 +65,36 @@ func listUserBlocksCmd(cli *cli) *cobra.Command { } func deleteUserBlocksCmd(cli *cli) *cobra.Command { - var inputs struct { - userID string - } - cmd := &cobra.Command{ - Use: "unblock", - Args: cobra.MaximumNArgs(1), - Short: "Remove brute-force protection blocks for a given user", - Long: "Remove brute-force protection blocks for a given user.", - Example: ` auth0 users blocks unblock `, + Use: "unblock", + Short: "Remove brute-force protection blocks for a given user", + Long: "Remove brute-force protection blocks for a given user.", + Example: ` auth0 users blocks unblock + auth0 users blocks unblock + auth0 users blocks unblock `, RunE: func(cmd *cobra.Command, args []string) error { + ids := make([]string, len(args)) if len(args) == 0 { - if err := userID.Ask(cmd, &inputs.userID); err != nil { + var id string + if err := userID.Ask(cmd, &id); err != nil { return err } + ids = append(ids, id) } else { - inputs.userID = args[0] + ids = append(ids, args...) } - err := ansi.Spinner("Unblocking user...", func() error { - return cli.api.User.Unblock(cmd.Context(), inputs.userID) + return ansi.Spinner("Unblocking user(s)...", func() error { + var errs []error + for _, id := range ids { + if id != "" { + if err := cli.api.User.Unblock(cmd.Context(), id); err != nil { + errs = append(errs, fmt.Errorf("failed to unblock user with ID %s: %w", id, err)) + } + } + } + return errors.Join(errs...) }) - if err != nil { - return fmt.Errorf("failed to unblock user with ID %s: %w", inputs.userID, err) - } - - return nil }, } diff --git a/internal/prompt/prompt.go b/internal/prompt/prompt.go index 45ae3936a..2d2598962 100644 --- a/internal/prompt/prompt.go +++ b/internal/prompt/prompt.go @@ -24,6 +24,17 @@ func askOne(prompt survey.Prompt, response interface{}) error { return survey.AskOne(prompt, response, stdErrWriter, Icons) } +func AskMultiSelect(message string, response interface{}, options ...string) error { + prompt := &survey.MultiSelect{ + Message: message, + Options: options, + } + + err := askOne(prompt, response) + + return err +} + func AskBool(message string, value *bool, defaultValue bool) error { prompt := &survey.Confirm{ Message: message, diff --git a/test/integration/scripts/test-cleanup.sh b/test/integration/scripts/test-cleanup.sh index da5a09913..9b51d685b 100755 --- a/test/integration/scripts/test-cleanup.sh +++ b/test/integration/scripts/test-cleanup.sh @@ -1,142 +1,35 @@ #! /bin/bash -apps=$( auth0 apps list --json --no-input ) +function delete_resources { + local cmd=${1-} name=${2-} field=${3-} -for app in $( echo "${apps}" | jq -r '.[] | @base64' ); do - _jq() { - echo "${app}" | base64 --decode | jq -r "${1}" - } + printf "\r\nGathering resource %ss for %s...\r\n" ${field} ${cmd} + resources=$( auth0 ${cmd} list --json --no-input | jq -r ".[] | select(.name | test(\"^${name}.+\")) | .${field}" | tr '\n' ' ' ) - clientid=$(_jq '.client_id') - name=$(_jq '.name') - # TODO(jfatta): should remove only those - # created during the same test session - if [[ $name = integration-test-app-* ]] - then - echo deleting "$name" - $( auth0 apps delete "$clientid") - fi -done + if [[ $resources ]] + then + echo "Deleting resources for ${cmd}... ${field} (e.g. IDs): ${resources}" + auth0 ${cmd} delete --force ${resources[*]} + fi +} -apis=$( auth0 apis list --json --no-input ) - -for api in $( echo "${apis}" | jq -r '.[] | @base64' ); do - _jq() { - echo "${api}" | base64 --decode | jq -r "${1}" - } - - id=$(_jq '.id') - name=$(_jq '.name') - # TODO(jfatta): should remove only those - # created during the same test session - if [[ $name = integration-test-api-* ]] - then - echo deleting "$name" - $( auth0 apis delete "$id") - fi -done +delete_resources "apps" "integration-test-app" "client_id" +delete_resources "apis" "integration-test-api" "id" +printf "\r\nGathering resource user_ids for users...\r\n" # using the search command since users have no list command -users=$( auth0 users search -q "*" --json --no-input ) - -for user in $( echo "${users}" | jq -r '.[] | @base64' ); do - _jq() { - echo "${user}" | base64 --decode | jq -r "${1}" - } - - userid=$(_jq '.user_id') - # created during the same test session - if [[ integration-* ]] - then - echo deleting "$userid" - $( auth0 users delete "$userid") - fi -done - -roles=$( auth0 roles list --json --no-input ) - -for role in $( echo "${roles}" | jq -r '.[] | @base64' ); do - _jq() { - echo "${role}" | base64 --decode | jq -r "${1}" - } - - id=$(_jq '.id') - name=$(_jq '.name') - # TODO(jfatta): should remove only those - # created during the same test session - if [[ $name = integration-test-role-* ]] - then - echo deleting "$name" - $( auth0 roles delete "$id") - fi -done - -rules=$( auth0 rules list --json --no-input ) - -for rule in $( printf "%s" "$rules" | jq -r '.[] | @base64' ); do - _jq() { - echo "${rule}" | base64 --decode | jq -r "${1}" - } - - id=$(_jq '.id') - name=$(_jq '.name') - # TODO(jfatta): should remove only those - # created during the same test session - if [[ $name = integration-test-rule-* ]] - then - echo deleting "$name" - $( auth0 rules delete "$id") - fi -done - -orgs=$( auth0 orgs list --json --no-input ) - -for org in $( echo "${orgs}" | jq -r '.[] | @base64' ); do - _jq() { - echo "${org}" | base64 --decode | jq -r "${1}" - } - - id=$(_jq '.id') - name=$(_jq '.name') - if [[ $name = integration-test-org-* ]] - then - echo deleting "$name" - $( auth0 orgs delete "$id") - fi -done - -actions=$( auth0 actions list --json --no-input ) - -for action in $( echo "${actions}" | jq -r '.[] | @base64' ); do - _jq() { - echo "${action}" | base64 --decode | jq -r "${1}" - } - - id=$(_jq '.id') - name=$(_jq '.name') - - if [[ $name = integration-test-* ]] - then - echo deleting "$name" - $( auth0 actions delete "$id") - fi -done - -logStreams=$( auth0 logs streams list --json ) -for logStream in $( echo "${logStreams}" | jq -r '.[] | @base64' ); do - _jq() { - echo "${logStream}" | base64 --decode | jq -r "${1}" - } - - id=$(_jq '.id') - name=$(_jq '.name') - - if [[ $name = integration-test-* ]] - then - echo deleting "$name" - $( auth0 logs streams delete "$id") - fi -done +users=$( auth0 users search -q "*" --json --no-input | jq -r '.[] | select(.name | test("^integration-.+")) | .user_id' | tr '\n' ' ' ) +if [[ $users ]] +then + echo "Deleting resources for users... id (e.g. IDs): ${users}" + auth0 users delete --force ${users[*]} +fi + +delete_resources "roles" "integration-test-role" "id" +delete_resources "rules" "integration-test-rule" "id" +delete_resources "orgs" "integration-test-org" "id" +delete_resources "actions" "integration-test-" "id" +delete_resources "logs streams" "integration-test-" "id" auth0 domains delete $(./test/integration/scripts/get-custom-domain-id.sh) --no-input