diff --git a/CHANGELOG.md b/CHANGELOG.md index 42029dafa7..d157b64478 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ Versioning follows [SemVer](https://semver.org/). Sections: **Added**, **Changed - `server create --confidential` creates a Confidential VM from a confidential boot image. Requires `--type ENTERPRISE` and `--image-id` (a private, SEV-SNP image; `--image-id` tab-completion is filtered to these). A boot volume is built from the image and attached in the same request (size it with `--size`/`--storage-type`), so the API can derive `cores` and `cpuFamily` from the image's `launch-config.json` — `--cores` and `--cpu-family` must not be set. - `RequiredFeatures` and `EnabledFeatures` columns. +### Changed +- Consistent `delete --all` across all resources: Bulk deletion now prints a preview of every resource that will be deleted (with identifying details such as name, ID, public IP, location, and description), confirms per item, reports per-item success, and ends with a `deleted / skipped / failed` summary instead of staying silent unless an error occurred. On regional APIs the preview and deletion span all locations by default (use `--location` to target one). This also fixes a bug where when deleting certain resources and answering 'no' for only one of them, all the remaining resources would be skipped. + +### Fixed +- `dbaas mariadb cluster delete --all --name` and `dbaas mongo cluster delete --all --name` now filter by the given name. The `--name` flag was previously registered as a boolean and could not accept a value, so the filter never applied. +- `vpn wireguard peer delete` no longer reports success when the deletion fails. The single-peer path was discarding the API error and always exiting 0. + ## [v6.10.2] - June 2026 ### Added @@ -48,7 +55,6 @@ Versioning follows [SemVer](https://semver.org/). Sections: **Added**, **Changed - Colored syntax highlighting - Prevent 'ionosctl shell' from being called inside another 'ionosctl shell' instance. - ## [v6.9.9] – April 2026 ### Changed diff --git a/commands/cdn/distribution/delete.go b/commands/cdn/distribution/delete.go index f7d62e6c0a..964d6c3f1b 100644 --- a/commands/cdn/distribution/delete.go +++ b/commands/cdn/distribution/delete.go @@ -8,7 +8,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/client" "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" "github.com/ionos-cloud/sdk-go-bundle/products/cdn/v2" "github.com/spf13/viper" @@ -58,13 +57,23 @@ func Delete() *core.Command { } func deleteAll(c *core.CommandConfig) error { - records, err := completer.Distributions() - if err != nil { - return fmt.Errorf("failed getting all distributions: %w", err) - } - - return functional.ApplyAndAggregateErrors(records.GetItems(), func(d cdn.Distribution) error { - return deleteSingle(c, d.Id) + return core.DeleteAll(c, core.DeleteAllOptions[cdn.Distribution]{ + Resource: "distribution", + List: func() ([]cdn.Distribution, error) { + records, err := completer.Distributions() + if err != nil { + return nil, fmt.Errorf("failed getting all distributions: %w", err) + } + return records.GetItems(), nil + }, + Summary: func(d cdn.Distribution) string { + return fmt.Sprintf("%s (domain: %s)", d.Id, d.Properties.Domain) + }, + ID: func(d cdn.Distribution) string { return d.Id }, + Delete: func(d cdn.Distribution) error { + _, err := client.Must().CDNClient.DistributionsApi.DistributionsDelete(context.Background(), d.Id).Execute() + return err + }, }) } diff --git a/commands/cert/autocertificate/delete.go b/commands/cert/autocertificate/delete.go index 0f45e2c122..65c179e41d 100644 --- a/commands/cert/autocertificate/delete.go +++ b/commands/cert/autocertificate/delete.go @@ -8,7 +8,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" "github.com/ionos-cloud/sdk-go-bundle/products/cert/v2" "github.com/spf13/viper" ) @@ -65,22 +64,24 @@ func AutocertificateDeleteCmd() *core.Command { } func deleteAll(c *core.CommandConfig) error { - c.Verbose("Deleting all AutoCertificates!") - xs, _, err := client.Must().CertManagerClient.AutoCertificateApi.AutoCertificatesGet(context.Background()).Execute() - if err != nil { - return fmt.Errorf("failed getting the AutoCertificates: %w", err) - } - err = functional.ApplyAndAggregateErrors(xs.GetItems(), func(z cert.AutoCertificateRead) error { - yes := confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Are you sure you want to delete AutoCertificate with name: %s, id: %s ", z.Properties.Name, z.Id), - viper.GetBool(constants.ArgForce)) - if yes { - _, delErr := client.Must().CertManagerClient.AutoCertificateApi.AutoCertificatesDelete(context.Background(), z.Id).Execute() - if delErr != nil { - return fmt.Errorf("failed deleting %s (name: %s): %w", z.Id, z.Properties.Name, delErr) + return core.DeleteAll(c, core.DeleteAllOptions[cert.AutoCertificateRead]{ + Resource: "AutoCertificate", + List: func() ([]cert.AutoCertificateRead, error) { + xs, _, err := client.Must().CertManagerClient.AutoCertificateApi.AutoCertificatesGet(context.Background()).Execute() + if err != nil { + return nil, fmt.Errorf("failed getting the AutoCertificates: %w", err) } - } - return nil + return xs.GetItems(), nil + }, + Summary: func(z cert.AutoCertificateRead) string { + return fmt.Sprintf("name: %s, id: %s", z.Properties.Name, z.Id) + }, + ID: func(z cert.AutoCertificateRead) string { + return z.Id + }, + Delete: func(z cert.AutoCertificateRead) error { + _, err := client.Must().CertManagerClient.AutoCertificateApi.AutoCertificatesDelete(context.Background(), z.Id).Execute() + return err + }, }) - - return err } diff --git a/commands/cert/certificate/delete.go b/commands/cert/certificate/delete.go index 9e82012058..6553171174 100644 --- a/commands/cert/certificate/delete.go +++ b/commands/cert/certificate/delete.go @@ -8,6 +8,7 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" + "github.com/ionos-cloud/sdk-go-bundle/products/cert/v2" "github.com/spf13/viper" ) @@ -40,32 +41,32 @@ func CertDeleteCmd() *core.Command { } func CmdDelete(c *core.CommandConfig) error { - var err error - allFlag, err := c.Command.Command.Flags().GetBool(constants.ArgAll) if err != nil { return err } if allFlag { - c.Verbose("Deleting all Certificates...") - - certs, _, err := client.Must().CertManagerClient.CertificateApi.CertificatesGet(context.Background()).Execute() - if err != nil { - return err - } - - for _, cert := range certs.Items { - msg := fmt.Sprintf("delete Certificate ID: %s", cert.Id) - if !confirm.FAsk(c.Command.Command.InOrStdin(), msg, viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - _, err = client.Must().CertManagerClient.CertificateApi.CertificatesDelete(context.Background(), cert.Id).Execute() - if err != nil { + return core.DeleteAll(c, core.DeleteAllOptions[cert.CertificateRead]{ + Resource: "Certificate", + List: func() ([]cert.CertificateRead, error) { + certs, _, err := client.Must().CertManagerClient.CertificateApi.CertificatesGet(context.Background()).Execute() + if err != nil { + return nil, err + } + return certs.Items, nil + }, + Summary: func(z cert.CertificateRead) string { + return fmt.Sprintf("name: %s, id: %s", z.Properties.Name, z.Id) + }, + ID: func(z cert.CertificateRead) string { + return z.Id + }, + Delete: func(z cert.CertificateRead) error { + _, err := client.Must().CertManagerClient.CertificateApi.CertificatesDelete(context.Background(), z.Id).Execute() return err - } - } + }, + }) } else { id, err := c.Command.Command.Flags().GetString(constants.FlagCertId) if err != nil { @@ -81,7 +82,6 @@ func CmdDelete(c *core.CommandConfig) error { return err } - return err } func PreCmdDelete(c *core.PreCommandConfig) error { diff --git a/commands/cert/provider/delete.go b/commands/cert/provider/delete.go index 762df293b0..da8a522e20 100644 --- a/commands/cert/provider/delete.go +++ b/commands/cert/provider/delete.go @@ -8,7 +8,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" "github.com/ionos-cloud/sdk-go-bundle/products/cert/v2" "github.com/spf13/viper" ) @@ -56,7 +55,7 @@ func ProviderDeleteCmd() *core.Command { }, constants.CertApiRegionalURL, constants.CertLocations), ) - cmd.AddBoolFlag(constants.ArgAll, constants.ArgAllShort, false, fmt.Sprintf("Delete all Providers. Required or -%s", constants.FlagGatewayShort)) + cmd.AddBoolFlag(constants.ArgAll, constants.ArgAllShort, false, fmt.Sprintf("Delete all Providers. Required or --%s", constants.FlagProviderID)) cmd.Command.SilenceUsage = true cmd.Command.Flags().SortFlags = false @@ -65,22 +64,24 @@ func ProviderDeleteCmd() *core.Command { } func deleteAll(c *core.CommandConfig) error { - c.Verbose("Deleting all providers!") - xs, _, err := client.Must().CertManagerClient.ProviderApi.ProvidersGet(context.Background()).Execute() - if err != nil { - return fmt.Errorf("failed getting the Providers: %w", err) - } - err = functional.ApplyAndAggregateErrors(xs.GetItems(), func(z cert.ProviderRead) error { - yes := confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Are you sure you want to delete Provider with name: %s, id: %s ", z.Properties.Name, z.Id), - viper.GetBool(constants.ArgForce)) - if yes { - _, delErr := client.Must().CertManagerClient.ProviderApi.ProvidersDelete(context.Background(), z.Id).Execute() - if delErr != nil { - return fmt.Errorf("failed deleting %s (name: %s): %w", z.Id, z.Properties.Name, delErr) + return core.DeleteAll(c, core.DeleteAllOptions[cert.ProviderRead]{ + Resource: "Provider", + List: func() ([]cert.ProviderRead, error) { + xs, _, err := client.Must().CertManagerClient.ProviderApi.ProvidersGet(context.Background()).Execute() + if err != nil { + return nil, fmt.Errorf("failed getting the Providers: %w", err) } - } - return nil + return xs.GetItems(), nil + }, + Summary: func(z cert.ProviderRead) string { + return fmt.Sprintf("name: %s, id: %s", z.Properties.Name, z.Id) + }, + ID: func(z cert.ProviderRead) string { + return z.Id + }, + Delete: func(z cert.ProviderRead) error { + _, err := client.Must().CertManagerClient.ProviderApi.ProvidersDelete(context.Background(), z.Id).Execute() + return err + }, }) - - return err } diff --git a/commands/compute/applicationloadbalancer/applicationloadbalancer_test.go b/commands/compute/applicationloadbalancer/applicationloadbalancer_test.go index eb7b55cf77..037b02d678 100644 --- a/commands/compute/applicationloadbalancer/applicationloadbalancer_test.go +++ b/commands/compute/applicationloadbalancer/applicationloadbalancer_test.go @@ -395,7 +395,10 @@ func TestRunApplicationLoadBalancerDeleteAllErr(t *testing.T) { }) } -func TestRunApplicationLoadBalancerDeleteAllAskForConfirmErr(t *testing.T) { +// Denying confirmation on an item during --all skips ONLY that item and never +// aborts the operation, so with no items confirmed nothing is deleted and no +// error is returned (no Delete mock is expected). +func TestRunApplicationLoadBalancerDeleteAllDenySkips(t *testing.T) { var b bytes.Buffer w := bufio.NewWriter(&b) core.CmdConfigTest(t, w, func(cfg *core.CommandConfig, rm *core.ResourcesMocksTest) { @@ -408,7 +411,7 @@ func TestRunApplicationLoadBalancerDeleteAllAskForConfirmErr(t *testing.T) { rm.CloudApiV6Mocks.ApplicationLoadBalancer.EXPECT().List(testApplicationLoadBalancerVar).Return(applicationloadbalancers, &testutil.TestResponse, nil) cfg.Command.Command.SetIn(bytes.NewReader([]byte("\n"))) err := RunApplicationLoadBalancerDelete(cfg) - assert.Error(t, err) + assert.NoError(t, err) }) } diff --git a/commands/compute/applicationloadbalancer/flowlog/run_applicationloadbalancer_flowlog.go b/commands/compute/applicationloadbalancer/flowlog/run_applicationloadbalancer_flowlog.go index 849287a429..5664440a34 100644 --- a/commands/compute/applicationloadbalancer/flowlog/run_applicationloadbalancer_flowlog.go +++ b/commands/compute/applicationloadbalancer/flowlog/run_applicationloadbalancer_flowlog.go @@ -179,53 +179,51 @@ func RunApplicationLoadBalancerFlowLogDelete(c *core.CommandConfig) error { } func DeleteAllApplicationLoadBalancerFlowLog(c *core.CommandConfig) error { - c.Msg("Getting Application Load Balancer FlowLogs...") - - applicationLoadBalancerFlowlogs, resp, err := c.CloudApiV6Services.ApplicationLoadBalancers().ListFlowLogs( - viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgDataCenterId)), - viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgApplicationLoadBalancerId)), - ) - if err != nil { - return err - } - - albFlowLogItems, ok := applicationLoadBalancerFlowlogs.GetItemsOk() - if !ok || albFlowLogItems == nil { - return errors.New("could not get items of Application Load Balancer Flow Logs") - } - - if len(*albFlowLogItems) <= 0 { - return errors.New("no Application Load Balancer Flow Logs found") - } - - var multiErr error - for _, fl := range *albFlowLogItems { - id := fl.GetId() - name := fl.GetProperties().Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete Application Load Balancer FlowLog Id: %s , Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.ApplicationLoadBalancers().DeleteFlowLog( - viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgDataCenterId)), - viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgApplicationLoadBalancerId)), *id, - ) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } - - if multiErr != nil { - return multiErr - } - - return nil + dcId := viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgDataCenterId)) + albId := viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgApplicationLoadBalancerId)) + + c.Verbose(constants.DatacenterId, dcId) + c.Verbose(constants.ApplicationLoadBalancerId, albId) + + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.FlowLog]{ + Resource: "Application Load Balancer FlowLog", + List: func() ([]ionoscloud.FlowLog, error) { + applicationLoadBalancerFlowlogs, _, err := c.CloudApiV6Services.ApplicationLoadBalancers().ListFlowLogs(dcId, albId) + if err != nil { + return nil, err + } + items, ok := applicationLoadBalancerFlowlogs.GetItemsOk() + if !ok || items == nil { + return nil, errors.New("could not get items of Application Load Balancer Flow Logs") + } + return *items, nil + }, + Summary: func(fl ionoscloud.FlowLog) string { + summary := "" + if props, ok := fl.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil { + summary += *name + } + } + if id, ok := fl.GetIdOk(); ok && id != nil { + summary += fmt.Sprintf(" (id: %s)", *id) + } + return summary + }, + ID: func(fl ionoscloud.FlowLog) string { + if id := fl.GetId(); id != nil { + return *id + } + return "" + }, + Delete: func(fl ionoscloud.FlowLog) error { + resp, err := c.CloudApiV6Services.ApplicationLoadBalancers().DeleteFlowLog(dcId, albId, *fl.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } func PreRunDcApplicationLoadBalancerIds(c *core.PreCommandConfig) error { diff --git a/commands/compute/applicationloadbalancer/rule/run_applicationloadbalancer_rule.go b/commands/compute/applicationloadbalancer/rule/run_applicationloadbalancer_rule.go index b4bb5cd985..9ae6457bd5 100644 --- a/commands/compute/applicationloadbalancer/rule/run_applicationloadbalancer_rule.go +++ b/commands/compute/applicationloadbalancer/rule/run_applicationloadbalancer_rule.go @@ -175,53 +175,54 @@ func RunApplicationLoadBalancerForwardingRuleDelete(c *core.CommandConfig) error } func DeleteAllApplicationLoadBalancerForwardingRule(c *core.CommandConfig) error { - c.Msg("Getting Application Load Balancer Forwarding Rules...") - - applicationLoadBalancerRules, resp, err := c.CloudApiV6Services.ApplicationLoadBalancers().ListForwardingRules( - viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgDataCenterId)), - viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgApplicationLoadBalancerId)), - ) - if err != nil { - return err - } - - albRuleItems, ok := applicationLoadBalancerRules.GetItemsOk() - if !ok || albRuleItems == nil { - return errors.New("could not get items of Target Groups") - } - - if len(*albRuleItems) <= 0 { - return errors.New("no Target Groups found") - } - - var multiErr error - for _, fr := range *albRuleItems { - id := fr.GetId() - name := fr.GetProperties().Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete Forwarding Rule Id: %s , Name: %s ", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.ApplicationLoadBalancers().DeleteForwardingRule( - viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgDataCenterId)), - viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgApplicationLoadBalancerId)), *id, - ) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } - - if multiErr != nil { - return multiErr - } - - return nil + dcId := viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgDataCenterId)) + albId := viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgApplicationLoadBalancerId)) + + c.Verbose(constants.DatacenterId, dcId) + c.Verbose(constants.ApplicationLoadBalancerId, albId) + + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.ApplicationLoadBalancerForwardingRule]{ + Resource: "Application Load Balancer Forwarding Rule", + List: func() ([]ionoscloud.ApplicationLoadBalancerForwardingRule, error) { + applicationLoadBalancerRules, _, err := c.CloudApiV6Services.ApplicationLoadBalancers().ListForwardingRules(dcId, albId) + if err != nil { + return nil, err + } + items, ok := applicationLoadBalancerRules.GetItemsOk() + if !ok || items == nil { + return nil, errors.New("could not get items of Application Load Balancer Forwarding Rules") + } + return *items, nil + }, + Summary: func(rule ionoscloud.ApplicationLoadBalancerForwardingRule) string { + summary := "" + if props, ok := rule.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil { + summary += *name + } + if ip, ok := props.GetListenerIpOk(); ok && ip != nil && *ip != "" { + summary += fmt.Sprintf(" (listenerIp: %s)", *ip) + } + } + if id, ok := rule.GetIdOk(); ok && id != nil { + summary += fmt.Sprintf(" (id: %s)", *id) + } + return summary + }, + ID: func(rule ionoscloud.ApplicationLoadBalancerForwardingRule) string { + if id := rule.GetId(); id != nil { + return *id + } + return "" + }, + Delete: func(rule ionoscloud.ApplicationLoadBalancerForwardingRule) error { + resp, err := c.CloudApiV6Services.ApplicationLoadBalancers().DeleteForwardingRule(dcId, albId, *rule.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } func getAlbForwardingRulePropertiesSet(c *core.CommandConfig) *resources.ApplicationLoadBalancerForwardingRuleProperties { diff --git a/commands/compute/applicationloadbalancer/run_applicationloadbalancer.go b/commands/compute/applicationloadbalancer/run_applicationloadbalancer.go index 135c2c5d5a..9fc28772ec 100644 --- a/commands/compute/applicationloadbalancer/run_applicationloadbalancer.go +++ b/commands/compute/applicationloadbalancer/run_applicationloadbalancer.go @@ -211,49 +211,52 @@ func RunApplicationLoadBalancerDelete(c *core.CommandConfig) error { } func DeleteAllApplicationLoadBalancer(c *core.CommandConfig) error { - c.Msg("Getting Application Load Balancers...") - - applicationLoadBalancers, resp, err := c.CloudApiV6Services.ApplicationLoadBalancers().List( - viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgDataCenterId))) - if err != nil { - return err - } - - albItems, ok := applicationLoadBalancers.GetItemsOk() - if !ok || albItems == nil { - return errors.New("could not get items of Application Load Balancers") - } - - if len(*albItems) <= 0 { - return errors.New("no Application Load Balancers found") - } - - var multiErr error - - for _, alb := range *albItems { - id := alb.GetId() - name := alb.Properties.Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete Application Load Balancer Id: %s , Name: %s ", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.ApplicationLoadBalancers().Delete(viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgDataCenterId)), *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } - - if multiErr != nil { - return multiErr - } + dcId := viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgDataCenterId)) - return nil + c.Verbose(constants.DatacenterId, dcId) + + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.ApplicationLoadBalancer]{ + Resource: "Application Load Balancer", + List: func() ([]ionoscloud.ApplicationLoadBalancer, error) { + applicationLoadBalancers, _, err := c.CloudApiV6Services.ApplicationLoadBalancers().List(dcId) + if err != nil { + return nil, err + } + items, ok := applicationLoadBalancers.GetItemsOk() + if !ok || items == nil { + return nil, errors.New("could not get items of Application Load Balancers") + } + return *items, nil + }, + Summary: func(alb ionoscloud.ApplicationLoadBalancer) string { + summary := "" + if props, ok := alb.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil { + summary += *name + } + if ips, ok := props.GetIpsOk(); ok && ips != nil && len(*ips) > 0 { + summary += fmt.Sprintf(" (public IPs: %v)", *ips) + } + } + if id, ok := alb.GetIdOk(); ok && id != nil { + summary += fmt.Sprintf(" (id: %s)", *id) + } + return summary + }, + ID: func(alb ionoscloud.ApplicationLoadBalancer) string { + if id := alb.GetId(); id != nil { + return *id + } + return "" + }, + Delete: func(alb ionoscloud.ApplicationLoadBalancer) error { + resp, err := c.CloudApiV6Services.ApplicationLoadBalancers().Delete(dcId, *alb.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } func getNewApplicationLoadBalancerInfo(c *core.CommandConfig) *resources.ApplicationLoadBalancerProperties { diff --git a/commands/compute/backupunit/backupunit_test.go b/commands/compute/backupunit/backupunit_test.go index 27a78cd9b4..e7b559c351 100644 --- a/commands/compute/backupunit/backupunit_test.go +++ b/commands/compute/backupunit/backupunit_test.go @@ -81,28 +81,7 @@ var ( }, }, } - testListQueryParamFilters = resources.ListQueryParams{ - Filters: &map[string][]string{ - testQueryParamVar: {testQueryParamVar}, - }, - OrderBy: &testQueryParamVar, - QueryParams: resources.QueryParams{ - Depth: &testDepthListVar, - }, - } - testListQueryParam = resources.ListQueryParams{ - OrderBy: &testOrderByVar, - QueryParams: resources.QueryParams{ - Depth: &testDepthListVar, - }, - } - testQueryParamOther = resources.QueryParams{ - Depth: &testDepthOtherVar, - } - testDepthListVar = int32(1) - testDepthOtherVar = int32(0) testQueryParamVar = "test-filter" - testOrderByVar = "" // default orderBy. Add to cloudapi constants? testBackupUnitVar = "test-backup-unit" testBackUnitId = "87aa25ec-5f74-4927-bd95-c8e42db06fe2" testBackupUnitNewVar = "test-new-backup-unit" diff --git a/commands/compute/backupunit/run_backupunit.go b/commands/compute/backupunit/run_backupunit.go index 27a3f47fdc..382c0de005 100644 --- a/commands/compute/backupunit/run_backupunit.go +++ b/commands/compute/backupunit/run_backupunit.go @@ -1,7 +1,6 @@ package backupunit import ( - "errors" "fmt" "github.com/ionos-cloud/ionosctl/v6/internal/constants" @@ -166,46 +165,49 @@ func getBackupUnitInfo(c *core.CommandConfig) *resources.BackupUnitProperties { } func DeleteAllBackupUnits(c *core.CommandConfig) error { - c.Verbose("Getting Backup Units...") - - backupUnits, resp, err := c.CloudApiV6Services.BackupUnit().List() - if err != nil { - return err - } - - backupUnitsItems, ok := backupUnits.GetItemsOk() - if !ok || backupUnitsItems == nil { - return fmt.Errorf("could not get Backup Unit items") - } - - if len(*backupUnitsItems) <= 0 { - return fmt.Errorf("no Backup Units found") - } - - c.Msg("Backup Units to be deleted:") - - var multiErr error - for _, backupUnit := range *backupUnitsItems { - id := backupUnit.GetId() - name := backupUnit.GetProperties().Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete BackupUnit Id: %s , Name: %s ", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.BackupUnit().Delete(*id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - } - - if multiErr != nil { - return multiErr - } - - return nil + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.BackupUnit]{ + Resource: "BackupUnit", + List: func() ([]ionoscloud.BackupUnit, error) { + backupUnits, _, err := c.CloudApiV6Services.BackupUnit().List() + if err != nil { + return nil, err + } + + items, ok := backupUnits.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get Backup Unit items") + } + + return *items, nil + }, + Summary: func(backupUnit ionoscloud.BackupUnit) string { + var id string + if v, ok := backupUnit.GetIdOk(); ok && v != nil { + id = *v + } + summary := fmt.Sprintf("id: %s", id) + if props, ok := backupUnit.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil && *name != "" { + summary = fmt.Sprintf("%s (name: %s)", summary, *name) + } + if email, ok := props.GetEmailOk(); ok && email != nil && *email != "" { + summary = fmt.Sprintf("%s (email: %s)", summary, *email) + } + } + return summary + }, + ID: func(backupUnit ionoscloud.BackupUnit) string { + if id, ok := backupUnit.GetIdOk(); ok && id != nil { + return *id + } + return "" + }, + Delete: func(backupUnit ionoscloud.BackupUnit) error { + resp, err := c.CloudApiV6Services.BackupUnit().Delete(*backupUnit.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } diff --git a/commands/compute/datacenter/run_datacenter.go b/commands/compute/datacenter/run_datacenter.go index f9448b8727..4de8bed9f6 100644 --- a/commands/compute/datacenter/run_datacenter.go +++ b/commands/compute/datacenter/run_datacenter.go @@ -1,7 +1,6 @@ package datacenter import ( - "errors" "fmt" "github.com/ionos-cloud/ionosctl/v6/internal/constants" @@ -129,59 +128,58 @@ func RunDataCenterDelete(c *core.CommandConfig) error { } func DeleteAllDatacenters(c *core.CommandConfig) error { - c.Verbose("Getting Datacenters...") - - datacenters, resp, err := c.CloudApiV6Services.DataCenters().List() - if err != nil { - return err - } - - datacentersItems, ok := datacenters.GetItemsOk() - if !ok || datacentersItems == nil { - return fmt.Errorf("could not get items of Datacenters") - } - - if len(*datacentersItems) <= 0 { - return fmt.Errorf("no Datacenters found") - } - - c.Msg("Datacenters to be deleted:") - - var multiErr error - for _, dc := range *datacentersItems { - id := dc.GetId() - name := dc.GetProperties().Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete Datacenter with Id: %s , Name: %s", *id, *name), viper.IsSet(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.DataCenters().Delete(*id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } - - if multiErr != nil { - return multiErr - } - - return nil -} - -func getDataCenters(datacenters resources.Datacenters) []resources.Datacenter { - dc := make([]resources.Datacenter, 0) - if items, ok := datacenters.GetItemsOk(); ok && items != nil { - for _, datacenter := range *items { - dc = append(dc, resources.Datacenter{Datacenter: datacenter}) - } - } - return dc + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud2.Datacenter]{ + Resource: "datacenter", + List: func() ([]ionoscloud2.Datacenter, error) { + datacenters, _, err := c.CloudApiV6Services.DataCenters().List() + if err != nil { + return nil, err + } + + items, ok := datacenters.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Datacenters") + } + + return *items, nil + }, + Summary: func(dc ionoscloud2.Datacenter) string { + var id, name, location, description string + if dc.Id != nil { + id = *dc.Id + } + if p := dc.Properties; p != nil { + if p.Name != nil { + name = *p.Name + } + if p.Location != nil { + location = *p.Location + } + if p.Description != nil { + description = *p.Description + } + } + + s := fmt.Sprintf("%s (id: %s, location: %s)", name, id, location) + if description != "" { + s = fmt.Sprintf("%s (id: %s, location: %s, desc: %s)", name, id, location, description) + } + return s + }, + ID: func(dc ionoscloud2.Datacenter) string { + if dc.Id != nil { + return *dc.Id + } + return "" + }, + Delete: func(dc ionoscloud2.Datacenter) error { + resp, err := c.CloudApiV6Services.DataCenters().Delete(*dc.Id) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } func GetIPv6CidrBlockFromDatacenter(dc ionoscloud2.Datacenter) (string, error) { diff --git a/commands/compute/firewallrule/firewallrule_test.go b/commands/compute/firewallrule/firewallrule_test.go index 1809c9c1f1..81befa5a8e 100644 --- a/commands/compute/firewallrule/firewallrule_test.go +++ b/commands/compute/firewallrule/firewallrule_test.go @@ -5,9 +5,7 @@ import ( "bytes" "errors" "fmt" - "net/http" "testing" - "time" "github.com/ionos-cloud/ionosctl/v6/commands/compute/testutil" @@ -76,16 +74,6 @@ var ( Items: &[]ionoscloud.FirewallRule{testRule.FirewallRule}, }, } - testResponseErr = resources.Response{ - APIResponse: ionoscloud.APIResponse{ - Response: &http.Response{ - Header: map[string][]string{ - "Location": {""}, - }, - }, - RequestTime: time.Duration(50), - }, - } testFirewallRuleProtocol = "TCP" testFirewallRuleType = "INGRESS" testFirewallRuleState = "AVAILABLE" diff --git a/commands/compute/firewallrule/run_firewallrule.go b/commands/compute/firewallrule/run_firewallrule.go index cd8f87a9e1..eb2c2a8b50 100644 --- a/commands/compute/firewallrule/run_firewallrule.go +++ b/commands/compute/firewallrule/run_firewallrule.go @@ -1,7 +1,6 @@ package firewallrule import ( - "errors" "fmt" "net" "strings" @@ -263,44 +262,49 @@ func DeleteAllFirewallRules(c *core.CommandConfig) error { c.Verbose(constants.DatacenterId, datacenterId) c.Verbose("Server ID: %v", serverId) c.Verbose("NIC with ID: %v", nicId) - c.Verbose("Getting Firewall Rules...") - firewallRules, _, err := c.CloudApiV6Services.FirewallRules().List(datacenterId, serverId, nicId) - if err != nil { - return err - } - firewallRulesItems, ok := firewallRules.GetItemsOk() - if !ok || firewallRulesItems == nil { - return fmt.Errorf("could not get items of Firewall Rules") - } - - if len(*firewallRulesItems) <= 0 { - return fmt.Errorf("no Firewall Rule found") - } + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.FirewallRule]{ + Resource: "Firewall Rule", + List: func() ([]ionoscloud.FirewallRule, error) { + firewallRules, _, err := c.CloudApiV6Services.FirewallRules().List(datacenterId, serverId, nicId) + if err != nil { + return nil, err + } - var multiErr error - for _, firewall := range *firewallRulesItems { - id := firewall.GetId() - name := firewall.GetProperties().Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete Firewall Rule with Id: %s , Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - _, err = c.CloudApiV6Services.FirewallRules().Delete(datacenterId, serverId, nicId, *id) - - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } + items, ok := firewallRules.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Firewall Rules") + } - } - - if multiErr != nil { - return multiErr - } - - return nil + return *items, nil + }, + Summary: func(firewall ionoscloud.FirewallRule) string { + var id string + if v, ok := firewall.GetIdOk(); ok && v != nil { + id = *v + } + summary := fmt.Sprintf("id: %s", id) + if props, ok := firewall.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil && *name != "" { + summary = fmt.Sprintf("%s (name: %s)", summary, *name) + } + } + return summary + }, + ID: func(firewall ionoscloud.FirewallRule) string { + if id, ok := firewall.GetIdOk(); ok && id != nil { + return *id + } + return "" + }, + Delete: func(firewall ionoscloud.FirewallRule) error { + resp, err := c.CloudApiV6Services.FirewallRules().Delete(datacenterId, serverId, nicId, *firewall.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } // checkSourceIPAndTargetIPVersions returns true if the source and destination diff --git a/commands/compute/flowlog/flowlog_test.go b/commands/compute/flowlog/flowlog_test.go index ff9137e6e0..cae3920859 100644 --- a/commands/compute/flowlog/flowlog_test.go +++ b/commands/compute/flowlog/flowlog_test.go @@ -49,19 +49,6 @@ var ( Properties: testFlowLog.FlowLog.Properties, }, } - testFlowLogUpdated = resources.FlowLog{ - FlowLog: ionoscloud.FlowLog{ - Properties: &testFlowLogProperties.FlowLogProperties, - }, - } - testFlowLogProperties = resources.FlowLogProperties{ - FlowLogProperties: ionoscloud.FlowLogProperties{ - Name: &testFlowLogNewVar, - Action: &testFlowLogNewUpperVar, - Direction: &testFlowLogNewUpperVar, - Bucket: &testFlowLogNewVar, - }, - } testFlowLogs = resources.FlowLogs{ FlowLogs: ionoscloud.FlowLogs{ Id: &testFlowLogVar, @@ -71,8 +58,6 @@ var ( testFlowLogState = "AVAILABLE" testFlowLogVar = "test-flowlog" testFlowLogUpperVar = strings.ToUpper(testFlowLogVar) - testFlowLogNewVar = "test-new-flowlog" - testFlowLogNewUpperVar = strings.ToUpper(testFlowLogNewVar) testFlowLogErr = errors.New("flowlog test error") ) diff --git a/commands/compute/flowlog/run_flowlog.go b/commands/compute/flowlog/run_flowlog.go index bb3e8eeb6a..5ee8db0849 100644 --- a/commands/compute/flowlog/run_flowlog.go +++ b/commands/compute/flowlog/run_flowlog.go @@ -138,45 +138,47 @@ func DeleteAllFlowlogs(c *core.CommandConfig) error { c.Verbose(constants.DatacenterId, dcId) c.Verbose("Server ID: %v", serverId) c.Verbose("NIC ID: %v", nicId) - c.Verbose("Getting Flowlogs...") - flowlogs, resp, err := c.CloudApiV6Services.FlowLogs().List(dcId, serverId, nicId) - if err != nil { - return err - } - - flowlogsItems, ok := flowlogs.GetItemsOk() - if !ok || flowlogsItems == nil { - return errors.New("could not get items of Flowlogs") - } - - if len(*flowlogsItems) <= 0 { - return errors.New("no Flowlogs found") - } - - var multiErr error - for _, backupUnit := range *flowlogsItems { - id := backupUnit.GetId() - name := backupUnit.GetProperties().Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete flow log with Id: %s , Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.FlowLogs().Delete(dcId, serverId, nicId, *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.FlowLog]{ + Resource: "Flowlog", + List: func() ([]ionoscloud.FlowLog, error) { + flowlogs, _, err := c.CloudApiV6Services.FlowLogs().List(dcId, serverId, nicId) + if err != nil { + return nil, err + } - if multiErr != nil { - return multiErr - } + items, ok := flowlogs.GetItemsOk() + if !ok || items == nil { + return nil, errors.New("could not get items of Flowlogs") + } - return nil + return *items, nil + }, + Summary: func(flowlog ionoscloud.FlowLog) string { + var id string + if v, ok := flowlog.GetIdOk(); ok && v != nil { + id = *v + } + summary := fmt.Sprintf("id: %s", id) + if props, ok := flowlog.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil && *name != "" { + summary = fmt.Sprintf("%s (name: %s)", summary, *name) + } + } + return summary + }, + ID: func(flowlog ionoscloud.FlowLog) string { + if id, ok := flowlog.GetIdOk(); ok && id != nil { + return *id + } + return "" + }, + Delete: func(flowlog ionoscloud.FlowLog) error { + resp, err := c.CloudApiV6Services.FlowLogs().Delete(dcId, serverId, nicId, *flowlog.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } diff --git a/commands/compute/group/run_group.go b/commands/compute/group/run_group.go index a5cb442d5f..9c2332fa06 100644 --- a/commands/compute/group/run_group.go +++ b/commands/compute/group/run_group.go @@ -1,7 +1,6 @@ package group import ( - "errors" "fmt" "github.com/ionos-cloud/ionosctl/v6/internal/constants" @@ -368,57 +367,46 @@ func getGroupUpdateInfo(oldGroup *resources.Group, c *core.CommandConfig) *resou } func DeleteAllGroups(c *core.CommandConfig) error { - c.Verbose("Getting Groups...") - - groups, resp, err := c.CloudApiV6Services.Groups().List() - if err != nil { - return err - } - - groupsItems, ok := groups.GetItemsOk() - if !ok || groupsItems == nil { - return fmt.Errorf("could not get items of Groups") - } - - if len(*groupsItems) <= 0 { - return fmt.Errorf("no Groups found") - } - - c.Msg("Groups to be deleted:") - - var multiErr error - for _, group := range *groupsItems { - id := group.GetId() - name := group.GetProperties().Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the Group with Id: %s , Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.Groups().Delete(*id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } - - if multiErr != nil { - return multiErr - } + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.Group]{ + Resource: "Group", + List: func() ([]ionoscloud.Group, error) { + groups, _, err := c.CloudApiV6Services.Groups().List() + if err != nil { + return nil, err + } - return nil -} + items, ok := groups.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Groups") + } -func getGroups(groups resources.Groups) []resources.Group { - u := make([]resources.Group, 0) - if items, ok := groups.GetItemsOk(); ok && items != nil { - for _, item := range *items { - u = append(u, resources.Group{Group: item}) - } - } - return u + return *items, nil + }, + Summary: func(group ionoscloud.Group) string { + var id string + if v, ok := group.GetIdOk(); ok && v != nil { + id = *v + } + summary := fmt.Sprintf("id: %s", id) + if props, ok := group.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil && *name != "" { + summary = fmt.Sprintf("%s (name: %s)", summary, *name) + } + } + return summary + }, + ID: func(group ionoscloud.Group) string { + if id, ok := group.GetIdOk(); ok && id != nil { + return *id + } + return "" + }, + Delete: func(group ionoscloud.Group) error { + resp, err := c.CloudApiV6Services.Groups().Delete(*group.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } diff --git a/commands/compute/image/run_image.go b/commands/compute/image/run_image.go index 0866892424..866ae36294 100644 --- a/commands/compute/image/run_image.go +++ b/commands/compute/image/run_image.go @@ -84,52 +84,66 @@ func RunImageDelete(c *core.CommandConfig) error { // DeleteAllNonPublicImages deletes non-public images, as deleting public images is forbidden by the API. func DeleteAllNonPublicImages(c *core.CommandConfig) error { - images, resp, err := c.CloudApiV6Services.Images().List() - if err != nil { - return err - } - allItems, ok := images.GetItemsOk() - if !(ok && len(*allItems) > 0 && allItems != nil) { - return errors.New("could not retrieve images") - } - - items, err := getNonPublicImages(*allItems, c.Command.Command.ErrOrStderr()) - if err != nil { - return err - } - if len(items) < 1 { - return errors.New("no non-public images found") - } - - c.Msg("Images to be deleted:") - // TODO: this is duplicated across all resources - refactor this (across all resources) - var multiErr error - for _, img := range items { - id := img.GetId() - name := img.GetProperties().Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the image with Id: %s , Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.Images().Delete(*id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } else { - c.Msg(constants.MessageDeletingAll, c.Resource, *id) - } + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.Image]{ + Resource: "image", + List: func() ([]ionoscloud.Image, error) { + images, _, err := c.CloudApiV6Services.Images().List() + if err != nil { + return nil, err + } - } + allItems, ok := images.GetItemsOk() + if !ok || allItems == nil { + return nil, errors.New("could not retrieve images") + } - if multiErr != nil { - return multiErr - } + // deleting public images is forbidden by the API, so only consider non-public ones + nonPublic, err := getNonPublicImages(*allItems, c.Command.Command.ErrOrStderr()) + if err != nil { + return nil, err + } + if len(nonPublic) == 0 && len(*allItems) > 0 { + return nil, errors.New("no deletable images found: all images are public (deleting public images is not allowed)") + } + return nonPublic, nil + }, + Summary: func(img ionoscloud.Image) string { + var id, name, location, description string + if img.Id != nil { + id = *img.Id + } + if p := img.Properties; p != nil { + if p.Name != nil { + name = *p.Name + } + if p.Location != nil { + location = *p.Location + } + if p.Description != nil { + description = *p.Description + } + } - return nil + s := fmt.Sprintf("%s (id: %s, location: %s)", name, id, location) + if description != "" { + s = fmt.Sprintf("%s (id: %s, location: %s, desc: %s)", name, id, location, description) + } + return s + }, + ID: func(img ionoscloud.Image) string { + if img.Id != nil { + return *img.Id + } + return "" + }, + Delete: func(img ionoscloud.Image) error { + resp, err := c.CloudApiV6Services.Images().Delete(*img.Id) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } // Util func - Given a slice of public & non-public images, return only those images that are non-public. diff --git a/commands/compute/ipblock/run_ipblock.go b/commands/compute/ipblock/run_ipblock.go index 67ab0e60cd..d984ab5110 100644 --- a/commands/compute/ipblock/run_ipblock.go +++ b/commands/compute/ipblock/run_ipblock.go @@ -1,8 +1,8 @@ package ipblock import ( - "errors" "fmt" + "strings" "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" @@ -10,6 +10,7 @@ import ( "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" cloudapiv6 "github.com/ionos-cloud/ionosctl/v6/services/cloudapi-v6" "github.com/ionos-cloud/ionosctl/v6/services/cloudapi-v6/resources" + ionoscloud "github.com/ionos-cloud/sdk-go/v6" "github.com/spf13/viper" ) @@ -118,45 +119,55 @@ func RunIpBlockDelete(c *core.CommandConfig) error { } func DeleteAllIpBlocks(c *core.CommandConfig) error { - c.Verbose("Getting all Ip Blocks...") - - ipBlocks, resp, err := c.CloudApiV6Services.IpBlocks().List() - if err != nil { - return err - } - - ipBlocksItems, ok := ipBlocks.GetItemsOk() - if !ok || ipBlocksItems == nil { - return fmt.Errorf("could not get items of Ip Blocks") - } - - if len(*ipBlocksItems) <= 0 { - return fmt.Errorf("no Ip Blocks found") - } - - var multiErr error - for _, dc := range *ipBlocksItems { - id := dc.GetId() - name := dc.GetProperties().Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the IpBlock with Id: %s , Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.IpBlocks().Delete(*id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } - - if multiErr != nil { - return multiErr - } - - return nil + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.IpBlock]{ + Resource: "ipblock", + List: func() ([]ionoscloud.IpBlock, error) { + ipBlocks, _, err := c.CloudApiV6Services.IpBlocks().List() + if err != nil { + return nil, err + } + + items, ok := ipBlocks.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Ip Blocks") + } + + return *items, nil + }, + Summary: func(ipBlock ionoscloud.IpBlock) string { + var id, name, location string + var ips []string + if ipBlock.Id != nil { + id = *ipBlock.Id + } + if p := ipBlock.Properties; p != nil { + if p.Name != nil { + name = *p.Name + } + if p.Location != nil { + location = *p.Location + } + if p.Ips != nil { + ips = *p.Ips + } + } + if len(ips) > 0 { + return fmt.Sprintf("%s (id: %s, ips: %s, location: %s)", name, id, strings.Join(ips, ", "), location) + } + return fmt.Sprintf("%s (id: %s, location: %s)", name, id, location) + }, + ID: func(ipBlock ionoscloud.IpBlock) string { + if ipBlock.Id != nil { + return *ipBlock.Id + } + return "" + }, + Delete: func(ipBlock ionoscloud.IpBlock) error { + resp, err := c.CloudApiV6Services.IpBlocks().Delete(*ipBlock.Id) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } diff --git a/commands/compute/k8s/cluster/run_k8s_cluster.go b/commands/compute/k8s/cluster/run_k8s_cluster.go index 7daf2cdfae..c40c2df8d9 100644 --- a/commands/compute/k8s/cluster/run_k8s_cluster.go +++ b/commands/compute/k8s/cluster/run_k8s_cluster.go @@ -1,7 +1,6 @@ package cluster import ( - "errors" "fmt" "strings" @@ -260,46 +259,41 @@ func getK8sClusterInfo(oldUser *resources.K8sCluster, c *core.CommandConfig) res } func DeleteAllK8sClusters(c *core.CommandConfig) error { - c.Verbose("Getting K8sClusters...") - - k8Clusters, resp, err := c.CloudApiV6Services.K8s().ListClusters() - if err != nil { - return err - } - - k8sClustersItems, ok := k8Clusters.GetItemsOk() - if !ok || k8sClustersItems == nil { - return fmt.Errorf("could not get items of K8sClusters") - } - - if len(*k8sClustersItems) <= 0 { - return fmt.Errorf("no K8sClusters found") - } - - var multiErr error - for _, k8sCluster := range *k8sClustersItems { - id := k8sCluster.GetId() - name := k8sCluster.GetProperties().GetName() - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the K8sCluster with Id: %s , Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.K8s().DeleteCluster(*id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.KubernetesCluster]{ + Resource: "K8sCluster", + List: func() ([]ionoscloud.KubernetesCluster, error) { + k8Clusters, _, err := c.CloudApiV6Services.K8s().ListClusters() + if err != nil { + return nil, err + } - if multiErr != nil { - return multiErr - } + items, ok := k8Clusters.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of K8sClusters") + } - return nil + return *items, nil + }, + Summary: func(k8sCluster ionoscloud.KubernetesCluster) string { + summary := fmt.Sprintf("id: %s", *k8sCluster.GetId()) + if props, ok := k8sCluster.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil && *name != "" { + summary = fmt.Sprintf("%s (name: %s)", summary, *name) + } + } + return summary + }, + ID: func(k8sCluster ionoscloud.KubernetesCluster) string { + return *k8sCluster.GetId() + }, + Delete: func(k8sCluster ionoscloud.KubernetesCluster) error { + resp, err := c.CloudApiV6Services.K8s().DeleteCluster(*k8sCluster.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } func GetK8sVersion(c *core.CommandConfig) (string, error) { diff --git a/commands/compute/k8s/node/run_k8s_node.go b/commands/compute/k8s/node/run_k8s_node.go index 9be63bafe3..f82ed2d649 100644 --- a/commands/compute/k8s/node/run_k8s_node.go +++ b/commands/compute/k8s/node/run_k8s_node.go @@ -1,7 +1,6 @@ package node import ( - "errors" "fmt" "github.com/ionos-cloud/ionosctl/v6/internal/constants" @@ -9,6 +8,7 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/request" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" cloudapiv6 "github.com/ionos-cloud/ionosctl/v6/services/cloudapi-v6" + ionoscloud "github.com/ionos-cloud/sdk-go/v6" "github.com/spf13/viper" ) @@ -134,44 +134,47 @@ func DeleteAllK8sNodes(c *core.CommandConfig) error { c.Verbose("K8sCluster ID: %v", clusterId) c.Verbose("K8sNodePool ID: %v", nodepoolId) - c.Verbose("Getting K8sNodes...") - k8sNodes, resp, err := c.CloudApiV6Services.K8s().ListNodes(clusterId, nodepoolId) - if err != nil { - return err - } - - k8sNodesItems, ok := k8sNodes.GetItemsOk() - if !ok || k8sNodesItems == nil { - return fmt.Errorf("could not get items of Kubernetes Nodes") - } - - if len(*k8sNodesItems) <= 0 { - return fmt.Errorf("no Kubernetes Nodes found") - } - - var multiErr error - for _, dc := range *k8sNodesItems { - id := dc.GetId() - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Deleting Node with ID: %v from K8s NodePool ID: %v from K8s Cluster ID: %v...", *id, nodepoolId, clusterId), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.K8s().DeleteNode(clusterId, nodepoolId, *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } - - if multiErr != nil { - return multiErr - } - - return nil + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.KubernetesNode]{ + Resource: "Kubernetes Node", + List: func() ([]ionoscloud.KubernetesNode, error) { + k8sNodes, _, err := c.CloudApiV6Services.K8s().ListNodes(clusterId, nodepoolId) + if err != nil { + return nil, err + } + + items, ok := k8sNodes.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Kubernetes Nodes") + } + + return *items, nil + }, + Summary: func(node ionoscloud.KubernetesNode) string { + var id string + if v, ok := node.GetIdOk(); ok && v != nil { + id = *v + } + summary := fmt.Sprintf("id: %s", id) + if props, ok := node.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil && *name != "" { + summary = fmt.Sprintf("%s (name: %s)", summary, *name) + } + } + return summary + }, + ID: func(node ionoscloud.KubernetesNode) string { + if id, ok := node.GetIdOk(); ok && id != nil { + return *id + } + return "" + }, + Delete: func(node ionoscloud.KubernetesNode) error { + resp, err := c.CloudApiV6Services.K8s().DeleteNode(clusterId, nodepoolId, *node.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } diff --git a/commands/compute/k8s/nodepool/k8s_nodepool_test.go b/commands/compute/k8s/nodepool/k8s_nodepool_test.go index 7976c22bfe..2a1754e7ba 100644 --- a/commands/compute/k8s/nodepool/k8s_nodepool_test.go +++ b/commands/compute/k8s/nodepool/k8s_nodepool_test.go @@ -46,22 +46,6 @@ var ( }, }, } - nodepoolTestPrivatePost = resources.K8sNodePoolForPost{ - KubernetesNodePoolForPost: ionoscloud.KubernetesNodePoolForPost{ - Properties: &ionoscloud.KubernetesNodePoolPropertiesForPost{ - Name: &testNodepoolVar, - NodeCount: &testNodepoolIntVar, - DatacenterId: &testNodepoolVar, - CpuFamily: &testNodepoolVar, - AvailabilityZone: &testNodepoolVar, - RamSize: &testNodepoolIntVar, - StorageSize: &testNodepoolIntVar, - StorageType: &testNodepoolVar, - CoresCount: &testNodepoolIntVar, - K8sVersion: &testNodepoolVar, - }, - }, - } nodepoolTest = resources.K8sNodePool{ KubernetesNodePool: ionoscloud.KubernetesNodePool{ Properties: &ionoscloud.KubernetesNodePoolProperties{ @@ -173,46 +157,6 @@ var ( }, }, } - nodepoolTestGetNew = resources.K8sNodePool{ - KubernetesNodePool: ionoscloud.KubernetesNodePool{ - Id: &testNodepoolVar, - Properties: &ionoscloud.KubernetesNodePoolProperties{ - Name: &testNodepoolVar, - NodeCount: &testNodepoolIntVar, - DatacenterId: &testNodepoolVar, - CpuFamily: &testNodepoolVar, - AvailabilityZone: &testNodepoolVar, - RamSize: &testNodepoolIntVar, - StorageSize: &testNodepoolIntVar, - StorageType: &testNodepoolVar, - K8sVersion: &testNodepoolVar, - CoresCount: &testNodepoolIntVar, - PublicIps: &testNodepoolSliceVar, - AvailableUpgradeVersions: &testNodepoolSliceVar, - MaintenanceWindow: &ionoscloud.KubernetesMaintenanceWindow{ - DayOfTheWeek: &testNodepoolVar, - Time: &testNodepoolVar, - }, - AutoScaling: &ionoscloud.KubernetesAutoScaling{ - MinNodeCount: &testNodepoolIntVar, - MaxNodeCount: &testNodepoolIntVar, - }, - Lans: &[]ionoscloud.KubernetesNodePoolLan{ - { - Id: &testNodepoolIntVar, - Dhcp: &testK8sNodePoolLanBoolVar, - }, - { - Id: &testNodepoolIntNewVar, - Dhcp: &testK8sNodePoolLanBoolVar, - }, - }, - }, - Metadata: &ionoscloud.DatacenterElementMetadata{ - State: &testutil.TestStateVar, - }, - }, - } nodepools = resources.K8sNodePools{ KubernetesNodePools: ionoscloud.KubernetesNodePools{ Id: &testNodepoolVar, @@ -279,24 +223,6 @@ var ( }, }, } - nodepoolTestOld = resources.K8sNodePool{ - KubernetesNodePool: ionoscloud.KubernetesNodePool{ - Id: &testNodepoolVar, - Properties: &ionoscloud.KubernetesNodePoolProperties{ - Name: &testNodepoolVar, - NodeCount: &testNodepoolIntVar, - K8sVersion: &testNodepoolVar, - }, - }, - } - nodepoolTestUpdateOld = resources.K8sNodePoolForPut{ - KubernetesNodePoolForPut: ionoscloud.KubernetesNodePoolForPut{ - Properties: &ionoscloud.KubernetesNodePoolPropertiesForPut{ - NodeCount: &testNodepoolIntVar, - K8sVersion: &testNodepoolVar, - }, - }, - } testNodepoolKVMap = map[string]string{testNodepoolVar: testNodepoolVar} testNodepoolKVNewMap = map[string]string{testNodepoolNewVar: testNodepoolNewVar} testNodepoolIntVar = int32(1) diff --git a/commands/compute/k8s/nodepool/run_k8s_nodepool.go b/commands/compute/k8s/nodepool/run_k8s_nodepool.go index d5d6037bdc..0112ca6a2e 100644 --- a/commands/compute/k8s/nodepool/run_k8s_nodepool.go +++ b/commands/compute/k8s/nodepool/run_k8s_nodepool.go @@ -2,7 +2,6 @@ package nodepool import ( "context" - "errors" "fmt" "math" "time" @@ -488,48 +487,47 @@ func DeleteAllK8sNodepools(c *core.CommandConfig) error { k8sClusterId := viper.GetString(core.GetFlagName(c.NS, constants.FlagClusterId)) c.Verbose("K8sCluster ID: %v", k8sClusterId) - c.Verbose("Getting K8sNodePools...") - k8sNodePools, resp, err := c.CloudApiV6Services.K8s().ListNodePools(k8sClusterId) - if err != nil { - return err - } - - k8sNodePoolsItems, ok := k8sNodePools.GetItemsOk() - if !ok || k8sNodePoolsItems == nil { - return fmt.Errorf("could not get items of Kubernetes Nodepools") - } - - if len(*k8sNodePoolsItems) <= 0 { - return fmt.Errorf("no Kubernetes Nodepools found") - } - - c.Msg("K8sNodePools to be deleted:") - - var multiErr error - for _, dc := range *k8sNodePoolsItems { - id := dc.GetId() - name := dc.GetProperties().Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete K8sNodePool with Id: %s , Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.K8s().DeleteNodePool(k8sClusterId, *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.KubernetesNodePool]{ + Resource: "Kubernetes Nodepool", + List: func() ([]ionoscloud.KubernetesNodePool, error) { + k8sNodePools, _, err := c.CloudApiV6Services.K8s().ListNodePools(k8sClusterId) + if err != nil { + return nil, err + } - if multiErr != nil { - return multiErr - } + items, ok := k8sNodePools.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Kubernetes Nodepools") + } - return nil + return *items, nil + }, + Summary: func(np ionoscloud.KubernetesNodePool) string { + var id string + if v, ok := np.GetIdOk(); ok && v != nil { + id = *v + } + summary := fmt.Sprintf("id: %s", id) + if props, ok := np.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil && *name != "" { + summary = fmt.Sprintf("%s (name: %s)", summary, *name) + } + } + return summary + }, + ID: func(np ionoscloud.KubernetesNodePool) string { + if id, ok := np.GetIdOk(); ok && id != nil { + return *id + } + return "" + }, + Delete: func(np ionoscloud.KubernetesNodePool) error { + resp, err := c.CloudApiV6Services.K8s().DeleteNodePool(k8sClusterId, *np.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } diff --git a/commands/compute/lan/run_lan.go b/commands/compute/lan/run_lan.go index 51bff9524f..ff59a4578e 100644 --- a/commands/compute/lan/run_lan.go +++ b/commands/compute/lan/run_lan.go @@ -2,7 +2,6 @@ package lan import ( "context" - "errors" "fmt" "strings" "time" @@ -275,49 +274,46 @@ func DeleteAllLans(c *core.CommandConfig) error { dcId := viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgDataCenterId)) c.Verbose(constants.DatacenterId, dcId) - c.Verbose("Getting Lans...") - lans, resp, err := c.CloudApiV6Services.Lans().List(dcId) - if err != nil { - return err - } - - lansItems, ok := lans.GetItemsOk() - if !ok || lansItems == nil { - return fmt.Errorf("could not get items of Lans") - } - - if len(*lansItems) <= 0 { - return fmt.Errorf("no Lans found") - } - - c.Msg("Lans to be deleted:") - - var multiErr error - for _, lan := range *lansItems { - id := lan.GetId() - name := lan.GetProperties().GetName() - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the Lan with Id: %s , Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.Lans().Delete(dcId, *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.Lan]{ + Resource: "lan", + List: func() ([]ionoscloud.Lan, error) { + lans, _, err := c.CloudApiV6Services.Lans().List(dcId) + if err != nil { + return nil, err + } - if multiErr != nil { - return multiErr - } + items, ok := lans.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Lans") + } - return nil + return *items, nil + }, + Summary: func(lan ionoscloud.Lan) string { + var id, name string + if lan.Id != nil { + id = *lan.Id + } + if p := lan.Properties; p != nil && p.Name != nil { + name = *p.Name + } + return fmt.Sprintf("%s (id: %s)", name, id) + }, + ID: func(lan ionoscloud.Lan) string { + if lan.Id != nil { + return *lan.Id + } + return "" + }, + Delete: func(lan ionoscloud.Lan) error { + resp, err := c.CloudApiV6Services.Lans().Delete(dcId, *lan.Id) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } func GetIPv6CidrBlockFromLAN(lan ionoscloud.Lan) (string, error) { diff --git a/commands/compute/loadbalancer/run_loadbalancer.go b/commands/compute/loadbalancer/run_loadbalancer.go index 8d6188875b..41c1ecdcd1 100644 --- a/commands/compute/loadbalancer/run_loadbalancer.go +++ b/commands/compute/loadbalancer/run_loadbalancer.go @@ -1,7 +1,6 @@ package loadbalancer import ( - "errors" "fmt" "time" @@ -196,47 +195,49 @@ func DeleteAllLoadBalancers(c *core.CommandConfig) error { dcid := viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgDataCenterId)) c.Verbose(constants.DatacenterId, dcid) - c.Verbose("Getting LoadBalancers...") - loadBalancers, resp, err := c.CloudApiV6Services.Loadbalancers().List(dcid) - if err != nil { - return err - } - - loadBalancersItems, ok := loadBalancers.GetItemsOk() - if !ok || loadBalancersItems == nil { - return fmt.Errorf("could not get items of Load Balancers") - } - - if len(*loadBalancersItems) <= 0 { - return fmt.Errorf("no Load Balancers found") - } - - var multiErr error - for _, lb := range *loadBalancersItems { - name := lb.GetProperties().Name - id := lb.GetId() - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the LoadBalancer with Id: %s , Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.Loadbalancers().Delete(dcid, *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } - - if multiErr != nil { - return multiErr - } - - return nil + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.Loadbalancer]{ + Resource: "Load Balancer", + List: func() ([]ionoscloud.Loadbalancer, error) { + loadBalancers, _, err := c.CloudApiV6Services.Loadbalancers().List(dcid) + if err != nil { + return nil, err + } + items, ok := loadBalancers.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Load Balancers") + } + return *items, nil + }, + Summary: func(lb ionoscloud.Loadbalancer) string { + summary := "" + if props, ok := lb.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil { + summary += *name + } + if ip, ok := props.GetIpOk(); ok && ip != nil && *ip != "" { + summary += fmt.Sprintf(" (public IP: %s)", *ip) + } + } + if id, ok := lb.GetIdOk(); ok && id != nil { + summary += fmt.Sprintf(" (id: %s)", *id) + } + return summary + }, + ID: func(lb ionoscloud.Loadbalancer) string { + if id := lb.GetId(); id != nil { + return *id + } + return "" + }, + Delete: func(lb ionoscloud.Loadbalancer) error { + resp, err := c.CloudApiV6Services.Loadbalancers().Delete(dcid, *lb.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } func PreRunDataCenterId(c *core.PreCommandConfig) error { diff --git a/commands/compute/natgateway/flowlog/run_natgateway_flowlog.go b/commands/compute/natgateway/flowlog/run_natgateway_flowlog.go index 00a193df64..382f8b192f 100644 --- a/commands/compute/natgateway/flowlog/run_natgateway_flowlog.go +++ b/commands/compute/natgateway/flowlog/run_natgateway_flowlog.go @@ -1,7 +1,6 @@ package flowlog import ( - "errors" "fmt" "github.com/ionos-cloud/ionosctl/v6/commands/compute/helpers" @@ -148,44 +147,44 @@ func DeleteAllNatGatewayFlowLogs(c *core.CommandConfig) error { c.Verbose(constants.DatacenterId, dcId) c.Verbose("NatGateway ID: %v", natgatewayId) - c.Verbose("Getting NatGatewayFlowLogs...") - flowlogs, resp, err := c.CloudApiV6Services.NatGateways().ListFlowLogs(dcId, natgatewayId) - if err != nil { - return err - } - - natgatewaysItems, ok := flowlogs.GetItemsOk() - if !ok || natgatewaysItems == nil { - return fmt.Errorf("could not get items of NAT Gateway FlowLogs") - } - - if len(*natgatewaysItems) <= 0 { - return fmt.Errorf("no Nat Gateway FlowLogs found") - } - - var multiErr error - for _, natgateway := range *natgatewaysItems { - name := natgateway.GetProperties().Name - id := natgateway.GetId() - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the NatGatewayFlowLog with Id: %s, Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.NatGateways().DeleteFlowLog(dcId, natgatewayId, *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - } - - } - - if multiErr != nil { - return multiErr - } - - return nil + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.FlowLog]{ + Resource: "NAT Gateway FlowLog", + List: func() ([]ionoscloud.FlowLog, error) { + flowlogs, _, err := c.CloudApiV6Services.NatGateways().ListFlowLogs(dcId, natgatewayId) + if err != nil { + return nil, err + } + items, ok := flowlogs.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of NAT Gateway FlowLogs") + } + return *items, nil + }, + Summary: func(fl ionoscloud.FlowLog) string { + summary := "" + if props, ok := fl.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil { + summary += *name + } + } + if id, ok := fl.GetIdOk(); ok && id != nil { + summary += fmt.Sprintf(" (id: %s)", *id) + } + return summary + }, + ID: func(fl ionoscloud.FlowLog) string { + if id := fl.GetId(); id != nil { + return *id + } + return "" + }, + Delete: func(fl ionoscloud.FlowLog) error { + resp, err := c.CloudApiV6Services.NatGateways().DeleteFlowLog(dcId, natgatewayId, *fl.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } diff --git a/commands/compute/natgateway/rule/run_natgateway_rule.go b/commands/compute/natgateway/rule/run_natgateway_rule.go index 85ead412db..be8a3321df 100644 --- a/commands/compute/natgateway/rule/run_natgateway_rule.go +++ b/commands/compute/natgateway/rule/run_natgateway_rule.go @@ -1,7 +1,6 @@ package rule import ( - "errors" "fmt" "strings" @@ -214,44 +213,47 @@ func DeleteAllNatgatewayRules(c *core.CommandConfig) error { c.Verbose(constants.DatacenterId, dcId) c.Verbose("NatGateway ID: %v", natGatewayId) - c.Verbose("Getting NatGateway Rules...") - natGatewayRules, resp, err := c.CloudApiV6Services.NatGateways().ListRules(dcId, natGatewayId) - if err != nil { - return err - } - - natGatewayRuleItems, ok := natGatewayRules.GetItemsOk() - if !ok || natGatewayRuleItems == nil { - return fmt.Errorf("could not get items of NAT Gateway Rules") - } - - if len(*natGatewayRuleItems) <= 0 { - return fmt.Errorf("no NAT Gateway Rules found") - } - - var multiErr error - for _, natGateway := range *natGatewayRuleItems { - id := natGateway.GetId() - name := natGateway.GetProperties().Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the NatGatewayRule with Id: %s, Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.NatGateways().DeleteRule(dcId, natGatewayId, *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - } - - } - - if multiErr != nil { - return multiErr - } - - return nil + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.NatGatewayRule]{ + Resource: "NAT Gateway Rule", + List: func() ([]ionoscloud.NatGatewayRule, error) { + natGatewayRules, _, err := c.CloudApiV6Services.NatGateways().ListRules(dcId, natGatewayId) + if err != nil { + return nil, err + } + items, ok := natGatewayRules.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of NAT Gateway Rules") + } + return *items, nil + }, + Summary: func(rule ionoscloud.NatGatewayRule) string { + summary := "" + if props, ok := rule.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil { + summary += *name + } + if ip, ok := props.GetPublicIpOk(); ok && ip != nil && *ip != "" { + summary += fmt.Sprintf(" (public IP: %s)", *ip) + } + } + if id, ok := rule.GetIdOk(); ok && id != nil { + summary += fmt.Sprintf(" (id: %s)", *id) + } + return summary + }, + ID: func(rule ionoscloud.NatGatewayRule) string { + if id := rule.GetId(); id != nil { + return *id + } + return "" + }, + Delete: func(rule ionoscloud.NatGatewayRule) error { + resp, err := c.CloudApiV6Services.NatGateways().DeleteRule(dcId, natGatewayId, *rule.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } diff --git a/commands/compute/natgateway/run_natgateway.go b/commands/compute/natgateway/run_natgateway.go index ce00b7aaf2..de3f58bdcd 100644 --- a/commands/compute/natgateway/run_natgateway.go +++ b/commands/compute/natgateway/run_natgateway.go @@ -1,7 +1,6 @@ package natgateway import ( - "errors" "fmt" "time" @@ -205,45 +204,47 @@ func DeleteAllNatgateways(c *core.CommandConfig) error { dcId := viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgDataCenterId)) c.Verbose(constants.DatacenterId, dcId) - c.Verbose("Getting NatGateways...") - natGateways, resp, err := c.CloudApiV6Services.NatGateways().List(dcId) - if err != nil { - return err - } - - natGatewayItems, ok := natGateways.GetItemsOk() - if !ok || natGatewayItems == nil { - return fmt.Errorf("could not get items of NAT Gateway") - } - - if len(*natGatewayItems) <= 0 { - return fmt.Errorf("no NAT Gateways found") - } - - var multiErr error - for _, natGateway := range *natGatewayItems { - name := natGateway.GetProperties().Name - id := natGateway.GetId() - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the NAT Gateway with Id: %s , Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.NatGateways().Delete(dcId, *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } - - if multiErr != nil { - return multiErr - } - - return nil + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.NatGateway]{ + Resource: "NAT Gateway", + List: func() ([]ionoscloud.NatGateway, error) { + natGateways, _, err := c.CloudApiV6Services.NatGateways().List(dcId) + if err != nil { + return nil, err + } + items, ok := natGateways.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of NAT Gateway") + } + return *items, nil + }, + Summary: func(ng ionoscloud.NatGateway) string { + summary := "" + if props, ok := ng.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil { + summary += *name + } + if ips, ok := props.GetPublicIpsOk(); ok && ips != nil && len(*ips) > 0 { + summary += fmt.Sprintf(" (public IPs: %v)", *ips) + } + } + if id, ok := ng.GetIdOk(); ok && id != nil { + summary += fmt.Sprintf(" (id: %s)", *id) + } + return summary + }, + ID: func(ng ionoscloud.NatGateway) string { + if id := ng.GetId(); id != nil { + return *id + } + return "" + }, + Delete: func(ng ionoscloud.NatGateway) error { + resp, err := c.CloudApiV6Services.NatGateways().Delete(dcId, *ng.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } diff --git a/commands/compute/networkloadbalancer/flowlog/run_networkloadbalancer_flowlog.go b/commands/compute/networkloadbalancer/flowlog/run_networkloadbalancer_flowlog.go index a56d58f445..bc3a74f8da 100644 --- a/commands/compute/networkloadbalancer/flowlog/run_networkloadbalancer_flowlog.go +++ b/commands/compute/networkloadbalancer/flowlog/run_networkloadbalancer_flowlog.go @@ -1,7 +1,6 @@ package flowlog import ( - "errors" "fmt" "github.com/ionos-cloud/ionosctl/v6/commands/compute/helpers" @@ -152,45 +151,44 @@ func DeleteAllNetworkLoadBalancerFlowLogs(c *core.CommandConfig) error { c.Verbose(constants.DatacenterId, dcId) c.Verbose("Network Load Balancer ID: %v", networkLoadBalancerId) - c.Verbose("Getting Network Load Balancer FlowLogs...") - flowLogs, resp, err := c.CloudApiV6Services.NetworkLoadBalancers().ListFlowLogs(dcId, networkLoadBalancerId) - if err != nil { - return err - } - - flowLogsItems, ok := flowLogs.GetItemsOk() - if !ok || flowLogsItems == nil { - return fmt.Errorf("could not get items of Network Load Balancer FlowLogs") - } - - if len(*flowLogsItems) <= 0 { - return fmt.Errorf("no Network Load Balancer FlowLogs found") - } - - var multiErr error - for _, flowLog := range *flowLogsItems { - id := flowLog.GetId() - name := flowLog.GetProperties().Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the Network Load Balancer FlowLog with Id: %s, Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.NetworkLoadBalancers().DeleteFlowLog(dcId, networkLoadBalancerId, *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } - - if multiErr != nil { - return multiErr - } - - return nil + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.FlowLog]{ + Resource: "Network Load Balancer FlowLog", + List: func() ([]ionoscloud.FlowLog, error) { + flowLogs, _, err := c.CloudApiV6Services.NetworkLoadBalancers().ListFlowLogs(dcId, networkLoadBalancerId) + if err != nil { + return nil, err + } + items, ok := flowLogs.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Network Load Balancer FlowLogs") + } + return *items, nil + }, + Summary: func(fl ionoscloud.FlowLog) string { + summary := "" + if props, ok := fl.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil { + summary += *name + } + } + if id, ok := fl.GetIdOk(); ok && id != nil { + summary += fmt.Sprintf(" (id: %s)", *id) + } + return summary + }, + ID: func(fl ionoscloud.FlowLog) string { + if id := fl.GetId(); id != nil { + return *id + } + return "" + }, + Delete: func(fl ionoscloud.FlowLog) error { + resp, err := c.CloudApiV6Services.NetworkLoadBalancers().DeleteFlowLog(dcId, networkLoadBalancerId, *fl.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } diff --git a/commands/compute/networkloadbalancer/rule/run_networkloadbalancer_rule.go b/commands/compute/networkloadbalancer/rule/run_networkloadbalancer_rule.go index 79c5081bfa..edb38da7da 100644 --- a/commands/compute/networkloadbalancer/rule/run_networkloadbalancer_rule.go +++ b/commands/compute/networkloadbalancer/rule/run_networkloadbalancer_rule.go @@ -1,7 +1,6 @@ package rule import ( - "errors" "fmt" "strings" @@ -229,45 +228,47 @@ func DeleteAllNetworkLoadBalancerForwardingRules(c *core.CommandConfig) error { c.Verbose(constants.DatacenterId, dcId) c.Verbose("Network Load Balancer ID: %v", loadBalancerId) - c.Verbose("Getting Network Load Balancer Forwarding Rules...") - nlbForwardingRules, resp, err := c.CloudApiV6Services.NetworkLoadBalancers().ListForwardingRules(dcId, loadBalancerId) - if err != nil { - return err - } - - nlbForwardingRulesItems, ok := nlbForwardingRules.GetItemsOk() - if !ok || nlbForwardingRulesItems == nil { - return fmt.Errorf("could not get items of Network Load Balancer Forwarding Rules") - } - - if len(*nlbForwardingRulesItems) <= 0 { - return fmt.Errorf("no Network Load Balancer Forwarding Rules found") - } - - var multiErr error - for _, nlbForwardingRule := range *nlbForwardingRulesItems { - name := nlbForwardingRule.GetProperties().Name - id := nlbForwardingRule.GetId() - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the Network Load Balancer Forwarding Rule with Id: %s, Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.NetworkLoadBalancers().DeleteForwardingRule(dcId, loadBalancerId, *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } - - if multiErr != nil { - return multiErr - } - - return nil + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.NetworkLoadBalancerForwardingRule]{ + Resource: "Network Load Balancer Forwarding Rule", + List: func() ([]ionoscloud.NetworkLoadBalancerForwardingRule, error) { + nlbForwardingRules, _, err := c.CloudApiV6Services.NetworkLoadBalancers().ListForwardingRules(dcId, loadBalancerId) + if err != nil { + return nil, err + } + items, ok := nlbForwardingRules.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Network Load Balancer Forwarding Rules") + } + return *items, nil + }, + Summary: func(rule ionoscloud.NetworkLoadBalancerForwardingRule) string { + summary := "" + if props, ok := rule.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil { + summary += *name + } + if ip, ok := props.GetListenerIpOk(); ok && ip != nil && *ip != "" { + summary += fmt.Sprintf(" (listenerIp: %s)", *ip) + } + } + if id, ok := rule.GetIdOk(); ok && id != nil { + summary += fmt.Sprintf(" (id: %s)", *id) + } + return summary + }, + ID: func(rule ionoscloud.NetworkLoadBalancerForwardingRule) string { + if id := rule.GetId(); id != nil { + return *id + } + return "" + }, + Delete: func(rule ionoscloud.NetworkLoadBalancerForwardingRule) error { + resp, err := c.CloudApiV6Services.NetworkLoadBalancers().DeleteForwardingRule(dcId, loadBalancerId, *rule.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } diff --git a/commands/compute/networkloadbalancer/run_networkloadbalancer.go b/commands/compute/networkloadbalancer/run_networkloadbalancer.go index be2a6b6468..4365197204 100644 --- a/commands/compute/networkloadbalancer/run_networkloadbalancer.go +++ b/commands/compute/networkloadbalancer/run_networkloadbalancer.go @@ -1,7 +1,6 @@ package networkloadbalancer import ( - "errors" "fmt" "time" @@ -237,45 +236,47 @@ func DeleteAllNetworkLoadBalancers(c *core.CommandConfig) error { dcId := viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgDataCenterId)) c.Verbose(constants.DatacenterId, dcId) - c.Verbose("Getting Network Load Balancers...") - networkLoadBalancers, resp, err := c.CloudApiV6Services.NetworkLoadBalancers().List(dcId) - if err != nil { - return err - } - - nlbItems, ok := networkLoadBalancers.GetItemsOk() - if !ok || nlbItems == nil { - return fmt.Errorf("could not get items of Network Load Balancers") - } - - if len(*nlbItems) <= 0 { - return fmt.Errorf("no Network Load Balancers found") - } - - var multiErr error - for _, networkLoadBalancer := range *nlbItems { - id := networkLoadBalancer.GetId() - name := networkLoadBalancer.Properties.Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the Network Load Balancer with Id: %s, Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.NetworkLoadBalancers().Delete(dcId, *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } - - if multiErr != nil { - return multiErr - } - - return nil + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.NetworkLoadBalancer]{ + Resource: "Network Load Balancer", + List: func() ([]ionoscloud.NetworkLoadBalancer, error) { + networkLoadBalancers, _, err := c.CloudApiV6Services.NetworkLoadBalancers().List(dcId) + if err != nil { + return nil, err + } + items, ok := networkLoadBalancers.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Network Load Balancers") + } + return *items, nil + }, + Summary: func(nlb ionoscloud.NetworkLoadBalancer) string { + summary := "" + if props, ok := nlb.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil { + summary += *name + } + if ips, ok := props.GetIpsOk(); ok && ips != nil && len(*ips) > 0 { + summary += fmt.Sprintf(" (public IPs: %v)", *ips) + } + } + if id, ok := nlb.GetIdOk(); ok && id != nil { + summary += fmt.Sprintf(" (id: %s)", *id) + } + return summary + }, + ID: func(nlb ionoscloud.NetworkLoadBalancer) string { + if id := nlb.GetId(); id != nil { + return *id + } + return "" + }, + Delete: func(nlb ionoscloud.NetworkLoadBalancer) error { + resp, err := c.CloudApiV6Services.NetworkLoadBalancers().Delete(dcId, *nlb.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } diff --git a/commands/compute/nic/run_nic.go b/commands/compute/nic/run_nic.go index 93ef3d82e9..a0a8832fd0 100644 --- a/commands/compute/nic/run_nic.go +++ b/commands/compute/nic/run_nic.go @@ -1,7 +1,6 @@ package nic import ( - "errors" "fmt" "net" "strings" @@ -222,47 +221,55 @@ func DeleteAllNics(c *core.CommandConfig) error { c.Verbose(constants.DatacenterId, dcId) c.Verbose("Server ID: %v", serverId) - c.Verbose("Getting NICs...") - nics, resp, err := c.CloudApiV6Services.Nics().List(dcId, serverId) - if err != nil { - return err - } - - nicsItems, ok := nics.GetItemsOk() - if !ok || nicsItems == nil { - return fmt.Errorf("could not get items of NICs") - } - - if len(*nicsItems) <= 0 { - return fmt.Errorf("no NICs found") - } - - var multiErr error - for _, nic := range *nicsItems { - id := nic.GetId() - name := nic.GetProperties().Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the Nic with Id: %s, Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.Nics().Delete(dcId, serverId, *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.Nic]{ + Resource: "nic", + List: func() ([]ionoscloud.Nic, error) { + nics, _, err := c.CloudApiV6Services.Nics().List(dcId, serverId) + if err != nil { + return nil, err + } - if multiErr != nil { - return multiErr - } + items, ok := nics.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of NICs") + } - return nil + return *items, nil + }, + Summary: func(nic ionoscloud.Nic) string { + var id, name string + var ips []string + if nic.Id != nil { + id = *nic.Id + } + if p := nic.Properties; p != nil { + if p.Name != nil { + name = *p.Name + } + if p.Ips != nil { + ips = *p.Ips + } + } + if len(ips) > 0 { + return fmt.Sprintf("%s (id: %s, ips: %s)", name, id, strings.Join(ips, ", ")) + } + return fmt.Sprintf("%s (id: %s)", name, id) + }, + ID: func(nic ionoscloud.Nic) string { + if nic.Id != nil { + return *nic.Id + } + return "" + }, + Delete: func(nic ionoscloud.Nic) error { + resp, err := c.CloudApiV6Services.Nics().Delete(dcId, serverId, *nic.Id) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } func validateIPv6IPs(cidr string, ips ...string) error { diff --git a/commands/compute/pcc/run_pcc.go b/commands/compute/pcc/run_pcc.go index aeceb8e7d7..271bda026e 100644 --- a/commands/compute/pcc/run_pcc.go +++ b/commands/compute/pcc/run_pcc.go @@ -1,7 +1,6 @@ package pcc import ( - "errors" "fmt" "github.com/ionos-cloud/ionosctl/v6/internal/constants" @@ -159,47 +158,51 @@ func getPccInfo(oldUser *resources.PrivateCrossConnect, c *core.CommandConfig) * } func DeleteAllPccs(c *core.CommandConfig) error { - c.Verbose("Getting PrivateCrossConnects...") - - pccs, resp, err := c.CloudApiV6Services.Pccs().List() - if err != nil { - return err - } - - pccsItems, ok := pccs.GetItemsOk() - if !ok || pccsItems == nil { - return fmt.Errorf("could not get items of PrivateCrossConnects") - } - - if len(*pccsItems) <= 0 { - return fmt.Errorf("no PrivateCrossConnects found") - } - - var multiErr error - for _, pcc := range *pccsItems { - id := pcc.GetId() - name := pcc.GetProperties().Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the PrivateCrossConnect with Id: %s, Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.Pccs().Delete(*id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.PrivateCrossConnect]{ + Resource: "PrivateCrossConnect", + List: func() ([]ionoscloud.PrivateCrossConnect, error) { + pccs, _, err := c.CloudApiV6Services.Pccs().List() + if err != nil { + return nil, err + } - if multiErr != nil { - return multiErr - } + items, ok := pccs.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of PrivateCrossConnects") + } - return nil + return *items, nil + }, + Summary: func(pcc ionoscloud.PrivateCrossConnect) string { + var id string + if v, ok := pcc.GetIdOk(); ok && v != nil { + id = *v + } + summary := fmt.Sprintf("id: %s", id) + if props, ok := pcc.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil && *name != "" { + summary = fmt.Sprintf("%s (name: %s)", summary, *name) + } + if desc, ok := props.GetDescriptionOk(); ok && desc != nil && *desc != "" { + summary = fmt.Sprintf("%s (desc: %s)", summary, *desc) + } + } + return summary + }, + ID: func(pcc ionoscloud.PrivateCrossConnect) string { + if id, ok := pcc.GetIdOk(); ok && id != nil { + return *id + } + return "" + }, + Delete: func(pcc ionoscloud.PrivateCrossConnect) error { + resp, err := c.CloudApiV6Services.Pccs().Delete(*pcc.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } func RunPccPeersList(c *core.CommandConfig) error { diff --git a/commands/compute/server/run_server.go b/commands/compute/server/run_server.go index 67785b15f3..982f18f3a0 100644 --- a/commands/compute/server/run_server.go +++ b/commands/compute/server/run_server.go @@ -832,47 +832,46 @@ func DeleteAllServers(c *core.CommandConfig) error { dcId := viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgDataCenterId)) c.Verbose(constants.DatacenterId, dcId) - c.Verbose("Getting Servers...") - servers, resp, err := c.CloudApiV6Services.Servers().List(dcId) - if err != nil { - return err - } - - serversItems, ok := servers.GetItemsOk() - if !ok || serversItems == nil { - return fmt.Errorf("could not get items of Servers") - } - - if len(*serversItems) <= 0 { - return fmt.Errorf("no Servers found") - } - - var multiErr error - for _, server := range *serversItems { - id := server.GetId() - name := server.Properties.Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the Server with Id: %s, Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.Servers().Delete(dcId, *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.Server]{ + Resource: "server", + List: func() ([]ionoscloud.Server, error) { + servers, _, err := c.CloudApiV6Services.Servers().List(dcId) + if err != nil { + return nil, err + } - if multiErr != nil { - return multiErr - } + items, ok := servers.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Servers") + } - return nil + return *items, nil + }, + Summary: func(server ionoscloud.Server) string { + var id, name string + if server.Id != nil { + id = *server.Id + } + if p := server.Properties; p != nil && p.Name != nil { + name = *p.Name + } + return fmt.Sprintf("%s (id: %s)", name, id) + }, + ID: func(server ionoscloud.Server) string { + if server.Id != nil { + return *server.Id + } + return "" + }, + Delete: func(server ionoscloud.Server) error { + resp, err := c.CloudApiV6Services.Servers().Delete(dcId, *server.Id) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } func DefaultCpuFamily(c *core.CommandConfig) (string, error) { diff --git a/commands/compute/share/run_share.go b/commands/compute/share/run_share.go index 49e6a64b78..cca1364bf9 100644 --- a/commands/compute/share/run_share.go +++ b/commands/compute/share/run_share.go @@ -1,7 +1,6 @@ package share import ( - "errors" "fmt" "time" @@ -234,43 +233,41 @@ func DeleteAllShares(c *core.CommandConfig) error { groupId := viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgGroupId)) c.Verbose("Group ID: %v", groupId) - c.Verbose("Getting Group Shares...") - groupShares, resp, err := c.CloudApiV6Services.Groups().ListShares(groupId) - if err != nil { - return err - } - - groupSharesItems, ok := groupShares.GetItemsOk() - if !ok || groupSharesItems == nil { - return fmt.Errorf("could not get items of Group Shares") - } - - if len(*groupSharesItems) <= 0 { - return fmt.Errorf("no Group Shares found") - } - - var multiErr error - for _, share := range *groupSharesItems { - id := share.GetId() - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the GroupShare with Id: %s", *id), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.Groups().RemoveShare(groupId, *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.GroupShare]{ + Resource: "GroupShare", + List: func() ([]ionoscloud.GroupShare, error) { + groupShares, _, err := c.CloudApiV6Services.Groups().ListShares(groupId) + if err != nil { + return nil, err + } - if multiErr != nil { - return multiErr - } + items, ok := groupShares.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Group Shares") + } - return nil + return *items, nil + }, + Summary: func(share ionoscloud.GroupShare) string { + id := "" + if v, ok := share.GetIdOk(); ok && v != nil { + id = *v + } + return fmt.Sprintf("id: %s", id) + }, + ID: func(share ionoscloud.GroupShare) string { + if id, ok := share.GetIdOk(); ok && id != nil { + return *id + } + return "" + }, + Delete: func(share ionoscloud.GroupShare) error { + resp, err := c.CloudApiV6Services.Groups().RemoveShare(groupId, *share.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } diff --git a/commands/compute/snapshot/run_snapshot.go b/commands/compute/snapshot/run_snapshot.go index 898831ed76..3aff99b127 100644 --- a/commands/compute/snapshot/run_snapshot.go +++ b/commands/compute/snapshot/run_snapshot.go @@ -1,7 +1,6 @@ package snapshot import ( - "errors" "fmt" "github.com/ionos-cloud/ionosctl/v6/internal/constants" @@ -10,6 +9,7 @@ import ( "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" cloudapiv6 "github.com/ionos-cloud/ionosctl/v6/services/cloudapi-v6" "github.com/ionos-cloud/ionosctl/v6/services/cloudapi-v6/resources" + ionoscloud "github.com/ionos-cloud/sdk-go/v6" "github.com/spf13/viper" ) @@ -248,47 +248,58 @@ func getSnapshotPropertiesSet(c *core.CommandConfig) resources.SnapshotPropertie } func DeleteAllSnapshots(c *core.CommandConfig) error { - c.Verbose("Getting Snapshots...") - - snapshots, resp, err := c.CloudApiV6Services.Snapshots().List() - if err != nil { - return err - } - - snapshotsItems, ok := snapshots.GetItemsOk() - if !ok || snapshotsItems == nil { - return fmt.Errorf("could not get items of Snapshots") - } - - if len(*snapshotsItems) <= 0 { - return fmt.Errorf("no Snapshots found") - } - - var multiErr error - for _, snapshot := range *snapshotsItems { - id := snapshot.GetId() - name := snapshot.GetProperties().Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the Snapshot with Id: %s, Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.Snapshots().Delete(*id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } - - if multiErr != nil { - return multiErr - } - - return nil + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.Snapshot]{ + Resource: "snapshot", + List: func() ([]ionoscloud.Snapshot, error) { + snapshots, _, err := c.CloudApiV6Services.Snapshots().List() + if err != nil { + return nil, err + } + + items, ok := snapshots.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Snapshots") + } + + return *items, nil + }, + Summary: func(snapshot ionoscloud.Snapshot) string { + var id, name, location, description string + if snapshot.Id != nil { + id = *snapshot.Id + } + if p := snapshot.Properties; p != nil { + if p.Name != nil { + name = *p.Name + } + if p.Location != nil { + location = *p.Location + } + if p.Description != nil { + description = *p.Description + } + } + + s := fmt.Sprintf("%s (id: %s, location: %s)", name, id, location) + if description != "" { + s = fmt.Sprintf("%s (id: %s, location: %s, desc: %s)", name, id, location, description) + } + return s + }, + ID: func(snapshot ionoscloud.Snapshot) string { + if snapshot.Id != nil { + return *snapshot.Id + } + return "" + }, + Delete: func(snapshot ionoscloud.Snapshot) error { + resp, err := c.CloudApiV6Services.Snapshots().Delete(*snapshot.Id) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } func PreRunDcVolumeIds(c *core.PreCommandConfig) error { diff --git a/commands/compute/targetgroup/run_targetgroup.go b/commands/compute/targetgroup/run_targetgroup.go index 04a2328300..efd908d733 100644 --- a/commands/compute/targetgroup/run_targetgroup.go +++ b/commands/compute/targetgroup/run_targetgroup.go @@ -1,7 +1,6 @@ package targetgroup import ( - "errors" "fmt" "github.com/ionos-cloud/ionosctl/v6/internal/constants" @@ -116,47 +115,48 @@ func RunTargetGroupDelete(c *core.CommandConfig) error { } func DeleteAllTargetGroup(c *core.CommandConfig) error { - c.Msg("Getting Target Groups...") - - targetGroups, resp, err := c.CloudApiV6Services.TargetGroups().List() - if err != nil { - return err - } - - targetGroupItems, ok := targetGroups.GetItemsOk() - if !ok || targetGroupItems == nil { - return fmt.Errorf("could not get items of Target Groups") - } - - if len(*targetGroupItems) <= 0 { - return fmt.Errorf("no Target Groups found") - } - - var multiErr error - for _, tg := range *targetGroupItems { - id := tg.GetId() - name := tg.GetProperties().Name - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the Target Group with Id: %s, Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.TargetGroups().Delete(*id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } - - if multiErr != nil { - return multiErr - } - - return nil + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.TargetGroup]{ + Resource: "Target Group", + List: func() ([]ionoscloud.TargetGroup, error) { + targetGroups, _, err := c.CloudApiV6Services.TargetGroups().List() + if err != nil { + return nil, err + } + + items, ok := targetGroups.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Target Groups") + } + + return *items, nil + }, + Summary: func(tg ionoscloud.TargetGroup) string { + var id string + if v, ok := tg.GetIdOk(); ok && v != nil { + id = *v + } + summary := fmt.Sprintf("id: %s", id) + if props, ok := tg.GetPropertiesOk(); ok && props != nil { + if name, ok := props.GetNameOk(); ok && name != nil && *name != "" { + summary = fmt.Sprintf("%s (name: %s)", summary, *name) + } + } + return summary + }, + ID: func(tg ionoscloud.TargetGroup) string { + if id, ok := tg.GetIdOk(); ok && id != nil { + return *id + } + return "" + }, + Delete: func(tg ionoscloud.TargetGroup) error { + resp, err := c.CloudApiV6Services.TargetGroups().Delete(*tg.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } func getTargetGroupNew(c *core.CommandConfig) resources.TargetGroup { diff --git a/commands/compute/user/run_s3key.go b/commands/compute/user/run_s3key.go index a8cc79b025..a6d218dbb6 100644 --- a/commands/compute/user/run_s3key.go +++ b/commands/compute/user/run_s3key.go @@ -1,7 +1,6 @@ package user import ( - "errors" "fmt" "github.com/ionos-cloud/ionosctl/v6/internal/constants" @@ -134,43 +133,41 @@ func DeleteAllS3keys(c *core.CommandConfig) error { userId := viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgUserId)) c.Verbose("User ID: %v", userId) - c.Verbose("Getting S3 Keys...") - s3Keys, resp, err := c.CloudApiV6Services.S3Keys().List(userId) - if err != nil { - return err - } + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.S3Key]{ + Resource: "S3Key", + List: func() ([]ionoscloud.S3Key, error) { + s3Keys, _, err := c.CloudApiV6Services.S3Keys().List(userId) + if err != nil { + return nil, err + } - s3KeysItems, ok := s3Keys.GetItemsOk() - if !ok || s3KeysItems == nil { - return fmt.Errorf("could not get items of S3 Keys") - } - - if len(*s3KeysItems) <= 0 { - return fmt.Errorf("no S3 Keys found") - } - - var multiErr error - for _, s3Key := range *s3KeysItems { - id := s3Key.GetId() - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the S3Key with Id: %s", *id), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.S3Keys().Delete(userId, *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } + items, ok := s3Keys.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of S3 Keys") + } - } - - if multiErr != nil { - return multiErr - } - - return nil + return *items, nil + }, + Summary: func(s3Key ionoscloud.S3Key) string { + id := "" + if v, ok := s3Key.GetIdOk(); ok && v != nil { + id = *v + } + return fmt.Sprintf("id: %s", id) + }, + ID: func(s3Key ionoscloud.S3Key) string { + if id, ok := s3Key.GetIdOk(); ok && id != nil { + return *id + } + return "" + }, + Delete: func(s3Key ionoscloud.S3Key) error { + resp, err := c.CloudApiV6Services.S3Keys().Delete(userId, *s3Key.GetId()) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } diff --git a/commands/compute/user/run_user.go b/commands/compute/user/run_user.go index ab46b5218f..e08f4338d6 100644 --- a/commands/compute/user/run_user.go +++ b/commands/compute/user/run_user.go @@ -1,7 +1,6 @@ package user import ( - "errors" "fmt" "github.com/ionos-cloud/ionosctl/v6/internal/constants" @@ -224,47 +223,51 @@ func getUserInfo(oldUser *resources.User, c *core.CommandConfig) *resources.User } func DeleteAllUsers(c *core.CommandConfig) error { - c.Verbose("Getting Users...") - - users, _, err := c.CloudApiV6Services.Users().List() - if err != nil { - return err - } - - usersItems, ok := users.GetItemsOk() - if !ok || usersItems == nil { - return fmt.Errorf("could not get items of Users") - } - - if len(*usersItems) <= 0 { - return fmt.Errorf("no Users found") - } - - var multiErr error - for _, user := range *usersItems { - id := user.GetId() - lastname := user.GetProperties().Lastname - firstname := user.GetProperties().Firstname - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the User with Id: %s, LastName: %s, FirstName: %s", *id, *lastname, *firstname), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - _, err = c.CloudApiV6Services.Users().Delete(*id) - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - c.Msg(constants.MessageDeletingAll, c.Resource, *id) - - } + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.User]{ + Resource: "User", + List: func() ([]ionoscloud.User, error) { + users, _, err := c.CloudApiV6Services.Users().List() + if err != nil { + return nil, err + } - if multiErr != nil { - return multiErr - } + items, ok := users.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Users") + } - return nil + return *items, nil + }, + Summary: func(user ionoscloud.User) string { + var id string + if v, ok := user.GetIdOk(); ok && v != nil { + id = *v + } + summary := fmt.Sprintf("id: %s", id) + if props, ok := user.GetPropertiesOk(); ok && props != nil { + if firstname, ok := props.GetFirstnameOk(); ok && firstname != nil && *firstname != "" { + summary = fmt.Sprintf("%s, firstname: %s", summary, *firstname) + } + if lastname, ok := props.GetLastnameOk(); ok && lastname != nil && *lastname != "" { + summary = fmt.Sprintf("%s, lastname: %s", summary, *lastname) + } + if email, ok := props.GetEmailOk(); ok && email != nil && *email != "" { + summary = fmt.Sprintf("%s, email: %s", summary, *email) + } + } + return summary + }, + ID: func(user ionoscloud.User) string { + if id, ok := user.GetIdOk(); ok && id != nil { + return *id + } + return "" + }, + Delete: func(user ionoscloud.User) error { + _, err := c.CloudApiV6Services.Users().Delete(*user.GetId()) + return err + }, + }) } func RunGroupUserList(c *core.CommandConfig) error { @@ -342,49 +345,46 @@ func RunGroupUserRemove(c *core.CommandConfig) error { func RemoveAllUsers(c *core.CommandConfig) error { groupId := viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgGroupId)) - c.Verbose("Group ID: %v", groupId) - c.Verbose("Getting Users...") - - users, resp, err := c.CloudApiV6Services.Groups().ListUsers(groupId) - if err != nil { - return err - } - - usersItems, ok := users.GetItemsOk() - if !ok || usersItems == nil { - return fmt.Errorf("could not get items of Users") - } - - if len(*usersItems) <= 0 { - return fmt.Errorf("no Users found") - } - - c.Msg("Users to be removed:") - - var multiErr error - for _, user := range *usersItems { - id := user.GetId() - firstname := user.GetProperties().GetFirstname() - lastname := user.GetProperties().GetLastname() - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Remove the User with Id: %s, LastName: %s, FirstName: %s", *id, *lastname, *firstname), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.Groups().RemoveUser(groupId, *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.User]{ + Resource: "User", + List: func() ([]ionoscloud.User, error) { + users, _, err := c.CloudApiV6Services.Groups().ListUsers(groupId) + if err != nil { + return nil, err + } - if multiErr != nil { - return multiErr - } + items, ok := users.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Users") + } - return nil + return *items, nil + }, + Summary: func(user ionoscloud.User) string { + var id string + if v, ok := user.GetIdOk(); ok && v != nil { + id = *v + } + summary := fmt.Sprintf("id: %s", id) + if props, ok := user.GetPropertiesOk(); ok && props != nil { + if firstname, ok := props.GetFirstnameOk(); ok && firstname != nil && *firstname != "" { + summary = fmt.Sprintf("%s, firstname: %s", summary, *firstname) + } + if lastname, ok := props.GetLastnameOk(); ok && lastname != nil && *lastname != "" { + summary = fmt.Sprintf("%s, lastname: %s", summary, *lastname) + } + } + return summary + }, + ID: func(user ionoscloud.User) string { + if id, ok := user.GetIdOk(); ok && id != nil { + return *id + } + return "" + }, + Delete: func(user ionoscloud.User) error { + _, err := c.CloudApiV6Services.Groups().RemoveUser(groupId, *user.GetId()) + return err + }, + }) } diff --git a/commands/compute/user/user_test.go b/commands/compute/user/user_test.go index 6719d40003..9b4da7b5a6 100644 --- a/commands/compute/user/user_test.go +++ b/commands/compute/user/user_test.go @@ -517,11 +517,6 @@ var ( Items: &[]ionoscloud.User{userTestGet.User}, }, } - groupUserTest = resources.User{ - User: ionoscloud.User{ - Id: &testUserVar, - }, - } ) func TestRunGroupUserList(t *testing.T) { diff --git a/commands/compute/volume/run_volume.go b/commands/compute/volume/run_volume.go index 0984ffeb4c..8d50f1486e 100644 --- a/commands/compute/volume/run_volume.go +++ b/commands/compute/volume/run_volume.go @@ -2,7 +2,6 @@ package volume import ( "context" - "errors" "fmt" "time" @@ -442,46 +441,44 @@ func DeleteAllVolumes(c *core.CommandConfig) error { dcId := viper.GetString(core.GetFlagName(c.NS, cloudapiv6.ArgDataCenterId)) c.Verbose(constants.DatacenterId, dcId) - c.Verbose("Getting Volumes...") - volumes, resp, err := c.CloudApiV6Services.Volumes().List(dcId) - if err != nil { - return err - } - - volumesItems, ok := volumes.GetItemsOk() - if !ok || volumesItems == nil { - return fmt.Errorf("could not get items of Volumes") - } - - if len(*volumesItems) <= 0 { - return fmt.Errorf("no Volumes found") - } - - c.Msg("Volumes to be deleted:") - - var multiErr error - for _, volume := range *volumesItems { - id := volume.GetId() - name := volume.GetProperties().Name - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Delete the Volume with Id: %s, Name: %s", *id, *name), viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - resp, err = c.CloudApiV6Services.Volumes().Delete(dcId, *id) - if resp != nil && request.GetId(resp) != "" { - c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) - } - if err != nil { - multiErr = errors.Join(multiErr, fmt.Errorf(constants.ErrDeleteAll, c.Resource, *id, err)) - continue - } - - } + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.Volume]{ + Resource: "volume", + List: func() ([]ionoscloud.Volume, error) { + volumes, _, err := c.CloudApiV6Services.Volumes().List(dcId) + if err != nil { + return nil, err + } - if multiErr != nil { - return multiErr - } + items, ok := volumes.GetItemsOk() + if !ok || items == nil { + return nil, fmt.Errorf("could not get items of Volumes") + } - return nil + return *items, nil + }, + Summary: func(volume ionoscloud.Volume) string { + var id, name string + if volume.Id != nil { + id = *volume.Id + } + if p := volume.Properties; p != nil && p.Name != nil { + name = *p.Name + } + return fmt.Sprintf("%s (id: %s)", name, id) + }, + ID: func(volume ionoscloud.Volume) string { + if volume.Id != nil { + return *volume.Id + } + return "" + }, + Delete: func(volume ionoscloud.Volume) error { + resp, err := c.CloudApiV6Services.Volumes().Delete(dcId, *volume.Id) + if resp != nil && request.GetId(resp) != "" { + c.Verbose(constants.MessageRequestInfo, request.GetId(resp), resp.RequestTime) + } + return err + }, + }) } diff --git a/commands/container-registry/registry/delete.go b/commands/container-registry/registry/delete.go index 1d17e6a034..5ba3cffa32 100644 --- a/commands/container-registry/registry/delete.go +++ b/commands/container-registry/registry/delete.go @@ -8,6 +8,7 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" + "github.com/ionos-cloud/sdk-go-bundle/products/containerregistry/v2" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -48,25 +49,26 @@ func CmdDelete(c *core.CommandConfig) error { } if allFlag { - c.Verbose("Deleting all Container Registries...") - - regs, _, err := client.Must().RegistryClient.RegistriesApi.RegistriesGet(context.Background()).Execute() - if err != nil { - return err - } - - for _, reg := range regs.Items { - msg := fmt.Sprintf("delete Container Registry: %s", *reg.Id) - - if !confirm.FAsk(c.Command.Command.InOrStdin(), msg, viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - _, err = client.Must().RegistryClient.RegistriesApi.RegistriesDelete(context.Background(), *reg.Id).Execute() - if err != nil { + return core.DeleteAll(c, core.DeleteAllOptions[containerregistry.RegistryResponse]{ + Resource: "Container Registry", + List: func() ([]containerregistry.RegistryResponse, error) { + regs, _, err := client.Must().RegistryClient.RegistriesApi.RegistriesGet(context.Background()).Execute() + if err != nil { + return nil, err + } + return regs.Items, nil + }, + Summary: func(reg containerregistry.RegistryResponse) string { + return fmt.Sprintf("name: %s, id: %s", reg.Properties.Name, *reg.Id) + }, + ID: func(reg containerregistry.RegistryResponse) string { + return *reg.Id + }, + Delete: func(reg containerregistry.RegistryResponse) error { + _, err := client.Must().RegistryClient.RegistriesApi.RegistriesDelete(context.Background(), *reg.Id).Execute() return err - } - } + }, + }) } else { id, err := c.Command.Command.Flags().GetString(constants.FlagRegistryId) if err != nil { diff --git a/commands/container-registry/token/delete.go b/commands/container-registry/token/delete.go index 9d8b054b51..cc8115f14e 100644 --- a/commands/container-registry/token/delete.go +++ b/commands/container-registry/token/delete.go @@ -9,6 +9,7 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" + "github.com/ionos-cloud/sdk-go-bundle/products/containerregistry/v2" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -79,56 +80,68 @@ func CmdDeleteToken(c *core.CommandConfig) error { return nil } - tokens, _, err := client.Must().RegistryClient.TokensApi.RegistriesTokensGet(context.Background(), regId).Execute() - if err != nil { - return err - } - - for _, token := range tokens.GetItems() { - msg := fmt.Sprintf("delete Token: %s", *token.Id) - - if !confirm.FAsk(c.Command.Command.InOrStdin(), msg, viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - _, err := client.Must().RegistryClient.TokensApi.RegistriesTokensDelete(context.Background(), regId, *token.Id).Execute() - if err != nil { + // delete all tokens of a single registry + return core.DeleteAll(c, core.DeleteAllOptions[containerregistry.TokenResponse]{ + Resource: "Token", + List: func() ([]containerregistry.TokenResponse, error) { + tokens, _, err := client.Must().RegistryClient.TokensApi.RegistriesTokensGet(context.Background(), regId).Execute() + if err != nil { + return nil, err + } + return tokens.GetItems(), nil + }, + Summary: func(t containerregistry.TokenResponse) string { + return fmt.Sprintf("name: %s, id: %s, registry: %s", t.Properties.Name, *t.Id, regId) + }, + ID: func(t containerregistry.TokenResponse) string { + return *t.Id + }, + Delete: func(t containerregistry.TokenResponse) error { + _, err := client.Must().RegistryClient.TokensApi.RegistriesTokensDelete(context.Background(), regId, *t.Id).Execute() return err - } - } - - return nil - } - - regs, _, err := client.Must().RegistryClient.RegistriesApi.RegistriesGet(context.Background()).Execute() - if err != nil { - return err + }, + }) } - for _, reg := range regs.GetItems() { - tokens, _, err := client.Must().RegistryClient.TokensApi.RegistriesTokensGet(context.Background(), *reg.Id).Execute() - if err != nil { - return err - } - - for _, token := range tokens.GetItems() { - msg := fmt.Sprintf("delete Token: %s", *token.Id) - - if !confirm.FAsk(c.Command.Command.InOrStdin(), msg, viper.GetBool(constants.ArgForce)) { - return fmt.Errorf(confirm.UserDenied) - } - - _, err := client.Must().RegistryClient.TokensApi.RegistriesTokensDelete(context.Background(), *reg.Id, *token.Id).Execute() + // delete all tokens across all registries + return core.DeleteAll(c, core.DeleteAllOptions[regToken]{ + Resource: "Token", + List: func() ([]regToken, error) { + regs, _, err := client.Must().RegistryClient.RegistriesApi.RegistriesGet(context.Background()).Execute() if err != nil { - return err + return nil, err } - } - if err != nil { + var all []regToken + for _, reg := range regs.GetItems() { + tokens, _, err := client.Must().RegistryClient.TokensApi.RegistriesTokensGet(context.Background(), *reg.Id).Execute() + if err != nil { + return nil, err + } + for _, tok := range tokens.GetItems() { + all = append(all, regToken{regID: *reg.Id, tok: tok}) + } + } + return all, nil + }, + Summary: func(rt regToken) string { + return fmt.Sprintf("name: %s, id: %s, registry: %s", rt.tok.Properties.Name, *rt.tok.Id, rt.regID) + }, + ID: func(rt regToken) string { + return *rt.tok.Id + }, + Delete: func(rt regToken) error { + _, err := client.Must().RegistryClient.TokensApi.RegistriesTokensDelete(context.Background(), rt.regID, *rt.tok.Id).Execute() return err - } - } - return nil + }, + }) +} + +// regToken pairs a token with its owning registry id so that tokens from all +// registries can be flattened into a single DeleteAll slice. +type regToken struct { + regID string + tok containerregistry.TokenResponse } func PreCmdDeleteToken(c *core.PreCommandConfig) error { diff --git a/commands/dbaas/inmemorydb/replicaset/delete.go b/commands/dbaas/inmemorydb/replicaset/delete.go index 12671a1e69..6a3f3d0095 100644 --- a/commands/dbaas/inmemorydb/replicaset/delete.go +++ b/commands/dbaas/inmemorydb/replicaset/delete.go @@ -9,7 +9,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" "github.com/ionos-cloud/sdk-go-bundle/products/dbaas/inmemorydb/v2" "github.com/ionos-cloud/sdk-go-bundle/shared" "github.com/spf13/viper" @@ -100,41 +99,40 @@ func deleteSingle(c *core.CommandConfig, id string) error { } func deleteAll(c *core.CommandConfig) error { - return c.RunForAllLocations(func(cfg *shared.Configuration, location string) error { - apiClient := inmemorydb.NewAPIClient(cfg) - - list, _, err := apiClient.ReplicaSetApi. - ReplicasetsGet(context.Background()). - Execute() + // Gather replica-sets from every location (unless --location pins one), tagging each with + // its location and location-scoped client, then hand the flat list to core.DeleteAll for a + // consistent preview / per-item confirm-skip / summary flow. + type located struct { + rs inmemorydb.ReplicaSetRead + loc string + api *inmemorydb.APIClient + } + var items []located + if err := c.RunForAllLocations(func(cfg *shared.Configuration, location string) error { + api := inmemorydb.NewAPIClient(cfg) + list, _, err := api.ReplicaSetApi.ReplicasetsGet(context.Background()).Execute() if err != nil { return fmt.Errorf("failed listing replica-sets in location %s: %w", location, err) } + for _, rs := range list.GetItems() { + items = append(items, located{rs: rs, loc: location, api: api}) + } + return nil + }); err != nil { + return err + } - return functional.ApplyAndAggregateErrors(list.GetItems(), func(rs inmemorydb.ReplicaSetRead) error { - prompt := fmt.Sprintf( - "Are you sure you want to delete replica-set '%s' (dns name: '%s', replicas: %d, location: %s)", - rs.Properties.DisplayName, - rs.Metadata.DnsName, - rs.Properties.Replicas, - location, - ) - yes := confirm.FAsk( - c.Command.Command.InOrStdin(), - prompt, - viper.GetBool(constants.ArgForce), - ) - if !yes { - return nil - } - - _, err := apiClient.ReplicaSetApi. - ReplicasetsDelete(context.Background(), rs.Id). - Execute() - if err != nil { - return fmt.Errorf("failed deleting replicaset %q in location %s: %w", rs.Id, location, err) - } - - return nil - }) + return core.DeleteAll(c, core.DeleteAllOptions[located]{ + Resource: "replica-set", + List: func() ([]located, error) { return items, nil }, + Summary: func(l located) string { + return fmt.Sprintf("%s (name: %s, dns name: %s, replicas: %d, location: %s)", + l.rs.Id, l.rs.Properties.DisplayName, l.rs.Metadata.DnsName, l.rs.Properties.Replicas, l.loc) + }, + ID: func(l located) string { return l.rs.Id }, + Delete: func(l located) error { + _, err := l.api.ReplicaSetApi.ReplicasetsDelete(context.Background(), l.rs.Id).Execute() + return err + }, }) } diff --git a/commands/dbaas/mariadb/cluster/delete.go b/commands/dbaas/mariadb/cluster/delete.go index 736faab565..fd8f188c7c 100644 --- a/commands/dbaas/mariadb/cluster/delete.go +++ b/commands/dbaas/mariadb/cluster/delete.go @@ -8,7 +8,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" "github.com/ionos-cloud/sdk-go-bundle/products/dbaas/mariadb/v2" "github.com/ionos-cloud/sdk-go-bundle/shared" "github.com/spf13/viper" @@ -90,7 +89,7 @@ ionosctl db mar c d --all --name `, }, constants.MariaDBApiRegionalURL, constants.MariaDBLocations), ) cmd.AddBoolFlag(constants.ArgAll, constants.ArgAllShort, false, "Delete all mariadb clusters") - cmd.AddBoolFlag(constants.FlagName, constants.FlagNameShort, false, "When deleting all clusters, filter the clusters by a name") + cmd.AddStringFlag(constants.FlagName, constants.FlagNameShort, "", "When deleting all clusters, filter the clusters by a name") cmd.Command.SilenceUsage = true cmd.Command.Flags().SortFlags = false @@ -98,29 +97,57 @@ ionosctl db mar c d --all --name `, return cmd } -func deleteAll(c *core.CommandConfig) error { - c.Verbose("Deleting All Clusters!") - - return c.RunForAllLocations(func(cfg *shared.Configuration, location string) error { - apiClient := mariadb.NewAPIClient(cfg) - - req := apiClient.ClustersApi.ClustersGet(context.Background()) - req = FilterNameFlags(c)(req) +func clusterSummary(c mariadb.ClusterResponse) string { + s := "" + if c.Id != nil { + s = *c.Id + } + if p := c.Properties; p != nil { + if n := p.DisplayName; n != nil { + s = fmt.Sprintf("%s (%s)", s, *n) + } + if v := p.MariadbVersion; v != nil { + s = fmt.Sprintf("%s version v%s", s, *v) + } + } + return s +} +func deleteAll(c *core.CommandConfig) error { + // Gather clusters from every location (unless --location pins one), tagging each with + // its location and the location-scoped client, then hand the flat list to core.DeleteAll + // so the preview / per-item confirm-skip / summary flow is consistent across all resources. + type located struct { + cluster mariadb.ClusterResponse + loc string + api *mariadb.APIClient + } + var items []located + if err := c.RunForAllLocations(func(cfg *shared.Configuration, location string) error { + api := mariadb.NewAPIClient(cfg) + req := FilterNameFlags(c)(api.ClustersApi.ClustersGet(context.Background())) xs, _, err := req.Execute() if err != nil { return fmt.Errorf("failed getting clusters: %w", err) } + for _, x := range xs.GetItems() { + items = append(items, located{cluster: x, loc: location, api: api}) + } + return nil + }); err != nil { + return err + } - return functional.ApplyAndAggregateErrors(xs.GetItems(), func(x mariadb.ClusterResponse) error { - yes := confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("%s (location: %s)", confirmStringForCluster(x), location), viper.GetBool(constants.ArgForce)) - if yes { - _, _, delErr := apiClient.ClustersApi.ClustersDelete(context.Background(), *x.Id).Execute() - if delErr != nil { - return fmt.Errorf("failed deleting cluster %s: %w", *x.Properties.DisplayName, delErr) - } - } - return nil - }) + return core.DeleteAll(c, core.DeleteAllOptions[located]{ + Resource: "cluster", + List: func() ([]located, error) { return items, nil }, + Summary: func(l located) string { + return fmt.Sprintf("%s (location: %s)", clusterSummary(l.cluster), l.loc) + }, + ID: func(l located) string { return *l.cluster.Id }, + Delete: func(l located) error { + _, _, delErr := l.api.ClustersApi.ClustersDelete(context.Background(), *l.cluster.Id).Execute() + return delErr + }, }) } diff --git a/commands/dbaas/mongo/cluster/delete.go b/commands/dbaas/mongo/cluster/delete.go index 20ade3fa4e..e4fcaeabfe 100644 --- a/commands/dbaas/mongo/cluster/delete.go +++ b/commands/dbaas/mongo/cluster/delete.go @@ -9,7 +9,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" sdkgo "github.com/ionos-cloud/sdk-go-bundle/products/dbaas/mongo/v2" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -85,7 +84,7 @@ ionosctl db m c d --all --name `, return completer.MongoClusterIds(), cobra.ShellCompDirectiveNoFileComp }) cmd.AddBoolFlag(constants.ArgAll, constants.ArgAllShort, false, "Delete all mongo clusters") - cmd.AddBoolFlag(constants.FlagName, constants.FlagNameShort, false, "When deleting all clusters, filter the clusters by a name") + cmd.AddStringFlag(constants.FlagName, constants.FlagNameShort, "", "When deleting all clusters, filter the clusters by a name") cmd.Command.SilenceUsage = true cmd.Command.Flags().SortFlags = false @@ -93,21 +92,46 @@ ionosctl db m c d --all --name `, return cmd } -func deleteAll(c *core.CommandConfig) error { - c.Verbose("Deleting All Clusters!") - xs, err := Clusters(FilterNameFlags(c)) - if err != nil { - return err +func clusterSummary(c sdkgo.ClusterResponse) string { + s := "" + if c.Id != nil { + s = *c.Id } + if p := c.Properties; p != nil { + if n := p.DisplayName; n != nil { + s = fmt.Sprintf("%s (%s)", s, *n) + } + if v := p.MongoDBVersion; v != nil { + s = fmt.Sprintf("%s version v%s", s, *v) + } + if edition := p.Edition; edition != nil { + s = fmt.Sprintf("%s edition %s", s, *edition) + } + if ctype := p.Type; ctype != nil { + s = fmt.Sprintf("%s type %s", s, *ctype) + } + if l := p.Location; l != nil { + s = fmt.Sprintf("%s located in %s", s, *l) + } + } + return s +} - return functional.ApplyAndAggregateErrors(xs.GetItems(), func(x sdkgo.ClusterResponse) error { - yes := confirm.FAsk(c.Command.Command.InOrStdin(), confirmStringForCluster(x), viper.GetBool(constants.ArgForce)) - if yes { - _, _, delErr := client.Must().MongoClient.ClustersApi.ClustersDelete(c.Context, *x.Id).Execute() - if delErr != nil { - return fmt.Errorf("failed deleting cluster %s: %w", *x.Properties.DisplayName, delErr) +func deleteAll(c *core.CommandConfig) error { + return core.DeleteAll(c, core.DeleteAllOptions[sdkgo.ClusterResponse]{ + Resource: "cluster", + List: func() ([]sdkgo.ClusterResponse, error) { + xs, err := Clusters(FilterNameFlags(c)) + if err != nil { + return nil, err } - } - return nil + return xs.GetItems(), nil + }, + Summary: clusterSummary, + ID: func(x sdkgo.ClusterResponse) string { return *x.Id }, + Delete: func(x sdkgo.ClusterResponse) error { + _, _, delErr := client.Must().MongoClient.ClustersApi.ClustersDelete(c.Context, *x.Id).Execute() + return delErr + }, }) } diff --git a/commands/dbaas/mongo/user/delete.go b/commands/dbaas/mongo/user/delete.go index 861911a33c..0b4c3d1818 100644 --- a/commands/dbaas/mongo/user/delete.go +++ b/commands/dbaas/mongo/user/delete.go @@ -9,7 +9,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" sdkgo "github.com/ionos-cloud/sdk-go-bundle/products/dbaas/mongo/v2" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -64,21 +63,20 @@ func UserDeleteCmd() *core.Command { } func deleteAll(c *core.CommandConfig, clusterId string) error { - c.Verbose("Deleting all users") - xs, _, err := client.Must().MongoClient.UsersApi.ClustersUsersGet(c.Context, clusterId).Execute() - if err != nil { - return err - } - - return functional.ApplyAndAggregateErrors(xs.GetItems(), func(x sdkgo.User) error { - yes := confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("delete user %s", x.Properties.Username), viper.GetBool(constants.ArgForce)) - if !yes { - return fmt.Errorf("user %s skipped by confirmation check", x.Properties.Username) - } - _, _, delErr := client.Must().MongoClient.UsersApi.ClustersUsersDelete(c.Context, clusterId, x.Properties.Username).Execute() - if delErr != nil { - return fmt.Errorf("failed deleting one of the resources: %w", delErr) - } - return nil + return core.DeleteAll(c, core.DeleteAllOptions[sdkgo.User]{ + Resource: "user", + List: func() ([]sdkgo.User, error) { + xs, _, err := client.Must().MongoClient.UsersApi.ClustersUsersGet(c.Context, clusterId).Execute() + if err != nil { + return nil, err + } + return xs.GetItems(), nil + }, + Summary: func(x sdkgo.User) string { return x.Properties.Username }, + ID: func(x sdkgo.User) string { return x.Properties.Username }, + Delete: func(x sdkgo.User) error { + _, _, delErr := client.Must().MongoClient.UsersApi.ClustersUsersDelete(c.Context, clusterId, x.Properties.Username).Execute() + return delErr + }, }) } diff --git a/commands/dbaas/postgres-v2/cluster/delete.go b/commands/dbaas/postgres-v2/cluster/delete.go index a483508ef7..da43ecde04 100644 --- a/commands/dbaas/postgres-v2/cluster/delete.go +++ b/commands/dbaas/postgres-v2/cluster/delete.go @@ -10,7 +10,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" psqlv2 "github.com/ionos-cloud/sdk-go-bundle/products/dbaas/psql/v3" "github.com/ionos-cloud/sdk-go-bundle/shared" "github.com/spf13/viper" @@ -96,10 +95,18 @@ func ClusterDeleteAll(c *core.CommandConfig) error { c.Verbose("Filtering based on Cluster Name: %v", viper.GetString(core.GetFlagName(c.NS, constants.FlagName))) } - return c.RunForAllLocations(func(cfg *shared.Configuration, location string) error { - apiClient := psqlv2.NewAPIClient(cfg) - - req := apiClient.ClustersApi.ClustersGet(context.Background()) + // Gather clusters from every location (unless --location pins one), tagging each with its + // location and location-scoped client, then hand the flat list to core.DeleteAll for a + // consistent preview / per-item confirm-skip / summary flow. + type located struct { + cluster psqlv2.ClusterRead + loc string + api *psqlv2.APIClient + } + var items []located + if err := c.RunForAllLocations(func(cfg *shared.Configuration, location string) error { + api := psqlv2.NewAPIClient(cfg) + req := api.ClustersApi.ClustersGet(context.Background()) if fn := core.GetFlagName(c.NS, constants.FlagName); viper.IsSet(fn) && viper.GetString(fn) != "" { req = req.FilterName(viper.GetString(fn)) } @@ -110,23 +117,24 @@ func ClusterDeleteAll(c *core.CommandConfig) error { if err != nil { return fmt.Errorf("failed listing clusters in location %s: %w", location, err) } + for _, cluster := range clusters.GetItems() { + items = append(items, located{cluster: cluster, loc: location, api: api}) + } + return nil + }); err != nil { + return err + } - return functional.ApplyAndAggregateErrors(clusters.GetItems(), func(cluster psqlv2.ClusterRead) error { - // Skip (not fail) on decline, matching the other delete --all - // commands: a "no" for one cluster must not abort the whole run. - if !confirm.FAsk(c.Command.Command.InOrStdin(), - fmt.Sprintf("delete cluster %s (%s) (location: %s)", cluster.Id, cluster.Properties.Name, location), - viper.GetBool(constants.ArgForce)) { - return nil - } - - c.Verbose("Deleting cluster: %s (%s)", cluster.Id, cluster.Properties.Name) - _, delErr := apiClient.ClustersApi.ClustersDelete(context.Background(), cluster.Id).Execute() - if delErr != nil { - return fmt.Errorf("failed deleting cluster %s (%s): %w", cluster.Id, cluster.Properties.Name, delErr) - } - - return nil - }) + return core.DeleteAll(c, core.DeleteAllOptions[located]{ + Resource: "Cluster", + List: func() ([]located, error) { return items, nil }, + Summary: func(l located) string { + return fmt.Sprintf("%s (%s) (location: %s)", l.cluster.Id, l.cluster.Properties.Name, l.loc) + }, + ID: func(l located) string { return l.cluster.Id }, + Delete: func(l located) error { + _, delErr := l.api.ClustersApi.ClustersDelete(context.Background(), l.cluster.Id).Execute() + return delErr + }, }) } diff --git a/commands/dns/record/delete.go b/commands/dns/record/delete.go index ce8dcdb349..9211e1b5f5 100644 --- a/commands/dns/record/delete.go +++ b/commands/dns/record/delete.go @@ -117,29 +117,24 @@ ionosctl dns r delete --record PARTIAL_NAME --zone ZONE`, } func deleteAll(c *core.CommandConfig) error { - xs, err := Records(FilterRecordsByZoneAndRecordFlags(c.NS)) // full zone name and partial record name filter, if set - if err != nil { - return fmt.Errorf("failed listing records: %w", err) - } - - if len(xs.Items) == 0 { - return fmt.Errorf("found no records matching given filters") - } - - err = functional.ApplyAndAggregateErrors(xs.GetItems(), func(r dns.RecordRead) error { - yes := confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Are you sure you want to delete record %s (type: '%s'; content: '%s')", r.Properties.Name, r.Properties.Type, r.Properties.Content), - viper.GetBool(constants.ArgForce)) - - if yes { - _, _, delErr := client.Must().DnsClient.RecordsApi.ZonesRecordsDelete(c.Context, r.Metadata.ZoneId, r.Id).Execute() - if delErr != nil { - return fmt.Errorf("failed deleting %s (name: %s): %w", r.Id, r.Properties.Name, delErr) + return core.DeleteAll(c, core.DeleteAllOptions[dns.RecordRead]{ + Resource: "record", + List: func() ([]dns.RecordRead, error) { + xs, err := Records(FilterRecordsByZoneAndRecordFlags(c.NS)) // full zone name and partial record name filter, if set + if err != nil { + return nil, fmt.Errorf("failed listing records: %w", err) } - } - return nil + return xs.GetItems(), nil + }, + Summary: func(r dns.RecordRead) string { + return fmt.Sprintf("%s (id: %s, type: %s, content: %s)", r.Properties.Name, r.Id, r.Properties.Type, r.Properties.Content) + }, + ID: func(r dns.RecordRead) string { return r.Id }, + Delete: func(r dns.RecordRead) error { + _, _, err := client.Must().DnsClient.RecordsApi.ZonesRecordsDelete(c.Context, r.Metadata.ZoneId, r.Id).Execute() + return err + }, }) - - return err } func deleteSingleWithFilters(c *core.CommandConfig) (dns.RecordRead, error) { diff --git a/commands/dns/reverse-record/delete.go b/commands/dns/reverse-record/delete.go index dbc09860c3..8040daae95 100644 --- a/commands/dns/reverse-record/delete.go +++ b/commands/dns/reverse-record/delete.go @@ -8,7 +8,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" ionoscloud "github.com/ionos-cloud/sdk-go-bundle/products/dns/v2" "github.com/spf13/viper" ) @@ -57,13 +56,27 @@ func Delete() *core.Command { } func deleteAll(c *core.CommandConfig) error { - records, err := Records() - if err != nil { - return fmt.Errorf("failed getting all records: %w", err) - } - - return functional.ApplyAndAggregateErrors(records.Items, func(r ionoscloud.ReverseRecordRead) error { - return deleteSingle(c, r.Id) + return core.DeleteAll(c, core.DeleteAllOptions[ionoscloud.ReverseRecordRead]{ + Resource: "record", + List: func() ([]ionoscloud.ReverseRecordRead, error) { + records, err := Records() + if err != nil { + return nil, fmt.Errorf("failed getting all records: %w", err) + } + return records.Items, nil + }, + Summary: func(r ionoscloud.ReverseRecordRead) string { + s := fmt.Sprintf("%s (id: %s, ip: %s", r.Properties.Name, r.Id, r.Properties.Ip) + if r.Properties.Description != nil && *r.Properties.Description != "" { + s += fmt.Sprintf(", desc: %s", *r.Properties.Description) + } + return s + ")" + }, + ID: func(r ionoscloud.ReverseRecordRead) string { return r.Id }, + Delete: func(r ionoscloud.ReverseRecordRead) error { + _, _, err := client.Must().DnsClient.ReverseRecordsApi.ReverserecordsDelete(context.Background(), r.Id).Execute() + return err + }, }) } diff --git a/commands/dns/secondary-zones/delete.go b/commands/dns/secondary-zones/delete.go index c6ee80accd..d03b89b622 100644 --- a/commands/dns/secondary-zones/delete.go +++ b/commands/dns/secondary-zones/delete.go @@ -10,7 +10,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" "github.com/ionos-cloud/sdk-go-bundle/products/dns/v2" "github.com/spf13/viper" ) @@ -60,21 +59,28 @@ func deleteCmd() *core.Command { } func deleteAll(c *core.CommandConfig) error { - secZones, _, err := client.Must().DnsClient.SecondaryZonesApi.SecondaryzonesGet(context.Background()).Execute() - if err != nil { - return err - } - - if err = functional.ApplyAndAggregateErrors( - secZones.Items, func(item dns.SecondaryZoneRead) error { - return deleteSingle(c, item.Id) + return core.DeleteAll(c, core.DeleteAllOptions[dns.SecondaryZoneRead]{ + Resource: "secondary zone", + List: func() ([]dns.SecondaryZoneRead, error) { + secZones, _, err := client.Must().DnsClient.SecondaryZonesApi.SecondaryzonesGet(context.Background()).Execute() + if err != nil { + return nil, err + } + return secZones.Items, nil }, - ); err != nil { - return err - } - - c.Msg("Successfully deleted all secondary zones") - return nil + Summary: func(item dns.SecondaryZoneRead) string { + s := fmt.Sprintf("%s (id: %s", item.Properties.ZoneName, item.Id) + if item.Properties.Description != nil && *item.Properties.Description != "" { + s += fmt.Sprintf(", desc: %s", *item.Properties.Description) + } + return s + ")" + }, + ID: func(item dns.SecondaryZoneRead) string { return item.Id }, + Delete: func(item dns.SecondaryZoneRead) error { + _, _, err := client.Must().DnsClient.SecondaryZonesApi.SecondaryzonesDelete(context.Background(), item.Id).Execute() + return err + }, + }) } func deleteSingle(c *core.CommandConfig, zoneId string) error { diff --git a/commands/dns/zone/delete.go b/commands/dns/zone/delete.go index d02fc8a7f6..47fb21bf3a 100644 --- a/commands/dns/zone/delete.go +++ b/commands/dns/zone/delete.go @@ -8,7 +8,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" "github.com/ionos-cloud/sdk-go-bundle/products/dns/v2" "github.com/ionos-cloud/ionosctl/v6/internal/client" @@ -72,23 +71,26 @@ func ZonesDeleteCmd() *core.Command { } func deleteAll(c *core.CommandConfig) error { - c.Verbose("Deleting all zones!") - xs, _, err := client.Must().DnsClient.ZonesApi.ZonesGet(c.Context).Execute() - if err != nil { - return err - } - - err = functional.ApplyAndAggregateErrors(xs.GetItems(), func(z dns.ZoneRead) error { - yes := confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("Are you sure you want to delete zone %s (desc: '%s')", z.Properties.ZoneName, *z.Properties.Description), - viper.GetBool(constants.ArgForce)) - if yes { - _, _, delErr := client.Must().DnsClient.ZonesApi.ZonesDelete(c.Context, z.Id).Execute() - if delErr != nil { - return fmt.Errorf("failed deleting %s (name: %s): %w", z.Id, z.Properties.ZoneName, delErr) + return core.DeleteAll(c, core.DeleteAllOptions[dns.ZoneRead]{ + Resource: "zone", + List: func() ([]dns.ZoneRead, error) { + xs, _, err := client.Must().DnsClient.ZonesApi.ZonesGet(c.Context).Execute() + if err != nil { + return nil, err + } + return xs.GetItems(), nil + }, + Summary: func(z dns.ZoneRead) string { + s := fmt.Sprintf("%s (id: %s", z.Properties.ZoneName, z.Id) + if z.Properties.Description != nil && *z.Properties.Description != "" { + s += fmt.Sprintf(", desc: %s", *z.Properties.Description) } - } - return nil + return s + ")" + }, + ID: func(z dns.ZoneRead) string { return z.Id }, + Delete: func(z dns.ZoneRead) error { + _, _, err := client.Must().DnsClient.ZonesApi.ZonesDelete(c.Context, z.Id).Execute() + return err + }, }) - - return err } diff --git a/commands/kafka/cluster/delete.go b/commands/kafka/cluster/delete.go index c01c617fa3..63a5cf20a1 100644 --- a/commands/kafka/cluster/delete.go +++ b/commands/kafka/cluster/delete.go @@ -8,7 +8,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/client" "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" "github.com/ionos-cloud/sdk-go-bundle/products/kafka/v2" "github.com/ionos-cloud/sdk-go-bundle/shared" "github.com/spf13/viper" @@ -76,36 +75,40 @@ func Delete() *core.Command { } func deleteAll(c *core.CommandConfig) error { - // Fan out over every location (when --location is unset) so `delete --all` - // spans all locations, matching `list`. Each location gets its own client. - return c.RunForAllLocations(func(cfg *shared.Configuration, location string) error { + // Gather clusters from every location (unless --location pins one), tagging each with its + // location and location-scoped client, then hand the flat list to core.DeleteAll for a + // consistent preview / per-item confirm-skip / summary flow. + type located struct { + cluster kafka.ClusterRead + loc string + api *kafka.APIClient + } + var items []located + if err := c.RunForAllLocations(func(cfg *shared.Configuration, location string) error { kc := kafka.NewAPIClient(cfg) - c.Verbose("Deleting all clusters in %s!", location) - records, _, err := kc.ClustersApi.ClustersGet(context.Background()).Execute() if err != nil { return fmt.Errorf("failed listing kafka clusters: %w", err) } + for _, d := range records.GetItems() { + items = append(items, located{cluster: d, loc: location, api: kc}) + } + return nil + }); err != nil { + return err + } - return functional.ApplyAndAggregateErrors( - records.GetItems(), func(d kafka.ClusterRead) error { - yes := confirm.FAsk( - c.Command.Command.InOrStdin(), - fmt.Sprintf( - "Are you sure you want to delete cluster %s with name %s (location: %s) ", - d.Id, d.Properties.Name, location, - ), - viper.GetBool(constants.ArgForce), - ) - if yes { - _, delErr := kc.ClustersApi.ClustersDelete(context.Background(), d.Id).Execute() - if delErr != nil { - return fmt.Errorf("failed deleting %s (name: %s): %w", d.Id, d.Properties.Name, delErr) - } - } - return nil - }, - ) + return core.DeleteAll(c, core.DeleteAllOptions[located]{ + Resource: "cluster", + List: func() ([]located, error) { return items, nil }, + Summary: func(l located) string { + return fmt.Sprintf("name: %s, id: %s (location: %s)", l.cluster.Properties.Name, l.cluster.Id, l.loc) + }, + ID: func(l located) string { return l.cluster.Id }, + Delete: func(l located) error { + _, err := l.api.ClustersApi.ClustersDelete(context.Background(), l.cluster.Id).Execute() + return err + }, }) } diff --git a/commands/kafka/topic/delete.go b/commands/kafka/topic/delete.go index 648c4197ff..7ab8f2a73c 100644 --- a/commands/kafka/topic/delete.go +++ b/commands/kafka/topic/delete.go @@ -9,7 +9,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" "github.com/ionos-cloud/sdk-go-bundle/products/kafka/v2" "github.com/spf13/viper" ) @@ -39,14 +38,8 @@ func deleteCmd() *core.Command { return nil }, CmdRun: func(cmd *core.CommandConfig) error { - if cmd.Command.Command.Flags().Changed(constants.ArgAll) { - err := deleteAll(cmd) - if err != nil { - return err - } - - fmt.Fprintf(cmd.Command.Command.OutOrStdout(), "All topics deleted\n") - return nil + if viper.GetBool(core.GetFlagName(cmd.NS, constants.ArgAll)) { + return deleteAll(cmd) } clusterID := viper.GetString(core.GetFlagName(cmd.NS, constants.FlagClusterId)) @@ -101,30 +94,28 @@ func deleteCmd() *core.Command { func deleteAll(cmd *core.CommandConfig) error { clusterID, _ := cmd.Command.Command.Flags().GetString(constants.FlagClusterId) - topics, _, err := client.Must().Kafka.TopicsApi.ClustersTopicsGet( - context.Background(), clusterID, - ).Execute() - if err != nil { - return err - } - - return functional.ApplyAndAggregateErrors( - topics.Items, func(topic kafka.TopicRead) error { - if !confirm.FAsk( - cmd.Command.Command.InOrStdin(), fmt.Sprintf("delete topic %v (%v)", topic.Id, topic.Properties.Name), - viper.GetBool(constants.ArgForce), - ) { - return fmt.Errorf(confirm.UserDenied) + return core.DeleteAll(cmd, core.DeleteAllOptions[kafka.TopicRead]{ + Resource: "topic", + List: func() ([]kafka.TopicRead, error) { + topics, _, err := client.Must().Kafka.TopicsApi.ClustersTopicsGet( + context.Background(), clusterID, + ).Execute() + if err != nil { + return nil, err } - + return topics.Items, nil + }, + Summary: func(topic kafka.TopicRead) string { + return fmt.Sprintf("name: %s, id: %s", topic.Properties.Name, topic.Id) + }, + ID: func(topic kafka.TopicRead) string { + return topic.Id + }, + Delete: func(topic kafka.TopicRead) error { _, err := client.Must().Kafka.TopicsApi.ClustersTopicsDelete( context.Background(), clusterID, topic.Id, ).Execute() - if err != nil { - return fmt.Errorf("failed deleting topic %v: %w", topic.Id, err) - } - - return nil + return err }, - ) + }) } diff --git a/commands/logging-service/pipeline/delete.go b/commands/logging-service/pipeline/delete.go index b621f9eb71..ea2f292955 100644 --- a/commands/logging-service/pipeline/delete.go +++ b/commands/logging-service/pipeline/delete.go @@ -11,7 +11,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/internal/printer/table" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" "github.com/ionos-cloud/sdk-go-bundle/products/logging/v2" "github.com/ionos-cloud/sdk-go-bundle/shared" "github.com/spf13/viper" @@ -46,14 +45,8 @@ func preRunDeleteCmd(c *core.PreCommandConfig) error { } func runDeleteCmd(c *core.CommandConfig) error { - if viper.IsSet(core.GetFlagName(c.NS, constants.ArgAll)) { - if err := deleteAll(c); err != nil { - return err - } - - c.Msg("Successfully deleted all logging pipelines") - - return nil + if viper.GetBool(core.GetFlagName(c.NS, constants.ArgAll)) { + return deleteAll(c) } if err := c.RequireExplicitLocation(); err != nil { @@ -80,52 +73,50 @@ func runDeleteCmd(c *core.CommandConfig) error { return nil } +func pipelineSummary(p logging.PipelineRead) string { + t := table.New(allCols) + if extractErr := t.Extract(p); extractErr == nil { + if rows := t.Rows(); len(rows) > 0 { + return strings.TrimSpace(fmt.Sprintf("%v (%v)", rows[0]["Id"], rows[0]["Name"])) + } + } + return p.Id +} + func deleteAll(c *core.CommandConfig) error { - // When --location is unset, RunForAllLocations invokes fn once per location - // (aggregating errors) so that --all spans every location instead of only - // the default one. Each invocation builds its own location-scoped client. - return c.RunForAllLocations(func(cfg *shared.Configuration, location string) error { + // Gather pipelines from every location (unless --location pins one), tagging each with its + // location and location-scoped client, then hand the flat list to core.DeleteAll for a + // consistent preview / per-item confirm-skip / summary flow. + type located struct { + pipeline logging.PipelineRead + loc string + api *logging.APIClient + } + var items []located + if err := c.RunForAllLocations(func(cfg *shared.Configuration, location string) error { lc := logging.NewAPIClient(cfg) - - c.Verbose("Deleting all logging pipelines in %s!", location) - pipelines, _, err := lc.PipelinesApi.PipelinesGet(context.Background()).Execute() if err != nil { return fmt.Errorf("failed listing pipelines in %s: %w", location, err) } + for _, p := range pipelines.GetItems() { + items = append(items, located{pipeline: p, loc: location, api: lc}) + } + return nil + }); err != nil { + return err + } - return functional.ApplyAndAggregateErrors( - pipelines.GetItems(), func(p logging.PipelineRead) error { - t := table.New(allCols) - if extractErr := t.Extract(p); extractErr != nil { - return extractErr - } - rows := t.Rows() - var pInfo string - if len(rows) > 0 { - pInfo = fmt.Sprintf("%v (%v)", rows[0]["Id"], rows[0]["Name"]) - } else { - pInfo = p.Id - } - pInfo = strings.TrimSpace(pInfo) - - yes := confirm.FAsk( - c.Command.Command.InOrStdin(), fmt.Sprintf( - "delete %s (location: %s)", pInfo, location, - ), - viper.GetBool(constants.ArgForce), - ) - if yes { - _, delErr := lc.PipelinesApi.PipelinesDelete( - context.Background(), - p.Id, - ).Execute() - if delErr != nil { - return fmt.Errorf("failed deleting %s: %w", pInfo, delErr) - } - } - return nil - }, - ) + return core.DeleteAll(c, core.DeleteAllOptions[located]{ + Resource: "Logging-Service Pipeline", + List: func() ([]located, error) { return items, nil }, + Summary: func(l located) string { + return fmt.Sprintf("%s (location: %s)", pipelineSummary(l.pipeline), l.loc) + }, + ID: func(l located) string { return l.pipeline.Id }, + Delete: func(l located) error { + _, delErr := l.api.PipelinesApi.PipelinesDelete(context.Background(), l.pipeline.Id).Execute() + return delErr + }, }) } diff --git a/commands/monitoring/pipeline/delete.go b/commands/monitoring/pipeline/delete.go index be55418ffc..4c4b8ac564 100644 --- a/commands/monitoring/pipeline/delete.go +++ b/commands/monitoring/pipeline/delete.go @@ -9,7 +9,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" "github.com/ionos-cloud/sdk-go-bundle/products/monitoring/v2" "github.com/ionos-cloud/sdk-go-bundle/shared" "github.com/spf13/viper" @@ -63,7 +62,7 @@ func MonitoringDeleteCmd() *core.Command { }, constants.MonitoringApiRegionalURL, constants.MonitoringLocations), ) - cmd.AddBoolFlag(constants.ArgAll, constants.ArgAllShort, false, fmt.Sprintf("Delete all pipelines.")) + cmd.AddBoolFlag(constants.ArgAll, constants.ArgAllShort, false, "Delete all pipelines.") cmd.Command.SilenceUsage = true cmd.Command.Flags().SortFlags = false @@ -72,27 +71,39 @@ func MonitoringDeleteCmd() *core.Command { } func deleteAll(c *core.CommandConfig) error { - // Fan out over every location (when --location is unset) so `delete --all` - // spans all locations, matching `list`. Each location gets its own client. - return c.RunForAllLocations(func(cfg *shared.Configuration, location string) error { + // Gather pipelines from every location (unless --location pins one), tagging each with its + // location and location-scoped client, then hand the flat list to core.DeleteAll for a + // consistent preview / per-item confirm-skip / summary flow. + type located struct { + pipeline monitoring.PipelineRead + loc string + api *monitoring.APIClient + } + var items []located + if err := c.RunForAllLocations(func(cfg *shared.Configuration, location string) error { mc := monitoring.NewAPIClient(cfg) - c.Verbose("Deleting all pipelines in %s!", location) xs, _, err := mc.PipelinesApi.PipelinesGet(context.Background()).Execute() if err != nil { return fmt.Errorf("failed listing pipelines: %w", err) } + for _, z := range xs.GetItems() { + items = append(items, located{pipeline: z, loc: location, api: mc}) + } + return nil + }); err != nil { + return err + } - return functional.ApplyAndAggregateErrors(xs.GetItems(), func(z monitoring.PipelineRead) error { - yes := confirm.FAsk(c.Command.Command.InOrStdin(), - fmt.Sprintf("Are you sure you want to delete pipeline with name: %s, id: %s (location: %s) ", z.Properties.Name, z.Id, location), - viper.GetBool(constants.ArgForce)) - if yes { - _, delErr := mc.PipelinesApi.PipelinesDelete(context.Background(), z.Id).Execute() - if delErr != nil { - return fmt.Errorf("failed deleting %s (name: %s): %w", z.Id, z.Properties.Name, delErr) - } - } - return nil - }) + return core.DeleteAll(c, core.DeleteAllOptions[located]{ + Resource: "pipeline", + List: func() ([]located, error) { return items, nil }, + Summary: func(l located) string { + return fmt.Sprintf("%s (name: %s, location: %s)", l.pipeline.Id, l.pipeline.Properties.Name, l.loc) + }, + ID: func(l located) string { return l.pipeline.Id }, + Delete: func(l located) error { + _, delErr := l.api.PipelinesApi.PipelinesDelete(context.Background(), l.pipeline.Id).Execute() + return delErr + }, }) } diff --git a/commands/object-storage/bucket/delete.go b/commands/object-storage/bucket/delete.go index 7e37ffd133..4e1acf045e 100644 --- a/commands/object-storage/bucket/delete.go +++ b/commands/object-storage/bucket/delete.go @@ -12,7 +12,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" ) func DeleteBucketCmd() *core.Command { @@ -80,20 +79,19 @@ func deleteAllBuckets(c *core.CommandConfig) error { buckets = filtered } - return functional.ApplyAndAggregateErrors(buckets, func(b objectstorage.Bucket) error { - name := b.GetName() - - if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("delete bucket %q", name), viper.GetBool(constants.ArgForce)) { - return nil - } - - _, delErr := s3.BucketsApi.DeleteBucket(c.Context, name).Execute() - if delErr != nil { - return fmt.Errorf("failed deleting bucket %q: %w", name, delErr) - } - - fmt.Fprintf(c.Command.Command.OutOrStdout(), "Bucket %q deleted successfully\n", name) - return nil + return core.DeleteAll(c, core.DeleteAllOptions[objectstorage.Bucket]{ + Resource: "bucket", + List: func() ([]objectstorage.Bucket, error) { + return buckets, nil + }, + Summary: func(b objectstorage.Bucket) string { + return fmt.Sprintf("%q (created: %s)", b.GetName(), b.GetCreationDate()) + }, + ID: func(b objectstorage.Bucket) string { return b.GetName() }, + Delete: func(b objectstorage.Bucket) error { + _, delErr := s3.BucketsApi.DeleteBucket(c.Context, b.GetName()).Execute() + return delErr + }, }) } diff --git a/commands/vm-autoscaling/group/delete.go b/commands/vm-autoscaling/group/delete.go index 2717a57acc..745b89c109 100644 --- a/commands/vm-autoscaling/group/delete.go +++ b/commands/vm-autoscaling/group/delete.go @@ -31,9 +31,7 @@ func Delete() *core.Command { }, CmdRun: func(c *core.CommandConfig) error { if viper.GetBool(core.GetFlagName(c.NS, constants.ArgAll)) { - return deleteGroups(c, GroupsProperty(func(r vmasc.Group) string { - return *r.Id - })) + return deleteAll(c) } id := viper.GetString(core.GetFlagName(c.NS, constants.FlagGroupId)) return deleteGroups(c, []string{id}) @@ -58,6 +56,44 @@ func Delete() *core.Command { return cmd } +func deleteAll(c *core.CommandConfig) error { + return core.DeleteAll(c, core.DeleteAllOptions[vmasc.Group]{ + Resource: "group", + List: func() ([]vmasc.Group, error) { + groups, err := Groups(func(r vmasc.ApiGroupsGetRequest) (vmasc.ApiGroupsGetRequest, error) { + return r.Depth(1), nil + }) + if err != nil { + return nil, err + } + if groups.Items == nil { + return nil, nil + } + return *groups.Items, nil + }, + Summary: func(g vmasc.Group) string { + s := "" + if g.Id != nil { + s = *g.Id + } + if p := g.Properties; p != nil { + if p.Name != nil { + s = fmt.Sprintf("%s (%s)", s, *p.Name) + } + if p.Location != nil { + s = fmt.Sprintf("%s located in %s", s, *p.Location) + } + } + return s + }, + ID: func(g vmasc.Group) string { return *g.Id }, + Delete: func(g vmasc.Group) error { + _, err := client.Must().VMAscClient.GroupsDelete(context.Background(), *g.Id).Execute() + return err + }, + }) +} + func deleteGroups(c *core.CommandConfig, ids []string) error { var errs error for _, id := range ids { diff --git a/commands/vpn/ipsec/gateway/delete.go b/commands/vpn/ipsec/gateway/delete.go index c0bbd41b32..d7ba9e4d06 100644 --- a/commands/vpn/ipsec/gateway/delete.go +++ b/commands/vpn/ipsec/gateway/delete.go @@ -10,7 +10,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" "github.com/ionos-cloud/sdk-go-bundle/products/vpn/v2" "github.com/ionos-cloud/sdk-go-bundle/shared" "github.com/spf13/viper" @@ -70,28 +69,39 @@ func Delete() *core.Command { } func deleteAll(c *core.CommandConfig) error { - // Fan out over every location (when --location is unset) so `delete --all` - // spans all locations, matching `list`. Each location gets its own client. - return c.RunForAllLocations(func(cfg *shared.Configuration, location string) error { + // Gather gateways from every location (unless --location pins one), tagging each with its + // location and location-scoped client, then hand the flat list to core.DeleteAll for a + // consistent preview / per-item confirm-skip / summary flow. + type located struct { + gateway vpn.IPSecGatewayRead + loc string + api *vpn.APIClient + } + var items []located + if err := c.RunForAllLocations(func(cfg *shared.Configuration, location string) error { vc := vpn.NewAPIClient(cfg) - c.Verbose("Deleting all gateways in %s!", location) xs, _, err := vc.IPSecGatewaysApi.IpsecgatewaysGet(context.Background()).Execute() if err != nil { return fmt.Errorf("failed listing gateways: %w", err) } + for _, g := range xs.GetItems() { + items = append(items, located{gateway: g, loc: location, api: vc}) + } + return nil + }); err != nil { + return err + } - return functional.ApplyAndAggregateErrors(xs.GetItems(), func(g vpn.IPSecGatewayRead) error { - yes := confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf( - "Are you sure you want to delete gateway %s at %s (location: %s)", - g.Properties.Name, g.Properties.GatewayIP, location), - viper.GetBool(constants.ArgForce)) - if yes { - _, delErr := vc.IPSecGatewaysApi.IpsecgatewaysDelete(context.Background(), g.Id).Execute() - if delErr != nil { - return fmt.Errorf("failed deleting %s (name: %s): %w", g.Id, g.Properties.Name, delErr) - } - } - return nil - }) + return core.DeleteAll(c, core.DeleteAllOptions[located]{ + Resource: "gateway", + List: func() ([]located, error) { return items, nil }, + Summary: func(l located) string { + return fmt.Sprintf("%s (id: %s, ip: %s, location: %s)", l.gateway.Properties.Name, l.gateway.Id, l.gateway.Properties.GatewayIP, l.loc) + }, + ID: func(l located) string { return l.gateway.Id }, + Delete: func(l located) error { + _, err := l.api.IPSecGatewaysApi.IpsecgatewaysDelete(context.Background(), l.gateway.Id).Execute() + return err + }, }) } diff --git a/commands/vpn/ipsec/tunnel/delete.go b/commands/vpn/ipsec/tunnel/delete.go index 001a78c737..4aaf25b134 100644 --- a/commands/vpn/ipsec/tunnel/delete.go +++ b/commands/vpn/ipsec/tunnel/delete.go @@ -10,7 +10,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" "github.com/ionos-cloud/sdk-go-bundle/products/vpn/v2" "github.com/spf13/viper" ) @@ -75,26 +74,23 @@ func Delete() *core.Command { func deleteAll(c *core.CommandConfig) error { gatewayId := viper.GetString(core.GetFlagName(c.NS, constants.FlagGatewayID)) - c.Verbose("Deleting all tunnels from gateway %s!", gatewayId) - xs, _, err := client.Must().VPNClient.IPSecTunnelsApi.IpsecgatewaysTunnelsGet(context.Background(), gatewayId).Execute() - if err != nil { - return err - } - - err = functional.ApplyAndAggregateErrors(xs.GetItems(), func(p vpn.IPSecTunnelRead) error { - yes := confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf( - "Are you sure you want to delete tunnel %s at %s", - p.Properties.Name, p.Properties.RemoteHost), - viper.GetBool(constants.ArgForce)) - if yes { - _, delErr := client.Must().VPNClient.IPSecGatewaysApi.IpsecgatewaysDelete(context.Background(), p.Id).Execute() - if delErr != nil { - return fmt.Errorf("failed deleting %s (name: %s): %w", p.Id, p.Properties.Name, delErr) + return core.DeleteAll(c, core.DeleteAllOptions[vpn.IPSecTunnelRead]{ + Resource: "tunnel", + List: func() ([]vpn.IPSecTunnelRead, error) { + xs, _, err := client.Must().VPNClient.IPSecTunnelsApi.IpsecgatewaysTunnelsGet(context.Background(), gatewayId).Execute() + if err != nil { + return nil, err } - } - return nil + return xs.GetItems(), nil + }, + Summary: func(p vpn.IPSecTunnelRead) string { + return fmt.Sprintf("%s (id: %s, host: %s)", p.Properties.Name, p.Id, p.Properties.RemoteHost) + }, + ID: func(p vpn.IPSecTunnelRead) string { return p.Id }, + Delete: func(p vpn.IPSecTunnelRead) error { + _, err := client.Must().VPNClient.IPSecTunnelsApi.IpsecgatewaysTunnelsDelete(context.Background(), gatewayId, p.Id).Execute() + return err + }, }) - - return err } diff --git a/commands/vpn/wireguard/gateway/delete.go b/commands/vpn/wireguard/gateway/delete.go index 0142c2d56f..1b6c53f51e 100644 --- a/commands/vpn/wireguard/gateway/delete.go +++ b/commands/vpn/wireguard/gateway/delete.go @@ -10,7 +10,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" "github.com/ionos-cloud/sdk-go-bundle/products/vpn/v2" "github.com/ionos-cloud/sdk-go-bundle/shared" "github.com/spf13/viper" @@ -71,28 +70,39 @@ func Delete() *core.Command { } func deleteAll(c *core.CommandConfig) error { - // Fan out over every location (when --location is unset) so `delete --all` - // spans all locations, matching `list`. Each location gets its own client. - return c.RunForAllLocations(func(cfg *shared.Configuration, location string) error { + // Gather gateways from every location (unless --location pins one), tagging each with its + // location and location-scoped client, then hand the flat list to core.DeleteAll for a + // consistent preview / per-item confirm-skip / summary flow. + type located struct { + gateway vpn.WireguardGatewayRead + loc string + api *vpn.APIClient + } + var items []located + if err := c.RunForAllLocations(func(cfg *shared.Configuration, location string) error { vc := vpn.NewAPIClient(cfg) - c.Verbose("Deleting all gateways in %s!", location) xs, _, err := vc.WireguardGatewaysApi.WireguardgatewaysGet(context.Background()).Execute() if err != nil { return fmt.Errorf("failed listing gateways: %w", err) } + for _, g := range xs.GetItems() { + items = append(items, located{gateway: g, loc: location, api: vc}) + } + return nil + }); err != nil { + return err + } - return functional.ApplyAndAggregateErrors(xs.GetItems(), func(g vpn.WireguardGatewayRead) error { - yes := confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf( - "Are you sure you want to delete gateway %s at %s (location: %s)", - g.Properties.Name, g.Properties.GatewayIP, location), - viper.GetBool(constants.ArgForce)) - if yes { - _, delErr := vc.WireguardGatewaysApi.WireguardgatewaysDelete(context.Background(), g.Id).Execute() - if delErr != nil { - return fmt.Errorf("failed deleting %s (name: %s): %w", g.Id, g.Properties.Name, delErr) - } - } - return nil - }) + return core.DeleteAll(c, core.DeleteAllOptions[located]{ + Resource: "gateway", + List: func() ([]located, error) { return items, nil }, + Summary: func(l located) string { + return fmt.Sprintf("%s (id: %s, ip: %s, location: %s)", l.gateway.Properties.Name, l.gateway.Id, l.gateway.Properties.GatewayIP, l.loc) + }, + ID: func(l located) string { return l.gateway.Id }, + Delete: func(l located) error { + _, err := l.api.WireguardGatewaysApi.WireguardgatewaysDelete(context.Background(), l.gateway.Id).Execute() + return err + }, }) } diff --git a/commands/vpn/wireguard/peer/delete.go b/commands/vpn/wireguard/peer/delete.go index bc8bf09140..e5e72dec40 100644 --- a/commands/vpn/wireguard/peer/delete.go +++ b/commands/vpn/wireguard/peer/delete.go @@ -10,7 +10,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" "github.com/ionos-cloud/sdk-go-bundle/products/vpn/v2" "github.com/spf13/viper" ) @@ -42,7 +41,7 @@ func Delete() *core.Command { } yes := confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf( "Are you sure you want to delete peer %s (host: '%s')", - p.Properties.Name, p.Properties.Endpoint.Host), + p.Properties.Name, peerHost(p)), viper.GetBool(constants.ArgForce)) if !yes { return fmt.Errorf(confirm.UserDenied) @@ -50,7 +49,7 @@ func Delete() *core.Command { _, err = client.Must().VPNClient.WireguardPeersApi.WireguardgatewaysPeersDelete(context.Background(), gatewayId, id).Execute() - return nil + return err }, }) @@ -73,27 +72,34 @@ func Delete() *core.Command { return cmd } +// peerHost returns the peer's endpoint host, or "" if no endpoint is set. +// WireguardPeer.Endpoint is optional, so it must be nil-checked before deref. +func peerHost(p vpn.WireguardPeerRead) string { + if p.Properties.Endpoint == nil { + return "" + } + return p.Properties.Endpoint.Host +} + func deleteAll(c *core.CommandConfig) error { gatewayId := viper.GetString(core.GetFlagName(c.NS, constants.FlagGatewayID)) - c.Verbose("Deleting all peers from gateway %s!", gatewayId) - - xs, _, err := client.Must().VPNClient.WireguardPeersApi.WireguardgatewaysPeersGet(context.Background(), gatewayId).Execute() - if err != nil { - return err - } - err = functional.ApplyAndAggregateErrors(xs.GetItems(), func(p vpn.WireguardPeerRead) error { - yes := confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf( - "Are you sure you want to delete peer %s at %s", p.Properties.Name, p.Properties.Endpoint.Host), - viper.GetBool(constants.ArgForce)) - if yes { - _, delErr := client.Must().VPNClient.WireguardGatewaysApi.WireguardgatewaysDelete(context.Background(), p.Id).Execute() - if delErr != nil { - return fmt.Errorf("failed deleting %s (name: %s): %w", p.Id, p.Properties.Name, delErr) + return core.DeleteAll(c, core.DeleteAllOptions[vpn.WireguardPeerRead]{ + Resource: "peer", + List: func() ([]vpn.WireguardPeerRead, error) { + xs, _, err := client.Must().VPNClient.WireguardPeersApi.WireguardgatewaysPeersGet(context.Background(), gatewayId).Execute() + if err != nil { + return nil, err } - } - return nil + return xs.GetItems(), nil + }, + Summary: func(p vpn.WireguardPeerRead) string { + return fmt.Sprintf("%s (id: %s, host: %s)", p.Properties.Name, p.Id, peerHost(p)) + }, + ID: func(p vpn.WireguardPeerRead) string { return p.Id }, + Delete: func(p vpn.WireguardPeerRead) error { + _, err := client.Must().VPNClient.WireguardPeersApi.WireguardgatewaysPeersDelete(context.Background(), gatewayId, p.Id).Execute() + return err + }, }) - - return err } diff --git a/docs/subcommands/Certificate-Manager/provider/delete.md b/docs/subcommands/Certificate-Manager/provider/delete.md index 1aaa63ff14..80af83f33d 100644 --- a/docs/subcommands/Certificate-Manager/provider/delete.md +++ b/docs/subcommands/Certificate-Manager/provider/delete.md @@ -37,7 +37,7 @@ Delete an Provider ## Options ```text - -a, --all Delete all Providers. Required or -g + -a, --all Delete all Providers. Required or --provider-id -u, --api-url string Override default host URL. If contains placeholder, location will be embedded. Preferred over the config file override 'cert' and env var 'IONOS_API_URL' (default "https://certificate-manager.%s.ionos.com") --cols strings Set of columns to be printed on output Available columns: [Id Name Email Server KeyId KeySecret State] diff --git a/docs/subcommands/Database-as-a-Service/mariadb/cluster/delete.md b/docs/subcommands/Database-as-a-Service/mariadb/cluster/delete.md index 93abcf8504..7b53ee8734 100644 --- a/docs/subcommands/Database-as-a-Service/mariadb/cluster/delete.md +++ b/docs/subcommands/Database-as-a-Service/mariadb/cluster/delete.md @@ -49,7 +49,7 @@ Delete a MariaDB Cluster by ID -h, --help Print usage --limit int Maximum number of items to return per request (default 50) -l, --location string Location of the resource to operate on. When unset, list commands query all locations. Can be one of: de/txl, de/fra, es/vit, fr/par, gb/lhr, us/ewr, us/las, us/mci - -n, --name When deleting all clusters, filter the clusters by a name + -n, --name string When deleting all clusters, filter the clusters by a name --no-headers Don't print table headers when table output is used --offset int Number of items to skip before starting to collect the results --order-by string Property to order the results by diff --git a/docs/subcommands/Database-as-a-Service/mongo/cluster/delete.md b/docs/subcommands/Database-as-a-Service/mongo/cluster/delete.md index 030c13e0ad..2c77791165 100644 --- a/docs/subcommands/Database-as-a-Service/mongo/cluster/delete.md +++ b/docs/subcommands/Database-as-a-Service/mongo/cluster/delete.md @@ -48,7 +48,7 @@ Delete a Mongo Cluster by ID -f, --force Force command to execute without user input -h, --help Print usage --limit int Maximum number of items to return per request (default 50) - -n, --name When deleting all clusters, filter the clusters by a name + -n, --name string When deleting all clusters, filter the clusters by a name --no-headers Don't print table headers when table output is used --offset int Number of items to skip before starting to collect the results --order-by string Property to order the results by diff --git a/internal/core/delete_all.go b/internal/core/delete_all.go new file mode 100644 index 0000000000..37e418ee9b --- /dev/null +++ b/internal/core/delete_all.go @@ -0,0 +1,133 @@ +package core + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/ionos-cloud/ionosctl/v6/internal/constants" + "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" + "github.com/spf13/viper" +) + +// DeleteAllError is returned by DeleteAll when one or more deletions fail. +// Its message is a concise count so the terminal stays readable; the per-item +// compact reasons are available to programmatic callers via Unwrap. +type DeleteAllError struct { + Resource string + Failed, Total int + Errs error // joined one-line reasons, one per failed item +} + +func (e *DeleteAllError) Error() string { + return fmt.Sprintf("%d of %d %s(s) could not be deleted", e.Failed, e.Total, e.Resource) +} + +func (e *DeleteAllError) Unwrap() error { return e.Errs } + +// shortAPIError collapses an IONOS API error to "STATUS - message; message". +// IONOS services share the {"httpStatus":N,"messages":[{"message":...}]} error +// envelope, so it is parsed out of the error text regardless of which SDK +// produced it. Falls back to whitespace-collapsed, truncated text otherwise. +func shortAPIError(err error) string { + s := err.Error() + if i := strings.IndexByte(s, '{'); i >= 0 { + var env struct { + HTTPStatus int `json:"httpStatus"` + Messages []struct { + Message string `json:"message"` + } `json:"messages"` + } + if json.Unmarshal([]byte(s[i:]), &env) == nil && len(env.Messages) > 0 { + msgs := make([]string, len(env.Messages)) + for j, m := range env.Messages { + msgs[j] = m.Message + } + return fmt.Sprintf("%d - %s", env.HTTPStatus, strings.Join(msgs, "; ")) + } + } + s = strings.Join(strings.Fields(s), " ") + if len(s) > 200 { + s = s[:200] + "…" + } + return s +} + +// DeleteAllOptions configures a consistent "delete --all" flow for a resource type. +// +// All ionosctl resources should drive their --all deletion through DeleteAll so that +// behaviour is uniform: a preview of what will be deleted, a per-item confirmation that +// skips (never aborts the rest) when the user answers 'n', error aggregation that +// continues on failure, and a per-item plus summary output. +type DeleteAllOptions[T any] struct { + // Resource is the singular human-readable label used in messages, e.g. "datacenter". + Resource string + // List fetches all candidate resources. Apply any --name/location filters here. + List func() ([]T, error) + // Summary returns a rich one-line description of a resource for the preview list + // and the confirmation prompt, e.g. "myDC (id: abc, location: de/txl, desc: prod)". + Summary func(T) string + // ID returns the resource identifier, used in error messages. + ID func(T) string + // Delete removes a single resource. + Delete func(T) error +} + +// DeleteAll drives the canonical "delete --all" flow: +// +// 1. List candidates; error out if none are found. +// 2. Print a preview of every resource that will be deleted. +// 3. Ask for confirmation per item. Answering 'n' skips ONLY that item and +// continues with the rest; it never cancels the whole operation. The global +// --force flag skips all prompts. +// 4. Delete each confirmed item, aggregating errors and continuing on failure. +// 5. Print per-item success and a final summary (deleted / skipped / failed). +// +// It returns the joined error of all failed deletions, or nil if none failed. +func DeleteAll[T any](c *CommandConfig, o DeleteAllOptions[T]) error { + c.Verbose("Deleting all %ss!", o.Resource) + + items, err := o.List() + if err != nil { + return err + } + if len(items) == 0 { + return fmt.Errorf("no %ss found", o.Resource) + } + + c.Msg("%d %s(s) found to delete:", len(items), o.Resource) + for _, it := range items { + c.Msg(" - %s", o.Summary(it)) + } + + force := viper.GetBool(constants.ArgForce) + + var errs error + var deleted, skipped, failed int + for _, it := range items { + if !confirm.FAsk(c.Command.Command.InOrStdin(), + fmt.Sprintf("delete %s %s", o.Resource, o.Summary(it)), force) { + skipped++ + continue // skip THIS item only - never abort the remaining items + } + + if err := o.Delete(it); err != nil { + failed++ + short := shortAPIError(err) + c.Msg("✗ %s %s — %s", o.Resource, o.ID(it), short) + c.Verbose("failed to delete %s %s: %v", o.Resource, o.ID(it), err) // raw detail only with --verbose + errs = errors.Join(errs, fmt.Errorf("%s %s: %s", o.Resource, o.ID(it), short)) + continue + } + + deleted++ + c.Msg("✓ %s %s deleted", o.Resource, o.ID(it)) + } + + c.Msg("Done: %d deleted, %d skipped, %d failed", deleted, skipped, failed) + if failed > 0 { + return &DeleteAllError{Resource: o.Resource, Failed: failed, Total: len(items), Errs: errs} + } + return nil +} diff --git a/internal/core/delete_all_test.go b/internal/core/delete_all_test.go new file mode 100644 index 0000000000..a8dde58b64 --- /dev/null +++ b/internal/core/delete_all_test.go @@ -0,0 +1,178 @@ +package core + +import ( + "bytes" + "errors" + "fmt" + "io" + "strings" + "testing" + + "github.com/ionos-cloud/ionosctl/v6/internal/constants" + "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +// scriptedConfirmer answers each FAsk call from a queue of booleans. +// A true override (e.g. --force) short-circuits to true without consuming an answer. +type scriptedConfirmer struct{ answers []bool } + +func (s *scriptedConfirmer) Ask(_ io.Reader, _ string, overrides ...bool) bool { + for _, o := range overrides { + if o { + return true + } + } + if len(s.answers) == 0 { + return false + } + a := s.answers[0] + s.answers = s.answers[1:] + return a +} + +func newTestCmdConfig(out io.Writer) *CommandConfig { + c := &CommandConfig{ + Command: &Command{ + Command: &cobra.Command{Use: "test"}, + }, + NS: "test", + Resource: "thing", + } + c.Command.Command.SetOut(out) + c.Command.Command.SetIn(strings.NewReader("")) + return c +} + +func TestDeleteAll(t *testing.T) { + type res struct { + id string + name string + } + all := []res{{"a", "alpha"}, {"b", "beta"}, {"c", "gamma"}} + + baseOpts := func(deleted *[]string, failIDs map[string]bool) DeleteAllOptions[res] { + return DeleteAllOptions[res]{ + Resource: "thing", + List: func() ([]res, error) { return all, nil }, + Summary: func(r res) string { return fmt.Sprintf("%s (id: %s)", r.name, r.id) }, + ID: func(r res) string { return r.id }, + Delete: func(r res) error { + if failIDs[r.id] { + return fmt.Errorf("boom %s", r.id) + } + *deleted = append(*deleted, r.id) + return nil + }, + } + } + + t.Run("empty list errors", func(t *testing.T) { + viper.Reset() + viper.Set(constants.ArgOutput, constants.DefaultOutputFormat) + c := newTestCmdConfig(&bytes.Buffer{}) + var deleted []string + opts := baseOpts(&deleted, nil) + opts.List = func() ([]res, error) { return nil, nil } + err := DeleteAll(c, opts) + assert.Error(t, err) + assert.Empty(t, deleted) + }) + + t.Run("list error propagates", func(t *testing.T) { + viper.Reset() + viper.Set(constants.ArgOutput, constants.DefaultOutputFormat) + c := newTestCmdConfig(&bytes.Buffer{}) + var deleted []string + opts := baseOpts(&deleted, nil) + opts.List = func() ([]res, error) { return nil, fmt.Errorf("list failed") } + err := DeleteAll(c, opts) + assert.Error(t, err) + }) + + t.Run("force deletes all without prompting", func(t *testing.T) { + viper.Reset() + viper.Set(constants.ArgOutput, constants.DefaultOutputFormat) + viper.Set(constants.ArgForce, true) + confirm.SetStrategy(&scriptedConfirmer{answers: nil}) // no answers; force must bypass + defer confirm.SetStrategy(nil) + + c := newTestCmdConfig(&bytes.Buffer{}) + var deleted []string + err := DeleteAll(c, baseOpts(&deleted, nil)) + assert.NoError(t, err) + assert.Equal(t, []string{"a", "b", "c"}, deleted) + }) + + t.Run("answering n skips only that item and continues", func(t *testing.T) { + viper.Reset() + viper.Set(constants.ArgOutput, constants.DefaultOutputFormat) + viper.Set(constants.ArgForce, false) + // yes, no, yes -> delete a, skip b, delete c + confirm.SetStrategy(&scriptedConfirmer{answers: []bool{true, false, true}}) + defer confirm.SetStrategy(nil) + + c := newTestCmdConfig(&bytes.Buffer{}) + var deleted []string + err := DeleteAll(c, baseOpts(&deleted, nil)) + assert.NoError(t, err) + assert.Equal(t, []string{"a", "c"}, deleted, "b must be skipped, a and c still deleted") + }) + + t.Run("delete error is aggregated and does not stop the rest", func(t *testing.T) { + viper.Reset() + viper.Set(constants.ArgOutput, constants.DefaultOutputFormat) + viper.Set(constants.ArgForce, true) + + c := newTestCmdConfig(&bytes.Buffer{}) + var deleted []string + err := DeleteAll(c, baseOpts(&deleted, map[string]bool{"b": true})) + assert.Error(t, err) + assert.Equal(t, []string{"a", "c"}, deleted, "a and c still deleted despite b failing") + + var dErr *DeleteAllError + assert.ErrorAs(t, err, &dErr) + assert.Equal(t, 1, dErr.Failed) + assert.Equal(t, 3, dErr.Total) + // Top-level message is the concise count, not the raw per-item detail... + assert.NotContains(t, err.Error(), "boom") + assert.Contains(t, err.Error(), "1 of 3") + // ...but the per-item reasons remain reachable via Unwrap for programmatic callers. + assert.Contains(t, errors.Unwrap(err).Error(), "b") + }) + + t.Run("shortAPIError extracts IONOS envelope", func(t *testing.T) { + raw := errors.New(`403 Forbidden { + "httpStatus" : 403, + "messages" : [ { + "errorCode" : "451", + "message" : "Access Denied: Lan 188ba923 is delete-protected by 'in-memory-db'." + } ] +}`) + got := shortAPIError(raw) + assert.Equal(t, "403 - Access Denied: Lan 188ba923 is delete-protected by 'in-memory-db'.", got) + }) + + t.Run("shortAPIError falls back for non-envelope errors", func(t *testing.T) { + got := shortAPIError(errors.New("plain network\n\ttimeout")) + assert.Equal(t, "plain network timeout", got) + }) + + t.Run("preview lists every resource", func(t *testing.T) { + viper.Reset() + viper.Set(constants.ArgOutput, constants.DefaultOutputFormat) + viper.Set(constants.ArgForce, true) + + buf := &bytes.Buffer{} + c := newTestCmdConfig(buf) + var deleted []string + _ = DeleteAll(c, baseOpts(&deleted, nil)) + out := buf.String() + assert.Contains(t, out, "alpha (id: a)") + assert.Contains(t, out, "beta (id: b)") + assert.Contains(t, out, "gamma (id: c)") + assert.Contains(t, out, "Done: 3 deleted, 0 skipped, 0 failed") + }) +}