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 9974e9869..339e066ed 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 // ============================================================================= @@ -129,6 +140,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 } @@ -150,6 +170,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 3239bf51b..6ea68bf63 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" @@ -1291,3 +1292,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) + } + }) + } +}