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

feat: TF_CLI_ARGS_* Handling #898

Merged
merged 19 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 57 additions & 11 deletions internal/exec/shell_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,13 +227,11 @@ func execTerraformShellCommand(
}
}()

// Set the Terraform environment variables to reference the var file
componentEnvList = append(componentEnvList, fmt.Sprintf("TF_CLI_ARGS_plan=-var-file=%s", varFile))
componentEnvList = append(componentEnvList, fmt.Sprintf("TF_CLI_ARGS_apply=-var-file=%s", varFile))
componentEnvList = append(componentEnvList, fmt.Sprintf("TF_CLI_ARGS_refresh=-var-file=%s", varFile))
componentEnvList = append(componentEnvList, fmt.Sprintf("TF_CLI_ARGS_import=-var-file=%s", varFile))
componentEnvList = append(componentEnvList, fmt.Sprintf("TF_CLI_ARGS_destroy=-var-file=%s", varFile))
componentEnvList = append(componentEnvList, fmt.Sprintf("TF_CLI_ARGS_console=-var-file=%s", varFile))
// Define the Terraform commands that may use var-file configuration
tfCommands := []string{"plan", "apply", "refresh", "import", "destroy", "console"}
for _, cmd := range tfCommands {
componentEnvList = append(componentEnvList, fmt.Sprintf("TF_CLI_ARGS_%s=-var-file=%s", cmd, varFile))
}

// Set environment variables to indicate the details of the Atmos shell configuration
componentEnvList = append(componentEnvList, fmt.Sprintf("ATMOS_STACK=%s", stack))
Expand Down Expand Up @@ -269,15 +267,15 @@ func execTerraformShellCommand(
u.LogDebug(atmosConfig, fmt.Sprintf("Working directory: %s\n", workingDir))
u.LogDebug(atmosConfig, fmt.Sprintf("Terraform workspace: %s\n", workspaceName))
u.LogDebug(atmosConfig, "\nSetting the ENV vars in the shell:\n")
for _, v := range componentEnvList {
u.LogDebug(atmosConfig, v)
}

// Merge env vars, ensuring componentEnvList takes precedence
mergedEnv := mergeEnvVars(atmosConfig, componentEnvList)

// Transfer stdin, stdout, and stderr to the new process and also set the target directory for the shell to start in
pa := os.ProcAttr{
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
Dir: componentPath,
Env: append(os.Environ(), componentEnvList...),
Env: mergedEnv,
}

// Start a new shell
Expand Down Expand Up @@ -334,3 +332,51 @@ func execTerraformShellCommand(
u.LogDebug(atmosConfig, fmt.Sprintf("Exited shell: %s\n", state.String()))
return nil
}

// mergeEnvVars adds a list of environment variables to the system environment variables
//
// This is necessary because:
// 1. We need to preserve existing system environment variables (PATH, HOME, etc.)
// 2. Atmos-specific variables (TF_CLI_ARGS, ATMOS_* vars) must take precedence
// 3. For conflicts, such as TF_CLI_ARGS_*, we need special handling to ensure proper merging rather than simple overwriting
milldr marked this conversation as resolved.
Show resolved Hide resolved
func mergeEnvVars(atmosConfig schema.AtmosConfiguration, componentEnvList []string) []string {
envMap := make(map[string]string)

// Parse system environment variables
for _, env := range os.Environ() {
if parts := strings.SplitN(env, "=", 2); len(parts) == 2 {
if strings.HasPrefix(parts[0], "TF_") {
u.LogWarning(atmosConfig, fmt.Sprintf("detected '%s' set in the environment; this may interfere with Atmos's control of Terraform.", parts[0]))
}
envMap[parts[0]] = parts[1]
}
}

// Merge with new, Atmos defined environment variables
for _, env := range componentEnvList {
if parts := strings.SplitN(env, "=", 2); len(parts) == 2 {
// Special handling for Terraform CLI arguments environment variables
if strings.HasPrefix(parts[0], "TF_CLI_ARGS_") {
// For TF_CLI_ARGS_* variables, we need to append new values to any existing values
if existing, exists := envMap[parts[0]]; exists {
// Put the new, Atmos defined value first so it takes precedence
envMap[parts[0]] = parts[1] + " " + existing
} else {
// No existing value, just set the new value
envMap[parts[0]] = parts[1]
}
} else {
// For all other environment variables, simply override any existing value
envMap[parts[0]] = parts[1]
}
}
}

// Convert back to slice
merged := make([]string, 0, len(envMap))
for k, v := range envMap {
u.LogDebug(atmosConfig, fmt.Sprintf("%s=%s", k, v))
merged = append(merged, k+"="+v)
}
return merged
}
8 changes: 8 additions & 0 deletions internal/exec/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,14 @@ func ExecuteTerraform(info schema.ConfigAndStacksInfo) error {
}
}

// Check for any Terraform environment variables that might conflict with Atmos
for _, envVar := range os.Environ() {
if strings.HasPrefix(envVar, "TF_") {
varName := strings.SplitN(envVar, "=", 2)[0]
u.LogWarning(atmosConfig, fmt.Sprintf("detected '%s' set in the environment; this may interfere with Atmos's control of Terraform.", varName))
}
}

// Set `TF_IN_AUTOMATION` ENV var to `true` to suppress verbose instructions after terraform commands
// https://developer.hashicorp.com/terraform/cli/config/environment-variables#tf_in_automation
info.ComponentEnvList = append(info.ComponentEnvList, "TF_IN_AUTOMATION=true")
Expand Down
Loading