From a88ee32bbad2d175f9cf8b54d1ff2f25f9dc00d0 Mon Sep 17 00:00:00 2001 From: Emma Jacobs Date: Sun, 4 May 2025 21:13:03 -0400 Subject: [PATCH] Child function for GetEnvVars that runs terraform output and adds each output to the list of environment variables, has another child function for formatting the json returned by the terraform output command --- pkg/env/shims.go | 2 + pkg/env/terraform_env.go | 67 +++++++++++++++ pkg/env/terraform_env_test.go | 155 ++++++++++++++++++++++++++++++++++ 3 files changed, 224 insertions(+) diff --git a/pkg/env/shims.go b/pkg/env/shims.go index 62c2e303a..0146368f1 100644 --- a/pkg/env/shims.go +++ b/pkg/env/shims.go @@ -39,6 +39,7 @@ type Shims struct { LookupEnv func(string) (string, bool) Environ func() []string Getenv func(string) string + Command func(name string, arg ...string) *exec.Cmd } // ============================================================================= @@ -66,5 +67,6 @@ func NewShims() *Shims { LookupEnv: os.LookupEnv, Environ: os.Environ, Getenv: os.Getenv, + Command: exec.Command, } } diff --git a/pkg/env/terraform_env.go b/pkg/env/terraform_env.go index bd6b77094..2f7b3447a 100644 --- a/pkg/env/terraform_env.go +++ b/pkg/env/terraform_env.go @@ -6,6 +6,7 @@ package env import ( + "encoding/json" "fmt" "os" "path/filepath" @@ -25,6 +26,16 @@ type TerraformEnvPrinter struct { BaseEnvPrinter } +// TerraformOutput a struct that mimics the terraform output JSON +type TerraformOutput struct { + Value interface{} `json:"value"` + Type interface{} `json:"type"` + Sensitive bool `json:"sensitive"` +} + +// TerraformOutputs a map of output names to TerraformOutput +type TerraformOutputs map[string]TerraformOutput + // ============================================================================= // Constructor // ============================================================================= @@ -127,6 +138,15 @@ func (e *TerraformEnvPrinter) GetEnvVars() (map[string]string, error) { envVars["TF_VAR_os_type"] = "unix" } + outputVars, err := e.getTerraformOutputs(projectPath) + if err != nil { + return nil, fmt.Errorf("error deriving output vars: %w", err) + } + + for k, v := range outputVars { + envVars[k] = v + } + return envVars, nil } @@ -148,6 +168,53 @@ func (e *TerraformEnvPrinter) Print() error { // Private Methods // ============================================================================= +// getTerraformOutputs retrieves Terraform outputs from the specified project path. +// It executes 'terraform output -json' and converts the outputs into environment variables. +// Each output is prefixed with 'TF_VAR_' to make it available as a Terraform variable. +// Returns a map of environment variable names to their formatted values, or an error if the command fails. +func (e *TerraformEnvPrinter) getTerraformOutputs(projectPath string) (map[string]string, error) { + cmd := e.shims.Command("terraform", "-chdir="+projectPath, "output", "-json") + outputJSON, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("failed to run terraform output: %w", err) + } + var outputs TerraformOutputs + if err := json.Unmarshal(outputJSON, &outputs); err != nil { + return nil, fmt.Errorf("failed to parse terraform output: %w", err) + } + + envVars := make(map[string]string) + for name, output := range outputs { + envVars["TF_VAR_"+name] = formatTerraformValue(output.Value) + } + return envVars, nil +} + +// formatTerraformValue converts a Terraform output value to a string representation. +// It handles different value types: +// - Strings are returned as-is +// - Numbers and booleans are converted to strings +// - Lists are joined with commas +// - Complex types are JSON marshaled +// Returns a string representation of the value suitable for environment variables. +func formatTerraformValue(val interface{}) string { + switch v := val.(type) { + case string: + return v + case float64, int, bool: + return fmt.Sprintf("%v", v) + case []interface{}: + parts := make([]string, len(v)) + for i, elem := range v { + parts[i] = fmt.Sprintf("%v", elem) + } + return strings.Join(parts, ",") + default: + b, _ := json.Marshal(v) + return string(b) + } +} + // generateBackendOverrideTf creates the backend_override.tf file for the project by determining // the backend type and writing the appropriate configuration to the file. func (e *TerraformEnvPrinter) generateBackendOverrideTf() error { diff --git a/pkg/env/terraform_env_test.go b/pkg/env/terraform_env_test.go index 58bdd8f2d..556b99776 100644 --- a/pkg/env/terraform_env_test.go +++ b/pkg/env/terraform_env_test.go @@ -3,6 +3,7 @@ package env import ( "fmt" "os" + "os/exec" "path/filepath" "reflect" "sort" @@ -1126,3 +1127,157 @@ func TestTerraformEnv_processBackendConfig(t *testing.T) { } }) } + +func TestTerraformEnv_getTerraformOutputs(t *testing.T) { + setup := func(t *testing.T) (*TerraformEnvPrinter, *Mocks) { + t.Helper() + mocks := setupTerraformEnvMocks(t) + printer := NewTerraformEnvPrinter(mocks.Injector) + printer.shims = mocks.Shims + if err := printer.Initialize(); err != nil { + t.Fatalf("Failed to initialize printer: %v", err) + } + return printer, mocks + } + + t.Run("Success", func(t *testing.T) { + // Given a TerraformEnvPrinter with mock terraform output + printer, mocks := setup(t) + projectPath := "project/path" + + // Mock terraform output command + mocks.Shims.Command = func(name string, arg ...string) *exec.Cmd { + cmd := exec.Command("echo", `{ + "instance_id": {"value": "i-123456", "type": "string"}, + "port": {"value": 8080, "type": "number"}, + "tags": {"value": ["prod", "web"], "type": "list"} + }`) + return cmd + } + + // When getTerraformOutputs is called + outputs, err := printer.getTerraformOutputs(projectPath) + + // Then no error should occur and outputs should be correctly formatted + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + expectedOutputs := map[string]string{ + "TF_VAR_instance_id": "i-123456", + "TF_VAR_port": "8080", + "TF_VAR_tags": "prod,web", + } + + if !reflect.DeepEqual(outputs, expectedOutputs) { + t.Errorf("Expected outputs %v, got %v", expectedOutputs, outputs) + } + }) + + t.Run("CommandError", func(t *testing.T) { + // Given a TerraformEnvPrinter with failing terraform command + printer, mocks := setup(t) + projectPath := "project/path" + + // Mock failing terraform command + mocks.Shims.Command = func(name string, arg ...string) *exec.Cmd { + cmd := exec.Command("false") + return cmd + } + + // When getTerraformOutputs is called + _, err := printer.getTerraformOutputs(projectPath) + + // Then an error should be returned + if err == nil { + t.Errorf("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to run terraform output") { + t.Errorf("Expected error message to contain 'failed to run terraform output', got %v", err) + } + }) + + t.Run("InvalidJSON", func(t *testing.T) { + // Given a TerraformEnvPrinter with invalid JSON output + printer, mocks := setup(t) + projectPath := "project/path" + + // Mock terraform command with invalid JSON + mocks.Shims.Command = func(name string, arg ...string) *exec.Cmd { + cmd := exec.Command("echo", `invalid json`) + return cmd + } + + // When getTerraformOutputs is called + _, err := printer.getTerraformOutputs(projectPath) + + // Then an error should be returned + if err == nil { + t.Errorf("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to parse terraform output") { + t.Errorf("Expected error message to contain 'failed to parse terraform output', got %v", err) + } + }) +} + +func TestTerraformEnv_formatTerraformValue(t *testing.T) { + tests := []struct { + name string + input interface{} + expected string + }{ + { + name: "String value", + input: "test-string", + expected: "test-string", + }, + { + name: "Integer value", + input: 42, + expected: "42", + }, + { + name: "Float value", + input: 3.14, + expected: "3.14", + }, + { + name: "Boolean value", + input: true, + expected: "true", + }, + { + name: "String array", + input: []interface{}{"item1", "item2", "item3"}, + expected: "item1,item2,item3", + }, + { + name: "Mixed array", + input: []interface{}{"item1", 42, true}, + expected: "item1,42,true", + }, + { + name: "Map value", + input: map[string]interface{}{"key": "value"}, + expected: `{"key":"value"}`, + }, + { + name: "Nil value", + input: nil, + expected: "null", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // When formatTerraformValue is called + result := formatTerraformValue(tt.input) + + // Then the result should match the expected output + if result != tt.expected { + t.Errorf("Expected %q, got %q", tt.expected, result) + } + }) + } +}