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

fix: Handle TF_DATA_DIR and Error Logging for !terraform.output #1037

Closed
wants to merge 8 commits into from
Closed
73 changes: 33 additions & 40 deletions internal/exec/terraform_outputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
Expand All @@ -13,6 +14,7 @@

"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
l "github.com/charmbracelet/log"
cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/schema"
"github.com/cloudposse/atmos/pkg/ui/theme"
Expand Down Expand Up @@ -78,8 +80,7 @@
if atmosConfig.Components.Terraform.AutoGenerateBackendFile {
backendFileName := filepath.Join(componentPath, "backend.tf.json")

u.LogDebug("\nWriting the backend config to file:")
u.LogDebug(backendFileName)
l.Debug("Writing the backend config to file:", "file", backendFileName)

backendTypeSection, ok := sections["backend_type"].(string)
if !ok {
Expand All @@ -101,8 +102,7 @@
return nil, err
}

u.LogDebug("\nWrote the backend config to file:")
u.LogDebug(backendFileName)
l.Debug("Wrote the backend config to file:", "file", backendFileName)
}

// Generate `providers_override.tf.json` file if the `providers` section is configured
Expand All @@ -111,17 +111,15 @@
if ok && len(providersSection) > 0 {
providerOverrideFileName := filepath.Join(componentPath, "providers_override.tf.json")

u.LogDebug("\nWriting the provider overrides to file:")
u.LogDebug(providerOverrideFileName)
l.Debug("Writing the provider overrides to file:", "file", providerOverrideFileName)

providerOverrides := generateComponentProviderOverrides(providersSection)
err = u.WriteToFileAsJSON(providerOverrideFileName, providerOverrides, 0o644)
if err != nil {
return nil, err
}

u.LogDebug("\nWrote the provider overrides to file:")
u.LogDebug(providerOverrideFileName)
l.Debug("Wrote the provider overrides to file:", "file", providerOverrideFileName)
}

// Initialize Terraform/OpenTofu
Expand All @@ -137,7 +135,7 @@
// Before executing `terraform init`, delete the `.terraform/environment` file from the component directory
cleanTerraformWorkspace(*atmosConfig, componentPath)

u.LogDebug(fmt.Sprintf("\nExecuting 'terraform init %s -s %s'", component, stack))
l.Debug(fmt.Sprintf("Executing 'terraform init %s -s %s'", component, stack))

var initOptions []tfexec.InitOption
initOptions = append(initOptions, tfexec.Upgrade(false))
Expand All @@ -149,50 +147,50 @@
if err != nil {
return nil, err
}
u.LogDebug(fmt.Sprintf("\nExecuted 'terraform init %s -s %s'", component, stack))
l.Debug(fmt.Sprintf("Executed 'terraform init %s -s %s'", component, stack))

// Terraform workspace
u.LogDebug(fmt.Sprintf("\nExecuting 'terraform workspace new %s' for component '%s' in stack '%s'", terraformWorkspace, component, stack))
l.Debug(fmt.Sprintf("Executing 'terraform workspace new %s' for component '%s' in stack '%s'", terraformWorkspace, component, stack))
err = tf.WorkspaceNew(ctx, terraformWorkspace)
if err != nil {
u.LogDebug(fmt.Sprintf("\nWorkspace exists. Executing 'terraform workspace select %s' for component '%s' in stack '%s'", terraformWorkspace, component, stack))
l.Debug(fmt.Sprintf("Workspace exists. Executing 'terraform workspace select %s' for component '%s' in stack '%s'", terraformWorkspace, component, stack))
err = tf.WorkspaceSelect(ctx, terraformWorkspace)
if err != nil {
return nil, err
}
u.LogDebug(fmt.Sprintf("\nExecuted 'terraform workspace select %s' for component '%s' in stack '%s'", terraformWorkspace, component, stack))
l.Debug(fmt.Sprintf("Executed 'terraform workspace select %s' for component '%s' in stack '%s'", terraformWorkspace, component, stack))
} else {
u.LogDebug(fmt.Sprintf("\nExecuted 'terraform workspace new %s' for component '%s' in stack '%s'", terraformWorkspace, component, stack))
l.Debug(fmt.Sprintf("Executed 'terraform workspace new %s' for component '%s' in stack '%s'", terraformWorkspace, component, stack))
}

// Terraform output
u.LogDebug(fmt.Sprintf("\nExecuting 'terraform output %s -s %s'", component, stack))
l.Debug(fmt.Sprintf("Executing 'terraform output %s -s %s'", component, stack))
outputMeta, err := tf.Output(ctx)
if err != nil {
return nil, err
}
u.LogDebug(fmt.Sprintf("\nExecuted 'terraform output %s -s %s'", component, stack))
l.Debug(fmt.Sprintf("Executed 'terraform output %s -s %s'", component, stack))

if atmosConfig.Logs.Level == u.LogLevelTrace {
y, err2 := u.ConvertToYAML(outputMeta)
if err2 != nil {
u.LogError(err2)
l.Error("Error converting output to YAML:", "error", err2)
} else {
u.LogDebug(fmt.Sprintf("\nResult of 'terraform output %s -s %s' before processing it:\n%s\n", component, stack, y))
l.Debug(fmt.Sprintf("Result of 'terraform output %s -s %s' before processing it:\n%s\n", component, stack, y))
}
}

outputProcessed = lo.MapEntries(outputMeta, func(k string, v tfexec.OutputMeta) (string, any) {
s := string(v.Value)
u.LogDebug(fmt.Sprintf("Converting the variable '%s' with the value\n%s\nfrom JSON to 'Go' data type\n", k, s))
l.Debug(fmt.Sprintf("Converting the variable '%s' with the value\n%s\nfrom JSON to 'Go' data type\n", k, s))

d, err2 := u.ConvertFromJSON(s)

if err2 != nil {
u.LogError(fmt.Errorf("failed to convert output '%s': %w", k, err2))
l.Error(fmt.Sprintf("failed to convert output '%s'", k), "error", err2)
return k, nil
} else {
u.LogDebug(fmt.Sprintf("Converted the variable '%s' with the value\n%s\nfrom JSON to 'Go' data type\nResult: %v\n", k, s, d))
l.Debug(fmt.Sprintf("Converted the variable '%s' with the value\n%s\nfrom JSON to 'Go' data type\nResult: %v\n", k, s, d))
}

return k, d
Expand All @@ -202,7 +200,7 @@
if componentAbstract {
componentStatus = "abstract"
}
u.LogDebug(fmt.Sprintf("\nNot executing 'terraform output %s -s %s' because the component is %s", component, stack, componentStatus))
l.Debug(fmt.Sprintf("Not executing 'terraform output %s -s %s' because the component is %s", component, stack, componentStatus))
}

return outputProcessed, nil
Expand All @@ -222,7 +220,7 @@
if !skipCache {
cachedOutputs, found := terraformOutputsCache.Load(stackSlug)
if found && cachedOutputs != nil {
u.LogDebug(fmt.Sprintf("Found the result of the Atmos YAML function '!terraform.output %s %s %s' in the cache", component, stack, output))
l.Debug("Found the result of the Atmos YAML function '!terraform.output %s %s %s' in the cache", component, stack, output)
return getTerraformOutputVariable(atmosConfig, component, stack, cachedOutputs.(map[string]any), output)
}
}
Expand All @@ -235,7 +233,7 @@
if !CheckTTYSupport() {
// set tea.WithInput(nil) workaround tea program not run on not TTY mod issue
opts = []tea.ProgramOption{tea.WithoutRenderer(), tea.WithInput(nil)}
u.LogTrace("No TTY detected. Falling back to basic output. This can happen when no terminal is attached or when commands are pipelined.")
l.Trace("No TTY detected. Falling back to basic output. This can happen when no terminal is attached or when commands are pipelined.")

Check failure on line 236 in internal/exec/terraform_outputs.go

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, linux)

undefined: l.Trace

Check failure on line 236 in internal/exec/terraform_outputs.go

View workflow job for this annotation

GitHub Actions / Build (windows-latest, windows)

undefined: l.Trace

Check failure on line 236 in internal/exec/terraform_outputs.go

View workflow job for this annotation

GitHub Actions / Build (macos-latest, macos)

undefined: l.Trace
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 undefined l.Trace method.

The static analysis indicates that l.Trace is undefined.

Replace with the correct logging level:

-l.Trace("No TTY detected. Falling back to basic output. This can happen when no terminal is attached or when commands are pipelined.")
+l.Debug("No TTY detected. Falling back to basic output. This can happen when no terminal is attached or when commands are pipelined.")
📝 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
l.Trace("No TTY detected. Falling back to basic output. This can happen when no terminal is attached or when commands are pipelined.")
l.Debug("No TTY detected. Falling back to basic output. This can happen when no terminal is attached or when commands are pipelined.")
🧰 Tools
🪛 GitHub Check: Build (macos-latest, macos)

[failure] 236-236:
undefined: l.Trace

🪛 GitHub Check: Build (ubuntu-latest, linux)

[failure] 236-236:
undefined: l.Trace

fmt.Println(message)
}

Expand All @@ -249,7 +247,7 @@
if _, err := p.Run(); err != nil {
// If there's any error running the spinner, just print the message
fmt.Println(message)
u.LogError(fmt.Errorf("failed to run spinner: %w", err))
l.Error("Failed to run spinner:", "error", err)
}
close(spinnerDone)
}()
Expand All @@ -259,7 +257,8 @@
p.Quit()
<-spinnerDone
fmt.Printf("\r✗ %s\n", message)
u.LogErrorAndExit(err)
l.Error("Failed to describe the component", "component", component, "stack", stack, "error", err)
os.Exit(1)
Comment on lines +260 to +261
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
l.Error("Failed to describe the component", "component", component, "stack", stack, "error", err)
os.Exit(1)
l.Fatal("Failed to describe the component", "component", component, "stack", stack, "error", err)

}

// Check if the component in the stack is configured with the 'static' remote state backend, in which case get the
Expand All @@ -269,7 +268,8 @@
p.Quit()
<-spinnerDone
fmt.Printf("\r✗ %s\n", message)
u.LogErrorAndExit(err)
l.Error("Failed to get remote state backend static type outputs", "error", err)
os.Exit(1)
Comment on lines +271 to +272
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
l.Error("Failed to get remote state backend static type outputs", "error", err)
os.Exit(1)
l.Fatal("Failed to get remote state backend static type outputs", "error", err)

}

var result any
Expand All @@ -284,7 +284,8 @@
p.Quit()
<-spinnerDone
fmt.Printf("\r✗ %s\n", message)
u.LogErrorAndExit(err)
l.Error("Failed to execute terraform output", "component", component, "stack", stack, "error", err)
os.Exit(1)
Comment on lines +287 to +288
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
l.Error("Failed to execute terraform output", "component", component, "stack", stack, "error", err)
os.Exit(1)
l.Fatal("Failed to execute terraform output", "component", component, "stack", stack, "error", err)

}

// Cache the result
Expand Down Expand Up @@ -314,12 +315,8 @@

res, err := u.EvaluateYqExpression(atmosConfig, outputs, val)
if err != nil {
u.LogErrorAndExit(fmt.Errorf("error evaluating terrform output '%s' for the component '%s' in the stack '%s':\n%v",
output,
component,
stack,
err,
))
l.Error("Error evaluating terraform output", "output", output, "component", component, "stack", stack, "error", err)
u.Exit(1)

Check failure on line 319 in internal/exec/terraform_outputs.go

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, linux)

undefined: u.Exit

Check failure on line 319 in internal/exec/terraform_outputs.go

View workflow job for this annotation

GitHub Actions / Build (windows-latest, windows)

undefined: u.Exit

Check failure on line 319 in internal/exec/terraform_outputs.go

View workflow job for this annotation

GitHub Actions / Build (macos-latest, macos)

undefined: u.Exit
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 undefined u.Exit function.

The static analysis indicates that u.Exit is undefined.

Replace with the standard os.Exit:

-u.Exit(1)
+os.Exit(1)
📝 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
u.Exit(1)
os.Exit(1)
🧰 Tools
🪛 GitHub Check: Build (macos-latest, macos)

[failure] 319-319:
undefined: u.Exit

🪛 GitHub Check: Build (ubuntu-latest, linux)

[failure] 319-319:
undefined: u.Exit

Comment on lines +318 to +319
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
l.Error("Error evaluating terraform output", "output", output, "component", component, "stack", stack, "error", err)
u.Exit(1)
l.Fatal("Error evaluating terraform output", "output", output, "component", component, "stack", stack, "error", err)

}

return res
Expand All @@ -339,12 +336,8 @@

res, err := u.EvaluateYqExpression(atmosConfig, remoteStateSection, val)
if err != nil {
u.LogErrorAndExit(fmt.Errorf("error evaluating the 'static' remote state backend output '%s' for the component '%s' in the stack '%s':\n%v",
output,
component,
stack,
err,
))
l.Error("Error evaluating the 'static' remote state backend output", "output", output, "component", component, "stack", stack, "error", err)
os.Exit(1)
Comment on lines +339 to +340
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
l.Error("Error evaluating the 'static' remote state backend output", "output", output, "component", component, "stack", stack, "error", err)
os.Exit(1)
l.Fatal("Error evaluating the 'static' remote state backend output", "output", output, "component", component, "stack", stack, "error", err)

}

return res
Expand Down
57 changes: 49 additions & 8 deletions internal/exec/terraform_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package exec

import (
"errors"
"fmt"
"os"
"path/filepath"

l "github.com/charmbracelet/log"
"github.com/cloudposse/atmos/pkg/schema"
u "github.com/cloudposse/atmos/pkg/utils"
)
Expand All @@ -25,9 +25,48 @@ func checkTerraformConfig(atmosConfig schema.AtmosConfiguration) error {
// We delete the file to prevent the Terraform prompt asking to select the default or the
// previously used workspace. This happens when different backends are used for the same component.
func cleanTerraformWorkspace(atmosConfig schema.AtmosConfiguration, componentPath string) {
filePath := filepath.Join(componentPath, ".terraform", "environment")
u.LogDebug(fmt.Sprintf("\nDeleting Terraform environment file:\n'%s'", filePath))
_ = os.Remove(filePath)
// Get TF_DATA_DIR, default to .terraform if not set
tfDataDir := os.Getenv("TF_DATA_DIR")
if tfDataDir == "" {
tfDataDir = ".terraform"
}

// Convert relative path to absolute
if !filepath.IsAbs(tfDataDir) {
tfDataDir = filepath.Join(componentPath, tfDataDir)
}

// Ensure the path is cleaned properly
tfDataDir = filepath.Clean(tfDataDir)

// Construct the full file path
filePath := filepath.Join(tfDataDir, "environment")

// Check if the file exists before attempting deletion
if _, err := os.Stat(filePath); err == nil {
l.Debug("Terraform environment file found. Proceeding with deletion.",
"file", filePath,
)
if err := os.Remove(filePath); err != nil {
l.Debug("Failed to delete Terraform environment file.",
"file", filePath,
"error", err,
)
} else {
l.Debug("Successfully deleted Terraform environment file.",
"file", filePath,
)
}
} else if os.IsNotExist(err) {
l.Debug("Terraform environment file not found. No action needed.",
"file", filePath,
)
} else {
l.Debug("Error checking Terraform environment file.",
"file", filePath,
"error", err,
)
}
}

func shouldProcessStacks(info *schema.ConfigAndStacksInfo) (bool, bool) {
Expand All @@ -50,8 +89,9 @@ func generateBackendConfig(atmosConfig *schema.AtmosConfiguration, info *schema.
if atmosConfig.Components.Terraform.AutoGenerateBackendFile {
backendFileName := filepath.Join(workingDir, "backend.tf.json")

u.LogDebug("\nWriting the backend config to file:")
u.LogDebug(backendFileName)
l.Debug("Writing the backend config to file.",
"file", backendFileName,
)

if !info.DryRun {
componentBackendConfig, err := generateComponentBackendConfig(info.ComponentBackendType, info.ComponentBackendSection, info.TerraformWorkspace)
Expand All @@ -74,8 +114,9 @@ func generateProviderOverrides(atmosConfig *schema.AtmosConfiguration, info *sch
if len(info.ComponentProvidersSection) > 0 {
providerOverrideFileName := filepath.Join(workingDir, "providers_override.tf.json")

u.LogDebug("\nWriting the provider overrides to file:")
u.LogDebug(providerOverrideFileName)
l.Debug("Writing the provider overrides to file.",
"file", providerOverrideFileName,
)

if !info.DryRun {
providerOverrides := generateComponentProviderOverrides(info.ComponentProvidersSection)
Expand Down
Loading