diff --git a/cmd/mapt/cmd/aws/hosts/fedora.go b/cmd/mapt/cmd/aws/hosts/fedora.go index b5de1bded..009996f4b 100644 --- a/cmd/mapt/cmd/aws/hosts/fedora.go +++ b/cmd/mapt/cmd/aws/hosts/fedora.go @@ -97,12 +97,14 @@ func getFedoraDestroy() *cobra.Command { DebugLevel: viper.GetUint(params.DebugLevel), Serverless: viper.IsSet(params.Serverless), ForceDestroy: viper.IsSet(params.ForceDestroy), + CleanupState: viper.IsSet(params.CleanupState), }) }, } flagSet := pflag.NewFlagSet(params.DestroyCmdName, pflag.ExitOnError) flagSet.Bool(params.Serverless, false, params.ServerlessDesc) flagSet.Bool(params.ForceDestroy, false, params.ForceDestroyDesc) + flagSet.Bool(params.CleanupState, true, params.CleanupStateDesc) c.PersistentFlags().AddFlagSet(flagSet) return c } diff --git a/cmd/mapt/cmd/aws/hosts/mac.go b/cmd/mapt/cmd/aws/hosts/mac.go index ad42b1168..70d4a7b9e 100644 --- a/cmd/mapt/cmd/aws/hosts/mac.go +++ b/cmd/mapt/cmd/aws/hosts/mac.go @@ -107,14 +107,16 @@ func getMacDestroy() *cobra.Command { } return mac.Destroy( &maptContext.ContextArgs{ - Debug: viper.IsSet(params.Debug), - DebugLevel: viper.GetUint(params.DebugLevel), + Debug: viper.IsSet(params.Debug), + DebugLevel: viper.GetUint(params.DebugLevel), + CleanupState: viper.IsSet(params.CleanupState), }, viper.GetString(awsParams.MACDHID)) }, } flagSet := pflag.NewFlagSet(params.DestroyCmdName, pflag.ExitOnError) flagSet.StringP(awsParams.MACDHID, "", "", awsParams.MACDHIDDesc) + flagSet.Bool(params.CleanupState, true, params.CleanupStateDesc) c.PersistentFlags().AddFlagSet(flagSet) err := c.MarkPersistentFlagRequired(awsParams.MACDHID) if err != nil { diff --git a/cmd/mapt/cmd/aws/hosts/rhel.go b/cmd/mapt/cmd/aws/hosts/rhel.go index ea67e8020..eee1e73c2 100644 --- a/cmd/mapt/cmd/aws/hosts/rhel.go +++ b/cmd/mapt/cmd/aws/hosts/rhel.go @@ -94,16 +94,18 @@ func getRHELDestroy() *cobra.Command { return err } return rhel.Destroy(&maptContext.ContextArgs{ - ProjectName: viper.GetString(params.ProjectName), - BackedURL: viper.GetString(params.BackedURL), - Debug: viper.IsSet(params.Debug), - DebugLevel: viper.GetUint(params.DebugLevel), - Serverless: viper.IsSet(params.Serverless), + ProjectName: viper.GetString(params.ProjectName), + BackedURL: viper.GetString(params.BackedURL), + Debug: viper.IsSet(params.Debug), + DebugLevel: viper.GetUint(params.DebugLevel), + Serverless: viper.IsSet(params.Serverless), + CleanupState: viper.IsSet(params.CleanupState), }) }, } flagSet := pflag.NewFlagSet(params.DestroyCmdName, pflag.ExitOnError) flagSet.Bool(params.Serverless, false, params.ServerlessDesc) + flagSet.Bool(params.CleanupState, true, params.CleanupStateDesc) c.PersistentFlags().AddFlagSet(flagSet) return c } diff --git a/cmd/mapt/cmd/aws/hosts/rhelai.go b/cmd/mapt/cmd/aws/hosts/rhelai.go index 61000aed6..95b0be6b7 100644 --- a/cmd/mapt/cmd/aws/hosts/rhelai.go +++ b/cmd/mapt/cmd/aws/hosts/rhelai.go @@ -92,12 +92,14 @@ func getRHELAIDestroy() *cobra.Command { DebugLevel: viper.GetUint(params.DebugLevel), Serverless: viper.IsSet(params.Serverless), ForceDestroy: viper.IsSet(params.ForceDestroy), + CleanupState: viper.IsSet(params.CleanupState), }) }, } flagSet := pflag.NewFlagSet(params.DestroyCmdName, pflag.ExitOnError) flagSet.Bool(params.Serverless, false, params.ServerlessDesc) flagSet.Bool(params.ForceDestroy, false, params.ForceDestroyDesc) + flagSet.Bool(params.CleanupState, true, params.CleanupStateDesc) c.PersistentFlags().AddFlagSet(flagSet) return c } diff --git a/cmd/mapt/cmd/aws/hosts/windows.go b/cmd/mapt/cmd/aws/hosts/windows.go index 92f583c2f..d370a268b 100644 --- a/cmd/mapt/cmd/aws/hosts/windows.go +++ b/cmd/mapt/cmd/aws/hosts/windows.go @@ -101,17 +101,22 @@ func getWindowsDestroy() *cobra.Command { Use: params.DestroyCmdName, Short: params.DestroyCmdName, RunE: func(cmd *cobra.Command, args []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return err + } return windows.Destroy(&maptContext.ContextArgs{ - ProjectName: viper.GetString(params.ProjectName), - BackedURL: viper.GetString(params.BackedURL), - Debug: viper.IsSet(params.Debug), - DebugLevel: viper.GetUint(params.DebugLevel), - Serverless: viper.IsSet(params.Serverless), + ProjectName: viper.GetString(params.ProjectName), + BackedURL: viper.GetString(params.BackedURL), + Debug: viper.IsSet(params.Debug), + DebugLevel: viper.GetUint(params.DebugLevel), + Serverless: viper.IsSet(params.Serverless), + CleanupState: viper.IsSet(params.CleanupState), }) }, } flagSet := pflag.NewFlagSet(params.DestroyCmdName, pflag.ExitOnError) flagSet.Bool(params.Serverless, false, params.ServerlessDesc) + flagSet.Bool(params.CleanupState, true, params.CleanupStateDesc) c.PersistentFlags().AddFlagSet(flagSet) return c } diff --git a/cmd/mapt/cmd/aws/services/eks.go b/cmd/mapt/cmd/aws/services/eks.go index a92433a6d..ada111fd9 100644 --- a/cmd/mapt/cmd/aws/services/eks.go +++ b/cmd/mapt/cmd/aws/services/eks.go @@ -98,7 +98,7 @@ func getCreateEKS() *cobra.Command { } func getDestroyEKS() *cobra.Command { - return &cobra.Command{ + c := &cobra.Command{ Use: params.DestroyCmdName, Short: params.DestroyCmdName, RunE: func(cmd *cobra.Command, args []string) error { @@ -107,11 +107,16 @@ func getDestroyEKS() *cobra.Command { } return awsEKS.Destroy( &maptContext.ContextArgs{ - ProjectName: viper.GetString(params.ProjectName), - BackedURL: viper.GetString(params.BackedURL), - Debug: viper.IsSet(params.Debug), - DebugLevel: viper.GetUint(params.DebugLevel), + ProjectName: viper.GetString(params.ProjectName), + BackedURL: viper.GetString(params.BackedURL), + Debug: viper.IsSet(params.Debug), + DebugLevel: viper.GetUint(params.DebugLevel), + CleanupState: viper.IsSet(params.CleanupState), }) }, } + flagSet := pflag.NewFlagSet(params.DestroyCmdName, pflag.ExitOnError) + flagSet.Bool(params.CleanupState, true, params.CleanupStateDesc) + c.PersistentFlags().AddFlagSet(flagSet) + return c } diff --git a/cmd/mapt/cmd/aws/services/kind.go b/cmd/mapt/cmd/aws/services/kind.go index a5e2c2ace..d17e4888d 100644 --- a/cmd/mapt/cmd/aws/services/kind.go +++ b/cmd/mapt/cmd/aws/services/kind.go @@ -100,12 +100,14 @@ func destroyKind() *cobra.Command { DebugLevel: viper.GetUint(params.DebugLevel), Serverless: viper.IsSet(params.Serverless), ForceDestroy: viper.IsSet(params.ForceDestroy), + CleanupState: viper.IsSet(params.CleanupState), }) }, } flagSet := pflag.NewFlagSet(params.DestroyCmdName, pflag.ExitOnError) flagSet.Bool(params.Serverless, false, params.ServerlessDesc) flagSet.Bool(params.ForceDestroy, false, params.ForceDestroyDesc) + flagSet.Bool(params.CleanupState, true, params.CleanupStateDesc) c.PersistentFlags().AddFlagSet(flagSet) return c } diff --git a/cmd/mapt/cmd/aws/services/mac-pool.go b/cmd/mapt/cmd/aws/services/mac-pool.go index 6a9117da8..6a658d435 100644 --- a/cmd/mapt/cmd/aws/services/mac-pool.go +++ b/cmd/mapt/cmd/aws/services/mac-pool.go @@ -98,15 +98,17 @@ func destroyMP() *cobra.Command { return err } return macpool.Destroy(&maptContext.ContextArgs{ - ProjectName: viper.GetString(params.ProjectName), - BackedURL: viper.GetString(params.BackedURL), - Debug: viper.IsSet(params.Debug), - DebugLevel: viper.GetUint(params.DebugLevel), + ProjectName: viper.GetString(params.ProjectName), + BackedURL: viper.GetString(params.BackedURL), + Debug: viper.IsSet(params.Debug), + DebugLevel: viper.GetUint(params.DebugLevel), + CleanupState: viper.IsSet(params.CleanupState), }) }, } flagSet := pflag.NewFlagSet(params.CreateCmdName, pflag.ExitOnError) params.AddCommonFlags(flagSet) + flagSet.Bool(params.CleanupState, true, params.CleanupStateDesc) c.PersistentFlags().AddFlagSet(flagSet) return c } diff --git a/cmd/mapt/cmd/aws/services/openshift-snc.go b/cmd/mapt/cmd/aws/services/openshift-snc.go index ed0c73ff2..0fdd7443b 100644 --- a/cmd/mapt/cmd/aws/services/openshift-snc.go +++ b/cmd/mapt/cmd/aws/services/openshift-snc.go @@ -89,16 +89,18 @@ func destroySNC() *cobra.Command { return err } return openshiftsnc.Destroy(&maptContext.ContextArgs{ - ProjectName: viper.GetString(params.ProjectName), - BackedURL: viper.GetString(params.BackedURL), - Debug: viper.IsSet(params.Debug), - DebugLevel: viper.GetUint(params.DebugLevel), - Serverless: viper.IsSet(params.Serverless), + ProjectName: viper.GetString(params.ProjectName), + BackedURL: viper.GetString(params.BackedURL), + Debug: viper.IsSet(params.Debug), + DebugLevel: viper.GetUint(params.DebugLevel), + Serverless: viper.IsSet(params.Serverless), + CleanupState: viper.IsSet(params.CleanupState), }) }, } flagSet := pflag.NewFlagSet(params.DestroyCmdName, pflag.ExitOnError) flagSet.Bool(params.Serverless, false, params.ServerlessDesc) + flagSet.Bool(params.CleanupState, true, params.CleanupStateDesc) c.PersistentFlags().AddFlagSet(flagSet) return c } diff --git a/cmd/mapt/cmd/params/params.go b/cmd/mapt/cmd/params/params.go index 4b8a0f790..b5db844e0 100644 --- a/cmd/mapt/cmd/params/params.go +++ b/cmd/mapt/cmd/params/params.go @@ -99,9 +99,11 @@ const ( Serverless string = "serverless" ServerlessDesc string = "if serverless is set the command will be executed as a serverless action." - // Desytoy + // Destroy ForceDestroy string = "force-destroy" ForceDestroyDesc string = "if force-destroy is set the command will destroy even if there is a lock." + CleanupState string = "cleanup-state" + CleanupStateDesc string = "automatically remove Pulumi state files from S3 backend after successful destroy" // Kind KindCmd = "kind" diff --git a/pkg/manager/context/context.go b/pkg/manager/context/context.go index 9f37d3c95..210a6a1c2 100644 --- a/pkg/manager/context/context.go +++ b/pkg/manager/context/context.go @@ -43,6 +43,8 @@ type ContextArgs struct { Serverless bool // This forces destroy even when lock exists ForceDestroy bool + // Automatically remove Pulumi state files from backend after successful destroy + CleanupState bool // integrations GHRunnerArgs *github.GithubRunnerArgs CirrusPWArgs *cirrus.PersistentWorkerArgs @@ -57,6 +59,7 @@ type Context struct { debugLevel uint serverless bool forceDestroy bool + cleanupState bool // spotPriceIncreaseRate int tags map[string]string tagsAsPulumiStringMap pulumi.StringMap @@ -83,6 +86,7 @@ func Init(ca *ContextArgs, provider Provider) (*Context, error) { tags: ca.Tags, serverless: ca.Serverless, forceDestroy: ca.ForceDestroy, + cleanupState: ca.CleanupState, } addCommonTags(c) hp, err := provider.DefaultHostingPlace() @@ -126,6 +130,8 @@ func (c *Context) IsServerless() bool { return c.serverless } func (c *Context) IsForceDestroy() bool { return c.forceDestroy } +func (c *Context) IsCleanupState() bool { return c.cleanupState } + func (c *Context) TargetHostingPlace() string { return c.targetHostingPlace } // Get tags ready to be added to any pulumi resource diff --git a/pkg/provider/aws/action/eks/eks.go b/pkg/provider/aws/action/eks/eks.go index 509fec73f..100fed76a 100644 --- a/pkg/provider/aws/action/eks/eks.go +++ b/pkg/provider/aws/action/eks/eks.go @@ -129,12 +129,17 @@ func Destroy(ctx *mc.ContextArgs) error { if err != nil { return err } - return aws.DestroyStack( + if err := aws.DestroyStack( mCtx, aws.DestroyStackRequest{ BackedURL: mCtx.BackedURL(), Stackname: stackName, - }) + }); err != nil { + return err + } + + // Cleanup S3 state after all stacks have been destroyed + return aws.CleanupState(mCtx) } // Main function to deploy all requried resources to AWS diff --git a/pkg/provider/aws/action/fedora/fedora.go b/pkg/provider/aws/action/fedora/fedora.go index 5e041a066..56ca731be 100644 --- a/pkg/provider/aws/action/fedora/fedora.go +++ b/pkg/provider/aws/action/fedora/fedora.go @@ -127,9 +127,13 @@ func Destroy(c *mc.ContextArgs) (err error) { // Destroy spot orchestrated stack if spot.Exist(mCtx) { - return spot.Destroy(mCtx) + if err := spot.Destroy(mCtx); err != nil { + return err + } } - return nil + + // Cleanup S3 state after all stacks have been destroyed + return aws.CleanupState(mCtx) } func (r *fedoraRequest) createMachine() error { diff --git a/pkg/provider/aws/action/kind/kind.go b/pkg/provider/aws/action/kind/kind.go index 9fd4ffc0f..05d15c81f 100644 --- a/pkg/provider/aws/action/kind/kind.go +++ b/pkg/provider/aws/action/kind/kind.go @@ -89,9 +89,13 @@ func Destroy(mCtxArgs *mc.ContextArgs) (err error) { return err } if spot.Exist(mCtx) { - return spot.Destroy(mCtx) + if err := spot.Destroy(mCtx); err != nil { + return err + } } - return nil + + // Cleanup S3 state after all stacks have been destroyed + return aws.CleanupState(mCtx) } func (r *kindRequest) createHost() (*utilKind.KindResults, error) { diff --git a/pkg/provider/aws/action/mac-pool/mac-pool.go b/pkg/provider/aws/action/mac-pool/mac-pool.go index c3a1a4054..1171f6091 100644 --- a/pkg/provider/aws/action/mac-pool/mac-pool.go +++ b/pkg/provider/aws/action/mac-pool/mac-pool.go @@ -49,7 +49,12 @@ func Destroy(mCtxArgs *mc.ContextArgs) error { if err := iam.Destroy(mCtx); err != nil { return err } - return serverless.Destroy(mCtx) + if err := serverless.Destroy(mCtx); err != nil { + return err + } + + // Cleanup S3 state after all stacks have been destroyed + return aws.CleanupState(mCtx) } // House keeper is the function executed serverless to check if is there any diff --git a/pkg/provider/aws/action/mac/mac.go b/pkg/provider/aws/action/mac/mac.go index b68ba55cf..c18d7a1e7 100644 --- a/pkg/provider/aws/action/mac/mac.go +++ b/pkg/provider/aws/action/mac/mac.go @@ -124,14 +124,19 @@ func Destroy(mCtxArgs *mc.ContextArgs, hostID string) error { }); err != nil { return err } - return aws.DestroyStack( + if err := aws.DestroyStack( mCtx, aws.DestroyStackRequest{ Stackname: mac.StackDedicatedHost, // TODO check if needed to add region for backedURL Region: *hi.Region, BackedURL: *hi.BackedURL, - }) + }); err != nil { + return err + } + + // Cleanup S3 state after all stacks have been destroyed + return aws.CleanupState(mCtx) } logging.Debug("nothing to be destroyed") return nil diff --git a/pkg/provider/aws/action/openshift-snc/openshift-snc.go b/pkg/provider/aws/action/openshift-snc/openshift-snc.go index ceba57125..4e5160c74 100644 --- a/pkg/provider/aws/action/openshift-snc/openshift-snc.go +++ b/pkg/provider/aws/action/openshift-snc/openshift-snc.go @@ -133,9 +133,13 @@ func Destroy(mCtxArgs *mc.ContextArgs) (err error) { } // Destroy spot orchestrated stack if spot.Exist(mCtx) { - return spot.Destroy(mCtx) + if err := spot.Destroy(mCtx); err != nil { + return err + } } - return nil + + // Cleanup S3 state after all stacks have been destroyed + return aws.CleanupState(mCtx) } func (r *openshiftSNCRequest) createCluster() (*OpenshiftSncResultsMetadata, error) { diff --git a/pkg/provider/aws/action/rhel-ai/rhelai.go b/pkg/provider/aws/action/rhel-ai/rhelai.go index 0baf9cbff..223199f32 100644 --- a/pkg/provider/aws/action/rhel-ai/rhelai.go +++ b/pkg/provider/aws/action/rhel-ai/rhelai.go @@ -123,9 +123,13 @@ func Destroy(mCtxArgs *mc.ContextArgs) error { return err } if spot.Exist(mCtx) { - return spot.Destroy(mCtx) + if err := spot.Destroy(mCtx); err != nil { + return err + } } - return nil + + // Cleanup S3 state after all stacks have been destroyed + return aws.CleanupState(mCtx) } func (r *rhelAIRequest) createMachine() error { diff --git a/pkg/provider/aws/action/rhel/rhel.go b/pkg/provider/aws/action/rhel/rhel.go index e28217fd6..5e0ec7fce 100644 --- a/pkg/provider/aws/action/rhel/rhel.go +++ b/pkg/provider/aws/action/rhel/rhel.go @@ -132,9 +132,13 @@ func Destroy(mCtxArgs *mc.ContextArgs) error { return err } if spot.Exist(mCtx) { - return spot.Destroy(mCtx) + if err := spot.Destroy(mCtx); err != nil { + return err + } } - return nil + + // Cleanup S3 state after all stacks have been destroyed + return aws.CleanupState(mCtx) } func (r *rhelRequest) createMachine() error { diff --git a/pkg/provider/aws/action/windows/windows.go b/pkg/provider/aws/action/windows/windows.go index 1ce14133d..8a243869e 100644 --- a/pkg/provider/aws/action/windows/windows.go +++ b/pkg/provider/aws/action/windows/windows.go @@ -181,9 +181,13 @@ func Destroy(mCtxArgs *mc.ContextArgs) (err error) { } } if spot.Exist(mCtx) { - return spot.Destroy(mCtx) + if err := spot.Destroy(mCtx); err != nil { + return err + } } - return nil + + // Cleanup S3 state after all stacks have been destroyed + return aws.CleanupState(mCtx) } func (r *windowsServerRequest) createMachine() error { diff --git a/pkg/provider/aws/aws.go b/pkg/provider/aws/aws.go index 21382ef73..f95d57aac 100644 --- a/pkg/provider/aws/aws.go +++ b/pkg/provider/aws/aws.go @@ -219,3 +219,27 @@ func parseS3BackedURL(mCtx *mc.Context) (*string, *string, error) { key := strings.TrimPrefix(u.Path, "/") return &u.Host, &key, nil } + +// CleanupState removes Pulumi state files from S3 backend after successful destroy +// This should be called AFTER all stacks for a project have been destroyed +func CleanupState(mCtx *mc.Context) error { + if !mCtx.IsCleanupState() { + return nil + } + + bucket, key, parseErr := parseS3BackedURL(mCtx) + if parseErr != nil { + logging.Warnf("Failed to parse S3 backend URL, skipping state cleanup: %v", parseErr) + return nil // Don't fail the operation + } + + logging.Infof("Cleaning up Pulumi state from s3://%s/%s", *bucket, *key) + if deleteErr := s3.Delete(bucket, key); deleteErr != nil { + logging.Warnf("Failed to cleanup S3 state: %v", deleteErr) + // Don't return error - resources are already destroyed + } else { + logging.Info("Successfully cleaned up Pulumi state from S3") + } + + return nil +}