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

Terraform 0.12 support #731

Merged
merged 58 commits into from
Jun 10, 2019
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
42fa65c
Switch to HCL2 parser
brikis98 Jun 6, 2019
6466cd5
Update all the test fixtures
brikis98 Jun 6, 2019
5222aab
Update all the tests
brikis98 Jun 6, 2019
b42fd86
Fix compile error and bool check
brikis98 Jun 6, 2019
0e3ddc0
Fix configstack test
brikis98 Jun 6, 2019
2d35949
Fix dependencies syntax
brikis98 Jun 6, 2019
73bb229
Fix a few more configstack tests
brikis98 Jun 6, 2019
1bfa50f
Fix terraform block syntax
brikis98 Jun 6, 2019
77ce597
Attr should be called inputs
brikis98 Jun 6, 2019
50cfb11
Remove missing references to terraform.tfvars
brikis98 Jun 6, 2019
ab495c9
Fix handling of tags
brikis98 Jun 6, 2019
e88e106
Fix include syntax
brikis98 Jun 6, 2019
b2b7f4b
Set inputs as env vars
brikis98 Jun 6, 2019
0611d5a
Run make fmt
brikis98 Jun 6, 2019
477ac27
Fix string handling. Add integration test case for input handling.
brikis98 Jun 6, 2019
08402c1
Update docs
brikis98 Jun 6, 2019
1faf766
Document inputs
brikis98 Jun 6, 2019
9fe446c
Rename get_tfvars_dir and get_parent_tfvars_dir
brikis98 Jun 6, 2019
722b6f2
Add migration guide
brikis98 Jun 6, 2019
ae9e4b1
Merge branch 'master' into tf12
brikis98 Jun 6, 2019
02b0eb2
Remove TF 0.12 note
brikis98 Jun 6, 2019
78cb92f
Update Terraform version check
brikis98 Jun 6, 2019
9857bbb
Update to new circle image with Terraform 0.12 on it
brikis98 Jun 7, 2019
fdce37c
Don't override env vars user has already set
brikis98 Jun 7, 2019
993e195
Add tests for setTerragruntInputsAsEnvVars
brikis98 Jun 7, 2019
47391c6
Wrap example in inputs attr
brikis98 Jun 7, 2019
9f19fd9
Add comments to explain testCase copying
brikis98 Jun 7, 2019
b00c3f9
Fix terraform init -from-module behavior
brikis98 Jun 7, 2019
9c6c2f2
Apply suggestions from code review
brikis98 Jun 7, 2019
dc5f0aa
Test get_env helper in inputs block
brikis98 Jun 7, 2019
249662e
Proper indentation
brikis98 Jun 7, 2019
e951bc2
Fix download dir deletion/creation
brikis98 Jun 7, 2019
07a4c49
Update all Terraform test fixtures to 0.12 syntax
brikis98 Jun 7, 2019
80ecfba
Work around Terraform issue with 'init -get=false'
brikis98 Jun 7, 2019
78792ed
Re-order init args
brikis98 Jun 8, 2019
b5a4d20
Switch to go-getter for downloading source
brikis98 Jun 8, 2019
fb91862
Fix config block in terraform_remote_state
brikis98 Jun 8, 2019
ea56319
Fix how we pass S3 bucket name to config stacks
brikis98 Jun 8, 2019
72d46bb
Execute hooks around download source
brikis98 Jun 8, 2019
76b432c
Update README on how we download
brikis98 Jun 8, 2019
837fb4f
Fix state equality check
brikis98 Jun 8, 2019
8d76430
Don't automatically apply external dependencies in non-interactive mode
brikis98 Jun 8, 2019
2d79cde
Fix configstack tests
brikis98 Jun 8, 2019
8d8dcc7
Skip external dependencies test
brikis98 Jun 8, 2019
65741db
Put tmp dir into configured download dir
brikis98 Jun 8, 2019
b0ca9e4
Fix tmp dir location
brikis98 Jun 9, 2019
cc241e8
Try CopyFolderContents implementation based on filepath.Glob
brikis98 Jun 9, 2019
bb61c8d
Exztract UniqueId helper
brikis98 Jun 9, 2019
e239808
Use UniqueId in temp folder creation. Defer temp folder deletion.
brikis98 Jun 9, 2019
58556e0
Try to add sleep on S3 bucket creation
brikis98 Jun 9, 2019
68f6a1b
Increase sleep
brikis98 Jun 9, 2019
71b72e1
Replace filepath.Walk in tests
brikis98 Jun 9, 2019
f9c08ed
Fix dest dir in copyfoldercontents
brikis98 Jun 9, 2019
31fa15b
Create parent dir before copying
brikis98 Jun 9, 2019
866e689
Remove deprecated S3 settings
brikis98 Jun 9, 2019
a531167
Slightly clearer logging. Remove sleep as it doesn't help.
brikis98 Jun 10, 2019
2aa4573
Do S3 bucket creation in retry loop
brikis98 Jun 10, 2019
264812d
Skip symlinks
brikis98 Jun 10, 2019
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
15 changes: 7 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ When Terragrunt finds the `terraform` block with a `source` parameter in `live/s

1. Download the configurations specified via the `source` parameter into the `--terragrunt-download-dir` folder (by
default `.terragrunt-cache` in the working directory, which we recommend adding to `.gitignore`). This downloading
is done by using the [terraform init command](https://www.terraform.io/docs/commands/init.html), so the `source`
is done by using the same [go-getter library](https://github.com/hashicorp/go-getter) Terraform uses, so the `source`
parameter supports the exact same syntax as the [module source](https://www.terraform.io/docs/modules/sources.html)
parameter, including local file paths, Git URLs, and Git URLs with `ref` parameters (useful for checking out a
specific tag, commit, or branch of Git repo). Terragrunt will download all the code in the repo (i.e. the part
Expand Down Expand Up @@ -1741,13 +1741,12 @@ Hooks support the following arguments:
* `execute` (required): the shell command to execute.
* `run_on_error` (optional): if set to true, this hook will run even if a previous hook hit an error, or in the case of
"after" hooks, if the Terraform command hit an error. Default is false.
* `init_from_module` vs `init`: This is not an argument, but a special name for a hook that runs on `terraform init`.
Terragrunt uses `terraform init` in two different ways: one is to download
[remote configurations](#keep-your-terraform-code-dry) using `terraform init -from-module`; the other is to as part
of [Auto-Init](#auto-init), which includes configuring the backend, retrieving provider plugins, and remote modules
specified within the root module. If you wish to execute a hook when Terragrunt is using `terraform init` to download
remote configurations, name the hook `init_from_module`. If you wish to execute a hook when Terragrunt is using
`terraform init` for Auto-Init, name the hook `init`.
* `init_from_module` and `init`: This is not an argument, but a special name you can use for hooks that run during
initialization. There are two stages of initialization: one is to download
[remote configurations](#keep-your-terraform-code-dry) using `go-getter`; the other is [Auto-Init](#auto-init), which
configures the backend and downloads provider plugins and modules. If you wish to execute a hook when Terragrunt is
using `go-getter` to download remote configurations, name the hook `init_from_module`. If you wish to execute a hook
when Terragrunt is using `terraform init` for Auto-Init, name the hook `init`.



Expand Down
125 changes: 21 additions & 104 deletions cli/cli_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"io"
"os"
"regexp"
"strings"
"time"
Expand Down Expand Up @@ -401,11 +400,26 @@ func runTerragruntWithConfig(terragruntOptions *options.TerragruntOptions, terra
return err
}

return runActionWithHooks("terraform", terragruntOptions, terragruntConfig, func() error {
return runTerraformWithRetry(terragruntOptions)
})
}

// Run the given action function surrounded by hooks. That is, run the before hooks first, then, if there were no
// errors, run the action, and finally, run the after hooks. Return any errors hit from the hooks or action.
func runActionWithHooks(description string, terragruntOptions *options.TerragruntOptions, terragruntConfig *config.TerragruntConfig, action func() error) error {
beforeHookErrors := processHooks(terragruntConfig.Terraform.GetBeforeHooks(), terragruntOptions)
terraformError := runTerraformCommandIfNoErrors(beforeHookErrors, terragruntOptions)
postHookErrors := processHooks(terragruntConfig.Terraform.GetAfterHooks(), terragruntOptions, beforeHookErrors, terraformError)

return errors.NewMultiError(beforeHookErrors, terraformError, postHookErrors)
var actionErrors error
if beforeHookErrors == nil {
actionErrors = action()
} else {
terragruntOptions.Logger.Printf("Errors encountered running before_hooks. Not running '%s'.", description)
}

postHookErrors := processHooks(terragruntConfig.Terraform.GetAfterHooks(), terragruntOptions, beforeHookErrors, actionErrors)

return errors.NewMultiError(beforeHookErrors, actionErrors, postHookErrors)
}

// The Terragrunt configuration can contain a set of inputs to pass to Terraform as environment variables. This method
Expand All @@ -430,35 +444,6 @@ func setTerragruntInputsAsEnvVars(terragruntOptions *options.TerragruntOptions,
return nil
}

var moduleNotFoundErr = regexp.MustCompile(`Error loading modules: module .+?: not found, may need to run 'terraform init'`)

func runTerraformCommandIfNoErrors(possibleErrors error, terragruntOptions *options.TerragruntOptions) error {
if possibleErrors != nil {
terragruntOptions.Logger.Println("Errors encountered running before_hooks. Not running terraform.")
return nil
}

// Workaround for https://github.com/hashicorp/terraform/issues/18460. Calling 'terraform init -get=false '
// sometimes results in Terraform trying to download/validate modules anyway, so we need to ignore that error.
if terragruntOptions.TerraformCommand == CMD_INIT_FROM_MODULE {
// Redirect all log output to stderr to make sure we don't pollute stdout with this extra call to 'init'
terragruntOptionsCopy := terragruntOptions.Clone(terragruntOptions.TerragruntConfigPath)
terragruntOptionsCopy.Writer = terragruntOptionsCopy.ErrWriter

out, err := shell.RunTerraformCommandWithOutput(terragruntOptionsCopy, terragruntOptionsCopy.TerraformCliArgs...)

// If we got an error and the error output included this error message, ignore the error and keep going
if err != nil && (len(moduleNotFoundErr.FindStringSubmatch(out.Stderr)) > 0 || strings.Contains(out.Stderr, "Missing required providers.")) {
terragruntOptions.Logger.Println("Ignoring error from call to init, as this is a known Terraform bug: https://github.com/hashicorp/terraform/issues/18460")
return nil
}

return err
}

return runTerraformWithRetry(terragruntOptions)
}

func runTerraformWithRetry(terragruntOptions *options.TerragruntOptions) error {
// Retry the command configurable time with sleep in between
for i := 0; i < terragruntOptions.MaxRetryAttempts; i++ {
Expand All @@ -477,20 +462,9 @@ func runTerraformWithRetry(terragruntOptions *options.TerragruntOptions) error {
return errors.WithStackTrace(MaxRetriesExceeded{terragruntOptions})
}

// Prepare for running 'terraform init' by
// preventing users from passing source download arguments,
// initializing remote state storage, and
// adding backend configuration arguments to the TerraformCliArgs
// Prepare for running 'terraform init' by initializing remote state storage and adding backend configuration arguments
// to the TerraformCliArgs
func prepareInitCommand(terragruntOptions *options.TerragruntOptions, terragruntConfig *config.TerragruntConfig, allowSourceDownload bool) error {

// Do not allow the user to specify the source or DIR arguments
// on the command line or as part of extra_arguments
//
// However, allow download_source.go to specify the source and DIR arguments.
if err := verifySourceDownloadArguments(allowSourceDownload, terragruntOptions); err != nil {
return err
}

if terragruntConfig.RemoteState != nil {
// Initialize the remote state if necessary (e.g. create S3 bucket and DynamoDB table)
remoteStateNeedsInit, err := remoteStateNeedsInit(terragruntConfig.RemoteState, terragruntOptions)
Expand Down Expand Up @@ -629,69 +603,12 @@ func prepareInitOptions(terragruntOptions *options.TerragruntOptions, terraformS
initOptions.WorkingDir = terragruntOptions.WorkingDir
initOptions.TerraformCommand = CMD_INIT

// Don't pollute stdout with the stdout from Aoto Init
// Don't pollute stdout with the stdout from Auto Init
initOptions.Writer = initOptions.ErrWriter

// Only add the arguments to download source if terraformSource was specified
if terraformSource != nil {
initOptions.WorkingDir = terraformSource.WorkingDir
if util.FileExists(terraformSource.WorkingDir) {
if err := os.MkdirAll(terraformSource.WorkingDir, 0700); err != nil {
return nil, errors.WithStackTrace(err)
}
}

// terraform init -from-module will only work if the destination folder is empty, so we have to
if util.FileExists(terraformSource.DownloadDir) {
terragruntOptions.Logger.Printf("Download dir %s already exists, so deleting it before downloading into it.", terraformSource.DownloadDir)
if err := os.RemoveAll(terraformSource.DownloadDir); err != nil {
return nil, errors.WithStackTrace(err)
}
if err := os.MkdirAll(terraformSource.DownloadDir, 0700); err != nil {
return nil, errors.WithStackTrace(err)
}
}

// We will run init separately to download modules, plugins, backend state, etc, so don't run it at this point
initOptions.AppendTerraformCliArgs("-get=false")
initOptions.AppendTerraformCliArgs("-get-plugins=false")
initOptions.AppendTerraformCliArgs("-backend=false")

// Set the TerraformCommand attribute to match hooks on `init-from-module`
initOptions.TerraformCommand = CMD_INIT_FROM_MODULE

// Use the -from-module parameter to tell Terraform to download the module for us
initOptions.AppendTerraformCliArgs("-from-module="+terraformSource.CanonicalSourceURL.String(), "-no-color")

initOptions.AppendTerraformCliArgs(terraformSource.DownloadDir)
}
return initOptions, nil
}

// Returns an error if allowSourceDownload is false, and terragruntOptions.TerraformCliArgs contains source download related arguments
func verifySourceDownloadArguments(allowSourceDownload bool, terragruntOptions *options.TerragruntOptions) error {
if allowSourceDownload || len(terragruntOptions.TerraformCliArgs) <= 1 {
return nil
}
for _, arg := range terragruntOptions.TerraformCliArgs[1:] {
// Enforce that the user did not specify -from-module
if strings.Contains(arg, "-from-module") {
return errors.WithStackTrace(ArgumentNotAllowed{
Argument: arg,
Message: "Option not allowed: %s. Terragrunt will handle setting -from-module automatically.",
})
}
// The user is not allowed to pass non-option arguments (such as DIR)
if !strings.HasPrefix(arg, "-") {
return errors.WithStackTrace(ArgumentNotAllowed{
Argument: arg,
Message: "Argument not allowed: %s. Terragrunt will handle setting the module source and DIR arguments automatically.",
})
}
}
return nil
}

// Returns true if the command the user wants to execute is supposed to affect multiple Terraform modules, such as the
// apply-all or destroy-all command.
func isMultiModuleCommand(command string) bool {
Expand Down
50 changes: 44 additions & 6 deletions cli/download_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,17 @@ func downloadTerraformSourceIfNecessary(terraformSource *TerraformSource, terrag
return err
}

if err := terraformInit(terraformSource, terragruntOptions, terragruntConfig); err != nil {
return err
// When downloading source, we need to process any hooks waiting on `init-from-module`. Therefore, we clone the
// options struct, set the command to the value the hooks are expecting, and run the download action surrounded by
// before and after hooks (if any).
terragruntOptionsForDownload := terragruntOptions.Clone(terragruntOptions.TerragruntConfigPath)
terragruntOptionsForDownload.TerraformCommand = CMD_INIT_FROM_MODULE
downloadErr := runActionWithHooks("download source", terragruntOptionsForDownload, terragruntConfig, func() error {
return downloadSource(terraformSource, terragruntOptions, terragruntConfig)
})

if downloadErr != nil {
return downloadErr
}

if err := writeVersionFile(terraformSource); err != nil {
Expand Down Expand Up @@ -356,9 +365,38 @@ func getTerraformSourceUrl(terragruntOptions *options.TerragruntOptions, terragr
}
}

// Download the code from the Canonical Source URL into the Download Folder using the terraform init command
func terraformInit(terraformSource *TerraformSource, terragruntOptions *options.TerragruntOptions, terragruntConfig *config.TerragruntConfig) error {
terragruntOptions.Logger.Printf("Downloading Terraform configurations from %s into %s using terraform init", terraformSource.CanonicalSourceURL, terraformSource.DownloadDir)
// Download the code from the Canonical Source URL into the Download Folder using the go-getter library
func downloadSource(terraformSource *TerraformSource, terragruntOptions *options.TerragruntOptions, terragruntConfig *config.TerragruntConfig) error {
terragruntOptions.Logger.Printf("Downloading Terraform configurations from %s into %s", terraformSource.CanonicalSourceURL, terraformSource.DownloadDir)

// go-getter will not download into folders that already exist, so initially, we download into a brand new temp
// folder
if err := os.MkdirAll(terragruntOptions.DownloadDir, 0700); err != nil {
return errors.WithStackTrace(err)
}
tmpDownloadDir, err := ioutil.TempDir(terragruntOptions.DownloadDir, "terragrunt-download-temp")
if err != nil {
return errors.WithStackTrace(err)
}
if err := os.RemoveAll(tmpDownloadDir); err != nil {
return errors.WithStackTrace(err)
}
if err := getter.GetAny(tmpDownloadDir, terraformSource.CanonicalSourceURL.String()); err != nil {
return errors.WithStackTrace(err)
}

// Now copy all the contents of the tmp folder into the original download dir
if err := os.MkdirAll(terraformSource.DownloadDir, 0700); err != nil {
return errors.WithStackTrace(err)
}
if err := util.CopyFolderContents(tmpDownloadDir, terraformSource.DownloadDir); err != nil {
return err
}

// Clean up the tmp folder
if err := os.RemoveAll(tmpDownloadDir); err != nil {
return errors.WithStackTrace(err)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use defer here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: e239808


return runTerraformInit(terragruntOptions, terragruntConfig, terraformSource)
return nil
}
2 changes: 1 addition & 1 deletion cli/download_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ func testDownloadTerraformSourceIfNecessary(t *testing.T, canonicalUrl string, d
assert.Nil(t, err, "For terraform source %v: %v", terraformSource, err)

err = downloadTerraformSourceIfNecessary(terraformSource, terragruntOptions, terragruntConfig)
assert.Nil(t, err, "For terraform source %v: %v", terraformSource, err)
require.NoError(t, err, "For terraform source %v: %v", terraformSource, err)

expectedFilePath := util.JoinPath(downloadDir, "main.tf")
if assert.True(t, util.FileExists(expectedFilePath), "For terraform source %v", terraformSource) {
Expand Down
4 changes: 2 additions & 2 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ func TestParseTerragruntConfigRemoteStateMinimalConfig(t *testing.T) {

config := `
remote_state {
backend = "s3"
config = {}
backend = "s3"
config = {}
}
`

Expand Down
7 changes: 6 additions & 1 deletion configstack/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,8 +417,13 @@ func resolveExternalDependenciesForModule(module *TerraformModule, moduleMap map
}

// Confirm with the user whether they want Terragrunt to assume the given dependency of the given module is already
// applied. If the user selects "no", then Terragrunt will apply that module as well.
// applied. If the user selects "yes", then Terragrunt will apply that module as well.
func confirmShouldApplyExternalDependency(module *TerraformModule, dependency *TerraformModule, terragruntOptions *options.TerragruntOptions) (bool, error) {
if terragruntOptions.NonInteractive {
terragruntOptions.Logger.Printf("The --non-interactive flag is set. To avoid accidentally affecting external dependencies with an xxx-all command, will not run this command against module %s, which is a dependency of module %s.", dependency.Path, module.Path)
return false, nil
}

prompt := fmt.Sprintf("Module %s depends on module %s, which is an external dependency outside of the current working directory. Should Terragrunt run this external dependency? Warning, if you say 'yes', Terragrunt will make changes in %s as well!", module.Path, dependency.Path, dependency.Path)
return shell.PromptUserForYesNo(prompt, terragruntOptions)
}
Expand Down
6 changes: 3 additions & 3 deletions configstack/module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ func TestResolveTerraformModulesMultipleModulesWithExternalDependencies(t *testi
Dependencies: []*TerraformModule{},
Config: config.TerragruntConfig{},
TerragruntOptions: mockOptions.Clone(canonical(t, "../test/fixture-modules/module-f/"+config.DefaultTerragruntConfigPath)),
AssumeAlreadyApplied: false,
AssumeAlreadyApplied: true,
}

moduleG := &TerraformModule{
Expand Down Expand Up @@ -411,7 +411,7 @@ func TestResolveTerraformModulesMultipleModulesWithNestedExternalDependencies(t
Dependencies: []*TerraformModule{},
Config: config.TerragruntConfig{},
TerragruntOptions: mockOptions.Clone(canonical(t, "../test/fixture-modules/module-h/"+config.DefaultTerragruntConfigPath)),
AssumeAlreadyApplied: false,
AssumeAlreadyApplied: true,
}

moduleI := &TerraformModule{
Expand All @@ -421,7 +421,7 @@ func TestResolveTerraformModulesMultipleModulesWithNestedExternalDependencies(t
Dependencies: &config.ModuleDependencies{Paths: []string{"../module-h"}},
},
TerragruntOptions: mockOptions.Clone(canonical(t, "../test/fixture-modules/module-i/"+config.DefaultTerragruntConfigPath)),
AssumeAlreadyApplied: false,
AssumeAlreadyApplied: true,
}

moduleJ := &TerraformModule{
Expand Down
24 changes: 23 additions & 1 deletion remote/remote_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (remoteState *RemoteState) differsFrom(existingBackend *TerraformBackend, t
return true
}

if !reflect.DeepEqual(existingBackend.Config, remoteState.Config) {
if !terraformStateConfigEqual(existingBackend.Config, remoteState.Config) {
terragruntOptions.Logger.Printf("Backend config has changed from %s to %s", existingBackend.Config, remoteState.Config)
return true
}
Expand All @@ -103,6 +103,28 @@ func (remoteState *RemoteState) differsFrom(existingBackend *TerraformBackend, t
return false
}

// Return true if the existing config from a .tfstate file is equal to the new config from the user's backend
// configuration. Under the hood, this method does a reflect.DeepEqual check, but with one twist: we strip out any
// null values in the existing config. This is because Terraform >= 0.12 stores ALL possible keys for a given backend
// in the .tfstate file, even if the user hasn't configured that key, in which case the value will be null, and cause
// reflect.DeepEqual to fail.
func terraformStateConfigEqual(existingConfig map[string]interface{}, newConfig map[string]interface{}) bool {
if existingConfig == nil {
return newConfig == nil
}

existingConfigNonNil := map[string]interface{}{}
for existingKey, existingValue := range existingConfig {
_, newValueIsSet := newConfig[existingKey]
if existingValue == nil && !newValueIsSet {
continue
}
existingConfigNonNil[existingKey] = existingValue
}

return reflect.DeepEqual(existingConfigNonNil, newConfig)
}

// Convert the RemoteState config into the format used by the terraform init command
func (remoteState RemoteState) ToTerraformInitArgs() []string {

Expand Down
2 changes: 1 addition & 1 deletion remote/remote_state_s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func configValuesEqual(config map[string]interface{}, existingBackend *Terraform
delete(config, key)
}

if !reflect.DeepEqual(existingBackend.Config, config) {
if !terraformStateConfigEqual(existingBackend.Config, config) {
terragruntOptions.Logger.Printf("Backend config has changed from %s to %s", existingBackend.Config, config)
return false
}
Expand Down
6 changes: 6 additions & 0 deletions remote/remote_state_s3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ func TestConfigValuesEqual(t *testing.T) {
&TerraformBackend{Type: "s3", Config: map[string]interface{}{"something": "false"}},
false,
},
{
"equal-null-ignored",
map[string]interface{}{"something": "foo"},
&TerraformBackend{Type: "s3", Config: map[string]interface{}{"something": "foo", "ignored-because-null": nil}},
true,
},
}

for _, testCase := range testCases {
Expand Down
Loading