Skip to content

docs: auto discovering of projects #3038

Open
@nitrocode

Description

@nitrocode

Community Note

  • Please vote on this issue by adding a 👍 reaction to the original issue to help the community and maintainers prioritize this request. Searching for pre-existing feature requests helps us consolidate datapoints for identical requirements into a single place, thank you!
  • Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request.
  • If you are interested in working on this issue or have submitted a pull request, please leave a comment.

Describe the user story

Describe the solution you'd like

The auto discover (discovery / autodiscovery / autodiscover) feature of Atlantis is relied on by many Atlantis users since it is the default. Most users do not know the magic that is used to calculate the discovered projects to get auto planning to work and so it should be documented.

This will allow greater understanding and future improvements to the feature.

Here are some gems for the documentation writer

buildAllCommandsByCfg

} else {
// If there is no config file or it specified no projects, then we'll plan each project that
// our algorithm determines was modified.
if hasRepoCfg {
ctx.Log.Info("No projects are defined in %s. Will resume automatic detection", repoCfgFile)
} else {
ctx.Log.Info("found no %s file", repoCfgFile)
}
// build a module index for projects that are explicitly included
moduleInfo, err := FindModuleProjects(repoDir, p.AutoDetectModuleFiles)
if err != nil {
ctx.Log.Warn("error(s) loading project module dependencies: %s", err)
}
ctx.Log.Debug("moduleInfo for %s (matching %q) = %v", repoDir, p.AutoDetectModuleFiles, moduleInfo)
modifiedProjects := p.ProjectFinder.DetermineProjects(ctx.Log, modifiedFiles, ctx.Pull.BaseRepo.FullName, repoDir, p.AutoplanFileList, moduleInfo)
ctx.Log.Info("automatically determined that there were %d projects modified in this pull request: %s", len(modifiedProjects), modifiedProjects)
for _, mp := range modifiedProjects {
ctx.Log.Debug("determining config for project at dir: %q", mp.Path)
pWorkspace, err := p.ProjectFinder.DetermineWorkspaceFromHCL(ctx.Log, repoDir)
if err != nil {
return nil, errors.Wrapf(err, "looking for Terraform Cloud workspace from configuration %s", repoDir)
}
automerge := DefaultAutomergeEnabled
parallelApply := DefaultParallelApplyEnabled
parallelPlan := DefaultParallelPlanEnabled
if hasRepoCfg {
automerge = repoCfg.Automerge
parallelApply = repoCfg.ParallelApply
parallelPlan = repoCfg.ParallelPlan
}
pCfg := p.GlobalCfg.DefaultProjCfg(ctx.Log, ctx.Pull.BaseRepo.ID(), mp.Path, pWorkspace)
projCtxs = append(projCtxs,
p.ProjectCommandContextBuilder.BuildProjectContext(
ctx,
cmdName,
subCmdName,
pCfg,
commentFlags,
repoDir,
automerge,
parallelApply,
parallelPlan,
verbose,
p.TerraformExecutor,
)...)
}

Calls FindModuleProjects

func FindModuleProjects(absRepoDir string, autoplanModuleDependants string) (ModuleProjects, error) {
return findModuleDependants(os.DirFS(absRepoDir), autoplanModuleDependants)
}

Calls findModuleDependants

func findModuleDependants(files fs.FS, autoplanModuleDependants string) (ModuleProjects, error) {
if autoplanModuleDependants == "" {
return moduleInfo{}, nil
}
// find all the projects matching autoplanModuleDependants
filter, _ := fileutils.NewPatternMatcher(strings.Split(autoplanModuleDependants, ","))
var projects []string
err := fs.WalkDir(files, ".", func(rel string, info fs.DirEntry, err error) error {
if match, _ := filter.Matches(rel); match {
if projectDir := getProjectDirFromFs(files, rel); projectDir != "" {
projects = append(projects, projectDir)
}
}
return err
})
if err != nil {
return nil, fmt.Errorf("find projects for module dependants: %w", err)
}
result := make(moduleInfo)
var diags tfconfig.Diagnostics
// for each project, find the modules it depends on, their deps, etc.
for _, projectDir := range projects {
if _, err := result.load(files, projectDir, projectDir); err != nil {
diags = append(diags, err...)
}
}
// if there are any errors, prefer one with a source location
if diags.HasErrors() {
for _, d := range diags {
if d.Pos != nil {
return nil, fmt.Errorf("%s:%d - %s: %s", d.Pos.Filename, d.Pos.Line, d.Summary, d.Detail)
}
}
}
return result, diags.Err()
}

Calls getProjectDirFromFs

func getProjectDirFromFs(files fs.FS, modifiedFilePath string) string {
dir := path.Dir(modifiedFilePath)
if path.Base(dir) == "env" {
// If the modified file was inside an env/ directory, we treat this
// specially and run plan one level up. This supports directory structures
// like:
// root/
// main.tf
// env/
// dev.tfvars
// staging.tfvars
return path.Dir(dir)
}
// Surrounding dir with /'s so we can match on /modules/ even if dir is
// "modules" or "project1/modules"
if isModule(dir) {
// We treat changes inside modules/ folders specially. There are two cases:
// 1. modules folder inside project:
// root/
// main.tf
// modules/
// ...
// In this case, if we detect a change in modules/, we will determine
// the project root to be at root/.
//
// 2. shared top-level modules folder
// root/
// project1/
// main.tf # uses modules via ../modules
// project2/
// main.tf # uses modules via ../modules
// modules/
// ...
// In this case, if we detect a change in modules/ we don't know which
// project was using this module so we can't suggest a project root, but we
// also detect that there's no main.tf in the parent folder of modules/
// so we won't suggest that as a project. So in this case we return nothing.
// The code below makes this happen.
// Need to add a trailing slash before splitting on modules/ because if
// the input was modules/file.tf then path.Dir will be "modules" and so our
// split on "modules/" will fail.
dirWithTrailingSlash := dir + "/"
modulesSplit := strings.SplitN(dirWithTrailingSlash, "modules/", 2)
modulesParent := modulesSplit[0]
// Now we check whether there is a main.tf in the parent.
if _, err := fs.Stat(files, filepath.Join(modulesParent, "main.tf")); errors.Is(err, fs.ErrNotExist) {
return ""
}
return path.Clean(modulesParent)
}
// If it wasn't a modules directory, we assume we're in a project and return
// this directory.
return dir
}
func isModule(dir string) bool {
return strings.Contains("/"+dir+"/", "/modules/")
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions