Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: auto discovering of projects #3038

Open
1 task
nitrocode opened this issue Jan 24, 2023 · 0 comments
Open
1 task

docs: auto discovering of projects #3038

nitrocode opened this issue Jan 24, 2023 · 0 comments
Labels
docs Documentation Stale

Comments

@nitrocode
Copy link
Member

nitrocode commented Jan 24, 2023

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/")
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Documentation Stale
Projects
None yet
Development

No branches or pull requests

1 participant