diff --git a/README.md b/README.md index 277987a942..c4744a00c7 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,9 @@ for a quick introduction to Terragrunt. downloading the binary for your OS, renaming it to `terragrunt`, and adding it to your PATH. * See the [Install Terragrunt](#install-terragrunt) docs for other installation options. -1. Go into a folder with your Terraform configurations (`.tf` files) and create a `terragrunt.hcl` file that contains - the configuration for Terragrunt (Terragrunt configuration uses the exact language, HCL, as Terraform). Here's an - example of using Terragrunt to keep your Terraform backend configuration DRY (check out the [Use cases](#use-cases) +1. Go into a folder with your Terraform configurations (`.tf` files) and create a `terragrunt.hcl` or `terragrunt.hcl.json` file that contains + the configuration for Terragrunt (Terragrunt configuration uses the exact language, HCL, as Terraform. [JSON configuration syntax](https://www.terraform.io/docs/configuration/syntax-json.html) is also supported.). Here's an + example of using Terragrunt to keep your Terraform backend configuration DRY (check out the [Use cases](#use-cases) section for other types of configuration Terragrunt supports): ```hcl @@ -35,6 +35,22 @@ for a quick introduction to Terragrunt. region = "us-east-1" encrypt = true dynamodb_table = "my-lock-table" + } + } + ``` + + ```json + # terragrunt.hcl.json example + { + "remote_state":{ + "backend": "s3", + "config":{ + "bucket" : "my-terraform-state", + "key" : "${path_relative_to_include()}/terraform.tfstate", + "region" : "us-east-1", + "encrypt" : true, + "dynamodb_table" : "my-lock-table" + } } } ``` diff --git a/cli/args.go b/cli/args.go index daaa818d62..2e7964feca 100644 --- a/cli/args.go +++ b/cli/args.go @@ -63,7 +63,7 @@ func parseTerragruntOptionsFromArgs(args []string, writer, errWriter io.Writer) return nil, err } if terragruntConfigPath == "" { - terragruntConfigPath = config.DefaultConfigPath(workingDir) + terragruntConfigPath = config.GetDefaultConfigPath(workingDir) } terraformPath, err := parseStringArg(args, OPT_TERRAGRUNT_TFPATH, os.Getenv("TERRAGRUNT_TFPATH")) diff --git a/config/config.go b/config/config.go index 9a7aa0f5c5..1f18e0b499 100644 --- a/config/config.go +++ b/config/config.go @@ -19,6 +19,7 @@ import ( ) const DefaultTerragruntConfigPath = "terragrunt.hcl" +const DefaultTerragruntJsonConfigPath = "terragrunt.hcl.json" // TerragruntConfig represents a parsed and expanded configuration type TerragruntConfig struct { @@ -186,11 +187,25 @@ func (conf *TerraformExtraArguments) String() string { conf.EnvVars) } -// Return the default path to use for the Terragrunt configuration file in the given directory +// Return the default hcl path to use for the Terragrunt configuration file in the given directory func DefaultConfigPath(workingDir string) string { return util.JoinPath(workingDir, DefaultTerragruntConfigPath) } +// Return the default path to use for the Terragrunt Json configuration file in the given directory +func DefaultJsonConfigPath(workingDir string) string { + return util.JoinPath(workingDir, DefaultTerragruntJsonConfigPath) +} + +// Return the default path to use for the Terragrunt configuration that exists within the path giving preference to `terragrunt.hcl` +func GetDefaultConfigPath(workingDir string) string { + if util.FileNotExists(DefaultConfigPath(workingDir)) && util.FileExists(DefaultJsonConfigPath(workingDir)) { + return DefaultJsonConfigPath(workingDir) + } + + return DefaultConfigPath(workingDir) +} + // Returns a list of all Terragrunt config files in the given path or any subfolder of the path. A file is a Terragrunt // config file if it has a name as returned by the DefaultConfigPath method func FindConfigFilesInPath(rootPath string, terragruntOptions *options.TerragruntOptions) ([]string, error) { @@ -207,7 +222,7 @@ func FindConfigFilesInPath(rootPath string, terragruntOptions *options.Terragrun } if isTerragruntModule { - configFiles = append(configFiles, DefaultConfigPath(path)) + configFiles = append(configFiles, GetDefaultConfigPath(path)) } return nil @@ -217,7 +232,7 @@ func FindConfigFilesInPath(rootPath string, terragruntOptions *options.Terragrun } // Returns true if the given path with the given FileInfo contains a Terragrunt module and false otherwise. A path -// contains a Terragrunt module if it contains a Terragrunt configuration file (terragrunt.hcl) and is not a cache +// contains a Terragrunt module if it contains a Terragrunt configuration file (terragrunt.hcl, terragrunt.hcl.json) and is not a cache // or download dir. func containsTerragruntModule(path string, info os.FileInfo, terragruntOptions *options.TerragruntOptions) (bool, error) { if !info.IsDir() { @@ -244,7 +259,7 @@ func containsTerragruntModule(path string, info os.FileInfo, terragruntOptions * return false, err } - return util.FileExists(DefaultConfigPath(path)), nil + return util.FileExists(GetDefaultConfigPath(path)), nil } // Read the Terragrunt config file from its default location @@ -417,6 +432,15 @@ func parseHcl(parser *hclparse.Parser, hcl string, filename string) (file *hcl.F } }() + if filepath.Ext(filename) == ".json" { + file, parseDiagnostics := parser.ParseJSON([]byte(hcl), filename) + if parseDiagnostics != nil && parseDiagnostics.HasErrors() { + return nil, parseDiagnostics + } + + return file, nil + } + file, parseDiagnostics := parser.ParseHCL([]byte(hcl), filename) if parseDiagnostics != nil && parseDiagnostics.HasErrors() { return nil, parseDiagnostics diff --git a/config/config_helpers.go b/config/config_helpers.go index e5860b6cca..1454a5d4f3 100644 --- a/config/config_helpers.go +++ b/config/config_helpers.go @@ -255,7 +255,7 @@ func findInParentFolders(params []string, include *IncludeConfig, terragruntOpti return "", errors.WithStackTrace(ParentFileNotFound{Path: terragruntOptions.TerragruntConfigPath, File: fileToFindStr, Cause: "Traversed all the way to the root"}) } - fileToFind := DefaultConfigPath(currentDir) + fileToFind := GetDefaultConfigPath(currentDir) if fileToFindParam != "" { fileToFind = util.JoinPath(currentDir, fileToFindParam) } diff --git a/config/config_test.go b/config/config_test.go index 60018c9429..a0086331d1 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -35,7 +35,32 @@ remote_state { } } -func TestParseTerragruntConfigRemoteStateMissingBackend(t *testing.T) { +func TestParseTerragruntJsonConfigRemoteStateMinimalConfig(t *testing.T) { + t.Parallel() + + config := ` +{ + "remote_state": { + "backend": "s3", + "config": {} + } +} +` + + terragruntConfig, err := ParseConfigString(config, mockOptionsForTest(t), nil, DefaultTerragruntJsonConfigPath) + require.NoError(t, err) + + assert.Nil(t, terragruntConfig.Terraform) + + assert.Empty(t, terragruntConfig.IamRole) + + if assert.NotNil(t, terragruntConfig.RemoteState) { + assert.Equal(t, "s3", terragruntConfig.RemoteState.Backend) + assert.Empty(t, terragruntConfig.RemoteState.Config) + } +} + +func TestParseTerragruntHclConfigRemoteStateMissingBackend(t *testing.T) { t.Parallel() config := ` @@ -47,7 +72,21 @@ remote_state {} require.Contains(t, err.Error(), "Missing required argument; The argument \"backend\" is required") } -func TestParseTerragruntConfigRemoteStateFullConfig(t *testing.T) { +func TestParseTerragruntJsonConfigRemoteStateMissingBackend(t *testing.T) { + t.Parallel() + + config := ` +{ + "remote_state": {} +} +` + + _, err := ParseConfigString(config, mockOptionsForTest(t), nil, DefaultTerragruntJsonConfigPath) + require.Error(t, err) + require.Contains(t, err.Error(), "Missing required argument; The argument \"backend\" is required") +} + +func TestParseTerragruntHclConfigRemoteStateFullConfig(t *testing.T) { t.Parallel() config := ` @@ -81,6 +120,42 @@ remote_state { } } +func TestParseTerragruntJsonConfigRemoteStateFullConfig(t *testing.T) { + t.Parallel() + + config := ` +{ + "remote_state":{ + "backend":"s3", + "config":{ + "encrypt": true, + "bucket": "my-bucket", + "key": "terraform.tfstate", + "region":"us-east-1" + } + } +} +` + + terragruntConfig, err := ParseConfigString(config, mockOptionsForTest(t), nil, DefaultTerragruntJsonConfigPath) + if err != nil { + t.Fatal(err) + } + + assert.Nil(t, terragruntConfig.Terraform) + + assert.Empty(t, terragruntConfig.IamRole) + + 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, "terraform.tfstate", terragruntConfig.RemoteState.Config["key"]) + assert.Equal(t, "us-east-1", terragruntConfig.RemoteState.Config["region"]) + } +} + func TestParseIamRole(t *testing.T) { t.Parallel() @@ -194,6 +269,54 @@ dependencies { } } +func TestParseTerragruntJsonConfigRemoteStateDynamoDbTerraformConfigAndDependenciesFullConfig(t *testing.T) { + t.Parallel() + + config := ` +{ + "terraform": { + "source": "foo" + }, + "remote_state": { + "backend": "s3", + "config": { + "encrypt": true, + "bucket": "my-bucket", + "key": "terraform.tfstate", + "region": "us-east-1" + } + }, + "dependencies":{ + "paths": ["../vpc", "../mysql", "../backend-app"] + } +} +` + + terragruntConfig, err := ParseConfigString(config, mockOptionsForTest(t), nil, DefaultTerragruntJsonConfigPath) + if err != nil { + t.Fatal(err) + } + + require.NotNil(t, terragruntConfig.Terraform) + require.NotNil(t, terragruntConfig.Terraform.Source) + assert.Equal(t, "foo", *terragruntConfig.Terraform.Source) + + assert.Empty(t, terragruntConfig.IamRole) + + 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, "terraform.tfstate", terragruntConfig.RemoteState.Config["key"]) + assert.Equal(t, "us-east-1", terragruntConfig.RemoteState.Config["region"]) + } + + if assert.NotNil(t, terragruntConfig.Dependencies) { + assert.Equal(t, []string{"../vpc", "../mysql", "../backend-app"}, terragruntConfig.Dependencies.Paths) + } +} + func TestParseTerragruntConfigInclude(t *testing.T) { t.Parallel() @@ -342,6 +465,54 @@ dependencies { assert.Equal(t, []string{"override"}, terragruntConfig.Dependencies.Paths) } +func TestParseTerragruntJsonConfigIncludeOverrideAll(t *testing.T) { + t.Parallel() + + config := + fmt.Sprintf(` +{ + "include":{ + "path": "../../../%s" + }, + "terraform":{ + "source": "foo" + }, + "remote_state":{ + "backend": "s3", + "config":{ + "encrypt": false, + "bucket": "override", + "key": "override", + "region": "override" + } + }, + "dependencies":{ + "paths": ["override"] + } +} +`, DefaultTerragruntConfigPath) + + opts := mockOptionsForTestWithConfigPath(t, "../test/fixture-parent-folders/terragrunt-in-root/child/sub-child/sub-sub-child/"+DefaultTerragruntJsonConfigPath) + + terragruntConfig, err := ParseConfigString(config, opts, nil, opts.TerragruntConfigPath) + require.NoError(t, err, "Unexpected error: %v", errors.PrintErrorWithStackTrace(err)) + + require.NotNil(t, terragruntConfig.Terraform) + require.NotNil(t, terragruntConfig.Terraform.Source) + assert.Equal(t, "foo", *terragruntConfig.Terraform.Source) + + 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, "override", terragruntConfig.RemoteState.Config["bucket"]) + assert.Equal(t, "override", terragruntConfig.RemoteState.Config["key"]) + assert.Equal(t, "override", terragruntConfig.RemoteState.Config["region"]) + } + + assert.Equal(t, []string{"override"}, terragruntConfig.Dependencies.Paths) +} + func TestParseTerragruntConfigTwoLevels(t *testing.T) { t.Parallel() @@ -681,6 +852,62 @@ terraform { } } +func TestParseTerragruntJsonConfigTerraformWithMultipleExtraArguments(t *testing.T) { + t.Parallel() + + config := ` +{ + "terraform":{ + "extra_arguments":{ + "json_output":{ + "arguments": ["-json"], + "commands": ["output"] + }, + "fmt_diff":{ + "arguments": ["-diff=true"], + "commands": ["fmt"] + }, + "required_tfvars":{ + "required_var_files":[ + "file1.tfvars", + "file2.tfvars" + ], + "commands": "${get_terraform_commands_that_need_vars()}" + }, + "optional_tfvars":{ + "optional_var_files":[ + "opt1.tfvars", + "opt2.tfvars" + ], + "commands": "${get_terraform_commands_that_need_vars()}" + } + } + } +} +` + + terragruntConfig, err := ParseConfigString(config, mockOptionsForTest(t), nil, DefaultTerragruntJsonConfigPath) + require.NoError(t, err) + + assert.Nil(t, terragruntConfig.RemoteState) + assert.Nil(t, terragruntConfig.Dependencies) + + if assert.NotNil(t, terragruntConfig.Terraform) { + assert.Equal(t, "json_output", terragruntConfig.Terraform.ExtraArgs[0].Name) + assert.Equal(t, &[]string{"-json"}, terragruntConfig.Terraform.ExtraArgs[0].Arguments) + assert.Equal(t, []string{"output"}, terragruntConfig.Terraform.ExtraArgs[0].Commands) + assert.Equal(t, "fmt_diff", terragruntConfig.Terraform.ExtraArgs[1].Name) + assert.Equal(t, &[]string{"-diff=true"}, terragruntConfig.Terraform.ExtraArgs[1].Arguments) + assert.Equal(t, []string{"fmt"}, terragruntConfig.Terraform.ExtraArgs[1].Commands) + assert.Equal(t, "required_tfvars", terragruntConfig.Terraform.ExtraArgs[2].Name) + assert.Equal(t, &[]string{"file1.tfvars", "file2.tfvars"}, terragruntConfig.Terraform.ExtraArgs[2].RequiredVarFiles) + assert.Equal(t, TERRAFORM_COMMANDS_NEED_VARS, terragruntConfig.Terraform.ExtraArgs[2].Commands) + assert.Equal(t, "optional_tfvars", terragruntConfig.Terraform.ExtraArgs[3].Name) + assert.Equal(t, &[]string{"opt1.tfvars", "opt2.tfvars"}, terragruntConfig.Terraform.ExtraArgs[3].OptionalVarFiles) + assert.Equal(t, TERRAFORM_COMMANDS_NEED_VARS, terragruntConfig.Terraform.ExtraArgs[3].Commands) + } +} + func TestFindConfigFilesInPathNone(t *testing.T) { t.Parallel() @@ -707,6 +934,19 @@ func TestFindConfigFilesInPathOneConfig(t *testing.T) { assert.Equal(t, expected, actual) } +func TestFindConfigFilesInPathOneJsonConfig(t *testing.T) { + t.Parallel() + + expected := []string{"../test/fixture-config-files/one-json-config/subdir/terragrunt.hcl.json"} + terragruntOptions, err := options.NewTerragruntOptionsForTest("test") + require.NoError(t, err) + + actual, err := FindConfigFilesInPath("../test/fixture-config-files/one-json-config", terragruntOptions) + + assert.Nil(t, err, "Unexpected error: %v", err) + assert.Equal(t, expected, actual) +} + func TestFindConfigFilesInPathMultipleConfigs(t *testing.T) { t.Parallel() @@ -724,6 +964,40 @@ func TestFindConfigFilesInPathMultipleConfigs(t *testing.T) { assert.Equal(t, expected, actual) } +func TestFindConfigFilesInPathMultipleJsonConfigs(t *testing.T) { + t.Parallel() + + expected := []string{ + "../test/fixture-config-files/multiple-json-configs/terragrunt.hcl.json", + "../test/fixture-config-files/multiple-json-configs/subdir-2/subdir/terragrunt.hcl.json", + "../test/fixture-config-files/multiple-json-configs/subdir-3/terragrunt.hcl.json", + } + terragruntOptions, err := options.NewTerragruntOptionsForTest("test") + require.NoError(t, err) + + actual, err := FindConfigFilesInPath("../test/fixture-config-files/multiple-json-configs", terragruntOptions) + + assert.Nil(t, err, "Unexpected error: %v", err) + assert.Equal(t, expected, actual) +} + +func TestFindConfigFilesInPathMultipleMixedConfigs(t *testing.T) { + t.Parallel() + + expected := []string{ + "../test/fixture-config-files/multiple-mixed-configs/terragrunt.hcl.json", + "../test/fixture-config-files/multiple-mixed-configs/subdir-2/subdir/terragrunt.hcl", + "../test/fixture-config-files/multiple-mixed-configs/subdir-3/terragrunt.hcl.json", + } + terragruntOptions, err := options.NewTerragruntOptionsForTest("test") + require.NoError(t, err) + + actual, err := FindConfigFilesInPath("../test/fixture-config-files/multiple-mixed-configs", terragruntOptions) + + assert.Nil(t, err, "Unexpected error: %v", err) + assert.Equal(t, expected, actual) +} + func TestFindConfigFilesIgnoresTerragruntCache(t *testing.T) { t.Parallel() diff --git a/configstack/module.go b/configstack/module.go index 000dd9d38d..0bb174c3bb 100644 --- a/configstack/module.go +++ b/configstack/module.go @@ -421,7 +421,7 @@ func resolveExternalDependenciesForModule(module *TerraformModule, moduleMap map return map[string]*TerraformModule{}, err } - terragruntConfigPath := config.DefaultConfigPath(dependencyPath) + terragruntConfigPath := config.GetDefaultConfigPath(dependencyPath) if _, alreadyContainsModule := moduleMap[dependencyPath]; !alreadyContainsModule { externalTerragruntConfigPaths = append(externalTerragruntConfigPaths, terragruntConfigPath) } diff --git a/configstack/module_test.go b/configstack/module_test.go index 646bbdf707..4f9c41e13d 100644 --- a/configstack/module_test.go +++ b/configstack/module_test.go @@ -2,10 +2,11 @@ package configstack import ( "fmt" - "github.com/stretchr/testify/require" "os" "testing" + "github.com/stretchr/testify/require" + "github.com/gruntwork-io/terragrunt/config" "github.com/gruntwork-io/terragrunt/errors" "github.com/gruntwork-io/terragrunt/options" @@ -43,6 +44,24 @@ func TestResolveTerraformModulesOneModuleNoDependencies(t *testing.T) { assertModuleListsEqual(t, expected, actualModules) } +func TestResolveTerraformModulesOneJsonModuleNoDependencies(t *testing.T) { + t.Parallel() + + moduleA := &TerraformModule{ + Path: canonical(t, "../test/fixture-modules/json-module-a"), + Dependencies: []*TerraformModule{}, + Config: config.TerragruntConfig{Terraform: &config.TerraformConfig{Source: ptr("test")}, IsPartial: true}, + TerragruntOptions: mockOptions.Clone(canonical(t, "../test/fixture-modules/json-module-a/"+config.DefaultTerragruntJsonConfigPath)), + } + + configPaths := []string{"../test/fixture-modules/json-module-a/" + config.DefaultTerragruntJsonConfigPath} + expected := []*TerraformModule{moduleA} + + actualModules, actualErr := ResolveTerraformModules(configPaths, mockOptions, mockHowThesePathsWereFound) + assert.Nil(t, actualErr, "Unexpected error: %v", actualErr) + assertModuleListsEqual(t, expected, actualModules) +} + func TestResolveTerraformModulesOneModuleWithIncludesNoDependencies(t *testing.T) { t.Parallel() @@ -64,6 +83,48 @@ func TestResolveTerraformModulesOneModuleWithIncludesNoDependencies(t *testing.T assertModuleListsEqual(t, expected, actualModules) } +func TestResolveTerraformModulesOneJsonModuleWithIncludesNoDependencies(t *testing.T) { + t.Parallel() + + moduleB := &TerraformModule{ + Path: canonical(t, "../test/fixture-modules/json-module-b/module-b-child"), + Dependencies: []*TerraformModule{}, + Config: config.TerragruntConfig{ + Terraform: &config.TerraformConfig{Source: ptr("...")}, + IsPartial: true, + }, + TerragruntOptions: mockOptions.Clone(canonical(t, "../test/fixture-modules/json-module-b/module-b-child/"+config.DefaultTerragruntJsonConfigPath)), + } + + configPaths := []string{"../test/fixture-modules/json-module-b/module-b-child/" + config.DefaultTerragruntJsonConfigPath} + expected := []*TerraformModule{moduleB} + + actualModules, actualErr := ResolveTerraformModules(configPaths, mockOptions, mockHowThesePathsWereFound) + assert.Nil(t, actualErr, "Unexpected error: %v", actualErr) + assertModuleListsEqual(t, expected, actualModules) +} + +func TestResolveTerraformModulesOneHclModuleWithIncludesNoDependencies(t *testing.T) { + t.Parallel() + + moduleB := &TerraformModule{ + Path: canonical(t, "../test/fixture-modules/hcl-module-b/module-b-child"), + Dependencies: []*TerraformModule{}, + Config: config.TerragruntConfig{ + Terraform: &config.TerraformConfig{Source: ptr("...")}, + IsPartial: true, + }, + TerragruntOptions: mockOptions.Clone(canonical(t, "../test/fixture-modules/hcl-module-b/module-b-child/"+config.DefaultTerragruntConfigPath)), + } + + configPaths := []string{"../test/fixture-modules/hcl-module-b/module-b-child/" + config.DefaultTerragruntConfigPath} + expected := []*TerraformModule{moduleB} + + actualModules, actualErr := ResolveTerraformModules(configPaths, mockOptions, mockHowThesePathsWereFound) + assert.Nil(t, actualErr, "Unexpected error: %v", actualErr) + assertModuleListsEqual(t, expected, actualModules) +} + func TestResolveTerraformModulesTwoModulesWithDependencies(t *testing.T) { t.Parallel() @@ -96,6 +157,70 @@ func TestResolveTerraformModulesTwoModulesWithDependencies(t *testing.T) { assertModuleListsEqual(t, expected, actualModules) } +func TestResolveTerraformModulesJsonModulesWithHclDependencies(t *testing.T) { + t.Parallel() + + moduleA := &TerraformModule{ + Path: canonical(t, "../test/fixture-modules/module-a"), + Dependencies: []*TerraformModule{}, + Config: config.TerragruntConfig{ + Terraform: &config.TerraformConfig{Source: ptr("test")}, + IsPartial: true, + }, + TerragruntOptions: mockOptions.Clone(canonical(t, "../test/fixture-modules/module-a/"+config.DefaultTerragruntConfigPath)), + } + + moduleC := &TerraformModule{ + Path: canonical(t, "../test/fixture-modules/json-module-c"), + Dependencies: []*TerraformModule{moduleA}, + Config: config.TerragruntConfig{ + Dependencies: &config.ModuleDependencies{Paths: []string{"../module-a"}}, + Terraform: &config.TerraformConfig{Source: ptr("temp")}, + IsPartial: true, + }, + TerragruntOptions: mockOptions.Clone(canonical(t, "../test/fixture-modules/json-module-c/"+config.DefaultTerragruntJsonConfigPath)), + } + + configPaths := []string{"../test/fixture-modules/module-a/" + config.DefaultTerragruntConfigPath, "../test/fixture-modules/json-module-c/" + config.DefaultTerragruntJsonConfigPath} + expected := []*TerraformModule{moduleA, moduleC} + + actualModules, actualErr := ResolveTerraformModules(configPaths, mockOptions, mockHowThesePathsWereFound) + assert.Nil(t, actualErr, "Unexpected error: %v", actualErr) + assertModuleListsEqual(t, expected, actualModules) +} + +func TestResolveTerraformModulesHclModulesWithJsonDependencies(t *testing.T) { + t.Parallel() + + moduleA := &TerraformModule{ + Path: canonical(t, "../test/fixture-modules/json-module-a"), + Dependencies: []*TerraformModule{}, + Config: config.TerragruntConfig{ + Terraform: &config.TerraformConfig{Source: ptr("test")}, + IsPartial: true, + }, + TerragruntOptions: mockOptions.Clone(canonical(t, "../test/fixture-modules/json-module-a/"+config.DefaultTerragruntJsonConfigPath)), + } + + moduleC := &TerraformModule{ + Path: canonical(t, "../test/fixture-modules/hcl-module-c"), + Dependencies: []*TerraformModule{moduleA}, + Config: config.TerragruntConfig{ + Dependencies: &config.ModuleDependencies{Paths: []string{"../json-module-a"}}, + Terraform: &config.TerraformConfig{Source: ptr("temp")}, + IsPartial: true, + }, + TerragruntOptions: mockOptions.Clone(canonical(t, "../test/fixture-modules/hcl-module-c/"+config.DefaultTerragruntConfigPath)), + } + + configPaths := []string{"../test/fixture-modules/json-module-a/" + config.DefaultTerragruntJsonConfigPath, "../test/fixture-modules/hcl-module-c/" + config.DefaultTerragruntConfigPath} + expected := []*TerraformModule{moduleA, moduleC} + + actualModules, actualErr := ResolveTerraformModules(configPaths, mockOptions, mockHowThesePathsWereFound) + assert.Nil(t, actualErr, "Unexpected error: %v", actualErr) + assertModuleListsEqual(t, expected, actualModules) +} + func TestResolveTerraformModulesTwoModulesWithDependenciesExcludedDirsWithDependency(t *testing.T) { t.Parallel() @@ -162,7 +287,6 @@ func TestResolveTerraformModulesTwoModulesWithDependenciesExcludedDirsWithDepend TerragruntOptions: opts.Clone(canonical(t, "../test/fixture-modules/module-c/"+config.DefaultTerragruntConfigPath)), } - moduleAbba := &TerraformModule{ Path: canonical(t, "../test/fixture-modules/module-abba"), Dependencies: []*TerraformModule{moduleA}, @@ -213,7 +337,6 @@ func TestResolveTerraformModulesTwoModulesWithDependenciesExcludedDirsWithDepend TerragruntOptions: opts.Clone(canonical(t, "../test/fixture-modules/module-c/"+config.DefaultTerragruntConfigPath)), } - moduleAbba := &TerraformModule{ Path: canonical(t, "../test/fixture-modules/module-abba"), Dependencies: []*TerraformModule{moduleA}, @@ -452,6 +575,55 @@ func TestResolveTerraformModulesMultipleModulesWithDependencies(t *testing.T) { assertModuleListsEqual(t, expected, actualModules) } +func TestResolveTerraformModulesMultipleModulesWithMixedDependencies(t *testing.T) { + t.Parallel() + + moduleA := &TerraformModule{ + Path: canonical(t, "../test/fixture-modules/module-a"), + Dependencies: []*TerraformModule{}, + Config: config.TerragruntConfig{Terraform: &config.TerraformConfig{Source: ptr("test")}, IsPartial: true}, + TerragruntOptions: mockOptions.Clone(canonical(t, "../test/fixture-modules/module-a/"+config.DefaultTerragruntConfigPath)), + } + + moduleB := &TerraformModule{ + Path: canonical(t, "../test/fixture-modules/json-module-b/module-b-child"), + Dependencies: []*TerraformModule{}, + Config: config.TerragruntConfig{ + Terraform: &config.TerraformConfig{Source: ptr("...")}, + IsPartial: true, + }, + TerragruntOptions: mockOptions.Clone(canonical(t, "../test/fixture-modules/json-module-b/module-b-child/"+config.DefaultTerragruntJsonConfigPath)), + } + + moduleC := &TerraformModule{ + Path: canonical(t, "../test/fixture-modules/module-c"), + Dependencies: []*TerraformModule{moduleA}, + Config: config.TerragruntConfig{ + Dependencies: &config.ModuleDependencies{Paths: []string{"../module-a"}}, + Terraform: &config.TerraformConfig{Source: ptr("temp")}, + IsPartial: true, + }, + TerragruntOptions: mockOptions.Clone(canonical(t, "../test/fixture-modules/module-c/"+config.DefaultTerragruntConfigPath)), + } + + moduleD := &TerraformModule{ + Path: canonical(t, "../test/fixture-modules/json-module-d"), + Dependencies: []*TerraformModule{moduleA, moduleB, moduleC}, + Config: config.TerragruntConfig{ + Dependencies: &config.ModuleDependencies{Paths: []string{"../module-a", "../json-module-b/module-b-child", "../module-c"}}, + IsPartial: true, + }, + TerragruntOptions: mockOptions.Clone(canonical(t, "../test/fixture-modules/json-module-d/"+config.DefaultTerragruntJsonConfigPath)), + } + + configPaths := []string{"../test/fixture-modules/module-a/" + config.DefaultTerragruntConfigPath, "../test/fixture-modules/json-module-b/module-b-child/" + config.DefaultTerragruntJsonConfigPath, "../test/fixture-modules/module-c/" + config.DefaultTerragruntConfigPath, "../test/fixture-modules/json-module-d/" + config.DefaultTerragruntJsonConfigPath} + expected := []*TerraformModule{moduleA, moduleB, moduleC, moduleD} + + actualModules, actualErr := ResolveTerraformModules(configPaths, mockOptions, mockHowThesePathsWereFound) + assert.Nil(t, actualErr, "Unexpected error: %v", actualErr) + assertModuleListsEqual(t, expected, actualModules) +} + func TestResolveTerraformModulesMultipleModulesWithDependenciesWithIncludes(t *testing.T) { t.Parallel() diff --git a/test/fixture-config-files/multiple-json-configs/subdir-1/empty.txt b/test/fixture-config-files/multiple-json-configs/subdir-1/empty.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixture-config-files/multiple-json-configs/subdir-2/subdir/terragrunt.hcl.json b/test/fixture-config-files/multiple-json-configs/subdir-2/subdir/terragrunt.hcl.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/test/fixture-config-files/multiple-json-configs/subdir-2/subdir/terragrunt.hcl.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/fixture-config-files/multiple-json-configs/subdir-3/terragrunt.hcl.json b/test/fixture-config-files/multiple-json-configs/subdir-3/terragrunt.hcl.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/test/fixture-config-files/multiple-json-configs/subdir-3/terragrunt.hcl.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/fixture-config-files/multiple-json-configs/terragrunt.hcl.json b/test/fixture-config-files/multiple-json-configs/terragrunt.hcl.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/test/fixture-config-files/multiple-json-configs/terragrunt.hcl.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/fixture-config-files/multiple-mixed-configs/subdir-1/empty.txt b/test/fixture-config-files/multiple-mixed-configs/subdir-1/empty.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixture-config-files/multiple-mixed-configs/subdir-2/subdir/terragrunt.hcl b/test/fixture-config-files/multiple-mixed-configs/subdir-2/subdir/terragrunt.hcl new file mode 100644 index 0000000000..48ddb6fcc9 --- /dev/null +++ b/test/fixture-config-files/multiple-mixed-configs/subdir-2/subdir/terragrunt.hcl @@ -0,0 +1 @@ +# Intentionally empty \ No newline at end of file diff --git a/test/fixture-config-files/multiple-mixed-configs/subdir-3/terragrunt.hcl.json b/test/fixture-config-files/multiple-mixed-configs/subdir-3/terragrunt.hcl.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/test/fixture-config-files/multiple-mixed-configs/subdir-3/terragrunt.hcl.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/fixture-config-files/multiple-mixed-configs/terragrunt.hcl.json b/test/fixture-config-files/multiple-mixed-configs/terragrunt.hcl.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/test/fixture-config-files/multiple-mixed-configs/terragrunt.hcl.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/fixture-config-files/one-json-config/empty.txt b/test/fixture-config-files/one-json-config/empty.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixture-config-files/one-json-config/subdir/terragrunt.hcl.json b/test/fixture-config-files/one-json-config/subdir/terragrunt.hcl.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/test/fixture-config-files/one-json-config/subdir/terragrunt.hcl.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/fixture-config-files/single-json-config/main.tf b/test/fixture-config-files/single-json-config/main.tf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixture-config-files/single-json-config/terragrunt.hcl.json b/test/fixture-config-files/single-json-config/terragrunt.hcl.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/test/fixture-config-files/single-json-config/terragrunt.hcl.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/fixture-modules/hcl-module-b/module-b-child/main.tf b/test/fixture-modules/hcl-module-b/module-b-child/main.tf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixture-modules/hcl-module-b/module-b-child/terragrunt.hcl b/test/fixture-modules/hcl-module-b/module-b-child/terragrunt.hcl new file mode 100644 index 0000000000..3e1f65a238 --- /dev/null +++ b/test/fixture-modules/hcl-module-b/module-b-child/terragrunt.hcl @@ -0,0 +1,3 @@ +include { + path = find_in_parent_folders() +} diff --git a/test/fixture-modules/hcl-module-b/terragrunt.hcl.json b/test/fixture-modules/hcl-module-b/terragrunt.hcl.json new file mode 100644 index 0000000000..72d3c9a522 --- /dev/null +++ b/test/fixture-modules/hcl-module-b/terragrunt.hcl.json @@ -0,0 +1,12 @@ +{ + "remote_state": { + "backend": "s3", + "config": { + "bucket": "bucket", + "key": "${path_relative_to_include()}/terraform.tfstate" + } + }, + "terraform": { + "source": "..." + } +} \ No newline at end of file diff --git a/test/fixture-modules/hcl-module-c/terragrunt.hcl b/test/fixture-modules/hcl-module-c/terragrunt.hcl new file mode 100644 index 0000000000..0b2e16d99f --- /dev/null +++ b/test/fixture-modules/hcl-module-c/terragrunt.hcl @@ -0,0 +1,7 @@ +terraform { + source = "temp" +} + +dependencies { + paths = ["../json-module-a"] +} diff --git a/test/fixture-modules/json-module-a/terragrunt.hcl.json b/test/fixture-modules/json-module-a/terragrunt.hcl.json new file mode 100644 index 0000000000..98a08c8a60 --- /dev/null +++ b/test/fixture-modules/json-module-a/terragrunt.hcl.json @@ -0,0 +1,5 @@ +{ + "terraform": { + "source": "test" + } +} \ No newline at end of file diff --git a/test/fixture-modules/json-module-b/module-b-child/main.tf b/test/fixture-modules/json-module-b/module-b-child/main.tf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixture-modules/json-module-b/module-b-child/terragrunt.hcl.json b/test/fixture-modules/json-module-b/module-b-child/terragrunt.hcl.json new file mode 100644 index 0000000000..5a5b6b786e --- /dev/null +++ b/test/fixture-modules/json-module-b/module-b-child/terragrunt.hcl.json @@ -0,0 +1,5 @@ +{ + "include": { + "path": "${find_in_parent_folders()}" + } +} \ No newline at end of file diff --git a/test/fixture-modules/json-module-b/terragrunt.hcl b/test/fixture-modules/json-module-b/terragrunt.hcl new file mode 100644 index 0000000000..d8ee52aa00 --- /dev/null +++ b/test/fixture-modules/json-module-b/terragrunt.hcl @@ -0,0 +1,11 @@ +# Configure Terragrunt to automatically store tfstate files in an S3 bucket +remote_state { + backend = "s3" + config = { + bucket = "bucket" + key = "${path_relative_to_include()}/terraform.tfstate" + } +} +terraform { + source = "..." +} diff --git a/test/fixture-modules/json-module-c/terragrunt.hcl.json b/test/fixture-modules/json-module-c/terragrunt.hcl.json new file mode 100644 index 0000000000..2586adb521 --- /dev/null +++ b/test/fixture-modules/json-module-c/terragrunt.hcl.json @@ -0,0 +1,10 @@ +{ + "terraform": { + "source": "temp" + }, + "dependencies": { + "paths": [ + "../module-a" + ] + } +} \ No newline at end of file diff --git a/test/fixture-modules/json-module-d/main.tf b/test/fixture-modules/json-module-d/main.tf new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/test/fixture-modules/json-module-d/main.tf @@ -0,0 +1 @@ + diff --git a/test/fixture-modules/json-module-d/terragrunt.hcl.json b/test/fixture-modules/json-module-d/terragrunt.hcl.json new file mode 100644 index 0000000000..6bcbf8a30e --- /dev/null +++ b/test/fixture-modules/json-module-d/terragrunt.hcl.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "paths": [ + "../module-a", + "../json-module-b/module-b-child", + "../module-c" + ] + } +} \ No newline at end of file diff --git a/test/integration_test.go b/test/integration_test.go index 5515bb468f..641e256c62 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -52,6 +52,7 @@ const ( TEST_FIXTURE_EXTRA_ARGS_PATH = "fixture-extra-args/" TEST_FIXTURE_ENV_VARS_BLOCK_PATH = "fixture-env-vars-block/" TEST_FIXTURE_SKIP = "fixture-skip/" + TEST_FIXTURE_CONFIG_SINGLE_JSON_PATH = "fixture-config-files/single-json-config" TEST_FIXTURE_LOCAL_DOWNLOAD_PATH = "fixture-download/local" TEST_FIXTURE_REMOTE_DOWNLOAD_PATH = "fixture-download/remote" TEST_FIXTURE_OVERRIDE_DOWNLOAD_PATH = "fixture-download/override" @@ -605,6 +606,17 @@ func TestTerragruntWorksWithIncludes(t *testing.T) { runTerragrunt(t, fmt.Sprintf("terragrunt apply --terragrunt-non-interactive --terragrunt-config %s --terragrunt-working-dir %s", tmpTerragruntConfigPath, childPath)) } +func TestTerragruntWorksWithSingleJsonConfig(t *testing.T) { + t.Parallel() + + cleanupTerraformFolder(t, TEST_FIXTURE_CONFIG_SINGLE_JSON_PATH) + tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_CONFIG_SINGLE_JSON_PATH) + + rootTerragruntConfigPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_CONFIG_SINGLE_JSON_PATH) + + runTerragrunt(t, fmt.Sprintf("terragrunt plan --terragrunt-non-interactive --terragrunt-working-dir %s", rootTerragruntConfigPath)) +} + func TestTerragruntReportsTerraformErrorsWithPlanAll(t *testing.T) { cleanupTerraformFolder(t, TEST_FIXTURE_FAILED_TERRAFORM) diff --git a/util/file.go b/util/file.go index 9905420962..a2ac80b0cb 100644 --- a/util/file.go +++ b/util/file.go @@ -21,6 +21,12 @@ func FileExists(path string) bool { return err == nil } +// Return true if the given file does not exist +func FileNotExists(path string) bool { + _, err := os.Stat(path) + return os.IsNotExist(err) +} + // Return the canonical version of the given path, relative to the given base path. That is, if the given path is a // relative path, assume it is relative to the given base path. A canonical path is an absolute path with all relative // components (e.g. "../") fully resolved, which makes it safe to compare paths as strings.