github.com/diggerhq/digger/libs@v0.0.0-20240604170430-9d61cdf01cc5/digger_config/terragrunt/atlantis/generate.go (about)

     1  package atlantis
     2  
     3  import (
     4  	"context"
     5  	"regexp"
     6  	"sort"
     7  
     8  	"github.com/gruntwork-io/terragrunt/cli/commands/terraform"
     9  	"github.com/gruntwork-io/terragrunt/config"
    10  	"github.com/gruntwork-io/terragrunt/options"
    11  	"golang.org/x/sync/errgroup"
    12  	"golang.org/x/sync/semaphore"
    13  
    14  	"github.com/hashicorp/go-getter"
    15  	log "github.com/sirupsen/logrus"
    16  	"golang.org/x/sync/singleflight"
    17  
    18  	"os"
    19  	"path/filepath"
    20  	"strings"
    21  	"sync"
    22  )
    23  
    24  // Parse env vars into a map
    25  func getEnvs() map[string]string {
    26  	envs := os.Environ()
    27  	m := make(map[string]string)
    28  
    29  	for _, env := range envs {
    30  		results := strings.Split(env, "=")
    31  		m[results[0]] = results[1]
    32  	}
    33  
    34  	return m
    35  }
    36  
    37  // Terragrunt imports can be relative or absolute
    38  // This makes relative paths absolute
    39  func makePathAbsolute(gitRoot string, path string, parentPath string) string {
    40  	if strings.HasPrefix(path, filepath.ToSlash(gitRoot)) {
    41  		return path
    42  	}
    43  
    44  	parentDir := filepath.Dir(parentPath)
    45  	return filepath.Join(parentDir, path)
    46  }
    47  
    48  var requestGroup singleflight.Group
    49  
    50  // Set up a cache for the getDependencies function
    51  type getDependenciesOutput struct {
    52  	dependencies []string
    53  	err          error
    54  }
    55  
    56  type GetDependenciesCache struct {
    57  	mtx  sync.RWMutex
    58  	data map[string]getDependenciesOutput
    59  }
    60  
    61  func newGetDependenciesCache() *GetDependenciesCache {
    62  	return &GetDependenciesCache{data: map[string]getDependenciesOutput{}}
    63  }
    64  
    65  func (m *GetDependenciesCache) set(k string, v getDependenciesOutput) {
    66  	m.mtx.Lock()
    67  	defer m.mtx.Unlock()
    68  	m.data[k] = v
    69  }
    70  
    71  func (m *GetDependenciesCache) get(k string) (getDependenciesOutput, bool) {
    72  	m.mtx.RLock()
    73  	defer m.mtx.RUnlock()
    74  	v, ok := m.data[k]
    75  	return v, ok
    76  }
    77  
    78  var getDependenciesCache = newGetDependenciesCache()
    79  
    80  func uniqueStrings(str []string) []string {
    81  	keys := make(map[string]bool)
    82  	list := []string{}
    83  	for _, entry := range str {
    84  		if _, value := keys[entry]; !value {
    85  			keys[entry] = true
    86  			list = append(list, entry)
    87  		}
    88  	}
    89  	return list
    90  }
    91  
    92  func lookupProjectHcl(m map[string][]string, value string) (key string) {
    93  	for k, values := range m {
    94  		for _, val := range values {
    95  			if val == value {
    96  				key = k
    97  				return
    98  			}
    99  		}
   100  	}
   101  	return key
   102  }
   103  
   104  // sliceUnion takes two slices of strings and produces a union of them, containing only unique values
   105  func sliceUnion(a, b []string) []string {
   106  	m := make(map[string]bool)
   107  
   108  	for _, item := range a {
   109  		m[item] = true
   110  	}
   111  
   112  	for _, item := range b {
   113  		if _, ok := m[item]; !ok {
   114  			a = append(a, item)
   115  		}
   116  	}
   117  	return a
   118  }
   119  
   120  // Parses the terragrunt digger_config at `path` to find all modules it depends on
   121  func getDependencies(ignoreParentTerragrunt bool, ignoreDependencyBlocks bool, gitRoot string, cascadeDependencies bool, path string, terragruntOptions *options.TerragruntOptions) ([]string, error) {
   122  	res, err, _ := requestGroup.Do(path, func() (interface{}, error) {
   123  		// Check if this path has already been computed
   124  		cachedResult, ok := getDependenciesCache.get(path)
   125  		if ok {
   126  			return cachedResult.dependencies, cachedResult.err
   127  		}
   128  
   129  		// parse the module path to find what it includes, as well as its potential to be a parent
   130  		// return nils to indicate we should skip this project
   131  		isParent, includes, err := parseModule(path, terragruntOptions)
   132  		if err != nil {
   133  			getDependenciesCache.set(path, getDependenciesOutput{nil, err})
   134  			return nil, err
   135  		}
   136  		if isParent && ignoreParentTerragrunt {
   137  			getDependenciesCache.set(path, getDependenciesOutput{nil, nil})
   138  			return nil, nil
   139  		}
   140  
   141  		dependencies := []string{}
   142  		if len(includes) > 0 {
   143  			for _, includeDep := range includes {
   144  				getDependenciesCache.set(includeDep.Path, getDependenciesOutput{nil, err})
   145  				dependencies = append(dependencies, includeDep.Path)
   146  			}
   147  		}
   148  
   149  		// Parse the HCL file
   150  		decodeTypes := []config.PartialDecodeSectionType{
   151  			config.DependencyBlock,
   152  			config.DependenciesBlock,
   153  			config.TerraformBlock,
   154  		}
   155  		parsedConfig, err := config.PartialParseConfigFile(path, terragruntOptions, nil, decodeTypes)
   156  		if err != nil {
   157  			getDependenciesCache.set(path, getDependenciesOutput{nil, err})
   158  			return nil, err
   159  		}
   160  
   161  		// Parse out locals
   162  		locals, err := parseLocals(path, terragruntOptions, nil)
   163  		if err != nil {
   164  			getDependenciesCache.set(path, getDependenciesOutput{nil, err})
   165  			return nil, err
   166  		}
   167  
   168  		// Get deps from locals
   169  		if locals.ExtraAtlantisDependencies != nil {
   170  			dependencies = sliceUnion(dependencies, locals.ExtraAtlantisDependencies)
   171  		}
   172  
   173  		// Get deps from `dependencies` and `dependency` blocks
   174  		if parsedConfig.Dependencies != nil && !ignoreDependencyBlocks {
   175  			for _, parsedPaths := range parsedConfig.Dependencies.Paths {
   176  				dependencies = append(dependencies, filepath.Join(parsedPaths, "terragrunt.hcl"))
   177  			}
   178  		}
   179  
   180  		// Get deps from the `Source` field of the `Terraform` block
   181  		if parsedConfig.Terraform != nil && parsedConfig.Terraform.Source != nil {
   182  			source := parsedConfig.Terraform.Source
   183  
   184  			// Use `go-getter` to normalize the source paths
   185  			parsedSource, err := getter.Detect(*source, filepath.Dir(path), getter.Detectors)
   186  			if err != nil {
   187  				return nil, err
   188  			}
   189  
   190  			// Check if the path begins with a drive letter, denoting Windows
   191  			isWindowsPath, err := regexp.MatchString(`^[A-Z]:`, parsedSource)
   192  			if err != nil {
   193  				return nil, err
   194  			}
   195  
   196  			// If the normalized source begins with `file://`, or matched the Windows drive letter check, it is a local path
   197  			if strings.HasPrefix(parsedSource, "file://") || isWindowsPath {
   198  				// Remove the prefix so we have a valid filesystem path
   199  				parsedSource = strings.TrimPrefix(parsedSource, "file://")
   200  
   201  				dependencies = append(dependencies, filepath.Join(parsedSource, "*.tf*"))
   202  
   203  				ls, err := parseTerraformLocalModuleSource(parsedSource)
   204  				if err != nil {
   205  					return nil, err
   206  				}
   207  				sort.Strings(ls)
   208  
   209  				dependencies = append(dependencies, ls...)
   210  			}
   211  		}
   212  
   213  		// Get deps from `extra_arguments` fields of the `Terraform` block
   214  		if parsedConfig.Terraform != nil && parsedConfig.Terraform.ExtraArgs != nil {
   215  			extraArgs := parsedConfig.Terraform.ExtraArgs
   216  			for _, arg := range extraArgs {
   217  				if arg.RequiredVarFiles != nil {
   218  					dependencies = append(dependencies, *arg.RequiredVarFiles...)
   219  				}
   220  				if arg.OptionalVarFiles != nil {
   221  					dependencies = append(dependencies, *arg.OptionalVarFiles...)
   222  				}
   223  				if arg.Arguments != nil {
   224  					for _, cliFlag := range *arg.Arguments {
   225  						if strings.HasPrefix(cliFlag, "-var-file=") {
   226  							dependencies = append(dependencies, strings.TrimPrefix(cliFlag, "-var-file="))
   227  						}
   228  					}
   229  				}
   230  			}
   231  		}
   232  
   233  		// Filter out and dependencies that are the empty string
   234  		nonEmptyDeps := []string{}
   235  		for _, dep := range dependencies {
   236  			if dep != "" {
   237  				childDepAbsPath := dep
   238  				if !filepath.IsAbs(childDepAbsPath) {
   239  					childDepAbsPath = makePathAbsolute(gitRoot, dep, path)
   240  				}
   241  				childDepAbsPath = filepath.ToSlash(childDepAbsPath)
   242  				nonEmptyDeps = append(nonEmptyDeps, childDepAbsPath)
   243  			}
   244  		}
   245  
   246  		// Recurse to find dependencies of all dependencies
   247  		cascadedDeps := []string{}
   248  		for _, dep := range nonEmptyDeps {
   249  			cascadedDeps = append(cascadedDeps, dep)
   250  
   251  			// The "cascading" feature is protected by a flag
   252  			if !cascadeDependencies {
   253  				continue
   254  			}
   255  
   256  			depPath := dep
   257  			terrOpts, _ := options.NewTerragruntOptionsWithConfigPath(depPath)
   258  			terrOpts.OriginalTerragruntConfigPath = terragruntOptions.OriginalTerragruntConfigPath
   259  			childDeps, err := getDependencies(ignoreParentTerragrunt, ignoreDependencyBlocks, gitRoot, cascadeDependencies, depPath, terrOpts)
   260  			if err != nil {
   261  				continue
   262  			}
   263  
   264  			for _, childDep := range childDeps {
   265  				// If `childDep` is a relative path, it will be relative to `childDep`, as it is from the nested
   266  				// `getDependencies` call on the top level module's dependencies. So here we update any relative
   267  				// path to be from the top level module instead.
   268  				childDepAbsPath := childDep
   269  				if !filepath.IsAbs(childDep) {
   270  					childDepAbsPath, err = filepath.Abs(filepath.Join(depPath, "..", childDep))
   271  					if err != nil {
   272  						getDependenciesCache.set(path, getDependenciesOutput{nil, err})
   273  						return nil, err
   274  					}
   275  				}
   276  				childDepAbsPath = filepath.ToSlash(childDepAbsPath)
   277  
   278  				// Ensure we are not adding a duplicate dependency
   279  				alreadyExists := false
   280  				for _, dep := range cascadedDeps {
   281  					if dep == childDepAbsPath {
   282  						alreadyExists = true
   283  						break
   284  					}
   285  				}
   286  				if !alreadyExists {
   287  					cascadedDeps = append(cascadedDeps, childDepAbsPath)
   288  				}
   289  			}
   290  		}
   291  
   292  		if filepath.Base(path) == "terragrunt.hcl" {
   293  			dir := filepath.Dir(path)
   294  
   295  			ls, err := parseTerraformLocalModuleSource(dir)
   296  			if err != nil {
   297  				return nil, err
   298  			}
   299  			sort.Strings(ls)
   300  
   301  			cascadedDeps = append(cascadedDeps, ls...)
   302  		}
   303  
   304  		getDependenciesCache.set(path, getDependenciesOutput{cascadedDeps, err})
   305  		return cascadedDeps, nil
   306  	})
   307  
   308  	if res != nil {
   309  		return res.([]string), err
   310  	} else {
   311  		return nil, err
   312  	}
   313  }
   314  
   315  // Creates an AtlantisProject for a directory
   316  func createProject(ignoreParentTerragrunt bool, ignoreDependencyBlocks bool, gitRoot string, cascadeDependencies bool, defaultWorkflow string, defaultApplyRequirements []string, autoPlan bool, defaultTerraformVersion string, createProjectName bool, createWorkspace bool, sourcePath string) (*AtlantisProject, []string, error) {
   317  	options, err := options.NewTerragruntOptionsWithConfigPath(sourcePath)
   318  
   319  	var potentialProjectDependencies []string
   320  	if err != nil {
   321  		return nil, potentialProjectDependencies, err
   322  	}
   323  	options.OriginalTerragruntConfigPath = sourcePath
   324  	options.RunTerragrunt = terraform.Run
   325  	options.Env = getEnvs()
   326  
   327  	dependencies, err := getDependencies(ignoreParentTerragrunt, ignoreDependencyBlocks, gitRoot, cascadeDependencies, sourcePath, options)
   328  	if err != nil {
   329  		return nil, potentialProjectDependencies, err
   330  	}
   331  
   332  	// dependencies being nil is a sign from `getDependencies` that this project should be skipped
   333  	if dependencies == nil {
   334  		return nil, potentialProjectDependencies, nil
   335  	}
   336  
   337  	absoluteSourceDir := filepath.Dir(sourcePath) + string(filepath.Separator)
   338  
   339  	locals, err := parseLocals(sourcePath, options, nil)
   340  	if err != nil {
   341  		return nil, potentialProjectDependencies, err
   342  	}
   343  
   344  	// If `atlantis_skip` is true on the module, then do not produce a project for it
   345  	if locals.Skip != nil && *locals.Skip {
   346  		return nil, potentialProjectDependencies, nil
   347  	}
   348  
   349  	// All dependencies depend on their own .hcl file, and any tf files in their directory
   350  	relativeDependencies := []string{
   351  		"*.hcl",
   352  		"*.tf*",
   353  	}
   354  
   355  	// Add other dependencies based on their relative paths. We always want to output with Unix path separators
   356  	for _, dependencyPath := range dependencies {
   357  		absolutePath := dependencyPath
   358  		if !filepath.IsAbs(absolutePath) {
   359  			absolutePath = makePathAbsolute(gitRoot, dependencyPath, sourcePath)
   360  		}
   361  		potentialProjectDependencies = append(potentialProjectDependencies, projectNameFromDir(filepath.Dir(strings.TrimPrefix(absolutePath, gitRoot))))
   362  
   363  		relativePath, err := filepath.Rel(absoluteSourceDir, absolutePath)
   364  		if err != nil {
   365  			return nil, potentialProjectDependencies, err
   366  		}
   367  
   368  		relativeDependencies = append(relativeDependencies, filepath.ToSlash(relativePath))
   369  	}
   370  
   371  	// Clean up the relative path to the format Atlantis expects
   372  	relativeSourceDir := strings.TrimPrefix(absoluteSourceDir, gitRoot)
   373  	relativeSourceDir = strings.TrimSuffix(relativeSourceDir, string(filepath.Separator))
   374  	if relativeSourceDir == "" {
   375  		relativeSourceDir = "."
   376  	}
   377  
   378  	workflow := defaultWorkflow
   379  	if locals.AtlantisWorkflow != "" {
   380  		workflow = locals.AtlantisWorkflow
   381  	}
   382  
   383  	applyRequirements := &defaultApplyRequirements
   384  	if len(defaultApplyRequirements) == 0 {
   385  		applyRequirements = nil
   386  	}
   387  	if locals.ApplyRequirements != nil {
   388  		applyRequirements = &locals.ApplyRequirements
   389  	}
   390  
   391  	resolvedAutoPlan := autoPlan
   392  	if locals.AutoPlan != nil {
   393  		resolvedAutoPlan = *locals.AutoPlan
   394  	}
   395  
   396  	terraformVersion := defaultTerraformVersion
   397  	if locals.TerraformVersion != "" {
   398  		terraformVersion = locals.TerraformVersion
   399  	}
   400  
   401  	project := &AtlantisProject{
   402  		Dir:               filepath.ToSlash(relativeSourceDir),
   403  		Workflow:          workflow,
   404  		TerraformVersion:  terraformVersion,
   405  		ApplyRequirements: applyRequirements,
   406  		Autoplan: AutoplanConfig{
   407  			Enabled:      resolvedAutoPlan,
   408  			WhenModified: uniqueStrings(relativeDependencies),
   409  		},
   410  	}
   411  
   412  	projectName := projectNameFromDir(project.Dir)
   413  
   414  	if createProjectName {
   415  		project.Name = projectName
   416  	}
   417  
   418  	if createWorkspace {
   419  		project.Workspace = projectName
   420  	}
   421  
   422  	return project, potentialProjectDependencies, nil
   423  }
   424  
   425  func projectNameFromDir(projectDir string) string {
   426  	// Terraform Cloud limits the workspace names to be less than 90 characters
   427  	// with letters, numbers, -, and _
   428  	// https://www.terraform.io/docs/cloud/workspaces/naming.html
   429  	// It is not clear from documentation whether the normal workspaces have those limitations
   430  	// However a workspace 97 chars long has been working perfectly.
   431  	// We are going to use the same name for both workspace & project name as it is unique.
   432  	regex := regexp.MustCompile(`[^a-zA-Z0-9_-]+`)
   433  	projectName := regex.ReplaceAllString(projectDir, "_")
   434  	return projectName
   435  }
   436  
   437  func createHclProject(defaultWorkflow string, defaultApplyRequirements []string, autoplan bool, useProjectMarkers bool, defaultTerraformVersion string, ignoreParentTerragrunt bool, ignoreDependencyBlocks bool, gitRoot string, cascadeDependencies bool, createProjectName bool, createWorkspace bool, sourcePaths []string, workingDir string, projectHcl string) (*AtlantisProject, error) {
   438  	var projectHclDependencies []string
   439  	var childDependencies []string
   440  	workflow := defaultWorkflow
   441  	applyRequirements := &defaultApplyRequirements
   442  	resolvedAutoPlan := autoplan
   443  	terraformVersion := defaultTerraformVersion
   444  
   445  	projectHclFile := filepath.Join(workingDir, projectHcl)
   446  	projectHclOptions, err := options.NewTerragruntOptionsWithConfigPath(workingDir)
   447  	if err != nil {
   448  		return nil, err
   449  	}
   450  	projectHclOptions.RunTerragrunt = terraform.Run
   451  	projectHclOptions.Env = getEnvs()
   452  
   453  	locals, err := parseLocals(projectHclFile, projectHclOptions, nil)
   454  	if err != nil {
   455  		return nil, err
   456  	}
   457  
   458  	// If `atlantis_skip` is true on the module, then do not produce a project for it
   459  	if locals.Skip != nil && *locals.Skip {
   460  		return nil, nil
   461  	}
   462  
   463  	// if project markers are enabled, check if locals are set
   464  	markedProject := false
   465  	if locals.markedProject != nil {
   466  		markedProject = *locals.markedProject
   467  	}
   468  	if useProjectMarkers && !markedProject {
   469  		return nil, nil
   470  	}
   471  
   472  	if locals.ExtraAtlantisDependencies != nil {
   473  		for _, dep := range locals.ExtraAtlantisDependencies {
   474  			relDep, err := filepath.Rel(workingDir, dep)
   475  			if err != nil {
   476  				return nil, err
   477  			}
   478  			projectHclDependencies = append(projectHclDependencies, filepath.ToSlash(relDep))
   479  		}
   480  	}
   481  
   482  	if locals.AtlantisWorkflow != "" {
   483  		workflow = locals.AtlantisWorkflow
   484  	}
   485  
   486  	if len(defaultApplyRequirements) == 0 {
   487  		applyRequirements = nil
   488  	}
   489  	if locals.ApplyRequirements != nil {
   490  		applyRequirements = &locals.ApplyRequirements
   491  	}
   492  
   493  	if locals.AutoPlan != nil {
   494  		resolvedAutoPlan = *locals.AutoPlan
   495  	}
   496  
   497  	if locals.TerraformVersion != "" {
   498  		terraformVersion = locals.TerraformVersion
   499  	}
   500  
   501  	// build dependencies for terragrunt childs in directories below project hcl file
   502  	for _, sourcePath := range sourcePaths {
   503  		options, err := options.NewTerragruntOptionsWithConfigPath(sourcePath)
   504  		if err != nil {
   505  			return nil, err
   506  		}
   507  		options.RunTerragrunt = terraform.Run
   508  		options.Env = getEnvs()
   509  
   510  		dependencies, err := getDependencies(ignoreParentTerragrunt, ignoreDependencyBlocks, gitRoot, cascadeDependencies, sourcePath, options)
   511  		if err != nil {
   512  			return nil, err
   513  		}
   514  		// dependencies being nil is a sign from `getDependencies` that this project should be skipped
   515  		if dependencies == nil {
   516  			return nil, nil
   517  		}
   518  
   519  		// All dependencies depend on their own .hcl file, and any tf files in their directory
   520  		relativeDependencies := []string{
   521  			"*.hcl",
   522  			"*.tf*",
   523  			"**/*.hcl",
   524  			"**/*.tf*",
   525  		}
   526  
   527  		// Add other dependencies based on their relative paths. We always want to output with Unix path separators
   528  		for _, dependencyPath := range dependencies {
   529  			absolutePath := dependencyPath
   530  			if !filepath.IsAbs(absolutePath) {
   531  				absolutePath = makePathAbsolute(gitRoot, dependencyPath, sourcePath)
   532  			}
   533  
   534  			relativePath, err := filepath.Rel(workingDir, absolutePath)
   535  			if err != nil {
   536  				return nil, err
   537  			}
   538  
   539  			if !strings.Contains(absolutePath, filepath.ToSlash(workingDir)) {
   540  				relativeDependencies = append(relativeDependencies, filepath.ToSlash(relativePath))
   541  			}
   542  		}
   543  
   544  		childDependencies = append(childDependencies, relativeDependencies...)
   545  	}
   546  	dir, err := filepath.Rel(gitRoot, workingDir)
   547  	if err != nil {
   548  		return nil, err
   549  	}
   550  
   551  	project := &AtlantisProject{
   552  		Dir:               filepath.ToSlash(dir),
   553  		Workflow:          workflow,
   554  		TerraformVersion:  terraformVersion,
   555  		ApplyRequirements: applyRequirements,
   556  		Autoplan: AutoplanConfig{
   557  			Enabled:      resolvedAutoPlan,
   558  			WhenModified: uniqueStrings(append(childDependencies, projectHclDependencies...)),
   559  		},
   560  	}
   561  
   562  	// Terraform Cloud limits the workspace names to be less than 90 characters
   563  	// with letters, numbers, -, and _
   564  	// https://www.terraform.io/docs/cloud/workspaces/naming.html
   565  	// It is not clear from documentation whether the normal workspaces have those limitations
   566  	// However a workspace 97 chars long has been working perfectly.
   567  	// We are going to use the same name for both workspace & project name as it is unique.
   568  	regex := regexp.MustCompile(`[^a-zA-Z0-9_-]+`)
   569  	projectName := regex.ReplaceAllString(project.Dir, "_")
   570  
   571  	if createProjectName {
   572  		project.Name = projectName
   573  	}
   574  
   575  	if createWorkspace {
   576  		project.Workspace = projectName
   577  	}
   578  
   579  	return project, nil
   580  }
   581  
   582  // Finds the absolute paths of all terragrunt.hcl files
   583  func getAllTerragruntFiles(filterPath string, projectHclFiles []string, path string) ([]string, error) {
   584  	options, err := options.NewTerragruntOptionsWithConfigPath(path)
   585  	if err != nil {
   586  		return nil, err
   587  	}
   588  
   589  	// If filterPath is provided, override workingPath instead of gitRoot
   590  	// We do this here because we want to keep the relative path structure of Terragrunt files
   591  	// to root and just ignore the ConfigFiles
   592  	workingPaths := []string{path}
   593  
   594  	// filters are not working (yet) if using project hcl files (which are kind of filters by themselves)
   595  	if filterPath != "" && len(projectHclFiles) == 0 {
   596  		// get all matching folders
   597  		workingPaths, err = filepath.Glob(filterPath)
   598  		if err != nil {
   599  			return nil, err
   600  		}
   601  	}
   602  
   603  	uniqueConfigFilePaths := make(map[string]bool)
   604  	orderedConfigFilePaths := []string{}
   605  	for _, workingPath := range workingPaths {
   606  		paths, err := config.FindConfigFilesInPath(workingPath, options)
   607  		if err != nil {
   608  			return nil, err
   609  		}
   610  		for _, p := range paths {
   611  			// if path not yet seen, insert once
   612  			if !uniqueConfigFilePaths[p] {
   613  				orderedConfigFilePaths = append(orderedConfigFilePaths, p)
   614  				uniqueConfigFilePaths[p] = true
   615  			}
   616  		}
   617  	}
   618  
   619  	uniqueConfigFileAbsPaths := []string{}
   620  	for _, uniquePath := range orderedConfigFilePaths {
   621  		uniqueAbsPath, err := filepath.Abs(uniquePath)
   622  		if err != nil {
   623  			return nil, err
   624  		}
   625  		uniqueConfigFileAbsPaths = append(uniqueConfigFileAbsPaths, uniqueAbsPath)
   626  	}
   627  
   628  	return uniqueConfigFileAbsPaths, nil
   629  }
   630  
   631  // Finds the absolute paths of all arbitrary project hcl files
   632  func getAllTerragruntProjectHclFiles(projectHclFiles []string, gitRoot string) map[string][]string {
   633  	orderedHclFilePaths := map[string][]string{}
   634  	uniqueHclFileAbsPaths := map[string][]string{}
   635  	for _, projectHclFile := range projectHclFiles {
   636  		err := filepath.Walk(gitRoot, func(path string, info os.FileInfo, err error) error {
   637  			if err != nil {
   638  				return err
   639  			}
   640  
   641  			if !info.IsDir() && info.Name() == projectHclFile {
   642  				orderedHclFilePaths[projectHclFile] = append(orderedHclFilePaths[projectHclFile], filepath.Dir(path))
   643  			}
   644  
   645  			return nil
   646  		})
   647  
   648  		if err != nil {
   649  			log.Fatal(err)
   650  		}
   651  
   652  		for _, uniquePath := range orderedHclFilePaths[projectHclFile] {
   653  			uniqueAbsPath, err := filepath.Abs(uniquePath)
   654  			if err != nil {
   655  				return nil
   656  			}
   657  			uniqueHclFileAbsPaths[projectHclFile] = append(uniqueHclFileAbsPaths[projectHclFile], uniqueAbsPath)
   658  		}
   659  	}
   660  	return uniqueHclFileAbsPaths
   661  }
   662  
   663  func Parse(gitRoot string, projectHclFiles []string, createHclProjectExternalChilds bool, autoMerge bool, parallel bool, filterPath string, createHclProjectChilds bool, ignoreParentTerragrunt bool, ignoreDependencyBlocks bool, cascadeDependencies bool, defaultWorkflow string, defaultApplyRequirements []string, autoPlan bool, defaultTerraformVersion string, createProjectName bool, createWorkspace bool, preserveProjects bool, useProjectMarkers bool, executionOrderGroups bool) (*AtlantisConfig, map[string][]string, error) {
   664  	// Ensure the gitRoot has a trailing slash and is an absolute path
   665  	absoluteGitRoot, err := filepath.Abs(gitRoot)
   666  	if err != nil {
   667  		return nil, nil, err
   668  	}
   669  	gitRoot = absoluteGitRoot + string(filepath.Separator)
   670  	workingDirs := []string{gitRoot}
   671  	projectHclDirMap := map[string][]string{}
   672  	var projectHclDirs []string
   673  	if len(projectHclFiles) > 0 {
   674  		workingDirs = nil
   675  		// map [project-hcl-file] => directories containing project-hcl-file
   676  		projectHclDirMap = getAllTerragruntProjectHclFiles(projectHclFiles, gitRoot)
   677  		for _, projectHclFile := range projectHclFiles {
   678  			projectHclDirs = append(projectHclDirs, projectHclDirMap[projectHclFile]...)
   679  			workingDirs = append(workingDirs, projectHclDirMap[projectHclFile]...)
   680  		}
   681  		// parse terragrunt child modules outside the scope of projectHclDirs
   682  		if createHclProjectExternalChilds {
   683  			workingDirs = append(workingDirs, gitRoot)
   684  		}
   685  	}
   686  	atlantisConfig := AtlantisConfig{
   687  		Version:       3,
   688  		AutoMerge:     autoMerge,
   689  		ParallelPlan:  parallel,
   690  		ParallelApply: parallel,
   691  	}
   692  
   693  	lock := sync.Mutex{}
   694  	ctx := context.Background()
   695  	errGroup, _ := errgroup.WithContext(ctx)
   696  	sem := semaphore.NewWeighted(10)
   697  	projectDependenciesMap := sync.Map{}
   698  	for _, workingDir := range workingDirs {
   699  		terragruntFiles, err := getAllTerragruntFiles(filterPath, projectHclFiles, workingDir)
   700  		if err != nil {
   701  			return nil, nil, err
   702  		}
   703  
   704  		if len(projectHclDirs) == 0 || createHclProjectChilds || (createHclProjectExternalChilds && workingDir == gitRoot) {
   705  			// Concurrently looking all dependencies
   706  			for _, terragruntPath := range terragruntFiles {
   707  				terragruntPath := terragruntPath // https://golang.org/doc/faq#closures_and_goroutines
   708  
   709  				// don't create atlantis projects already covered by project hcl file projects
   710  				skipProject := false
   711  				if createHclProjectExternalChilds && workingDir == gitRoot && len(projectHclDirs) > 0 {
   712  					for _, projectHclDir := range projectHclDirs {
   713  						if strings.HasPrefix(terragruntPath, projectHclDir) {
   714  							skipProject = true
   715  							break
   716  						}
   717  					}
   718  				}
   719  				if skipProject {
   720  					continue
   721  				}
   722  				err := sem.Acquire(ctx, 1)
   723  				if err != nil {
   724  					return nil, nil, err
   725  				}
   726  
   727  				errGroup.Go(func() error {
   728  					defer sem.Release(1)
   729  					project, projDeps, err := createProject(ignoreParentTerragrunt, ignoreDependencyBlocks, gitRoot, cascadeDependencies, defaultWorkflow, defaultApplyRequirements, autoPlan, defaultTerraformVersion, createProjectName, createWorkspace, terragruntPath)
   730  					if err != nil {
   731  						return err
   732  					}
   733  					// if project and err are nil then skip this project
   734  					if err == nil && project == nil {
   735  						return nil
   736  					}
   737  
   738  					projectDependenciesMap.Store(project.Name, projDeps)
   739  
   740  					// Lock the list as only one goroutine should be writing to atlantisConfig.Projects at a time
   741  					lock.Lock()
   742  					defer lock.Unlock()
   743  
   744  					// When preserving existing projects, we should update existing blocks instead of creating a
   745  					// duplicate, when generating something which already has representation
   746  					if preserveProjects {
   747  						updateProject := false
   748  
   749  						// TODO: with Go 1.19, we can replace for loop with slices.IndexFunc for increased performance
   750  						for i := range atlantisConfig.Projects {
   751  							if atlantisConfig.Projects[i].Dir == project.Dir {
   752  								updateProject = true
   753  								log.Info("Updated project for ", terragruntPath)
   754  								atlantisConfig.Projects[i] = *project
   755  
   756  								// projects should be unique, let's exit for loop for performance
   757  								// once first occurrence is found and replaced
   758  								break
   759  							}
   760  						}
   761  
   762  						if !updateProject {
   763  							log.Info("Created project for ", terragruntPath)
   764  							atlantisConfig.Projects = append(atlantisConfig.Projects, *project)
   765  						}
   766  					} else {
   767  						log.Info("Created project for ", terragruntPath)
   768  						atlantisConfig.Projects = append(atlantisConfig.Projects, *project)
   769  					}
   770  
   771  					return nil
   772  				})
   773  			}
   774  
   775  			if err := errGroup.Wait(); err != nil {
   776  				return nil, nil, err
   777  			}
   778  		}
   779  		if len(projectHclDirs) > 0 && workingDir != gitRoot {
   780  			projectHcl := lookupProjectHcl(projectHclDirMap, workingDir)
   781  			err := sem.Acquire(ctx, 1)
   782  			if err != nil {
   783  				return nil, nil, err
   784  			}
   785  
   786  			errGroup.Go(func() error {
   787  				defer sem.Release(1)
   788  				project, err := createHclProject(defaultWorkflow, defaultApplyRequirements, autoPlan, useProjectMarkers, defaultTerraformVersion, ignoreParentTerragrunt, ignoreDependencyBlocks, gitRoot, cascadeDependencies, createProjectName, createWorkspace, terragruntFiles, workingDir, projectHcl)
   789  				if err != nil {
   790  					return err
   791  				}
   792  				// if project and err are nil then skip this project
   793  				if err == nil && project == nil {
   794  					return nil
   795  				}
   796  				// Lock the list as only one goroutine should be writing to atlantisConfig.Projects at a time
   797  				lock.Lock()
   798  				defer lock.Unlock()
   799  
   800  				log.Info("Created "+projectHcl+" project for ", workingDir)
   801  				atlantisConfig.Projects = append(atlantisConfig.Projects, *project)
   802  
   803  				return nil
   804  			})
   805  
   806  			if err := errGroup.Wait(); err != nil {
   807  				return nil, nil, err
   808  			}
   809  		}
   810  	}
   811  
   812  	// Sort the projects in atlantisConfig by Dir
   813  	sort.Slice(atlantisConfig.Projects, func(i, j int) bool { return atlantisConfig.Projects[i].Dir < atlantisConfig.Projects[j].Dir })
   814  	//
   815  	if executionOrderGroups {
   816  		projectsMap := make(map[string]*AtlantisProject, len(atlantisConfig.Projects))
   817  		for i := range atlantisConfig.Projects {
   818  			projectsMap[atlantisConfig.Projects[i].Dir] = &atlantisConfig.Projects[i]
   819  		}
   820  
   821  		// Compute order groups in the cycle to avoid incorrect values in cascade dependencies
   822  		hasChanges := true
   823  		for i := 0; hasChanges && i <= len(atlantisConfig.Projects); i++ {
   824  			hasChanges = false
   825  			for _, project := range atlantisConfig.Projects {
   826  				executionOrderGroup := 0
   827  				// choose order group based on dependencies
   828  				for _, dep := range project.Autoplan.WhenModified {
   829  					depPath := filepath.Dir(filepath.Join(project.Dir, dep))
   830  					if depPath == project.Dir {
   831  						// skip dependency on oneself
   832  						continue
   833  					}
   834  
   835  					depProject, ok := projectsMap[depPath]
   836  					if !ok {
   837  						// skip not project dependencies
   838  						continue
   839  					}
   840  					if depProject.ExecutionOrderGroup+1 > executionOrderGroup {
   841  						executionOrderGroup = depProject.ExecutionOrderGroup + 1
   842  					}
   843  				}
   844  				if projectsMap[project.Dir].ExecutionOrderGroup != executionOrderGroup {
   845  					projectsMap[project.Dir].ExecutionOrderGroup = executionOrderGroup
   846  					// repeat the main cycle when changed some project
   847  					hasChanges = true
   848  				}
   849  			}
   850  		}
   851  
   852  		if hasChanges {
   853  			// Should be unreachable
   854  			log.Warn("Computing execution_order_groups failed. Probably cycle exists")
   855  		}
   856  
   857  		// Sort by execution_order_group
   858  		sort.Slice(atlantisConfig.Projects, func(i, j int) bool {
   859  			if atlantisConfig.Projects[i].ExecutionOrderGroup == atlantisConfig.Projects[j].ExecutionOrderGroup {
   860  				return atlantisConfig.Projects[i].Dir < atlantisConfig.Projects[j].Dir
   861  			}
   862  			return atlantisConfig.Projects[i].ExecutionOrderGroup < atlantisConfig.Projects[j].ExecutionOrderGroup
   863  		})
   864  	}
   865  
   866  	dependsOn := make(map[string][]string)
   867  
   868  	projectDependenciesMap.Range(func(projectName, dependencies interface{}) bool {
   869  		project := projectName.(string)
   870  		for _, dep := range dependencies.([]string) {
   871  			_, ok := projectDependenciesMap.Load(dep)
   872  			if ok {
   873  				deps, ok := dependsOn[project]
   874  				if ok {
   875  					dependsOn[project] = append(deps, dep)
   876  				} else {
   877  					dependsOn[project] = []string{dep}
   878  				}
   879  			}
   880  		}
   881  		return true
   882  	})
   883  
   884  	return &atlantisConfig, dependsOn, nil
   885  }