Skip to content

enables multiple includes #707

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 29 additions & 24 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,20 +301,23 @@ func parseConfigString(configString string, terragruntOptions *options.Terragrun
return nil, err
}

if include != nil && terragruntConfigFile.Include != nil {
return nil, errors.WithStackTrace(TooManyLevelsOfInheritance{
ConfigPath: terragruntOptions.TerragruntConfigPath,
FirstLevelIncludePath: include.Path,
SecondLevelIncludePath: terragruntConfigFile.Include.Path,
})
includedConfig, err := parseIncludedConfig(terragruntConfigFile.Include, terragruntOptions)
if err != nil {
return nil, err
}

includedConfig, err := parseIncludedConfig(terragruntConfigFile.Include, terragruntOptions)
merged, err := mergeConfigWithIncludedConfig(config, includedConfig, terragruntOptions)
if err != nil {
return nil, err
}

return mergeConfigWithIncludedConfig(config, includedConfig, terragruntOptions)
if includedConfig == nil {
err = validateTerragruntConfig(merged)
if err != nil {
return nil, err
}
}
return merged, nil
}

// Parse the given config string, read from the given config file, as a terragruntConfigFile struct. This method solely
Expand Down Expand Up @@ -344,7 +347,16 @@ func mergeConfigWithIncludedConfig(config *TerragruntConfig, includedConfig *Ter
}

if config.RemoteState != nil {
includedConfig.RemoteState = config.RemoteState
if includedConfig.RemoteState == nil {
includedConfig.RemoteState = config.RemoteState
} else {
if config.RemoteState.Backend != "" {
includedConfig.RemoteState.Backend = config.RemoteState.Backend
}
for k, v := range config.RemoteState.Config {
includedConfig.RemoteState.Config[k] = v
}
}
}
if config.PreventDestroy {
includedConfig.PreventDestroy = config.PreventDestroy
Expand Down Expand Up @@ -463,7 +475,7 @@ func parseIncludedConfig(includedConfig *IncludeConfig, terragruntOptions *optio
return nil, errors.WithStackTrace(IncludedConfigMissingPath(terragruntOptions.TerragruntConfigPath))
}

resolvedIncludePath, err := ResolveTerragruntConfigString(includedConfig.Path, nil, terragruntOptions)
resolvedIncludePath, err := ResolveTerragruntConfigString(includedConfig.Path, includedConfig, terragruntOptions)
if err != nil {
return nil, err
}
Expand All @@ -485,10 +497,6 @@ func convertToTerragruntConfig(terragruntConfigFromFile *terragruntConfigFile, t

if terragruntConfigFromFile.RemoteState != nil {
terragruntConfigFromFile.RemoteState.FillDefaults()
if err := terragruntConfigFromFile.RemoteState.Validate(); err != nil {
return nil, err
}

terragruntConfig.RemoteState = terragruntConfigFromFile.RemoteState
}

Expand All @@ -505,6 +513,13 @@ func convertToTerragruntConfig(terragruntConfigFromFile *terragruntConfigFile, t
return terragruntConfig, nil
}

func validateTerragruntConfig(config *TerragruntConfig) error {
if config.RemoteState != nil {
return config.RemoteState.Validate()
}
return nil
}

// Custom error types

type InvalidArgError string
Expand All @@ -519,16 +534,6 @@ func (err IncludedConfigMissingPath) Error() string {
return fmt.Sprintf("The include configuration in %s must specify a 'path' parameter", string(err))
}

type TooManyLevelsOfInheritance struct {
ConfigPath string
FirstLevelIncludePath string
SecondLevelIncludePath string
}

func (err TooManyLevelsOfInheritance) Error() string {
return fmt.Sprintf("%s includes %s, which itself includes %s. Only one level of includes is allowed.", err.ConfigPath, err.FirstLevelIncludePath, err.SecondLevelIncludePath)
}

type CouldNotResolveTerragruntConfigInFile string

func (err CouldNotResolveTerragruntConfigInFile) Error() string {
Expand Down
10 changes: 7 additions & 3 deletions config/config_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func ResolveTerragruntConfigString(terragruntConfigString string, include *Inclu
func executeTerragruntHelperFunction(functionName string, parameters string, include *IncludeConfig, terragruntOptions *options.TerragruntOptions) (interface{}, error) {
switch functionName {
case "find_in_parent_folders":
return findInParentFolders(parameters, terragruntOptions)
return findInParentFolders(parameters, include, terragruntOptions)
case "path_relative_to_include":
return pathRelativeToInclude(include, terragruntOptions)
case "path_relative_from_include":
Expand Down Expand Up @@ -258,7 +258,7 @@ func getEnvironmentVariable(parameters string, terragruntOptions *options.Terrag

// Find a parent Terragrunt configuration file in the parent folders above the current Terragrunt configuration file
// and return its path
func findInParentFolders(parameters string, terragruntOptions *options.TerragruntOptions) (string, error) {
func findInParentFolders(parameters string, include *IncludeConfig, terragruntOptions *options.TerragruntOptions) (string, error) {
fileToFindParam, fallbackParam, numParams, err := parseOptionalQuotedParam(parameters)
if err != nil {
return "", err
Expand All @@ -267,7 +267,11 @@ func findInParentFolders(parameters string, terragruntOptions *options.Terragrun
return "", errors.WithStackTrace(EmptyStringNotAllowed("parameter to the find_in_parent_folders_function"))
}

previousDir, err := filepath.Abs(filepath.Dir(terragruntOptions.TerragruntConfigPath))
previousDir := filepath.Dir(terragruntOptions.TerragruntConfigPath)
if include != nil {
previousDir = filepath.Join(previousDir, filepath.Dir(include.Path))
}
previousDir, err = filepath.Abs(previousDir)
previousDir = filepath.ToSlash(previousDir)

if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion config/config_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ func TestFindInParentFolders(t *testing.T) {

for _, testCase := range testCases {
t.Run(testCase.terragruntOptions.TerragruntConfigPath, func(t *testing.T) {
actualPath, actualErr := findInParentFolders(testCase.params, testCase.terragruntOptions)
actualPath, actualErr := findInParentFolders(testCase.params, nil, testCase.terragruntOptions)
if testCase.expectedErr != nil {
if assert.Error(t, actualErr) {
assert.IsType(t, testCase.expectedErr, errors.Unwrap(actualErr))
Expand Down
82 changes: 70 additions & 12 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,46 @@ terragrunt = {

}

func TestParseTerragruntConfigIncludeMergeRemote(t *testing.T) {
t.Parallel()

config :=
fmt.Sprintf(`
terragrunt = {
include {
path = "../../../%s"
}

# Configure Terragrunt to automatically store tfstate files in an S3 bucket
remote_state {
backend = "s3"
config {
encrypt = false
profile = "new"
}
}
}
`, DefaultTerragruntConfigPath)

opts := mockOptionsForTestWithConfigPath(t, "../test/fixture-parent-folders/terragrunt-in-root/child/sub-child/sub-sub-child/"+DefaultTerragruntConfigPath)

terragruntConfig, err := parseConfigString(config, opts, nil, opts.TerragruntConfigPath)
if assert.Nil(t, err, "Unexpected error: %v", errors.PrintErrorWithStackTrace(err)) {
assert.Nil(t, terragruntConfig.Terraform)

if assert.NotNil(t, terragruntConfig.RemoteState) {
assert.Equal(t, "s3", terragruntConfig.RemoteState.Backend)
assert.NotEmpty(t, terragruntConfig.RemoteState.Config)
assert.Equal(t, false, terragruntConfig.RemoteState.Config["encrypt"])
assert.Equal(t, "my-bucket", terragruntConfig.RemoteState.Config["bucket"])
assert.Equal(t, "child/sub-child/sub-sub-child/terraform.tfstate", terragruntConfig.RemoteState.Config["key"])
assert.Equal(t, "us-east-1", terragruntConfig.RemoteState.Config["region"])
assert.Equal(t, "new", terragruntConfig.RemoteState.Config["profile"])
}
}

}

func TestParseTerragruntConfigIncludeOverrideAll(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -427,13 +467,21 @@ func TestParseTerragruntConfigTwoLevels(t *testing.T) {

opts := mockOptionsForTestWithConfigPath(t, configPath)

_, actualErr := parseConfigString(config, opts, nil, configPath)
expectedErr := TooManyLevelsOfInheritance{
ConfigPath: configPath,
FirstLevelIncludePath: "../" + DefaultTerragruntConfigPath,
SecondLevelIncludePath: "../" + DefaultTerragruntConfigPath,
terragruntConfig, err := parseConfigString(config, opts, nil, opts.TerragruntConfigPath)
if assert.Nil(t, err, "Unexpected error: %v", errors.PrintErrorWithStackTrace(err)) {
if assert.NotNil(t, terragruntConfig.RemoteState) {
assert.Equal(t, "s3", terragruntConfig.RemoteState.Backend)
assert.NotEmpty(t, terragruntConfig.RemoteState.Config)
assert.Equal(t, true, terragruntConfig.RemoteState.Config["encrypt"])
assert.Equal(t, "my-bucket", terragruntConfig.RemoteState.Config["bucket"])
assert.Equal(t, "child/sub-child/terraform.tfstate", terragruntConfig.RemoteState.Config["key"])
assert.Equal(t, "us-east-1", terragruntConfig.RemoteState.Config["region"])
assert.Equal(t, "child/sub-child", terragruntConfig.Terraform.ExtraArgs[0].EnvVars["TF_VAR_root_to"])
assert.Equal(t, "../..", terragruntConfig.Terraform.ExtraArgs[0].EnvVars["TF_VAR_root_from"])
assert.Equal(t, "sub-child", terragruntConfig.Terraform.ExtraArgs[1].EnvVars["TF_VAR_child_to"])
assert.Equal(t, "..", terragruntConfig.Terraform.ExtraArgs[1].EnvVars["TF_VAR_child_from"])
}
}
assert.True(t, errors.IsError(actualErr, expectedErr), "Expected error %v but got %v", expectedErr, actualErr)
}

func TestParseTerragruntConfigThreeLevels(t *testing.T) {
Expand All @@ -448,13 +496,23 @@ func TestParseTerragruntConfigThreeLevels(t *testing.T) {

opts := mockOptionsForTestWithConfigPath(t, configPath)

_, actualErr := parseConfigString(config, opts, nil, configPath)
expectedErr := TooManyLevelsOfInheritance{
ConfigPath: configPath,
FirstLevelIncludePath: "../" + DefaultTerragruntConfigPath,
SecondLevelIncludePath: "../" + DefaultTerragruntConfigPath,
terragruntConfig, err := parseConfigString(config, opts, nil, opts.TerragruntConfigPath)
if assert.Nil(t, err, "Unexpected error: %v", errors.PrintErrorWithStackTrace(err)) {
if assert.NotNil(t, terragruntConfig.RemoteState) {
assert.Equal(t, "s3", terragruntConfig.RemoteState.Backend)
assert.NotEmpty(t, terragruntConfig.RemoteState.Config)
assert.Equal(t, true, terragruntConfig.RemoteState.Config["encrypt"])
assert.Equal(t, "my-bucket", terragruntConfig.RemoteState.Config["bucket"])
assert.Equal(t, "child/sub-child/sub-sub-child/terraform.tfstate", terragruntConfig.RemoteState.Config["key"])
assert.Equal(t, "us-east-1", terragruntConfig.RemoteState.Config["region"])
assert.Equal(t, "child/sub-child/sub-sub-child", terragruntConfig.Terraform.ExtraArgs[0].EnvVars["TF_VAR_root_to"])
assert.Equal(t, "../../..", terragruntConfig.Terraform.ExtraArgs[0].EnvVars["TF_VAR_root_from"])
assert.Equal(t, "sub-child/sub-sub-child", terragruntConfig.Terraform.ExtraArgs[1].EnvVars["TF_VAR_child_to"])
assert.Equal(t, "../..", terragruntConfig.Terraform.ExtraArgs[1].EnvVars["TF_VAR_child_from"])
assert.Equal(t, "sub-sub-child", terragruntConfig.Terraform.ExtraArgs[2].EnvVars["TF_VAR_sub_child_to"])
assert.Equal(t, "..", terragruntConfig.Terraform.ExtraArgs[2].EnvVars["TF_VAR_sub_child_from"])
}
}
assert.True(t, errors.IsError(actualErr, expectedErr), "Expected error %v but got %v", expectedErr, actualErr)
}

func TestParseTerragruntConfigEmptyConfig(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,14 @@ terragrunt = {
include {
path = "${find_in_parent_folders()}"
}
}

terraform {
extra_arguments "sub-child" {
commands = ["${get_terraform_commands_that_need_vars()}"]
env_vars = {
TF_VAR_sub_child_from = "${path_relative_from_include()}"
TF_VAR_sub_child_to = "${path_relative_to_include()}"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,21 @@ terragrunt = {
include {
path = "${find_in_parent_folders()}"
}

remote_state {
config {
region = "us-east-1"
}
}

terraform {
extra_arguments "child" {
commands = ["${get_terraform_commands_that_need_vars()}"]
env_vars = {
TF_VAR_child_from = "${path_relative_from_include()}"
TF_VAR_child_to = "${path_relative_to_include()}"
}
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@ terragrunt = {
encrypt = true
bucket = "my-bucket"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
region = "none"
}
}

terraform {
extra_arguments "root" {
commands = ["${get_terraform_commands_that_need_vars()}"]
env_vars = {
TF_VAR_root_from = "${path_relative_from_include()}"
TF_VAR_root_to = "${path_relative_to_include()}"
}
}
}
}