Skip to content

Commit f42f14b

Browse files
haitham911autofix-ci[bot]aknysh
authored
Fix atmos terraform clean command for Terraform components in sub-directories (#1195)
* fix terraform clean for sub folders * fix relative path * add unit test to clean terraform files * [autofix.ci] apply automated fixes * fix linter errors * [autofix.ci] apply automated fixes * fix linter error * fix linter * fix linter error * update log * improve log * fix lint error * improve logs * refactor: update paths in terraform tests * refactor: remove redundant error check in terraform clean test * refactor: update paths in terraform tests and remove unused main.tf files * refactor: improve error logging for file existence check in terraform clean test * refactor: initialize PathManager and update PATH in TestCLITerraformClean * fix: improve logging for relative path errors and update name pattern in atmos.yaml * fix: enhance error handling and logging in terraform_clean and add tests for folder operations * test: add unit tests for CollectComponentsDirectoryObjects with various scenarios * [autofix.ci] apply automated fixes * fix: enhance root path validation for Windows and Unix in IsValidDataDir function * fix linter error * fix: replace error message with predefined ErrEmptyPath in CollectDirectoryObjects function --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Andriy Knysh <[email protected]>
1 parent 0264c72 commit f42f14b

File tree

7 files changed

+592
-33
lines changed

7 files changed

+592
-33
lines changed

internal/exec/terraform_clean.go

Lines changed: 78 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,31 @@ import (
88

99
"github.com/charmbracelet/huh"
1010
"github.com/charmbracelet/lipgloss"
11+
log "github.com/charmbracelet/log"
1112
"github.com/cloudposse/atmos/pkg/schema"
1213
"github.com/cloudposse/atmos/pkg/ui/theme"
1314
u "github.com/cloudposse/atmos/pkg/utils"
15+
"github.com/pkg/errors"
16+
)
17+
18+
var (
19+
ErrParseStacks = errors.New("could not parse stacks")
20+
ErrParseComponents = errors.New("could not parse components")
21+
ErrParseTerraformComponents = errors.New("could not parse Terraform components")
22+
ErrParseComponentsAttributes = errors.New("could not parse component attributes")
23+
ErrDescribeStack = errors.New("error describe stacks")
24+
ErrEmptyPath = errors.New("path cannot be empty")
25+
ErrPathNotExist = errors.New("path not exist")
26+
ErrFileStat = errors.New("error get file stat")
27+
ErrMatchPattern = errors.New("error matching pattern")
28+
ErrReadDir = errors.New("error reading directory")
29+
ErrFailedFoundStack = errors.New("failed to find stack folders")
30+
ErrCollectFiles = errors.New("failed to collect files")
31+
ErrEmptyEnvDir = errors.New("ENV TF_DATA_DIR is empty")
32+
ErrResolveEnvDir = errors.New("error resolving TF_DATA_DIR path")
33+
ErrRefusingToDeleteDir = errors.New("refusing to delete root directory")
34+
ErrRefusingToDelete = errors.New("refusing to delete directory containing")
35+
ErrRootPath = errors.New("root path cannot be empty")
1436
)
1537

1638
type ObjectInfo struct {
@@ -32,12 +54,12 @@ type Directory struct {
3254
func findFoldersNamesWithPrefix(root, prefix string, atmosConfig schema.AtmosConfiguration) ([]string, error) {
3355
var folderNames []string
3456
if root == "" {
35-
return nil, fmt.Errorf("root path cannot be empty")
57+
return nil, ErrRootPath
3658
}
3759
// First, read the directories at the root level (level 1)
3860
level1Dirs, err := os.ReadDir(root)
3961
if err != nil {
40-
return nil, fmt.Errorf("error reading root directory %s: %w", root, err)
62+
return nil, fmt.Errorf("%w path %s: error %v", ErrReadDir, root, err)
4163
}
4264

4365
for _, dir := range level1Dirs {
@@ -51,7 +73,7 @@ func findFoldersNamesWithPrefix(root, prefix string, atmosConfig schema.AtmosCon
5173
level2Path := filepath.Join(root, dir.Name())
5274
level2Dirs, err := os.ReadDir(level2Path)
5375
if err != nil {
54-
u.LogWarning(fmt.Sprintf("Error reading subdirectory %s: %v", level2Path, err))
76+
log.Debug("Error reading subdirectory", "directory", level2Path, "error", err)
5577
continue
5678
}
5779

@@ -68,24 +90,24 @@ func findFoldersNamesWithPrefix(root, prefix string, atmosConfig schema.AtmosCon
6890

6991
func CollectDirectoryObjects(basePath string, patterns []string) ([]Directory, error) {
7092
if basePath == "" {
71-
return nil, fmt.Errorf("path cannot be empty")
93+
return nil, ErrEmptyPath
7294
}
7395
if _, err := os.Stat(basePath); os.IsNotExist(err) {
74-
return nil, fmt.Errorf("path %s does not exist", basePath)
96+
return nil, fmt.Errorf("%w %s", ErrPathNotExist, basePath)
7597
}
7698
var folders []Directory
7799

78100
// Helper function to add file information if it exists
79101
addFileInfo := func(filePath string) (*ObjectInfo, error) {
80102
relativePath, err := filepath.Rel(basePath, filePath)
81103
if err != nil {
82-
return nil, fmt.Errorf("error determining relative path for %s: %v", filePath, err)
104+
return nil, fmt.Errorf("%w %s: %v", ErrRelPath, filePath, err)
83105
}
84106
info, err := os.Stat(filePath)
85107
if os.IsNotExist(err) {
86108
return nil, nil // Skip if the file doesn't exist
87109
} else if err != nil {
88-
return nil, fmt.Errorf("error stating file %s: %v", filePath, err)
110+
return nil, fmt.Errorf("%w,path %s error %v", ErrFileStat, filePath, err)
89111
}
90112

91113
return &ObjectInfo{
@@ -100,7 +122,7 @@ func CollectDirectoryObjects(basePath string, patterns []string) ([]Directory, e
100122
createFolder := func(folderPath string, folderName string) (*Directory, error) {
101123
relativePath, err := filepath.Rel(basePath, folderPath)
102124
if err != nil {
103-
return nil, fmt.Errorf("error determining relative path for folder %s: %v", folderPath, err)
125+
return nil, fmt.Errorf("%w %s: %v", ErrRelPath, folderPath, err)
104126
}
105127

106128
return &Directory{
@@ -116,7 +138,7 @@ func CollectDirectoryObjects(basePath string, patterns []string) ([]Directory, e
116138
for _, pat := range patterns {
117139
matchedFiles, err := filepath.Glob(filepath.Join(folderPath, pat))
118140
if err != nil {
119-
return fmt.Errorf("error matching pattern %s in folder %s: %v", pat, folderPath, err)
141+
return fmt.Errorf("%w %s in folder %s: %v", ErrMatchPattern, pat, folderPath, err)
120142
}
121143

122144
// Add matched files to folder
@@ -184,7 +206,7 @@ func getStackTerraformStateFolder(componentPath string, stack string, atmosConfi
184206
tfStateFolderPath := filepath.Join(componentPath, "terraform.tfstate.d")
185207
tfStateFolderNames, err := findFoldersNamesWithPrefix(tfStateFolderPath, stack, atmosConfig)
186208
if err != nil {
187-
return nil, fmt.Errorf("failed to find stack folders: %w", err)
209+
return nil, fmt.Errorf("%w : %v", ErrFailedFoundStack, err)
188210
}
189211
var stackTfStateFolders []Directory
190212
for _, folderName := range tfStateFolderNames {
@@ -195,7 +217,7 @@ func getStackTerraformStateFolder(componentPath string, stack string, atmosConfi
195217
}
196218
directories, err := CollectDirectoryObjects(tfStateFolderPath, []string{"*.tfstate", "*.tfstate.backup"})
197219
if err != nil {
198-
return nil, fmt.Errorf("failed to collect files in %s: %w", tfStateFolderPath, err)
220+
return nil, fmt.Errorf("%w in %s: %v", ErrCollectFiles, tfStateFolderPath, err)
199221
}
200222
for i := range directories {
201223
if directories[i].Files != nil {
@@ -286,7 +308,7 @@ func confirmDeletion(atmosConfig schema.AtmosConfiguration) (bool, error) {
286308
return false, err
287309
}
288310
if !confirm {
289-
u.LogWarning("Mission aborted.")
311+
log.Warn("Mission aborted.")
290312
return false, nil
291313
}
292314
return true, nil
@@ -297,29 +319,33 @@ func deleteFolders(folders []Directory, relativePath string, atmosConfig schema.
297319
var errors []error
298320
for _, folder := range folders {
299321
for _, file := range folder.Files {
300-
path := filepath.ToSlash(filepath.Join(relativePath, file.Name))
322+
fileRel, err := getRelativePath(atmosConfig.BasePath, file.FullPath)
323+
if err != nil {
324+
log.Debug("failed to get relative path", "path", file.FullPath, "error", err)
325+
fileRel = filepath.Join(relativePath, file.Name)
326+
}
301327
if file.IsDir {
302-
if err := DeletePathTerraform(file.FullPath, path+"/"); err != nil {
303-
errors = append(errors, fmt.Errorf("failed to delete %s: %w", path, err))
328+
if err := DeletePathTerraform(file.FullPath, fileRel+"/"); err != nil {
329+
errors = append(errors, fmt.Errorf("failed to delete %s: %w", fileRel, err))
304330
}
305331
} else {
306-
if err := DeletePathTerraform(file.FullPath, path); err != nil {
307-
errors = append(errors, fmt.Errorf("failed to delete %s: %w", path, err))
332+
if err := DeletePathTerraform(file.FullPath, fileRel); err != nil {
333+
errors = append(errors, fmt.Errorf("failed to delete %s: %w", fileRel, err))
308334
}
309335
}
310336
}
311337
}
312338
if len(errors) > 0 {
313339
for _, err := range errors {
314-
u.LogWarning(err.Error())
340+
log.Debug(err)
315341
}
316342
}
317343
// check if the folder is empty by using the os.ReadDir function
318344
for _, folder := range folders {
319345
entries, err := os.ReadDir(folder.FullPath)
320346
if err == nil && len(entries) == 0 {
321347
if err := os.Remove(folder.FullPath); err != nil {
322-
u.LogWarning(fmt.Sprintf("Error removing directory %s: %v", folder.FullPath, err))
348+
log.Debug("Error removing directory", "path", folder.FullPath, "error", err)
323349
}
324350
}
325351
}
@@ -332,15 +358,15 @@ func handleTFDataDir(componentPath string, relativePath string, atmosConfig sche
332358
return
333359
}
334360
if err := IsValidDataDir(tfDataDir); err != nil {
335-
u.LogWarning(err.Error())
361+
log.Debug("error validating TF_DATA_DIR", "error", err)
336362
return
337363
}
338364
if _, err := os.Stat(filepath.Join(componentPath, tfDataDir)); os.IsNotExist(err) {
339-
u.LogWarning(fmt.Sprintf("TF_DATA_DIR '%s' does not exist", tfDataDir))
365+
log.Debug("TF_DATA_DIR does not exist", "TF_DATA_DIR", tfDataDir, "error", err)
340366
return
341367
}
342368
if err := DeletePathTerraform(filepath.Join(componentPath, tfDataDir), filepath.Join(relativePath, tfDataDir)); err != nil {
343-
u.LogWarning(err.Error())
369+
log.Debug("error deleting TF_DATA_DIR", "TF_DATA_DIR", tfDataDir, "error", err)
344370
}
345371
}
346372

@@ -365,17 +391,25 @@ func initializeFilesToClear(info schema.ConfigAndStacksInfo, atmosConfig schema.
365391

366392
func IsValidDataDir(tfDataDir string) error {
367393
if tfDataDir == "" {
368-
return fmt.Errorf("ENV TF_DATA_DIR is empty")
394+
return ErrEmptyEnvDir
369395
}
370396
absTFDataDir, err := filepath.Abs(tfDataDir)
371397
if err != nil {
372-
return fmt.Errorf("error resolving TF_DATA_DIR path: %v", err)
398+
return fmt.Errorf("%w: %v", ErrResolveEnvDir, err)
373399
}
400+
401+
// Check for root path on both Unix and Windows systems
374402
if absTFDataDir == "/" || absTFDataDir == filepath.Clean("/") {
375-
return fmt.Errorf("refusing to delete root directory '/'")
403+
return fmt.Errorf("%w: %s", ErrRefusingToDeleteDir, absTFDataDir)
404+
}
405+
406+
// Windows-specific root path check (like C:\ or D:\)
407+
if len(absTFDataDir) == 3 && absTFDataDir[1:] == ":\\" {
408+
return fmt.Errorf("%w: %s", ErrRefusingToDeleteDir, absTFDataDir)
376409
}
410+
377411
if strings.Contains(absTFDataDir, "..") {
378-
return fmt.Errorf("refusing to delete directory containing '..'")
412+
return fmt.Errorf("%w: %s", ErrRefusingToDelete, "..")
379413
}
380414
return nil
381415
}
@@ -385,6 +419,7 @@ func handleCleanSubCommand(info schema.ConfigAndStacksInfo, componentPath string
385419
if info.SubCommand != "clean" {
386420
return nil
387421
}
422+
388423
cleanPath := componentPath
389424
if info.ComponentFromArg != "" && info.StackFromArg == "" {
390425
if info.Context.BaseComponent == "" {
@@ -406,17 +441,27 @@ func handleCleanSubCommand(info schema.ConfigAndStacksInfo, componentPath string
406441

407442
force := u.SliceContainsString(info.AdditionalArgsAndFlags, forceFlag)
408443
filesToClear := initializeFilesToClear(info, atmosConfig)
409-
folders, err := CollectDirectoryObjects(cleanPath, filesToClear)
444+
var FilterComponents []string
445+
if info.ComponentFromArg != "" {
446+
FilterComponents = append(FilterComponents, info.ComponentFromArg)
447+
}
448+
stacksMap, err := ExecuteDescribeStacks(
449+
atmosConfig, info.StackFromArg,
450+
FilterComponents,
451+
nil, nil, false, false, false, false, nil)
410452
if err != nil {
411-
u.LogTrace(fmt.Errorf("error collecting folders and files: %v", err).Error())
453+
return fmt.Errorf("%w: %v", ErrDescribeStack, err)
454+
}
455+
allComponentsRelativePaths := getAllStacksComponentsPaths(stacksMap)
456+
folders, err := CollectComponentsDirectoryObjects(atmosConfig.TerraformDirAbsolutePath, allComponentsRelativePaths, filesToClear)
457+
if err != nil {
458+
log.Debug("error collecting folders and files", "error", err)
412459
return err
413460
}
414-
415461
if info.Component != "" && info.Stack != "" {
416462
stackFolders, err := getStackTerraformStateFolder(cleanPath, info.Stack, atmosConfig)
417463
if err != nil {
418-
errMsg := fmt.Errorf("error getting stack terraform state folders: %v", err)
419-
u.LogTrace(errMsg.Error())
464+
log.Debug("error getting stack terraform state folders", "error", err)
420465
}
421466
if stackFolders != nil {
422467
folders = append(folders, stackFolders...)
@@ -427,11 +472,11 @@ func handleCleanSubCommand(info schema.ConfigAndStacksInfo, componentPath string
427472
var tfDataDirFolders []Directory
428473
if tfDataDir != "" {
429474
if err := IsValidDataDir(tfDataDir); err != nil {
430-
u.LogTrace(err.Error())
475+
log.Debug("error validating TF_DATA_DIR", "error", err)
431476
} else {
432477
tfDataDirFolders, err = CollectDirectoryObjects(cleanPath, []string{tfDataDir})
433478
if err != nil {
434-
u.LogTrace(fmt.Errorf("error collecting folder of ENV TF_DATA_DIR: %v", err).Error())
479+
log.Debug("error collecting folder of ENV TF_DATA_DIR", "error", err)
435480
}
436481
}
437482
}

0 commit comments

Comments
 (0)