From e02d6bcaabaffc3f05ccf51fbdca76e6e6f48d34 Mon Sep 17 00:00:00 2001 From: Cezar Craciunoiu Date: Wed, 24 Apr 2024 18:01:13 +0300 Subject: [PATCH 1/4] refactor(cloud): Move img remove to function Signed-off-by: Cezar Craciunoiu --- internal/cli/kraft/cloud/img/remove/remove.go | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/internal/cli/kraft/cloud/img/remove/remove.go b/internal/cli/kraft/cloud/img/remove/remove.go index cd2a88bdc..476664c7b 100644 --- a/internal/cli/kraft/cloud/img/remove/remove.go +++ b/internal/cli/kraft/cloud/img/remove/remove.go @@ -14,7 +14,6 @@ import ( "github.com/spf13/cobra" kraftcloud "sdk.kraft.cloud" - kcimages "sdk.kraft.cloud/images" "kraftkit.sh/cmdfactory" "kraftkit.sh/config" @@ -23,11 +22,11 @@ import ( ) type RemoveOptions struct { - All bool `long:"all" usage:"Remove all images"` - Auth *config.AuthConfig `noattribute:"true"` - Client kcimages.ImagesService `noattribute:"true"` - Metro string `noattribute:"true"` - Token string `noattribute:"true"` + All bool `long:"all" usage:"Remove all images"` + Auth *config.AuthConfig `noattribute:"true"` + Client kraftcloud.KraftCloud `noattribute:"true"` + Metro string `noattribute:"true"` + Token string `noattribute:"true"` } func NewCmd() *cobra.Command { @@ -82,7 +81,8 @@ func (opts *RemoveOptions) Pre(cmd *cobra.Command, args []string) error { return nil } -func (opts *RemoveOptions) Run(ctx context.Context, args []string) error { +// Remove an image from your account. +func Remove(ctx context.Context, opts *RemoveOptions, args ...string) error { var err error if opts.Auth == nil { @@ -93,13 +93,13 @@ func (opts *RemoveOptions) Run(ctx context.Context, args []string) error { } if opts.Client == nil { - opts.Client = kraftcloud.NewImagesClient( + opts.Client = kraftcloud.NewClient( kraftcloud.WithToken(config.GetKraftCloudTokenAuthConfig(*opts.Auth)), ) } if opts.All { - imgListResp, err := opts.Client.WithMetro(opts.Metro).List(ctx) + imgListResp, err := opts.Client.Images().WithMetro(opts.Metro).List(ctx) if err != nil { return fmt.Errorf("listing images: %w", err) } @@ -119,7 +119,7 @@ func (opts *RemoveOptions) Run(ctx context.Context, args []string) error { log.G(ctx).Infof("removing %s", image.Digest) - if err := opts.Client.WithMetro(opts.Metro).DeleteByName(ctx, image.Digest); err != nil { + if err := opts.Client.Images().WithMetro(opts.Metro).DeleteByName(ctx, image.Digest); err != nil { log.G(ctx).Warnf("could not delete image: %s", err.Error()) if strings.Contains(err.Error(), "NOT_FOUND") { @@ -136,7 +136,7 @@ func (opts *RemoveOptions) Run(ctx context.Context, args []string) error { for _, arg := range args { log.G(ctx).Infof("removing %s", arg) - if err := opts.Client.WithMetro(opts.Metro).DeleteByName(ctx, arg); err != nil { + if err := opts.Client.Images().WithMetro(opts.Metro).DeleteByName(ctx, arg); err != nil { if strings.Contains(err.Error(), "NOT_FOUND") { log.G(ctx).Warnf("%s not found. This is expected if you have already removed it.", arg) } else { @@ -147,3 +147,7 @@ func (opts *RemoveOptions) Run(ctx context.Context, args []string) error { return err } + +func (opts *RemoveOptions) Run(ctx context.Context, args []string) error { + return Remove(ctx, opts, args...) +} From bd7dba7819105f2a413338113dd932ee742ab568 Mon Sep 17 00:00:00 2001 From: Cezar Craciunoiu Date: Wed, 24 Apr 2024 18:02:08 +0300 Subject: [PATCH 2/4] feat(cloud): Move volume remove to function & add '--all' Signed-off-by: Cezar Craciunoiu --- .../cli/kraft/cloud/volume/remove/remove.go | 61 +++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/internal/cli/kraft/cloud/volume/remove/remove.go b/internal/cli/kraft/cloud/volume/remove/remove.go index c081977ed..cb39a21dc 100644 --- a/internal/cli/kraft/cloud/volume/remove/remove.go +++ b/internal/cli/kraft/cloud/volume/remove/remove.go @@ -21,22 +21,13 @@ import ( ) type RemoveOptions struct { + All bool `long:"all" short:"a" usage:"Remove all volumes"` Auth *config.AuthConfig `noattribute:"true"` Client kraftcloud.KraftCloud `noattribute:"true"` - All bool `long:"all" short:"a" usage:"Remove all volumes that are not attached"` Metro string `noattribute:"true"` Token string `noattribute:"true"` } -// Remove a KraftCloud persistent volume. -func Remove(ctx context.Context, opts *RemoveOptions, args ...string) error { - if opts == nil { - opts = &RemoveOptions{} - } - - return opts.Run(ctx, args) -} - func NewCmd() *cobra.Command { cmd, err := cmdfactory.New(&RemoveOptions{}, cobra.Command{ Short: "Permanently delete persistent volume(s)", @@ -76,7 +67,8 @@ func (opts *RemoveOptions) Pre(cmd *cobra.Command, _ []string) error { return nil } -func (opts *RemoveOptions) Run(ctx context.Context, args []string) error { +// Remove a KraftCloud volume. +func Remove(ctx context.Context, opts *RemoveOptions, args ...string) error { var err error if opts.All && len(args) > 0 { @@ -99,42 +91,49 @@ func (opts *RemoveOptions) Run(ctx context.Context, args []string) error { if opts.All { volListResp, err := opts.Client.Volumes().WithMetro(opts.Metro).List(ctx) if err != nil { - return fmt.Errorf("could not list volumes: %w", err) + return fmt.Errorf("listing volumes: %w", err) } - vols, err := volListResp.AllOrErr() + volList, err := volListResp.AllOrErr() if err != nil { - return fmt.Errorf("could not list volumes: %w", err) + return fmt.Errorf("listing volumes: %w", err) } - if len(vols) == 0 { + if len(volList) == 0 { log.G(ctx).Info("no volumes found") return nil } - uuids := make([]string, 0, len(vols)) - for _, vol := range vols { - uuids = append(uuids, vol.UUID) + vols := make([]string, len(volList)) + for i, vol := range volList { + vols[i] = vol.UUID } - log.G(ctx).Infof("removing %d volumes(s)", len(uuids)) + log.G(ctx).Infof("deleting %d volume(s)", len(volList)) - if _, err := opts.Client.Volumes().WithMetro(opts.Metro).Delete(ctx, uuids...); err != nil { - return fmt.Errorf("removing %d volumes(s): %w", len(uuids), err) + delResp, err := opts.Client.Volumes().WithMetro(opts.Metro).Delete(ctx, vols...) + if err != nil { + return fmt.Errorf("deleting %d volume(s): %w", len(volList), err) } - return nil - } - - log.G(ctx).Infof("removing %d volume(s)", len(args)) + if _, err = delResp.AllOrErr(); err != nil { + return fmt.Errorf("deleting %d volume(s): %w", len(volList), err) + } + } else { + log.G(ctx).Infof("deleting %d volume(s)", len(args)) - delResp, err := opts.Client.Volumes().WithMetro(opts.Metro).Delete(ctx, args...) - if err != nil { - return fmt.Errorf("deleting %d volume(s): %w", len(args), err) - } - if _, err = delResp.AllOrErr(); err != nil { - return fmt.Errorf("deleting %d volume(s): %w", len(args), err) + delResp, err := opts.Client.Volumes().WithMetro(opts.Metro).Delete(ctx, args...) + if err != nil { + return fmt.Errorf("deleting %d volume(s): %w", len(args), err) + } + if _, err = delResp.AllOrErr(); err != nil { + return fmt.Errorf("deleting %d volume(s): %w", len(args), err) + } } return nil } + +func (opts *RemoveOptions) Run(ctx context.Context, args []string) error { + return Remove(ctx, opts, args...) +} From f0fa6d455214e34ae5212b7ddea200a1abd4822a Mon Sep 17 00:00:00 2001 From: Cezar Craciunoiu Date: Wed, 24 Apr 2024 18:02:45 +0300 Subject: [PATCH 3/4] refactor(cloud): Move certificate remove to function Signed-off-by: Cezar Craciunoiu --- .../kraft/cloud/certificate/remove/remove.go | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/internal/cli/kraft/cloud/certificate/remove/remove.go b/internal/cli/kraft/cloud/certificate/remove/remove.go index 78d3524ab..add145aa2 100644 --- a/internal/cli/kraft/cloud/certificate/remove/remove.go +++ b/internal/cli/kraft/cloud/certificate/remove/remove.go @@ -21,11 +21,12 @@ import ( ) type RemoveOptions struct { - Output string `long:"output" short:"o" usage:"Set output format. Options: table,yaml,json,list,raw" default:"table"` - All bool `long:"all" usage:"Remove all certificates"` - - metro string - token string + Output string `long:"output" short:"o" usage:"Set output format. Options: table,yaml,json,list,raw" default:"table"` + All bool `long:"all" usage:"Remove all certificates"` + Metro string `noattribute:"true"` + Token string `noattribute:"true"` + Auth *config.AuthConfig `noattribute:"true"` + Client kraftcloud.KraftCloud `noattribute:"true"` } // Remove a KraftCloud certificate. @@ -72,7 +73,7 @@ func (opts *RemoveOptions) Pre(cmd *cobra.Command, args []string) error { return fmt.Errorf("either specify a certificate name or UUID, or use the --all flag") } - err := utils.PopulateMetroToken(cmd, &opts.metro, &opts.token) + err := utils.PopulateMetroToken(cmd, &opts.Metro, &opts.Token) if err != nil { return fmt.Errorf("could not populate metro and token: %w", err) } @@ -85,17 +86,23 @@ func (opts *RemoveOptions) Pre(cmd *cobra.Command, args []string) error { } func (opts *RemoveOptions) Run(ctx context.Context, args []string) error { - auth, err := config.GetKraftCloudAuthConfig(ctx, opts.token) - if err != nil { - return fmt.Errorf("could not retrieve credentials: %w", err) + var err error + + if opts.Auth == nil { + opts.Auth, err = config.GetKraftCloudAuthConfig(ctx, opts.Token) + if err != nil { + return fmt.Errorf("could not retrieve credentials: %w", err) + } } - client := kraftcloud.NewCertificatesClient( - kraftcloud.WithToken(config.GetKraftCloudTokenAuthConfig(*auth)), - ) + if opts.Client == nil { + opts.Client = kraftcloud.NewClient( + kraftcloud.WithToken(config.GetKraftCloudTokenAuthConfig(*opts.Auth)), + ) + } if opts.All { - certListResp, err := client.WithMetro(opts.metro).List(ctx) + certListResp, err := opts.Client.Certificates().WithMetro(opts.Metro).List(ctx) if err != nil { return fmt.Errorf("could not list certificates: %w", err) } @@ -104,6 +111,7 @@ func (opts *RemoveOptions) Run(ctx context.Context, args []string) error { return fmt.Errorf("could not list certificates: %w", err) } if len(certList) == 0 { + log.G(ctx).Info("no certificates found") return nil } @@ -114,7 +122,7 @@ func (opts *RemoveOptions) Run(ctx context.Context, args []string) error { uuids = append(uuids, certItem.UUID) } - delResp, err := client.WithMetro(opts.metro).Delete(ctx, uuids...) + delResp, err := opts.Client.Certificates().WithMetro(opts.Metro).Delete(ctx, uuids...) if err != nil { return fmt.Errorf("removing %d certificate(s): %w", len(uuids), err) } @@ -126,7 +134,7 @@ func (opts *RemoveOptions) Run(ctx context.Context, args []string) error { log.G(ctx).Infof("removing %d certificate(s)", len(args)) - delResp, err := client.WithMetro(opts.metro).Delete(ctx, args...) + delResp, err := opts.Client.Certificates().WithMetro(opts.Metro).Delete(ctx, args...) if err != nil { return fmt.Errorf("removing %d certificate(s): %w", len(args), err) } From b30f2dd4fbb8d7d7de4c9417aaa8c98ead5ec3fa Mon Sep 17 00:00:00 2001 From: Cezar Craciunoiu Date: Wed, 24 Apr 2024 18:03:16 +0300 Subject: [PATCH 4/4] feat(cloud): Add cloud purge subcommand Signed-off-by: Cezar Craciunoiu --- internal/cli/kraft/cloud/cloud.go | 2 + internal/cli/kraft/cloud/purge/purge.go | 221 ++++++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 internal/cli/kraft/cloud/purge/purge.go diff --git a/internal/cli/kraft/cloud/cloud.go b/internal/cli/kraft/cloud/cloud.go index 24c70d49f..d6bdcdb9c 100644 --- a/internal/cli/kraft/cloud/cloud.go +++ b/internal/cli/kraft/cloud/cloud.go @@ -18,6 +18,7 @@ import ( "kraftkit.sh/internal/cli/kraft/cloud/img" "kraftkit.sh/internal/cli/kraft/cloud/instance" "kraftkit.sh/internal/cli/kraft/cloud/metros" + "kraftkit.sh/internal/cli/kraft/cloud/purge" "kraftkit.sh/internal/cli/kraft/cloud/quotas" "kraftkit.sh/internal/cli/kraft/cloud/scale" "kraftkit.sh/internal/cli/kraft/cloud/service" @@ -88,6 +89,7 @@ func NewCmd() *cobra.Command { cmd.AddCommand(deploy.NewCmd()) cmd.AddCommand(quotas.NewCmd()) cmd.AddCommand(tunnel.NewCmd()) + cmd.AddCommand(purge.NewCmd()) cmd.AddGroup(&cobra.Group{ID: "kraftcloud-img", Title: "IMAGE COMMANDS"}) cmd.AddCommand(img.NewCmd()) diff --git a/internal/cli/kraft/cloud/purge/purge.go b/internal/cli/kraft/cloud/purge/purge.go new file mode 100644 index 000000000..a79efbb7b --- /dev/null +++ b/internal/cli/kraft/cloud/purge/purge.go @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2024, Unikraft GmbH and The KraftKit Authors. +// Licensed under the BSD-3-Clause License (the "License"). +// You may not use this file except in compliance with the License. + +package purge + +import ( + "context" + "fmt" + "strings" + + "github.com/MakeNowJust/heredoc" + "github.com/spf13/cobra" + + "kraftkit.sh/cmdfactory" + "kraftkit.sh/config" + crm "kraftkit.sh/internal/cli/kraft/cloud/certificate/remove" + mrm "kraftkit.sh/internal/cli/kraft/cloud/img/remove" + irm "kraftkit.sh/internal/cli/kraft/cloud/instance/remove" + srm "kraftkit.sh/internal/cli/kraft/cloud/service/remove" + "kraftkit.sh/internal/cli/kraft/cloud/utils" + vrm "kraftkit.sh/internal/cli/kraft/cloud/volume/remove" + "kraftkit.sh/log" + "kraftkit.sh/tui/selection" + + kraftcloud "sdk.kraft.cloud" +) + +type PurgeOptions struct { + Force bool `local:"true" long:"force" short:"f" usage:"Continue removing everything regardless of any errors." default:"false"` + All bool `local:"true" long:"all" short:"a" usage:"Remove from all metros." default:"false"` + Metro string `noattribute:"true"` + Token string `noattribute:"true"` + Auth *config.AuthConfig `noattribute:"true"` + Client kraftcloud.KraftCloud `noattribute:"true"` + + imagesRemoved bool +} + +type purgeAccept string + +func (p purgeAccept) String() string { + return string(p) +} + +const ( + PurgeAcceptYes purgeAccept = "yes" + PurgeAcceptNo purgeAccept = "no" +) + +func NewCmd() *cobra.Command { + cmd, err := cmdfactory.New(&PurgeOptions{}, cobra.Command{ + Short: "Remove everything on KraftCloud", + Use: "purge", + Args: cobra.NoArgs, + Aliases: []string{"p"}, + Annotations: map[string]string{ + cmdfactory.AnnotationHelpGroup: "kraftcloud", + }, + Long: heredoc.Doc(` + Remove everything on KraftCloud. + `), + Example: heredoc.Doc(` + # Remove everything on KraftCloud from the default metro + $ kraft cloud purge + + # Remove everything on KraftCloud from all metros + $ kraft cloud purge -a + + # Remove everything on KraftCloud and continue regardless of any errors + $ kraft cloud purge -f + `), + }) + if err != nil { + panic(err) + } + + return cmd +} + +// Purge removes everything from a metro. +func Purge(ctx context.Context, opts *PurgeOptions) error { + // 1. Instances + err := irm.Remove(ctx, &irm.RemoveOptions{ + Metro: opts.Metro, + Token: opts.Token, + All: true, + }) + if err != nil && !opts.Force { + return fmt.Errorf("could not remove instances: %w", err) + } + + // 2. Certificates + err = crm.Remove(ctx, &crm.RemoveOptions{ + Metro: opts.Metro, + Token: opts.Token, + All: true, + Output: "list", + }) + if err != nil && !opts.Force { + return fmt.Errorf("could not remove certificates: %w", err) + } + + // 4. Services + err = srm.Remove(ctx, &srm.RemoveOptions{ + Metro: opts.Metro, + Token: opts.Token, + All: true, + WaitEmpty: !opts.Force, + }) + if err != nil && !opts.Force { + return fmt.Errorf("could not remove services: %w", err) + } + + // 5. Volumes + err = vrm.Remove(ctx, &vrm.RemoveOptions{ + Metro: opts.Metro, + Token: opts.Token, + All: true, + }) + if err != nil && !opts.Force { + return fmt.Errorf("could not remove volumes: %w", err) + } + + // 6. Images + if !opts.imagesRemoved { + opts.imagesRemoved = true + + err = mrm.Remove(ctx, &mrm.RemoveOptions{ + Metro: opts.Metro, + Token: opts.Token, + All: true, + }) + if err != nil && !opts.Force { + return fmt.Errorf("could not remove images: %w", err) + } + } + + return nil +} + +func (opts *PurgeOptions) Pre(cmd *cobra.Command, _ []string) error { + err := utils.PopulateMetroToken(cmd, &opts.Metro, &opts.Token) + + // Ignore the metro error if the user wants to remove from all metros + if err != nil && !(opts.All && strings.Contains(err.Error(), "metro")) { + return fmt.Errorf("could not populate metro and token: %w", err) + } + + return nil +} + +func (opts *PurgeOptions) Run(ctx context.Context, _ []string) error { + var metros []string + var err error + + if opts.Auth == nil { + opts.Auth, err = config.GetKraftCloudAuthConfig(ctx, opts.Token) + if err != nil { + return fmt.Errorf("could not retrieve credentials: %w", err) + } + } + + if opts.Client == nil { + opts.Client = kraftcloud.NewClient( + kraftcloud.WithToken(config.GetKraftCloudTokenAuthConfig(*opts.Auth)), + ) + } + + if opts.All { + metrosResp, err := opts.Client.Metros().List(ctx, true) + if err != nil { + return fmt.Errorf("could not list metros: %w", err) + } + + for _, metro := range metrosResp { + if !metro.Online { + log.G(ctx).WithField("metro", metro.Code).Warn("kipping offline metro") + continue + } + + metros = append(metros, metro.Code) + } + } else { + metros = append(metros, opts.Metro) + } + + if len(metros) != 0 && !config.G[config.KraftKit](ctx).NoPrompt { + purgeYesNo, err := selection.Select( + fmt.Sprintf("You are about to delete all instances, services, certificates, volumes, images from the following metros: %s. Proceed?", + strings.Join(metros, ", "), + ), + PurgeAcceptNo, + PurgeAcceptYes, + ) + if err != nil { + return err + } + + if purgeYesNo == nil || *purgeYesNo == PurgeAcceptNo { + return nil + } + } + + for _, metro := range metros { + opts.Metro = metro + + log.G(ctx).WithField("metro", metro).Info("purging") + + if err := Purge(ctx, opts); err != nil { + if !opts.Force { + return fmt.Errorf("could not purge metro %q: %w", metro, err) + } + + log.G(ctx).WithField("metro", metro).WithError(err).Warn("could not fully purge") + } + } + + return nil +}