From dba337f8da90f313d64ef61457b80b3d866b7065 Mon Sep 17 00:00:00 2001 From: Michael Christenson II Date: Tue, 5 Dec 2023 14:01:44 -0500 Subject: [PATCH] Dxcdt 586 select multiple ids for delete resource (#935) * Bump github.com/auth0/go-auth0 from 1.2.0 to 1.3.0 (#908) Bumps [github.com/auth0/go-auth0](https://github.com/auth0/go-auth0) from 1.2.0 to 1.3.0. - [Release notes](https://github.com/auth0/go-auth0/releases) - [Changelog](https://github.com/auth0/go-auth0/blob/main/CHANGELOG.md) - [Commits](https://github.com/auth0/go-auth0/compare/v1.2.0...v1.3.0) --- updated-dependencies: - dependency-name: github.com/auth0/go-auth0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * dxcdt-615-goreleaser-upgrade (#909) * Fix: Remove or replace deprecations for GoReleaser config Remove deprecated replacements Replace deprecated brews tap Replace deprecated scoop entries Fix name_template to reflect exact current naming conventions * DXCDT-582: Convert audience into a drop down in interactive mode in test token cmd (#906) Convert audience into a drop down in interactive mode in test token cmd * Bump github.com/auth0/go-auth0 from 1.2.0 to 1.3.0 (#908) Bumps [github.com/auth0/go-auth0](https://github.com/auth0/go-auth0) from 1.2.0 to 1.3.0. - [Release notes](https://github.com/auth0/go-auth0/releases) - [Changelog](https://github.com/auth0/go-auth0/blob/main/CHANGELOG.md) - [Commits](https://github.com/auth0/go-auth0/compare/v1.2.0...v1.3.0) --- updated-dependencies: - dependency-name: github.com/auth0/go-auth0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Co-authored-by: Sergiu Ghitea <28300158+sergiught@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * DXCDT-582: Convert audience into a drop down in interactive mode in test token cmd (#906) Convert audience into a drop down in interactive mode in test token cmd Title Os for final release name fit * Added batch deletes to actions * Add batch deletions Added batch deletion to actions command Added batch deletion to apis command Added batch deletion to apps command Added batch deletion to custom domains command Added batch deletion to log streams command Added batch deletion for organizations command Added batch deletion for roles command Added batch deletion for rules command Added batch deletion for users non-interactive command Added batch deletion for user blocks non-interactive command Update required golang minimum version * DXCDT-595: Add ability to update signing alg for apis (#926) * Add ability to update signing alg for apis * Removing short alias --------- Co-authored-by: Will Vedder * Bump github.com/auth0/go-auth0 from 1.3.0 to 1.3.1 (#929) * Updated docs * Simplify args loops * Updates for code review Ensure valid id before deletion process Normalize messages for deletion Remove unnecessary path escapes Continue on read failure instead of attempting delete Inform user of failed id on batch deletion Only document at most two ids likely * Validate input for askMultiSelect * Update docs * Remove unused variable * Use Batch deletion for integration test cleanup * Check to see if the tenant id is contained in the ids * Remove unused ids variable in warning * Fix print typo --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sergiu Ghitea <28300158+sergiught@users.noreply.github.com> Co-authored-by: Will Vedder --- docs/auth0_actions_delete.md | 2 + docs/auth0_apis_delete.md | 2 + docs/auth0_apps_delete.md | 2 + docs/auth0_domains_delete.md | 2 + docs/auth0_logs_streams_delete.md | 2 + docs/auth0_orgs_delete.md | 2 + docs/auth0_roles_delete.md | 2 + docs/auth0_rules_delete.md | 2 + docs/auth0_users_blocks_unblock.md | 2 + docs/auth0_users_delete.md | 2 + go.mod | 2 +- internal/cli/actions.go | 26 +-- internal/cli/apis.go | 34 ++-- internal/cli/apps.go | 38 +++-- internal/cli/arguments.go | 21 +++ internal/cli/custom_domains.go | 32 ++-- internal/cli/input.go | 14 ++ internal/cli/log_streams.go | 33 ++-- internal/cli/organizations.go | 33 ++-- internal/cli/picker_options.go | 12 ++ ..._option_test.go => picker_options_test.go} | 16 ++ internal/cli/roles.go | 35 ++-- internal/cli/rules.go | 39 +++-- internal/cli/users.go | 35 ++-- internal/cli/users_blocks.go | 40 +++-- internal/prompt/prompt.go | 11 ++ test/integration/scripts/test-cleanup.sh | 157 +++--------------- 27 files changed, 319 insertions(+), 279 deletions(-) rename internal/cli/{picker_option_test.go => picker_options_test.go} (65%) 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