diff --git a/CHANGELOG.md b/CHANGELOG.md index a3e3608a3..6438b2f71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,19 @@ # Changelog -## Unreleased +## 1.78.4 + +### Improvements + +- Compute Instance delete: Remove multiple entities by their IDs/Names #619 ### Bug Fixes - output template: use text/template #617 +### Improvements + +- egoscale/v3: use separate module v3.1.0 #621 + ## 1.78.3 ### Improvements diff --git a/bucket/exoscale-cli.json b/bucket/exoscale-cli.json index d92f512a9..8b3169295 100644 --- a/bucket/exoscale-cli.json +++ b/bucket/exoscale-cli.json @@ -1,12 +1,12 @@ { - "version": "1.78.3", + "version": "1.78.4", "architecture": { "64bit": { - "url": "https://github.com/exoscale/cli/releases/download/v1.78.3/exoscale-cli_1.78.3_windows_amd64.zip", + "url": "https://github.com/exoscale/cli/releases/download/v1.78.4/exoscale-cli_1.78.4_windows_amd64.zip", "bin": [ "exo.exe" ], - "hash": "1f7835d226fad75095ae847395d7c0a92d72fece5f6168ec4774369007346df0" + "hash": "0b0f0357641fb62006504e0a8068393bf986be0f27b8b5ecc5fa3e7eff23acff" } }, "homepage": "https://github.com/exoscale/cli", diff --git a/cmd/instance_delete.go b/cmd/instance_delete.go index 1667300d4..01f734d21 100644 --- a/cmd/instance_delete.go +++ b/cmd/instance_delete.go @@ -1,16 +1,15 @@ package cmd import ( - "errors" "fmt" "os" "path" + "strings" "github.com/spf13/cobra" - "github.com/exoscale/cli/pkg/account" "github.com/exoscale/cli/pkg/globalstate" - exoapi "github.com/exoscale/egoscale/v2/api" + v3 "github.com/exoscale/egoscale/v3" ) type instanceDeleteCmd struct { @@ -18,7 +17,7 @@ type instanceDeleteCmd struct { _ bool `cli-cmd:"delete"` - Instance string `cli-arg:"#" cli-usage:"NAME|ID"` + Instances []string `cli-arg:"#" cli-usage:"NAME|ID"` Force bool `cli-short:"f" cli-usage:"don't prompt for confirmation"` Zone string `cli-short:"z" cli-usage:"instance zone"` @@ -36,33 +35,67 @@ func (c *instanceDeleteCmd) cmdPreRun(cmd *cobra.Command, args []string) error { } func (c *instanceDeleteCmd) cmdRun(_ *cobra.Command, _ []string) error { - ctx := exoapi.WithEndpoint(gContext, exoapi.NewReqEndpoint(account.CurrentAccount.Environment, c.Zone)) + ctx := gContext + client, err := switchClientZoneV3( + ctx, + globalstate.EgoscaleV3Client, + v3.ZoneName(c.Zone), + ) + if err != nil { + return err + } - instance, err := globalstate.EgoscaleClient.FindInstance(ctx, c.Zone, c.Instance) + instances, err := client.ListInstances(ctx) if err != nil { - if errors.Is(err, exoapi.ErrNotFound) { - return fmt.Errorf("resource not found in zone %q", c.Zone) - } return err } - if !c.Force { - if !askQuestion(fmt.Sprintf("Are you sure you want to delete instance %q?", c.Instance)) { - return nil + instanceToDelete := []v3.UUID{} + for _, i := range c.Instances { + instance, err := instances.FindListInstancesResponseInstances(i) + if err != nil { + if !c.Force { + return err + } + fmt.Fprintf(os.Stderr, "warning: %s not found.\n", i) + + continue } + + if !c.Force { + if !askQuestion(fmt.Sprintf("Are you sure you want to delete instance %q?", i)) { + return nil + } + } + + instanceToDelete = append(instanceToDelete, instance.ID) + } + + var fns []func() error + for _, i := range instanceToDelete { + fns = append(fns, func() error { + op, err := client.DeleteInstance(ctx, i) + if err != nil { + return err + } + _, err = client.Wait(ctx, op, v3.OperationStateSuccess) + return err + }) } - decorateAsyncOperation(fmt.Sprintf("Deleting instance %q...", c.Instance), func() { - err = globalstate.EgoscaleClient.DeleteInstance(ctx, c.Zone, instance) - }) + err = decorateAsyncOperations(fmt.Sprintf("Deleting instance %q...", strings.Join(c.Instances, ", ")), fns...) if err != nil { return err } - instanceDir := path.Join(globalstate.ConfigFolder, "instances", *instance.ID) - if _, err := os.Stat(instanceDir); !os.IsNotExist(err) { - if err := os.RemoveAll(instanceDir); err != nil { - return fmt.Errorf("error deleting instance directory: %w", err) + // Cleaning up resources created in create instance + // https://github.com/exoscale/cli/blob/master/cmd/instance_create.go#L220 + for _, i := range instanceToDelete { + instanceDir := path.Join(globalstate.ConfigFolder, "instances", i.String()) + if _, err := os.Stat(instanceDir); !os.IsNotExist(err) { + if err := os.RemoveAll(instanceDir); err != nil { + return fmt.Errorf("error deleting instance directory: %w", err) + } } } diff --git a/cmd/limits.go b/cmd/limits.go index e1c9168a5..c1df2fdcf 100644 --- a/cmd/limits.go +++ b/cmd/limits.go @@ -62,8 +62,8 @@ Supported output template annotations: %s`, limitSKSClusters: "SKS clusters", limitSOSBuckets: "SOS buckets", limitBlockStorageVolumes: "Block Storage Volumes", - limitBlockStorage: "Block Storage cumulative size", - limitBlockStorageMaxSize: "Max Size of Block Storage Volumes", + limitBlockStorage: "Block Storage cumulative size (GiB)", + limitBlockStorageMaxSize: "Max size of a Block Storage Volume (GiB)", } out := LimitsOutput{} diff --git a/cmd/output.go b/cmd/output.go index 7d2f46742..3714658c6 100644 --- a/cmd/output.go +++ b/cmd/output.go @@ -5,6 +5,7 @@ import ( "strconv" "time" + "github.com/hashicorp/go-multierror" "github.com/spf13/cobra" "github.com/vbauerster/mpb/v4" "github.com/vbauerster/mpb/v4/decor" @@ -46,6 +47,7 @@ func printOutput(o output.Outputter, err error) error { // decorateAsyncOperation is a cosmetic helper intended for wrapping long // asynchronous operations, outputting progress feedback to the user's // terminal. +// TODO remove this one once all has been migrated to decorateAsyncOperations. func decorateAsyncOperation(message string, fn func()) { p := mpb.New( mpb.WithOutput(os.Stderr), @@ -75,6 +77,54 @@ func decorateAsyncOperation(message string, fn func()) { p.Wait() } +func decorateAsyncOperations(message string, fns ...func() error) error { + if len(fns) == 0 { + return nil + } + + p := mpb.New( + mpb.WithOutput(os.Stderr), + mpb.WithWidth(1), + mpb.ContainerOptOn(mpb.WithOutput(nil), func() bool { return globalstate.Quiet }), + ) + + spinner := p.AddSpinner( + int64(len(fns)), + mpb.SpinnerOnLeft, + mpb.AppendDecorators( + decor.Name(message, decor.WC{W: len(message) + 1, C: decor.DidentRight}), + decor.Elapsed(decor.ET_STYLE_GO), + ), + mpb.BarOnComplete("✔"), + ) + + errs := &multierror.Error{} + done := make(chan struct{}) + defer close(done) + + for i := 0; i < len(fns); i += 10 { + batchSize := min(10, len(fns)-i) + for j := 0; j < batchSize; j++ { + fnIndex := i + j + go func(doneCh chan struct{}, fn func() error) { + if err := fn(); err != nil { + errs = multierror.Append(errs, err) + } + doneCh <- struct{}{} + }(done, fns[fnIndex]) + } + + for j := 0; j < batchSize; j++ { + <-done + spinner.Increment(1) + } + } + + p.Wait() + + return errs.ErrorOrNil() +} + func init() { RootCmd.AddCommand(&cobra.Command{ Use: "output", diff --git a/go.mod b/go.mod index c2e314f65..8d7114748 100644 --- a/go.mod +++ b/go.mod @@ -10,8 +10,8 @@ require ( github.com/aws/aws-sdk-go-v2/service/s3 v1.2.0 github.com/aws/smithy-go v1.1.0 github.com/dustin/go-humanize v1.0.1 - github.com/exoscale/egoscale v0.102.5-0.20240712161721-275fc20694fd - github.com/exoscale/egoscale/v3 v3.1.1-0.20240712161721-275fc20694fd + github.com/exoscale/egoscale v0.102.4 + github.com/exoscale/egoscale/v3 v3.1.0 github.com/exoscale/openapi-cli-generator v1.1.0 github.com/fatih/camelcase v1.0.0 github.com/google/uuid v1.4.0 diff --git a/go.sum b/go.sum index 8afdc2a3f..00994b315 100644 --- a/go.sum +++ b/go.sum @@ -195,10 +195,10 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= -github.com/exoscale/egoscale v0.102.5-0.20240712161721-275fc20694fd h1:2rQ9GatI7MzMwfx1hX0i5znmjk8F9XCyJX/DTz9UCjE= -github.com/exoscale/egoscale v0.102.5-0.20240712161721-275fc20694fd/go.mod h1:xKtCzfF+1O6sKtMb7QANYrTher0EFhkmw8LQLu7Scm0= -github.com/exoscale/egoscale/v3 v3.1.1-0.20240712161721-275fc20694fd h1:RhnSciyRNzsa2qdAr7k8S7diePF8bx6Nyg/nmTQXMjI= -github.com/exoscale/egoscale/v3 v3.1.1-0.20240712161721-275fc20694fd/go.mod h1:lPsza7G+giSxdzvzaHSEcjEAYz/YTiu2bEEha9KVAc4= +github.com/exoscale/egoscale v0.102.4 h1:GBKsZMIOzwBfSu+4ZmWka3Ejf2JLiaBDHp4CQUgvp2E= +github.com/exoscale/egoscale v0.102.4/go.mod h1:ROSmPtle0wvf91iLZb09++N/9BH2Jo9XxIpAEumvocA= +github.com/exoscale/egoscale/v3 v3.1.0 h1:8MSA0j4TZbUiE6iIzTmoY0URa3RoGGuHhX5oamCql4o= +github.com/exoscale/egoscale/v3 v3.1.0/go.mod h1:lPsza7G+giSxdzvzaHSEcjEAYz/YTiu2bEEha9KVAc4= github.com/exoscale/openapi-cli-generator v1.1.0 h1:fYjmPqHR5vxlOBrbvde7eo7bISNQIFxsGn4A5/acwKA= github.com/exoscale/openapi-cli-generator v1.1.0/go.mod h1:TZBnbT7f3hJ5ImyUphJwRM+X5xF/zCQZ6o8a42gQeTs= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= diff --git a/vendor/modules.txt b/vendor/modules.txt index 0b33812eb..da07b495c 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -214,13 +214,13 @@ github.com/dlclark/regexp2/syntax # github.com/dustin/go-humanize v1.0.1 ## explicit; go 1.16 github.com/dustin/go-humanize -# github.com/exoscale/egoscale v0.102.5-0.20240712161721-275fc20694fd +# github.com/exoscale/egoscale v0.102.4 ## explicit; go 1.22 github.com/exoscale/egoscale/v2 github.com/exoscale/egoscale/v2/api github.com/exoscale/egoscale/v2/oapi github.com/exoscale/egoscale/version -# github.com/exoscale/egoscale/v3 v3.1.1-0.20240712161721-275fc20694fd +# github.com/exoscale/egoscale/v3 v3.1.0 ## explicit; go 1.22 github.com/exoscale/egoscale/v3 github.com/exoscale/egoscale/v3/credentials