Skip to content

Commit

Permalink
feat: add terramate.config.change_detection.terragrunt option. (#1660)
Browse files Browse the repository at this point in the history
## What this PR does / why we need it:

Introduces the `terramate.config.change_detection.terragrunt.enabled`
attribute supporting the options below:
- `auto`: detects if Terragrunt modules are present and enables
automatically.
- `off`: turn off the integration.
- `force`: do Terragrunt change detection even if no Terragrunt module
is detected.

## Which issue(s) this PR fixes:
Relates to #1656 

## Special notes for your reviewer:

Additional changes were required because the `config.Root` was not a
singleton.

## Does this PR introduce a user-facing change?
```
yes, adds a new feature.
```
  • Loading branch information
i4ki committed Apr 24, 2024
2 parents e04c95d + 9c437a1 commit 85453f2
Show file tree
Hide file tree
Showing 13 changed files with 338 additions and 53 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ Given a version number `MAJOR.MINOR.PATCH`, we increment the:

## Unreleased

### Added

- Add `terramate.config.change_detection.terragrunt.enabled` attribute. It supports the values below:
- `auto` (*default*): Automatically detects if Terragrunt is being used and enables change detection if needed.
- `force`: Enables Terragrunt change detection even if no Terragrunt file is detected.
- `off`: Disables Terragrunt change detection.

## v0.6.4

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion cmd/terramate/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -1727,7 +1727,7 @@ func (c *cli) generateGraph() {
fatal(`-label expects the values "stack.name" or "stack.dir"`)
}

entries, err := stack.List(c.cfg().Tree())
entries, err := stack.List(c.cfg(), c.cfg().Tree())
if err != nil {
fatalWithDetails(err, "listing stacks to build graph")
}
Expand Down
82 changes: 69 additions & 13 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ const (
type Root struct {
tree Tree

// hasTerragruntStacks tells if the repository has any Terragrunt stack.
hasTerragruntStacks *bool

runtime project.Runtime
}

Expand All @@ -65,6 +68,8 @@ type Tree struct {
// Parent is the parent node or nil if none.
Parent *Tree

// project root is only set if Parent == nil.
root *Root
stack *Stack

dir string
Expand Down Expand Up @@ -116,9 +121,10 @@ func TryLoadConfig(fromdir string) (tree *Root, configpath string, found bool, e

// NewRoot creates a new [Root] tree for the cfg tree.
func NewRoot(tree *Tree) *Root {
r := &Root{
tree: *tree,
}
r := &Root{}
tree.root = r
r.tree = *tree

r.initRuntime()
return r
}
Expand Down Expand Up @@ -311,7 +317,15 @@ func (tree *Tree) Root() *Root {
if tree.Parent != nil {
return tree.Parent.Root()
}
return NewRoot(tree)
return tree.root
}

// RootTree returns the tree at the project root.
func (tree *Tree) RootTree() *Tree {
if tree.Parent != nil {
return tree.Parent.RootTree()
}
return tree
}

// IsStack tells if the node is a stack.
Expand Down Expand Up @@ -419,7 +433,7 @@ func loadTree(parentTree *Tree, cfgdir string, rootcfg *hcl.Config) (_ *Tree, er
}

if parentTree != nil && rootcfg == nil {
rootcfg = &parentTree.Root().Tree().Node
rootcfg = &parentTree.RootTree().Node
}

if cfgdir != parentTree.RootDir() {
Expand All @@ -437,7 +451,7 @@ func loadTree(parentTree *Tree, cfgdir string, rootcfg *hcl.Config) (_ *Tree, er
parentTree = tree
}

err = processTmGenFiles(parentTree.Root(), &parentTree.Node, cfgdir, dirEntries)
err = processTmGenFiles(parentTree.RootTree(), &parentTree.Node, cfgdir, dirEntries)
if err != nil {
return nil, err
}
Expand All @@ -460,10 +474,10 @@ func loadTree(parentTree *Tree, cfgdir string, rootcfg *hcl.Config) (_ *Tree, er
return parentTree, nil
}

func processTmGenFiles(root *Root, cfg *hcl.Config, cfgdir string, dirEntries []fs.DirEntry) error {
func processTmGenFiles(rootTree *Tree, cfg *hcl.Config, cfgdir string, dirEntries []fs.DirEntry) error {
const tmgenSuffix = ".tmgen"

tmgenEnabled := root.HasExperiment("tmgen")
tmgenEnabled := rootTree.hasExperiment("tmgen")

// process all .tmgen files.
for _, dirEntry := range dirEntries {
Expand Down Expand Up @@ -522,9 +536,9 @@ func processTmGenFiles(root *Root, cfg *hcl.Config, cfgdir string, dirEntries []

implicitGenBlock := hcl.GenHCLBlock{
IsImplicitBlock: true,
Dir: project.PrjAbsPath(root.HostDir(), cfgdir),
Dir: project.PrjAbsPath(rootTree.HostDir(), cfgdir),
Inherit: inheritAttr,
Range: info.NewRange(root.HostDir(), hhcl.Range{
Range: info.NewRange(rootTree.HostDir(), hhcl.Range{
Filename: absFname,
Start: hhcl.InitialPos,
End: hhcl.Pos{
Expand Down Expand Up @@ -572,13 +586,55 @@ func NewTree(cfgdir string) *Tree {
}
}

func (tree *Tree) hasExperiment(name string) bool {
if tree.Parent != nil {
return tree.Parent.hasExperiment(name)
}
if tree.Node.Terramate == nil || tree.Node.Terramate.Config == nil {
return false
}

return slices.Contains(tree.Node.Terramate.Config.Experiments, name)
}

// HasExperiment returns true if the given experiment name is set.
func (root *Root) HasExperiment(name string) bool {
if root.tree.Node.Terramate == nil || root.tree.Node.Terramate.Config == nil {
return false
return root.tree.hasExperiment(name)
}

// TerragruntEnabledOption returns the configured `terramate.config.change_detection.terragrunt.enabled` option.
func (root *Root) TerragruntEnabledOption() hcl.TerragruntChangeDetectionEnabledOption {
if root.tree.Node.Terramate != nil &&
root.tree.Node.Terramate.Config != nil &&
root.tree.Node.Terramate.Config.ChangeDetection != nil &&
root.tree.Node.Terramate.Config.ChangeDetection.Terragrunt != nil {
return root.tree.Node.Terramate.Config.ChangeDetection.Terragrunt.Enabled
}
return hcl.TerragruntAutoOption // "auto" is the default.
}

return slices.Contains(root.tree.Node.Terramate.Config.Experiments, name)
// HasTerragruntStacks returns true if the stack loading has detected Terragrunt files.
func (root *Root) HasTerragruntStacks() bool {
b := root.hasTerragruntStacks
if b == nil {
panic(errors.E(errors.ErrInternal, "root.HasTerragruntStacks should be called after stacks list is computed"))
}
return *b
}

// IsTerragruntChangeDetectionEnabled returns true if Terragrunt change detection integration
// must be executed.
func (root *Root) IsTerragruntChangeDetectionEnabled() bool {
switch opt := root.TerragruntEnabledOption(); opt {
case hcl.TerragruntOffOption:
return false
case hcl.TerragruntForceOption:
return true
case hcl.TerragruntAutoOption:
return root.HasTerragruntStacks()
default:
panic(errors.E(errors.ErrInternal, "unexpected terragrunt option: %v", opt))
}
}

// Skip returns true if the given file/dir name should be ignored by Terramate.
Expand Down
24 changes: 12 additions & 12 deletions config/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"regexp"
"strings"

"github.com/rs/zerolog/log"
"github.com/terramate-io/terramate/config/tag"
"github.com/terramate-io/terramate/errors"
"github.com/terramate-io/terramate/hcl"
Expand Down Expand Up @@ -294,12 +293,9 @@ func StacksFromTrees(trees List[*Tree]) (List[*SortableStack], error) {
}

// LoadAllStacks loads all stacks inside the given rootdir.
func LoadAllStacks(cfg *Tree) (List[*SortableStack], error) {
logger := log.With().
Str("action", "stack.LoadAll()").
Str("root", cfg.RootDir()).
Logger()

func LoadAllStacks(root *Root, cfg *Tree) (List[*SortableStack], error) {
falsy := false
root.hasTerragruntStacks = &falsy
stacks := List[*SortableStack]{}
stacksIDs := map[string]*Stack{}

Expand All @@ -309,13 +305,17 @@ func LoadAllStacks(cfg *Tree) (List[*SortableStack], error) {
return nil, err
}

logger := logger.With().
Stringer("stack", stack).
Logger()

logger.Debug().Msg("Found stack")
stacks = append(stacks, stack.Sortable())

if !*root.hasTerragruntStacks {
st, err := os.Lstat(
filepath.Join(stack.Dir.HostPath(root.HostDir()), "terragrunt.hcl"),
)
if err == nil && st.Mode().IsRegular() {
*root.hasTerragruntStacks = true
}
}

if stack.ID != "" {
if otherStack, ok := stacksIDs[strings.ToLower(stack.ID)]; ok {
return List[*SortableStack]{}, errors.E(ErrStackDuplicatedID,
Expand Down
4 changes: 2 additions & 2 deletions generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ type LoadResult struct {
// a non-nil error. In this case the error is not specific to generating code
// for a specific dir.
func Load(root *config.Root, vendorDir project.Path) ([]LoadResult, error) {
stacks, err := config.LoadAllStacks(root.Tree())
stacks, err := config.LoadAllStacks(root, root.Tree())
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -515,7 +515,7 @@ func DetectOutdated(root *config.Root, vendorDir project.Path) ([]string, error)
Str("action", "generate.DetectOutdated()").
Logger()

stacks, err := config.LoadAllStacks(root.Tree())
stacks, err := config.LoadAllStacks(root, root.Tree())
if err != nil {
return nil, err
}
Expand Down
8 changes: 4 additions & 4 deletions globals/globals_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4066,7 +4066,7 @@ func TestLoadGlobalsErrors(t *testing.T) {
test.AppendFile(t, path, config.DefaultFilename, c.body)
}

cfg, err := config.LoadTree(s.RootDir(), s.RootDir())
root, err := config.LoadRoot(s.RootDir())
// TODO(i4k): this better not be tested here.
if errors.IsKind(tcase.want, hcl.ErrHCLSyntax) {
errtest.Assert(t, err, tcase.want)
Expand All @@ -4076,7 +4076,7 @@ func TestLoadGlobalsErrors(t *testing.T) {
return
}

stacks, err := config.LoadAllStacks(cfg)
stacks, err := config.LoadAllStacks(root, root.Tree())
assert.NoError(t, err)
for _, elem := range stacks {
report := globals.ForStack(s.Config(), elem.Stack)
Expand All @@ -4102,13 +4102,13 @@ func testGlobals(t *testing.T, tcase testcase) {

wantGlobals := tcase.want

cfg, err := config.LoadTree(s.RootDir(), s.RootDir())
root, err := config.LoadRoot(s.RootDir())
if err != nil {
errtest.Assert(t, err, tcase.wantErr)
return
}

stackEntries, err := stack.List(cfg)
stackEntries, err := stack.List(root, root.Tree())
assert.NoError(t, err)

var stacks config.List[*config.SortableStack]
Expand Down
Loading

0 comments on commit 85453f2

Please sign in to comment.