Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion pkg/env/terraform_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ import (
// Types
// =============================================================================

// TerraformArgs contains the generated terraform arguments and environment variables
// TerraformArgs contains all the arguments needed for terraform operations
type TerraformArgs struct {
ModulePath string
TFDataDir string
InitArgs []string
PlanArgs []string
ApplyArgs []string
RefreshArgs []string
ImportArgs []string
DestroyArgs []string
PlanDestroyArgs []string
Expand Down Expand Up @@ -160,6 +161,9 @@ func (e *TerraformEnvPrinter) GenerateTerraformArgs(projectPath, modulePath stri

applyArgs := []string{tfPlanPath}

refreshArgs := []string{}
refreshArgs = append(refreshArgs, varFileArgs...)

planDestroyArgs := []string{"-destroy"}
planDestroyArgs = append(planDestroyArgs, varFileArgs...)

Expand All @@ -171,6 +175,7 @@ func (e *TerraformEnvPrinter) GenerateTerraformArgs(projectPath, modulePath stri
terraformVars["TF_CLI_ARGS_init"] = strings.TrimSpace(fmt.Sprintf("-backend=true %s", strings.Join(backendConfigArgsForEnv, " ")))
terraformVars["TF_CLI_ARGS_plan"] = strings.TrimSpace(fmt.Sprintf("-out=\"%s\" %s", tfPlanPath, strings.Join(varFileArgsForEnv, " ")))
terraformVars["TF_CLI_ARGS_apply"] = strings.TrimSpace(fmt.Sprintf("\"%s\"", tfPlanPath))
terraformVars["TF_CLI_ARGS_refresh"] = strings.TrimSpace(strings.Join(varFileArgsForEnv, " "))
terraformVars["TF_CLI_ARGS_import"] = strings.TrimSpace(strings.Join(varFileArgsForEnv, " "))
terraformVars["TF_CLI_ARGS_destroy"] = strings.TrimSpace(strings.Join(varFileArgsForEnv, " "))
terraformVars["TF_VAR_context_path"] = strings.TrimSpace(filepath.ToSlash(configRoot))
Expand All @@ -189,6 +194,7 @@ func (e *TerraformEnvPrinter) GenerateTerraformArgs(projectPath, modulePath stri
InitArgs: initArgs,
PlanArgs: planArgs,
ApplyArgs: applyArgs,
RefreshArgs: refreshArgs,
ImportArgs: varFileArgs,
DestroyArgs: destroyArgs,
PlanDestroyArgs: planDestroyArgs,
Expand Down
3 changes: 3 additions & 0 deletions pkg/env/terraform_env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,9 @@ func TestTerraformEnv_Print(t *testing.T) {
filepath.Join(configRoot, "terraform/project/path.tfvars"),
filepath.Join(configRoot, "terraform/project/path.tfvars.json")),
"TF_CLI_ARGS_apply": fmt.Sprintf(`"%s"`, filepath.Join(configRoot, ".terraform/project/path/terraform.tfplan")),
"TF_CLI_ARGS_refresh": fmt.Sprintf(`-var-file="%s" -var-file="%s"`,
filepath.Join(configRoot, "terraform/project/path.tfvars"),
filepath.Join(configRoot, "terraform/project/path.tfvars.json")),
"TF_CLI_ARGS_import": fmt.Sprintf(`-var-file="%s" -var-file="%s"`,
filepath.Join(configRoot, "terraform/project/path.tfvars"),
filepath.Join(configRoot, "terraform/project/path.tfvars.json")),
Expand Down
29 changes: 20 additions & 9 deletions pkg/stack/windsor_stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ func (s *WindsorStack) Up() error {
return fmt.Errorf("error running terraform init for %s: %w", component.Path, err)
}

// Run terraform refresh to sync state with actual infrastructure
// This is tolerant of failures for non-existent state
refreshArgs := []string{fmt.Sprintf("-chdir=%s", terraformArgs.ModulePath), "refresh"}
refreshArgs = append(refreshArgs, terraformArgs.RefreshArgs...)
_, _ = s.shell.ExecProgress(fmt.Sprintf("🔄 Refreshing Terraform state in %s", component.Path), "terraform", refreshArgs...)

planArgs := []string{fmt.Sprintf("-chdir=%s", terraformArgs.ModulePath), "plan"}
planArgs = append(planArgs, terraformArgs.PlanArgs...)
_, err = s.shell.ExecProgress(fmt.Sprintf("🌎 Planning Terraform changes in %s", component.Path), "terraform", planArgs...)
Expand All @@ -146,10 +152,10 @@ func (s *WindsorStack) Up() error {
return nil
}

// Down destroys the stack of components by running Terraform destroy operations.
// It processes components in reverse order, generating terraform arguments, running
// Terraform plan with destroy flag, and destroy operations, and cleaning up backend override files.
// The method ensures proper directory management and terraform argument setup for each component.
// Down destroys all Terraform components in the stack by executing Terraform destroy operations in reverse dependency order.
// For each component, Down generates Terraform arguments, sets required environment variables, unsets conflicting TF_CLI_ARGS_* variables,
// creates backend override files, runs Terraform refresh, plan (with destroy flag), and destroy commands, and removes backend override files.
// Components with Destroy set to false are skipped. Directory state is restored after execution. Errors are returned on any operation failure.
func (s *WindsorStack) Down() error {
currentDir, err := s.shims.Getwd()
if err != nil {
Expand All @@ -165,6 +171,10 @@ func (s *WindsorStack) Down() error {
for i := len(components) - 1; i >= 0; i-- {
component := components[i]

if component.Destroy != nil && !*component.Destroy {
continue
}

if _, err := s.shims.Stat(component.FullPath); os.IsNotExist(err) {
continue
}
Expand All @@ -174,8 +184,6 @@ func (s *WindsorStack) Down() error {
return fmt.Errorf("error generating terraform args for %s: %w", component.Path, err)
}

// Set terraform environment variables (TF_VAR_* and TF_DATA_DIR)
// First, unset any existing TF_CLI_ARGS_* environment variables to avoid conflicts
tfCliArgsVars := []string{"TF_CLI_ARGS_init", "TF_CLI_ARGS_plan", "TF_CLI_ARGS_apply", "TF_CLI_ARGS_destroy", "TF_CLI_ARGS_import"}
for _, envVar := range tfCliArgsVars {
if err := s.shims.Unsetenv(envVar); err != nil {
Expand All @@ -191,20 +199,23 @@ func (s *WindsorStack) Down() error {
}
}

// Create backend_override.tf file in the component directory
if err := s.terraformEnv.PostEnvHook(component.FullPath); err != nil {
return fmt.Errorf("error creating backend override file for %s: %w", component.Path, err)
}

refreshArgs := []string{fmt.Sprintf("-chdir=%s", terraformArgs.ModulePath), "refresh"}
refreshArgs = append(refreshArgs, terraformArgs.RefreshArgs...)
_, _ = s.shell.ExecProgress(fmt.Sprintf("🔄 Refreshing Terraform state in %s", component.Path), "terraform", refreshArgs...)

planArgs := []string{fmt.Sprintf("-chdir=%s", terraformArgs.ModulePath), "plan"}
planArgs = append(planArgs, terraformArgs.PlanDestroyArgs...)
if _, err := s.shell.ExecProgress(fmt.Sprintf("🗑️ Planning terraform destroy for %s", component.Path), "terraform", planArgs...); err != nil {
if _, err := s.shell.ExecProgress(fmt.Sprintf("🗑️ Planning terraform destroy for %s", component.Path), "terraform", planArgs...); err != nil {
return fmt.Errorf("error running terraform plan destroy for %s: %w", component.Path, err)
}

destroyArgs := []string{fmt.Sprintf("-chdir=%s", terraformArgs.ModulePath), "destroy"}
destroyArgs = append(destroyArgs, terraformArgs.DestroyArgs...)
if _, err := s.shell.ExecProgress(fmt.Sprintf("🗑️ Destroying terraform for %s", component.Path), "terraform", destroyArgs...); err != nil {
if _, err := s.shell.ExecProgress(fmt.Sprintf("🗑️ Destroying terraform for %s", component.Path), "terraform", destroyArgs...); err != nil {
return fmt.Errorf("error running terraform destroy for %s: %w", component.Path, err)
}

Expand Down
58 changes: 58 additions & 0 deletions pkg/stack/windsor_stack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,64 @@ func TestWindsorStack_Down(t *testing.T) {
}
})

t.Run("SkipComponentsWithDestroyFalse", func(t *testing.T) {
stack, mocks := setup(t)

// Set up components with one having destroy: false
destroyFalse := false
mocks.Blueprint.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent {
return []blueprintv1alpha1.TerraformComponent{
{
Source: "source1",
Path: "module/path1",
FullPath: filepath.Join(os.Getenv("WINDSOR_PROJECT_ROOT"), ".windsor", ".tf_modules", "remote", "path1"),
Destroy: &destroyFalse, // This component should be skipped
},
{
Source: "source2",
Path: "module/path2",
FullPath: filepath.Join(os.Getenv("WINDSOR_PROJECT_ROOT"), ".windsor", ".tf_modules", "remote", "path2"),
// Destroy defaults to true, so this should be destroyed
},
}
}

// Track terraform commands executed
var terraformCommands []string
mocks.Shell.ExecProgressFunc = func(message string, command string, args ...string) (string, error) {
if command == "terraform" && len(args) > 1 {
terraformCommands = append(terraformCommands, fmt.Sprintf("%s %s", args[0], args[1]))
}
return "", nil
}

// When Down is called
if err := stack.Down(); err != nil {
t.Errorf("Expected Down to return nil, got %v", err)
}

// Then only the component without destroy: false should be destroyed
// We should see terraform commands for path2 but not path1
foundPath1Commands := false
foundPath2Commands := false

for _, cmd := range terraformCommands {
if strings.Contains(cmd, "path1") {
foundPath1Commands = true
}
if strings.Contains(cmd, "path2") {
foundPath2Commands = true
}
}

if foundPath1Commands {
t.Errorf("Expected no terraform commands for path1 (destroy: false), but found commands")
}
if !foundPath2Commands {
t.Errorf("Expected terraform commands for path2 (destroy: true), but found none")
}
})

t.Run("ErrorRunningTerraformDestroy", func(t *testing.T) {
stack, mocks := setup(t)
mocks.Shell.ExecProgressFunc = func(message string, command string, args ...string) (string, error) {
Expand Down
Loading