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 }