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

terragrunt-info parsing improvement #2901

Merged
merged 9 commits into from
Apr 4, 2024
26 changes: 14 additions & 12 deletions cli/commands/terraform/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,12 @@ func RunWithTarget(opts *options.TerragruntOptions, target *Target) error {

func runTerraform(terragruntOptions *options.TerragruntOptions, target *Target) error {
if err := checkVersionConstraints(terragruntOptions); err != nil {
return err
return target.runErrorCallback(terragruntOptions, nil, err)
}

terragruntConfig, err := config.ReadTerragruntConfig(terragruntOptions)
if err != nil {
return err
return target.runErrorCallback(terragruntOptions, terragruntConfig, err)
}

if target.isPoint(TargetPointParseConfig) {
Expand All @@ -112,7 +112,7 @@ func runTerraform(terragruntOptions *options.TerragruntOptions, target *Target)
terragruntOptionsClone.TerraformCommand = CommandNameTerragruntReadConfig

if err := processHooks(terragruntConfig.Terraform.GetAfterHooks(), terragruntOptionsClone, terragruntConfig, nil); err != nil {
return err
return target.runErrorCallback(terragruntOptions, terragruntConfig, err)
}

if terragruntConfig.Skip {
Expand All @@ -137,7 +137,7 @@ func runTerraform(terragruntOptions *options.TerragruntOptions, target *Target)
// get the default download dir
_, defaultDownloadDir, err := options.DefaultWorkingAndDownloadDirs(terragruntOptions.TerragruntConfigPath)
if err != nil {
return err
return target.runErrorCallback(terragruntOptions, terragruntConfig, err)
}

// if the download dir hasn't been changed from default, and is set in the config,
Expand Down Expand Up @@ -168,7 +168,7 @@ func runTerraform(terragruntOptions *options.TerragruntOptions, target *Target)
updatedTerragruntOptions := terragruntOptions
sourceUrl, err := config.GetTerraformSourceUrl(terragruntOptions, terragruntConfig)
if err != nil {
return err
return target.runErrorCallback(terragruntOptions, terragruntConfig, err)
}

if sourceUrl != "" {
Expand All @@ -180,7 +180,7 @@ func runTerraform(terragruntOptions *options.TerragruntOptions, target *Target)
})

if err != nil {
return err
return target.runErrorCallback(terragruntOptions, terragruntConfig, err)
}
}

Expand All @@ -193,7 +193,7 @@ func runTerraform(terragruntOptions *options.TerragruntOptions, target *Target)
// Handle code generation configs, both generate blocks and generate attribute of remote_state.
// Note that relative paths are relative to the terragrunt working dir (where terraform is called).
if err = generateConfig(terragruntConfig, updatedTerragruntOptions); err != nil {
return err
return target.runErrorCallback(terragruntOptions, terragruntConfig, err)
}

if target.isPoint(TargetPointGenerateConfig) {
Expand All @@ -203,14 +203,13 @@ func runTerraform(terragruntOptions *options.TerragruntOptions, target *Target)
// We do the debug file generation here, after all the terragrunt generated terraform files are created so that we
// can ensure the tfvars json file only includes the vars that are defined in the module.
if updatedTerragruntOptions.Debug {
err := WriteTerragruntDebugFile(updatedTerragruntOptions, terragruntConfig)
if err != nil {
return err
if err := WriteTerragruntDebugFile(updatedTerragruntOptions, terragruntConfig); err != nil {
return target.runErrorCallback(terragruntOptions, terragruntConfig, err)
}
}

if err := checkFolderContainsTerraformCode(updatedTerragruntOptions); err != nil {
return err
return target.runErrorCallback(terragruntOptions, terragruntConfig, err)
}

if terragruntOptions.CheckDependentModules {
Expand All @@ -219,7 +218,10 @@ func runTerraform(terragruntOptions *options.TerragruntOptions, target *Target)
return nil
}
}
return runTerragruntWithConfig(terragruntOptions, updatedTerragruntOptions, terragruntConfig, target)
if err := runTerragruntWithConfig(terragruntOptions, updatedTerragruntOptions, terragruntConfig, target); err != nil {
return target.runErrorCallback(terragruntOptions, terragruntConfig, err)
}
return nil
}

func generateConfig(terragruntConfig *config.TerragruntConfig, updatedTerragruntOptions *options.TerragruntOptions) error {
Expand Down
22 changes: 20 additions & 2 deletions cli/commands/terraform/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type TargetPointType byte

type TargetCallbackType func(opts *options.TerragruntOptions, config *config.TerragruntConfig) error

type TargetErrorCallbackType func(opts *options.TerragruntOptions, config *config.TerragruntConfig, e error) error

// Since most terragrunt CLI commands like `render-json`, `aws-provider-patch` ... require preparatory steps, such as `generate configuration` which is already coded in `terraform.runTerraform` and com;licated to extracted into a separate function due to some steps that can be called recursively in case of nested configuration or dependencies.
// Target struct helps to run `terraform.runTerraform` func up to the certain logic point, and the runs target's callback func and returns the flow.
// For example, `terragrunt-info` CLI command requires source to be downloaded before running its specific action. To do this it:
Expand Down Expand Up @@ -51,8 +53,9 @@ type TargetCallbackType func(opts *options.TerragruntOptions, config *config.Ter
*/

type Target struct {
point TargetPointType
callbackFunc TargetCallbackType
point TargetPointType
callbackFunc TargetCallbackType
errorCallbackFunc TargetErrorCallbackType
}

func NewTarget(point TargetPointType, callbackFunc TargetCallbackType) *Target {
Expand All @@ -62,10 +65,25 @@ func NewTarget(point TargetPointType, callbackFunc TargetCallbackType) *Target {
}
}

func NewTargetWithErrorHandler(point TargetPointType, callbackFunc TargetCallbackType, errorCallbackFunc TargetErrorCallbackType) *Target {
return &Target{
point: point,
callbackFunc: callbackFunc,
errorCallbackFunc: errorCallbackFunc,
}
}

func (target *Target) isPoint(point TargetPointType) bool {
return target.point == point
}

func (target *Target) runCallback(opts *options.TerragruntOptions, config *config.TerragruntConfig) error {
return target.callbackFunc(opts, config)
}

func (target *Target) runErrorCallback(opts *options.TerragruntOptions, config *config.TerragruntConfig, e error) error {
if target.errorCallbackFunc == nil {
return e
}
return target.errorCallbackFunc(opts, config, e)
}
24 changes: 19 additions & 5 deletions cli/commands/terragrunt-info/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import (
"encoding/json"
"fmt"

"github.com/gruntwork-io/go-commons/errors"

"github.com/gruntwork-io/terragrunt/cli/commands/terraform"
"github.com/gruntwork-io/terragrunt/config"
"github.com/gruntwork-io/terragrunt/options"
)

func Run(opts *options.TerragruntOptions) error {
target := terraform.NewTarget(terraform.TargetPointDownloadSource, runTerragruntInfo)
target := terraform.NewTargetWithErrorHandler(terraform.TargetPointDownloadSource, runTerragruntInfo, runErrorTerragruntInfo)

return terraform.RunWithTarget(opts, target)
}
Expand All @@ -25,7 +27,7 @@ type TerragruntInfoGroup struct {
WorkingDir string
}

func runTerragruntInfo(opts *options.TerragruntOptions, cfg *config.TerragruntConfig) error {
func printTerragruntInfo(opts *options.TerragruntOptions) error {
group := TerragruntInfoGroup{
ConfigPath: opts.TerragruntConfigPath,
DownloadDir: opts.DownloadDir,
Expand All @@ -38,10 +40,22 @@ func runTerragruntInfo(opts *options.TerragruntOptions, cfg *config.TerragruntCo
b, err := json.MarshalIndent(group, "", " ")
if err != nil {
opts.Logger.Errorf("JSON error marshalling terragrunt-info")
return err
return errors.WithStackTrace(err)
}
if _, err := fmt.Fprintf(opts.Writer, "%s\n", b); err != nil {
return errors.WithStackTrace(err)
}
fmt.Fprintf(opts.Writer, "%s\n", b)

return nil
}

func runTerragruntInfo(opts *options.TerragruntOptions, cfg *config.TerragruntConfig) error {
return printTerragruntInfo(opts)
}

func runErrorTerragruntInfo(opts *options.TerragruntOptions, cfg *config.TerragruntConfig, err error) error {
opts.Logger.Debugf("Fetching terragrunt-info: %v", err)
if err := printTerragruntInfo(opts); err != nil {
opts.Logger.Errorf("Error printing terragrunt-info: %v", err)
}
return err
}
3 changes: 3 additions & 0 deletions test/fixture-terragrunt-info-error/module-a/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
output "test_var" {
value = "hello"
}
3 changes: 3 additions & 0 deletions test/fixture-terragrunt-info-error/module-a/terragrunt.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
terraform {
source = ".//"
}
7 changes: 7 additions & 0 deletions test/fixture-terragrunt-info-error/module-b/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
variable "test_var" {
type = string
}

output "result" {
value = var.test_var
}
16 changes: 16 additions & 0 deletions test/fixture-terragrunt-info-error/module-b/terragrunt.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
terraform {
source = ".//"
}

dependency "module_a" {
config_path = "../module-a"
mock_outputs_allowed_terraform_commands = ["init", "plan", "validate"]
mock_outputs_merge_strategy_with_state = "shallow"
mock_outputs = {
test_mock = "abc"
}
}

inputs = {
test_var = dependency.module_a.outputs.test_mock
}
20 changes: 20 additions & 0 deletions test/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ const (
TEST_FIXTURE_ASSUME_ROLE_DURATION = "fixture-assume-role/duration"
TEST_FIXTURE_GRAPH = "fixture-graph"
TEST_FIXTURE_SKIP_DEPENDENCIES = "fixture-skip-dependencies"
TEST_FIXTURE_INFO_ERROR = "fixture-terragrunt-info-error"
TERRAFORM_BINARY = "terraform"
TOFU_BINARY = "tofu"
TERRAFORM_FOLDER = ".terraform"
Expand Down Expand Up @@ -6728,6 +6729,25 @@ func prepareGraphFixture(t *testing.T) string {
return tmpEnvPath
}

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

tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_INFO_ERROR)
cleanupTerraformFolder(t, tmpEnvPath)
testPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_INFO_ERROR, "module-b")

stdout := bytes.Buffer{}
stderr := bytes.Buffer{}

err := runTerragruntCommand(t, fmt.Sprintf("terragrunt terragrunt-info --terragrunt-non-interactive --terragrunt-working-dir %s", testPath), &stdout, &stderr)
assert.Error(t, err)

// parse stdout json as TerragruntInfoGroup
var output terragruntinfo.TerragruntInfoGroup
err = json.Unmarshal(stdout.Bytes(), &output)
assert.NoError(t, err)
}

func validateOutput(t *testing.T, outputs map[string]TerraformOutput, key string, value interface{}) {
t.Helper()
output, hasPlatform := outputs[key]
Expand Down