From 382de94bdddabdbe42366b99512c22d0e08fb6ee Mon Sep 17 00:00:00 2001 From: Haitham Rageh Date: Thu, 5 Dec 2024 03:50:56 +0200 Subject: [PATCH] add `terraform clean --everything` and `terraform clean --force` to delete `terraform.tfstate.d` folder (#727) * add terraform clean delete terraform.tfstate.d folder if the `--everything` flag is provided. * add comment * Refactor processArgsAndFlags function to handle additional options with "--" * Refactor path_utils.go and add new functions for finding folders with a prefix and deleting files and folders recursively * add --everything provide additional options for the 'atmos terraform clean' command * Refactor path_utils.go to improve folder search and deletion functionality * add comment to function * Refactor processArgsAndFlags function to handle options and components separately * Refactor help.go to improve 'atmos terraform clean' command documentation * use filepath ensure cross-platform compatibility * findFoldersNamesWithPrefix function to clarify search levels * Refactor ExecuteTerraform function to use descriptive variable names for files to clear * Refactor ExecuteTerraform function to use descriptive variable names for files and clarify boolean flags * Refactor processArgsAndFlags function to handle additional arguments and flags correctly * Refactor help.go to clarify Terraform state file deletion and add force option * Refactor deleteFilesAndFoldersRecursive function to improve error handling and logging * rename file bubble_msg.go * use log package for deletion logging * Refactor deleteFilesAndFoldersRecursive function to improve error handling and logging * Refactor bubble_msg.go to improve confirmation dialog state handling * Refactor Confirm function to handle confirmation dialog state and model type * Refactor bubble_msg.go to improve confirmation dialog state handling and add navigation instructions * command with --everything or --force flags * remove comment * Refactor error handling in ExecuteTerraform function * Refactor error handling in findFoldersNamesWithPrefix function * Refactor help.go to improve 'atmos terraform clean' command documentation * Refactor error handling in findFoldersNamesWithPrefix and ExecuteTerraform functions * Refactor confirm delete msg * log waring with no confirm msg * Refactor error handling in findFoldersNamesWithPrefix function * Refactor clean command to improve Terraform state file deletion * Refactor clean command to improve Terraform state file deletion * confirm msg color * add log clean all components * check file exist before delete * use filepath pkg * modify log * use DeletePathTerraform utility * Refactor clean subcommand to handle terraform component cleanup * Refactor clean subcommand to use filepath package for path manipulation * Refactor clean empty dir * remove print line for debug * Refactor clean subcommand to handle relative path correctly * Refactor clean subcommand to handle relative path correctly * Refactor help message for 'atmos terraform clean' command * Refactor clean subcommand * Refactor clean subcommand to use u.PrintMessage instead of u.LogInfo for displaying messages * Refactor clean TF_DATA_DIR with everything * Refactor handleTFDataDir to handle relative path correctly * Refactor error messages for invalid TF_DATA_DIR and missing stack * Update internal/exec/terraform_clean.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Refactor handleCleanSubCommand to improve deletion messaging and streamline TF_DATA_DIR handling * Calculate total objects to delete by counting files in folders * Update dependencies in go.mod and go.sum to include new packages * update dependencies: upgrade lipgloss to v1.0.0 and x/ansi to v0.4.2; * chore: update charmbracelet/x dependencies in go.mod * fix: remove duplicate help message for clean operation in help.go * feat: enhance terraform clean command with --everything and --force options; improve argument validation * Update website/docs/cli/commands/terraform/terraform-clean.mdx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update website/docs/cli/commands/terraform/terraform-clean.mdx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update website/docs/cli/commands/terraform/usage.mdx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update website/docs/cli/commands/terraform/usage.mdx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update website/docs/cli/commands/terraform/usage.mdx * Update website/docs/cli/commands/terraform/usage.mdx * Refactor error handling in handleCleanSubCommand for TF_DATA_DIR validation * fix: correct typo in documentation * fix html doc * fix: validate that the base path exists in CollectDirectoryObjects * fix: pass cliConfig to findFoldersNamesWithPrefix and getStackTerraformStateFolder functions * fix: enhance DeletePathTerraform to handle symbolic links and improve error messages * fix: improve error handling in deleteFolders function for better deletion feedback * fix: streamline error handling in deleteFolders function for improved feedback --------- Co-authored-by: Andriy Knysh Co-authored-by: Erik Osterman (CEO @ Cloud Posse) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- go.mod | 5 + go.sum | 10 + internal/exec/help.go | 20 +- internal/exec/path_utils.go | 12 +- internal/exec/terraform.go | 93 ++-- internal/exec/terraform_clean.go | 483 ++++++++++++++++++ internal/exec/utils.go | 23 +- .../commands/terraform/terraform-clean.mdx | 12 +- website/docs/cli/commands/terraform/usage.mdx | 19 +- .../Screengrabs/atmos-terraform--help.html | 1 + .../atmos-terraform-clean--help.html | 2 +- 11 files changed, 607 insertions(+), 73 deletions(-) create mode 100644 internal/exec/terraform_clean.go diff --git a/go.mod b/go.mod index 2dd1a7ddc..927f0c009 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/charmbracelet/bubbles v0.20.0 github.com/charmbracelet/bubbletea v1.2.4 github.com/charmbracelet/glamour v0.8.0 + github.com/charmbracelet/huh v0.6.0 github.com/charmbracelet/lipgloss v1.0.0 github.com/elewis787/boa v0.1.2 github.com/fatih/color v1.18.0 @@ -92,10 +93,12 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 // indirect + github.com/catppuccin/go v0.2.0 // indirect github.com/cenkalti/backoff/v3 v3.2.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chainguard-dev/git-urls v1.0.2 // indirect github.com/charmbracelet/x/ansi v0.4.5 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect @@ -111,6 +114,7 @@ require ( github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/docker/libkv v0.2.2-0.20180912205406-458977154600 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect @@ -176,6 +180,7 @@ require ( github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/locker v1.0.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect diff --git a/go.sum b/go.sum index ebe252106..cafcf5969 100644 --- a/go.sum +++ b/go.sum @@ -240,6 +240,8 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0/go.mod h1:spvB9eLJH9dutlbPSRmHvSXXHOwGRyeXh1jVdquA2G8= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= @@ -379,6 +381,8 @@ github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0 github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= +github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= +github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= @@ -399,12 +403,16 @@ github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= +github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8= +github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= @@ -987,6 +995,8 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= diff --git a/internal/exec/help.go b/internal/exec/help.go index e28c8a98c..60267c509 100644 --- a/internal/exec/help.go +++ b/internal/exec/help.go @@ -33,8 +33,13 @@ func processHelp( u.PrintMessage(" - 'atmos terraform apply' and 'atmos terraform deploy' commands commands support '--planfile' flag to specify the path " + "to a planfile. The '--planfile' flag should be used instead of the planfile argument in the native 'terraform apply ' command") u.PrintMessage(" - 'atmos terraform clean' command deletes the '.terraform' folder, '.terraform.lock.hcl' lock file, " + - "and the previously generated 'planfile', 'varfile' and 'backend.tf.json' file for the specified component and stack. " + - "Use --skip-lock-file flag to skip deleting the lock file.") + "and the previously generated 'planfile', 'varfile', and 'backend.tf.json' file for the specified component and stack. " + + "Use the --everything flag to also delete the Terraform state files and directories for the component. " + + "Note: State files store the local state of your infrastructure and should be handled with care, if not using a remote backend.\n\n" + + "Additional flags:\n" + + " --force Forcefully delete Terraform state files and directories without interaction\n" + + " --skip-lock-file Skip deleting the '.terraform.lock.hcl' file\n\n" + + "If no component or stack is specified, the clean operation will apply globally to all components.") u.PrintMessage(" - 'atmos terraform workspace' command first runs 'terraform init -reconfigure', then 'terraform workspace select', " + "and if the workspace was not created before, it then runs 'terraform workspace new'") u.PrintMessage(" - 'atmos terraform import' command searches for 'region' in the variables for the specified component and stack, " + @@ -71,10 +76,17 @@ func processHelp( " - '.terraform.lock.hcl' file\n" + " - generated varfile for the component in the stack\n" + " - generated planfile for the component in the stack\n" + - " - generated 'backend.tf.json' file\n\n" + + " - generated 'backend.tf.json' file\n" + + " - 'terraform.tfstate.d' folder (if '--everything' flag is used)\n\n" + "Usage: atmos terraform clean -s \n\n" + - "Use '--skip-lock-file' flag to skip deleting the lock file.\n\n" + + "Use '--everything' flag to also delete the Terraform state files and and directories with confirm message.\n\n" + + "Use --force to forcefully delete Terraform state files and directories for the component.\n\n" + + "- If no component is specified, the command will apply to all components and stacks.\n" + + "- If no stack is specified, the command will apply to all stacks for the specified component.\n" + + "Use '--skip-lock-file' flag to skip deleting the '.terraform.lock.hcl' file.\n\n" + + "If no component or stack is specified, the clean operation will apply globally to all components.\n\n" + "For more details refer to https://atmos.tools/cli/commands/terraform/clean\n") + } else if componentType == "terraform" && command == "deploy" { u.PrintMessage("\n'atmos terraform deploy' command executes 'terraform apply -auto-approve' on an Atmos component in an Atmos stack.\n\n" + "Usage: atmos terraform deploy -s \n\n" + diff --git a/internal/exec/path_utils.go b/internal/exec/path_utils.go index 731e2d125..c29660f60 100644 --- a/internal/exec/path_utils.go +++ b/internal/exec/path_utils.go @@ -2,14 +2,14 @@ package exec import ( "fmt" - "path" + "path/filepath" "github.com/cloudposse/atmos/pkg/schema" ) // constructTerraformComponentWorkingDir constructs the working dir for a terraform component in a stack func constructTerraformComponentWorkingDir(cliConfig schema.CliConfiguration, info schema.ConfigAndStacksInfo) string { - return path.Join( + return filepath.Join( cliConfig.BasePath, cliConfig.Components.Terraform.BasePath, info.ComponentFolderPrefix, @@ -43,7 +43,7 @@ func constructTerraformComponentVarfileName(info schema.ConfigAndStacksInfo) str // constructTerraformComponentVarfilePath constructs the varfile path for a terraform component in a stack func constructTerraformComponentVarfilePath(Config schema.CliConfiguration, info schema.ConfigAndStacksInfo) string { - return path.Join( + return filepath.Join( constructTerraformComponentWorkingDir(Config, info), constructTerraformComponentVarfileName(info), ) @@ -51,7 +51,7 @@ func constructTerraformComponentVarfilePath(Config schema.CliConfiguration, info // constructTerraformComponentPlanfilePath constructs the planfile path for a terraform component in a stack func constructTerraformComponentPlanfilePath(cliConfig schema.CliConfiguration, info schema.ConfigAndStacksInfo) string { - return path.Join( + return filepath.Join( constructTerraformComponentWorkingDir(cliConfig, info), constructTerraformComponentPlanfileName(info), ) @@ -59,7 +59,7 @@ func constructTerraformComponentPlanfilePath(cliConfig schema.CliConfiguration, // constructHelmfileComponentWorkingDir constructs the working dir for a helmfile component in a stack func constructHelmfileComponentWorkingDir(cliConfig schema.CliConfiguration, info schema.ConfigAndStacksInfo) string { - return path.Join( + return filepath.Join( cliConfig.BasePath, cliConfig.Components.Helmfile.BasePath, info.ComponentFolderPrefix, @@ -80,7 +80,7 @@ func constructHelmfileComponentVarfileName(info schema.ConfigAndStacksInfo) stri // constructHelmfileComponentVarfilePath constructs the varfile path for a helmfile component in a stack func constructHelmfileComponentVarfilePath(cliConfig schema.CliConfiguration, info schema.ConfigAndStacksInfo) string { - return path.Join( + return filepath.Join( constructHelmfileComponentWorkingDir(cliConfig, info), constructHelmfileComponentVarfileName(info), ) diff --git a/internal/exec/terraform.go b/internal/exec/terraform.go index ecd6a6a3f..0c0021ff8 100644 --- a/internal/exec/terraform.go +++ b/internal/exec/terraform.go @@ -4,7 +4,7 @@ import ( "fmt" "os" osexec "os/exec" - "path" + "path/filepath" "strings" "github.com/pkg/errors" @@ -21,6 +21,8 @@ const ( outFlag = "-out" varFileFlag = "-var-file" skipTerraformLockFileFlag = "--skip-lock-file" + everythingFlag = "--everything" + forceFlag = "--force" ) // ExecuteTerraformCmd parses the provided arguments and flags and executes terraform commands @@ -29,7 +31,6 @@ func ExecuteTerraformCmd(cmd *cobra.Command, args []string, additionalArgsAndFla if err != nil { return err } - return ExecuteTerraform(info) } @@ -61,13 +62,29 @@ func ExecuteTerraform(info schema.ConfigAndStacksInfo) error { return nil } - info, err = ProcessStacks(cliConfig, info, true, true) - if err != nil { - return err + shouldProcessStacks := true + shouldCheckStack := true + // Skip stack processing when cleaning with --everything or --force flags to allow + // cleaning without requiring stack configuration + if info.SubCommand == "clean" && + (u.SliceContainsString(info.AdditionalArgsAndFlags, everythingFlag) || + u.SliceContainsString(info.AdditionalArgsAndFlags, forceFlag)) { + if info.ComponentFromArg == "" { + shouldProcessStacks = false + } + + shouldCheckStack = info.Stack != "" + } - if len(info.Stack) < 1 { - return errors.New("stack must be specified") + if shouldProcessStacks { + info, err = ProcessStacks(cliConfig, info, shouldCheckStack, true) + if err != nil { + return err + } + if len(info.Stack) < 1 && shouldCheckStack { + return errors.New("stack must be specified when not using --everything or --force flags") + } } if !info.ComponentIsEnabled { @@ -79,73 +96,33 @@ func ExecuteTerraform(info schema.ConfigAndStacksInfo) error { if err != nil { return err } - // Check if the component (or base component) exists as Terraform component - componentPath := path.Join(cliConfig.TerraformDirAbsolutePath, info.ComponentFolderPrefix, info.FinalComponent) + componentPath := filepath.Join(cliConfig.TerraformDirAbsolutePath, info.ComponentFolderPrefix, info.FinalComponent) componentPathExists, err := u.IsDirectory(componentPath) if err != nil || !componentPathExists { return fmt.Errorf("'%s' points to the Terraform component '%s', but it does not exist in '%s'", info.ComponentFromArg, info.FinalComponent, - path.Join(cliConfig.Components.Terraform.BasePath, info.ComponentFolderPrefix), + filepath.Join(cliConfig.Components.Terraform.BasePath, info.ComponentFolderPrefix), ) } // Check if the component is allowed to be provisioned (`metadata.type` attribute is not set to `abstract`) if (info.SubCommand == "plan" || info.SubCommand == "apply" || info.SubCommand == "deploy" || info.SubCommand == "workspace") && info.ComponentIsAbstract { return fmt.Errorf("abstract component '%s' cannot be provisioned since it's explicitly prohibited from being deployed "+ - "by 'metadata.type: abstract' attribute", path.Join(info.ComponentFolderPrefix, info.Component)) + "by 'metadata.type: abstract' attribute", filepath.Join(info.ComponentFolderPrefix, info.Component)) } - varFile := constructTerraformComponentVarfileName(info) - planFile := constructTerraformComponentPlanfileName(info) - if info.SubCommand == "clean" { - u.LogInfo(cliConfig, "Deleting '.terraform' folder") - err = os.RemoveAll(path.Join(componentPath, ".terraform")) + err := handleCleanSubCommand(info, componentPath, cliConfig) if err != nil { - u.LogWarning(cliConfig, err.Error()) - } - - if !u.SliceContainsString(info.AdditionalArgsAndFlags, skipTerraformLockFileFlag) { - u.LogInfo(cliConfig, "Deleting '.terraform.lock.hcl' file") - _ = os.Remove(path.Join(componentPath, ".terraform.lock.hcl")) - } - - u.LogInfo(cliConfig, fmt.Sprintf("Deleting terraform varfile: %s", varFile)) - _ = os.Remove(path.Join(componentPath, varFile)) - - u.LogInfo(cliConfig, fmt.Sprintf("Deleting terraform planfile: %s", planFile)) - _ = os.Remove(path.Join(componentPath, planFile)) - - // If `auto_generate_backend_file` is `true` (we are auto-generating backend files), remove `backend.tf.json` - if cliConfig.Components.Terraform.AutoGenerateBackendFile { - u.LogInfo(cliConfig, "Deleting 'backend.tf.json' file") - _ = os.Remove(path.Join(componentPath, "backend.tf.json")) - } - - tfDataDir := os.Getenv("TF_DATA_DIR") - if len(tfDataDir) > 0 && tfDataDir != "." && tfDataDir != "/" && tfDataDir != "./" { - u.PrintMessage(fmt.Sprintf("Found ENV var TF_DATA_DIR=%s", tfDataDir)) - var userAnswer string - u.PrintMessage(fmt.Sprintf("Do you want to delete the folder '%s'? (only 'yes' will be accepted to approve)\n", tfDataDir)) - fmt.Print("Enter a value: ") - count, err := fmt.Scanln(&userAnswer) - if count > 0 && err != nil { - return err - } - if userAnswer == "yes" { - u.PrintMessage(fmt.Sprintf("Deleting folder '%s'\n", tfDataDir)) - err = os.RemoveAll(path.Join(componentPath, tfDataDir)) - if err != nil { - u.LogWarning(cliConfig, err.Error()) - } - } + u.LogTrace(cliConfig, fmt.Errorf("error cleaning the terraform component: %v", err).Error()) + return err } - return nil } - + varFile := constructTerraformComponentVarfileName(info) + planFile := constructTerraformComponentPlanfileName(info) // Print component variables and write to file // Don't process variables when executing `terraform workspace` commands if info.SubCommand != "workspace" { @@ -209,7 +186,7 @@ func ExecuteTerraform(info schema.ConfigAndStacksInfo) error { // Auto-generate backend file if cliConfig.Components.Terraform.AutoGenerateBackendFile { - backendFileName := path.Join(workingDir, "backend.tf.json") + backendFileName := filepath.Join(workingDir, "backend.tf.json") u.LogDebug(cliConfig, "\nWriting the backend config to file:") u.LogDebug(cliConfig, backendFileName) @@ -229,7 +206,7 @@ func ExecuteTerraform(info schema.ConfigAndStacksInfo) error { // Generate `providers_override.tf.json` file if the `providers` section is configured if len(info.ComponentProvidersSection) > 0 { - providerOverrideFileName := path.Join(workingDir, "providers_override.tf.json") + providerOverrideFileName := filepath.Join(workingDir, "providers_override.tf.json") u.LogDebug(cliConfig, "\nWriting the provider overrides to file:") u.LogDebug(cliConfig, providerOverrideFileName) @@ -341,7 +318,7 @@ func ExecuteTerraform(info schema.ConfigAndStacksInfo) error { u.LogDebug(cliConfig, "Stack: "+info.StackFromArg) } else { u.LogDebug(cliConfig, "Stack: "+info.StackFromArg) - u.LogDebug(cliConfig, "Stack path: "+path.Join(cliConfig.BasePath, cliConfig.Stacks.BasePath, info.Stack)) + u.LogDebug(cliConfig, "Stack path: "+filepath.Join(cliConfig.BasePath, cliConfig.Stacks.BasePath, info.Stack)) } u.LogDebug(cliConfig, fmt.Sprintf("Working dir: %s", workingDir)) diff --git a/internal/exec/terraform_clean.go b/internal/exec/terraform_clean.go new file mode 100644 index 000000000..f77cbc98e --- /dev/null +++ b/internal/exec/terraform_clean.go @@ -0,0 +1,483 @@ +package exec + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" + "github.com/cloudposse/atmos/pkg/schema" + u "github.com/cloudposse/atmos/pkg/utils" +) + +type ObjectInfo struct { + FullPath string + RelativePath string + Name string + IsDir bool +} + +type Directory struct { + Name string + FullPath string + RelativePath string + Files []ObjectInfo +} + +// findFoldersNamesWithPrefix finds the names of folders that match the given prefix under the specified root path. +// The search is performed at the root level (level 1) and one level deeper (level 2). +func findFoldersNamesWithPrefix(root, prefix string, cliConfig schema.CliConfiguration) ([]string, error) { + var folderNames []string + if root == "" { + return nil, fmt.Errorf("root path cannot be empty") + } + // First, read the directories at the root level (level 1) + level1Dirs, err := os.ReadDir(root) + if err != nil { + return nil, fmt.Errorf("error reading root directory %s: %w", root, err) + } + + for _, dir := range level1Dirs { + if dir.IsDir() { + // If the directory at level 1 matches the prefix, add it + if prefix == "" || strings.HasPrefix(dir.Name(), prefix) { + folderNames = append(folderNames, dir.Name()) + } + + // Now, explore one level deeper (level 2) + level2Path := filepath.Join(root, dir.Name()) + level2Dirs, err := os.ReadDir(level2Path) + if err != nil { + u.LogWarning(cliConfig, fmt.Sprintf("Error reading subdirectory %s: %v", level2Path, err)) + continue + } + + for _, subDir := range level2Dirs { + if subDir.IsDir() && (prefix == "" || strings.HasPrefix(subDir.Name(), prefix)) { + folderNames = append(folderNames, filepath.Join(dir.Name(), subDir.Name())) + } + } + } + } + + return folderNames, nil +} + +func CollectDirectoryObjects(basePath string, patterns []string) ([]Directory, error) { + if basePath == "" { + return nil, fmt.Errorf("path cannot be empty") + } + if _, err := os.Stat(basePath); os.IsNotExist(err) { + return nil, fmt.Errorf("path %s does not exist", basePath) + } + var folders []Directory + + // Helper function to add file information if it exists + addFileInfo := func(filePath string) (*ObjectInfo, error) { + relativePath, err := filepath.Rel(basePath, filePath) + if err != nil { + return nil, fmt.Errorf("error determining relative path for %s: %v", filePath, err) + } + info, err := os.Stat(filePath) + if os.IsNotExist(err) { + return nil, nil // Skip if the file doesn't exist + } else if err != nil { + return nil, fmt.Errorf("error stating file %s: %v", filePath, err) + } + + return &ObjectInfo{ + FullPath: filePath, + RelativePath: relativePath, + Name: filepath.Base(filePath), + IsDir: info.IsDir(), + }, nil + } + + // Function to create a folder entry with its files + createFolder := func(folderPath string, folderName string) (*Directory, error) { + relativePath, err := filepath.Rel(basePath, folderPath) + if err != nil { + return nil, fmt.Errorf("error determining relative path for folder %s: %v", folderPath, err) + } + + return &Directory{ + Name: folderName, + FullPath: folderPath, + RelativePath: relativePath, + Files: []ObjectInfo{}, + }, nil + } + + // Function to collect files for a given folder path + collectFilesInFolder := func(folder *Directory, folderPath string) error { + for _, pat := range patterns { + matchedFiles, err := filepath.Glob(filepath.Join(folderPath, pat)) + if err != nil { + return fmt.Errorf("error matching pattern %s in folder %s: %v", pat, folderPath, err) + } + + // Add matched files to folder + for _, matchedFile := range matchedFiles { + fileInfo, err := addFileInfo(matchedFile) + if err != nil { + return err + } + if fileInfo != nil { + folder.Files = append(folder.Files, *fileInfo) + } + } + } + return nil + } + + // Collect files for the base path itself + baseFolder, err := createFolder(basePath, filepath.Base(basePath)) + if err != nil { + return nil, err + } + err = collectFilesInFolder(baseFolder, basePath) + if err != nil { + return nil, err + } + if len(baseFolder.Files) > 0 { + folders = append(folders, *baseFolder) + } + + // Now, search for folders and their files from immediate subdirectories + entries, err := os.ReadDir(basePath) + if err != nil { + return nil, fmt.Errorf("error reading the base path %s: %v", basePath, err) + } + + for _, entry := range entries { + // Only proceed if the entry is a directory + if entry.IsDir() { + subDirPath := filepath.Join(basePath, entry.Name()) + + // Create the folder entry + folder, err := createFolder(subDirPath, entry.Name()) + if err != nil { + return nil, err + } + + // Collect files in the subdirectory + err = collectFilesInFolder(folder, subDirPath) + if err != nil { + return nil, err + } + + // Add folder to the list only if it contains files + if len(folder.Files) > 0 { + folders = append(folders, *folder) + } + } + } + + return folders, nil +} + +// get stack terraform state files +func getStackTerraformStateFolder(componentPath string, stack string, cliConfig schema.CliConfiguration) ([]Directory, error) { + tfStateFolderPath := filepath.Join(componentPath, "terraform.tfstate.d") + tfStateFolderNames, err := findFoldersNamesWithPrefix(tfStateFolderPath, stack, cliConfig) + if err != nil { + return nil, fmt.Errorf("failed to find stack folders: %w", err) + } + var stackTfStateFolders []Directory + for _, folderName := range tfStateFolderNames { + tfStateFolderPath := filepath.Join(componentPath, "terraform.tfstate.d", folderName) + // Check if exists + if _, err := os.Stat(tfStateFolderPath); os.IsNotExist(err) { + continue + } + directories, err := CollectDirectoryObjects(tfStateFolderPath, []string{"*.tfstate", "*.tfstate.backup"}) + if err != nil { + return nil, fmt.Errorf("failed to collect files in %s: %w", tfStateFolderPath, err) + } + for i := range directories { + if directories[i].Files != nil { + for j := range directories[i].Files { + directories[i].Files[j].Name = folderName + "/" + directories[i].Files[j].Name + + } + + } + } + stackTfStateFolders = append(stackTfStateFolders, directories...) + } + + return stackTfStateFolders, nil +} + +// getRelativePath computes the relative path from basePath to componentPath. +func getRelativePath(basePath, componentPath string) (string, error) { + absBasePath, err := filepath.Abs(basePath) + if err != nil { + return "", err + } + absComponentPath, err := filepath.Abs(componentPath) + if err != nil { + return "", err + } + + relPath, err := filepath.Rel(absBasePath, absComponentPath) + if err != nil { + return "", err + } + + return filepath.Base(absBasePath) + "/" + relPath, nil +} +func confirmDeleteTerraformLocal(message string) (confirm bool, err error) { + confirm = false + t := huh.ThemeCharm() + cream := lipgloss.AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"} + purple := lipgloss.AdaptiveColor{Light: "#5B00FF", Dark: "#5B00FF"} + t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(cream).Background(purple) + t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(purple) + t.Blurred.Title = t.Blurred.Title.Foreground(purple) + confirmPrompt := huh.NewConfirm(). + Title(message). + Affirmative("Yes!"). + Negative("No."). + Value(&confirm).WithTheme(t) + if err := confirmPrompt.Run(); err != nil { + if err == huh.ErrUserAborted { + return confirm, fmt.Errorf("Mission aborted") + } + return confirm, err + } + + return confirm, nil +} + +// DeletePathTerraform deletes the specified file or folder. with a checkmark or xmark +func DeletePathTerraform(fullPath string, objectName string) error { + fileInfo, err := os.Lstat(fullPath) + if os.IsNotExist(err) { + xMark := lipgloss.NewStyle().Foreground(lipgloss.Color("9")).SetString("x") + fmt.Printf("%s Cannot delete %s: path does not exist", xMark, objectName) + fmt.Println() + return err + } + if fileInfo.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("refusing to delete symbolic link: %s", objectName) + } + // Proceed with deletion + err = os.RemoveAll(fullPath) + if err != nil { + xMark := lipgloss.NewStyle().Foreground(lipgloss.Color("9")).SetString("x") + fmt.Printf("%s Error deleting %s", xMark, objectName) + fmt.Println() + return err + } + checkMark := lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓") + fmt.Printf("%s Deleted %s", checkMark, objectName) + fmt.Println() + return nil +} + +// confirmDeletion prompts the user for confirmation before deletion. +func confirmDeletion(cliConfig schema.CliConfiguration) (bool, error) { + message := "Are you sure?" + confirm, err := confirmDeleteTerraformLocal(message) + if err != nil { + return false, err + } + if !confirm { + u.LogWarning(cliConfig, "Mission aborted.") + return false, nil + } + return true, nil +} + +// deleteFolders handles the deletion of the specified folders and files. +func deleteFolders(folders []Directory, relativePath string, cliConfig schema.CliConfiguration) { + var errors []error + for _, folder := range folders { + for _, file := range folder.Files { + path := filepath.ToSlash(filepath.Join(relativePath, file.Name)) + if file.IsDir { + if err := DeletePathTerraform(file.FullPath, path+"/"); err != nil { + errors = append(errors, fmt.Errorf("failed to delete %s: %w", path, err)) + } + } else { + if err := DeletePathTerraform(file.FullPath, path); err != nil { + errors = append(errors, fmt.Errorf("failed to delete %s: %w", path, err)) + } + } + } + } + if len(errors) > 0 { + for _, err := range errors { + u.LogWarning(cliConfig, err.Error()) + } + } + // check if the folder is empty by using the os.ReadDir function + for _, folder := range folders { + entries, err := os.ReadDir(folder.FullPath) + if err == nil && len(entries) == 0 { + if err := os.Remove(folder.FullPath); err != nil { + u.LogWarning(cliConfig, fmt.Sprintf("Error removing directory %s: %v", folder.FullPath, err)) + } + } + } + +} + +// handleTFDataDir handles the deletion of the TF_DATA_DIR if specified. +func handleTFDataDir(componentPath string, relativePath string, cliConfig schema.CliConfiguration) { + + tfDataDir := os.Getenv("TF_DATA_DIR") + if tfDataDir == "" { + return + } + if err := IsValidDataDir(tfDataDir); err != nil { + u.LogWarning(cliConfig, err.Error()) + return + } + if _, err := os.Stat(filepath.Join(componentPath, tfDataDir)); os.IsNotExist(err) { + u.LogWarning(cliConfig, fmt.Sprintf("TF_DATA_DIR '%s' does not exist", tfDataDir)) + return + } + if err := DeletePathTerraform(filepath.Join(componentPath, tfDataDir), filepath.Join(relativePath, tfDataDir)); err != nil { + u.LogWarning(cliConfig, err.Error()) + } + +} +func initializeFilesToClear(info schema.ConfigAndStacksInfo, cliConfig schema.CliConfiguration, everything bool) []string { + if everything && info.Stack == "" { + return []string{".terraform", ".terraform.lock.hcl", "*.tfvar.json", "terraform.tfstate.d"} + } + varFile := constructTerraformComponentVarfileName(info) + planFile := constructTerraformComponentPlanfileName(info) + files := []string{".terraform", varFile, planFile} + + if !u.SliceContainsString(info.AdditionalArgsAndFlags, skipTerraformLockFileFlag) { + files = append(files, ".terraform.lock.hcl") + } + + if cliConfig.Components.Terraform.AutoGenerateBackendFile { + files = append(files, "backend.tf.json") + } + + return files +} +func IsValidDataDir(tfDataDir string) error { + if tfDataDir == "" { + return fmt.Errorf("ENV TF_DATA_DIR is empty") + } + absTFDataDir, err := filepath.Abs(tfDataDir) + if err != nil { + return fmt.Errorf("error resolving TF_DATA_DIR path: %v", err) + } + if absTFDataDir == "/" || absTFDataDir == filepath.Clean("/") { + return fmt.Errorf("refusing to delete root directory '/'") + } + if strings.Contains(absTFDataDir, "..") { + return fmt.Errorf("refusing to delete directory containing '..'") + } + return nil +} + +// handleCleanSubCommand handles the 'clean' subcommand logic. +func handleCleanSubCommand(info schema.ConfigAndStacksInfo, componentPath string, cliConfig schema.CliConfiguration) error { + if info.SubCommand != "clean" { + return nil + } + cleanPath := componentPath + if info.ComponentFromArg != "" && info.StackFromArg == "" { + if info.Context.BaseComponent == "" { + return fmt.Errorf("could not find the component '%s'", info.ComponentFromArg) + } + cleanPath = filepath.Join(componentPath, info.Context.BaseComponent) + } + + relativePath, err := getRelativePath(cliConfig.BasePath, componentPath) + if err != nil { + return err + } + if info.Context.BaseComponent != "" { + // remove the base component from the relative path + relativePath = strings.Replace(relativePath, info.Context.BaseComponent, "", 1) + // remove the leading slash + relativePath = strings.TrimPrefix(relativePath, "/") + } + + force := u.SliceContainsString(info.AdditionalArgsAndFlags, forceFlag) + everything := u.SliceContainsString(info.AdditionalArgsAndFlags, everythingFlag) + filesToClear := initializeFilesToClear(info, cliConfig, everything) + folders, err := CollectDirectoryObjects(cleanPath, filesToClear) + if err != nil { + u.LogTrace(cliConfig, fmt.Errorf("error collecting folders and files: %v", err).Error()) + return err + } + + if info.Component != "" && info.Stack != "" { + stackFolders, err := getStackTerraformStateFolder(cleanPath, info.Stack, cliConfig) + if err != nil { + errMsg := fmt.Errorf("error getting stack terraform state folders: %v", err) + u.LogTrace(cliConfig, errMsg.Error()) + } + if stackFolders != nil { + folders = append(folders, stackFolders...) + } + } + tfDataDir := os.Getenv("TF_DATA_DIR") + + var tfDataDirFolders []Directory + if tfDataDir != "" { + if err := IsValidDataDir(tfDataDir); err != nil { + u.LogTrace(cliConfig, err.Error()) + } else { + tfDataDirFolders, err = CollectDirectoryObjects(cleanPath, []string{tfDataDir}) + if err != nil { + u.LogTrace(cliConfig, fmt.Errorf("error collecting folder of ENV TF_DATA_DIR: %v", err).Error()) + } + } + } + objectCount := 0 + for _, folder := range folders { + objectCount += len(folder.Files) + } + total := objectCount + len(tfDataDirFolders) + + if total == 0 { + u.PrintMessage("Nothing to delete") + return nil + } + + if total > 0 { + if !force { + if len(tfDataDirFolders) > 0 { + u.PrintMessage(fmt.Sprintf("Found ENV var TF_DATA_DIR=%s", tfDataDir)) + u.PrintMessage(fmt.Sprintf("Do you want to delete the folder '%s'? ", tfDataDir)) + } + var message string + if everything && info.ComponentFromArg == "" { + message = fmt.Sprintf("This will delete %v local terraform state files affecting all components", total) + } else if info.Component != "" && info.Stack != "" { + message = fmt.Sprintf("This will delete %v local terraform state files for component '%s' in stack '%s'", total, info.Component, info.Stack) + } else if info.ComponentFromArg != "" { + message = fmt.Sprintf("This will delete %v local terraform state files for component '%s'", total, info.ComponentFromArg) + } else { + message = "This will delete selected terraform state files" + } + u.PrintMessage(message) + println() + if confirm, err := confirmDeletion(cliConfig); err != nil || !confirm { + return err + } + } + + deleteFolders(folders, relativePath, cliConfig) + if len(tfDataDirFolders) > 0 { + tfDataDirFolder := tfDataDirFolders[0] + handleTFDataDir(tfDataDirFolder.FullPath, relativePath, cliConfig) + } + + } + + return nil +} diff --git a/internal/exec/utils.go b/internal/exec/utils.go index e319ff6e7..6dea676bb 100644 --- a/internal/exec/utils.go +++ b/internal/exec/utils.go @@ -393,6 +393,13 @@ func ProcessStacks( } } + if foundStackCount == 0 { + // Allow proceeding without error if checkStack is false (e.g., for operations that don't require a stack) + if !checkStack { + return configAndStacksInfo, nil + } + } + if foundStackCount == 0 && configAndStacksInfo.ComponentIsEnabled { cliConfigYaml := "" @@ -1012,7 +1019,6 @@ func processArgsAndFlags(componentType string, inputArgsAndFlags []string) (sche // Handle terraform two-words commands // https://developer.hashicorp.com/terraform/cli/commands if componentType == "terraform" { - // Handle the custom legacy command `terraform write varfile` (NOTE: use `terraform generate varfile` instead) if additionalArgsAndFlags[0] == "write" && additionalArgsAndFlags[1] == "varfile" { info.SubCommand = "write" @@ -1049,7 +1055,20 @@ func processArgsAndFlags(componentType string, inputArgsAndFlags []string) (sche } } else { info.SubCommand = additionalArgsAndFlags[0] - info.ComponentFromArg = additionalArgsAndFlags[1] + if len(additionalArgsAndFlags) > 1 { + secondArg := additionalArgsAndFlags[1] + if len(secondArg) == 0 { + return info, fmt.Errorf("invalid empty argument provided") + } + if strings.HasPrefix(secondArg, "--") { + if len(secondArg) <= 2 { + return info, fmt.Errorf("invalid option format: %s", secondArg) + } + info.AdditionalArgsAndFlags = []string{secondArg} + } else { + info.ComponentFromArg = secondArg + } + } if len(additionalArgsAndFlags) > 2 { info.AdditionalArgsAndFlags = additionalArgsAndFlags[2:] } diff --git a/website/docs/cli/commands/terraform/terraform-clean.mdx b/website/docs/cli/commands/terraform/terraform-clean.mdx index 5b7937bea..3ddc4b341 100644 --- a/website/docs/cli/commands/terraform/terraform-clean.mdx +++ b/website/docs/cli/commands/terraform/terraform-clean.mdx @@ -20,7 +20,12 @@ component in a stack. Execute the `terraform clean` command like this: ```shell -atmos terraform clean -s [--skip-lock-file] +atmos terraform clean -s [--skip-lock-file] [--everything] [--force] + +:::warning +The `--everything` flag will delete all Terraform-related files including state files. The `--force` flag will bypass confirmation prompts. +Use these flags with extreme caution as they can lead to irreversible data loss. +::: ``` :::tip @@ -30,6 +35,11 @@ Run `atmos terraform clean --help` to see all the available options ## Examples ```shell +# Delete all Terraform-related files for all components (with confirmation) +atmos terraform clean --everything + +# Force delete all Terraform-related files for all components (no confirmation) +atmos terraform clean --everything --force atmos terraform clean top-level-component1 -s tenant1-ue2-dev atmos terraform clean infra/vpc -s tenant1-ue2-staging atmos terraform clean infra/vpc -s tenant1-ue2-staging --skip-lock-file diff --git a/website/docs/cli/commands/terraform/usage.mdx b/website/docs/cli/commands/terraform/usage.mdx index 94709912b..ce60dbee6 100644 --- a/website/docs/cli/commands/terraform/usage.mdx +++ b/website/docs/cli/commands/terraform/usage.mdx @@ -58,7 +58,13 @@ HCL-based domain-specific language and its interpreter. Atmos works with [OpenTo planfile - `atmos terraform clean` command deletes the `.terraform` folder, `.terraform.lock.hcl` lock file, and the previously generated `planfile` - and `varfile` for the specified component and stack. Use the `--skip-lock-file` flag to skip deleting the `.terraform.lock.hcl` file. + and `varfile` for the specified component and stack. Use the `--skip-lock-file` flag to skip deleting the `.terraform.lock.hcl` file. + Use the `--everything` flag to delete all the local Terraform state files and directories (including `terraform.tfstate.d`) for all components and stacks. + Use the `--force` flag to bypass the safety confirmation prompt and force the deletion (use with caution). + + :::warning + The `--everything` flag performs destructive operations that can lead to permanent state loss. Always ensure you have remote state configured in your components before proceeding. + ::: - `atmos terraform workspace` command first runs `terraform init -reconfigure`, then `terraform workspace select`, and if the workspace was not created before, it then runs `terraform workspace new` @@ -106,6 +112,17 @@ atmos terraform destroy test/test-component-override -s tenant1-ue2-dev --redire atmos terraform init test/test-component-override-3 -s tenant1-ue2-dev +# Clean all components (with confirmation) +atmos terraform clean --everything + +# Clean a specific component +atmos terraform clean vpc --everything + +# Clean a specific component in a stack +atmos terraform clean vpc --stack dev --everything + +# Clean without confirmation prompt +atmos terraform clean --everything --force atmos terraform clean test/test-component-override-3 -s tenant1-ue2-dev atmos terraform workspace test/test-component-override-3 -s tenant1-ue2-dev diff --git a/website/src/components/Screengrabs/atmos-terraform--help.html b/website/src/components/Screengrabs/atmos-terraform--help.html index 84c3512f0..5142abd4e 100644 --- a/website/src/components/Screengrabs/atmos-terraform--help.html +++ b/website/src/components/Screengrabs/atmos-terraform--help.html @@ -21,6 +21,7 @@ - 'atmos terraform apply' and 'atmos terraform deploy' commands support '--from-plan' flag. If the flag is specified, the commands will use the planfile previously generated by 'atmos terraform plan' command instead of generating a new planfile - 'atmos terraform apply' and 'atmos terraform deploy' commands commands support '--planfile' flag to specify the path to a planfile. The '--planfile' flag should be used instead of the planfile argument in the native 'terraform apply <planfile>' command - 'atmos terraform clean' command deletes the '.terraform' folder, '.terraform.lock.hcl' lock file, and the previously generated 'planfile', 'varfile' and 'backend.tf.json' file for the specified component and stack. Use --skip-lock-file flag to skip deleting the lock file. + - 'atmos terraform clean' command supports '--everything' flag to delete all the Terraform state files and directories for all components and stacks . Use --force flag to skip the confirmation prompt. - 'atmos terraform workspace' command first runs 'terraform init -reconfigure', then 'terraform workspace select', and if the workspace was not created before, it then runs 'terraform workspace new' - 'atmos terraform import' command searches for 'region' in the variables for the specified component and stack, and if it finds it, sets 'AWS_REGION=<region>' ENV var before executing the command - 'atmos terraform generate backend' command generates a backend config file for an 'atmos' component in a stack diff --git a/website/src/components/Screengrabs/atmos-terraform-clean--help.html b/website/src/components/Screengrabs/atmos-terraform-clean--help.html index 84fc1c840..7d5710b43 100644 --- a/website/src/components/Screengrabs/atmos-terraform-clean--help.html +++ b/website/src/components/Screengrabs/atmos-terraform-clean--help.html @@ -17,7 +17,7 @@ - generated 'backend.tf.json' file Usage: atmos terraform clean <component> -s <stack> <flags> - +Use '--everything' flag to delete all the files and folders mentioned above. '--force' to delete the files without confirmation. Use '--skip-lock-file' flag to skip deleting the lock file. For more details refer to https://atmos.tools/cli/commands/terraform/clean