8000 Refactor terragrunt module and add dedicated test pipeline by james00012 · Pull Request #1569 · gruntwork-io/terratest · GitHub
[go: up one dir, main page]

Skip to content
Merged
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
50 changes: 49 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,13 @@ jobs:
# to kill the build after more than 10 minutes without log output.
# NOTE: because this doesn't build with the kubernetes tag, it will not run the kubernetes tests. See
# kubernetes_test build steps.
# NOTE: terragrunt tests are excluded here and run in a separate terragrunt_test job.
- run: mkdir -p /tmp/logs
# check we can compile the azure code, but don't actually run the tests
- run: run-go-tests --packages "-p 1 -tags=azure -run IDontExist ./modules/azure"
- run: run-go-tests --packages "-p 1 ./..." | tee /tmp/logs/test_output.log
- run: |
# Run all tests except terragrunt module (which has its own dedicated job)
run-go-tests --packages "-p 1 $(go list ./... | grep -v './modules/terragrunt' | tr '\n' ' ')" | tee /tmp/logs/test_output.log

- run:
command: |
Expand Down Expand Up @@ -378,6 +381,41 @@ jobs:
- store_test_results:
path: /tmp/logs

# Dedicated terragrunt tests that require terragrunt binary to be available
terragrunt_test:
<<: *defaults
resource_class: large
steps:
- attach_workspace:
at: /home/circleci

- run:
<<: *install_gruntwork_utils
- run:
<<: *install_docker_buildx

# The weird way you have to set PATH in Circle 2.0
- run: |
echo 'export PATH=$HOME/.local/bin:$HOME/terraform:$HOME/packer:$PATH' >> $BASH_ENV

# Run the terragrunt-specific tests. These tests specifically target the terragrunt module
# and require terragrunt binary to be available (which is installed via install_gruntwork_utils)
- run:
command: |
mkdir -p /tmp/logs
# Run only the terragrunt module tests
run-go-tests --packages "-p 1 ./modules/terragrunt" | tee /tmp/logs/test_output.log

- run:
command: |
./cmd/bin/terratest_log_parser_linux_amd64 --testlog /tmp/logs/test_output.log --outputdir /tmp/logs
when: always

# Store test result and log artifacts for browsing purposes
- store_artifacts:
path: /tmp/logs
- store_test_results:
path: /tmp/logs

deploy:
<<: *defaults
Expand Down Expand Up @@ -425,6 +463,16 @@ workflows:
tags:
only: /^v.*/

- terragrunt_test:
context:
- AWS__PHXDEVOPS__circle-ci-test
- GITHUB__PAT__gruntwork-ci
requires:
- setup
filters:
tags:
only: /^v.*/

- test_terraform:
context:
- AWS__PHXDEVOPS__circle-ci-test
Expand Down
35 changes: 29 additions & 6 deletions modules/terragrunt/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,17 @@ import (
// terragruntStackCommandE executes a terragrunt stack command without any subcommand
// This is used for commands like "terragrunt stack generate"
func terragruntStackCommandE(t testing.TestingT, opts *Options, additionalArgs ...string) (string, error) {
return runTerragruntStackSubCommandE(t, opts, "", additionalArgs...)
return runTerragruntStackCommandE(t, opts, "", additionalArgs...)
}

// runTerragruntStackSubCommandE is the core function that executes terragrunt stack commands
// It handles experimental flag detection, argument construction, and retry logic
func runTerragruntStackSubCommandE(t testing.TestingT, opts *Options, subCommand string, additionalArgs ...string) (string, error) {
// runTerragruntStackCommandE is the unified function that executes terragrunt stack commands
// It handles argument construction, retry logic, and error handling for all stack commands
func runTerragruntStackCommandE(t testing.TestingT, opts *Options, subCommand string, additionalArgs ...string) (string, error) {
// Validate required options
if err := validateOptions(opts); err != nil {
return "", err
}

// Build the base command arguments starting with "stack"
commandArgs := []string{"stack"}
if subCommand != "" {
Expand All @@ -29,8 +34,10 @@ func runTerragruntStackSubCommandE(t testing.TestingT, opts *Options, subCommand
// Apply common terragrunt options and get the final command arguments
terragruntOptions, finalArgs := GetCommonOptions(opts, commandArgs...)

// Append additional arguments with "--" separator
finalArgs = append(finalArgs, slices.Insert(additionalArgs, 0, "--")...)
// Append additional arguments with "--" separator for stack commands
if len(additionalArgs) > 0 {
finalArgs = append(finalArgs, slices.Insert(additionalArgs, 0, ArgSeparator)...)
}

// Generate the final shell command
execCommand := generateCommand(terragruntOptions, finalArgs...)
Expand Down Expand Up @@ -62,6 +69,11 @@ func runTerragruntStackSubCommandE(t testing.TestingT, opts *Options, subCommand
// runTerragruntCommandE is the core function that executes regular terragrunt commands
// It handles argument construction, retry logic, and error handling for non-stack commands
func runTerragruntCommandE(t testing.TestingT, opts *Options, command string, additionalArgs ...string) (string, error) {
// Validate required options
if err := validateOptions(opts); err != nil {
return "", err
}

// Build the base command arguments starting with the command
commandArgs := []string{command}

Expand Down Expand Up @@ -121,6 +133,17 @@ func hasWarning(opts *Options, commandOutput string) error {
return nil
}

// validateOptions validates that required options are provided
func validateOptions(opts *Options) error {
if opts == nil {
return fmt.Errorf("options cannot be nil")
}
if opts.TerragruntDir == "" {
return fmt.Errorf("TerragruntDir is required")
}
return nil
}

// generateCommand creates a shell.Command with the specified terragrunt options and arguments
// This function encapsulates the command creation logic for consistency
func generateCommand(terragruntOptions *Options, commandArgs ...string) shell.Command {
Expand Down
27 changes: 10 additions & 17 deletions modules/terragrunt/init.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package terragrunt

import (
"fmt"

"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/gruntwork-io/terratest/modules/testing"
)
Expand All @@ -22,24 +20,19 @@ func TgStackInitE(t testing.TestingT, options *Options) (string, error) {
return runTerragruntCommandE(t, options, "init", initStackArgs(options)...)
}

// initStackArgs builds the argument list for terragrunt init command.
// All terragrunt command-line flags are now passed via ExtraArgs.
// This function only handles complex configuration that requires special formatting.
func initStackArgs(options *Options) []string {
args := []string{fmt.Sprintf("-upgrade=%t", options.Upgrade)}

// Append reconfigure option if specified
if options.Reconfigure {
args = append(args, "-reconfigure")
}
// Append combination of migrate-state and force-copy to suppress answer prompt
if options.MigrateState {
args = append(args, "-migrate-state", "-force-copy")
}
// Append no-color option if needed
if options.NoColor {
args = append(args, "-no-color")
}
var args []string

// Add complex configuration that requires special formatting
args = append(args, terraform.FormatTerraformBackendConfigAsArgs(options.BackendConfig)...)
args = append(args, terraform.FormatTerraformPluginDirAsArgs(options.PluginDir)...)
args = append(args, options.ExtraArgs.Init...)

// Add all user-specified terragrunt command-line arguments
// This includes flags like -no-color, -upgrade=true, -reconfigure, etc.
args = append(args, options.ExtraArgs...)

return args
}
2 changes: 2 additions & 0 deletions modules/terragrunt/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func TestTerragruntInit(t *testing.T) {
out, err := TgStackInitE(t, &Options{
TerragruntDir: testFolder,
TerragruntBinary: "terragrunt",
ExtraArgs: []string{"-upgrade=true"}, // Common init flag
})
require.NoError(t, err)
require.Contains(t, out, "Terraform has been successfully initialized!")
Expand All @@ -31,6 +32,7 @@ func TestTerragruntInitWithInvalidConfig(t *testing.T) {
_, err = TgStackInitE(t, &Options{
TerragruntDir: testFolder,
TerragruntBinary: "terragrunt",
ExtraArgs: []string{"-upgrade=true"}, // Common init flag
})
require.Error(t, err)
// The error should contain information about the HCL parsing error
Expand Down
106 changes: 66 additions & 40 deletions modules/terragrunt/options.go
A3DA < 67E6 /tbody>
Original file line number Diff line number Diff line change
Expand Up @@ -7,53 +7,84 @@ import (
"github.com/gruntwork-io/terratest/modules/logger"
)

// Options represents the configuration options for terragrunt commands
type Options struct {
// Terragrunt-specific options
TerragruntBinary string // The terragrunt binary to use (should be "terragrunt")
TerragruntDir string // The directory containing the terragrunt configuration

// Command-specific options
NoColor bool // Whether to disable colored output
Upgrade bool // Whether to upgrade modules and plugins (init command)
Reconfigure bool // Whether to reconfigure the backend (init command)
MigrateState bool // Whether to migrate state (init command)
// Key concepts:
// - Options: Configure HOW the test framework executes terragrunt (directories, retry logic, logging)
// - ExtraArgs: Specify ALL command-line arguments passed to the specific terragrunt command
// - Use Options.TerragruntDir to specify WHERE to run terragrunt
// - Use ExtraArgs to pass ALL command-line arguments (including -no-color, -upgrade, etc.)
//
// Example:
//
// // For init
// TgStackInitE(t, &Options{
// TerragruntDir: "/path/to/config",
// ExtraArgs: []string{"-upgrade=true", "-no-color"},
// })
//
// // For generate
// TgStackGenerateE(t, &Options{
// TerragruntDir: "/path/to/config",
// ExtraArgs: []string{"-no-color"},
// })
//
// Constants for test framework configuration and environment variables
const (
DefaultTerragruntBinary = "terragrunt"
NonInteractiveFlag = "--non-interactive"
TerragruntLogFormatKey = "TG_LOG_FORMAT"
TerragruntLogCustomKey = "TG_LOG_CUSTOM_FORMAT"
DefaultLogFormat = "key-value"
DefaultLogCustomFormat = "%msg(color=disable)"
ArgSeparator = "--"
)

// Configuration options
BackendConfig map[string]interface{} // Backend configuration (init command)
PluginDir string // Plugin directory (init command)
EnvVars map[string]string // Environment variables for command execution
// Options represent the configuration options for terragrunt test execution.
//
// This struct is divided into two clear categories:
//
// 1. TEST FRAMEWORK CONFIGURATION:
// - Controls HOW the test framework executes terragrunt
// - Includes: binary paths, directories, retry logic, logging, environment
// - These are NOT passed as command-line arguments to terragrunt
//
// 2. TERRAGRUNT COMMAND ARGUMENTS:
// - All actual terragrunt command-line arguments go in ExtraArgs []string
// - This includes flags like -no-color, -upgrade, -reconfigure, etc.
// - These ARE passed directly to the specific terragrunt command being executed
//
// This separation eliminates confusion about which settings control the test
// framework vs which become terragrunt command-line arguments.
type Options struct {
// Test framework configuration (NOT passed to terragrunt command line)
TerragruntBinary string // The terragrunt binary to use (should be "terragrunt")
TerragruntDir string // The directory containing the terragrunt configuration
EnvVars map[string]string // Environment variables for command execution
Logger *logger.Logger // Logger for command output

// Execution options
Logger *logger.Logger // Logger for command output
// Test framework retry and error handling (NOT passed to terragrunt command line)
MaxRetries int // Maximum number of retries
TimeBetweenRetries time.Duration // Time between retries
RetryableTerraformErrors map[string]string // Retryable error patterns
WarningsAsErrors map[string]string // Warnings to treat as errors

// Terragrunt-specific extra arguments
ExtraArgs ExtraArgs
}
// Complex configuration that requires special formatting (NOT raw command-line args)
BackendConfig map[string]interface{} // Backend configuration (formatted specially)
PluginDir string // Plugin directory (formatted specially)

// ExtraArgs represents terragrunt-specific extra arguments for different commands
type ExtraArgs struct {
Init []string // Extra arguments for init command
Apply []string // Extra arguments for apply command (used by generate)
Plan []string // Extra arguments for plan command
Destroy []string // Extra arguments for destroy command
Generate []string // Extra arguments for generate command
// All terragrunt command-line arguments for the specific command being executed
ExtraArgs []string
}

// GetCommonOptions extracts common terragrunt options and prepares arguments
// This is the terragrunt-specific version of terraform.GetCommonOptions
func GetCommonOptions(options *Options, args ...string) (*Options, []string) {
// Set default binary if not specified
if options.TerragruntBinary == "" {
options.TerragruntBinary = "terragrunt"
options.TerragruntBinary = DefaultTerragruntBinary
}

// Add terragrunt-specific flags
args = append(args, "--non-interactive")
args = append(args, NonInteractiveFlag)

// Set terragrunt log formatting if not already set
setTerragruntLogFormatting(options)
Expand All @@ -64,30 +95,25 @@ func GetCommonOptions(options *Options, args ...string) (*Options, []string) {
// setTerragruntLogFormatting sets default log formatting for terragrunt
// if it is not already set in options.EnvVars or OS environment vars
func setTerragruntLogFormatting(options *Options) {
const (
tgLogFormatKey = "TG_LOG_FORMAT"
tgLogCustomFormatKey = "TG_LOG_CUSTOM_FORMAT"
)

if options.EnvVars == nil {
options.EnvVars = make(map[string]string)
}

_, inOpts := options.EnvVars[tgLogFormatKey]
_, inOpts := options.EnvVars[TerragruntLogFormatKey]
if !inOpts {
_, inEnv := os.LookupEnv(tgLogFormatKey)
_, inEnv := os.LookupEnv(TerragruntLogFormatKey)
if !inEnv {
// 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[tgLogFormatKey] = "key-value"
options.EnvVars[TerragruntLogFormatKey] = DefaultLogFormat
}
}

_, inOpts = options.EnvVars[tgLogCustomFormatKey]
_, inOpts = options.EnvVars[TerragruntLogCustomKey]
if !inOpts {
_, inEnv := os.LookupEnv(tgLogCustomFormatKey)
_, inEnv := os.LookupEnv(TerragruntLogCustomKey)
if !inEnv {
options.EnvVars[tgLogCustomFormatKey] = "%msg(color=disable)"
options.EnvVars[TerragruntLogCustomKey] = DefaultLogCustomFormat
}
}
}
13 changes: 5 additions & 8 deletions modules/terragrunt/stack_generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,14 @@ func TgStackGenerateE(t testing.TestingT, options *Options) (string, error) {
return terragruntStackCommandE(t, options, generateStackArgs(options)...)
}

// generateStackArgs builds the argument list for terragrunt stack generate command.
// All terragrunt command-line flags are now passed via ExtraArgs.
func generateStackArgs(options *Options) []string {
args := []string{"generate"}

// Append no-color option if needed
if options.NoColor {
args = append(args, "-no-color")
}
// Add all user-specified terragrunt command-line arguments
// This includes flags like -no-color, etc.
args = append(args, options.ExtraArgs...)

5065 // Use Apply extra args for generate command as it's a similar operation
if len(options.ExtraArgs.Apply) > 0 {
args = append(args, options.ExtraArgs.Apply...)
}
return args
}
Loading
0