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

Stacks: output #3796

Merged
merged 53 commits into from
Feb 8, 2025
Merged

Stacks: output #3796

merged 53 commits into from
Feb 8, 2025

Conversation

denis256
Copy link
Member

@denis256 denis256 commented Jan 20, 2025

Description

Add stacks output command:

  • output formatting
  • tests
  • documentation

Example:

stack-output

RFC: #3313

TODOs

Read the Gruntwork contribution guidelines.

  • Update the docs.
  • Run the relevant tests successfully, including pre-commit checks.
  • Ensure any 3rd party code adheres with our license policy or delete this line if its not applicable.
  • Include release notes. If this PR is backward incompatible, include a migration guide.

Release Notes (draft)

Added / Removed / Updated [X].

Migration Guide

Summary by CodeRabbit

  • New Features

    • Introduced a new “stack output” command that enables users to retrieve outputs from multiple stack units in various formats (default, JSON, raw).
    • Added an option to configure the desired output format for better control of output presentation.
    • Enhanced stack configuration capabilities with new units and output declarations.
  • Documentation

    • Updated CLI documentation to explain the usage of the new output command and available format options.
    • Revised stabilization criteria for the "stacks" feature to reflect the completion of support for stack output commands.
  • Tests

    • Expanded test coverage to ensure reliable retrieval and formatting of stack outputs, including various output formats and indexed values.

Copy link

vercel bot commented Feb 4, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
terragrunt-docs ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 7, 2025 9:47pm

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
cli/commands/stack/command.go (2)

26-52: Add format validation and consider default format.

The flag configuration looks good overall, but a few suggestions to make it more robust:

  1. Add validation for format values
  2. Consider setting a default format
  3. Document the precedence when multiple format flags are used

Here's a suggested improvement:

 func NewFlags(opts *options.TerragruntOptions) cli.Flags {
+	// Set default format
+	if opts.StackOutputFormat == "" {
+		opts.StackOutputFormat = jsonOutputFormat
+	}
 	return cli.Flags{
 		&cli.GenericFlag[string]{
 			Name:   OutputFormatFlagName,
 			EnvVar: OutputFormatEnvName,
 			Destination: &opts.StackOutputFormat,
 			Usage:       "Stack output format. Valid values are: json, raw",
+			Action: func(ctx *cli.Context, value string) error {
+				if value != jsonOutputFormat && value != rawOutputFormat {
+					return fmt.Errorf("invalid format %q, valid values are: json, raw", value)
+				}
+				return nil
+			},
 		},

77-87: Enhance output command documentation.

The command implementation looks good, but the usage description could be more helpful. Based on the PR feedback, users would benefit from examples showing how to use the raw format for shell script integration.

Here's a suggested improvement:

 &cli.Command{
 	Name:  output,
-	Usage: "Run fetch stack output",
+	Usage: "Fetch stack output with optional formatting",
+	Description: `Examples:
+		# Get all outputs in JSON format
+		terragrunt stack output --format=json
+
+		# Get specific output in raw format (useful for shell scripts)
+		terragrunt stack output --format=raw app1.url
+
+		# Example shell script usage
+		curl -s "$(terragrunt stack output --format=raw services.service['service-1'].url)"`,
 	Action: func(ctx *cli.Context) error {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a34356 and 61f5e0d.

📒 Files selected for processing (1)
  • cli/commands/stack/command.go (2 hunks)
🧰 Additional context used
🪛 golangci-lint (1.62.2)
cli/commands/stack/command.go

14-14: ST1003: const JsonFormatFlagName should be JSONFormatFlagName

(stylecheck)

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: unessential
  • GitHub Check: build-and-test
  • GitHub Check: Pull Request has non-contributor approval

Comment on lines 14 to 15
JsonFormatFlagName = "json"
RawFormatFlagName = "json"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix constant declarations for format flags.

There are a couple of issues with the flag name constants:

  1. RawFormatFlagName is incorrectly set to "json" - it should be "raw"
  2. JsonFormatFlagName should be JSONFormatFlagName to follow Go naming conventions

Here's how to fix these:

-	JsonFormatFlagName   = "json"
-	RawFormatFlagName    = "json"
+	JSONFormatFlagName   = "json"
+	RawFormatFlagName    = "raw"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
JsonFormatFlagName = "json"
RawFormatFlagName = "json"
JSONFormatFlagName = "json"
RawFormatFlagName = "raw"
🧰 Tools
🪛 golangci-lint (1.62.2)

14-14: ST1003: const JsonFormatFlagName should be JSONFormatFlagName

(stylecheck)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
cli/commands/stack/command.go (1)

14-14: 🛠️ Refactor suggestion

Follow Go naming conventions for JSON constant.

The constant name should be JSONFormatFlagName to follow Go's naming convention for acronyms.

-	JSONFormatFlagName   = "json"
+	JSONFormatFlagName   = "json"
🧹 Nitpick comments (1)
cli/commands/stack/command.go (1)

77-87: Enhance the usage description for clarity.

The current usage description "Run fetch stack output" could be more descriptive to help users understand what the command does.

-				Usage: "Run fetch stack output",
+				Usage: "Fetch and display stack outputs with optional filtering by output name",
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 61f5e0d and 19147ae.

📒 Files selected for processing (3)
  • cli/commands/stack/command.go (2 hunks)
  • docs/_docs/04_reference/02-cli-options.md (1 hunks)
  • test/integration_stacks_test.go (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/_docs/04_reference/02-cli-options.md
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build-and-test
  • GitHub Check: Pull Request has non-contributor approval
🔇 Additional comments (2)
cli/commands/stack/command.go (1)

26-51: Nice implementation of the output format flags!

The flags are well-structured with clear usage descriptions and proper environment variable integration.

test/integration_stacks_test.go (1)

162-311: Excellent test coverage! 👏

The test suite is comprehensive and well-structured, covering:

  • Different output formats (raw, JSON)
  • Index-based filtering
  • Both flag and format parameter approaches
  • Proper validation of output content

yhakbar
yhakbar previously approved these changes Feb 7, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (6)
cli/commands/stack/action.go (1)

51-85: Enhance error handling and switch statement robustness

A few suggestions to make the code more maintainable:

  1. Add more context to errors to help with debugging
  2. Handle invalid output formats explicitly
 func RunOutput(ctx context.Context, opts *options.TerragruntOptions, index string) error {
 	stacksEnabled := opts.Experiments[experiment.Stacks]
 	if !stacksEnabled.Enabled {
-		return errors.New("stacks experiment is not enabled use --experiment stacks to enable it")
+		return errors.New("stacks experiment is not enabled - use --experiment stacks to enable it")
 	}
 
 	// collect outputs
 	outputs, err := generateOutput(ctx, opts)
 	if err != nil {
-		return errors.New(err)
+		return errors.WithPrefixf(err, "error generating stack output")
 	}
 	// write outputs
 
 	writer := opts.Writer
 
 	switch opts.StackOutputFormat {
-	default:
+	case "":
 		if err := PrintOutputs(writer, outputs, index); err != nil {
-			return errors.New(err)
+			return errors.WithPrefixf(err, "error printing outputs")
 		}
 
 	case rawOutputFormat:
 		if err := PrintRawOutputs(opts, writer, outputs, index); err != nil {
-			return errors.New(err)
+			return errors.WithPrefixf(err, "error printing raw outputs")
 		}
 
 	case jsonOutputFormat:
 		if err := PrintJSONOutput(writer, outputs, index); err != nil {
-			return errors.New(err)
+			return errors.WithPrefixf(err, "error printing JSON output")
 		}
+	default:
+		return errors.New("invalid output format: " + opts.StackOutputFormat)
 	}
 
 	return nil
 }
cli/commands/stack/output.go (3)

23-46: Improve error handling in generateOutput

The error handling could be more descriptive to help with debugging.

 func generateOutput(ctx context.Context, opts *options.TerragruntOptions) (map[string]map[string]cty.Value, error) {
 	opts.TerragruntStackConfigPath = filepath.Join(opts.WorkingDir, defaultStackFile)
 	opts.Logger.Debugf("Generating output from %s", opts.TerragruntStackConfigPath)
 	stackFile, err := config.ReadStackConfigFile(ctx, opts)
 
 	if err != nil {
-		return nil, errors.New(err)
+		return nil, errors.WithPrefixf(err, "failed to read stack config file %s", opts.TerragruntStackConfigPath)
 	}
 
 	unitOutputs := make(map[string]map[string]cty.Value)
 	// process each unit and get outputs
 	for _, unit := range stackFile.Units {
 		opts.Logger.Debugf("Processing unit %s", unit.Name)
 		output, err := unit.ReadOutputs(ctx, opts)
 
 		if err != nil {
-			return nil, errors.New(err)
+			return nil, errors.WithPrefixf(err, "failed to read outputs for unit %s", unit.Name)
 		}
 
 		unitOutputs[unit.Name] = output
 	}
 
 	return unitOutputs, nil
 }

47-79: Enhance error handling in PrintRawOutputs

The function could handle errors more gracefully and provide better error messages.

 func PrintRawOutputs(opts *options.TerragruntOptions, writer io.Writer, outputs map[string]map[string]cty.Value, outputIndex string) error {
 	if len(outputIndex) == 0 {
-		return errors.New("output index is required in raw mode")
+		return errors.New("output index is required when using raw format (--format raw)")
 	}
 
 	filteredOutputs := FilterOutputs(outputs, outputIndex)
 
 	if filteredOutputs == nil {
-		return nil
+		return errors.New("no outputs found for index: " + outputIndex)
 	}
 
 	if len(filteredOutputs) > 1 {
-		return errors.New("multiple outputs found, please specify only one index")
+		return errors.New("multiple outputs found for index '" + outputIndex + "', please specify a unique index")
 	}
 
 	for key, value := range filteredOutputs {
 		valueStr, err := getValueString(value)
 		if err != nil {
 			opts.Logger.Warnf("Error fetching output for '%s': %v", key, err)
 			continue
 		}
 
 		line := fmt.Sprintf("%s\n", valueStr)
 		if _, err := writer.Write([]byte(line)); err != nil {
-			return errors.New(err)
+			return errors.WithPrefixf(err, "failed to write output for key %s", key)
 		}
 	}
 
 	return nil
 }

137-173: Simplify FilterOutputs function

The function could be simplified and made more readable.

 func FilterOutputs(outputs map[string]map[string]cty.Value, outputIndex string) map[string]cty.Value {
 	if outputIndex == "" {
-		flattened := make(map[string]cty.Value)
-		for unit, values := range outputs {
-			flattened[unit] = cty.ObjectVal(values)
-		}
-
-		return flattened
+		return flattenOutputs(outputs)
 	}
 
 	keys := strings.Split(outputIndex, ".")
-	currentMap := make(map[string]cty.Value)
+	result := make(map[string]cty.Value)
 
 	for unit, values := range outputs {
 		if !strings.HasPrefix(outputIndex, unit) {
 			continue
 		}
 
 		value := cty.ObjectVal(values)
-		for _, key := range keys[1:] {
-			if value.Type().IsObjectType() {
-				mapVal := value.AsValueMap()
-				if v, exists := mapVal[key]; exists {
-					value = v
-				} else {
-					return nil
-				}
-			} else {
-				return nil
-			}
+		value, ok := traverseValue(value, keys[1:])
+		if !ok {
+			return nil
 		}
 
-		currentMap[outputIndex] = value
+		result[outputIndex] = value
 	}
 
-	return currentMap
+	return result
 }
+
+func flattenOutputs(outputs map[string]map[string]cty.Value) map[string]cty.Value {
+	flattened := make(map[string]cty.Value)
+	for unit, values := range outputs {
+		flattened[unit] = cty.ObjectVal(values)
+	}
+	return flattened
+}
+
+func traverseValue(value cty.Value, keys []string) (cty.Value, bool) {
+	for _, key := range keys {
+		if !value.Type().IsObjectType() {
+			return cty.NilVal, false
+		}
+		mapVal := value.AsValueMap()
+		var exists bool
+		value, exists = mapVal[key]
+		if !exists {
+			return cty.NilVal, false
+		}
+	}
+	return value, true
+}
test/integration_stacks_test.go (2)

246-257: Consider adding more specific error assertions

The test could be more specific about the expected error message.

 func TestStackOutputsRawError(t *testing.T) {
 	t.Parallel()
 
 	helpers.CleanupTerraformFolder(t, testFixtureStacksOutputs)
 	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksOutputs)
 	rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksOutputs)
 
 	helpers.RunTerragrunt(t, "terragrunt stack run apply --experiment stacks --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)
 
-	_, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt stack output --format raw --experiment stacks --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)
-	require.Error(t, err)
+	stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt stack output --format raw --experiment stacks --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)
+	require.Error(t, err)
+	assert.Contains(t, stderr, "output index is required when using raw format")

276-291: Consider reducing test duplication

TestStackOutputsRawFlag and TestStackOutputsJsonFlag share similar setup code. Consider extracting common setup into a helper function.

+func setupStackOutputTest(t *testing.T) string {
+	t.Helper()
+	helpers.CleanupTerraformFolder(t, testFixtureStacksOutputs)
+	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksOutputs)
+	rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksOutputs)
+	helpers.RunTerragrunt(t, "terragrunt stack run apply --experiment stacks --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)
+	return rootPath
+}
+
 func TestStackOutputsRawFlag(t *testing.T) {
 	t.Parallel()
 
-	helpers.CleanupTerraformFolder(t, testFixtureStacksOutputs)
-	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksOutputs)
-	rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksOutputs)
-
-	helpers.RunTerragrunt(t, "terragrunt stack run apply --experiment stacks --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)
+	rootPath := setupStackOutputTest(t)
 
 	stdout, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt stack output -raw filtered_app2.data --experiment stacks --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)
 	require.NoError(t, err)
 
 	assert.Contains(t, stdout, "app2")
 	assert.NotContains(t, stdout, "project2_app1 = {")
 	assert.NotContains(t, stdout, "project2_app2 = {")
 }
 
 func TestStackOutputsJsonFlag(t *testing.T) {
 	t.Parallel()
 
-	helpers.CleanupTerraformFolder(t, testFixtureStacksOutputs)
-	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksOutputs)
-	rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksOutputs)
-
-	helpers.RunTerragrunt(t, "terragrunt stack run apply --experiment stacks --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)
+	rootPath := setupStackOutputTest(t)

Also applies to: 293-310

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 19147ae and 3f9903d.

📒 Files selected for processing (3)
  • cli/commands/stack/action.go (1 hunks)
  • cli/commands/stack/output.go (1 hunks)
  • test/integration_stacks_test.go (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Pull Request has non-contributor approval
  • GitHub Check: Pull Request has non-contributor approval
🔇 Additional comments (1)
test/integration_stacks_test.go (1)

162-183: LGTM! Comprehensive test coverage.

The test cases thoroughly validate the output functionality with different formats and scenarios.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (6)
cli/commands/stack/output.go (5)

22-45: Consider enhancing error messages for better debugging.

The error handling is good, but the messages could be more descriptive to help users troubleshoot issues. Consider wrapping the errors with additional context.

 if err != nil {
-    return nil, errors.New(err)
+    return nil, errors.WithPrefixf(err, "Failed to read stack config file at %s", opts.TerragruntStackConfigPath)
 }

 if err != nil {
-    return nil, errors.New(err)
+    return nil, errors.WithPrefixf(err, "Failed to read outputs for unit %s", unit.Name)
 }

79-85: Add validation for edge cases in value conversion.

Consider adding validation for nil values and better error handling for complex types that might fail conversion.

 func getValueString(value cty.Value) (string, error) {
+    if value.IsNull() {
+        return "", fmt.Errorf("cannot convert nil value to string")
+    }
     if value.Type() == cty.String {
         return value.AsString(), nil
     }
-    return config.CtyValueAsString(value)
+    str, err := config.CtyValueAsString(value)
+    if err != nil {
+        return "", fmt.Errorf("failed to convert %s to string: %w", value.Type().FriendlyName(), err)
+    }
+    return str, nil
 }

87-107: Consider optimizing for large outputs and improving error handling.

The function works well but could be more robust with better error handling and memory optimization.

 func PrintOutputs(writer io.Writer, outputs map[string]map[string]cty.Value, outputIndex string) error {
     filteredOutputs := FilterOutputs(outputs, outputIndex)
     if filteredOutputs == nil {
         return nil
     }
 
     f := hclwrite.NewEmptyFile()
     rootBody := f.Body()
 
+    // Write outputs in chunks to optimize memory usage
+    const chunkSize = 1000
+    chunk := make([]byte, 0, chunkSize)
     for key, value := range filteredOutputs {
         tokens := hclwrite.TokensForValue(value)
+        if tokens == nil {
+            return fmt.Errorf("failed to convert value for key %s to HCL tokens", key)
+        }
         rootBody.SetAttributeRaw(key, tokens)
+        
+        // Write in chunks if the buffer gets too large
+        if len(chunk) >= chunkSize {
+            if _, err := writer.Write(chunk); err != nil {
+                return errors.New(err)
+            }
+            chunk = chunk[:0]
+        }
     }
 
-    if _, err := writer.Write(f.Bytes()); err != nil {
+    // Write any remaining data
+    if _, err := writer.Write(chunk); err != nil {
         return errors.New(err)
     }
     return nil
 }

109-133: Add configurable JSON formatting options.

Consider adding options to control JSON formatting (e.g., indentation) and optimize for large outputs.

-func PrintJSONOutput(writer io.Writer, outputs map[string]map[string]cty.Value, outputIndex string) error {
+func PrintJSONOutput(writer io.Writer, outputs map[string]map[string]cty.Value, outputIndex string, opts ...JSONOutputOption) error {
+    config := defaultJSONOutputConfig()
+    for _, opt := range opts {
+        opt(config)
+    }
+
     filteredOutputs := FilterOutputs(outputs, outputIndex)
     if filteredOutputs == nil {
         return nil
     }
 
     topVal := cty.ObjectVal(filteredOutputs)
     rawJSON, err := ctyjson.Marshal(topVal, topVal.Type())
     if err != nil {
         return errors.New(err)
     }
 
-    var pretty bytes.Buffer
-    if err := json.Indent(&pretty, rawJSON, "", "  "); err != nil {
-        return errors.New(err)
+    if config.PrettyPrint {
+        var pretty bytes.Buffer
+        if err := json.Indent(&pretty, rawJSON, config.Prefix, config.Indent); err != nil {
+            return errors.New(err)
+        }
+        rawJSON = pretty.Bytes()
     }
 
-    if _, err := writer.Write(pretty.Bytes()); err != nil {
+    if _, err := writer.Write(rawJSON); err != nil {
         return errors.New(err)
     }
     return nil
 }

135-171: Enhance output filtering capabilities.

Consider adding support for array indexing and better handling of invalid paths.

 func FilterOutputs(outputs map[string]map[string]cty.Value, outputIndex string) map[string]cty.Value {
     if outputIndex == "" {
         flattened := make(map[string]cty.Value)
         for unit, values := range outputs {
             flattened[unit] = cty.ObjectVal(values)
         }
         return flattened
     }
 
     keys := strings.Split(outputIndex, ".")
+    // Support array indexing with regex
+    arrayIndexRegex := regexp.MustCompile(`^(.+)\[(\d+)\]$`)
     currentMap := make(map[string]cty.Value)
 
     for unit, values := range outputs {
         if !strings.HasPrefix(outputIndex, unit) {
             continue
         }
 
         value := cty.ObjectVal(values)
         for _, key := range keys[1:] {
+            // Handle array indexing
+            if matches := arrayIndexRegex.FindStringSubmatch(key); matches != nil {
+                key = matches[1]
+                idx, _ := strconv.Atoi(matches[2])
+                if value.Type().IsListType() {
+                    if idx >= 0 && idx < value.LengthInt() {
+                        value = value.Index(cty.NumberIntVal(int64(idx)))
+                        continue
+                    }
+                    return nil
+                }
+            }
+
             if value.Type().IsObjectType() {
                 mapVal := value.AsValueMap()
                 if v, exists := mapVal[key]; exists {
                     value = v
                 } else {
+                    // Provide more context about the invalid path
+                    log.Printf("Invalid path: key '%s' not found in object. Available keys: %v", 
+                        key, maps.Keys(mapVal))
                     return nil
                 }
             } else {
+                log.Printf("Invalid path: expected object type but got %s", 
+                    value.Type().FriendlyName())
                 return nil
             }
         }
         currentMap[outputIndex] = value
     }
     return currentMap
 }
docs/_docs/04_reference/02-cli-options.md (1)

823-957: Enhance documentation with more examples and format comparisons.

The documentation is good but could be more helpful with:

  1. Examples of array indexing for complex outputs
  2. A comparison table showing format differences
  3. More real-world use cases

Add these sections to the documentation:

+#### Array Indexing Examples
+
+Accessing elements in output arrays:
+```bash
+# Get the first URL from a list
+$ terragrunt stack output --format=raw app1.urls[0]
+https://example.com
+
+# Get a specific port from a list of services
+$ terragrunt stack output --format=raw services.ports[2]
+8080
+```
+
+#### Format Comparison
+
+| Use Case | Default | JSON | Raw |
+|----------|---------|------|-----|
+| Shell scripts | ❌ | ❌ | ✅ |
+| Parsing | ❌ | ✅ | ❌ |
+| Human readable | ✅ | ✅ | ❌ |
+| Complex types | ✅ | ✅ | ❌ |
+
+#### Real-world Examples
+
+```bash
+# Deploy to the latest endpoint
+$ kubectl apply -f "$(terragrunt stack output --format=raw k8s.manifest_path)"
+
+# Connect to the database
+$ psql "$(terragrunt stack output --format=raw db.connection_string)"
+```
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f9903d and 876cd85.

📒 Files selected for processing (2)
  • cli/commands/stack/output.go (1 hunks)
  • docs/_docs/04_reference/02-cli-options.md (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Pull Request has non-contributor approval
  • GitHub Check: build-and-test
  • GitHub Check: Pull Request has non-contributor approval

Comment on lines +46 to +77
func PrintRawOutputs(opts *options.TerragruntOptions, writer io.Writer, outputs map[string]map[string]cty.Value, outputIndex string) error {
if len(outputIndex) == 0 {
// output index is required in raw mode
return errors.New("output index is required in raw mode")
}

filteredOutputs := FilterOutputs(outputs, outputIndex)

if filteredOutputs == nil {
return nil
}

if len(filteredOutputs) > 1 {
// return error since in raw mode we want to print only one output
return errors.New("multiple outputs found, please specify only one index")
}

for key, value := range filteredOutputs {
valueStr, err := getValueString(value)
if err != nil {
opts.Logger.Warnf("Error fetching output for '%s': %v", key, err)
continue
}

line := valueStr + "\n"
if _, err := writer.Write([]byte(line)); err != nil {
return errors.New(err)
}
}

return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Modify raw output format to align with user feedback.

Based on yhakbar's feedback, the raw output format should only return the value without the key for easier shell script integration. This would enable use cases like:

curl -s "$(terragrunt stack output --format=raw services.service['service-1'].url)"
-        line := valueStr + "\n"
+        line := valueStr
+        if !opts.RawFormat {
+            line = key + " = " + line
+        }
+        line += "\n"
         if _, err := writer.Write([]byte(line)); err != nil {
             return errors.New(err)
         }

@denis256 denis256 requested a review from yhakbar February 7, 2025 21:36
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
docs/_docs/04_reference/02-cli-options.md (1)

875-884: Heading Style for 'Output Formats' Section
The "Output Formats" title is currently emphasized using bold text. For better markdown compliance (e.g., MD036), consider converting it into an actual header (e.g., using "#### Output Formats"). This change would align with common markdown style conventions.

🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

875-875: Emphasis used instead of a heading
null

(MD036, no-emphasis-as-heading)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 876cd85 and 722b8fa.

📒 Files selected for processing (1)
  • docs/_docs/04_reference/02-cli-options.md (1 hunks)
🧰 Additional context used
🪛 markdownlint-cli2 (0.17.2)
docs/_docs/04_reference/02-cli-options.md

875-875: Emphasis used instead of a heading
null

(MD036, no-emphasis-as-heading)

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Pull Request has non-contributor approval
  • GitHub Check: unessential
  • GitHub Check: build-and-test
🔇 Additional comments (7)
docs/_docs/04_reference/02-cli-options.md (7)

823-829: New 'stack output' Section Added
This new section clearly introduces the terragrunt stack output command, explaining its purpose and how it aggregates outputs from multiple units. The language is plain and accessible, making it easy for users to understand the new functionality.


830-838: Clear Example for Basic Usage
The provided basic usage example is straightforward and shows typical output formatting. It’s good to see explicit examples (with key/value pairs) so users know what to expect when not using output modifiers.


840-866: Documentation of Targeted Retrieval
The instructions and examples for retrieving outputs for a specific unit (and even a specific field) are very useful. They guide the user on how to narrow down output data effectively.


868-874: Concise Example for Single Output Retrieval
The example showing retrieval of a specific output value (e.g., project1_app1.custom_value1) is concise and clear. It demonstrates the command’s flexibility for detailed queries.


885-920: Structured JSON Output Example is Well Detailed
The example demonstrating how to use --format json is comprehensive. It clearly shows the JSON structure and includes nested objects and arrays. This detailed example will help users integrate Terragrunt output with other tools easily.


922-942: Accessing Nested JSON Lists Example
The successive example for accessing a specific list within the JSON output is clear. It illustrates how users can extract data for further processing, which is especially useful in automation scenarios.


945-952: Raw Format Example Demonstrates Simplified Output
The final example for retrieving a simple value using --format raw is succinct and effective. It confirms that when the raw format is used, the output returns just the value, making it more script-friendly.

@denis256 denis256 merged commit cdad895 into main Feb 8, 2025
8 of 9 checks passed
@denis256 denis256 deleted the tg-799-stack-output branch February 8, 2025 11:26
This was referenced Feb 11, 2025
@coderabbitai coderabbitai bot mentioned this pull request Mar 6, 2025
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants