diff --git a/.circleci/config.yml b/.circleci/config.yml index 1434159eb..0a0ac0cdc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ env: &env TERRAFORM_VERSION: 1.5.7 TOFU_VERSION: 1.8.0 PACKER_VERSION: 1.10.0 - TERRAGRUNT_VERSION: v0.52.0 + TERRAGRUNT_VERSION: v0.69.8 OPA_VERSION: v0.33.1 GO_VERSION: 1.21.1 GO111MODULE: auto diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 6b100e40e..cebc6ff96 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,6 +14,7 @@ Read the [Gruntwork contribution guidelines](https://gruntwork.notion.site/Grunt - [ ] Run the relevant tests successfully, including pre-commit checks. - [ ] Ensure any 3rd party code adheres with our [license policy](https://www.notion.so/gruntwork/Gruntwork-licenses-and-open-source-usage-policy-f7dece1f780341c7b69c1763f22b1378) or delete this line if its not applicable. - [ ] Include release notes. If this PR is backward incompatible, include a migration guide. +- [ ] Make a plan for release of the functionality in this PR. If it delivers value to an end user, you are responsible for ensuring it is released promptly, and correctly. If you are not a maintainer, you are responsible for finding a maintainer to do this for you. ## Release Notes (draft) diff --git a/modules/terraform/cmd.go b/modules/terraform/cmd.go index 48e1324f7..ea4baf511 100644 --- a/modules/terraform/cmd.go +++ b/modules/terraform/cmd.go @@ -39,6 +39,9 @@ const ( // TerraformDefaultPath to run terraform TerraformDefaultPath = "terraform" + + // TerragruntDefaultPath to run terragrunt + TerragruntDefaultPath = "terragrunt" ) var DefaultExecutable = defaultTerraformExecutable() @@ -49,8 +52,22 @@ func GetCommonOptions(options *Options, args ...string) (*Options, []string) { options.TerraformBinary = DefaultExecutable } - if options.TerraformBinary == "terragrunt" { + if options.TerraformBinary == TerragruntDefaultPath { args = append(args, "--terragrunt-non-interactive") + // for newer Terragrunt version, setting simplified log formatting + if options.EnvVars == nil { + options.EnvVars = map[string]string{} + } + _, tgLogSet := options.EnvVars["TERRAGRUNT_LOG_FORMAT"] + if !tgLogSet { + // key-value format for terragrunt logs to avoid colors and have plain form + // https://terragrunt.gruntwork.io/docs/reference/cli-options/#terragrunt-log-format + options.EnvVars["TERRAGRUNT_LOG_FORMAT"] = "key-value" + } + _, tgLogFormat := options.EnvVars["TERRAGRUNT_LOG_CUSTOM_FORMAT"] + if !tgLogFormat { + options.EnvVars["TERRAGRUNT_LOG_CUSTOM_FORMAT"] = "%msg(color=disable)" + } } if options.Parallelism > 0 && len(args) > 0 && collections.ListContains(commandsWithParallelism, args[0]) { diff --git a/modules/terraform/output.go b/modules/terraform/output.go index 6e294e7a2..b4b67ec7b 100644 --- a/modules/terraform/output.go +++ b/modules/terraform/output.go @@ -5,12 +5,23 @@ import ( "errors" "fmt" "reflect" + "regexp" "strconv" + "strings" "github.com/gruntwork-io/terratest/modules/testing" "github.com/stretchr/testify/require" ) +const skipJsonLogLine = " msg=" + +var ( + // ansiLineRegex matches lines starting with ANSI escape codes for text formatting (e.g., colors, styles). + ansiLineRegex = regexp.MustCompile(`(?m)^\x1b\[[0-9;]*m.*`) + // tgLogLevel matches log lines containing fields for time, level, prefix, binary, and message, each with non-whitespace values. + tgLogLevel = regexp.MustCompile(`.*time=\S+ level=\S+ prefix=\S+ binary=\S+ msg=.*`) +) + // Output calls terraform output for the given variable and return its string value representation. // It only designed to work with primitive terraform types: string, number and bool. // Please use OutputStruct for anything else. @@ -279,7 +290,11 @@ func OutputJsonE(t testing.TestingT, options *Options, key string) (string, erro args = append(args, key) } - return RunTerraformCommandAndGetStdoutE(t, options, args...) + rawJson, err := RunTerraformCommandAndGetStdoutE(t, options, args...) + if err != nil { + return rawJson, err + } + return cleanJson(rawJson) } // OutputStruct calls terraform output for the given variable and stores the @@ -348,3 +363,33 @@ func OutputAll(t testing.TestingT, options *Options) map[string]interface{} { func OutputAllE(t testing.TestingT, options *Options) (map[string]interface{}, error) { return OutputForKeysE(t, options, nil) } + +// clean the ANSI characters from the JSON and update formating +func cleanJson(input string) (string, error) { + // Remove ANSI escape codes + cleaned := ansiLineRegex.ReplaceAllString(input, "") + cleaned = tgLogLevel.ReplaceAllString(cleaned, "") + + lines := strings.Split(cleaned, "\n") + var result []string + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed != "" && !strings.Contains(trimmed, skipJsonLogLine) { + result = append(result, trimmed) + } + } + ansiClean := strings.Join(result, "\n") + + var jsonObj interface{} + if err := json.Unmarshal([]byte(ansiClean), &jsonObj); err != nil { + return "", err + } + + // Format JSON output with indentation + normalized, err := json.MarshalIndent(jsonObj, "", " ") + if err != nil { + return "", err + } + + return string(normalized), nil +} diff --git a/modules/terraform/output_test.go b/modules/terraform/output_test.go index f3285d69f..f2d0d927d 100644 --- a/modules/terraform/output_test.go +++ b/modules/terraform/output_test.go @@ -2,8 +2,11 @@ package terraform import ( "fmt" + "path/filepath" "testing" + "github.com/stretchr/testify/assert" + "github.com/gruntwork-io/terratest/modules/files" "github.com/stretchr/testify/require" ) @@ -31,6 +34,40 @@ func TestOutputString(t *testing.T) { num1 := Output(t, options, "number1") require.Equal(t, num1, "3", "Number %q should match %q", "3", num1) + + unicodeString := Output(t, options, "unicode_string") + require.Equal(t, "söme chäräcter", unicodeString) +} + +func TestTgOutputString(t *testing.T) { + t.Parallel() + + testFolder, err := files.CopyTerraformFolderToTemp("../../test/fixtures/terraform-output", t.Name()) + require.NoError(t, err) + + WriteFile(t, filepath.Join(testFolder, "terragrunt.hcl"), []byte{}) + + options := &Options{ + TerraformDir: testFolder, + TerraformBinary: "terragrunt", + } + + InitAndApply(t, options) + + b := Output(t, options, "bool") + require.Equal(t, b, "true", "Bool %q should match %q", "true", b) + + str := Output(t, options, "string") + require.Equal(t, str, "This is a string.", "String %q should match %q", "This is a string.", str) + + num := Output(t, options, "number") + require.Equal(t, num, "3.14", "Number %q should match %q", "3.14", num) + + num1 := Output(t, options, "number1") + require.Equal(t, num1, "3", "Number %q should match %q", "3", num1) + + unicodeString := Output(t, options, "unicode_string") + require.Equal(t, "söme chäräcter", unicodeString) } func TestOutputList(t *testing.T) { @@ -310,6 +347,11 @@ func TestOutputJson(t *testing.T) { "sensitive": false, "type": "string", "value": "This is a string." + }, + "unicode_string": { + "sensitive": false, + "type": "string", + "value": "söme chäräcter" } }` @@ -433,3 +475,27 @@ func TestOutputsForKeysError(t *testing.T) { require.Error(t, err) } + +func TestTgOutputJsonParsing(t *testing.T) { + t.Parallel() + + testFolder, err := files.CopyTerraformFolderToTemp("../../test/fixtures/terraform-output-map", t.Name()) + require.NoError(t, err) + + WriteFile(t, filepath.Join(testFolder, "terragrunt.hcl"), []byte{}) + + options := &Options{ + TerraformDir: testFolder, + TerraformBinary: "terragrunt", + } + + InitAndApply(t, options) + + output, err := OutputAllE(t, options) + + require.NoError(t, err) + assert.NotNil(t, output) + assert.NotEmpty(t, output) + assert.Contains(t, output, "mogwai") + assert.Equal(t, "söme chäräcter", output["not_a_map_unicode"]) +} diff --git a/test/fixtures/terraform-output-map/output.tf b/test/fixtures/terraform-output-map/output.tf index a722e8f6b..6a7aee48c 100644 --- a/test/fixtures/terraform-output-map/output.tf +++ b/test/fixtures/terraform-output-map/output.tf @@ -11,3 +11,6 @@ output "not_a_map" { value = "This is not a map." } +output "not_a_map_unicode" { + value = "söme chäräcter" +} diff --git a/test/fixtures/terraform-output/output.tf b/test/fixtures/terraform-output/output.tf index e9d5ae693..5b92808e3 100644 --- a/test/fixtures/terraform-output/output.tf +++ b/test/fixtures/terraform-output/output.tf @@ -13,3 +13,7 @@ output "number" { output "number1" { value = 3 } + +output "unicode_string" { + value = "söme chäräcter" +}