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 18 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
67 changes: 58 additions & 9 deletions internal/exec/shell_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,13 +227,28 @@ 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"}

// Prepare additions to the environment for TF_CLI arguments
for _, cmd := range tfCommands {
envVar := fmt.Sprintf("TF_CLI_ARGS_%s", cmd)
existing := os.Getenv(envVar)

// Collect arguments, starting with any existing value
args := []string{}
if existing != "" {
u.LogWarning(atmosConfig, fmt.Sprintf("detected '%s' set in the environment; this may interfere with Atmos's control of Terraform.", envVar))
args = append(args, existing)
}

// Always add the -var-file argument
args = append(args, fmt.Sprintf("-var-file=%s", varFile))

// Join arguments with a space and set the environment variable
newValue := strings.Join(args, " ")
componentEnvList = append(componentEnvList, fmt.Sprintf("%s=\"%s\"", envVar, newValue))
}
milldr marked this conversation as resolved.
Show resolved Hide resolved

// 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 @@ -263,21 +278,24 @@ func execTerraformShellCommand(
}
}

// Merge env vars, ensuring componentEnvList takes precedence
mergedEnv := mergeEnvVars(os.Environ(), componentEnvList)

u.LogDebug(atmosConfig, "\nStarting a new interactive shell where you can execute all native Terraform commands (type 'exit' to go back)")
u.LogDebug(atmosConfig, fmt.Sprintf("Component: %s\n", component))
u.LogDebug(atmosConfig, fmt.Sprintf("Stack: %s\n", stack))
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 {
for _, v := range mergedEnv {
osterman marked this conversation as resolved.
Show resolved Hide resolved
u.LogDebug(atmosConfig, v)
}
milldr marked this conversation as resolved.
Show resolved Hide resolved

// 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 +352,34 @@ func execTerraformShellCommand(
u.LogDebug(atmosConfig, fmt.Sprintf("Exited shell: %s\n", state.String()))
return nil
}

// mergeEnvVars combines two sets of environment variables, with overrideEnv taking precedence.
//
// 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(baseEnv, overrideEnv []string) []string {
envMap := make(map[string]string)

// Parse base environment variables
for _, env := range baseEnv {
if parts := strings.SplitN(env, "=", 2); len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}

// Override with new environment variables
for _, env := range overrideEnv {
if parts := strings.SplitN(env, "=", 2); len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}

// Convert back to slice
merged := make([]string, 0, len(envMap))
for k, v := range envMap {
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